diff --git a/library/src/org/whispersystems/textsecure/crypto/DuplicateMessageException.java b/library/src/org/whispersystems/textsecure/crypto/DuplicateMessageException.java new file mode 100644 index 000000000..0d795f3fc --- /dev/null +++ b/library/src/org/whispersystems/textsecure/crypto/DuplicateMessageException.java @@ -0,0 +1,7 @@ +package org.whispersystems.textsecure.crypto; + +public class DuplicateMessageException extends Exception { + public DuplicateMessageException(String s) { + super(s); + } +} diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java index cac3657e0..6273f8670 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipher.java @@ -29,7 +29,7 @@ public abstract class SessionCipher { protected static final Object SESSION_LOCK = new Object(); public abstract CiphertextMessage encrypt(byte[] paddedMessage); - public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException; + public abstract byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException, DuplicateMessageException; public abstract int getRemoteRegistrationId(); public static SessionCipher createFor(Context context, diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java index 7b5557998..970cfe4c5 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipherV2.java @@ -77,7 +77,9 @@ public class SessionCipherV2 extends SessionCipher { } @Override - public byte[] decrypt(byte[] decodedMessage) throws InvalidMessageException { + public byte[] decrypt(byte[] decodedMessage) + throws InvalidMessageException, DuplicateMessageException + { synchronized (SESSION_LOCK) { SessionRecordV2 sessionRecord = getSessionRecord(); SessionState sessionState = sessionRecord.getSessionState(); @@ -94,6 +96,7 @@ public class SessionCipherV2 extends SessionCipher { for (SessionState previousState : previousStates) { try { + Log.w("SessionCipherV2", "Attempting decrypt on previous state..."); byte[] plaintext = decrypt(previousState, decodedMessage); sessionRecord.save(); @@ -108,7 +111,7 @@ public class SessionCipherV2 extends SessionCipher { } public byte[] decrypt(SessionState sessionState, byte[] decodedMessage) - throws InvalidMessageException + throws InvalidMessageException, DuplicateMessageException { if (!sessionState.hasSenderChain()) { throw new InvalidMessageException("Uninitialized session!"); @@ -167,18 +170,19 @@ public class SessionCipherV2 extends SessionCipher { private MessageKeys getOrCreateMessageKeys(SessionState sessionState, ECPublicKey theirEphemeral, ChainKey chainKey, int counter) - throws InvalidMessageException + throws InvalidMessageException, DuplicateMessageException { if (chainKey.getIndex() > counter) { if (sessionState.hasMessageKeys(theirEphemeral, counter)) { return sessionState.removeMessageKeys(theirEphemeral, counter); } else { - throw new InvalidMessageException("Received message with old counter!"); + throw new DuplicateMessageException("Received message with old counter: " + + chainKey.getIndex() + " , " + counter); } } - if (chainKey.getIndex() - counter > 500) { - throw new InvalidMessageException("Over 500 messages into the future!"); + if (chainKey.getIndex() - counter > 2000) { + throw new InvalidMessageException("Over 2000 messages into the future!"); } while (chainKey.getIndex() < counter) { diff --git a/res/values/strings.xml b/res/values/strings.xml index 04099249c..11a153a33 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -298,6 +298,7 @@ Received updated but unknown identity information. Tap to validate identity. Secure session ended. + Duplicate message. Left the group... diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index 7022903ea..0ac1573b0 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -40,6 +40,7 @@ import org.thoughtcrime.securesms.service.PushReceiver; import org.thoughtcrime.securesms.service.SendReceiveService; import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.textsecure.crypto.DuplicateMessageException; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidMessageException; import org.whispersystems.textsecure.crypto.InvalidVersionException; @@ -213,6 +214,9 @@ public class DecryptingQueue { } catch (RecipientFormattingException e) { Log.w("DecryptionQueue", e); sendResult(PushReceiver.RESULT_DECRYPT_FAILED); + } catch (DuplicateMessageException e) { + Log.w("DecryptingQueue", e); + sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE); } } @@ -312,6 +316,9 @@ public class DecryptingQueue { } catch (InvalidMessageException ime) { Log.w("DecryptingQueue", ime); database.markAsDecryptFailed(messageId, threadId); + } catch (DuplicateMessageException dme) { + Log.w("DecryptingQueue", dme); + database.markAsDecryptDuplicate(messageId, threadId); } catch (MmsException mme) { Log.w("DecryptingQueue", mme); database.markAsDecryptFailed(messageId, threadId); @@ -391,6 +398,10 @@ public class DecryptingQueue { Log.w("DecryptionQueue", e); database.markAsDecryptFailed(messageId); return; + } catch (DuplicateMessageException e) { + Log.w("DecryptionQueue", e); + database.markAsDecryptDuplicate(messageId); + return; } database.updateMessageBody(masterSecret, messageId, plaintextBody); diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java index ee9659721..b2771b253 100644 --- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -347,6 +347,11 @@ public class MmsDatabase extends Database implements MmsSmsColumns { notifyConversationListeners(threadId); } + public void markAsDecryptDuplicate(long messageId, long threadId) { + updateMailboxBitmask(messageId, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_DUPLICATE_BIT); + notifyConversationListeners(threadId); + } + public void setMessagesRead(long threadId) { SQLiteDatabase database = databaseHelper.getWritableDatabase(); ContentValues contentValues = new ContentValues(); diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 81688973a..540ee73f9 100644 --- a/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/src/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -57,6 +57,7 @@ public interface MmsSmsColumns { protected static final long ENCRYPTION_REMOTE_BIT = 0x20000000; protected static final long ENCRYPTION_REMOTE_FAILED_BIT = 0x10000000; protected static final long ENCRYPTION_REMOTE_NO_SESSION_BIT = 0x08000000; + protected static final long ENCRYPTION_REMOTE_DUPLICATE_BIT = 0x04000000; public static boolean isFailedMessageType(long type) { return (type & BASE_TYPE_MASK) == BASE_SENT_FAILED_TYPE; @@ -145,6 +146,10 @@ public interface MmsSmsColumns { return (type & ENCRYPTION_REMOTE_FAILED_BIT) != 0; } + public static boolean isDuplicateMessageType(long type) { + return (type & ENCRYPTION_REMOTE_DUPLICATE_BIT) != 0; + } + public static boolean isDecryptInProgressType(long type) { return (type & ENCRYPTION_REMOTE_BIT) != 0 || diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java index 205e2dbaf..0a126747c 100644 --- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -186,6 +186,10 @@ public class SmsDatabase extends Database implements MmsSmsColumns { updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_FAILED_BIT); } + public void markAsDecryptDuplicate(long id) { + updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_DUPLICATE_BIT); + } + public void markAsNoSession(long id) { updateTypeBitmask(id, Types.ENCRYPTION_MASK, Types.ENCRYPTION_REMOTE_NO_SESSION_BIT); } diff --git a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index f600d2ae8..1fd4b4056 100644 --- a/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -74,6 +74,8 @@ public class MediaMmsMessageRecord extends MessageRecord { return emphasisAdded(context.getString(R.string.MmsMessageRecord_decrypting_mms_please_wait)); } else if (MmsDatabase.Types.isFailedDecryptType(type)) { return emphasisAdded(context.getString(R.string.MmsMessageRecord_bad_encrypted_mms_message)); + } else if (MmsDatabase.Types.isDuplicateMessageType(type)) { + return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); } else if (MmsDatabase.Types.isNoRemoteSessionType(type)) { return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session)); } else if (!getBody().isPlaintext()) { diff --git a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 2bf7f752d..0e1891314 100644 --- a/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/src/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -73,6 +73,8 @@ public class SmsMessageRecord extends MessageRecord { return emphasisAdded(context.getString(R.string.ConversationItem_received_key_exchange_message_click_to_process)); } else if (SmsDatabase.Types.isFailedDecryptType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_bad_encrypted_message)); + } else if (SmsDatabase.Types.isDuplicateMessageType(type)) { + return emphasisAdded(context.getString(R.string.SmsMessageRecord_duplicate_message)); } else if (SmsDatabase.Types.isDecryptInProgressType(type)) { return emphasisAdded(context.getString(R.string.MessageDisplayHelper_decrypting_please_wait)); } else if (SmsDatabase.Types.isNoRemoteSessionType(type)) { diff --git a/src/org/thoughtcrime/securesms/service/PushReceiver.java b/src/org/thoughtcrime/securesms/service/PushReceiver.java index 51d46315b..87eb9ee9d 100644 --- a/src/org/thoughtcrime/securesms/service/PushReceiver.java +++ b/src/org/thoughtcrime/securesms/service/PushReceiver.java @@ -24,7 +24,6 @@ import org.thoughtcrime.securesms.sms.IncomingEndSessionMessage; import org.thoughtcrime.securesms.sms.IncomingKeyExchangeMessage; import org.thoughtcrime.securesms.sms.IncomingPreKeyBundleMessage; import org.thoughtcrime.securesms.sms.IncomingTextMessage; -import org.thoughtcrime.securesms.sms.SmsTransportDetails; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.textsecure.crypto.InvalidKeyException; import org.whispersystems.textsecure.crypto.InvalidMessageException; @@ -44,9 +43,10 @@ import static org.whispersystems.textsecure.push.PushMessageProtos.PushMessageCo public class PushReceiver { - public static final int RESULT_OK = 0; - public static final int RESULT_NO_SESSION = 1; - public static final int RESULT_DECRYPT_FAILED = 2; + public static final int RESULT_OK = 0; + public static final int RESULT_NO_SESSION = 1; + public static final int RESULT_DECRYPT_FAILED = 2; + public static final int RESULT_DECRYPT_DUPLICATE = 3; private final Context context; private final GroupReceiver groupReceiver; @@ -69,9 +69,10 @@ public class PushReceiver { long messageId = intent.getLongExtra("message_id", -1); int result = intent.getIntExtra("result", 0); - if (result == RESULT_OK) handleReceivedMessage(masterSecret, message, true); - else if (result == RESULT_NO_SESSION) handleReceivedMessageForNoSession(masterSecret, message); - else if (result == RESULT_DECRYPT_FAILED) handleReceivedCorruptedMessage(masterSecret, message, true); + if (result == RESULT_OK) handleReceivedMessage(masterSecret, message, true); + else if (result == RESULT_NO_SESSION) handleReceivedMessageForNoSession(masterSecret, message); + else if (result == RESULT_DECRYPT_FAILED) handleReceivedCorruptedMessage(masterSecret, message, true); + else if (result == RESULT_DECRYPT_DUPLICATE) handleReceivedDuplicateMessage(message); DatabaseFactory.getPushDatabase(context).delete(messageId); } @@ -255,6 +256,10 @@ public class PushReceiver { MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); } + private void handleReceivedDuplicateMessage(IncomingPushMessage message) { + Log.w("PushReceiver", "Received duplicate message: " + message.getSource() + " , " + message.getSourceDevice()); + } + private void handleReceivedCorruptedKey(MasterSecret masterSecret, IncomingPushMessage message, boolean invalidVersion)