diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/NoSessionException.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/NoSessionException.java new file mode 100644 index 0000000000..246c5b025f --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/NoSessionException.java @@ -0,0 +1,7 @@ +package org.whispersystems.libaxolotl; + +public class NoSessionException extends Exception { + public NoSessionException(String s) { + super(s); + } +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java index 5b0b362187..8a02b4b53c 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionBuilder.java @@ -89,31 +89,35 @@ public class SessionBuilder { * @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) + /*package*/ boolean process(PreKeyWhisperMessage message) throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException { int messageVersion = message.getMessageVersion(); IdentityKey theirIdentityKey = message.getIdentityKey(); + boolean createdSession; + if (!identityKeyStore.isTrustedIdentity(recipientId, theirIdentityKey)) { throw new UntrustedIdentityException(); } - if (messageVersion == 2) processV2(message); - else if (messageVersion == 3) processV3(message); + if (messageVersion == 2) createdSession = processV2(message); + else if (messageVersion == 3) createdSession = processV3(message); else throw new AssertionError("Unknown version: " + messageVersion); identityKeyStore.saveIdentity(recipientId, theirIdentityKey); + + return createdSession; } - private void processV3(PreKeyWhisperMessage message) + private boolean processV3(PreKeyWhisperMessage message) throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException { SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); if (sessionRecord.hasSessionState(message.getMessageVersion(), message.getBaseKey().serialize())) { Log.w(TAG, "We've already setup a session for this V3 message, letting bundled message fall through..."); - return; + return false; } boolean simultaneousInitiate = sessionRecord.getSessionState().hasUnacknowledgedPreKeyMessage(); @@ -152,9 +156,11 @@ public class SessionBuilder { if (message.getPreKeyId() >= 0 && message.getPreKeyId() != Medium.MAX_VALUE) { preKeyStore.removePreKey(message.getPreKeyId()); } + + return true; } - private void processV2(PreKeyWhisperMessage message) + private boolean processV2(PreKeyWhisperMessage message) throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException { @@ -162,7 +168,7 @@ public class SessionBuilder { sessionStore.containsSession(recipientId, deviceId)) { Log.w(TAG, "We've already processed the prekey part of this V2 session, letting bundled message fall through..."); - return; + return false; } SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); @@ -194,6 +200,8 @@ public class SessionBuilder { if (message.getPreKeyId() != Medium.MAX_VALUE) { preKeyStore.removePreKey(message.getPreKeyId()); } + + return true; } /** diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java index faaf87d710..a146266f9f 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java @@ -25,9 +25,12 @@ import org.whispersystems.libaxolotl.protocol.WhisperMessage; import org.whispersystems.libaxolotl.ratchet.ChainKey; import org.whispersystems.libaxolotl.ratchet.MessageKeys; import org.whispersystems.libaxolotl.ratchet.RootKey; +import org.whispersystems.libaxolotl.state.IdentityKeyStore; +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.state.SignedPreKeyStore; import org.whispersystems.libaxolotl.util.ByteUtil; import org.whispersystems.libaxolotl.util.Pair; @@ -58,9 +61,10 @@ public class SessionCipher { private static final Object SESSION_LOCK = new Object(); - private final SessionStore sessionStore; - private final long recipientId; - private final int deviceId; + private final SessionStore sessionStore; + private final SessionBuilder sessionBuilder; + private final long recipientId; + private final int deviceId; /** * Construct a SessionCipher for encrypt/decrypt operations on a session. @@ -71,10 +75,15 @@ public class SessionCipher { * @param recipientId The remote ID that messages will be encrypted to or decrypted from. * @param deviceId The device corresponding to the recipientId. */ - public SessionCipher(SessionStore sessionStore, long recipientId, int deviceId) { - this.sessionStore = sessionStore; - this.recipientId = recipientId; - this.deviceId = deviceId; + public SessionCipher(SessionStore sessionStore, PreKeyStore preKeyStore, + SignedPreKeyStore signedPreKeyStore, IdentityKeyStore identityKeyStore, + long recipientId, int deviceId) + { + this.sessionStore = sessionStore; + this.recipientId = recipientId; + this.deviceId = deviceId; + this.sessionBuilder = new SessionBuilder(sessionStore, preKeyStore, signedPreKeyStore, + identityKeyStore, recipientId, deviceId); } /** @@ -115,6 +124,26 @@ public class SessionCipher { } } + public byte[] decrypt(PreKeyWhisperMessage ciphertext) + throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, + InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException, NoSessionException + { + synchronized (SESSION_LOCK) { + boolean sessionCreated = sessionBuilder.process(ciphertext); + + try { + return decrypt(ciphertext.getWhisperMessage()); + } catch (InvalidMessageException | DuplicateMessageException | LegacyMessageException e) { + if (sessionCreated) { + sessionStore.deleteSession(recipientId, deviceId); + } + + throw e; + } + } + } + + /** * Decrypt a message. * @@ -126,10 +155,15 @@ public class SessionCipher { * @throws LegacyMessageException if the input is a message formatted by a protocol version that * is no longer supported. */ - public byte[] decrypt(byte[] ciphertext) - throws InvalidMessageException, DuplicateMessageException, LegacyMessageException + public byte[] decrypt(WhisperMessage ciphertext) + throws InvalidMessageException, DuplicateMessageException, LegacyMessageException, NoSessionException { synchronized (SESSION_LOCK) { + + if (!sessionStore.containsSession(recipientId, deviceId)) { + throw new NoSessionException("No session for: " + recipientId + ", " + deviceId); + } + SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId); SessionState sessionState = sessionRecord.getSessionState(); List previousStates = sessionRecord.getPreviousSessionStates(); @@ -159,15 +193,13 @@ public class SessionCipher { } } - private byte[] decrypt(SessionState sessionState, byte[] decodedMessage) + private byte[] decrypt(SessionState sessionState, WhisperMessage ciphertextMessage) throws InvalidMessageException, DuplicateMessageException, LegacyMessageException { if (!sessionState.hasSenderChain()) { throw new InvalidMessageException("Uninitialized session!"); } - WhisperMessage ciphertextMessage = new WhisperMessage(decodedMessage); - if (ciphertextMessage.getMessageVersion() != sessionState.getSessionVersion()) { throw new InvalidMessageException(String.format("Message version %d, but session version %d", ciphertextMessage.getMessageVersion(), diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java deleted file mode 100644 index c97398a2f8..0000000000 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Copyright (C) 2013 Open Whisper Systems - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.whispersystems.textsecure.crypto; - - -import android.content.Context; - -import org.whispersystems.libaxolotl.SessionCipher; -import org.whispersystems.libaxolotl.state.SessionStore; -import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.TextSecureSessionStore; - -public class SessionCipherFactory { - - public static SessionCipher getInstance(Context context, - MasterSecret masterSecret, - RecipientDevice recipient) - { - SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); - - if (sessionStore.containsSession(recipient.getRecipientId(), recipient.getDeviceId())) { - return new SessionCipher(sessionStore, recipient.getRecipientId(), recipient.getDeviceId()); - } else { - throw new AssertionError("Attempt to initialize cipher for non-existing session."); - } - } -} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java index 37ebcebf1b..41797d7d61 100644 --- a/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java +++ b/src/org/thoughtcrime/securesms/ReceiveKeyActivity.java @@ -18,6 +18,7 @@ package org.thoughtcrime.securesms; import android.app.Activity; import android.app.ProgressDialog; +import android.content.Context; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -33,18 +34,24 @@ import android.widget.Toast; import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; +import org.thoughtcrime.securesms.crypto.TextSecureCipher; import org.thoughtcrime.securesms.crypto.TextSecureIdentityKeyStore; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.service.SendReceiveService; +import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage; +import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.Util; +import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.StaleKeyExchangeException; import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; @@ -53,8 +60,10 @@ 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.crypto.TransportDetails; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.textsecure.push.PushTransportDetails; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.InvalidNumberException; @@ -243,43 +252,36 @@ public class ReceiveKeyActivity extends Activity { } } else if (keyExchangeMessageBundle != null) { try { - RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); - KeyExchangeProcessor processor = new KeyExchangeProcessor(ReceiveKeyActivity.this, - masterSecret, recipientDevice); + Context context = ReceiveKeyActivity.this; + EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), recipientDeviceId); + + TransportDetails transportDetails = getIntent().getBooleanExtra("is_push", false) ? + new PushTransportDetails(keyExchangeMessageBundle.getMessageVersion()) : + new SmsTransportDetails(); + + TextSecureCipher cipher = new TextSecureCipher(ReceiveKeyActivity.this, masterSecret, recipientDevice, transportDetails); IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(ReceiveKeyActivity.this, masterSecret); identityKeyStore.saveIdentity(recipient.getRecipientId(), keyExchangeMessageBundle.getIdentityKey()); - processor.processKeyExchangeMessage(keyExchangeMessageBundle); + byte[] plaintext = cipher.decrypt(keyExchangeMessageBundle); - CiphertextMessage bundledMessage = keyExchangeMessageBundle.getWhisperMessage(); + database.updateBundleMessageBody(masterSecret, messageId, ""); + database.updateMessageBody(masterSecret, messageId, new String(plaintext)); - if (getIntent().getBooleanExtra("is_push", false)) { - String source = Util.canonicalizeNumber(ReceiveKeyActivity.this, recipient.getNumber()); - IncomingPushMessage incoming = new IncomingPushMessage(Type.CIPHERTEXT_VALUE, source, recipientDeviceId, bundledMessage.serialize(), System.currentTimeMillis()); - - DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) - .markAsProcessedKeyExchange(messageId); - - Intent intent = new Intent(ReceiveKeyActivity.this, SendReceiveService.class); - intent.setAction(SendReceiveService.RECEIVE_PUSH_ACTION); - intent.putExtra("message", incoming); - startService(intent); - } else { - SmsTransportDetails transportDetails = new SmsTransportDetails(); - String messageBody = new String(transportDetails.getEncodedMessage(bundledMessage.serialize())); - - DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) - .updateBundleMessageBody(masterSecret, messageId, messageBody); - - DecryptingQueue.scheduleDecryption(ReceiveKeyActivity.this, masterSecret, messageId, - threadId, recipient.getNumber(), recipientDeviceId, - messageBody, true, false, false); - } - } catch (InvalidKeyIdException | InvalidNumberException | InvalidKeyException e) { + } catch (InvalidKeyIdException | InvalidKeyException | LegacyMessageException | NoSessionException e) { Log.w("ReceiveKeyActivity", e); DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) .markAsCorruptKeyExchange(messageId); + } catch (InvalidMessageException e) { + Log.w("ReceiveKeyActivity", e); + DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) + .markAsDecryptFailed(messageId); + } catch (DuplicateMessageException e) { + Log.w("ReceiveKeyActivity", e); + DatabaseFactory.getEncryptingSmsDatabase(ReceiveKeyActivity.this) + .markAsDecryptDuplicate(messageId); } catch (UntrustedIdentityException e) { Log.w("ReceiveKeyActivity", e); Toast.makeText(ReceiveKeyActivity.this, "Untrusted!", Toast.LENGTH_LONG).show(); diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index a6997be629..520b119c24 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -44,14 +44,13 @@ import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; -import org.whispersystems.libaxolotl.SessionCipher; +import org.whispersystems.libaxolotl.NoSessionException; 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; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.crypto.SessionCipherFactory; import org.whispersystems.textsecure.crypto.TransportDetails; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.PushTransportDetails; @@ -213,12 +212,12 @@ public class DecryptingQueue { return; } - int sessionVersion = SessionUtil.getSessionVersion(context, masterSecret, recipientDevice); - SessionCipher sessionCipher = SessionCipherFactory.getInstance(context, masterSecret, recipientDevice); - byte[] plaintextBody = sessionCipher.decrypt(message.getBody()); - TransportDetails transport = new PushTransportDetails(sessionVersion); + int sessionVersion = SessionUtil.getSessionVersion(context, masterSecret, recipientDevice); + TransportDetails transportDetails = new PushTransportDetails(sessionVersion); + TextSecureCipher textSecureCipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + byte[] plaintextBody = textSecureCipher.decrypt(new WhisperMessage(message.getBody())); - message = message.withBody(transport.getStrippedPaddingMessageBody(plaintextBody)); + message = message.withBody(plaintextBody); sendResult(PushReceiver.RESULT_OK); } catch (InvalidMessageException | LegacyMessageException | RecipientFormattingException e) { Log.w("DecryptionQueue", e); @@ -226,6 +225,9 @@ public class DecryptingQueue { } catch (DuplicateMessageException e) { Log.w("DecryptingQueue", e); sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE); + } catch (NoSessionException e) { + Log.w("DecryptingQueue", e); + sendResult(PushReceiver.RESULT_NO_SESSION); } } @@ -294,12 +296,12 @@ public class DecryptingQueue { byte[] plaintextPduBytes; Log.w("DecryptingQueue", "Decrypting: " + Hex.toString(ciphertextPduBytes)); - TextTransport transportDetails = new TextTransport(); - SessionCipher sessionCipher = SessionCipherFactory.getInstance(context, masterSecret, recipientDevice); - byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes); + TextTransport transportDetails = new TextTransport(); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + byte[] decodedCiphertext = transportDetails.getDecodedMessage(ciphertextPduBytes); try { - plaintextPduBytes = sessionCipher.decrypt(decodedCiphertext); + plaintextPduBytes = cipher.decrypt(new WhisperMessage(decodedCiphertext)); } catch (InvalidMessageException ime) { // XXX - For some reason, Sprint seems to append a single character to the // end of message text segments. I don't know why, so here we just try @@ -308,7 +310,7 @@ public class DecryptingQueue { Log.w("DecryptingQueue", "Attempting truncated decrypt..."); byte[] truncated = Util.trim(ciphertextPduBytes, ciphertextPduBytes.length - 1); decodedCiphertext = transportDetails.getDecodedMessage(truncated); - plaintextPduBytes = sessionCipher.decrypt(decodedCiphertext); + plaintextPduBytes = cipher.decrypt(new WhisperMessage(decodedCiphertext)); } else { throw ime; } @@ -329,6 +331,9 @@ public class DecryptingQueue { } catch (LegacyMessageException lme) { Log.w("DecryptingQueue", lme); database.markAsLegacyVersion(messageId, threadId); + } catch (NoSessionException nse) { + Log.w("DecryptingQueue", nse); + database.markAsNoSession(messageId, threadId); } } } @@ -382,8 +387,8 @@ public class DecryptingQueue { return; } - SessionCipher sessionCipher = SessionCipherFactory.getInstance(context, masterSecret, recipientDevice); - byte[] paddedPlaintext = sessionCipher.decrypt(decodedCiphertext); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + byte[] paddedPlaintext = cipher.decrypt(new WhisperMessage(decodedCiphertext)); plaintextBody = new String(transportDetails.getStrippedPaddingMessageBody(paddedPlaintext)); @@ -405,6 +410,10 @@ public class DecryptingQueue { Log.w("DecryptionQueue", e); database.markAsDecryptDuplicate(messageId); return; + } catch (NoSessionException e) { + Log.w("DecryptingQueue", e); + database.markAsNoSession(messageId); + return; } database.updateMessageBody(masterSecret, messageId, plaintextBody); diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java index f29e46b874..9eed9697fd 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessor.java @@ -57,13 +57,6 @@ public class KeyExchangeProcessor { recipientDevice.getDeviceId()); } - public void processKeyExchangeMessage(PreKeyWhisperMessage message) - throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException - { - sessionBuilder.process(message); - PreKeyService.initiateRefresh(context, masterSecret); - } - public void processKeyExchangeMessage(PreKeyBundle bundle, long threadId) throws InvalidKeyException, UntrustedIdentityException { diff --git a/src/org/thoughtcrime/securesms/crypto/TextSecureCipher.java b/src/org/thoughtcrime/securesms/crypto/TextSecureCipher.java new file mode 100644 index 0000000000..67b52cf696 --- /dev/null +++ b/src/org/thoughtcrime/securesms/crypto/TextSecureCipher.java @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.crypto; + +import android.content.Context; + +import org.whispersystems.libaxolotl.DuplicateMessageException; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; +import org.whispersystems.libaxolotl.SessionCipher; +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.state.IdentityKeyStore; +import org.whispersystems.libaxolotl.state.PreKeyStore; +import org.whispersystems.libaxolotl.state.SessionStore; +import org.whispersystems.libaxolotl.state.SignedPreKeyStore; +import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.crypto.TransportDetails; +import org.whispersystems.textsecure.storage.RecipientDevice; +import org.whispersystems.textsecure.storage.TextSecurePreKeyStore; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; + +public class TextSecureCipher { + + private final SessionCipher sessionCipher; + private final TransportDetails transportDetails; + + public TextSecureCipher(Context context, MasterSecret masterSecret, + RecipientDevice recipient, TransportDetails transportDetails) + { + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + PreKeyStore preKeyStore = new TextSecurePreKeyStore(context, masterSecret); + SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context, masterSecret); + IdentityKeyStore identityKeyStore = new TextSecureIdentityKeyStore(context, masterSecret); + + this.transportDetails = transportDetails; + this.sessionCipher = new SessionCipher(sessionStore, preKeyStore, signedPreKeyStore, identityKeyStore, + recipient.getRecipientId(), recipient.getDeviceId()); + } + + public CiphertextMessage encrypt(byte[] unpaddedMessage) { + return sessionCipher.encrypt(transportDetails.getPaddedMessageBody(unpaddedMessage)); + } + + public byte[] decrypt(WhisperMessage message) + throws DuplicateMessageException, LegacyMessageException, InvalidMessageException, NoSessionException + { + byte[] paddedMessage = sessionCipher.decrypt(message); + return transportDetails.getStrippedPaddingMessageBody(paddedMessage); + } + + public byte[] decrypt(PreKeyWhisperMessage message) + throws InvalidKeyException, LegacyMessageException, InvalidMessageException, + DuplicateMessageException, InvalidKeyIdException, UntrustedIdentityException, NoSessionException + { + byte[] paddedMessage = sessionCipher.decrypt(message); + return transportDetails.getStrippedPaddingMessageBody(paddedMessage); + } + + public int getRemoteRegistrationId() { + return sessionCipher.getRemoteRegistrationId(); + } + +} diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index e4cf49ba38..efafac79c8 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -281,6 +281,7 @@ public class SmsDatabase extends Database implements MmsSmsColumns { else if (((IncomingKeyExchangeMessage)message).isInvalidVersion()) type |= Types.KEY_EXCHANGE_INVALID_VERSION_BIT; else if (((IncomingKeyExchangeMessage)message).isIdentityUpdate()) type |= Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT; else if (((IncomingKeyExchangeMessage)message).isLegacyVersion()) type |= Types.ENCRYPTION_REMOTE_LEGACY_BIT; + else if (((IncomingKeyExchangeMessage)message).isDuplicate()) type |= Types.ENCRYPTION_REMOTE_DUPLICATE_BIT; else if (((IncomingKeyExchangeMessage)message).isPreKeyBundle()) type |= Types.KEY_EXCHANGE_BUNDLE_BIT; } else if (message.isSecureMessage()) { type |= Types.SECURE_MESSAGE_BIT; diff --git a/src/org/thoughtcrime/securesms/service/PushReceiver.java b/src/org/thoughtcrime/securesms/service/PushReceiver.java index 0fbf3f5bef..586e7cc269 100644 --- a/src/org/thoughtcrime/securesms/service/PushReceiver.java +++ b/src/org/thoughtcrime/securesms/service/PushReceiver.java @@ -9,6 +9,7 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; +import org.thoughtcrime.securesms.crypto.TextSecureCipher; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -24,16 +25,21 @@ import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libaxolotl.DuplicateMessageException; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.crypto.TransportDetails; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.textsecure.push.PushTransportDetails; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Base64; @@ -113,34 +119,39 @@ public class PushReceiver { } try { - Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient(); - RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice()); - KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipientDevice); - PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(message.getBody()); + Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSource(), false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice()); + PreKeyWhisperMessage preKeyWhisperMessage = new PreKeyWhisperMessage(message.getBody()); + TransportDetails transportDetails = new PushTransportDetails(preKeyWhisperMessage.getMessageVersion()); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + byte[] plaintext = cipher.decrypt(preKeyWhisperMessage); - try { - processor.processKeyExchangeMessage(preKeyExchange); + IncomingPushMessage bundledMessage = message.withBody(plaintext); + handleReceivedMessage(masterSecret, bundledMessage, true); - IncomingPushMessage bundledMessage = message.withBody(preKeyExchange.getWhisperMessage().serialize()); - handleReceivedSecureMessage(masterSecret, bundledMessage); - } 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); - - MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); - } } catch (InvalidVersionException e) { Log.w("PushReceiver", e); handleReceivedCorruptedKey(masterSecret, message, true); } catch (InvalidKeyException | InvalidKeyIdException | InvalidMessageException | - RecipientFormattingException e) + RecipientFormattingException | LegacyMessageException e) { Log.w("PushReceiver", e); handleReceivedCorruptedKey(masterSecret, message, false); + } catch (DuplicateMessageException e) { + Log.w("PushReceiver", e); + handleReceivedDuplicateMessage(message); + } catch (NoSessionException e) { + Log.w("PushReceiver", e); + handleReceivedMessageForNoSession(masterSecret, message); + } catch (UntrustedIdentityException e) { + Log.w("PushReceiver", e); + String encoded = Base64.encodeBytes(message.getBody()); + IncomingTextMessage textMessage = new IncomingTextMessage(message, encoded, null); + IncomingPreKeyBundleMessage bundleMessage = new IncomingPreKeyBundleMessage(textMessage, encoded); + Pair messageAndThreadId = DatabaseFactory.getEncryptingSmsDatabase(context) + .insertMessageInbox(masterSecret, bundleMessage); + + MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); } } diff --git a/src/org/thoughtcrime/securesms/service/SmsReceiver.java b/src/org/thoughtcrime/securesms/service/SmsReceiver.java index 25a55992b1..8da158b322 100644 --- a/src/org/thoughtcrime/securesms/service/SmsReceiver.java +++ b/src/org/thoughtcrime/securesms/service/SmsReceiver.java @@ -24,6 +24,7 @@ import android.util.Pair; import org.thoughtcrime.securesms.crypto.DecryptingQueue; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; +import org.thoughtcrime.securesms.crypto.TextSecureCipher; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.EncryptingSmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; @@ -42,10 +43,12 @@ 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.DuplicateMessageException; import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; +import org.whispersystems.libaxolotl.NoSessionException; import org.whispersystems.libaxolotl.StaleKeyExchangeException; import org.whispersystems.libaxolotl.UntrustedIdentityException; import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage; @@ -115,39 +118,47 @@ public class SmsReceiver { IncomingPreKeyBundleMessage message) { Log.w("SmsReceiver", "Processing prekey message..."); + EncryptingSmsDatabase database = DatabaseFactory.getEncryptingSmsDatabase(context); - try { - Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient(); - RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSenderDeviceId()); - KeyExchangeProcessor processor = new KeyExchangeProcessor(context, masterSecret, recipientDevice); - SmsTransportDetails transportDetails = new SmsTransportDetails(); - byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes()); - PreKeyWhisperMessage preKeyExchange = new PreKeyWhisperMessage(rawMessage); + if (masterSecret != null) { + try { + Recipient recipient = RecipientFactory.getRecipientsFromString(context, message.getSender(), false).getPrimaryRecipient(); + RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSenderDeviceId()); + SmsTransportDetails transportDetails = new SmsTransportDetails(); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + byte[] rawMessage = transportDetails.getDecodedMessage(message.getMessageBody().getBytes()); + PreKeyWhisperMessage preKeyWhisperMessage = new PreKeyWhisperMessage(rawMessage); + byte[] plaintext = cipher.decrypt(preKeyWhisperMessage); - processor.processKeyExchangeMessage(preKeyExchange); + IncomingEncryptedMessage bundledMessage = new IncomingEncryptedMessage(message, new String(transportDetails.getEncodedMessage(preKeyWhisperMessage.getWhisperMessage().serialize()))); + Pair messageAndThreadId = database.insertMessageInbox(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); + database.updateMessageBody(masterSecret, messageAndThreadId.first, new String(plaintext)); - 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 | RecipientFormattingException | InvalidMessageException | IOException e) { - Log.w("SmsReceiver", e); - message.setCorrupted(true); - } catch (InvalidVersionException e) { - Log.w("SmsReceiver", e); - message.setInvalidVersion(true); - } catch (InvalidKeyIdException e) { - Log.w("SmsReceiver", e); - message.setStale(true); - } catch (UntrustedIdentityException e) { - Log.w("SmsReceiver", e); + return messageAndThreadId; + } catch (InvalidKeyException | RecipientFormattingException | InvalidMessageException | IOException | NoSessionException e) { + Log.w("SmsReceiver", e); + message.setCorrupted(true); + } catch (InvalidVersionException e) { + Log.w("SmsReceiver", e); + message.setInvalidVersion(true); + } catch (InvalidKeyIdException e) { + Log.w("SmsReceiver", e); + message.setStale(true); + } catch (UntrustedIdentityException e) { + Log.w("SmsReceiver", e); + } catch (DuplicateMessageException e) { + Log.w("SmsReceiver", e); + message.setDuplicate(true); + } catch (LegacyMessageException e) { + Log.w("SmsReceiver", e); + message.setLegacyVersion(true); + } } return storeStandardMessage(masterSecret, message); diff --git a/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java b/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java index e04dd65704..9230898f56 100644 --- a/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java +++ b/src/org/thoughtcrime/securesms/sms/IncomingKeyExchangeMessage.java @@ -7,6 +7,7 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage { private boolean isCorrupted; private boolean isInvalidVersion; private boolean isLegacyVersion; + private boolean isDuplicate; public IncomingKeyExchangeMessage(IncomingTextMessage base, String newBody) { super(base, newBody); @@ -69,6 +70,14 @@ public class IncomingKeyExchangeMessage extends IncomingTextMessage { this.isLegacyVersion = isLegacyVersion; } + public void setDuplicate(boolean isDuplicate) { + this.isDuplicate = isDuplicate; + } + + public boolean isDuplicate() { + return isDuplicate; + } + @Override public boolean isKeyExchange() { return true; diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/transport/MmsTransport.java index 0ad8d8dadc..2a24d59f46 100644 --- a/src/org/thoughtcrime/securesms/transport/MmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/MmsTransport.java @@ -21,6 +21,7 @@ import android.content.Context; import android.telephony.TelephonyManager; import android.util.Log; +import org.thoughtcrime.securesms.crypto.TextSecureCipher; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.mms.ApnUnavailableException; import org.thoughtcrime.securesms.mms.MmsRadio; @@ -33,10 +34,8 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientFactory; import org.thoughtcrime.securesms.recipients.RecipientFormattingException; import org.thoughtcrime.securesms.util.NumberUtil; -import org.whispersystems.libaxolotl.SessionCipher; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.crypto.SessionCipherFactory; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.SessionUtil; import org.whispersystems.textsecure.util.Hex; @@ -180,8 +179,8 @@ public class MmsTransport { throw new InsecureFallbackApprovalException("No session exists for this secure message."); } - SessionCipher sessionCipher = SessionCipherFactory.getInstance(context, masterSecret, recipientDevice); - CiphertextMessage ciphertextMessage = sessionCipher.encrypt(pduBytes); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + CiphertextMessage ciphertextMessage = cipher.encrypt(pduBytes); return transportDetails.getEncodedMessage(ciphertextMessage.serialize()); } catch (RecipientFormattingException e) { diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index d15682616a..c856d9c163 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -23,6 +23,7 @@ import android.util.Log; import com.google.protobuf.ByteString; import org.thoughtcrime.securesms.crypto.KeyExchangeProcessor; +import org.thoughtcrime.securesms.crypto.TextSecureCipher; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsColumns; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; @@ -35,13 +36,12 @@ import org.thoughtcrime.securesms.recipients.Recipients; import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.Util; import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.SessionCipher; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; import org.whispersystems.libaxolotl.state.PreKeyBundle; import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.AttachmentCipher; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.crypto.SessionCipherFactory; +import org.whispersystems.textsecure.crypto.TransportDetails; import org.whispersystems.textsecure.push.MismatchedDevices; import org.whispersystems.textsecure.push.MismatchedDevicesException; import org.whispersystems.textsecure.push.OutgoingPushMessage; @@ -345,10 +345,9 @@ public class PushTransport extends BaseTransport { } } - int sessionVersion = SessionUtil.getSessionVersion(context, masterSecret, pushAddress); - SessionCipher cipher = SessionCipherFactory.getInstance(context, masterSecret, pushAddress); - byte[] paddedPlaintext = new PushTransportDetails(sessionVersion).getPaddedMessageBody(plaintext); - CiphertextMessage message = cipher.encrypt(paddedPlaintext); + TransportDetails transportDetails = new PushTransportDetails(SessionUtil.getSessionVersion(context, masterSecret, pushAddress)); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, pushAddress, transportDetails); + CiphertextMessage message = cipher.encrypt(plaintext); int remoteRegistrationId = cipher.getRemoteRegistrationId(); if (message.getType() == CiphertextMessage.PREKEY_TYPE) { diff --git a/src/org/thoughtcrime/securesms/transport/SmsTransport.java b/src/org/thoughtcrime/securesms/transport/SmsTransport.java index 399f2da0d6..0ee328ef36 100644 --- a/src/org/thoughtcrime/securesms/transport/SmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/SmsTransport.java @@ -22,6 +22,7 @@ import android.content.Context; import android.telephony.SmsManager; import android.util.Log; +import org.thoughtcrime.securesms.crypto.TextSecureCipher; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.sms.MultipartSmsMessageHandler; @@ -30,15 +31,10 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage; import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.NumberUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.whispersystems.libaxolotl.SessionCipher; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; -import org.whispersystems.libaxolotl.state.SessionRecord; -import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.crypto.SessionCipherFactory; import org.whispersystems.textsecure.storage.RecipientDevice; import org.whispersystems.textsecure.storage.SessionUtil; -import org.whispersystems.textsecure.storage.TextSecureSessionStore; import java.util.ArrayList; @@ -183,9 +179,8 @@ public class SmsTransport extends BaseTransport { String body = message.getMessageBody(); SmsTransportDetails transportDetails = new SmsTransportDetails(); - SessionCipher sessionCipher = SessionCipherFactory.getInstance(context, masterSecret, recipientDevice); - byte[] paddedPlaintext = transportDetails.getPaddedMessageBody(body.getBytes()); - CiphertextMessage ciphertextMessage = sessionCipher.encrypt(paddedPlaintext); + TextSecureCipher cipher = new TextSecureCipher(context, masterSecret, recipientDevice, transportDetails); + CiphertextMessage ciphertextMessage = cipher.encrypt(body.getBytes()); String encodedCiphertext = new String(transportDetails.getEncodedMessage(ciphertextMessage.serialize())); if (ciphertextMessage.getType() == CiphertextMessage.PREKEY_TYPE) {