diff --git a/.travis.yml b/.travis.yml index 239637ba4..48a836ed1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ android: - '.+' before_script: - mkdir libs - - wget -O libs/libwebrtc-m85.aar https://gultsch.de/files/libwebrtc-m85.aar + - wget -O libs/libwebrtc-m87.aar https://gultsch.de/files/libwebrtc-m87.aar script: - ./gradlew assembleConversationsFreeSystemRelease - ./gradlew assembleQuicksyFreeCompatRelease diff --git a/CHANGELOG.md b/CHANGELOG.md index e58b08305..9b597c4f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +### Version 2.9.4 +* minor stability improvements for A/V calls + +### Version 2.9.3 + +* Fixed connectivity issues when different accounts used different SCRAM mechanisms +* Add support for SCRAM-SHA-512 +* Allow P2P (Jingle) file transfer with self contact + ### Version 2.9.2 * Offer Easy Invite generation on supporting servers diff --git a/build.gradle b/build.gradle index 365173a6b..ff48deaaa 100644 --- a/build.gradle +++ b/build.gradle @@ -80,7 +80,7 @@ dependencies { implementation 'com.squareup.okhttp3:okhttp:3.12.12' implementation 'com.google.guava:guava:27.1-android' quicksyImplementation 'io.michaelrocks:libphonenumber-android:8.11.1' - //implementation fileTree(include: ['libwebrtc-m85.aar'], dir: 'libs') + //implementation fileTree(include: ['libwebrtc-m87.aar'], dir: 'libs') implementation 'org.webrtc:google-webrtc:1.0.32006' } @@ -96,8 +96,8 @@ android { defaultConfig { minSdkVersion 21 targetSdkVersion 29 - versionCode 402 - versionName "2.9.2" + versionCode 404 + versionName "2.9.4" archivesBaseName += "-$versionName" applicationId "eu.sum7.conversations" resValue "string", "applicationId", applicationId diff --git a/doap.rdf b/conversations.doap similarity index 97% rename from doap.rdf rename to conversations.doap index 7dfd8f065..6dccb05f2 100644 --- a/doap.rdf +++ b/conversations.doap @@ -1,7 +1,10 @@ - + Conversations 2014-01-14 @@ -22,13 +25,12 @@ en - + Java Android - diff --git a/metadata/en-US/changelogs/404.txt b/metadata/en-US/changelogs/404.txt new file mode 100644 index 000000000..e903c4cf0 --- /dev/null +++ b/metadata/en-US/changelogs/404.txt @@ -0,0 +1,4 @@ +• Fixed connectivity issues when different accounts used different SCRAM mechanisms +• Add support for SCRAM-SHA-512 +• Allow P2P (Jingle) file transfer with self contact +• minor stability improvements for A/V calls diff --git a/src/conversations/res/values-it/strings.xml b/src/conversations/res/values-it/strings.xml index 7d638efdc..cbc589e59 100644 --- a/src/conversations/res/values-it/strings.xml +++ b/src/conversations/res/values-it/strings.xml @@ -11,4 +11,8 @@ In ogni caso per facilitare puoi creare facilmente un account su chat.sum7.eu, u Sei stato invitato su %1$s. È già stato scelto un nome utente per te. Ti guideremo nel procedimento per creare un account.\nSarai in grado di comunicare con utenti di altri fornitori dando loro l\'indirizzo XMPP completo. Il tuo invito al server Codice di approvvigionamento formattato male - + Tocca il pulsante condividi per inviare al contatto un invito per %1$s. + Se il contatto è vicino, può anche scansionare il codice sottostante per accettare il tuo invito. + Unisciti a %1$s e chatta con me: %2$s + Condividi invito con... + diff --git a/src/conversations/res/values-pl/strings.xml b/src/conversations/res/values-pl/strings.xml index b30048fec..7054df1e8 100644 --- a/src/conversations/res/values-pl/strings.xml +++ b/src/conversations/res/values-pl/strings.xml @@ -9,4 +9,8 @@ Zostałeś zaproszony do %1$s. Nazwa użytkownika została już dla ciebie wybrana. Poprowadzimy ciebie przez proces tworzenia konta.\nBęziesz mógł komunikować się z innymi użytkownikami podając swój adres XMPP. Zaproszenie twojego serwera Niepoprawnie sformatowany kod zaopatrywania - + Użyj przycisku udostępniania aby wysłać swojemu kontaktowi zaproszenie do %1$s. + Jeśli twój kontakt jest blisko może przeskanować kod poniżej aby zaakceptować twoje zaproszenie. + Dołącz do %1$s aby porozmawiać ze mną: %2$s + Udostępnij zaproszenie... + diff --git a/src/conversations/res/values-pt-rBR/strings.xml b/src/conversations/res/values-pt-rBR/strings.xml index a2420fad9..f1d43e18b 100644 --- a/src/conversations/res/values-pt-rBR/strings.xml +++ b/src/conversations/res/values-pt-rBR/strings.xml @@ -9,4 +9,8 @@ Você foi convidado para %1$s. Um nome de usuário já foi escolhido para você. Nós iremos guiá-lo ao longo do processo de criação de uma conta.\nVocê conseguirá se comunicar com usuários de outros provedores dando a eles seu endereço XMPP completo. Seu convite do servidor Código de provisionamento formatado de maneira imprópria - + Toque no botão compartilhar para enviar, para seu contato, um convite para %1$s. + Se seu contato estiver por perto, ele também pode escanear o código abaixo para aceitar seu convite. + Junte-se a %1$s e converse comigo: %2$s + Compartilhe o convite com... + diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java index 4672bc1ff..a9abb2bf8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Anonymous.java @@ -7,22 +7,24 @@ import eu.siacs.conversations.xml.TagWriter; public class Anonymous extends SaslMechanism { - public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); - } + public static final String MECHANISM = "ANONYMOUS"; - @Override - public int getPriority() { - return 0; - } + public Anonymous(TagWriter tagWriter, Account account, SecureRandom rng) { + super(tagWriter, account, rng); + } - @Override - public String getMechanism() { - return "ANONYMOUS"; - } + @Override + public int getPriority() { + return 0; + } - @Override - public String getClientFirstMessage() { - return ""; - } + @Override + public String getMechanism() { + return MECHANISM; + } + + @Override + public String getClientFirstMessage() { + return ""; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java index 3bf5fe342..74d4463d5 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/DigestMd5.java @@ -12,79 +12,82 @@ import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.TagWriter; public class DigestMd5 extends SaslMechanism { - public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } - @Override - public int getPriority() { - return 10; - } + public static final String MECHANISM = "DIGEST-MD5"; - @Override - public String getMechanism() { - return "DIGEST-MD5"; - } + public DigestMd5(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } - private State state = State.INITIAL; + @Override + public int getPriority() { + return 10; + } - @Override - public String getResponse(final String challenge) throws AuthenticationException { - switch (state) { - case INITIAL: - state = State.RESPONSE_SENT; - final String encodedResponse; - try { - final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); - String nonce = ""; - for (final String token : tokenizer) { - final String[] parts = token.split("=", 2); - if (parts[0].equals("nonce")) { - nonce = parts[1].replace("\"", ""); - } else if (parts[0].equals("rspauth")) { - return ""; - } - } - final String digestUri = "xmpp/" + account.getServer(); - final String nonceCount = "00000001"; - final String x = account.getUsername() + ":" + account.getServer() + ":" - + account.getPassword(); - final MessageDigest md = MessageDigest.getInstance("MD5"); - final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); - final String cNonce = CryptoHelper.random(100,rng); - final byte[] a1 = CryptoHelper.concatenateByteArrays(y, - (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); - final String a2 = "AUTHENTICATE:" + digestUri; - final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); - final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset - .defaultCharset()))); - final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce - + ":auth:" + ha2; - final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset - .defaultCharset()))); - final String saslString = "username=\"" + account.getUsername() - + "\",realm=\"" + account.getServer() + "\",nonce=\"" - + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount - + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" - + response + ",charset=utf-8"; - encodedResponse = Base64.encodeToString( - saslString.getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } catch (final NoSuchAlgorithmException e) { - throw new AuthenticationException(e); - } + @Override + public String getMechanism() { + return MECHANISM; + } - return encodedResponse; - case RESPONSE_SENT: - state = State.VALID_SERVER_RESPONSE; - break; - case VALID_SERVER_RESPONSE: - if (challenge==null) { - return null; //everything is fine - } - default: - throw new InvalidStateException(state); - } - return null; - } + private State state = State.INITIAL; + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case INITIAL: + state = State.RESPONSE_SENT; + final String encodedResponse; + try { + final Tokenizer tokenizer = new Tokenizer(Base64.decode(challenge, Base64.DEFAULT)); + String nonce = ""; + for (final String token : tokenizer) { + final String[] parts = token.split("=", 2); + if (parts[0].equals("nonce")) { + nonce = parts[1].replace("\"", ""); + } else if (parts[0].equals("rspauth")) { + return ""; + } + } + final String digestUri = "xmpp/" + account.getServer(); + final String nonceCount = "00000001"; + final String x = account.getUsername() + ":" + account.getServer() + ":" + + account.getPassword(); + final MessageDigest md = MessageDigest.getInstance("MD5"); + final byte[] y = md.digest(x.getBytes(Charset.defaultCharset())); + final String cNonce = CryptoHelper.random(100, rng); + final byte[] a1 = CryptoHelper.concatenateByteArrays(y, + (":" + nonce + ":" + cNonce).getBytes(Charset.defaultCharset())); + final String a2 = "AUTHENTICATE:" + digestUri; + final String ha1 = CryptoHelper.bytesToHex(md.digest(a1)); + final String ha2 = CryptoHelper.bytesToHex(md.digest(a2.getBytes(Charset + .defaultCharset()))); + final String kd = ha1 + ":" + nonce + ":" + nonceCount + ":" + cNonce + + ":auth:" + ha2; + final String response = CryptoHelper.bytesToHex(md.digest(kd.getBytes(Charset + .defaultCharset()))); + final String saslString = "username=\"" + account.getUsername() + + "\",realm=\"" + account.getServer() + "\",nonce=\"" + + nonce + "\",cnonce=\"" + cNonce + "\",nc=" + nonceCount + + ",qop=auth,digest-uri=\"" + digestUri + "\",response=" + + response + ",charset=utf-8"; + encodedResponse = Base64.encodeToString( + saslString.getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } catch (final NoSuchAlgorithmException e) { + throw new AuthenticationException(e); + } + + return encodedResponse; + case RESPONSE_SENT: + state = State.VALID_SERVER_RESPONSE; + break; + case VALID_SERVER_RESPONSE: + if (challenge == null) { + return null; //everything is fine + } + default: + throw new InvalidStateException(state); + } + return null; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java index 294f382d7..6e0ed4390 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/External.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/External.java @@ -1,6 +1,7 @@ package eu.siacs.conversations.crypto.sasl; import android.util.Base64; + import java.security.SecureRandom; import eu.siacs.conversations.entities.Account; @@ -8,22 +9,24 @@ import eu.siacs.conversations.xml.TagWriter; public class External extends SaslMechanism { - public External(TagWriter tagWriter, Account account, SecureRandom rng) { - super(tagWriter, account, rng); - } + public static final String MECHANISM = "EXTERNAL"; - @Override - public int getPriority() { - return 25; - } + public External(TagWriter tagWriter, Account account, SecureRandom rng) { + super(tagWriter, account, rng); + } - @Override - public String getMechanism() { - return "EXTERNAL"; - } + @Override + public int getPriority() { + return 25; + } - @Override - public String getClientFirstMessage() { - return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(),Base64.NO_WRAP); - } + @Override + public String getMechanism() { + return MECHANISM; + } + + @Override + public String getClientFirstMessage() { + return Base64.encodeToString(account.getJid().asBareJid().toEscapedString().getBytes(), Base64.NO_WRAP); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java index 7b774b681..d5cc037e1 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Plain.java @@ -8,27 +8,30 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.TagWriter; public class Plain extends SaslMechanism { - public Plain(final TagWriter tagWriter, final Account account) { - super(tagWriter, account, null); - } - @Override - public int getPriority() { - return 10; - } + public static final String MECHANISM = "PLAIN"; - @Override - public String getMechanism() { - return "PLAIN"; - } + public Plain(final TagWriter tagWriter, final Account account) { + super(tagWriter, account, null); + } - @Override - public String getClientFirstMessage() { - return getMessage(account.getUsername(), account.getPassword()); - } + @Override + public int getPriority() { + return 10; + } - public static String getMessage(String username, String password) { - final String message = '\u0000' + username + '\u0000' + password; - return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); - } + @Override + public String getMechanism() { + return MECHANISM; + } + + @Override + public String getClientFirstMessage() { + return getMessage(account.getUsername(), account.getPassword()); + } + + public static String getMessage(String username, String password) { + final String message = '\u0000' + username + '\u0000' + password; + return Base64.encodeToString(message.getBytes(Charset.defaultCharset()), Base64.NO_WRAP); + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java index 5833309ce..86fd6524e 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/SaslMechanism.java @@ -7,60 +7,63 @@ import eu.siacs.conversations.xml.TagWriter; public abstract class SaslMechanism { - final protected TagWriter tagWriter; - final protected Account account; - final protected SecureRandom rng; + final protected TagWriter tagWriter; + final protected Account account; + final protected SecureRandom rng; - protected enum State { - INITIAL, - AUTH_TEXT_SENT, - RESPONSE_SENT, - VALID_SERVER_RESPONSE, - } + protected enum State { + INITIAL, + AUTH_TEXT_SENT, + RESPONSE_SENT, + VALID_SERVER_RESPONSE, + } - public static class AuthenticationException extends Exception { - public AuthenticationException(final String message) { - super(message); - } + public static class AuthenticationException extends Exception { + public AuthenticationException(final String message) { + super(message); + } - public AuthenticationException(final Exception inner) { - super(inner); - } + public AuthenticationException(final Exception inner) { + super(inner); + } - public AuthenticationException(final String message, final Exception exception) { - super(message,exception); - } - } + public AuthenticationException(final String message, final Exception exception) { + super(message, exception); + } + } - public static class InvalidStateException extends AuthenticationException { - public InvalidStateException(final String message) { - super(message); - } + public static class InvalidStateException extends AuthenticationException { + public InvalidStateException(final String message) { + super(message); + } - public InvalidStateException(final State state) { - this("Invalid state: " + state.toString()); - } - } + public InvalidStateException(final State state) { + this("Invalid state: " + state.toString()); + } + } - public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - this.tagWriter = tagWriter; - this.account = account; - this.rng = rng; - } + public SaslMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + this.tagWriter = tagWriter; + this.account = account; + this.rng = rng; + } - /** - * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another - * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade - * attacks). - * @return An arbitrary int representing the priority - */ - public abstract int getPriority(); + /** + * The priority is used to pin the authentication mechanism. If authentication fails, it MAY be retried with another + * mechanism of the same priority, but MUST NOT be tried with a mechanism of lower priority (to prevent downgrade + * attacks). + * + * @return An arbitrary int representing the priority + */ + public abstract int getPriority(); - public abstract String getMechanism(); - public String getClientFirstMessage() { - return ""; - } - public String getResponse(final String challenge) throws AuthenticationException { - return ""; - } + public abstract String getMechanism(); + + public String getClientFirstMessage() { + return ""; + } + + public String getResponse(final String challenge) throws AuthenticationException { + return ""; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java index 4d40d2b74..807056bf8 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -1,53 +1,74 @@ package eu.siacs.conversations.crypto.sasl; -import android.annotation.TargetApi; -import android.os.Build; import android.util.Base64; -import android.util.LruCache; + +import com.google.common.base.Objects; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.macs.HMac; import org.bouncycastle.crypto.params.KeyParameter; -import java.math.BigInteger; import java.nio.charset.Charset; import java.security.InvalidKeyException; import java.security.SecureRandom; +import java.util.concurrent.ExecutionException; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.TagWriter; -@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) abstract class ScramMechanism extends SaslMechanism { // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. private final static String GS2_HEADER = "n,,"; private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - private static final LruCache CACHE; - static HMac HMAC; - static Digest DIGEST; - static { - CACHE = new LruCache(10) { - protected KeyPair create(final String k) { - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism". - // Changing any of these values forces a cache miss. `CryptoHelper.bytesToHex()' - // is applied to prevent commas in the strings breaking things. - final String[] kParts = k.split(",", 5); - try { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(CryptoHelper.hexToString(kParts[1]).getBytes(), - Base64.decode(CryptoHelper.hexToString(kParts[2]), Base64.DEFAULT), Integer.parseInt(kParts[3])); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + protected abstract HMac getHMAC(); - return new KeyPair(clientKey, serverKey); - } catch (final InvalidKeyException | NumberFormatException e) { - return null; - } - } - }; + protected abstract Digest getDigest(); + + private static final Cache CACHE = CacheBuilder.newBuilder().maximumSize(10).build(); + + private static class CacheKey { + final String algorithm; + final String password; + final String salt; + final int iterations; + + private CacheKey(String algorithm, String password, String salt, int iterations) { + this.algorithm = algorithm; + this.password = password; + this.salt = salt; + this.iterations = iterations; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CacheKey cacheKey = (CacheKey) o; + return iterations == cacheKey.iterations && + Objects.equal(algorithm, cacheKey.algorithm) && + Objects.equal(password, cacheKey.password) && + Objects.equal(salt, cacheKey.salt); + } + + @Override + public int hashCode() { + return Objects.hashCode(algorithm, password, salt, iterations); + } + } + + private KeyPair getKeyPair(final String password, final String salt, final int iterations) throws ExecutionException { + return CACHE.get(new CacheKey(getHMAC().getAlgorithmName(), password, salt, iterations), () -> { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = hi(password.getBytes(), Base64.decode(salt, Base64.DEFAULT), iterations); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + return new KeyPair(clientKey, serverKey); + }); } private final String clientNonce; @@ -63,20 +84,21 @@ abstract class ScramMechanism extends SaslMechanism { clientFirstMessageBare = ""; } - private static synchronized byte[] hmac(final byte[] key, final byte[] input) - throws InvalidKeyException { - HMAC.init(new KeyParameter(key)); - HMAC.update(input, 0, input.length); - final byte[] out = new byte[HMAC.getMacSize()]; - HMAC.doFinal(out, 0); + private byte[] hmac(final byte[] key, final byte[] input) throws InvalidKeyException { + final HMac hMac = getHMAC(); + hMac.init(new KeyParameter(key)); + hMac.update(input, 0, input.length); + final byte[] out = new byte[hMac.getMacSize()]; + hMac.doFinal(out, 0); return out; } - public static synchronized byte[] digest(byte[] bytes) { - DIGEST.reset(); - DIGEST.update(bytes, 0, bytes.length); - final byte[] out = new byte[DIGEST.getDigestSize()]; - DIGEST.doFinal(out, 0); + public byte[] digest(byte[] bytes) { + final Digest digest = getDigest(); + digest.reset(); + digest.update(bytes, 0, bytes.length); + final byte[] out = new byte[digest.getDigestSize()]; + digest.doFinal(out, 0); return out; } @@ -85,7 +107,7 @@ abstract class ScramMechanism extends SaslMechanism { * pseudorandom function (PRF) and with dkLen == output length of * HMAC() == output length of H(). */ - private static synchronized byte[] hi(final byte[] key, final byte[] salt, final int iterations) + private byte[] hi(final byte[] key, final byte[] salt, final int iterations) throws InvalidKeyException { byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); byte[] out = u.clone(); @@ -171,15 +193,10 @@ abstract class ScramMechanism extends SaslMechanism { final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' + clientFinalMessageWithoutProof).getBytes(); - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations,SASL-Mechanism". - final KeyPair keys = CACHE.get( - CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getJid().asBareJid().toEscapedString()).getBytes()) + "," - + CryptoHelper.bytesToHex(CryptoHelper.saslPrep(account.getPassword()).getBytes()) + "," - + CryptoHelper.bytesToHex(salt.getBytes()) + "," - + iterationCount + "," - + getMechanism() - ); - if (keys == null) { + final KeyPair keys; + try { + keys = getKeyPair(CryptoHelper.saslPrep(account.getPassword()), salt, iterationCount); + } catch (ExecutionException e) { throw new AuthenticationException("Invalid keys generated"); } final byte[] clientSignature; diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java index 13593778d..c58dd147c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.sasl; +import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.macs.HMac; @@ -9,22 +10,30 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.TagWriter; public class ScramSha1 extends ScramMechanism { - static { - DIGEST = new SHA1Digest(); - HMAC = new HMac(new SHA1Digest()); - } - public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } + public static final String MECHANISM = "SCRAM-SHA-1"; - @Override - public int getPriority() { - return 20; - } + @Override + protected HMac getHMAC() { + return new HMac(new SHA1Digest()); + } - @Override - public String getMechanism() { - return "SCRAM-SHA-1"; - } + @Override + protected Digest getDigest() { + return new SHA1Digest(); + } + + public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } + + @Override + public int getPriority() { + return 20; + } + + @Override + public String getMechanism() { + return MECHANISM; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java index 1b7a969d9..d5dc42b07 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -1,5 +1,6 @@ package eu.siacs.conversations.crypto.sasl; +import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.macs.HMac; @@ -9,22 +10,30 @@ import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.xml.TagWriter; public class ScramSha256 extends ScramMechanism { - static { - DIGEST = new SHA256Digest(); - HMAC = new HMac(new SHA256Digest()); - } - public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) { - super(tagWriter, account, rng); - } + public static final String MECHANISM = "SCRAM-SHA-256"; - @Override - public int getPriority() { - return 25; - } + @Override + protected HMac getHMAC() { + return new HMac(new SHA256Digest()); + } - @Override - public String getMechanism() { - return "SCRAM-SHA-256"; - } + @Override + protected Digest getDigest() { + return new SHA256Digest(); + } + + public ScramSha256(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } + + @Override + public int getPriority() { + return 25; + } + + @Override + public String getMechanism() { + return MECHANISM; + } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java new file mode 100644 index 000000000..dbd30945c --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha512.java @@ -0,0 +1,39 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.macs.HMac; + +import java.security.SecureRandom; + +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.xml.TagWriter; + +public class ScramSha512 extends ScramMechanism { + + public static final String MECHANISM = "SCRAM-SHA-512"; + + @Override + protected HMac getHMAC() { + return new HMac(new SHA512Digest()); + } + + @Override + protected Digest getDigest() { + return new SHA512Digest(); + } + + public ScramSha512(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + } + + @Override + public int getPriority() { + return 30; + } + + @Override + public String getMechanism() { + return MECHANISM; + } +} diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java index e37e0fa71..f9ba24f09 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/Tokenizer.java @@ -10,69 +10,69 @@ import java.util.NoSuchElementException; * A tokenizer for GS2 header strings */ public final class Tokenizer implements Iterator, Iterable { - private final List parts; - private int index; + private final List parts; + private int index; - public Tokenizer(final byte[] challenge) { - final String challengeString = new String(challenge); - parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); - // Trim parts. - for (int i = 0; i < parts.size(); i++) { - parts.set(i, parts.get(i).trim()); - } - index = 0; - } + public Tokenizer(final byte[] challenge) { + final String challengeString = new String(challenge); + parts = new ArrayList<>(Arrays.asList(challengeString.split(","))); + // Trim parts. + for (int i = 0; i < parts.size(); i++) { + parts.set(i, parts.get(i).trim()); + } + index = 0; + } - /** - * Returns true if there is at least one more element, false otherwise. - * - * @see #next - */ - @Override - public boolean hasNext() { - return parts.size() != index + 1; - } + /** + * Returns true if there is at least one more element, false otherwise. + * + * @see #next + */ + @Override + public boolean hasNext() { + return parts.size() != index + 1; + } - /** - * Returns the next object and advances the iterator. - * - * @return the next object. - * @throws java.util.NoSuchElementException if there are no more elements. - * @see #hasNext - */ - @Override - public String next() { - if (hasNext()) { - return parts.get(index++); - } else { - throw new NoSuchElementException("No such element. Size is: " + parts.size()); - } - } + /** + * Returns the next object and advances the iterator. + * + * @return the next object. + * @throws java.util.NoSuchElementException if there are no more elements. + * @see #hasNext + */ + @Override + public String next() { + if (hasNext()) { + return parts.get(index++); + } else { + throw new NoSuchElementException("No such element. Size is: " + parts.size()); + } + } - /** - * Removes the last object returned by {@code next} from the collection. - * This method can only be called once between each call to {@code next}. - * - * @throws UnsupportedOperationException if removing is not supported by the collection being - * iterated. - * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has - * already been called after the last call to {@code next}. - */ - @Override - public void remove() { - if(index <= 0) { - throw new IllegalStateException("You can't delete an element before first next() method call"); - } - parts.remove(--index); - } + /** + * Removes the last object returned by {@code next} from the collection. + * This method can only be called once between each call to {@code next}. + * + * @throws UnsupportedOperationException if removing is not supported by the collection being + * iterated. + * @throws IllegalStateException if {@code next} has not been called, or {@code remove} has + * already been called after the last call to {@code next}. + */ + @Override + public void remove() { + if (index <= 0) { + throw new IllegalStateException("You can't delete an element before first next() method call"); + } + parts.remove(--index); + } - /** - * Returns an {@link java.util.Iterator} for the elements in this object. - * - * @return An {@code Iterator} instance. - */ - @Override - public Iterator iterator() { - return parts.iterator(); - } + /** + * Returns an {@link java.util.Iterator} for the elements in this object. + * + * @return An {@code Iterator} instance. + */ + @Override + public Iterator iterator() { + return parts.iterator(); + } } diff --git a/src/main/java/eu/siacs/conversations/entities/MucOptions.java b/src/main/java/eu/siacs/conversations/entities/MucOptions.java index 1072eeeb9..67657159a 100644 --- a/src/main/java/eu/siacs/conversations/entities/MucOptions.java +++ b/src/main/java/eu/siacs/conversations/entities/MucOptions.java @@ -158,8 +158,11 @@ public class MucOptions { } public boolean allowInvites() { - final Field field = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites"); - return field != null && "1".equals(field.getValue()); + final Field allowInvitesField = getRoomInfoForm().getFieldByName("muc#roomconfig_allowinvites"); + final boolean allowInvites = allowInvitesField != null && "1".equals(allowInvitesField.getValue()); + final Field allowMemberInvitesField = getRoomInfoForm().getFieldByName("muc#roomconfig_allowmemberinvites"); + final boolean allowMemberInvites = allowMemberInvitesField != null && "1".equals(allowMemberInvitesField.getValue()); + return allowInvites || allowMemberInvites; } public boolean canChangeSubject() { diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 977c9fc27..a072e9d9a 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -77,7 +77,7 @@ public class FileBackend { private static final String FILE_PROVIDER = ".files"; private static final float IGNORE_PADDING = 0.15f; - private XmppConnectionService mXmppConnectionService; + private final XmppConnectionService mXmppConnectionService; public FileBackend(XmppConnectionService service) { this.mXmppConnectionService = service; diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index 89ddec12b..47d4a240b 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -2,6 +2,8 @@ package eu.siacs.conversations.services; import android.util.Log; +import org.jetbrains.annotations.NotNull; + import java.math.BigInteger; import java.util.ArrayList; import java.util.HashSet; @@ -615,6 +617,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { } } + @NotNull @Override public String toString() { StringBuilder builder = new StringBuilder(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index db47cf29a..df558d60e 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -182,7 +182,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private Toast messageLoaderToast; private ConversationsActivity activity; private boolean reInitRequiredOnStart = true; - private OnClickListener clickToMuc = new OnClickListener() { + private final OnClickListener clickToMuc = new OnClickListener() { @Override public void onClick(View v) { @@ -192,14 +192,14 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke startActivity(intent); } }; - private OnClickListener leaveMuc = new OnClickListener() { + private final OnClickListener leaveMuc = new OnClickListener() { @Override public void onClick(View v) { activity.xmppConnectionService.archiveConversation(conversation); } }; - private OnClickListener joinMuc = new OnClickListener() { + private final OnClickListener joinMuc = new OnClickListener() { @Override public void onClick(View v) { @@ -207,7 +207,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } }; - private OnClickListener acceptJoin = new OnClickListener() { + private final OnClickListener acceptJoin = new OnClickListener() { @Override public void onClick(View v) { conversation.setAttribute("accept_non_anonymous", true); @@ -216,7 +216,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } }; - private OnClickListener enterPassword = new OnClickListener() { + private final OnClickListener enterPassword = new OnClickListener() { @Override public void onClick(View v) { @@ -231,7 +231,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke }); } }; - private OnScrollListener mOnScrollListener = new OnScrollListener() { + private final OnScrollListener mOnScrollListener = new OnScrollListener() { @Override public void onScrollStateChanged(AbsListView view, int scrollState) { @@ -310,7 +310,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } }; - private EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { + private final EditMessage.OnCommitContentListener mEditorContentListener = new EditMessage.OnCommitContentListener() { @Override public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts, String[] contentMimeTypes) { // try to get permission to read the image, if applicable @@ -333,7 +333,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } }; private Message selectedMessage; - private OnClickListener mEnableAccountListener = new OnClickListener() { + private final OnClickListener mEnableAccountListener = new OnClickListener() { @Override public void onClick(View v) { final Account account = conversation == null ? null : conversation.getAccount(); @@ -343,7 +343,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } }; - private OnClickListener mUnblockClickListener = new OnClickListener() { + private final OnClickListener mUnblockClickListener = new OnClickListener() { @Override public void onClick(final View v) { v.post(() -> v.setVisibility(View.INVISIBLE)); @@ -354,8 +354,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } }; - private OnClickListener mBlockClickListener = this::showBlockSubmenu; - private OnClickListener mAddBackClickListener = new OnClickListener() { + private final OnClickListener mBlockClickListener = this::showBlockSubmenu; + private final OnClickListener mAddBackClickListener = new OnClickListener() { @Override public void onClick(View v) { @@ -366,8 +366,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } } }; - private View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu; - private OnClickListener mAllowPresenceSubscription = new OnClickListener() { + private final View.OnLongClickListener mLongPressBlockListener = this::showBlockSubmenu; + private final OnClickListener mAllowPresenceSubscription = new OnClickListener() { @Override public void onClick(View v) { final Contact contact = conversation == null ? null : conversation.getContact(); @@ -400,8 +400,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke updateSnackBar(conversation); } }; - private AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false); - private OnEditorActionListener mEditorActionListener = (v, actionId, event) -> { + private final AtomicBoolean mSendingPgpMessage = new AtomicBoolean(false); + private final OnEditorActionListener mEditorActionListener = (v, actionId, event) -> { if (actionId == EditorInfo.IME_ACTION_SEND) { InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); if (imm != null && imm.isFullscreenMode()) { @@ -413,7 +413,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke return false; } }; - private OnClickListener mScrollButtonListener = new OnClickListener() { + private final OnClickListener mScrollButtonListener = new OnClickListener() { @Override public void onClick(View v) { @@ -421,7 +421,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke setSelection(binding.messagesView.getCount() - 1, true); } }; - private OnClickListener mSendButtonListener = new OnClickListener() { + private final OnClickListener mSendButtonListener = new OnClickListener() { @Override public void onClick(View v) { @@ -517,7 +517,7 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke private static Conversation getConversation(Activity activity, @IdRes int res) { final Fragment fragment = activity.getFragmentManager().findFragmentById(res); - if (fragment != null && fragment instanceof ConversationFragment) { + if (fragment instanceof ConversationFragment) { return ((ConversationFragment) fragment).getConversation(); } else { return null; @@ -527,11 +527,11 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke public static ConversationFragment get(Activity activity) { FragmentManager fragmentManager = activity.getFragmentManager(); Fragment fragment = fragmentManager.findFragmentById(R.id.main_fragment); - if (fragment != null && fragment instanceof ConversationFragment) { + if (fragment instanceof ConversationFragment) { return (ConversationFragment) fragment; } else { fragment = fragmentManager.findFragmentById(R.id.secondary_fragment); - return fragment != null && fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; + return fragment instanceof ConversationFragment ? (ConversationFragment) fragment : null; } } @@ -986,7 +986,8 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke menuCall.setVisible(false); menuOngoingCall.setVisible(false); } else { - final Optional ongoingRtpSession = activity.xmppConnectionService.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); + final XmppConnectionService service = activity.xmppConnectionService; + final Optional ongoingRtpSession = service == null ? Optional.absent() : service.getJingleConnectionManager().getOngoingRtpConnection(conversation.getContact()); if (ongoingRtpSession.isPresent()) { menuOngoingCall.setVisible(true); menuCall.setVisible(false); @@ -998,7 +999,6 @@ public class ConversationFragment extends XmppFragment implements EditMessage.Ke } menuContactDetails.setVisible(!this.conversation.withSelf()); menuMucDetails.setVisible(false); - final XmppConnectionService service = activity.xmppConnectionService; menuInviteContact.setVisible(service != null && service.findConferenceServer(conversation.getAccount()) != null); } if (conversation.isMuted()) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java index ae81874e1..3c3c53f63 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationsOverviewFragment.java @@ -48,6 +48,7 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.Toast; import com.google.common.collect.Collections2; @@ -371,7 +372,10 @@ public class ConversationsOverviewFragment extends XmppFragment { private void selectAccountToStartEasyInvite() { final List accounts = EasyOnboardingInvite.getSupportingAccounts(activity.xmppConnectionService); - if (accounts.size() == 1) { + if (accounts.size() == 0) { + //This can technically happen if opening the menu item races with accounts reconnecting or something + Toast.makeText(getActivity(),R.string.no_active_accounts_support_this, Toast.LENGTH_LONG).show(); + } else if (accounts.size() == 1) { openEasyInviteScreen(accounts.get(0)); } else { final AtomicReference selectedAccount = new AtomicReference<>(accounts.get(0)); diff --git a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java index d73249f4b..1b2430f6d 100644 --- a/src/main/java/eu/siacs/conversations/ui/SearchActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/SearchActivity.java @@ -39,6 +39,7 @@ import android.text.TextWatcher; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; +import android.view.MotionEvent; import android.view.View; import android.widget.AdapterView; import android.widget.EditText; @@ -130,7 +131,8 @@ public class SearchActivity extends XmppActivity implements TextWatcher, OnSearc } @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + public void onCreateContextMenu(final ContextMenu menu, final View v, ContextMenu.ContextMenuInfo menuInfo) { + v.dispatchTouchEvent(MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)); AdapterView.AdapterContextMenuInfo acmi = (AdapterView.AdapterContextMenuInfo) menuInfo; final Message message = this.messages.get(acmi.position); this.selectedMessageReference = new WeakReference<>(message); diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java index fcdccb9c4..e143cee3d 100644 --- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java @@ -368,12 +368,13 @@ public abstract class XmppActivity extends ActionBarActivity { public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) { final Contact contact = conversation.getContact(); - if (!contact.showInRoster()) { - showAddToRosterDialog(conversation.getContact()); - } else { + if (contact.showInRoster() || contact.isSelf()) { final Presences presences = contact.getPresences(); if (presences.size() == 0) { - if (!contact.getOption(Contact.Options.TO) + if (contact.isSelf()) { + conversation.setNextCounterpart(null); + listener.onPresenceSelected(); + } else if (!contact.getOption(Contact.Options.TO) && !contact.getOption(Contact.Options.ASKING) && contact.getAccount().getStatus() == Account.State.ONLINE) { showAskForPresenceDialog(contact); @@ -391,6 +392,8 @@ public abstract class XmppActivity extends ActionBarActivity { } else { PresenceSelector.showPresenceSelectionDialog(this, conversation, listener); } + } else { + showAddToRosterDialog(conversation.getContact()); } } diff --git a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java index 00f42bcea..13b33570b 100644 --- a/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/SSLSocketHelper.java @@ -29,7 +29,7 @@ public class SSLSocketHelper { final Collection supportedProtocols = new LinkedList<>( Arrays.asList(sslSocket.getSupportedProtocols())); supportedProtocols.remove("SSLv3"); - supportProtocols = supportedProtocols.toArray(new String[supportedProtocols.size()]); + supportProtocols = supportedProtocols.toArray(new String[0]); sslSocket.setEnabledProtocols(supportProtocols); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 1e4060665..70374522a 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -64,6 +64,7 @@ import eu.siacs.conversations.crypto.sasl.Plain; import eu.siacs.conversations.crypto.sasl.SaslMechanism; import eu.siacs.conversations.crypto.sasl.ScramSha1; import eu.siacs.conversations.crypto.sasl.ScramSha256; +import eu.siacs.conversations.crypto.sasl.ScramSha512; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; @@ -106,32 +107,29 @@ public class XmppConnection implements Runnable { private static final int PACKET_IQ = 0; private static final int PACKET_MESSAGE = 1; private static final int PACKET_PRESENCE = 2; - public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() { - @Override - public void onIqPacketReceived(Account account, IqPacket packet) { - if (packet.getType() == IqPacket.TYPE.RESULT) { - account.setOption(Account.OPTION_REGISTER, false); - Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server"); - throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); - } else { - final List PASSWORD_TOO_WEAK_MSGS = Arrays.asList( - "The password is too weak", - "Please use a longer password."); - Element error = packet.findChild("error"); - Account.State state = Account.State.REGISTRATION_FAILED; - if (error != null) { - if (error.hasChild("conflict")) { - state = Account.State.REGISTRATION_CONFLICT; - } else if (error.hasChild("resource-constraint") - && "wait".equals(error.getAttribute("type"))) { - state = Account.State.REGISTRATION_PLEASE_WAIT; - } else if (error.hasChild("not-acceptable") - && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) { - state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; - } + public final OnIqPacketReceived registrationResponseListener = (account, packet) -> { + if (packet.getType() == IqPacket.TYPE.RESULT) { + account.setOption(Account.OPTION_REGISTER, false); + Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": successfully registered new account on server"); + throw new StateChangingError(Account.State.REGISTRATION_SUCCESSFUL); + } else { + final List PASSWORD_TOO_WEAK_MSGS = Arrays.asList( + "The password is too weak", + "Please use a longer password."); + Element error = packet.findChild("error"); + Account.State state = Account.State.REGISTRATION_FAILED; + if (error != null) { + if (error.hasChild("conflict")) { + state = Account.State.REGISTRATION_CONFLICT; + } else if (error.hasChild("resource-constraint") + && "wait".equals(error.getAttribute("type"))) { + state = Account.State.REGISTRATION_PLEASE_WAIT; + } else if (error.hasChild("not-acceptable") + && PASSWORD_TOO_WEAK_MSGS.contains(error.findChildContent("text"))) { + state = Account.State.REGISTRATION_PASSWORD_TOO_WEAK; } - throw new StateChangingError(state); } + throw new StateChangingError(state); } }; protected final Account account; @@ -159,10 +157,10 @@ public class XmppConnection implements Runnable { private long lastSessionStarted = 0; private long lastDiscoStarted = 0; private boolean isMamPreferenceAlways = false; - private AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0); - private AtomicBoolean mWaitForDisco = new AtomicBoolean(true); - private AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false); - private AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); + private final AtomicInteger mPendingServiceDiscoveries = new AtomicInteger(0); + private final AtomicBoolean mWaitForDisco = new AtomicBoolean(true); + private final AtomicBoolean mWaitingForSmCatchup = new AtomicBoolean(false); + private final AtomicInteger mSmCatchupMessageCounter = new AtomicInteger(0); private boolean mInteractive = false; private int attempt = 0; private OnPresencePacketReceived presenceListener = null; @@ -744,7 +742,7 @@ public class XmppConnection implements Runnable { } } - private void processMessage(final Tag currentTag) throws XmlPullParserException, IOException { + private void processMessage(final Tag currentTag) throws IOException { final MessagePacket packet = (MessagePacket) processPacket(currentTag, PACKET_MESSAGE); if (!packet.valid()) { Log.e(Config.LOGTAG, "encountered invalid message from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); @@ -753,7 +751,7 @@ public class XmppConnection implements Runnable { this.messageListener.onMessagePacketReceived(account, packet); } - private void processPresence(final Tag currentTag) throws XmlPullParserException, IOException { + private void processPresence(final Tag currentTag) throws IOException { PresencePacket packet = (PresencePacket) processPacket(currentTag, PACKET_PRESENCE); if (!packet.valid()) { Log.e(Config.LOGTAG, "encountered invalid presence from='" + packet.getFrom() + "' to='" + packet.getTo() + "'"); @@ -807,7 +805,7 @@ public class XmppConnection implements Runnable { return sslSocket; } - private void processStreamFeatures(final Tag currentTag) throws XmlPullParserException, IOException { + private void processStreamFeatures(final Tag currentTag) throws IOException { this.streamFeatures = tagReader.readElement(currentTag); final boolean isSecure = features.encryptionEnabled || Config.ALLOW_NON_TLS_CONNECTIONS || account.isOnion(); final boolean needsBinding = !isBound && !account.isOptionSet(Account.OPTION_REGISTER); @@ -843,20 +841,21 @@ public class XmppConnection implements Runnable { } private void authenticate() throws IOException { - final List mechanisms = extractMechanisms(streamFeatures - .findChild("mechanisms")); + final List mechanisms = extractMechanisms(streamFeatures.findChild("mechanisms")); final Element auth = new Element("auth", Namespace.SASL); - if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { + if (mechanisms.contains(External.MECHANISM) && account.getPrivateKeyAlias() != null) { saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("SCRAM-SHA-256")) { + } else if (mechanisms.contains(ScramSha512.MECHANISM)) { + saslMechanism = new ScramSha512(tagWriter, account, mXmppConnectionService.getRNG()); + } else if (mechanisms.contains(ScramSha256.MECHANISM)) { saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("SCRAM-SHA-1")) { + } else if (mechanisms.contains(ScramSha1.MECHANISM)) { saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("PLAIN") && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { + } else if (mechanisms.contains(Plain.MECHANISM) && !account.getJid().getDomain().toEscapedString().equals("nimbuzz.com")) { saslMechanism = new Plain(tagWriter, account); - } else if (mechanisms.contains("DIGEST-MD5")) { + } else if (mechanisms.contains(DigestMd5.MECHANISM)) { saslMechanism = new DigestMd5(tagWriter, account, mXmppConnectionService.getRNG()); - } else if (mechanisms.contains("ANONYMOUS")) { + } else if (mechanisms.contains(Anonymous.MECHANISM)) { saslMechanism = new Anonymous(tagWriter, account, mXmppConnectionService.getRNG()); } if (saslMechanism != null) { @@ -1238,27 +1237,27 @@ public class XmppConnection implements Runnable { request.setTo(account.getDomain()); request.addChild("query", Namespace.DISCO_ITEMS).setAttribute("node", Namespace.COMMANDS); sendIqPacket(request, (account, response) -> { - if (response.getType() == IqPacket.TYPE.RESULT) { - final Element query = response.findChild("query",Namespace.DISCO_ITEMS); - if (query == null) { - return; - } - final HashMap commands = new HashMap<>(); - for(final Element child : query.getChildren()) { - if ("item".equals(child.getName())) { - final String node = child.getAttribute("node"); - final Jid jid = child.getAttributeAsJid("jid"); - if (node != null && jid != null) { - commands.put(node, jid); - } - } - } - Log.d(Config.LOGTAG,commands.toString()); - synchronized (this.commands) { - this.commands.clear(); - this.commands.putAll(commands); - } - } + if (response.getType() == IqPacket.TYPE.RESULT) { + final Element query = response.findChild("query", Namespace.DISCO_ITEMS); + if (query == null) { + return; + } + final HashMap commands = new HashMap<>(); + for (final Element child : query.getChildren()) { + if ("item".equals(child.getName())) { + final String node = child.getAttribute("node"); + final Jid jid = child.getAttributeAsJid("jid"); + if (node != null && jid != null) { + commands.put(node, jid); + } + } + } + Log.d(Config.LOGTAG, commands.toString()); + synchronized (this.commands) { + this.commands.clear(); + this.commands.putAll(commands); + } + } }); } @@ -1297,7 +1296,7 @@ public class XmppConnection implements Runnable { iq.query("http://jabber.org/protocol/disco#items"); this.sendIqPacket(iq, (account, packet) -> { if (packet.getType() == IqPacket.TYPE.RESULT) { - HashSet items = new HashSet(); + final HashSet items = new HashSet<>(); final List elements = packet.query().getChildren(); for (final Element element : elements) { if (element.getName().equals("item")) { @@ -1325,23 +1324,19 @@ public class XmppConnection implements Runnable { private void sendEnableCarbons() { final IqPacket iq = new IqPacket(IqPacket.TYPE.SET); iq.addChild("enable", "urn:xmpp:carbons:2"); - this.sendIqPacket(iq, new OnIqPacketReceived() { - - @Override - public void onIqPacketReceived(final Account account, final IqPacket packet) { - if (!packet.hasChild("error")) { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": successfully enabled carbons"); - features.carbonsEnabled = true; - } else { - Log.d(Config.LOGTAG, account.getJid().asBareJid() - + ": error enableing carbons " + packet.toString()); - } + this.sendIqPacket(iq, (account, packet) -> { + if (!packet.hasChild("error")) { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + + ": successfully enabled carbons"); + features.carbonsEnabled = true; + } else { + Log.d(Config.LOGTAG, account.getJid().asBareJid() + + ": error enableing carbons " + packet.toString()); } }); } - private void processStreamError(final Tag currentTag) throws XmlPullParserException, IOException { + private void processStreamError(final Tag currentTag) throws IOException { final Element streamError = tagReader.readElement(currentTag); if (streamError == null) { return; @@ -1594,8 +1589,8 @@ public class XmppConnection implements Runnable { } public List getMucServersWithholdAccount() { - List servers = getMucServers(); - servers.remove(account.getDomain()); + final List servers = getMucServers(); + servers.remove(account.getDomain().toEscapedString()); return servers; } @@ -1766,7 +1761,7 @@ public class XmppConnection implements Runnable { } } - private class StateChangingError extends Error { + private static class StateChangingError extends Error { private final Account.State state; public StateChangingError(Account.State state) { @@ -1774,7 +1769,7 @@ public class XmppConnection implements Runnable { } } - private class StateChangingException extends IOException { + private static class StateChangingException extends IOException { private final Account.State state; public StateChangingException(Account.State state) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java index af7213220..52762407f 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/SessionDescription.java @@ -143,14 +143,16 @@ public class SessionDescription { final ArrayListMultimap mediaAttributes = ArrayListMultimap.create(); final String ufrag = transport.getAttribute("ufrag"); final String pwd = transport.getAttribute("pwd"); - if (!Strings.isNullOrEmpty(ufrag)) { - mediaAttributes.put("ice-ufrag", ufrag); + if (Strings.isNullOrEmpty(ufrag)) { + throw new IllegalArgumentException("Transport element is missing required ufrag attribute"); } checkNoWhitespace(ufrag, "ufrag value must not contain any whitespaces"); - if (!Strings.isNullOrEmpty(pwd)) { - mediaAttributes.put("ice-pwd", pwd); + mediaAttributes.put("ice-ufrag", ufrag); + if (Strings.isNullOrEmpty(pwd)) { + throw new IllegalArgumentException("Transport element is missing required pwd attribute"); } checkNoWhitespace(pwd, "pwd value must not contain any whitespaces"); + mediaAttributes.put("ice-pwd", pwd); mediaAttributes.put("ice-options", HARDCODED_ICE_OPTIONS); final IceUdpTransportInfo.Fingerprint fingerprint = transport.getFingerprint(); if (fingerprint != null) { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java index 99cf4d743..e43556d17 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/stanzas/IceUdpTransportInfo.java @@ -14,6 +14,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.UUID; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xml.Namespace; @@ -93,6 +94,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { if (pair.length == 2 && "candidate".equals(pair[0])) { final String[] segments = pair[1].split(" "); if (segments.length >= 6) { + final String id = UUID.randomUUID().toString(); final String foundation = segments[0]; final String component = segments[1]; final String transport = segments[2].toLowerCase(Locale.ROOT); @@ -109,6 +111,7 @@ public class IceUdpTransportInfo extends GenericTransportInfo { candidate.setAttribute("generation", additional.get("generation")); candidate.setAttribute("rel-addr", additional.get("raddr")); candidate.setAttribute("rel-port", additional.get("rport")); + candidate.setAttribute("id", id); candidate.setAttribute("ip", connectionAddress); candidate.setAttribute("port", port); candidate.setAttribute("priority", priority); diff --git a/src/main/res/values-cs/strings.xml b/src/main/res/values-cs/strings.xml index 4b0ce3f36..dd2a09c53 100644 --- a/src/main/res/values-cs/strings.xml +++ b/src/main/res/values-cs/strings.xml @@ -4,7 +4,9 @@ Nová konverzace Nastavení účtů Nastavení účtu + Zavřít konverzaci Detaily kontaktu + Detaily skupinového chatu Přidat účet Upravit jméno Přidat do adresáře @@ -17,6 +19,7 @@ Nastavení Sdílet s konverzací Začít konverzaci + Vybrat kontakt Seznam blokovaných právě teď před minutou @@ -30,14 +33,17 @@ Moderátor Účastník Návštěvník + Přejete si odstranit %s ze seznamu kontaktů? Předešlé rozhovory nebudou odstraněny. Chcete zablokovat příjem zpráv od %s? Chcete odblokovat příjem zpráv od %s? Zablokovat všechny kontakty z %s? Odblokovat všechny kontakty z %s? Kontakty zablokovány + Přejete si odstranit %s ze záložek? Předešlé rozhovory pod záložkou nebudou odstraněny. Registrovat nový účet na serveru Změnit heslo na serveru Sdílet s... + Začít konverzaci Pozvat kontakt Kontakty Zrušit @@ -144,9 +150,14 @@ nedostupný Chybí oznámení o veřejném klíči právě spatřen + naposledy spatřen před minutou naposledy spatřen před %d minutami + naposledy spatřen před hodinou naposledy spatřen před %d hodinami + naposledy spatřen včera naposledy spatřen před %d dny + Šifrovaná zpráva. Nainstalujte OpenKeychain pro její dešifrování. + Nalezeny nové OpenPGP šifrované zprávy OpenPGP ID klíče OMEMO otisk v\\OMEMO otisk @@ -160,6 +171,7 @@ Záložky Hledat Vložit kontakt + Smazat kontakt Zobrazit detaily kontaktu Zablokovat kontakt Odblokovat kontakt diff --git a/src/main/res/values-de/strings.xml b/src/main/res/values-de/strings.xml index 384724f8c..ba44d4438 100644 --- a/src/main/res/values-de/strings.xml +++ b/src/main/res/values-de/strings.xml @@ -951,4 +951,5 @@ Einladung zu Conversations Einladung kann nicht gelesen werden Server unterstützt keine Generierung von Einladungen + Keine aktiven Konten unterstützen diese Funktion diff --git a/src/main/res/values-gl/strings.xml b/src/main/res/values-gl/strings.xml index 1fc99e5c1..dc20afeb2 100644 --- a/src/main/res/values-gl/strings.xml +++ b/src/main/res/values-gl/strings.xml @@ -951,4 +951,5 @@ Convida a Conversations Non se puido enviar o convite O servidor non soporta a creación de convites + Ningunha conta activa soporta esta función diff --git a/src/main/res/values-it/strings.xml b/src/main/res/values-it/strings.xml index 600cc0d19..00aa0dccb 100644 --- a/src/main/res/values-it/strings.xml +++ b/src/main/res/values-it/strings.xml @@ -948,4 +948,7 @@ Recapiti falliti Altre opzioni Nessuna applicazione trovata + Invita su Conversations + Impossibile analizzare l\'invito + Il server non supporta la generazione di inviti diff --git a/src/main/res/values-pl/strings.xml b/src/main/res/values-pl/strings.xml index 1e23928de..9275587aa 100644 --- a/src/main/res/values-pl/strings.xml +++ b/src/main/res/values-pl/strings.xml @@ -970,4 +970,8 @@ Administrator twojego serwera będzie mógł czytać twoje wiadomości, ale moż Nie dostarczone wiadomości Więcej ustawień Nie znaleziono żadnej aplikacji + Zaproś do Conversations + Nie można przetworzyć zaproszenia + Serwer nie wspiera tworzenia zaproszeń + Nie ma aktywnych kont wspierających tę funkcję diff --git a/src/main/res/values-pt-rBR/strings.xml b/src/main/res/values-pt-rBR/strings.xml index ce7e3d873..9c2e272ea 100644 --- a/src/main/res/values-pt-rBR/strings.xml +++ b/src/main/res/values-pt-rBR/strings.xml @@ -947,4 +947,9 @@ Entregas não efetuadas Mais opções + Não foi encontrado nenhum aplicativo + Convidar para o Conversations + Não foi possível processar o convite + O servidor não suporta a criação de convites + Nenhuma conta ativa suporta esse recurso diff --git a/src/main/res/values-ro-rRO/strings.xml b/src/main/res/values-ro-rRO/strings.xml index 1022035f1..322e9ecd6 100644 --- a/src/main/res/values-ro-rRO/strings.xml +++ b/src/main/res/values-ro-rRO/strings.xml @@ -961,4 +961,5 @@ Invitați la Conversations Nu s-a putut procesa invitația Serverul nu suportă generarea de invitații + Nici un cont activ nu suporta această caracteristică diff --git a/src/main/res/values-tr-rTR/strings.xml b/src/main/res/values-tr-rTR/strings.xml index 83eb48990..1868d2dc5 100644 --- a/src/main/res/values-tr-rTR/strings.xml +++ b/src/main/res/values-tr-rTR/strings.xml @@ -974,4 +974,5 @@ Conversations\'a davet et Davet iletilemedi Sunucu, davet oluşturulmasını desteklemiyor + Bu özelliği destekleyen aktif bir hesap yok diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 9f79f86a5..226d4dcc4 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -953,4 +953,5 @@ Invite to Conversations Unable to parse invite Server does not support generating invites + No active accounts support this feature