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)