From 14b8f97de290d3e366b1a5450da56716db20766a Mon Sep 17 00:00:00 2001 From: Moxie Marlinspike Date: Tue, 22 Apr 2014 14:33:29 -0700 Subject: [PATCH] Reorganize session store load/store operations. --- build.gradle | 5 + .../test/InMemorySessionRecord.java | 54 +++++ .../test/InMemorySessionState.java | 2 +- .../test/InMemorySessionStore.java | 65 +++-- .../test/SessionCipherTest.java | 22 +- .../test/ratchet/RatchetingSessionTest.java | 2 +- .../libaxolotl/SessionCipher.java | 54 +++-- .../libaxolotl/SessionStore.java | 12 - .../libaxolotl/ratchet/RatchetingSession.java | 5 +- .../libaxolotl/state/SessionRecord.java | 13 + .../libaxolotl/{ => state}/SessionState.java | 5 +- .../libaxolotl/state/SessionStore.java | 14 ++ library/build.gradle | 5 + .../crypto/SessionCipherFactory.java | 10 +- .../textsecure/storage/Session.java | 80 ------ .../textsecure/storage/SessionRecordV2.java | 229 ------------------ .../textsecure/storage/SessionUtil.java | 32 +++ .../storage/TextSecureSessionRecord.java | 142 +++++++++++ .../storage/TextSecureSessionState.java | 2 +- .../storage/TextSecureSessionStore.java | 130 ++++++++++ .../storage/legacy/LocalKeyRecord.java | 34 --- .../storage/legacy/RemoteKeyRecord.java | 40 --- .../storage/legacy/SessionRecordV1.java | 18 -- .../securesms/AutoInitiateActivity.java | 11 +- .../securesms/ConversationActivity.java | 39 ++- .../securesms/DatabaseUpgradeActivity.java | 18 ++ .../securesms/VerifyIdentityActivity.java | 23 +- .../securesms/crypto/DecryptingQueue.java | 46 +--- .../crypto/KeyExchangeInitiator.java | 23 +- .../crypto/KeyExchangeProcessorV2.java | 48 ++-- .../securesms/database/DatabaseFactory.java | 12 +- .../securesms/service/PushReceiver.java | 20 +- .../securesms/service/SmsSender.java | 10 +- .../securesms/transport/MmsTransport.java | 8 +- .../securesms/transport/PushTransport.java | 55 ++--- .../securesms/transport/SmsTransport.java | 7 +- .../transport/UniversalTransport.java | 6 +- 37 files changed, 666 insertions(+), 635 deletions(-) create mode 100644 libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionRecord.java delete mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionStore.java create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionRecord.java rename libaxolotl/src/main/java/org/whispersystems/libaxolotl/{ => state}/SessionState.java (92%) create mode 100644 libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionStore.java delete mode 100644 library/src/org/whispersystems/textsecure/storage/Session.java delete mode 100644 library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java create mode 100644 library/src/org/whispersystems/textsecure/storage/SessionUtil.java create mode 100644 library/src/org/whispersystems/textsecure/storage/TextSecureSessionRecord.java create mode 100644 library/src/org/whispersystems/textsecure/storage/TextSecureSessionStore.java delete mode 100644 library/src/org/whispersystems/textsecure/storage/legacy/LocalKeyRecord.java delete mode 100644 library/src/org/whispersystems/textsecure/storage/legacy/RemoteKeyRecord.java delete mode 100644 library/src/org/whispersystems/textsecure/storage/legacy/SessionRecordV1.java diff --git a/build.gradle b/build.gradle index 84e6cc5c5..113d1c444 100644 --- a/build.gradle +++ b/build.gradle @@ -67,6 +67,11 @@ android { targetSdkVersion 19 } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + android { sourceSets { main { diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionRecord.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionRecord.java new file mode 100644 index 000000000..3186253c1 --- /dev/null +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionRecord.java @@ -0,0 +1,54 @@ +package org.whispersystems.test; + +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionState; + +import java.util.LinkedList; +import java.util.List; + +public class InMemorySessionRecord implements SessionRecord { + + private SessionState currentSessionState; + private List previousSessionStates; + + public InMemorySessionRecord() { + currentSessionState = new InMemorySessionState(); + previousSessionStates = new LinkedList<>(); + } + + public InMemorySessionRecord(SessionRecord copy) { + currentSessionState = new InMemorySessionState(copy.getSessionState()); + previousSessionStates = new LinkedList<>(); + + for (SessionState previousState : copy.getPreviousSessionStates()) { + previousSessionStates.add(new InMemorySessionState(previousState)); + } + } + + @Override + public SessionState getSessionState() { + return currentSessionState; + } + + @Override + public List getPreviousSessionStates() { + return previousSessionStates; + } + + @Override + public void reset() { + this.currentSessionState = new InMemorySessionState(); + this.previousSessionStates = new LinkedList<>(); + } + + @Override + public void archiveCurrentState() { + this.previousSessionStates.add(currentSessionState); + this.currentSessionState = new InMemorySessionState(); + } + + @Override + public byte[] serialize() { + throw new AssertionError(); + } +} diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionState.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionState.java index 44c103289..8ba761b20 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionState.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionState.java @@ -3,7 +3,7 @@ package org.whispersystems.test; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.SessionState; +import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.ratchet.ChainKey; diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionStore.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionStore.java index 08f7eb5de..e5976c03e 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionStore.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/InMemorySessionStore.java @@ -1,44 +1,63 @@ package org.whispersystems.test; -import org.whispersystems.libaxolotl.SessionState; -import org.whispersystems.libaxolotl.SessionStore; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionStore; +import org.whispersystems.libaxolotl.util.Pair; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; public class InMemorySessionStore implements SessionStore { - private SessionState currentSessionState; - private List previousSessionStates; + private Map, SessionRecord> sessions = new HashMap<>(); - private SessionState checkedOutSessionState; - private List checkedOutPreviousSessionStates; + public InMemorySessionStore() {} - public InMemorySessionStore(SessionState sessionState) { - this.currentSessionState = sessionState; - this.previousSessionStates = new LinkedList<>(); - this.checkedOutPreviousSessionStates = new LinkedList<>(); + @Override + public SessionRecord get(long recipientId, int deviceId) { + if (contains(recipientId, deviceId)) { + return new InMemorySessionRecord(sessions.get(new Pair<>(recipientId, deviceId))); + } else { + return new InMemorySessionRecord(); + } } @Override - public SessionState getSessionState() { - checkedOutSessionState = new InMemorySessionState(currentSessionState); - return checkedOutSessionState; - } + public List getSubDeviceSessions(long recipientId) { + List deviceIds = new LinkedList<>(); - @Override - public List getPreviousSessionStates() { - checkedOutPreviousSessionStates = new LinkedList<>(); - for (SessionState state : previousSessionStates) { - checkedOutPreviousSessionStates.add(new InMemorySessionState(state)); + for (Pair key : sessions.keySet()) { + if (key.first() == recipientId) { + deviceIds.add(key.second()); + } } - return checkedOutPreviousSessionStates; + return deviceIds; } @Override - public void save() { - this.currentSessionState = this.checkedOutSessionState; - this.previousSessionStates = this.checkedOutPreviousSessionStates; + public void put(long recipientId, int deviceId, SessionRecord record) { + sessions.put(new Pair<>(recipientId, deviceId), record); + } + + @Override + public boolean contains(long recipientId, int deviceId) { + return sessions.containsKey(new Pair<>(recipientId, deviceId)); + } + + @Override + public void delete(long recipientId, int deviceId) { + sessions.remove(new Pair<>(recipientId, deviceId)); + } + + @Override + public void deleteAll(long recipientId) { + for (Pair key : sessions.keySet()) { + if (key.first() == recipientId) { + sessions.remove(key); + } + } } } diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionCipherTest.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionCipherTest.java index 39345c16d..f51841542 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionCipherTest.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/SessionCipherTest.java @@ -9,8 +9,9 @@ import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.LegacyMessageException; import org.whispersystems.libaxolotl.SessionCipher; -import org.whispersystems.libaxolotl.SessionState; -import org.whispersystems.libaxolotl.SessionStore; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionState; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.protocol.CiphertextMessage; @@ -24,16 +25,19 @@ public class SessionCipherTest extends AndroidTestCase { throws InvalidKeyException, DuplicateMessageException, LegacyMessageException, InvalidMessageException { - SessionState aliceSessionState = new InMemorySessionState(); - SessionState bobSessionState = new InMemorySessionState(); + SessionRecord aliceSessionRecord = new InMemorySessionRecord(); + SessionRecord bobSessionRecord = new InMemorySessionRecord(); - initializeSessions(aliceSessionState, bobSessionState); + initializeSessions(aliceSessionRecord.getSessionState(), bobSessionRecord.getSessionState()); - SessionStore aliceSessionStore = new InMemorySessionStore(aliceSessionState); - SessionStore bobSessionStore = new InMemorySessionStore(bobSessionState); + SessionStore aliceSessionStore = new InMemorySessionStore(); + SessionStore bobSessionStore = new InMemorySessionStore(); - SessionCipher aliceCipher = new SessionCipher(aliceSessionStore); - SessionCipher bobCipher = new SessionCipher(bobSessionStore); + aliceSessionStore.put(2L, 1, aliceSessionRecord); + bobSessionStore.put(3L, 1, bobSessionRecord); + + SessionCipher aliceCipher = new SessionCipher(aliceSessionStore, 2L, 1); + SessionCipher bobCipher = new SessionCipher(bobSessionStore, 3L, 1); byte[] alicePlaintext = "This is a plaintext message.".getBytes(); CiphertextMessage message = aliceCipher.encrypt(alicePlaintext); diff --git a/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java b/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java index fa72ee192..d8a429a0e 100644 --- a/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java +++ b/libaxolotl/src/androidTest/java/org/whispersystems/test/ratchet/RatchetingSessionTest.java @@ -6,7 +6,7 @@ import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.test.InMemorySessionState; import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.SessionState; +import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPrivateKey; diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java index 7b8907a53..7aadb9501 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java @@ -25,6 +25,9 @@ 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.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionState; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.libaxolotl.util.ByteUtil; import org.whispersystems.libaxolotl.util.Pair; @@ -45,18 +48,23 @@ public class SessionCipher { private static final Object SESSION_LOCK = new Object(); private final SessionStore sessionStore; + private final long recipientId; + private final int deviceId; - public SessionCipher(SessionStore sessionStore) { + public SessionCipher(SessionStore sessionStore, long recipientId, int deviceId) { this.sessionStore = sessionStore; + this.recipientId = recipientId; + this.deviceId = deviceId; } public CiphertextMessage encrypt(byte[] paddedMessage) { synchronized (SESSION_LOCK) { - SessionState sessionState = sessionStore.getSessionState(); - ChainKey chainKey = sessionState.getSenderChainKey(); - MessageKeys messageKeys = chainKey.getMessageKeys(); - ECPublicKey senderEphemeral = sessionState.getSenderEphemeral(); - int previousCounter = sessionState.getPreviousCounter(); + SessionRecord sessionRecord = sessionStore.get(recipientId, deviceId); + SessionState sessionState = sessionRecord.getSessionState(); + ChainKey chainKey = sessionState.getSenderChainKey(); + MessageKeys messageKeys = chainKey.getMessageKeys(); + ECPublicKey senderEphemeral = sessionState.getSenderEphemeral(); + int previousCounter = sessionState.getPreviousCounter(); byte[] ciphertextBody = getCiphertext(messageKeys, paddedMessage); CiphertextMessage ciphertextMessage = new WhisperMessage(messageKeys.getMacKey(), @@ -74,7 +82,7 @@ public class SessionCipher { } sessionState.setSenderChainKey(chainKey.getNextChainKey()); - sessionStore.save(); + sessionStore.put(recipientId, deviceId, sessionRecord); return ciphertextMessage; } } @@ -83,13 +91,14 @@ public class SessionCipher { throws InvalidMessageException, DuplicateMessageException, LegacyMessageException { synchronized (SESSION_LOCK) { - SessionState sessionState = sessionStore.getSessionState(); - List previousStates = sessionStore.getPreviousSessionStates(); - List exceptions = new LinkedList(); + SessionRecord sessionRecord = sessionStore.get(recipientId, deviceId); + SessionState sessionState = sessionRecord.getSessionState(); + List previousStates = sessionRecord.getPreviousSessionStates(); + List exceptions = new LinkedList<>(); try { byte[] plaintext = decrypt(sessionState, decodedMessage); - sessionStore.save(); + sessionStore.put(recipientId, deviceId, sessionRecord); return plaintext; } catch (InvalidMessageException e) { @@ -99,7 +108,7 @@ public class SessionCipher { for (SessionState previousState : previousStates) { try { byte[] plaintext = decrypt(previousState, decodedMessage); - sessionStore.save(); + sessionStore.put(recipientId, deviceId, sessionRecord); return plaintext; } catch (InvalidMessageException e) { @@ -137,7 +146,8 @@ public class SessionCipher { public int getRemoteRegistrationId() { synchronized (SESSION_LOCK) { - return sessionStore.getSessionState().getRemoteRegistrationId(); + SessionRecord record = sessionStore.get(recipientId, deviceId); + return record.getSessionState().getRemoteRegistrationId(); } } @@ -201,9 +211,7 @@ public class SessionCipher { messageKeys.getCounter()); return cipher.doFinal(plaintext); - } catch (IllegalBlockSizeException e) { - throw new AssertionError(e); - } catch (BadPaddingException e) { + } catch (IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } @@ -214,9 +222,7 @@ public class SessionCipher { messageKeys.getCipherKey(), messageKeys.getCounter()); return cipher.doFinal(cipherText); - } catch (IllegalBlockSizeException e) { - throw new AssertionError(e); - } catch (BadPaddingException e) { + } catch (IllegalBlockSizeException | BadPaddingException e) { throw new AssertionError(e); } } @@ -232,13 +238,9 @@ public class SessionCipher { cipher.init(mode, key, iv); return cipher; - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); - } catch (NoSuchPaddingException e) { - throw new AssertionError(e); - } catch (java.security.InvalidKeyException e) { - throw new AssertionError(e); - } catch (InvalidAlgorithmParameterException e) { + } catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException | + InvalidAlgorithmParameterException e) + { throw new AssertionError(e); } } diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionStore.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionStore.java deleted file mode 100644 index 123ae449c..000000000 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionStore.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.whispersystems.libaxolotl; - -import java.util.List; - -public interface SessionStore { - - public SessionState getSessionState(); - public List getPreviousSessionStates(); - public void save(); - - -} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java index 9a7a5e20f..8442ce22b 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/ratchet/RatchetingSession.java @@ -16,18 +16,15 @@ */ package org.whispersystems.libaxolotl.ratchet; -import android.util.Log; - import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.SessionState; +import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.kdf.DerivedSecrets; import org.whispersystems.libaxolotl.kdf.HKDF; -import org.whispersystems.libaxolotl.util.Hex; import org.whispersystems.libaxolotl.util.Pair; import java.io.ByteArrayOutputStream; diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionRecord.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionRecord.java new file mode 100644 index 000000000..cdb1ea69f --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionRecord.java @@ -0,0 +1,13 @@ +package org.whispersystems.libaxolotl.state; + +import java.util.List; + +public interface SessionRecord { + + public SessionState getSessionState(); + public List getPreviousSessionStates(); + public void reset(); + public void archiveCurrentState(); + public byte[] serialize(); + +} diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionState.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionState.java similarity index 92% rename from libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionState.java rename to libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionState.java index 60deab98a..832a60749 100644 --- a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/SessionState.java +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionState.java @@ -1,5 +1,8 @@ -package org.whispersystems.libaxolotl; +package org.whispersystems.libaxolotl.state; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.ratchet.ChainKey; diff --git a/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionStore.java b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionStore.java new file mode 100644 index 000000000..835a8f16e --- /dev/null +++ b/libaxolotl/src/main/java/org/whispersystems/libaxolotl/state/SessionStore.java @@ -0,0 +1,14 @@ +package org.whispersystems.libaxolotl.state; + +import java.util.List; + +public interface SessionStore { + + public SessionRecord get(long recipientId, int deviceId); + public List getSubDeviceSessions(long recipientId); + public void put(long recipientId, int deviceId, SessionRecord record); + public boolean contains(long recipientId, int deviceId); + public void delete(long recipientId, int deviceId); + public void deleteAll(long recipientId); + +} diff --git a/library/build.gradle b/library/build.gradle index 131cb7ed9..832ef5d35 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -31,6 +31,11 @@ android { compileSdkVersion 19 buildToolsVersion '19.1.0' + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + android { sourceSets { main { diff --git a/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java b/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java index e3a5dacad..787c90826 100644 --- a/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java +++ b/library/src/org/whispersystems/textsecure/crypto/SessionCipherFactory.java @@ -20,8 +20,9 @@ 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.SessionRecordV2; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; public class SessionCipherFactory { @@ -29,9 +30,10 @@ public class SessionCipherFactory { MasterSecret masterSecret, RecipientDevice recipient) { - if (SessionRecordV2.hasSession(context, masterSecret, recipient)) { - SessionRecordV2 record = new SessionRecordV2(context, masterSecret, recipient); - return new SessionCipher(record); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + + if (sessionStore.contains(recipient.getRecipientId(), recipient.getDeviceId())) { + return new SessionCipher(sessionStore, recipient.getRecipientId(), recipient.getDeviceId()); } else { throw new AssertionError("Attempt to initialize cipher for non-existing session."); } diff --git a/library/src/org/whispersystems/textsecure/storage/Session.java b/library/src/org/whispersystems/textsecure/storage/Session.java deleted file mode 100644 index c47015484..000000000 --- a/library/src/org/whispersystems/textsecure/storage/Session.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.whispersystems.textsecure.storage; - -import android.content.Context; -import android.util.Log; - -import org.whispersystems.libaxolotl.IdentityKey; -import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.storage.legacy.LocalKeyRecord; -import org.whispersystems.textsecure.storage.legacy.RemoteKeyRecord; -import org.whispersystems.textsecure.storage.legacy.SessionRecordV1; - -/** - * Helper class for generating key pairs and calculating ECDH agreements. - * - * @author Moxie Marlinspike - */ - -public class Session { - - public static void clearV1SessionFor(Context context, CanonicalRecipient recipient) { - //XXX Obviously we should probably do something more thorough here eventually. - LocalKeyRecord.delete(context, recipient); - RemoteKeyRecord.delete(context, recipient); - SessionRecordV1.delete(context, recipient); - } - - public static void abortSessionFor(Context context, CanonicalRecipient recipient) { - Log.w("Session", "Aborting session, deleting keys..."); - clearV1SessionFor(context, recipient); - SessionRecordV2.deleteAll(context, recipient); - } - - public static boolean hasSession(Context context, MasterSecret masterSecret, - CanonicalRecipient recipient) - { - Log.w("Session", "Checking session..."); - return SessionRecordV2.hasSession(context, masterSecret, recipient.getRecipientId(), - RecipientDevice.DEFAULT_DEVICE_ID); - } - - public static boolean hasEncryptCapableSession(Context context, - MasterSecret masterSecret, - CanonicalRecipient recipient) - { - RecipientDevice device = new RecipientDevice(recipient.getRecipientId(), - RecipientDevice.DEFAULT_DEVICE_ID); - - return hasEncryptCapableSession(context, masterSecret, recipient, device); - } - - public static boolean hasEncryptCapableSession(Context context, - MasterSecret masterSecret, - CanonicalRecipient recipient, - RecipientDevice device) - { - return hasSession(context, masterSecret, recipient) && - !SessionRecordV2.needsRefresh(context, masterSecret, device); - } - - public static IdentityKey getRemoteIdentityKey(Context context, MasterSecret masterSecret, - CanonicalRecipient recipient) - { - return getRemoteIdentityKey(context, masterSecret, recipient.getRecipientId()); - } - - public static IdentityKey getRemoteIdentityKey(Context context, - MasterSecret masterSecret, - long recipientId) - { - if (SessionRecordV2.hasSession(context, masterSecret, recipientId, - RecipientDevice.DEFAULT_DEVICE_ID)) - { - return new SessionRecordV2(context, masterSecret, recipientId, - RecipientDevice.DEFAULT_DEVICE_ID).getSessionState() - .getRemoteIdentityKey(); - } else { - return null; - } - } -} diff --git a/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java b/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java deleted file mode 100644 index 05d141833..000000000 --- a/library/src/org/whispersystems/textsecure/storage/SessionRecordV2.java +++ /dev/null @@ -1,229 +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.storage; - -import android.content.Context; -import android.util.Log; - -import org.whispersystems.libaxolotl.InvalidMessageException; -import org.whispersystems.libaxolotl.SessionState; -import org.whispersystems.libaxolotl.SessionStore; -import org.whispersystems.textsecure.crypto.MasterCipher; -import org.whispersystems.textsecure.crypto.MasterSecret; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.channels.FileChannel; -import java.util.LinkedList; -import java.util.List; - -import static org.whispersystems.textsecure.storage.StorageProtos.RecordStructure; -import static org.whispersystems.textsecure.storage.StorageProtos.SessionStructure; - -/** - * A disk record representing a current session. - * - * @author Moxie Marlinspike - */ - -public class SessionRecordV2 extends Record implements SessionStore { - - private static final Object FILE_LOCK = new Object(); - - private static final int SINGLE_STATE_VERSION = 1; - private static final int ARCHIVE_STATES_VERSION = 2; - private static final int CURRENT_VERSION = 2; - - private final MasterSecret masterSecret; - - private TextSecureSessionState sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build()); - private List previousStates = new LinkedList(); - - public SessionRecordV2(Context context, MasterSecret masterSecret, RecipientDevice peer) { - this(context, masterSecret, peer.getRecipientId(), peer.getDeviceId()); - } - - public SessionRecordV2(Context context, MasterSecret masterSecret, long recipientId, int deviceId) { - super(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId)); - this.masterSecret = masterSecret; - loadData(); - } - - private static String getRecordName(long recipientId, int deviceId) { - return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId); - } - - public TextSecureSessionState getSessionState() { - return sessionState; - } - - - public List getPreviousSessionStates() { - return previousStates; - } - - public static List getSessionSubDevices(Context context, CanonicalRecipient recipient) { - List results = new LinkedList(); - File parent = getParentDirectory(context, SESSIONS_DIRECTORY_V2); - String[] children = parent.list(); - - if (children == null) return results; - - for (String child : children) { - try { - String[] parts = child.split("[.]", 2); - long sessionRecipientId = Long.parseLong(parts[0]); - - if (sessionRecipientId == recipient.getRecipientId() && parts.length > 1) { - results.add(Integer.parseInt(parts[1])); - } - } catch (NumberFormatException e) { - Log.w("SessionRecordV2", e); - } - } - - return results; - } - - public static void deleteAll(Context context, CanonicalRecipient recipient) { - List devices = getSessionSubDevices(context, recipient); - - delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipient.getRecipientId(), - RecipientDevice.DEFAULT_DEVICE_ID)); - - for (int device : devices) { - delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipient.getRecipientId(), device)); - } - } - - public static void delete(Context context, RecipientDevice recipientDevice) { - delete(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientDevice.getRecipientId(), - recipientDevice.getDeviceId())); - } - - public static boolean hasSession(Context context, MasterSecret masterSecret, - RecipientDevice recipient) - { - return hasSession(context, masterSecret, recipient.getRecipientId(), recipient.getDeviceId()); - } - - public static boolean hasSession(Context context, MasterSecret masterSecret, - long recipientId, int deviceId) - { - return hasRecord(context, SESSIONS_DIRECTORY_V2, getRecordName(recipientId, deviceId)) && - new SessionRecordV2(context, masterSecret, recipientId, deviceId).sessionState.hasSenderChain(); - } - - public static boolean needsRefresh(Context context, MasterSecret masterSecret, - RecipientDevice recipient) - { - return new SessionRecordV2(context, masterSecret, - recipient.getRecipientId(), - recipient.getDeviceId()).getSessionState() - .getNeedsRefresh(); - } - - public void clear() { - this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build()); - this.previousStates = new LinkedList(); - } - - public void archiveCurrentState() { - this.previousStates.add(sessionState); - this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build()); - } - - public void save() { - synchronized (FILE_LOCK) { - try { - List previousStructures = new LinkedList(); - - for (SessionState previousState : previousStates) { - previousStructures.add(((TextSecureSessionState)previousState).getStructure()); - } - - RecordStructure record = RecordStructure.newBuilder() - .setCurrentSession(sessionState.getStructure()) - .addAllPreviousSessions(previousStructures) - .build(); - - RandomAccessFile file = openRandomAccessFile(); - FileChannel out = file.getChannel(); - out.position(0); - - MasterCipher cipher = new MasterCipher(masterSecret); - writeInteger(CURRENT_VERSION, out); - writeBlob(cipher.encryptBytes(record.toByteArray()), out); - - out.truncate(out.position()); - file.close(); - } catch (IOException ioe) { - throw new IllegalArgumentException(ioe); - } - } - } - - private void loadData() { - synchronized (FILE_LOCK) { - try { - FileInputStream in = this.openInputStream(); - int versionMarker = readInteger(in); - - if (versionMarker > CURRENT_VERSION) { - throw new AssertionError("Unknown version: " + versionMarker); - } - - MasterCipher cipher = new MasterCipher(masterSecret); - byte[] encryptedBlob = readBlob(in); - - if (versionMarker == SINGLE_STATE_VERSION) { - byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob); - SessionStructure sessionStructure = SessionStructure.parseFrom(plaintextBytes); - this.sessionState = new TextSecureSessionState(sessionStructure); - } else if (versionMarker == ARCHIVE_STATES_VERSION) { - byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob); - RecordStructure recordStructure = RecordStructure.parseFrom(plaintextBytes); - - this.sessionState = new TextSecureSessionState(recordStructure.getCurrentSession()); - this.previousStates = new LinkedList(); - - for (SessionStructure sessionStructure : recordStructure.getPreviousSessionsList()) { - this.previousStates.add(new TextSecureSessionState(sessionStructure)); - } - } else { - throw new AssertionError("Unknown version: " + versionMarker); - } - - in.close(); - - } catch (FileNotFoundException e) { - Log.w("SessionRecordV2", "No session information found."); - // XXX - } catch (IOException ioe) { - Log.w("SessionRecordV2", ioe); - // XXX - } catch (InvalidMessageException ime) { - Log.w("SessionRecordV2", ime); - // XXX - } - } - } - -} diff --git a/library/src/org/whispersystems/textsecure/storage/SessionUtil.java b/library/src/org/whispersystems/textsecure/storage/SessionUtil.java new file mode 100644 index 000000000..a1be9a59f --- /dev/null +++ b/library/src/org/whispersystems/textsecure/storage/SessionUtil.java @@ -0,0 +1,32 @@ +package org.whispersystems.textsecure.storage; + +import android.content.Context; + +import org.whispersystems.libaxolotl.state.SessionStore; +import org.whispersystems.textsecure.crypto.MasterSecret; + +public class SessionUtil { + + public static boolean hasEncryptCapableSession(Context context, + MasterSecret masterSecret, + CanonicalRecipient recipient) + { + return hasEncryptCapableSession(context, masterSecret, + new RecipientDevice(recipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID)); + } + + public static boolean hasEncryptCapableSession(Context context, + MasterSecret masterSecret, + RecipientDevice recipientDevice) + { + long recipientId = recipientDevice.getRecipientId(); + int deviceId = recipientDevice.getDeviceId(); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + + return + sessionStore.contains(recipientId, deviceId) && + !sessionStore.get(recipientId, deviceId).getSessionState().getNeedsRefresh(); + } + +} diff --git a/library/src/org/whispersystems/textsecure/storage/TextSecureSessionRecord.java b/library/src/org/whispersystems/textsecure/storage/TextSecureSessionRecord.java new file mode 100644 index 000000000..f433228f7 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/storage/TextSecureSessionRecord.java @@ -0,0 +1,142 @@ +package org.whispersystems.textsecure.storage; + +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionState; +import org.whispersystems.textsecure.crypto.MasterCipher; +import org.whispersystems.textsecure.crypto.MasterSecret; +import org.whispersystems.textsecure.util.Conversions; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.LinkedList; +import java.util.List; + +import static org.whispersystems.textsecure.storage.StorageProtos.RecordStructure; +import static org.whispersystems.textsecure.storage.StorageProtos.SessionStructure; + +public class TextSecureSessionRecord implements SessionRecord { + + private static final int SINGLE_STATE_VERSION = 1; + private static final int ARCHIVE_STATES_VERSION = 2; + private static final int CURRENT_VERSION = 2; + + private TextSecureSessionState sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build()); + private List previousStates = new LinkedList<>(); + + private final MasterSecret masterSecret; + + public TextSecureSessionRecord(MasterSecret masterSecret) { + this.masterSecret = masterSecret; + } + + public TextSecureSessionRecord(MasterSecret masterSecret, FileInputStream in) + throws IOException, InvalidMessageException + { + this.masterSecret = masterSecret; + + int versionMarker = readInteger(in); + + if (versionMarker > CURRENT_VERSION) { + throw new AssertionError("Unknown version: " + versionMarker); + } + + MasterCipher cipher = new MasterCipher(masterSecret); + byte[] encryptedBlob = readBlob(in); + + if (versionMarker == SINGLE_STATE_VERSION) { + byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob); + SessionStructure sessionStructure = SessionStructure.parseFrom(plaintextBytes); + this.sessionState = new TextSecureSessionState(sessionStructure); + } else if (versionMarker == ARCHIVE_STATES_VERSION) { + byte[] plaintextBytes = cipher.decryptBytes(encryptedBlob); + RecordStructure recordStructure = RecordStructure.parseFrom(plaintextBytes); + + this.sessionState = new TextSecureSessionState(recordStructure.getCurrentSession()); + this.previousStates = new LinkedList<>(); + + for (SessionStructure sessionStructure : recordStructure.getPreviousSessionsList()) { + this.previousStates.add(new TextSecureSessionState(sessionStructure)); + } + } else { + throw new AssertionError("Unknown version: " + versionMarker); + } + + in.close(); + } + + @Override + public SessionState getSessionState() { + return sessionState; + } + + @Override + public List getPreviousSessionStates() { + return previousStates; + } + + @Override + public void reset() { + this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build()); + this.previousStates = new LinkedList<>(); + } + + @Override + public void archiveCurrentState() { + this.previousStates.add(sessionState); + this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build()); + } + + @Override + public byte[] serialize() { + try { + List previousStructures = new LinkedList<>(); + + for (SessionState previousState : previousStates) { + previousStructures.add(((TextSecureSessionState)previousState).getStructure()); + } + + RecordStructure record = RecordStructure.newBuilder() + .setCurrentSession(sessionState.getStructure()) + .addAllPreviousSessions(previousStructures) + .build(); + + + ByteArrayOutputStream serialized = new ByteArrayOutputStream(); + MasterCipher cipher = new MasterCipher(masterSecret); + + writeInteger(CURRENT_VERSION, serialized); + writeBlob(cipher.encryptBytes(record.toByteArray()), serialized); + + return serialized.toByteArray(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private byte[] readBlob(FileInputStream in) throws IOException { + int length = readInteger(in); + byte[] blobBytes = new byte[length]; + + in.read(blobBytes, 0, blobBytes.length); + return blobBytes; + } + + private void writeBlob(byte[] blobBytes, OutputStream out) throws IOException { + writeInteger(blobBytes.length, out); + out.write(blobBytes); + } + + private int readInteger(FileInputStream in) throws IOException { + byte[] integer = new byte[4]; + in.read(integer, 0, integer.length); + return Conversions.byteArrayToInt(integer); + } + + private void writeInteger(int value, OutputStream out) throws IOException { + byte[] valueBytes = Conversions.intToByteArray(value); + out.write(valueBytes); + } +} diff --git a/library/src/org/whispersystems/textsecure/storage/TextSecureSessionState.java b/library/src/org/whispersystems/textsecure/storage/TextSecureSessionState.java index f5a317bba..e46671eeb 100644 --- a/library/src/org/whispersystems/textsecure/storage/TextSecureSessionState.java +++ b/library/src/org/whispersystems/textsecure/storage/TextSecureSessionState.java @@ -23,7 +23,7 @@ import com.google.protobuf.ByteString; import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.InvalidKeyException; -import org.whispersystems.libaxolotl.SessionState; +import org.whispersystems.libaxolotl.state.SessionState; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPrivateKey; diff --git a/library/src/org/whispersystems/textsecure/storage/TextSecureSessionStore.java b/library/src/org/whispersystems/textsecure/storage/TextSecureSessionStore.java new file mode 100644 index 000000000..3d1278c12 --- /dev/null +++ b/library/src/org/whispersystems/textsecure/storage/TextSecureSessionStore.java @@ -0,0 +1,130 @@ +package org.whispersystems.textsecure.storage; + +import android.content.Context; +import android.util.Log; + +import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionStore; +import org.whispersystems.textsecure.crypto.MasterSecret; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.LinkedList; +import java.util.List; + +public class TextSecureSessionStore implements SessionStore { + + private static final String TAG = TextSecureSessionStore.class.getSimpleName(); + private static final String SESSIONS_DIRECTORY_V2 = "sessions-v2"; + private static final Object FILE_LOCK = new Object(); + + private final Context context; + private final MasterSecret masterSecret; + + public TextSecureSessionStore(Context context, MasterSecret masterSecret) { + this.context = context.getApplicationContext(); + this.masterSecret = masterSecret; + } + + @Override + public SessionRecord get(long recipientId, int deviceId) { + synchronized (FILE_LOCK) { + try { + FileInputStream input = new FileInputStream(getSessionFile(recipientId, deviceId)); + return new TextSecureSessionRecord(masterSecret, input); + } catch (InvalidMessageException | IOException e) { + Log.w(TAG, "No existing session information found."); + return new TextSecureSessionRecord(masterSecret); + } + } + } + + @Override + public void put(long recipientId, int deviceId, SessionRecord record) { + try { + RandomAccessFile sessionFile = new RandomAccessFile(getSessionFile(recipientId, deviceId), "rw"); + FileChannel out = sessionFile.getChannel(); + + out.position(0); + out.write(ByteBuffer.wrap(record.serialize())); + out.truncate(out.position()); + + sessionFile.close(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public boolean contains(long recipientId, int deviceId) { + return getSessionFile(recipientId, deviceId).exists() && + get(recipientId, deviceId).getSessionState().hasSenderChain(); + } + + @Override + public void delete(long recipientId, int deviceId) { + getSessionFile(recipientId, deviceId).delete(); + } + + @Override + public void deleteAll(long recipientId) { + List devices = getSubDeviceSessions(recipientId); + + delete(recipientId, RecipientDevice.DEFAULT_DEVICE_ID); + + for (int device : devices) { + delete(recipientId, device); + } + } + + @Override + public List getSubDeviceSessions(long recipientId) { + List results = new LinkedList<>(); + File parent = getSessionDirectory(); + String[] children = parent.list(); + + if (children == null) return results; + + for (String child : children) { + try { + String[] parts = child.split("[.]", 2); + long sessionRecipientId = Long.parseLong(parts[0]); + + if (sessionRecipientId == recipientId && parts.length > 1) { + results.add(Integer.parseInt(parts[1])); + } + } catch (NumberFormatException e) { + Log.w("SessionRecordV2", e); + } + } + + return results; + } + + + private File getSessionFile(long recipientId, int deviceId) { + return new File(getSessionDirectory(), getSessionName(recipientId, deviceId)); + } + + private File getSessionDirectory() { + File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2); + + if (!directory.exists()) { + if (!directory.mkdirs()) { + Log.w(TAG, "Session directory creation failed!"); + } + } + + return directory; + } + + private String getSessionName(long recipientId, int deviceId) { + return recipientId + (deviceId == RecipientDevice.DEFAULT_DEVICE_ID ? "" : "." + deviceId); + } + +} diff --git a/library/src/org/whispersystems/textsecure/storage/legacy/LocalKeyRecord.java b/library/src/org/whispersystems/textsecure/storage/legacy/LocalKeyRecord.java deleted file mode 100644 index cea52de04..000000000 --- a/library/src/org/whispersystems/textsecure/storage/legacy/LocalKeyRecord.java +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (C) 2011 Whisper Systems - * 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.storage.legacy; - -import android.content.Context; - -import org.whispersystems.textsecure.storage.CanonicalRecipient; -import org.whispersystems.textsecure.storage.Record; - -public class LocalKeyRecord { - - public static void delete(Context context, CanonicalRecipient recipient) { - Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); - } - - private static String getFileNameForRecipient(CanonicalRecipient recipient) { - return recipient.getRecipientId() + "-local"; - } -} diff --git a/library/src/org/whispersystems/textsecure/storage/legacy/RemoteKeyRecord.java b/library/src/org/whispersystems/textsecure/storage/legacy/RemoteKeyRecord.java deleted file mode 100644 index 89563d392..000000000 --- a/library/src/org/whispersystems/textsecure/storage/legacy/RemoteKeyRecord.java +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (C) 2011 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.storage.legacy; - -import android.content.Context; - -import org.whispersystems.textsecure.storage.CanonicalRecipient; -import org.whispersystems.textsecure.storage.Record; - -/** - * Represents the current and last public key belonging to the "remote" - * endpoint in an encrypted session. These are stored on disk. - * - * @author Moxie Marlinspike - */ - -public class RemoteKeyRecord { - - public static void delete(Context context, CanonicalRecipient recipient) { - Record.delete(context, Record.SESSIONS_DIRECTORY, getFileNameForRecipient(recipient)); - } - - private static String getFileNameForRecipient(CanonicalRecipient recipient) { - return recipient.getRecipientId() + "-remote"; - } -} diff --git a/library/src/org/whispersystems/textsecure/storage/legacy/SessionRecordV1.java b/library/src/org/whispersystems/textsecure/storage/legacy/SessionRecordV1.java deleted file mode 100644 index c85c9c1ee..000000000 --- a/library/src/org/whispersystems/textsecure/storage/legacy/SessionRecordV1.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.whispersystems.textsecure.storage.legacy; - -import android.content.Context; - -import org.whispersystems.textsecure.storage.CanonicalRecipient; -import org.whispersystems.textsecure.storage.Record; - -/** - * A disk record representing a current session. - * - * @author Moxie Marlinspike - */ - -public class SessionRecordV1 { - public static void delete(Context context, CanonicalRecipient recipient) { - Record.delete(context, Record.SESSIONS_DIRECTORY, recipient.getRecipientId() + ""); - } -} diff --git a/src/org/thoughtcrime/securesms/AutoInitiateActivity.java b/src/org/thoughtcrime/securesms/AutoInitiateActivity.java index 1842148da..74932a61e 100644 --- a/src/org/thoughtcrime/securesms/AutoInitiateActivity.java +++ b/src/org/thoughtcrime/securesms/AutoInitiateActivity.java @@ -30,10 +30,10 @@ import org.thoughtcrime.securesms.protocol.Tag; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.Session; -import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; /** * Activity which prompts the user to initiate a secure @@ -65,8 +65,8 @@ public class AutoInitiateActivity extends Activity { private void initializeResources() { this.threadId = this.getIntent().getLongExtra("threadId", -1); - this.recipient = (Recipient)this.getIntent().getParcelableExtra("recipient"); - this.masterSecret = (MasterSecret)this.getIntent().getParcelableExtra("masterSecret"); + this.recipient = this.getIntent().getParcelableExtra("recipient"); + this.masterSecret = this.getIntent().getParcelableExtra("masterSecret"); ((Button)findViewById(R.id.initiate_button)).setOnClickListener(new OkListener()); ((Button)findViewById(R.id.cancel_button)).setOnClickListener(new CancelListener()); @@ -117,6 +117,7 @@ public class AutoInitiateActivity extends Activity { MasterSecret masterSecret, Recipient recipient) { - return !Session.hasSession(context, masterSecret, recipient); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + return sessionStore.contains(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); } } diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 9d8541ee5..526d246ba 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -103,11 +103,11 @@ import org.thoughtcrime.securesms.util.GroupUtil; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libaxolotl.InvalidMessageException; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.Session; -import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Util; import java.io.IOException; @@ -316,9 +316,11 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { if (isEncryptedConversation && isSingleConversation()) { - boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients()); - Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient(); - boolean hasSession = Session.hasSession(this, masterSecret, primaryRecipient); + SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret); + Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient(); + boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients()); + boolean isSecureDestination = isSingleConversation() && sessionStore.contains(primaryRecipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID); getMenuInflater().inflate(R.menu.conversation_button_context, menu); @@ -334,7 +336,7 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi menu.removeItem(R.id.menu_context_send_push); } - if (!hasSession) { + if (!isSecureDestination) { menu.removeItem(R.id.menu_context_send_encrypted_mms); menu.removeItem(R.id.menu_context_send_encrypted_sms); } @@ -418,25 +420,14 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi @Override public void onClick(DialogInterface dialog, int which) { if (isSingleConversation()) { - ConversationActivity self = ConversationActivity.this; - Recipient recipient = getRecipients().getPrimaryRecipient(); + ConversationActivity self = ConversationActivity.this; - if (SessionRecordV2.hasSession(self, masterSecret, - recipient.getRecipientId(), - RecipientDevice.DEFAULT_DEVICE_ID)) - { - OutgoingEndSessionMessage endSessionMessage = - new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE")); + OutgoingEndSessionMessage endSessionMessage = + new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE")); - long allocatedThreadId = MessageSender.send(self, masterSecret, - endSessionMessage, threadId, false); + long allocatedThreadId = MessageSender.send(self, masterSecret, endSessionMessage, threadId, false); - sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId); - } else { - Session.abortSessionFor(self, recipient); - initializeSecurity(); - initializeTitleBar(); - } + sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId); } } }); @@ -699,9 +690,11 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi private void initializeSecurity() { TypedArray drawables = obtainStyledAttributes(SEND_ATTRIBUTES); + SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret); Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient(); boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients()); - boolean isSecureDestination = isSingleConversation() && Session.hasSession(this, masterSecret, primaryRecipient); + boolean isSecureDestination = isSingleConversation() && sessionStore.contains(primaryRecipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID); if (isPushDestination || isSecureDestination) { this.isEncryptedConversation = true; diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index 6f1cfb7d8..a515175eb 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -35,6 +35,7 @@ import org.whispersystems.textsecure.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.util.VersionTracker; +import java.io.File; import java.util.SortedSet; import java.util.TreeSet; @@ -45,6 +46,7 @@ public class DatabaseUpgradeActivity extends Activity { public static final int TOFU_IDENTITIES_VERSION = 50; public static final int CURVE25519_VERSION = 63; public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73; + public static final int NO_V1_VERSION = 83; private static final SortedSet UPGRADE_VERSIONS = new TreeSet() {{ add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION); @@ -137,6 +139,22 @@ public class DatabaseUpgradeActivity extends Activity { } } + if (params[0] < NO_V1_VERSION) { + File v1sessions = new File(context.getFilesDir(), "sessions"); + + if (v1sessions.exists() && v1sessions.isDirectory()) { + File[] contents = v1sessions.listFiles(); + + if (contents != null) { + for (File session : contents) { + session.delete(); + } + } + + v1sessions.delete(); + } + } + return null; } diff --git a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java index 66eaa5db3..481d0a3ce 100644 --- a/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java +++ b/src/org/thoughtcrime/securesms/VerifyIdentityActivity.java @@ -27,9 +27,12 @@ import org.thoughtcrime.securesms.util.DynamicLanguage; import org.thoughtcrime.securesms.util.DynamicTheme; import org.thoughtcrime.securesms.util.MemoryCleaner; import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.IdentityKeyParcelable; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.storage.Session; +import org.whispersystems.textsecure.storage.RecipientDevice; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; /** * Activity for verifying identity keys. @@ -92,7 +95,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity { } if (identityKey == null) { - identityKey = Session.getRemoteIdentityKey(this, masterSecret, recipient); + identityKey = getRemoteIdentityKey(masterSecret, recipient); } if (identityKey == null) { @@ -128,7 +131,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity { @Override protected void initiateScan() { - IdentityKey identityKey = Session.getRemoteIdentityKey(this, masterSecret, recipient); + IdentityKey identityKey = getRemoteIdentityKey(masterSecret, recipient); if (identityKey == null) { Toast.makeText(this, R.string.VerifyIdentityActivity_recipient_has_no_identity_key_exclamation, @@ -150,7 +153,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity { @Override protected IdentityKey getIdentityKeyToCompare() { - return Session.getRemoteIdentityKey(this, masterSecret, recipient); + return getRemoteIdentityKey(masterSecret, recipient); } @Override @@ -177,4 +180,16 @@ public class VerifyIdentityActivity extends KeyScanningActivity { protected String getVerifiedTitle() { return getString(R.string.VerifyIdentityActivity_verified_exclamation); } + + private IdentityKey getRemoteIdentityKey(MasterSecret masterSecret, Recipient recipient) { + SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret); + SessionRecord record = sessionStore.get(recipient.getRecipientId(), + RecipientDevice.DEFAULT_DEVICE_ID); + + if (record == null) { + return null; + } + + return record.getSessionState().getRemoteIdentityKey(); + } } diff --git a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java index 6af3ed5e6..f55b77b90 100644 --- a/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java +++ b/src/org/thoughtcrime/securesms/crypto/DecryptingQueue.java @@ -47,12 +47,12 @@ import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.LegacyMessageException; import org.whispersystems.libaxolotl.SessionCipher; 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.push.IncomingPushMessage; import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.Session; -import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Hex; import org.whispersystems.textsecure.util.Util; @@ -197,11 +197,12 @@ public class DecryptingQueue { public void run() { try { + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false); Recipient recipient = recipients.getPrimaryRecipient(); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice()); - if (!SessionRecordV2.hasSession(context, masterSecret, recipientDevice)) { + if (!sessionStore.contains(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) { sendResult(PushReceiver.RESULT_NO_SESSION); return; } @@ -211,18 +212,12 @@ public class DecryptingQueue { message = message.withBody(plaintextBody); sendResult(PushReceiver.RESULT_OK); - } catch (InvalidMessageException e) { - Log.w("DecryptionQueue", e); - sendResult(PushReceiver.RESULT_DECRYPT_FAILED); - } catch (RecipientFormattingException e) { + } catch (InvalidMessageException | LegacyMessageException | RecipientFormattingException e) { Log.w("DecryptionQueue", e); sendResult(PushReceiver.RESULT_DECRYPT_FAILED); } catch (DuplicateMessageException e) { Log.w("DecryptingQueue", e); sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE); - } catch (LegacyMessageException e) { - Log.w("DecryptionQueue", e); - sendResult(PushReceiver.RESULT_DECRYPT_FAILED); } } @@ -270,6 +265,7 @@ public class DecryptingQueue { try { String messageFrom = pdu.getFrom().getString(); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false); Recipient recipient = recipients.getPrimaryRecipient(); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); @@ -281,7 +277,7 @@ public class DecryptingQueue { return; } - if (!Session.hasSession(context, masterSecret, recipient)) { + if (!sessionStore.contains(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) { Log.w("DecryptingQueue", "No such recipient session for MMS..."); database.markAsNoSession(messageId, threadId); return; @@ -316,24 +312,15 @@ public class DecryptingQueue { Log.w("DecryptingQueue", "Successfully decrypted MMS!"); database.insertSecureDecryptedMessageInbox(masterSecret, new IncomingMediaMessage(plaintextPdu), threadId); database.delete(messageId); - } catch (RecipientFormattingException rfe) { + } catch (RecipientFormattingException | IOException | MmsException | InvalidMessageException rfe) { Log.w("DecryptingQueue", rfe); database.markAsDecryptFailed(messageId, threadId); - } catch (InvalidMessageException ime) { - Log.w("DecryptingQueue", ime); - database.markAsDecryptFailed(messageId, threadId); } catch (DuplicateMessageException dme) { Log.w("DecryptingQueue", dme); database.markAsDecryptDuplicate(messageId, threadId); } catch (LegacyMessageException lme) { Log.w("DecryptingQueue", lme); database.markAsLegacyVersion(messageId, threadId); - } catch (MmsException mme) { - Log.w("DecryptingQueue", mme); - database.markAsDecryptFailed(messageId, threadId); - } catch (IOException e) { - Log.w("DecryptingQueue", e); - database.markAsDecryptFailed(messageId, threadId); } } } @@ -373,6 +360,7 @@ public class DecryptingQueue { String plaintextBody; try { + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false); Recipient recipient = recipients.getPrimaryRecipient(); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId); @@ -380,7 +368,7 @@ public class DecryptingQueue { SmsTransportDetails transportDetails = new SmsTransportDetails(); byte[] decodedCiphertext = transportDetails.getDecodedMessage(body.getBytes()); - if (!Session.hasSession(context, masterSecret, recipient)) { + if (!sessionStore.contains(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) { if (WhisperMessage.isLegacy(decodedCiphertext)) database.markAsLegacyVersion(messageId); else database.markAsNoSession(messageId); return; @@ -393,11 +381,11 @@ public class DecryptingQueue { if (isEndSession && "TERMINATE".equals(plaintextBody) && - SessionRecordV2.hasSession(context, masterSecret, recipientDevice)) + sessionStore.contains(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) { - Session.abortSessionFor(context, recipient); + sessionStore.delete(recipientDevice.getRecipientId(), recipientDevice.getDeviceId()); } - } catch (InvalidMessageException e) { + } catch (InvalidMessageException | IOException | RecipientFormattingException e) { Log.w("DecryptionQueue", e); database.markAsDecryptFailed(messageId); return; @@ -405,14 +393,6 @@ public class DecryptingQueue { Log.w("DecryptionQueue", lme); database.markAsLegacyVersion(messageId); return; - } catch (RecipientFormattingException e) { - Log.w("DecryptionQueue", e); - database.markAsDecryptFailed(messageId); - return; - } catch (IOException e) { - Log.w("DecryptionQueue", e); - database.markAsDecryptFailed(messageId); - return; } catch (DuplicateMessageException e) { Log.w("DecryptionQueue", e); database.markAsDecryptDuplicate(messageId); diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java index 979f19c04..24abec36f 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeInitiator.java @@ -30,9 +30,11 @@ import org.thoughtcrime.securesms.util.Dialogs; import org.whispersystems.libaxolotl.IdentityKeyPair; import org.whispersystems.libaxolotl.ecc.Curve; import org.whispersystems.libaxolotl.ecc.ECKeyPair; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -70,12 +72,12 @@ public class KeyExchangeInitiator { ephemeralKey.getPublicKey(), identityKey.getPublicKey()); - OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize()); - RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); + OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize()); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + SessionRecord sessionRecord = sessionStore.get(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); - SessionRecordV2 sessionRecordV2 = new SessionRecordV2(context, masterSecret, recipientDevice); - sessionRecordV2.getSessionState().setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey); - sessionRecordV2.save(); + sessionRecord.getSessionState().setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey); + sessionStore.put(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID, sessionRecord); MessageSender.send(context, masterSecret, textMessage, -1, false); } @@ -83,11 +85,10 @@ public class KeyExchangeInitiator { private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret, Recipient recipient) { - RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); - return - new SessionRecordV2(context, masterSecret, recipientDevice) - .getSessionState() - .hasPendingKeyExchange(); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + SessionRecord sessionRecord = sessionStore.get(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); + + return sessionRecord.getSessionState().hasPendingPreKey(); } private static int getRandomSequence() { diff --git a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java index 36477fa17..ab030a885 100644 --- a/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java +++ b/src/org/thoughtcrime/securesms/crypto/KeyExchangeProcessorV2.java @@ -21,13 +21,14 @@ import org.whispersystems.libaxolotl.ecc.ECKeyPair; import org.whispersystems.libaxolotl.ecc.ECPublicKey; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; import org.whispersystems.libaxolotl.ratchet.RatchetingSession; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.push.PreKeyEntity; import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.PreKeyRecord; import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.Session; -import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Medium; /** @@ -41,14 +42,14 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { private Context context; private RecipientDevice recipientDevice; private MasterSecret masterSecret; - private SessionRecordV2 sessionRecord; + private SessionStore sessionStore; public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice) { this.context = context; this.recipientDevice = recipientDevice; this.masterSecret = masterSecret; - this.sessionRecord = new SessionRecordV2(context, masterSecret, recipientDevice); + this.sessionStore = new TextSecureSessionStore(context, masterSecret); } public boolean isTrusted(PreKeyWhisperMessage message) { @@ -70,7 +71,10 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { } public boolean isStale(KeyExchangeMessage m) { - KeyExchangeMessageV2 message = (KeyExchangeMessageV2)m; + KeyExchangeMessageV2 message = (KeyExchangeMessageV2) m; + SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(), + recipientDevice.getDeviceId()); + return message.isResponse() && (!sessionRecord.getSessionState().hasPendingKeyExchange() || @@ -88,7 +92,9 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { Log.w("KeyExchangeProcessor", "Received pre-key with local key ID: " + preKeyId); - if (!PreKeyRecord.hasRecord(context, preKeyId) && SessionRecordV2.hasSession(context, masterSecret, recipientDevice)) { + if (!PreKeyRecord.hasRecord(context, preKeyId) && + sessionStore.contains(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) + { Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through..."); return; } @@ -96,13 +102,15 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { if (!PreKeyRecord.hasRecord(context, preKeyId)) throw new InvalidKeyIdException("No such prekey: " + preKeyId); + SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(), + recipientDevice.getDeviceId()); PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId); ECKeyPair ourBaseKey = preKeyRecord.getKeyPair(); ECKeyPair ourEphemeralKey = ourBaseKey; IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey(); - if (!simultaneousInitiate) sessionRecord.clear(); + if (!simultaneousInitiate) sessionRecord.reset(); else sessionRecord.archiveCurrentState(); RatchetingSession.initializeSession(sessionRecord.getSessionState(), @@ -110,13 +118,12 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { ourEphemeralKey, theirEphemeralKey, ourIdentityKey, theirIdentityKey); - Session.clearV1SessionFor(context, recipientDevice.getRecipient()); sessionRecord.getSessionState().setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context)); sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId()); if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true); - sessionRecord.save(); + sessionStore.put(recipientDevice.getRecipientId(), recipientDevice.getDeviceId(), sessionRecord); if (preKeyId != Medium.MAX_VALUE) { PreKeyRecord.delete(context, preKeyId); @@ -131,6 +138,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { public void processKeyExchangeMessage(PreKeyEntity message, long threadId) throws InvalidKeyException { + SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(), + recipientDevice.getDeviceId()); ECKeyPair ourBaseKey = Curve.generateKeyPair(true); ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true); ECPublicKey theirBaseKey = message.getPublicKey(); @@ -139,7 +148,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret); if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState(); - else sessionRecord.clear(); + else sessionRecord.reset(); RatchetingSession.initializeSession(sessionRecord.getSessionState(), ourBaseKey, theirBaseKey, ourEphemeralKey, @@ -149,7 +158,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { sessionRecord.getSessionState().setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context)); sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId()); - sessionRecord.save(); + sessionStore.put(recipientDevice.getRecipientId(), recipientDevice.getDeviceId(), sessionRecord); DatabaseFactory.getIdentityDatabase(context) .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey()); @@ -164,11 +173,13 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { throws InvalidMessageException { try { - KeyExchangeMessageV2 message = (KeyExchangeMessageV2)_message; - Recipient recipient = RecipientFactory.getRecipientsForIds(context, - String.valueOf(recipientDevice.getRecipientId()), - false) - .getPrimaryRecipient(); + KeyExchangeMessageV2 message = (KeyExchangeMessageV2) _message; + SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(), + recipientDevice.getDeviceId()); + Recipient recipient = RecipientFactory.getRecipientsForIds(context, + String.valueOf(recipientDevice.getRecipientId()), + false) + .getPrimaryRecipient(); Log.w("KeyExchangeProcessorV2", "Received key exchange with sequence: " + message.getSequence()); @@ -219,7 +230,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { ECKeyPair ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey(); IdentityKeyPair ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey(); - sessionRecord.clear(); + sessionRecord.reset(); RatchetingSession.initializeSession(sessionRecord.getSessionState(), ourBaseKey, message.getBaseKey(), @@ -227,8 +238,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor { ourIdentityKey, message.getIdentityKey()); sessionRecord.getSessionState().setSessionVersion(message.getVersion()); - Session.clearV1SessionFor(context, recipientDevice.getRecipient()); - sessionRecord.save(); + sessionStore.put(recipientDevice.getRecipientId(), recipientDevice.getDeviceId(), sessionRecord); DatabaseFactory.getIdentityDatabase(context) .saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey()); diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 411487a08..ada85addb 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -33,7 +33,6 @@ import org.whispersystems.libaxolotl.IdentityKey; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.textsecure.crypto.MasterCipher; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.storage.Session; import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.Util; @@ -403,8 +402,15 @@ public class DatabaseFactory { String name = session.getName(); if (name.matches("[0-9]+")) { - long recipientId = Long.parseLong(name); - IdentityKey identityKey = Session.getRemoteIdentityKey(context, masterSecret, recipientId); + long recipientId = Long.parseLong(name); + IdentityKey identityKey = null; + // NOTE (4/21/14) -- At this moment in time, we're forgetting the ability to parse + // V1 session records. Despite our usual attempts to avoid using shared code in the + // upgrade path, this is too complex to put here directly. Thus, unfortunately + // this operation is now lost to the ages. From the git log, it seems to have been + // almost exactly a year since this went in, so hopefully the bulk of people have + // already upgraded. +// IdentityKey identityKey = Session.getRemoteIdentityKey(context, masterSecret, recipientId); if (identityKey != null) { MasterCipher masterCipher = new MasterCipher(masterSecret); diff --git a/src/org/thoughtcrime/securesms/service/PushReceiver.java b/src/org/thoughtcrime/securesms/service/PushReceiver.java index aa7d4c262..c0675d6be 100644 --- a/src/org/thoughtcrime/securesms/service/PushReceiver.java +++ b/src/org/thoughtcrime/securesms/service/PushReceiver.java @@ -29,12 +29,13 @@ import org.whispersystems.libaxolotl.InvalidKeyException; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.InvalidVersionException; import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.push.IncomingPushMessage; import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent; import org.whispersystems.textsecure.storage.InvalidKeyIdException; import org.whispersystems.textsecure.storage.RecipientDevice; -import org.whispersystems.textsecure.storage.Session; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Base64; import ws.com.google.android.mms.MmsException; @@ -131,19 +132,12 @@ public class PushReceiver { MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second); } - } catch (InvalidKeyException e) { - Log.w("PushReceiver", e); - handleReceivedCorruptedKey(masterSecret, message, false); } catch (InvalidVersionException e) { Log.w("PushReceiver", e); handleReceivedCorruptedKey(masterSecret, message, true); - } catch (InvalidKeyIdException e) { - Log.w("PushReceiver", e); - handleReceivedCorruptedKey(masterSecret, message, false); - } catch (InvalidMessageException e) { - Log.w("PushReceiver", e); - handleReceivedCorruptedKey(masterSecret, message, false); - } catch (RecipientFormattingException e) { + } catch (InvalidKeyException | InvalidKeyIdException | InvalidMessageException | + RecipientFormattingException e) + { Log.w("PushReceiver", e); handleReceivedCorruptedKey(masterSecret, message, false); } @@ -189,7 +183,9 @@ public class PushReceiver { Pair messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage); database.updateMessageBody(masterSecret, messageAndThreadId.first, messageContent.getBody()); - Session.abortSessionFor(context, recipient); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + sessionStore.deleteAll(recipient.getRecipientId()); + KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, messageAndThreadId.second); } catch (RecipientFormattingException e) { Log.w("PushReceiver", e); diff --git a/src/org/thoughtcrime/securesms/service/SmsSender.java b/src/org/thoughtcrime/securesms/service/SmsSender.java index e42319967..696be3abc 100644 --- a/src/org/thoughtcrime/securesms/service/SmsSender.java +++ b/src/org/thoughtcrime/securesms/service/SmsSender.java @@ -42,8 +42,9 @@ import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.transport.UniversalTransport; import org.thoughtcrime.securesms.transport.UntrustedIdentityException; +import org.whispersystems.libaxolotl.state.SessionStore; import org.whispersystems.textsecure.crypto.MasterSecret; -import org.whispersystems.textsecure.storage.Session; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; public class SmsSender { @@ -61,7 +62,7 @@ public class SmsSender { if (SendReceiveService.SEND_SMS_ACTION.equals(intent.getAction())) { handleSendMessage(masterSecret, intent); } else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) { - handleSentMessage(intent); + handleSentMessage(masterSecret, intent); } else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) { handleDeliveredMessage(intent); } @@ -116,7 +117,7 @@ public class SmsSender { } } - private void handleSentMessage(Intent intent) { + private void handleSentMessage(MasterSecret masterSecret, Intent intent) { long messageId = intent.getLongExtra("message_id", -1); int result = intent.getIntExtra("ResultCode", -31337); boolean upgraded = intent.getBooleanExtra("upgraded", false); @@ -138,7 +139,8 @@ public class SmsSender { if (record != null && record.isEndSession()) { Log.w("SmsSender", "Ending session..."); - Session.abortSessionFor(context, record.getIndividualRecipient()); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + sessionStore.deleteAll(record.getIndividualRecipient().getRecipientId()); KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, record.getThreadId()); } diff --git a/src/org/thoughtcrime/securesms/transport/MmsTransport.java b/src/org/thoughtcrime/securesms/transport/MmsTransport.java index 0b272cf24..0ad8d8dad 100644 --- a/src/org/thoughtcrime/securesms/transport/MmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/MmsTransport.java @@ -38,7 +38,7 @@ 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.Session; +import org.whispersystems.textsecure.storage.SessionUtil; import org.whispersystems.textsecure.util.Hex; import java.io.IOException; @@ -168,13 +168,15 @@ public class MmsTransport { return encryptedPdu; } - private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) throws InsecureFallbackApprovalException { + private byte[] getEncryptedPdu(MasterSecret masterSecret, String recipientString, byte[] pduBytes) + throws InsecureFallbackApprovalException + { try { TextTransport transportDetails = new TextTransport(); Recipient recipient = RecipientFactory.getRecipientsFromString(context, recipientString, false).getPrimaryRecipient(); RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); - if (!Session.hasEncryptCapableSession(context, masterSecret, recipient, recipientDevice)) { + if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, recipientDevice)) { throw new InsecureFallbackApprovalException("No session exists for this secure message."); } diff --git a/src/org/thoughtcrime/securesms/transport/PushTransport.java b/src/org/thoughtcrime/securesms/transport/PushTransport.java index 221bdfdb6..25a3950b8 100644 --- a/src/org/thoughtcrime/securesms/transport/PushTransport.java +++ b/src/org/thoughtcrime/securesms/transport/PushTransport.java @@ -38,6 +38,7 @@ 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.SessionStore; import org.whispersystems.textsecure.crypto.AttachmentCipher; import org.whispersystems.textsecure.crypto.MasterSecret; import org.whispersystems.textsecure.crypto.SessionCipherFactory; @@ -55,7 +56,8 @@ import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.StaleDevices; import org.whispersystems.textsecure.push.StaleDevicesException; import org.whispersystems.textsecure.push.UnregisteredUserException; -import org.whispersystems.textsecure.storage.SessionRecordV2; +import org.whispersystems.textsecure.storage.SessionUtil; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import org.whispersystems.textsecure.util.Base64; import org.whispersystems.textsecure.util.InvalidNumberException; @@ -93,7 +95,8 @@ public class PushTransport extends BaseTransport { deliver(socket, recipient, threadId, plaintext); if (message.isEndSession()) { - SessionRecordV2.deleteAll(context, recipient); + SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret); + sessionStore.deleteAll(recipient.getRecipientId()); KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, threadId); } @@ -121,8 +124,8 @@ public class PushTransport extends BaseTransport { recipients = RecipientFactory.getRecipientsFromString(context, destination, false); } - List untrustedIdentities = new LinkedList(); - List unregisteredUsers = new LinkedList(); + List untrustedIdentities = new LinkedList<>(); + List unregisteredUsers = new LinkedList<>(); for (Recipient recipient : recipients.getRecipientsList()) { try { @@ -164,7 +167,7 @@ public class PushTransport extends BaseTransport { private List getPushAttachmentPointers(PushServiceSocket socket, PduBody body) throws IOException { - List attachments = new LinkedList(); + List attachments = new LinkedList<>(); for (int i=0;i messages = new LinkedList(); + List messages = new LinkedList<>(); messages.add(new OutgoingPushMessage(masterDevice, masterBody)); - for (int deviceId : SessionRecordV2.getSessionSubDevices(context, recipient)) { + for (int deviceId : sessionStore.getSubDeviceSessions(recipientId)) { PushAddress device = PushAddress.create(context, recipientId, e164number, deviceId); PushBody body = getEncryptedMessage(socket, threadId, device, plaintext); @@ -328,9 +325,7 @@ public class PushTransport extends BaseTransport { PushAddress pushAddress, byte[] plaintext) throws IOException, UntrustedIdentityException { - if (!SessionRecordV2.hasSession(context, masterSecret, pushAddress) || - SessionRecordV2.needsRefresh(context, masterSecret, pushAddress)) - { + if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, pushAddress)) { try { List preKeys = socket.getPreKeys(pushAddress); diff --git a/src/org/thoughtcrime/securesms/transport/SmsTransport.java b/src/org/thoughtcrime/securesms/transport/SmsTransport.java index e56f1d61c..399f2da0d 100644 --- a/src/org/thoughtcrime/securesms/transport/SmsTransport.java +++ b/src/org/thoughtcrime/securesms/transport/SmsTransport.java @@ -32,10 +32,13 @@ 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.Session; +import org.whispersystems.textsecure.storage.SessionUtil; +import org.whispersystems.textsecure.storage.TextSecureSessionStore; import java.util.ArrayList; @@ -174,7 +177,7 @@ public class SmsTransport extends BaseTransport { RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID); - if (!Session.hasEncryptCapableSession(context, masterSecret, recipient, recipientDevice)) { + if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, recipientDevice)) { throw new InsecureFallbackApprovalException("No session exists for this secure message."); } diff --git a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java index bd7d6eb78..440251d81 100644 --- a/src/org/thoughtcrime/securesms/transport/UniversalTransport.java +++ b/src/org/thoughtcrime/securesms/transport/UniversalTransport.java @@ -38,7 +38,7 @@ import org.whispersystems.textsecure.directory.NotInDirectoryException; import org.whispersystems.textsecure.push.ContactTokenDetails; import org.whispersystems.textsecure.push.PushServiceSocket; import org.whispersystems.textsecure.push.UnregisteredUserException; -import org.whispersystems.textsecure.storage.Session; +import org.whispersystems.textsecure.storage.SessionUtil; import org.whispersystems.textsecure.util.DirectoryUtil; import org.whispersystems.textsecure.util.InvalidNumberException; @@ -177,7 +177,7 @@ public class UniversalTransport { Log.w("UniversalTransport", "Falling back to MMS"); DatabaseFactory.getMmsDatabase(context).markAsForcedSms(mediaMessage.getDatabaseMessageId()); return mmsTransport.deliver(mediaMessage); - } else if (!Session.hasEncryptCapableSession(context, masterSecret, recipient)) { + } else if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, recipient)) { Log.w("UniversalTransport", "Marking message as pending insecure SMS fallback"); throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS"); } else { @@ -199,7 +199,7 @@ public class UniversalTransport { Log.w("UniversalTransport", "Falling back to SMS"); DatabaseFactory.getSmsDatabase(context).markAsForcedSms(smsMessage.getId()); smsTransport.deliver(smsMessage); - } else if (!Session.hasEncryptCapableSession(context, masterSecret, recipient)) { + } else if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, recipient)) { Log.w("UniversalTransport", "Marking message as pending insecure fallback."); throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS"); } else {