diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemoryIdentityKeyStore.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemoryIdentityKeyStore.java index 520dd7d4b4..e510a26d1b 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemoryIdentityKeyStore.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemoryIdentityKeyStore.java @@ -44,4 +44,10 @@ public class InMemoryIdentityKeyStore implements IdentityKeyStore { public void saveIdentity(long recipientId, IdentityKey identityKey) { trustedKeys.put(recipientId, identityKey); } + + @Override + public boolean isTrustedIdentity(long recipientId, IdentityKey identityKey) { + IdentityKey trusted = trustedKeys.get(recipientId); + return (trusted == null || trusted.equals(identityKey)); + } } diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java index 713ef573b3..01980a0ae5 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionBuilderTest.java @@ -11,6 +11,8 @@ import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; import org.whispersystems.libaxolotl.SessionBuilder; import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.StaleKeyExchangeException; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPublicKey; @@ -33,8 +35,7 @@ public class SessionBuilderTest extends AndroidTestCase { private static final long BOB_RECIPIENT_ID = 2L; public void testBasicPreKey() - throws InvalidKeyException, InvalidVersionException, InvalidMessageException, InvalidKeyIdException, DuplicateMessageException, LegacyMessageException - { + throws InvalidKeyException, InvalidVersionException, InvalidMessageException, InvalidKeyIdException, DuplicateMessageException, LegacyMessageException, UntrustedIdentityException { SessionStore aliceSessionStore = new InMemorySessionStore(); PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore(); IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); @@ -74,9 +75,55 @@ public class SessionBuilderTest extends AndroidTestCase { byte[] plaintext = bobSessionCipher.decrypt(incomingMessage.getWhisperMessage().serialize()); assertTrue(originalMessage.equals(new String(plaintext))); + + CiphertextMessage bobOutgoingMessage = bobSessionCipher.encrypt(originalMessage.getBytes()); + assertTrue(bobOutgoingMessage.getType() == CiphertextMessage.WHISPER_TYPE); + + byte[] alicePlaintext = aliceSessionCipher.decrypt(bobOutgoingMessage.serialize()); + assertTrue(new String(alicePlaintext).equals(originalMessage)); + + runInteraction(aliceSessionStore, bobSessionStore); + + aliceSessionStore = new InMemorySessionStore(); + aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); + aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore, + aliceIdentityKeyStore, + BOB_RECIPIENT_ID, 1); + aliceSessionCipher = new SessionCipher(aliceSessionStore, BOB_RECIPIENT_ID, 1); + + bobPreKey = new InMemoryPreKey(31338, Curve.generateKeyPair(true), + bobIdentityKeyStore.getIdentityKeyPair().getPublicKey(), + bobIdentityKeyStore.getLocalRegistrationId()); + + bobPreKeyStore.store(31338, bobPreKey); + aliceSessionBuilder.process(bobPreKey); + + outgoingMessage = aliceSessionCipher.encrypt(originalMessage.getBytes()); + + try { + bobSessionBuilder.process(new PreKeyWhisperMessage(outgoingMessage.serialize())); + throw new AssertionError("shouldn't be trusted!"); + } catch (UntrustedIdentityException uie) { + bobIdentityKeyStore.saveIdentity(ALICE_RECIPIENT_ID, new PreKeyWhisperMessage(outgoingMessage.serialize()).getIdentityKey()); + bobSessionBuilder.process(new PreKeyWhisperMessage(outgoingMessage.serialize())); + } + + plaintext = bobSessionCipher.decrypt(new PreKeyWhisperMessage(outgoingMessage.serialize()).getWhisperMessage().serialize()); + assertTrue(new String(plaintext).equals(originalMessage)); + + bobPreKey = new InMemoryPreKey(31337, Curve.generateKeyPair(true), + aliceIdentityKeyStore.getIdentityKeyPair().getPublicKey(), + bobIdentityKeyStore.getLocalRegistrationId()); + + try { + aliceSessionBuilder.process(bobPreKey); + throw new AssertionError("shoulnd't be trusted!"); + } catch (UntrustedIdentityException uie) { + // good + } } - public void testBasicKeyExchange() throws InvalidKeyException, LegacyMessageException, InvalidMessageException, DuplicateMessageException { + public void testBasicKeyExchange() throws InvalidKeyException, LegacyMessageException, InvalidMessageException, DuplicateMessageException, UntrustedIdentityException, StaleKeyExchangeException { SessionStore aliceSessionStore = new InMemorySessionStore(); PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore(); IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); @@ -104,11 +151,28 @@ public class SessionBuilderTest extends AndroidTestCase { assertTrue(bobSessionStore.contains(ALICE_RECIPIENT_ID, 1)); runInteraction(aliceSessionStore, bobSessionStore); + + aliceSessionStore = new InMemorySessionStore(); + aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); + aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore, + aliceIdentityKeyStore, BOB_RECIPIENT_ID, 1); + aliceKeyExchangeMessage = aliceSessionBuilder.process(); + + try { + bobKeyExchangeMessage = bobSessionBuilder.process(aliceKeyExchangeMessage); + throw new AssertionError("This identity shouldn't be trusted!"); + } catch (UntrustedIdentityException uie) { + bobIdentityKeyStore.saveIdentity(ALICE_RECIPIENT_ID, aliceKeyExchangeMessage.getIdentityKey()); + bobKeyExchangeMessage = bobSessionBuilder.process(aliceKeyExchangeMessage); + } + + assertTrue(aliceSessionBuilder.process(bobKeyExchangeMessage) == null); + + runInteraction(aliceSessionStore, bobSessionStore); } public void testSimultaneousKeyExchange() - throws InvalidKeyException, DuplicateMessageException, LegacyMessageException, InvalidMessageException - { + throws InvalidKeyException, DuplicateMessageException, LegacyMessageException, InvalidMessageException, UntrustedIdentityException, StaleKeyExchangeException { SessionStore aliceSessionStore = new InMemorySessionStore(); PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore(); IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore(); diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java index a27f829fc4..11d7f06173 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java @@ -13,6 +13,7 @@ import org.whispersystems.libaxolotl.state.PreKey; import org.whispersystems.libaxolotl.state.PreKeyRecord; import org.whispersystems.libaxolotl.state.PreKeyStore; import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.libaxolotl.util.KeyHelper; import org.whispersystems.libaxolotl.util.Medium; @@ -77,16 +78,19 @@ public class SessionBuilder { * that corresponds to the PreKey ID in * the message. * @throws org.whispersystems.libaxolotl.InvalidKeyException when the message is formatted incorrectly. + * @throws org.whispersystems.libaxolotl.UntrustedIdentityException when the {@link IdentityKey} of the sender is untrusted. */ public void process(PreKeyWhisperMessage message) - throws InvalidKeyIdException, InvalidKeyException + throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException { int preKeyId = message.getPreKeyId(); ECPublicKey theirBaseKey = message.getBaseKey(); ECPublicKey theirEphemeralKey = message.getWhisperMessage().getSenderEphemeral(); IdentityKey theirIdentityKey = message.getIdentityKey(); - Log.w(TAG, "Received pre-key with local key ID: " + preKeyId); + if (!identityKeyStore.isTrustedIdentity(recipientId, theirIdentityKey)) { + throw new UntrustedIdentityException(); + } if (!preKeyStore.contains(preKeyId) && sessionStore.contains(recipientId, deviceId)) @@ -134,8 +138,16 @@ public class SessionBuilder { * @param preKey A PreKey for the destination recipient, retrieved from a server. * @throws InvalidKeyException when the {@link org.whispersystems.libaxolotl.state.PreKey} is * badly formatted. + * @throws org.whispersystems.libaxolotl.UntrustedIdentityException when the sender's + * {@link IdentityKey} is not + * trusted. */ - public void process(PreKey preKey) throws InvalidKeyException { + public void process(PreKey preKey) throws InvalidKeyException, UntrustedIdentityException { + + if (!identityKeyStore.isTrustedIdentity(recipientId, preKey.getIdentityKey())) { + throw new UntrustedIdentityException(); + } + SessionRecord sessionRecord = sessionStore.load(recipientId, deviceId); ECKeyPair ourBaseKey = Curve.generateKeyPair(true); ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true); @@ -168,43 +180,34 @@ public class SessionBuilder { * @return The KeyExchangeMessage to respond with, or null if no response is necessary. * @throws InvalidKeyException if the received KeyExchangeMessage is badly formatted. */ - public KeyExchangeMessage process(KeyExchangeMessage message) throws InvalidKeyException { + public KeyExchangeMessage process(KeyExchangeMessage message) + throws InvalidKeyException, UntrustedIdentityException, StaleKeyExchangeException + { + + if (!identityKeyStore.isTrustedIdentity(recipientId, message.getIdentityKey())) { + throw new UntrustedIdentityException(); + } + KeyExchangeMessage responseMessage = null; SessionRecord sessionRecord = sessionStore.load(recipientId, deviceId); Log.w(TAG, "Received key exchange with sequence: " + message.getSequence()); if (message.isInitiate()) { - ECKeyPair ourBaseKey, ourEphemeralKey; - IdentityKeyPair ourIdentityKey; - - int flags = KeyExchangeMessage.RESPONSE_FLAG; - Log.w(TAG, "KeyExchange is an initiate."); + responseMessage = processInitiate(sessionRecord, message); + } - if (!sessionRecord.getSessionState().hasPendingKeyExchange()) { - Log.w(TAG, "We don't have a pending initiate..."); - ourBaseKey = Curve.generateKeyPair(true); - ourEphemeralKey = Curve.generateKeyPair(true); - ourIdentityKey = identityKeyStore.getIdentityKeyPair(); + if (message.isResponse()) { + SessionState sessionState = sessionRecord.getSessionState(); + boolean hasPendingKeyExchange = sessionState.hasPendingKeyExchange(); + boolean isSimultaneousInitiateResponse = message.isResponseForSimultaneousInitiate(); - sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey, - ourEphemeralKey, ourIdentityKey); - } else { - Log.w(TAG, "We already have a pending initiate, responding as simultaneous initiate..."); - ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey(); - ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey(); - ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey(); - flags |= KeyExchangeMessage.SIMULTAENOUS_INITIATE_FLAG; - - sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey, - ourEphemeralKey, ourIdentityKey); + if ((!hasPendingKeyExchange || sessionState.getPendingKeyExchangeSequence() != message.getSequence()) && + !isSimultaneousInitiateResponse) + { + throw new StaleKeyExchangeException(); } - - responseMessage = new KeyExchangeMessage(message.getSequence(), - flags, ourBaseKey.getPublicKey(), - ourEphemeralKey.getPublicKey(), - ourIdentityKey.getPublicKey()); } if (message.getSequence() != sessionRecord.getSessionState().getPendingKeyExchangeSequence()) { @@ -232,6 +235,39 @@ public class SessionBuilder { return responseMessage; } + private KeyExchangeMessage processInitiate(SessionRecord sessionRecord, KeyExchangeMessage message) + throws InvalidKeyException + { + ECKeyPair ourBaseKey, ourEphemeralKey; + IdentityKeyPair ourIdentityKey; + + int flags = KeyExchangeMessage.RESPONSE_FLAG; + + if (!sessionRecord.getSessionState().hasPendingKeyExchange()) { + Log.w(TAG, "We don't have a pending initiate..."); + ourBaseKey = Curve.generateKeyPair(true); + ourEphemeralKey = Curve.generateKeyPair(true); + ourIdentityKey = identityKeyStore.getIdentityKeyPair(); + + sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey, + ourEphemeralKey, ourIdentityKey); + } else { + Log.w(TAG, "We already have a pending initiate, responding as simultaneous initiate..."); + ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey(); + ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey(); + ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey(); + flags |= KeyExchangeMessage.SIMULTAENOUS_INITIATE_FLAG; + + sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey, + ourEphemeralKey, ourIdentityKey); + } + + return new KeyExchangeMessage(message.getSequence(), + flags, ourBaseKey.getPublicKey(), + ourEphemeralKey.getPublicKey(), + ourIdentityKey.getPublicKey()); + } + /** * Initiate a new session by sending an initial KeyExchangeMessage to the recipient. * diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/StaleKeyExchangeException.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/StaleKeyExchangeException.java new file mode 100644 index 0000000000..f94a30087c --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/StaleKeyExchangeException.java @@ -0,0 +1,4 @@ +package org.whispersystems.libaxolotl; + +public class StaleKeyExchangeException extends Throwable { +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/UntrustedIdentityException.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/UntrustedIdentityException.java new file mode 100644 index 0000000000..f434b92301 --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/UntrustedIdentityException.java @@ -0,0 +1,4 @@ +package org.whispersystems.libaxolotl; + +public class UntrustedIdentityException extends Exception { +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/IdentityKeyStore.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/IdentityKeyStore.java index 4b6d5e60a6..d2024f7836 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/IdentityKeyStore.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/IdentityKeyStore.java @@ -37,4 +37,21 @@ public interface IdentityKeyStore { */ public void saveIdentity(long recipientId, IdentityKey identityKey); + + /** + * Verify a remote client's identity key. + *

+ * Determine whether a remote client's identity is trusted. Convention is + * that the TextSecure protocol is 'trust on first use.' This means that + * an identity key is considered 'trusted' if there is no entry for the recipient + * in the local store, or if it matches the saved key for a recipient in the local + * store. Only if it mismatches an entry in the local store is it considered + * 'untrusted.' + * + * @param recipientId The recipient ID of the remote client. + * @param identityKey The identity key to verify. + * @return true if trusted, false if untrusted. + */ + public boolean isTrustedIdentity(long recipientId, IdentityKey identityKey); + } diff --git a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java index dc63a4f3bf..37ebcebf1b 100644 --- a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java +++ b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java @@ -29,9 +29,11 @@ import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.TextView; +import android.widget.Toast; import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; +import org.thoughtcrime.securesms.crypto.TextSecureIdentityKeyStore; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SendReceiveService; @@ -43,9 +45,12 @@ import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.StaleKeyExchangeException; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; +import org.whispersystems.libaxolotl.state.IdentityKeyStore; import org.whispersystems.textsecure.crypto.IdentityKeyParcelable; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.push.IncomingPushMessage; @@ -91,14 +96,8 @@ public class ReceiveKeyActivity extends Activity { try { initializeKey(); initializeText(); - } catch (InvalidKeyException ike) { + } catch (InvalidKeyException | InvalidVersionException | InvalidMessageException | LegacyMessageException ike) { Log.w("ReceiveKeyActivity", ike); - } catch (InvalidVersionException ive) { - Log.w("ReceiveKeyActivity", ive); - } catch (InvalidMessageException e) { - Log.w("ReceiveKeyActivity", e); - } catch (LegacyMessageException e) { - Log.w("ReceiveKeyActivity", e); } initializeListeners(); } @@ -147,12 +146,12 @@ public class ReceiveKeyActivity extends Activity { } private boolean isTrusted(KeyExchangeMessage message, PreKeyWhisperMessage messageBundle, IdentityKey identityUpdateMessage) { - RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); - KeyExchangeProcessor processor = new KeyExchangeProcessor(this, masterSecret, recipientDevice); + long recipientId = recipient.getRecipientId(); + IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(this, masterSecret); - if (message != null) return processor.isTrusted(message); - else if (messageBundle != null) return processor.isTrusted(messageBundle); - else if (identityUpdateMessage != null) return processor.isTrusted(identityUpdateMessage); + if (message != null) return identityKeyStore.isTrustedIdentity(recipientId, message.getIdentityKey()); + else if (messageBundle != null) return identityKeyStore.isTrustedIdentity(recipientId, messageBundle.getIdentityKey()); + else if (identityUpdateMessage != null) return identityKeyStore.isTrustedIdentity(recipientId, identityUpdateMessage); return false; } @@ -224,6 +223,10 @@ public class ReceiveKeyActivity extends Activity { KeyExchangeProcessor processor = new KeyExchangeProcessor(ReceiveKeyActivity.this, masterSecret, recipientDevice); + IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(ReceiveKeyActivity.this, + masterSecret); + identityKeyStore.saveIdentity(recipient.getRecipientId(), keyExchangeMessage.getIdentityKey()); + processor.processKeyExchangeMessage(keyExchangeMessage, threadId); DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) @@ -232,12 +235,21 @@ public class ReceiveKeyActivity extends Activity { Log.w("ReceiveKeyActivity", e); DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) .markAsCorruptKeyExchange(messageId); + } catch (StaleKeyExchangeException e) { + DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) + .markAsStaleKeyExchange(messageId); + } catch (UntrustedIdentityException e) { + throw new AssertionError(e); } } else if (keyExchangeMessageBundle != null) { try { RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); KeyExchangeProcessor processor = new KeyExchangeProcessor(ReceiveKeyActivity.this, - masterSecret, recipientDevice); + masterSecret, recipientDevice); + IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(ReceiveKeyActivity.this, + masterSecret); + + identityKeyStore.saveIdentity(recipient.getRecipientId(), keyExchangeMessageBundle.getIdentityKey()); processor.processKeyExchangeMessage(keyExchangeMessageBundle); CiphertextMessage bundledMessage = keyExchangeMessageBundle.getWhisperMessage(); @@ -264,18 +276,13 @@ public class ReceiveKeyActivity extends Activity { threadId, recipient.getNumber(), recipientDeviceId, messageBody, true, false, false); } - } catch (InvalidKeyIdException e) { + } catch (InvalidKeyIdException | InvalidNumberException | InvalidKeyException e) { Log.w("ReceiveKeyActivity", e); DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) .markAsCorruptKeyExchange(messageId); - } catch (InvalidKeyException e) { + } catch (UntrustedIdentityException e) { Log.w("ReceiveKeyActivity", e); - DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) - .markAsCorruptKeyExchange(messageId); - } catch (InvalidNumberException e) { - Log.w("ReceiveKeyActivity", e); - DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) - .markAsCorruptKeyExchange(messageId); + Toast.makeText(ReceiveKeyActivity.this, "Untrusted!", Toast.LENGTH_LONG).show(); } } else if (identityUpdateMessage != null) { DatabaseFactory.getIdentityDatabase(ReceiveKeyActivity.this) diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index 50d2f149da..435b11ae74 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -45,6 +45,8 @@ import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.StaleKeyExchangeException; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage; import org.whispersystems.libaxolotl.protocol.WhisperMessage; import org.whispersystems.libaxolotl.state.SessionStore; @@ -433,12 +435,8 @@ public class DecryptingQueue { KeyExchangeMessage message = new KeyExchangeMessage(Base64.decodeWithoutPadding(plaintextBody)); KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipientDevice); - if (processor.isStale(message)) { - DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId); - } else if (processor.isTrusted(message)) { - DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId); - processor.processKeyExchangeMessage(message, threadId); - } + processor.processKeyExchangeMessage(message, threadId); + DatabaseFactory.getEncryptingSmsDatabase(context).markAsProcessedKeyExchange(messageId); } catch (InvalidVersionException e) { Log.w("DecryptingQueue", e); DatabaseFactory.getEncryptingSmsDatabase(context).markAsInvalidVersionKeyExchange(messageId); @@ -448,6 +446,11 @@ public class DecryptingQueue { } catch (LegacyMessageException e) { Log.w("DecryptingQueue", e); DatabaseFactory.getEncryptingSmsDatabase(context).markAsLegacyVersion(messageId); + } catch (StaleKeyExchangeException e) { + Log.w("DecryptingQueue", e); + DatabaseFactory.getEncryptingSmsDatabase(context).markAsStaleKeyExchange(messageId); + } catch (UntrustedIdentityException e) { + Log.w("DecryptingQueue", e); } } } diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java index c0577d5322..5ce12a56bc 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java @@ -3,22 +3,20 @@ package org.thoughtcrime.securesms.crypto; import android.content.Context; import android.content.Intent; -import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.PreKeyService; -import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; -import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidKeyIdException; import org.whispersystems.libaxolotl.SessionBuilder; +import org.whispersystems.libaxolotl.StaleKeyExchangeException; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.state.IdentityKeyStore; import org.whispersystems.libaxolotl.state.PreKeyStore; -import org.whispersystems.libaxolotl.state.SessionRecord; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.push.PreKeyEntity; @@ -41,7 +39,6 @@ public class KeyExchangeProcessor { private RecipientDevice recipientDevice; private MasterSecret masterSecret; private SessionBuilder sessionBuilder; - private SessionStore sessionStore; public KeyExchangeProcessor(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice) { @@ -51,51 +48,22 @@ public class KeyExchangeProcessor { IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret); PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); - this.sessionStore = new TextSecureSessionStore(context, masterSecret); this.sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, identityKeyStore, recipientDevice.getRecipientId(), recipientDevice.getDeviceId()); } - public boolean isTrusted(PreKeyWhisperMessage message) { - return isTrusted(message.getIdentityKey()); - } - - public boolean isTrusted(PreKeyEntity entity) { - return isTrusted(entity.getIdentityKey()); - } - - public boolean isTrusted(KeyExchangeMessage message) { - return message.hasIdentityKey() && isTrusted(message.getIdentityKey()); - } - - public boolean isTrusted(IdentityKey identityKey) { - return DatabaseFactory.getIdentityDatabase(context).isValidIdentity(masterSecret, - recipientDevice.getRecipientId(), - identityKey); - } - - public boolean isStale(KeyExchangeMessage message) { - SessionRecord sessionRecord = sessionStore.load(recipientDevice.getRecipientId(), - recipientDevice.getDeviceId()); - - return - message.isResponse() && - (!sessionRecord.getSessionState().hasPendingKeyExchange() || - sessionRecord.getSessionState().getPendingKeyExchangeSequence() != message.getSequence()) && - !message.isResponseForSimultaneousInitiate(); - } - public void processKeyExchangeMessage(PreKeyWhisperMessage message) - throws InvalidKeyIdException, InvalidKeyException + throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException { sessionBuilder.process(message); PreKeyService.initiateRefresh(context, masterSecret); } public void processKeyExchangeMessage(PreKeyEntity message, long threadId) - throws InvalidKeyException + throws InvalidKeyException, UntrustedIdentityException { sessionBuilder.process(message); @@ -104,24 +72,25 @@ public class KeyExchangeProcessor { } } - public void processKeyExchangeMessage(KeyExchangeMessage message, long threadId) - throws InvalidKeyException + public OutgoingKeyExchangeMessage processKeyExchangeMessage(KeyExchangeMessage message, long threadId) + throws InvalidKeyException, UntrustedIdentityException, StaleKeyExchangeException { KeyExchangeMessage responseMessage = sessionBuilder.process(message); - Recipient recipient = RecipientFactory.getRecipientsForIds(context, + Recipient recipient = RecipientFactory.getRecipientsForIds(context, String.valueOf(recipientDevice.getRecipientId()), false) .getPrimaryRecipient(); - if (responseMessage != null) { - String serializedResponse = Base64.encodeBytesWithoutPadding(responseMessage.serialize()); - OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, serializedResponse); - MessageSender.send(context, masterSecret, textMessage, threadId, true); - } - DecryptingQueue.scheduleRogueMessages(context, masterSecret, recipient); broadcastSecurityUpdateEvent(context, threadId); + + if (responseMessage != null) { + String serializedResponse = Base64.encodeBytesWithoutPadding(responseMessage.serialize()); + return new OutgoingKeyExchangeMessage(recipient, serializedResponse); + } else { + return null; + } } public static void broadcastSecurityUpdateEvent(Context context, long threadId) { diff --git a/src/org/thoughtcrime/securesms/crypto/TextSecureIdentityKeyStore.java b/src/org/thoughtcrime/securesms/crypto/TextSecureIdentityKeyStore.java index e870c02a8c..f2a5f18855 100644 --- a/src/org/thoughtcrime/securesms/crypto/TextSecureIdentityKeyStore.java +++ b/src/org/thoughtcrime/securesms/crypto/TextSecureIdentityKeyStore.java @@ -33,4 +33,10 @@ public class TextSecureIdentityKeyStore implements IdentityKeyStore { public void saveIdentity(long recipientId, IdentityKey identityKey) { DatabaseFactory.getIdentityDatabase(context).saveIdentity(masterSecret, recipientId, identityKey); } + + @Override + public boolean isTrustedIdentity(long recipientId, IdentityKey identityKey) { + return DatabaseFactory.getIdentityDatabase(context) + .isValidIdentity(masterSecret, recipientId, identityKey); + } } diff --git a/src/org/thoughtcrime/securesms/service/PushReceiver.java b/src/org/thoughtcrime/securesms/service/PushReceiver.java index b202310f08..81f8d073df 100644 --- a/src/org/thoughtcrime/securesms/service/PushReceiver.java +++ b/src/org/thoughtcrime/securesms/service/PushReceiver.java @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; @@ -117,15 +118,16 @@ public class PushReceiver { KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipientDevice); PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody()); - if (processor.isTrusted(preKeyExchange)) { + try { processor.processKeyExchangeMessage(preKeyExchange); IncomingPushMessage bundledMessage = message.withBody(preKeyExchange.getWhisperMessage().serialize()); handleReceivedSecureMessage(masterSecret, bundledMessage); - } else { - String encoded = Base64.encodeBytes(message.getBody()); - IncomingTextMessage textMessage = new IncomingTextMessage(message, encoded, null); - IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded); + } catch (UntrustedIdentityException uie) { + Log.w("PushReceiver", uie); + String encoded = Base64.encodeBytes(message.getBody()); + IncomingTextMessage textMessage = new IncomingTextMessage(message, encoded, null); + IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded); EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); Pair messageAndThreadId = database.insertMessageInbox(masterSecret, bundleMessage); diff --git a/src/org/thoughtcrime/securesms/service/SmsReceiver.java b/src/org/thoughtcrime/securesms/service/SmsReceiver.java index b67cf2db17..25a55992b1 100644 --- a/src/org/thoughtcrime/securesms/service/SmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/SmsReceiver.java @@ -32,17 +32,22 @@ import org.thoughtcrime.securesms.protocol.WirePrefix; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; +import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; +import org.thoughtcrime.securesms.sms.MessageSender; import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler; +import org.thoughtcrime.securesms.sms.OutgoingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.StaleKeyExchangeException; +import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.protocol.WhisperMessage; @@ -119,22 +124,20 @@ public class SmsReceiver { byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes()); PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(rawMessage); - if (processor.isTrusted(preKeyExchange)) { - processor.processKeyExchangeMessage(preKeyExchange); + processor.processKeyExchangeMessage(preKeyExchange); - WhisperMessage ciphertextMessage = preKeyExchange.getWhisperMessage(); - String bundledMessageBody = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); - IncomingEncryptedMessage bundledMessage = new IncomingEncryptedMessage(message, bundledMessageBody); - Pair messageAndThreadId = storeSecureMessage(masterSecret, bundledMessage); + WhisperMessage ciphertextMessage = preKeyExchange.getWhisperMessage(); + String bundledMessageBody = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); + IncomingEncryptedMessage bundledMessage = new IncomingEncryptedMessage(message, bundledMessageBody); + Pair messageAndThreadId = storeSecureMessage(masterSecret, bundledMessage); - Intent intent = new Intent(KeyExchangeProcessor.SECURITY_UPDATE_EVENT); - intent.putExtra("thread_id", messageAndThreadId.second); - intent.setPackage(context.getPackageName()); - context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION); + Intent intent = new Intent(KeyExchangeProcessor.SECURITY_UPDATE_EVENT); + intent.putExtra("thread_id", messageAndThreadId.second); + intent.setPackage(context.getPackageName()); + context.sendBroadcast(intent, KeyCachingService.KEY_PERMISSION); - return messageAndThreadId; - } - } catch (InvalidKeyException e) { + return messageAndThreadId; + } catch (InvalidKeyException | RecipientFormattingException | InvalidMessageException | IOException e) { Log.w("SmsReceiver", e); message.setCorrupted(true); } catch (InvalidVersionException e) { @@ -143,15 +146,8 @@ public class SmsReceiver { } catch (InvalidKeyIdException e) { Log.w("SmsReceiver", e); message.setStale(true); - } catch (IOException e) { - Log.w("SmsReceive", e); - message.setCorrupted(true); - } catch (InvalidMessageException e) { + } catch (UntrustedIdentityException e) { Log.w("SmsReceiver", e); - message.setCorrupted(true); - } catch (RecipientFormattingException e) { - Log.w("SmsReceiver", e); - message.setCorrupted(true); } return storeStandardMessage(masterSecret, message); @@ -166,17 +162,18 @@ public class SmsReceiver { RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSenderDeviceId()); KeyExchangeMessage exchangeMessage = new KeyExchangeMessage(Base64.decodeWithoutPadding(message.getMessageBody())); KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipientDevice); + long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(new Recipients(recipient)); + OutgoingKeyExchangeMessage response = processor.processKeyExchangeMessage(exchangeMessage, threadId); - if (processor.isStale(exchangeMessage)) { - message.setStale(true); - } else if (processor.isTrusted(exchangeMessage)) { - message.setProcessed(true); + message.setProcessed(true); - Pair messageAndThreadId = storeStandardMessage(masterSecret, message); - processor.processKeyExchangeMessage(exchangeMessage, messageAndThreadId.second); + Pair messageAndThreadId = storeStandardMessage(masterSecret, message); - return messageAndThreadId; + if (response != null) { + MessageSender.send(context, masterSecret, response, messageAndThreadId.second, true); } + + return messageAndThreadId; } catch (InvalidVersionException e) { Log.w("SmsReceiver", e); message.setInvalidVersion(true); @@ -186,6 +183,11 @@ public class SmsReceiver { } catch (LegacyMessageException e) { Log.w("SmsReceiver", e); message.setLegacyVersion(true); + } catch (StaleKeyExchangeException e) { + Log.w("SmsReceiver", e); + message.setStale(true); + } catch (UntrustedIdentityException e) { + Log.w("SmsReceiver", e); } } diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index 39cb282286..f62f215a66 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -213,11 +213,12 @@ public class PushTransport extends BaseTransport { PreKeyEntity preKey = socket.getPreKey(address); KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, address); - if (processor.isTrusted(preKey)) { + try { processor.processKeyExchangeMessage(preKey, threadId); - } else { + } catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) { throw new UntrustedIdentityException("Untrusted identity key!", e164number, preKey.getIdentityKey()); } + } } catch (InvalidKeyException e) { throw new IOException(e); @@ -332,9 +333,9 @@ public class PushTransport extends BaseTransport { PushAddress device = PushAddress.create(context, pushAddress.getRecipientId(), pushAddress.getNumber(), preKey.getDeviceId()); KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, device); - if (processor.isTrusted(preKey)) { + try { processor.processKeyExchangeMessage(preKey, threadId); - } else { + } catch (org.whispersystems.libaxolotl.UntrustedIdentityException e) { throw new UntrustedIdentityException("Untrusted identity key!", pushAddress.getNumber(), preKey.getIdentityKey()); } }