diff options
Diffstat (limited to 'src/main/java')
26 files changed, 541 insertions, 374 deletions
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java index 1a992e48..48d2620a 100644 --- a/src/main/java/eu/siacs/conversations/Config.java +++ b/src/main/java/eu/siacs/conversations/Config.java @@ -109,7 +109,7 @@ public final class Config { public static final int MAM_MAX_MESSAGES = 500; public static final long FREQUENT_RESTARTS_DETECTION_WINDOW = 12 * 60 * 60 * 1000; // 10 hours - public static final long FREQUENT_RESTARTS_THRESHOLD = 16; + public static final long FREQUENT_RESTARTS_THRESHOLD = 0; // previous value was 16; public static final ChatState DEFAULT_CHATSTATE = ChatState.ACTIVE; public static final int TYPING_TIMEOUT = 8; diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java index 65ac7b0a..6bde0fe7 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/AxolotlService.java @@ -439,8 +439,10 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { mXmppConnectionService.sendIqPacket(account, publish, null); } - public void purgeKey(final String fingerprint) { - axolotlStore.setFingerprintStatus(fingerprint.replaceAll("\\s", ""), FingerprintStatus.createCompromised()); + public void distrustFingerprint(final String fingerprint) { + final String fp = fingerprint.replaceAll("\\s", ""); + final FingerprintStatus fingerprintStatus = axolotlStore.getFingerprintStatus(fp); + axolotlStore.setFingerprintStatus(fp,fingerprintStatus.toUntrusted()); } public void publishOwnDeviceIdIfNeeded() { @@ -1120,7 +1122,7 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { session.resetPreKeyId(); } } catch (CryptoFailedException e) { - Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message: " + e.getMessage()); + Log.w(Config.LOGTAG, getLogprefix(account) + "Failed to decrypt message from "+message.getFrom()+": " + e.getMessage()); } if (session.isFresh() && plaintextMessage != null) { @@ -1134,7 +1136,12 @@ public class AxolotlService implements OnAdvancedStreamFeaturesLoaded { XmppAxolotlMessage.XmppAxolotlKeyTransportMessage keyTransportMessage; XmppAxolotlSession session = getReceivingSession(message); - keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + try { + keyTransportMessage = message.getParameters(session, getOwnDeviceId()); + } catch (CryptoFailedException e) { + Log.d(Config.LOGTAG,"could not decrypt keyTransport message "+e.getMessage()); + keyTransportMessage = null; + } if (session.isFresh() && keyTransportMessage != null) { putFreshSession(session); diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java index 5796ef30..e549598c 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/CryptoFailedException.java @@ -1,6 +1,11 @@ package eu.siacs.conversations.crypto.axolotl; public class CryptoFailedException extends Exception { + + public CryptoFailedException(String msg) { + super(msg); + } + public CryptoFailedException(Exception e){ super(e); } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java index 31b2264b..56f4a5d2 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/FingerprintStatus.java @@ -126,17 +126,17 @@ public class FingerprintStatus implements Comparable<FingerprintStatus> { return trust; } - public static FingerprintStatus createCompromised() { + public FingerprintStatus toVerified() { FingerprintStatus status = new FingerprintStatus(); - status.active = false; - status.trust = Trust.COMPROMISED; + status.active = active; + status.trust = Trust.VERIFIED; return status; } - public FingerprintStatus toVerified() { + public FingerprintStatus toUntrusted() { FingerprintStatus status = new FingerprintStatus(); status.active = active; - status.trust = Trust.VERIFIED; + status.trust = Trust.UNTRUSTED; return status; } diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java index 1f532370..cac298e0 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlMessage.java @@ -23,7 +23,6 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import eu.siacs.conversations.Config; -import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.Element; import eu.siacs.conversations.xmpp.jid.Jid; @@ -251,16 +250,16 @@ public class XmppAxolotlMessage { return encryptionElement; } - private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) { + private byte[] unpackKey(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { XmppAxolotlSession.AxolotlKey encryptedKey = keys.get(sourceDeviceId); - return (encryptedKey != null) ? session.processReceiving(encryptedKey) : null; + if (encryptedKey == null) { + throw new CryptoFailedException("Message was not encrypted for this device"); + } + return session.processReceiving(encryptedKey); } - public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) { - byte[] key = unpackKey(session, sourceDeviceId); - return (key != null) - ? new XmppAxolotlKeyTransportMessage(session.getFingerprint(), key, getIV()) - : null; + public XmppAxolotlKeyTransportMessage getParameters(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { + return new XmppAxolotlKeyTransportMessage(session.getFingerprint(), unpackKey(session, sourceDeviceId), getIV()); } public XmppAxolotlPlaintextMessage decrypt(XmppAxolotlSession session, Integer sourceDeviceId) throws CryptoFailedException { diff --git a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java index 773b6857..359cb7fd 100644 --- a/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java +++ b/src/main/java/eu/siacs/conversations/crypto/axolotl/XmppAxolotlSession.java @@ -4,6 +4,7 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Log; +import org.bouncycastle.math.ec.PreCompInfo; import org.whispersystems.libaxolotl.AxolotlAddress; import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.IdentityKey; @@ -18,9 +19,11 @@ import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.protocol.WhisperMessage; +import org.whispersystems.libaxolotl.util.guava.Optional; import eu.siacs.conversations.Config; import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.utils.CryptoHelper; public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> { private final SessionCipher cipher; @@ -82,42 +85,43 @@ public class XmppAxolotlSession implements Comparable<XmppAxolotlSession> { } @Nullable - public byte[] processReceiving(AxolotlKey encryptedKey) { - byte[] plaintext = null; + public byte[] processReceiving(AxolotlKey encryptedKey) throws CryptoFailedException { + byte[] plaintext; FingerprintStatus status = getTrust(); if (!status.isCompromised()) { try { + CiphertextMessage ciphertextMessage; try { - PreKeyWhisperMessage message = new PreKeyWhisperMessage(encryptedKey.key); - if (!message.getPreKeyId().isPresent()) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage did not contain a PreKeyId"); - return null; + ciphertextMessage = new PreKeyWhisperMessage(encryptedKey.key); + Optional<Integer> optionalPreKeyId = ((PreKeyWhisperMessage) ciphertextMessage).getPreKeyId(); + IdentityKey identityKey = ((PreKeyWhisperMessage) ciphertextMessage).getIdentityKey(); + if (!optionalPreKeyId.isPresent()) { + throw new CryptoFailedException("PreKeyWhisperMessage did not contain a PreKeyId"); } - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "PreKeyWhisperMessage received, new session ID:" + message.getSignedPreKeyId() + "/" + message.getPreKeyId()); - IdentityKey msgIdentityKey = message.getIdentityKey(); - if (this.identityKey != null && !this.identityKey.equals(msgIdentityKey)) { - Log.e(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Had session with fingerprint " + this.getFingerprint() + ", received message with fingerprint " + msgIdentityKey.getFingerprint()); - } else { - this.identityKey = msgIdentityKey; - plaintext = cipher.decrypt(message); - preKeyId = message.getPreKeyId().get(); + preKeyId = optionalPreKeyId.get(); + if (this.identityKey != null && !this.identityKey.equals(identityKey)) { + throw new CryptoFailedException("Received PreKeyWhisperMessage but preexisting identity key changed."); } - } catch (InvalidMessageException | InvalidVersionException e) { - Log.i(Config.LOGTAG, AxolotlService.getLogprefix(account) + "WhisperMessage received"); - WhisperMessage message = new WhisperMessage(encryptedKey.key); - plaintext = cipher.decrypt(message); - } catch (InvalidKeyException | InvalidKeyIdException | UntrustedIdentityException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); + this.identityKey = identityKey; + } catch (InvalidVersionException | InvalidMessageException e) { + ciphertextMessage = new WhisperMessage(encryptedKey.key); } - } catch (LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException e) { - Log.w(Config.LOGTAG, AxolotlService.getLogprefix(account) + "Error decrypting axolotl header, " + e.getClass().getName() + ": " + e.getMessage()); - } - - if (plaintext != null) { - if (!status.isActive()) { - setTrust(status.toActive()); + if (ciphertextMessage instanceof PreKeyWhisperMessage) { + plaintext = cipher.decrypt((PreKeyWhisperMessage) ciphertextMessage); + } else { + plaintext = cipher.decrypt((WhisperMessage) ciphertextMessage); + } + } catch (InvalidKeyException | LegacyMessageException | InvalidMessageException | DuplicateMessageException | NoSessionException | InvalidKeyIdException | UntrustedIdentityException e) { + if (!(e instanceof DuplicateMessageException)) { + e.printStackTrace(); } + throw new CryptoFailedException("Error decrypting WhisperMessage " + e.getClass().getSimpleName() + ": " + e.getMessage()); } + if (!status.isActive()) { + setTrust(status.toActive()); + } + } else { + throw new CryptoFailedException("not encrypting omemo message from fingerprint "+getFingerprint()+" because it was marked as compromised"); } return plaintext; } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java new file mode 100644 index 00000000..566835d6 --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramMechanism.java @@ -0,0 +1,228 @@ +package eu.siacs.conversations.crypto.sasl; + +import android.annotation.TargetApi; +import android.os.Build; +import android.util.Base64; +import android.util.LruCache; + +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 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 String clientFirstMessageBare; + private final String clientNonce; + private byte[] serverSignature = null; + static HMac HMAC; + static Digest DIGEST; + private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); + private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); + + private static class KeyPair { + final byte[] clientKey; + final byte[] serverKey; + + KeyPair(final byte[] clientKey, final byte[] serverKey) { + this.clientKey = clientKey; + this.serverKey = serverKey; + } + } + + static { + CACHE = new LruCache<String, KeyPair>(10) { + protected KeyPair create(final String k) { + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + // 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(",", 4); + try { + final byte[] saltedPassword, serverKey, clientKey; + saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), + Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); + serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); + clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); + + return new KeyPair(clientKey, serverKey); + } catch (final InvalidKeyException | NumberFormatException e) { + return null; + } + } + }; + } + + private static final LruCache<String, KeyPair> CACHE; + + protected State state = State.INITIAL; + + ScramMechanism(final TagWriter tagWriter, final Account account, final SecureRandom rng) { + super(tagWriter, account, rng); + + // This nonce should be different for each authentication attempt. + clientNonce = new BigInteger(100, this.rng).toString(32); + clientFirstMessageBare = ""; + } + + @Override + public String getClientFirstMessage() { + if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { + clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + + ",r=" + this.clientNonce; + state = State.AUTH_TEXT_SENT; + } + return Base64.encodeToString( + (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), + Base64.NO_WRAP); + } + + @Override + public String getResponse(final String challenge) throws AuthenticationException { + switch (state) { + case AUTH_TEXT_SENT: + if (challenge == null) { + throw new AuthenticationException("challenge can not be null"); + } + byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); + final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); + String nonce = ""; + int iterationCount = -1; + String salt = ""; + for (final String token : tokenizer) { + if (token.charAt(1) == '=') { + switch (token.charAt(0)) { + case 'i': + try { + iterationCount = Integer.parseInt(token.substring(2)); + } catch (final NumberFormatException e) { + throw new AuthenticationException(e); + } + break; + case 's': + salt = token.substring(2); + break; + case 'r': + nonce = token.substring(2); + break; + case 'm': + /* + * RFC 5802: + * m: This attribute is reserved for future extensibility. In this + * version of SCRAM, its presence in a client or a server message + * MUST cause authentication failure when the attribute is parsed by + * the other end. + */ + throw new AuthenticationException("Server sent reserved token: `m'"); + } + } + } + + if (iterationCount < 0) { + throw new AuthenticationException("Server did not send iteration count"); + } + if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { + throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); + } + if (salt.isEmpty()) { + throw new AuthenticationException("Server sent empty salt"); + } + + final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( + GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; + final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' + + clientFinalMessageWithoutProof).getBytes(); + + // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". + final KeyPair keys = CACHE.get( + CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," + + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," + + CryptoHelper.bytesToHex(salt.getBytes()) + "," + + String.valueOf(iterationCount) + ); + if (keys == null) { + throw new AuthenticationException("Invalid keys generated"); + } + final byte[] clientSignature; + try { + serverSignature = hmac(keys.serverKey, authMessage); + final byte[] storedKey = digest(keys.clientKey); + + clientSignature = hmac(storedKey, authMessage); + + } catch (final InvalidKeyException e) { + throw new AuthenticationException(e); + } + + final byte[] clientProof = new byte[keys.clientKey.length]; + + for (int i = 0; i < clientProof.length; i++) { + clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); + } + + + final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + + Base64.encodeToString(clientProof, Base64.NO_WRAP); + state = State.RESPONSE_SENT; + return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); + case RESPONSE_SENT: + try { + final String clientCalculatedServerFinalMessage = "v=" + + Base64.encodeToString(serverSignature, Base64.NO_WRAP); + if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { + throw new Exception(); + } + state = State.VALID_SERVER_RESPONSE; + return ""; + } catch(Exception e) { + throw new AuthenticationException("Server final message does not match calculated final message"); + } + default: + throw new InvalidStateException(state); + } + } + + 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); + 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); + return out; + } + + /* + * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the + * 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) + throws InvalidKeyException { + byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); + byte[] out = u.clone(); + for (int i = 1; i < iterations; i++) { + u = hmac(key, u); + for (int j = 0; j < u.length; j++) { + out[j] ^= u[j]; + } + } + return out; + } +} 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 f40eec55..13593778 100644 --- a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha1.java @@ -1,77 +1,21 @@ package eu.siacs.conversations.crypto.sasl; -import android.util.Base64; -import android.util.LruCache; - -import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA1Digest; 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 eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.xml.TagWriter; -public class ScramSha1 extends SaslMechanism { - // TODO: When channel binding (SCRAM-SHA1-PLUS) is supported in future, generalize this to indicate support and/or usage. - final private static String GS2_HEADER = "n,,"; - private String clientFirstMessageBare; - final private String clientNonce; - private byte[] serverSignature = null; - private static HMac HMAC; - private static Digest DIGEST; - private static final byte[] CLIENT_KEY_BYTES = "Client Key".getBytes(); - private static final byte[] SERVER_KEY_BYTES = "Server Key".getBytes(); - - public static class KeyPair { - final public byte[] clientKey; - final public byte[] serverKey; - - public KeyPair(final byte[] clientKey, final byte[] serverKey) { - this.clientKey = clientKey; - this.serverKey = serverKey; - } - } - - private static final LruCache<String, KeyPair> CACHE; - +public class ScramSha1 extends ScramMechanism { static { DIGEST = new SHA1Digest(); HMAC = new HMac(new SHA1Digest()); - CACHE = new LruCache<String, KeyPair>(10) { - protected KeyPair create(final String k) { - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". - // 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(",", 4); - try { - final byte[] saltedPassword, serverKey, clientKey; - saltedPassword = hi(CryptoHelper.hexToString(kparts[1]).getBytes(), - Base64.decode(CryptoHelper.hexToString(kparts[2]), Base64.DEFAULT), Integer.valueOf(kparts[3])); - serverKey = hmac(saltedPassword, SERVER_KEY_BYTES); - clientKey = hmac(saltedPassword, CLIENT_KEY_BYTES); - - return new KeyPair(clientKey, serverKey); - } catch (final InvalidKeyException | NumberFormatException e) { - return null; - } - } - }; } - private State state = State.INITIAL; - public ScramSha1(final TagWriter tagWriter, final Account account, final SecureRandom rng) { super(tagWriter, account, rng); - - // This nonce should be different for each authentication attempt. - clientNonce = new BigInteger(100, this.rng).toString(32); - clientFirstMessageBare = ""; } @Override @@ -83,156 +27,4 @@ public class ScramSha1 extends SaslMechanism { public String getMechanism() { return "SCRAM-SHA-1"; } - - @Override - public String getClientFirstMessage() { - if (clientFirstMessageBare.isEmpty() && state == State.INITIAL) { - clientFirstMessageBare = "n=" + CryptoHelper.saslEscape(CryptoHelper.saslPrep(account.getUsername())) + - ",r=" + this.clientNonce; - state = State.AUTH_TEXT_SENT; - } - return Base64.encodeToString( - (GS2_HEADER + clientFirstMessageBare).getBytes(Charset.defaultCharset()), - Base64.NO_WRAP); - } - - @Override - public String getResponse(final String challenge) throws AuthenticationException { - switch (state) { - case AUTH_TEXT_SENT: - if (challenge == null) { - throw new AuthenticationException("challenge can not be null"); - } - byte[] serverFirstMessage = Base64.decode(challenge, Base64.DEFAULT); - final Tokenizer tokenizer = new Tokenizer(serverFirstMessage); - String nonce = ""; - int iterationCount = -1; - String salt = ""; - for (final String token : tokenizer) { - if (token.charAt(1) == '=') { - switch (token.charAt(0)) { - case 'i': - try { - iterationCount = Integer.parseInt(token.substring(2)); - } catch (final NumberFormatException e) { - throw new AuthenticationException(e); - } - break; - case 's': - salt = token.substring(2); - break; - case 'r': - nonce = token.substring(2); - break; - case 'm': - /* - * RFC 5802: - * m: This attribute is reserved for future extensibility. In this - * version of SCRAM, its presence in a client or a server message - * MUST cause authentication failure when the attribute is parsed by - * the other end. - */ - throw new AuthenticationException("Server sent reserved token: `m'"); - } - } - } - - if (iterationCount < 0) { - throw new AuthenticationException("Server did not send iteration count"); - } - if (nonce.isEmpty() || !nonce.startsWith(clientNonce)) { - throw new AuthenticationException("Server nonce does not contain client nonce: " + nonce); - } - if (salt.isEmpty()) { - throw new AuthenticationException("Server sent empty salt"); - } - - final String clientFinalMessageWithoutProof = "c=" + Base64.encodeToString( - GS2_HEADER.getBytes(), Base64.NO_WRAP) + ",r=" + nonce; - final byte[] authMessage = (clientFirstMessageBare + ',' + new String(serverFirstMessage) + ',' - + clientFinalMessageWithoutProof).getBytes(); - - // Map keys are "bytesToHex(JID),bytesToHex(password),bytesToHex(salt),iterations". - final KeyPair keys = CACHE.get( - CryptoHelper.bytesToHex(account.getJid().toBareJid().toString().getBytes()) + "," - + CryptoHelper.bytesToHex(account.getPassword().getBytes()) + "," - + CryptoHelper.bytesToHex(salt.getBytes()) + "," - + String.valueOf(iterationCount) - ); - if (keys == null) { - throw new AuthenticationException("Invalid keys generated"); - } - final byte[] clientSignature; - try { - serverSignature = hmac(keys.serverKey, authMessage); - final byte[] storedKey = digest(keys.clientKey); - - clientSignature = hmac(storedKey, authMessage); - - } catch (final InvalidKeyException e) { - throw new AuthenticationException(e); - } - - final byte[] clientProof = new byte[keys.clientKey.length]; - - for (int i = 0; i < clientProof.length; i++) { - clientProof[i] = (byte) (keys.clientKey[i] ^ clientSignature[i]); - } - - - final String clientFinalMessage = clientFinalMessageWithoutProof + ",p=" + - Base64.encodeToString(clientProof, Base64.NO_WRAP); - state = State.RESPONSE_SENT; - return Base64.encodeToString(clientFinalMessage.getBytes(), Base64.NO_WRAP); - case RESPONSE_SENT: - try { - final String clientCalculatedServerFinalMessage = "v=" + - Base64.encodeToString(serverSignature, Base64.NO_WRAP); - if (!clientCalculatedServerFinalMessage.equals(new String(Base64.decode(challenge, Base64.DEFAULT)))) { - throw new Exception(); - }; - state = State.VALID_SERVER_RESPONSE; - return ""; - } catch(Exception e) { - throw new AuthenticationException("Server final message does not match calculated final message"); - } - default: - throw new InvalidStateException(state); - } - } - - public 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); - 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); - return out; - } - - /* - * Hi() is, essentially, PBKDF2 [RFC2898] with HMAC() as the - * 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) - throws InvalidKeyException { - byte[] u = hmac(key, CryptoHelper.concatenateByteArrays(salt, CryptoHelper.ONE)); - byte[] out = u.clone(); - for (int i = 1; i < iterations; i++) { - u = hmac(key, u); - for (int j = 0; j < u.length; j++) { - out[j] ^= u[j]; - } - } - return out; - } } diff --git a/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java new file mode 100644 index 00000000..1b7a969d --- /dev/null +++ b/src/main/java/eu/siacs/conversations/crypto/sasl/ScramSha256.java @@ -0,0 +1,30 @@ +package eu.siacs.conversations.crypto.sasl; + +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.macs.HMac; + +import java.security.SecureRandom; + +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); + } + + @Override + public int getPriority() { + return 25; + } + + @Override + public String getMechanism() { + return "SCRAM-SHA-256"; + } +} diff --git a/src/main/java/eu/siacs/conversations/entities/Conversation.java b/src/main/java/eu/siacs/conversations/entities/Conversation.java index ced48913..fa74c418 100644 --- a/src/main/java/eu/siacs/conversations/entities/Conversation.java +++ b/src/main/java/eu/siacs/conversations/entities/Conversation.java @@ -680,25 +680,41 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl return this.nextCounterpart; } - private int getMostRecentlyUsedIncomingEncryption() { - synchronized (this.messages) { - for(int i = this.messages.size() -1; i >= 0; --i) { - final Message m = this.messages.get(i); - if (m.getStatus() == Message.STATUS_RECEIVED) { - final int e = m.getEncryption(); - if (e == Message.ENCRYPTION_DECRYPTED || e == Message.ENCRYPTION_DECRYPTION_FAILED) { - return Message.ENCRYPTION_PGP; - } else { - return e; - } - } - } - } - return Message.ENCRYPTION_NONE; - } - public int getNextEncryption() { - return Math.max(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, Message.ENCRYPTION_NONE), Message.ENCRYPTION_NONE); + return fixAvailableEncryption(this.getIntAttribute(ATTRIBUTE_NEXT_ENCRYPTION, getDefaultEncryption())); + } + + private int fixAvailableEncryption(int selectedEncryption) { + switch(selectedEncryption) { + case Message.ENCRYPTION_NONE: + return Config.supportUnencrypted() ? selectedEncryption : getDefaultEncryption(); + case Message.ENCRYPTION_AXOLOTL: + return Config.supportOmemo() ? selectedEncryption : getDefaultEncryption(); + case Message.ENCRYPTION_OTR: + return Config.supportOtr() ? selectedEncryption : getDefaultEncryption(); + case Message.ENCRYPTION_PGP: + case Message.ENCRYPTION_DECRYPTED: + case Message.ENCRYPTION_DECRYPTION_FAILED: + return Config.supportOpenPgp() ? Message.ENCRYPTION_PGP : getDefaultEncryption(); + default: + return getDefaultEncryption(); + } + } + + private int getDefaultEncryption() { + AxolotlService axolotlService = account.getAxolotlService(); + if (Config.supportUnencrypted()) { + return Message.ENCRYPTION_NONE; + } else if (Config.supportOmemo() + && (axolotlService != null && axolotlService.isConversationAxolotlCapable(this) || !Config.multipleEncryptionChoices())) { + return Message.ENCRYPTION_AXOLOTL; + } else if (Config.supportOtr() && mode == MODE_SINGLE) { + return Message.ENCRYPTION_OTR; + } else if (Config.supportOpenPgp()) { + return Message.ENCRYPTION_PGP; + } else { + return Message.ENCRYPTION_NONE; + } } public void setNextEncryption(int encryption) { diff --git a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java index 356856dc..970ace6f 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpDownloadConnection.java @@ -120,7 +120,7 @@ public class HttpDownloadConnection implements Transferable { } else { message.setTransferable(null); } - mXmppConnectionService.updateConversationUi(); + mHttpConnectionManager.updateConversationUi(true); } private void finish() { @@ -131,7 +131,7 @@ public class HttpDownloadConnection implements Transferable { if (message.getEncryption() == Message.ENCRYPTION_PGP) { notify = message.getConversation().getAccount().getPgpDecryptionService().decrypt(message, notify); } - mXmppConnectionService.updateConversationUi(); + mHttpConnectionManager.updateConversationUi(true); if (notify) { mXmppConnectionService.getNotificationService().push(message); } @@ -139,7 +139,7 @@ public class HttpDownloadConnection implements Transferable { private void changeStatus(int status) { this.mStatus = status; - mXmppConnectionService.updateConversationUi(); + mHttpConnectionManager.updateConversationUi(true); } private void showToastForException(Exception e) { @@ -340,7 +340,7 @@ public class HttpDownloadConnection implements Transferable { public void updateProgress(int i) { this.mProgress = i; - mXmppConnectionService.updateConversationUi(); + mHttpConnectionManager.updateConversationUi(false); } @Override diff --git a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java index 63a3884b..06a325d0 100644 --- a/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java +++ b/src/main/java/eu/siacs/conversations/http/HttpUploadConnection.java @@ -182,7 +182,7 @@ public class HttpUploadConnection implements Transferable { while (((count = mFileInputStream.read(buffer)) != -1) && !canceled) { transmitted += count; os.write(buffer, 0, count); - mXmppConnectionService.updateConversationUi(); + mHttpConnectionManager.updateConversationUi(false); } os.flush(); os.close(); diff --git a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java index 3239ce3d..bc9ed259 100644 --- a/src/main/java/eu/siacs/conversations/persistance/FileBackend.java +++ b/src/main/java/eu/siacs/conversations/persistance/FileBackend.java @@ -555,6 +555,7 @@ public class FileBackend { File file = new File(getAvatarPath(hash)); FileInputStream is = null; try { + avatar.size = file.length(); BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(file.getAbsolutePath(), options); @@ -574,6 +575,7 @@ public class FileBackend { avatar.image = new String(mByteArrayOutputStream.toByteArray()); avatar.height = options.outHeight; avatar.width = options.outWidth; + avatar.type = options.outMimeType; return avatar; } catch (IOException e) { return null; @@ -593,6 +595,7 @@ public class FileBackend { File file; if (isAvatarCached(avatar)) { file = new File(getAvatarPath(avatar.getFilename())); + avatar.size = file.length(); } else { String filename = getAvatarPath(avatar.getFilename()); file = new File(filename + ".tmp"); @@ -604,7 +607,8 @@ public class FileBackend { MessageDigest digest = MessageDigest.getInstance("SHA-1"); digest.reset(); DigestOutputStream mDigestOutputStream = new DigestOutputStream(os, digest); - mDigestOutputStream.write(avatar.getImageAsBytes()); + final byte[] bytes = avatar.getImageAsBytes(); + mDigestOutputStream.write(bytes); mDigestOutputStream.flush(); mDigestOutputStream.close(); String sha1sum = CryptoHelper.bytesToHex(digest.digest()); @@ -615,13 +619,13 @@ public class FileBackend { file.delete(); return false; } + avatar.size = bytes.length; } catch (IllegalArgumentException | IOException | NoSuchAlgorithmException e) { return false; } finally { close(os); } } - avatar.size = file.length(); return true; } @@ -691,7 +695,7 @@ public class FileBackend { Bitmap dest = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(dest); canvas.drawBitmap(source, null, targetRect, null); - if (source != null && !source.isRecycled()) { + if (source.isRecycled()) { source.recycle(); } return dest; diff --git a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java index dfe4cb28..18512997 100644 --- a/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/services/AbstractConnectionManager.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.Build; import android.os.PowerManager; +import android.os.SystemClock; import android.util.Log; import android.util.Pair; @@ -22,6 +23,7 @@ import java.io.OutputStream; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; +import java.util.concurrent.atomic.AtomicLong; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; @@ -36,6 +38,9 @@ import eu.siacs.conversations.entities.DownloadableFile; public class AbstractConnectionManager { protected XmppConnectionService mXmppConnectionService; + private static final int UI_REFRESH_THRESHOLD = 250; + private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0); + public AbstractConnectionManager(XmppConnectionService service) { this.mXmppConnectionService = service; } @@ -136,6 +141,15 @@ public class AbstractConnectionManager { } } + public void updateConversationUi(boolean force) { + synchronized (LAST_UI_UPDATE_CALL) { + if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) { + LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime()); + mXmppConnectionService.updateConversationUi(); + } + } + } + public PowerManager.WakeLock createWakeLock(String name) { PowerManager powerManager = (PowerManager) mXmppConnectionService.getSystemService(Context.POWER_SERVICE); return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,name); diff --git a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java index a27e6c3e..a352ea8a 100644 --- a/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java +++ b/src/main/java/eu/siacs/conversations/services/MessageArchiveService.java @@ -111,7 +111,9 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { return null; } final Query query = new Query(conversation, start, end,PagingOrder.REVERSE); - query.reference = conversation.getFirstMamReference(); + if (start==0) { + query.reference = conversation.getFirstMamReference(); + } this.queries.add(query); this.execute(query); return query; @@ -224,7 +226,7 @@ public class MessageArchiveService implements OnAdvancedStreamFeaturesLoaded { this.finalizeQuery(query, done); Log.d(Config.LOGTAG,query.getAccount().getJid().toBareJid()+": finished mam after "+query.getTotalCount()+" messages. messages left="+Boolean.toString(!done)); if (query.getWith() == null && query.getMessageCount() > 0) { - mXmppConnectionService.getNotificationService().finishBacklog(true); + mXmppConnectionService.getNotificationService().finishBacklog(true,query.getAccount()); } } else { final Query nextQuery; diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 47364b30..904392ee 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -100,13 +100,28 @@ public class NotificationService { } } - public void finishBacklog(boolean notify) { + public void finishBacklog(boolean notify, Account account) { synchronized (notifications) { mXmppConnectionService.updateUnreadCountBadge(); - updateNotification(notify); + if (account == null || !notify) { + updateNotification(notify); + } else { + boolean hasPendingMessages = false; + for(ArrayList<Message> messages : notifications.values()) { + if (messages.size() > 0 && messages.get(0).getConversation().getAccount() == account) { + hasPendingMessages = true; + break; + } + } + updateNotification(hasPendingMessages); + } } } + public void finishBacklog(boolean notify) { + finishBacklog(notify,null); + } + private void pushToStack(final Message message) { final String conversationUuid = message.getConversationUuid(); if (notifications.containsKey(conversationUuid)) { @@ -507,7 +522,7 @@ public class NotificationService { return (m.find() || message.getType() == Message.TYPE_PRIVATE); } - private static Pattern generateNickHighlightPattern(final String nick) { + public static Pattern generateNickHighlightPattern(final String nick) { // We expect a word boundary, i.e. space or start of string, followed by // the // nick (matched in case-insensitive manner), followed by optional diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index ce49b2b6..e53b2524 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -2639,14 +2639,13 @@ public class XmppConnectionService extends Service { } public void publishAvatar(Account account, final Avatar avatar, final UiCallback<Avatar> callback) { - final IqPacket packet = this.mIqGenerator.publishAvatar(avatar); + IqPacket packet = this.mIqGenerator.publishAvatar(avatar); this.sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket result) { if (result.getType() == IqPacket.TYPE.RESULT) { - final IqPacket packet = XmppConnectionService.this.mIqGenerator - .publishAvatarMetadata(avatar); + final IqPacket packet = XmppConnectionService.this.mIqGenerator.publishAvatarMetadata(avatar); sendIqPacket(account, packet, new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket result) { @@ -2655,25 +2654,22 @@ public class XmppConnectionService extends Service { getAvatarService().clear(account); databaseBackend.updateAccount(account); } + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar "+(avatar.size/1024)+"KiB"); if (callback != null) { callback.success(avatar); - } else { - Log.d(Config.LOGTAG,account.getJid().toBareJid()+": published avatar"); } } else { if (callback != null) { - callback.error( - R.string.error_publish_avatar_server_reject, - avatar); + callback.error(R.string.error_publish_avatar_server_reject,avatar); } } } }); } else { + Element error = result.findChild("error"); + Log.d(Config.LOGTAG,account.getJid().toBareJid()+": server rejected avatar "+(avatar.size/1024)+"KiB "+(error!=null?error.toString():"")); if (callback != null) { - callback.error( - R.string.error_publish_avatar_server_reject, - avatar); + callback.error(R.string.error_publish_avatar_server_reject, avatar); } } } diff --git a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java index 69936b1d..1b76173a 100644 --- a/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ContactDetailsActivity.java @@ -116,6 +116,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp private Button mShowInactiveDevicesButton; private QuickContactBadge badge; private LinearLayout keys; + private LinearLayout keysWrapper; private FlowLayout tags; private boolean showDynamicTags = false; private boolean showLastSeen = false; @@ -220,6 +221,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp } }); keys = (LinearLayout) findViewById(R.id.details_contact_keys); + keysWrapper = (LinearLayout) findViewById(R.id.keys_wrapper); tags = (FlowLayout) findViewById(R.id.tags); mShowInactiveDevicesButton = (Button) findViewById(R.id.show_inactive_devices); if (getActionBar() != null) { @@ -521,11 +523,7 @@ public class ContactDetailsActivity extends OmemoActivity implements OnAccountUp }); keys.addView(view); } - if (hasKeys) { - keys.setVisibility(View.VISIBLE); - } else { - keys.setVisibility(View.GONE); - } + keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE); List<ListItem.Tag> tagList = contact.getTags(this); if (tagList.size() == 0 || !this.showDynamicTags) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java index 11fa76c2..189879a6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationActivity.java @@ -99,6 +99,7 @@ public class ConversationActivity extends XmppActivity private String mOpenConversation = null; private boolean mPanelOpen = true; + private AtomicBoolean mShouldPanelBeOpen = new AtomicBoolean(false); private Pair<Integer,Integer> mScrollPosition = null; final private List<Uri> mPendingImageUris = new ArrayList<>(); final private List<Uri> mPendingFileUris = new ArrayList<>(); @@ -134,6 +135,7 @@ public class ConversationActivity extends XmppActivity public void showConversationsOverview() { if (mContentView instanceof SlidingPaneLayout) { SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mShouldPanelBeOpen.set(true); mSlidingPaneLayout.openPane(); } } @@ -151,6 +153,7 @@ public class ConversationActivity extends XmppActivity public void hideConversationsOverview() { if (mContentView instanceof SlidingPaneLayout) { SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; + mShouldPanelBeOpen.set(false); mSlidingPaneLayout.closePane(); } } @@ -161,8 +164,7 @@ public class ConversationActivity extends XmppActivity public boolean isConversationsOverviewVisable() { if (mContentView instanceof SlidingPaneLayout) { - SlidingPaneLayout mSlidingPaneLayout = (SlidingPaneLayout) mContentView; - return mSlidingPaneLayout.isOpen(); + return mShouldPanelBeOpen.get(); } else { return true; } @@ -302,18 +304,19 @@ public class ConversationActivity extends XmppActivity @Override public void onPanelOpened(View arg0) { + mShouldPanelBeOpen.set(true); updateActionBarTitle(); invalidateOptionsMenu(); hideKeyboard(); if (xmppConnectionServiceBound) { - xmppConnectionService.getNotificationService() - .setOpenConversation(null); + xmppConnectionService.getNotificationService().setOpenConversation(null); } closeContextMenu(); } @Override public void onPanelClosed(View arg0) { + mShouldPanelBeOpen.set(false); listView.discardUndo(); openConversation(); } @@ -1266,6 +1269,9 @@ public class ConversationActivity extends XmppActivity if (!ExceptionHelper.checkForCrash(this, this.xmppConnectionService)) { openBatteryOptimizationDialogIfNeeded(); } + if (isConversationsOverviewVisable() && isConversationsOverviewHideable()) { + xmppConnectionService.getNotificationService().setOpenConversation(null); + } } private void handleViewConversationIntent(final Intent intent) { @@ -1498,7 +1504,7 @@ public class ConversationActivity extends XmppActivity private boolean hasAccountWithoutPush() { for(Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() != Account.State.DISABLED - && !xmppConnectionService.getPushManagementService().available(account)) { + && !xmppConnectionService.getPushManagementService().availableAndUseful(account)) { return true; } } diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 709244dd..e6d99579 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -34,6 +34,7 @@ import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; +import android.widget.EditText; import android.widget.ImageButton; import android.widget.ListView; import android.widget.RelativeLayout; @@ -849,16 +850,22 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa } protected void highlightInConference(String nick) { - String oldString = mEditMessage.getText().toString().trim(); - if (oldString.isEmpty() || mEditMessage.getSelectionStart() == 0) { + final Editable editable = mEditMessage.getText(); + String oldString = editable.toString().trim(); + final int pos = mEditMessage.getSelectionStart(); + if (oldString.isEmpty() || pos == 0) { mEditMessage.getText().insert(0, nick + ": "); } else { - if (mEditMessage.getText().charAt( - mEditMessage.getSelectionStart() - 1) != ' ') { - nick = " " + nick; + final char before = editable.charAt(pos - 1); + final char after = editable.length() > pos ? editable.charAt(pos) : '\0'; + if (before == '\n') { + editable.insert(pos, nick + ": "); + } else { + editable.insert(pos,(Character.isWhitespace(before)? "" : " ") + nick + (Character.isWhitespace(after) ? "" : " ")); + if (Character.isWhitespace(after)) { + mEditMessage.setSelection(mEditMessage.getSelectionStart()+1); + } } - mEditMessage.getText().insert(mEditMessage.getSelectionStart(), - nick + " "); } } diff --git a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java index 81d26b39..2080ddc0 100644 --- a/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/EditAccountActivity.java @@ -836,7 +836,7 @@ public class EditAccountActivity extends OmemoActivity implements OnAccountUpdat if (this.mAccount.isOnlineAndConnected() && !this.mFetchingAvatar) { Features features = this.mAccount.getXmppConnection().getFeatures(); this.mStats.setVisibility(View.VISIBLE); - boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().available(mAccount) && isOptimizingBattery(); + boolean showBatteryWarning = !xmppConnectionService.getPushManagementService().availableAndUseful(mAccount) && isOptimizingBattery(); boolean showDataSaverWarning = isAffectedByDataSaver(); showOsOptimizationWarning(showBatteryWarning,showDataSaverWarning); this.mSessionEst.setText(UIHelper.readableTimeDifferenceFull(this, this.mAccount.getXmppConnection() diff --git a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java index 8ce8c14d..7929e073 100644 --- a/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/OmemoActivity.java @@ -24,8 +24,6 @@ import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession; import eu.siacs.conversations.entities.Account; -import eu.siacs.conversations.ui.TrustKeysActivity; -import eu.siacs.conversations.ui.XmppActivity; import eu.siacs.conversations.ui.widget.Switch; import eu.siacs.conversations.utils.CryptoHelper; import eu.siacs.conversations.utils.XmppUri; @@ -51,16 +49,17 @@ public abstract class OmemoActivity extends XmppActivity { && fingerprint instanceof String && fingerprintStatus instanceof FingerprintStatus) { getMenuInflater().inflate(R.menu.omemo_key_context, menu); - MenuItem purgeItem = menu.findItem(R.id.purge_omemo_key); + MenuItem distrust = menu.findItem(R.id.distrust_key); MenuItem verifyScan = menu.findItem(R.id.verify_scan); if (this instanceof TrustKeysActivity) { - purgeItem.setVisible(false); + distrust.setVisible(false); verifyScan.setVisible(false); } else { FingerprintStatus status = (FingerprintStatus) fingerprintStatus; if (!status.isActive() || status.isVerified()) { verifyScan.setVisible(false); } + distrust.setVisible(status.isVerified()); } this.mSelectedAccount = (Account) account; this.mSelectedFingerprint = (String) fingerprint; @@ -70,7 +69,7 @@ public abstract class OmemoActivity extends XmppActivity { @Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { - case R.id.purge_omemo_key: + case R.id.distrust_key: showPurgeKeyDialog(mSelectedAccount,mSelectedFingerprint); break; case R.id.copy_omemo_key: @@ -242,17 +241,14 @@ public abstract class OmemoActivity extends XmppActivity { public void showPurgeKeyDialog(final Account account, final String fingerprint) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(R.string.purge_key)); - builder.setIconAttribute(android.R.attr.alertDialogIcon); - builder.setMessage(getString(R.string.purge_key_desc_part1) - + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2)) - + "\n\n" + getString(R.string.purge_key_desc_part2)); + builder.setTitle(R.string.distrust_omemo_key); + builder.setMessage(R.string.distrust_omemo_key_text); builder.setNegativeButton(getString(R.string.cancel), null); - builder.setPositiveButton(getString(R.string.purge_key), + builder.setPositiveButton(R.string.confirm, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - account.getAxolotlService().purgeKey(fingerprint); + account.getAxolotlService().distrustFingerprint(fingerprint); refreshUi(); } }); diff --git a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java index 9a7414ef..f5724fc6 100644 --- a/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ShareWithActivity.java @@ -310,15 +310,61 @@ public class ShareWithActivity extends XmppActivity implements XmppConnectionSer } else { if (mReturnToPrevious && this.share.text != null && !this.share.text.isEmpty() ) { final OnPresenceSelected callback = new OnPresenceSelected() { + + private void finishAndSend(Message message) { + xmppConnectionService.sendMessage(message); + replaceToast(getString(R.string.shared_text_with_x, conversation.getName())); + finish(); + } + + private UiCallback<Message> messageEncryptionCallback = new UiCallback<Message>() { + @Override + public void success(final Message message) { + message.setEncryption(Message.ENCRYPTION_DECRYPTED); + runOnUiThread(new Runnable() { + @Override + public void run() { + finishAndSend(message); + } + }); + } + + @Override + public void error(final int errorCode, Message object) { + runOnUiThread(new Runnable() { + @Override + public void run() { + replaceToast(getString(errorCode)); + finish(); + } + }); + } + + @Override + public void userInputRequried(PendingIntent pi, Message object) { + finish(); + } + }; + @Override public void onPresenceSelected() { - Message message = new Message(conversation,share.text, conversation.getNextEncryption()); - if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { + + final int encryption = conversation.getNextEncryption(); + + Message message = new Message(conversation,share.text, encryption); + + Log.d(Config.LOGTAG,"on presence selected encrpytion="+encryption); + + if (encryption == Message.ENCRYPTION_PGP) { + replaceToast(getString(R.string.encrypting_message)); + xmppConnectionService.getPgpEngine().encrypt(message,messageEncryptionCallback); + return; + } + + if (encryption == Message.ENCRYPTION_OTR) { message.setCounterpart(conversation.getNextCounterpart()); } - xmppConnectionService.sendMessage(message); - replaceToast(getString(R.string.shared_text_with_x, conversation.getName())); - finish(); + finishAndSend(message); } }; if (conversation.getNextEncryption() == Message.ENCRYPTION_OTR) { diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index ce56e30c..a01689ec 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -52,6 +52,7 @@ import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.Message.FileParams; import eu.siacs.conversations.entities.Transferable; import eu.siacs.conversations.persistance.FileBackend; +import eu.siacs.conversations.services.NotificationService; import eu.siacs.conversations.ui.ConversationActivity; import eu.siacs.conversations.ui.text.DividerSpan; import eu.siacs.conversations.ui.text.QuoteSpan; @@ -88,6 +89,12 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie } } }; + private static final Linkify.MatchFilter WEBURL_MATCH_FILTER = new Linkify.MatchFilter() { + @Override + public boolean acceptMatch(CharSequence charSequence, int start, int end) { + return start < 1 || charSequence.charAt(start-1) != '@'; + } + }; private ConversationActivity activity; @@ -442,8 +449,15 @@ public class MessageAdapter extends ArrayAdapter<Message> implements CopyTextVie privateMarkerIndex + 1 + nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } - Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", null, WEBURL_TRANSFORM_FILTER); + if (message.getConversation().getMode() == Conversation.MODE_MULTI && message.getStatus() == Message.STATUS_RECEIVED) { + Pattern pattern = NotificationService.generateNickHighlightPattern(message.getConversation().getMucOptions().getActualNick()); + Matcher matcher = pattern.matcher(body); + while(matcher.find()) { + body.setSpan(new StyleSpan(Typeface.BOLD), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } Linkify.addLinks(body, XMPP_PATTERN, "xmpp"); + Linkify.addLinks(body, Patterns.AUTOLINK_WEB_URL, "http", WEBURL_MATCH_FILTER, WEBURL_TRANSFORM_FILTER); Linkify.addLinks(body, GeoHelper.GEO_URI, "geo"); viewHolder.messageBody.setAutoLinkMask(0); viewHolder.messageBody.setText(body); diff --git a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java index 7cd4707d..08dbdc18 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/XmppConnection.java @@ -13,8 +13,6 @@ import android.util.Log; import android.util.Pair; import android.util.SparseArray; -import org.json.JSONException; -import org.json.JSONObject; import org.xmlpull.v1.XmlPullParserException; import java.io.ByteArrayInputStream; @@ -26,8 +24,8 @@ import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.UnknownHostException; import java.net.URL; +import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.Principal; @@ -61,6 +59,7 @@ import eu.siacs.conversations.crypto.sasl.External; 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.entities.Account; import eu.siacs.conversations.entities.Message; import eu.siacs.conversations.entities.ServiceDiscoveryResult; @@ -178,8 +177,6 @@ public class XmppConnection implements Runnable { } } - private Identity mServerIdentity = Identity.UNKNOWN; - public final OnIqPacketReceived registrationResponseListener = new OnIqPacketReceived() { @Override public void onIqPacketReceived(Account account, IqPacket packet) { @@ -253,17 +250,6 @@ public class XmppConnection implements Runnable { Log.d(Config.LOGTAG, account.getJid().toBareJid().toString() + ": connecting"); features.encryptionEnabled = false; this.attempt++; - switch (account.getJid().getDomainpart()) { - case "chat.facebook.com": - mServerIdentity = Identity.FACEBOOK; - break; - case "nimbuzz.com": - mServerIdentity = Identity.NIMBUZZ; - break; - default: - mServerIdentity = Identity.UNKNOWN; - break; - } try { Socket localSocket; shouldAuthenticate = needsBinding = !account.isOptionSet(Account.OPTION_REGISTER); @@ -747,7 +733,7 @@ public class XmppConnection implements Runnable { final Pair<IqPacket, OnIqPacketReceived> packetCallbackDuple = packetCallbacks.get(packet.getId()); // Packets to the server should have responses from the server if (packetCallbackDuple.first.toServer(account)) { - if (packet.fromServer(account) || mServerIdentity == Identity.FACEBOOK) { + if (packet.fromServer(account)) { callback = packetCallbackDuple.second; packetCallbacks.remove(packet.getId()); } else { @@ -868,6 +854,8 @@ public class XmppConnection implements Runnable { auth.setAttribute("xmlns", "urn:ietf:params:xml:ns:xmpp-sasl"); if (mechanisms.contains("EXTERNAL") && account.getPrivateKeyAlias() != null) { saslMechanism = new External(tagWriter, account, mXmppConnectionService.getRNG()); + } else if (mechanisms.contains("SCRAM-SHA-256")) { + saslMechanism = new ScramSha256(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains("SCRAM-SHA-1")) { saslMechanism = new ScramSha1(tagWriter, account, mXmppConnectionService.getRNG()); } else if (mechanisms.contains("PLAIN")) { @@ -1099,7 +1087,7 @@ public class XmppConnection implements Runnable { this.disco.clear(); } mPendingServiceDiscoveries.set(0); - mWaitForDisco.set(mServerIdentity != Identity.NIMBUZZ && smVersion != 0); + mWaitForDisco.set(smVersion != 0 && !account.getJid().getDomainpart().equalsIgnoreCase("nimbuzz.com")); lastDiscoStarted = SystemClock.elapsedRealtime(); Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": starting service discovery"); mXmppConnectionService.scheduleWakeUpCall(Config.CONNECT_DISCO_TIMEOUT, account.getUuid().hashCode()); @@ -1138,24 +1126,6 @@ public class XmppConnection implements Runnable { boolean advancedStreamFeaturesLoaded; synchronized (XmppConnection.this.disco) { ServiceDiscoveryResult result = new ServiceDiscoveryResult(packet); - for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { - if (mServerIdentity == Identity.UNKNOWN && id.getType().equals("im") && - id.getCategory().equals("server") && id.getName() != null && - jid.equals(account.getServer())) { - switch (id.getName()) { - case "Prosody": - mServerIdentity = Identity.PROSODY; - break; - case "ejabberd": - mServerIdentity = Identity.EJABBERD; - break; - case "Slack-XMPP": - mServerIdentity = Identity.SLACK; - break; - } - Log.d(Config.LOGTAG, account.getJid().toBareJid() + ": server name: " + id.getName()); - } - } if (jid.equals(account.getServer())) { mXmppConnectionService.databaseBackend.insertDiscoveryResult(result); } @@ -1540,7 +1510,25 @@ public class XmppConnection implements Runnable { } public Identity getServerIdentity() { - return mServerIdentity; + synchronized (this.disco) { + ServiceDiscoveryResult result = disco.get(account.getJid().toDomainJid()); + if (result == null) { + return Identity.UNKNOWN; + } + for (final ServiceDiscoveryResult.Identity id : result.getIdentities()) { + if (id.getType().equals("im") && id.getCategory().equals("server") && id.getName() != null) { + switch (id.getName()) { + case "Prosody": + return Identity.PROSODY; + case "ejabberd": + return Identity.EJABBERD; + case "Slack-XMPP": + return Identity.SLACK; + } + } + } + } + return Identity.UNKNOWN; } private class UnauthorizedException extends IOException { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java index 5461b9c6..0c0c054d 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnection.java @@ -388,7 +388,7 @@ public class JingleConnection implements Transferable { long size = Long.parseLong(fileSize.getContent()); message.setBody(Long.toString(size)); conversation.add(message); - mXmppConnectionService.updateConversationUi(); + mJingleConnectionManager.updateConversationUi(true); if (mJingleConnectionManager.hasStoragePermission() && size < this.mJingleConnectionManager.getAutoAcceptFileSize() && mXmppConnectionService.isDataSaverDisabled()) { @@ -510,7 +510,7 @@ public class JingleConnection implements Transferable { private void sendAccept() { mJingleStatus = JINGLE_STATUS_ACCEPTED; this.mStatus = Transferable.STATUS_DOWNLOADING; - mXmppConnectionService.updateConversationUi(); + this.mJingleConnectionManager.updateConversationUi(true); this.mJingleConnectionManager.getPrimaryCandidate(this.account, new OnPrimaryCandidateFound() { @Override public void onPrimaryCandidateFound(boolean success, final JingleCandidate candidate) { @@ -842,7 +842,7 @@ public class JingleConnection implements Transferable { if (this.file!=null) { file.delete(); } - this.mXmppConnectionService.updateConversationUi(); + this.mJingleConnectionManager.updateConversationUi(true); } else { this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED); @@ -868,7 +868,7 @@ public class JingleConnection implements Transferable { if (this.file!=null) { file.delete(); } - this.mXmppConnectionService.updateConversationUi(); + this.mJingleConnectionManager.updateConversationUi(true); } else { this.mXmppConnectionService.markMessage(this.message, Message.STATUS_SEND_FAILED, @@ -1016,7 +1016,7 @@ public class JingleConnection implements Transferable { public void updateProgress(int i) { this.mProgress = i; - mXmppConnectionService.updateConversationUi(); + mJingleConnectionManager.updateConversationUi(false); } public String getTransportId() { |