mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-22 08:48:26 +00:00
Reorganize session store load/store operations.
This commit is contained in:
parent
d902c12941
commit
14b8f97de2
@ -67,6 +67,11 @@ android {
|
|||||||
targetSdkVersion 19
|
targetSdkVersion 19
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
|
@ -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<SessionState> 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<SessionState> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,7 @@ package org.whispersystems.test;
|
|||||||
import org.whispersystems.libaxolotl.IdentityKey;
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
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.ECKeyPair;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
||||||
|
@ -1,44 +1,63 @@
|
|||||||
package org.whispersystems.test;
|
package org.whispersystems.test;
|
||||||
|
|
||||||
import org.whispersystems.libaxolotl.SessionState;
|
import org.whispersystems.libaxolotl.state.SessionRecord;
|
||||||
import org.whispersystems.libaxolotl.SessionStore;
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
|
import org.whispersystems.libaxolotl.util.Pair;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class InMemorySessionStore implements SessionStore {
|
public class InMemorySessionStore implements SessionStore {
|
||||||
|
|
||||||
private SessionState currentSessionState;
|
private Map<Pair<Long, Integer>, SessionRecord> sessions = new HashMap<>();
|
||||||
private List<SessionState> previousSessionStates;
|
|
||||||
|
|
||||||
private SessionState checkedOutSessionState;
|
public InMemorySessionStore() {}
|
||||||
private List<SessionState> checkedOutPreviousSessionStates;
|
|
||||||
|
|
||||||
public InMemorySessionStore(SessionState sessionState) {
|
@Override
|
||||||
this.currentSessionState = sessionState;
|
public SessionRecord get(long recipientId, int deviceId) {
|
||||||
this.previousSessionStates = new LinkedList<>();
|
if (contains(recipientId, deviceId)) {
|
||||||
this.checkedOutPreviousSessionStates = new LinkedList<>();
|
return new InMemorySessionRecord(sessions.get(new Pair<>(recipientId, deviceId)));
|
||||||
|
} else {
|
||||||
|
return new InMemorySessionRecord();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SessionState getSessionState() {
|
public List<Integer> getSubDeviceSessions(long recipientId) {
|
||||||
checkedOutSessionState = new InMemorySessionState(currentSessionState);
|
List<Integer> deviceIds = new LinkedList<>();
|
||||||
return checkedOutSessionState;
|
|
||||||
|
for (Pair<Long, Integer> key : sessions.keySet()) {
|
||||||
|
if (key.first() == recipientId) {
|
||||||
|
deviceIds.add(key.second());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return deviceIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SessionState> getPreviousSessionStates() {
|
public void put(long recipientId, int deviceId, SessionRecord record) {
|
||||||
checkedOutPreviousSessionStates = new LinkedList<>();
|
sessions.put(new Pair<>(recipientId, deviceId), record);
|
||||||
for (SessionState state : previousSessionStates) {
|
|
||||||
checkedOutPreviousSessionStates.add(new InMemorySessionState(state));
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkedOutPreviousSessionStates;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void save() {
|
public boolean contains(long recipientId, int deviceId) {
|
||||||
this.currentSessionState = this.checkedOutSessionState;
|
return sessions.containsKey(new Pair<>(recipientId, deviceId));
|
||||||
this.previousSessionStates = this.checkedOutPreviousSessionStates;
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void delete(long recipientId, int deviceId) {
|
||||||
|
sessions.remove(new Pair<>(recipientId, deviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteAll(long recipientId) {
|
||||||
|
for (Pair<Long, Integer> key : sessions.keySet()) {
|
||||||
|
if (key.first() == recipientId) {
|
||||||
|
sessions.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,9 @@ import org.whispersystems.libaxolotl.InvalidKeyException;
|
|||||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
import org.whispersystems.libaxolotl.LegacyMessageException;
|
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||||
import org.whispersystems.libaxolotl.SessionCipher;
|
import org.whispersystems.libaxolotl.SessionCipher;
|
||||||
import org.whispersystems.libaxolotl.SessionState;
|
import org.whispersystems.libaxolotl.state.SessionRecord;
|
||||||
import org.whispersystems.libaxolotl.SessionStore;
|
import org.whispersystems.libaxolotl.state.SessionState;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||||
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
||||||
@ -24,16 +25,19 @@ public class SessionCipherTest extends AndroidTestCase {
|
|||||||
throws InvalidKeyException, DuplicateMessageException,
|
throws InvalidKeyException, DuplicateMessageException,
|
||||||
LegacyMessageException, InvalidMessageException
|
LegacyMessageException, InvalidMessageException
|
||||||
{
|
{
|
||||||
SessionState aliceSessionState = new InMemorySessionState();
|
SessionRecord aliceSessionRecord = new InMemorySessionRecord();
|
||||||
SessionState bobSessionState = new InMemorySessionState();
|
SessionRecord bobSessionRecord = new InMemorySessionRecord();
|
||||||
|
|
||||||
initializeSessions(aliceSessionState, bobSessionState);
|
initializeSessions(aliceSessionRecord.getSessionState(), bobSessionRecord.getSessionState());
|
||||||
|
|
||||||
SessionStore aliceSessionStore = new InMemorySessionStore(aliceSessionState);
|
SessionStore aliceSessionStore = new InMemorySessionStore();
|
||||||
SessionStore bobSessionStore = new InMemorySessionStore(bobSessionState);
|
SessionStore bobSessionStore = new InMemorySessionStore();
|
||||||
|
|
||||||
SessionCipher aliceCipher = new SessionCipher(aliceSessionStore);
|
aliceSessionStore.put(2L, 1, aliceSessionRecord);
|
||||||
SessionCipher bobCipher = new SessionCipher(bobSessionStore);
|
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();
|
byte[] alicePlaintext = "This is a plaintext message.".getBytes();
|
||||||
CiphertextMessage message = aliceCipher.encrypt(alicePlaintext);
|
CiphertextMessage message = aliceCipher.encrypt(alicePlaintext);
|
||||||
|
@ -6,7 +6,7 @@ import org.whispersystems.libaxolotl.IdentityKey;
|
|||||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||||
import org.whispersystems.test.InMemorySessionState;
|
import org.whispersystems.test.InMemorySessionState;
|
||||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
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.Curve;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
|
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
|
||||||
|
@ -25,6 +25,9 @@ import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
|||||||
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
||||||
import org.whispersystems.libaxolotl.ratchet.MessageKeys;
|
import org.whispersystems.libaxolotl.ratchet.MessageKeys;
|
||||||
import org.whispersystems.libaxolotl.ratchet.RootKey;
|
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.ByteUtil;
|
||||||
import org.whispersystems.libaxolotl.util.Pair;
|
import org.whispersystems.libaxolotl.util.Pair;
|
||||||
|
|
||||||
@ -45,14 +48,19 @@ public class SessionCipher {
|
|||||||
private static final Object SESSION_LOCK = new Object();
|
private static final Object SESSION_LOCK = new Object();
|
||||||
|
|
||||||
private final SessionStore sessionStore;
|
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.sessionStore = sessionStore;
|
||||||
|
this.recipientId = recipientId;
|
||||||
|
this.deviceId = deviceId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CiphertextMessage encrypt(byte[] paddedMessage) {
|
public CiphertextMessage encrypt(byte[] paddedMessage) {
|
||||||
synchronized (SESSION_LOCK) {
|
synchronized (SESSION_LOCK) {
|
||||||
SessionState sessionState = sessionStore.getSessionState();
|
SessionRecord sessionRecord = sessionStore.get(recipientId, deviceId);
|
||||||
|
SessionState sessionState = sessionRecord.getSessionState();
|
||||||
ChainKey chainKey = sessionState.getSenderChainKey();
|
ChainKey chainKey = sessionState.getSenderChainKey();
|
||||||
MessageKeys messageKeys = chainKey.getMessageKeys();
|
MessageKeys messageKeys = chainKey.getMessageKeys();
|
||||||
ECPublicKey senderEphemeral = sessionState.getSenderEphemeral();
|
ECPublicKey senderEphemeral = sessionState.getSenderEphemeral();
|
||||||
@ -74,7 +82,7 @@ public class SessionCipher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sessionState.setSenderChainKey(chainKey.getNextChainKey());
|
sessionState.setSenderChainKey(chainKey.getNextChainKey());
|
||||||
sessionStore.save();
|
sessionStore.put(recipientId, deviceId, sessionRecord);
|
||||||
return ciphertextMessage;
|
return ciphertextMessage;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -83,13 +91,14 @@ public class SessionCipher {
|
|||||||
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
|
throws InvalidMessageException, DuplicateMessageException, LegacyMessageException
|
||||||
{
|
{
|
||||||
synchronized (SESSION_LOCK) {
|
synchronized (SESSION_LOCK) {
|
||||||
SessionState sessionState = sessionStore.getSessionState();
|
SessionRecord sessionRecord = sessionStore.get(recipientId, deviceId);
|
||||||
List<SessionState> previousStates = sessionStore.getPreviousSessionStates();
|
SessionState sessionState = sessionRecord.getSessionState();
|
||||||
List<Exception> exceptions = new LinkedList<Exception>();
|
List<SessionState> previousStates = sessionRecord.getPreviousSessionStates();
|
||||||
|
List<Exception> exceptions = new LinkedList<>();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
byte[] plaintext = decrypt(sessionState, decodedMessage);
|
byte[] plaintext = decrypt(sessionState, decodedMessage);
|
||||||
sessionStore.save();
|
sessionStore.put(recipientId, deviceId, sessionRecord);
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
} catch (InvalidMessageException e) {
|
} catch (InvalidMessageException e) {
|
||||||
@ -99,7 +108,7 @@ public class SessionCipher {
|
|||||||
for (SessionState previousState : previousStates) {
|
for (SessionState previousState : previousStates) {
|
||||||
try {
|
try {
|
||||||
byte[] plaintext = decrypt(previousState, decodedMessage);
|
byte[] plaintext = decrypt(previousState, decodedMessage);
|
||||||
sessionStore.save();
|
sessionStore.put(recipientId, deviceId, sessionRecord);
|
||||||
|
|
||||||
return plaintext;
|
return plaintext;
|
||||||
} catch (InvalidMessageException e) {
|
} catch (InvalidMessageException e) {
|
||||||
@ -137,7 +146,8 @@ public class SessionCipher {
|
|||||||
|
|
||||||
public int getRemoteRegistrationId() {
|
public int getRemoteRegistrationId() {
|
||||||
synchronized (SESSION_LOCK) {
|
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());
|
messageKeys.getCounter());
|
||||||
|
|
||||||
return cipher.doFinal(plaintext);
|
return cipher.doFinal(plaintext);
|
||||||
} catch (IllegalBlockSizeException e) {
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||||
throw new AssertionError(e);
|
|
||||||
} catch (BadPaddingException e) {
|
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,9 +222,7 @@ public class SessionCipher {
|
|||||||
messageKeys.getCipherKey(),
|
messageKeys.getCipherKey(),
|
||||||
messageKeys.getCounter());
|
messageKeys.getCounter());
|
||||||
return cipher.doFinal(cipherText);
|
return cipher.doFinal(cipherText);
|
||||||
} catch (IllegalBlockSizeException e) {
|
} catch (IllegalBlockSizeException | BadPaddingException e) {
|
||||||
throw new AssertionError(e);
|
|
||||||
} catch (BadPaddingException e) {
|
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -232,13 +238,9 @@ public class SessionCipher {
|
|||||||
cipher.init(mode, key, iv);
|
cipher.init(mode, key, iv);
|
||||||
|
|
||||||
return cipher;
|
return cipher;
|
||||||
} catch (NoSuchAlgorithmException e) {
|
} catch (NoSuchAlgorithmException | NoSuchPaddingException | java.security.InvalidKeyException |
|
||||||
throw new AssertionError(e);
|
InvalidAlgorithmParameterException e)
|
||||||
} catch (NoSuchPaddingException e) {
|
{
|
||||||
throw new AssertionError(e);
|
|
||||||
} catch (java.security.InvalidKeyException e) {
|
|
||||||
throw new AssertionError(e);
|
|
||||||
} catch (InvalidAlgorithmParameterException e) {
|
|
||||||
throw new AssertionError(e);
|
throw new AssertionError(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
package org.whispersystems.libaxolotl;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public interface SessionStore {
|
|
||||||
|
|
||||||
public SessionState getSessionState();
|
|
||||||
public List<SessionState> getPreviousSessionStates();
|
|
||||||
public void save();
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
@ -16,18 +16,15 @@
|
|||||||
*/
|
*/
|
||||||
package org.whispersystems.libaxolotl.ratchet;
|
package org.whispersystems.libaxolotl.ratchet;
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import org.whispersystems.libaxolotl.IdentityKey;
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
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.Curve;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
import org.whispersystems.libaxolotl.kdf.DerivedSecrets;
|
import org.whispersystems.libaxolotl.kdf.DerivedSecrets;
|
||||||
import org.whispersystems.libaxolotl.kdf.HKDF;
|
import org.whispersystems.libaxolotl.kdf.HKDF;
|
||||||
import org.whispersystems.libaxolotl.util.Hex;
|
|
||||||
import org.whispersystems.libaxolotl.util.Pair;
|
import org.whispersystems.libaxolotl.util.Pair;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package org.whispersystems.libaxolotl.state;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface SessionRecord {
|
||||||
|
|
||||||
|
public SessionState getSessionState();
|
||||||
|
public List<SessionState> getPreviousSessionStates();
|
||||||
|
public void reset();
|
||||||
|
public void archiveCurrentState();
|
||||||
|
public byte[] serialize();
|
||||||
|
|
||||||
|
}
|
@ -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.ECKeyPair;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
import org.whispersystems.libaxolotl.ratchet.ChainKey;
|
@ -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<Integer> 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);
|
||||||
|
|
||||||
|
}
|
@ -31,6 +31,11 @@ android {
|
|||||||
compileSdkVersion 19
|
compileSdkVersion 19
|
||||||
buildToolsVersion '19.1.0'
|
buildToolsVersion '19.1.0'
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_1_7
|
||||||
|
targetCompatibility JavaVersion.VERSION_1_7
|
||||||
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
main {
|
main {
|
||||||
|
@ -20,8 +20,9 @@ package org.whispersystems.textsecure.crypto;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import org.whispersystems.libaxolotl.SessionCipher;
|
import org.whispersystems.libaxolotl.SessionCipher;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
|
||||||
|
|
||||||
public class SessionCipherFactory {
|
public class SessionCipherFactory {
|
||||||
|
|
||||||
@ -29,9 +30,10 @@ public class SessionCipherFactory {
|
|||||||
MasterSecret masterSecret,
|
MasterSecret masterSecret,
|
||||||
RecipientDevice recipient)
|
RecipientDevice recipient)
|
||||||
{
|
{
|
||||||
if (SessionRecordV2.hasSession(context, masterSecret, recipient)) {
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
SessionRecordV2 record = new SessionRecordV2(context, masterSecret, recipient);
|
|
||||||
return new SessionCipher(record);
|
if (sessionStore.contains(recipient.getRecipientId(), recipient.getDeviceId())) {
|
||||||
|
return new SessionCipher(sessionStore, recipient.getRecipientId(), recipient.getDeviceId());
|
||||||
} else {
|
} else {
|
||||||
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
throw new AssertionError("Attempt to initialize cipher for non-existing session.");
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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<SessionState> previousStates = new LinkedList<SessionState>();
|
|
||||||
|
|
||||||
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<SessionState> getPreviousSessionStates() {
|
|
||||||
return previousStates;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<Integer> getSessionSubDevices(Context context, CanonicalRecipient recipient) {
|
|
||||||
List<Integer> results = new LinkedList<Integer>();
|
|
||||||
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<Integer> 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<SessionState>();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void archiveCurrentState() {
|
|
||||||
this.previousStates.add(sessionState);
|
|
||||||
this.sessionState = new TextSecureSessionState(SessionStructure.newBuilder().build());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
synchronized (FILE_LOCK) {
|
|
||||||
try {
|
|
||||||
List<SessionStructure> previousStructures = new LinkedList<SessionStructure>();
|
|
||||||
|
|
||||||
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<SessionState>();
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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<SessionState> 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<SessionState> 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<SessionStructure> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -23,7 +23,7 @@ import com.google.protobuf.ByteString;
|
|||||||
import org.whispersystems.libaxolotl.IdentityKey;
|
import org.whispersystems.libaxolotl.IdentityKey;
|
||||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
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.Curve;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
|
import org.whispersystems.libaxolotl.ecc.ECPrivateKey;
|
||||||
|
@ -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<Integer> devices = getSubDeviceSessions(recipientId);
|
||||||
|
|
||||||
|
delete(recipientId, RecipientDevice.DEFAULT_DEVICE_ID);
|
||||||
|
|
||||||
|
for (int device : devices) {
|
||||||
|
delete(recipientId, device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Integer> getSubDeviceSessions(long recipientId) {
|
||||||
|
List<Integer> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
@ -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() + "");
|
|
||||||
}
|
|
||||||
}
|
|
@ -30,10 +30,10 @@ import org.thoughtcrime.securesms.protocol.Tag;
|
|||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activity which prompts the user to initiate a secure
|
* Activity which prompts the user to initiate a secure
|
||||||
@ -65,8 +65,8 @@ public class AutoInitiateActivity extends Activity {
|
|||||||
|
|
||||||
private void initializeResources() {
|
private void initializeResources() {
|
||||||
this.threadId = this.getIntent().getLongExtra("threadId", -1);
|
this.threadId = this.getIntent().getLongExtra("threadId", -1);
|
||||||
this.recipient = (Recipient)this.getIntent().getParcelableExtra("recipient");
|
this.recipient = this.getIntent().getParcelableExtra("recipient");
|
||||||
this.masterSecret = (MasterSecret)this.getIntent().getParcelableExtra("masterSecret");
|
this.masterSecret = this.getIntent().getParcelableExtra("masterSecret");
|
||||||
|
|
||||||
((Button)findViewById(R.id.initiate_button)).setOnClickListener(new OkListener());
|
((Button)findViewById(R.id.initiate_button)).setOnClickListener(new OkListener());
|
||||||
((Button)findViewById(R.id.cancel_button)).setOnClickListener(new CancelListener());
|
((Button)findViewById(R.id.cancel_button)).setOnClickListener(new CancelListener());
|
||||||
@ -117,6 +117,7 @@ public class AutoInitiateActivity extends Activity {
|
|||||||
MasterSecret masterSecret,
|
MasterSecret masterSecret,
|
||||||
Recipient recipient)
|
Recipient recipient)
|
||||||
{
|
{
|
||||||
return !Session.hasSession(context, masterSecret, recipient);
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
|
return sessionStore.contains(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,11 +103,11 @@ import org.thoughtcrime.securesms.util.GroupUtil;
|
|||||||
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
|
||||||
import org.whispersystems.textsecure.util.Util;
|
import org.whispersystems.textsecure.util.Util;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -316,9 +316,11 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
|
|||||||
@Override
|
@Override
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
|
||||||
if (isEncryptedConversation && isSingleConversation()) {
|
if (isEncryptedConversation && isSingleConversation()) {
|
||||||
boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients());
|
SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret);
|
||||||
Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient();
|
Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient();
|
||||||
boolean hasSession = Session.hasSession(this, masterSecret, primaryRecipient);
|
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);
|
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);
|
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_mms);
|
||||||
menu.removeItem(R.id.menu_context_send_encrypted_sms);
|
menu.removeItem(R.id.menu_context_send_encrypted_sms);
|
||||||
}
|
}
|
||||||
@ -419,24 +421,13 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
|
|||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
if (isSingleConversation()) {
|
if (isSingleConversation()) {
|
||||||
ConversationActivity self = ConversationActivity.this;
|
ConversationActivity self = ConversationActivity.this;
|
||||||
Recipient recipient = getRecipients().getPrimaryRecipient();
|
|
||||||
|
|
||||||
if (SessionRecordV2.hasSession(self, masterSecret,
|
|
||||||
recipient.getRecipientId(),
|
|
||||||
RecipientDevice.DEFAULT_DEVICE_ID))
|
|
||||||
{
|
|
||||||
OutgoingEndSessionMessage endSessionMessage =
|
OutgoingEndSessionMessage endSessionMessage =
|
||||||
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE"));
|
new OutgoingEndSessionMessage(new OutgoingTextMessage(getRecipients(), "TERMINATE"));
|
||||||
|
|
||||||
long allocatedThreadId = MessageSender.send(self, masterSecret,
|
long allocatedThreadId = MessageSender.send(self, masterSecret, endSessionMessage, threadId, false);
|
||||||
endSessionMessage, threadId, false);
|
|
||||||
|
|
||||||
sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId);
|
sendComplete(recipients, allocatedThreadId, allocatedThreadId != self.threadId);
|
||||||
} else {
|
|
||||||
Session.abortSessionFor(self, recipient);
|
|
||||||
initializeSecurity();
|
|
||||||
initializeTitleBar();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -699,9 +690,11 @@ public class ConversationActivity extends PassphraseRequiredSherlockFragmentActi
|
|||||||
|
|
||||||
private void initializeSecurity() {
|
private void initializeSecurity() {
|
||||||
TypedArray drawables = obtainStyledAttributes(SEND_ATTRIBUTES);
|
TypedArray drawables = obtainStyledAttributes(SEND_ATTRIBUTES);
|
||||||
|
SessionStore sessionStore = new TextSecureSessionStore(this, masterSecret);
|
||||||
Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient();
|
Recipient primaryRecipient = getRecipients() == null ? null : getRecipients().getPrimaryRecipient();
|
||||||
boolean isPushDestination = DirectoryHelper.isPushDestination(this, getRecipients());
|
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) {
|
if (isPushDestination || isSecureDestination) {
|
||||||
this.isEncryptedConversation = true;
|
this.isEncryptedConversation = true;
|
||||||
|
@ -35,6 +35,7 @@ import org.whispersystems.textsecure.crypto.MasterSecret;
|
|||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.util.VersionTracker;
|
import org.thoughtcrime.securesms.util.VersionTracker;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.util.SortedSet;
|
import java.util.SortedSet;
|
||||||
import java.util.TreeSet;
|
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 TOFU_IDENTITIES_VERSION = 50;
|
||||||
public static final int CURVE25519_VERSION = 63;
|
public static final int CURVE25519_VERSION = 63;
|
||||||
public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73;
|
public static final int ASYMMETRIC_MASTER_SECRET_FIX_VERSION = 73;
|
||||||
|
public static final int NO_V1_VERSION = 83;
|
||||||
|
|
||||||
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
|
private static final SortedSet<Integer> UPGRADE_VERSIONS = new TreeSet<Integer>() {{
|
||||||
add(NO_MORE_KEY_EXCHANGE_PREFIX_VERSION);
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,9 +27,12 @@ import org.thoughtcrime.securesms.util.DynamicLanguage;
|
|||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
import org.thoughtcrime.securesms.util.MemoryCleaner;
|
||||||
import org.whispersystems.libaxolotl.IdentityKey;
|
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.IdentityKeyParcelable;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
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.
|
* Activity for verifying identity keys.
|
||||||
@ -92,7 +95,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (identityKey == null) {
|
if (identityKey == null) {
|
||||||
identityKey = Session.getRemoteIdentityKey(this, masterSecret, recipient);
|
identityKey = getRemoteIdentityKey(masterSecret, recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identityKey == null) {
|
if (identityKey == null) {
|
||||||
@ -128,7 +131,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void initiateScan() {
|
protected void initiateScan() {
|
||||||
IdentityKey identityKey = Session.getRemoteIdentityKey(this, masterSecret, recipient);
|
IdentityKey identityKey = getRemoteIdentityKey(masterSecret, recipient);
|
||||||
|
|
||||||
if (identityKey == null) {
|
if (identityKey == null) {
|
||||||
Toast.makeText(this, R.string.VerifyIdentityActivity_recipient_has_no_identity_key_exclamation,
|
Toast.makeText(this, R.string.VerifyIdentityActivity_recipient_has_no_identity_key_exclamation,
|
||||||
@ -150,7 +153,7 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected IdentityKey getIdentityKeyToCompare() {
|
protected IdentityKey getIdentityKeyToCompare() {
|
||||||
return Session.getRemoteIdentityKey(this, masterSecret, recipient);
|
return getRemoteIdentityKey(masterSecret, recipient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -177,4 +180,16 @@ public class VerifyIdentityActivity extends KeyScanningActivity {
|
|||||||
protected String getVerifiedTitle() {
|
protected String getVerifiedTitle() {
|
||||||
return getString(R.string.VerifyIdentityActivity_verified_exclamation);
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,12 +47,12 @@ import org.whispersystems.libaxolotl.InvalidVersionException;
|
|||||||
import org.whispersystems.libaxolotl.LegacyMessageException;
|
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||||
import org.whispersystems.libaxolotl.SessionCipher;
|
import org.whispersystems.libaxolotl.SessionCipher;
|
||||||
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
import org.whispersystems.libaxolotl.protocol.WhisperMessage;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
||||||
import org.whispersystems.textsecure.push.IncomingPushMessage;
|
import org.whispersystems.textsecure.push.IncomingPushMessage;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
|
||||||
import org.whispersystems.textsecure.util.Hex;
|
import org.whispersystems.textsecure.util.Hex;
|
||||||
import org.whispersystems.textsecure.util.Util;
|
import org.whispersystems.textsecure.util.Util;
|
||||||
|
|
||||||
@ -197,11 +197,12 @@ public class DecryptingQueue {
|
|||||||
|
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false);
|
Recipients recipients = RecipientFactory.getRecipientsFromString(context, message.getSource(), false);
|
||||||
Recipient recipient = recipients.getPrimaryRecipient();
|
Recipient recipient = recipients.getPrimaryRecipient();
|
||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), message.getSourceDevice());
|
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);
|
sendResult(PushReceiver.RESULT_NO_SESSION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -211,18 +212,12 @@ public class DecryptingQueue {
|
|||||||
|
|
||||||
message = message.withBody(plaintextBody);
|
message = message.withBody(plaintextBody);
|
||||||
sendResult(PushReceiver.RESULT_OK);
|
sendResult(PushReceiver.RESULT_OK);
|
||||||
} catch (InvalidMessageException e) {
|
} catch (InvalidMessageException | LegacyMessageException | RecipientFormattingException e) {
|
||||||
Log.w("DecryptionQueue", e);
|
|
||||||
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
|
|
||||||
} catch (RecipientFormattingException e) {
|
|
||||||
Log.w("DecryptionQueue", e);
|
Log.w("DecryptionQueue", e);
|
||||||
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
|
sendResult(PushReceiver.RESULT_DECRYPT_FAILED);
|
||||||
} catch (DuplicateMessageException e) {
|
} catch (DuplicateMessageException e) {
|
||||||
Log.w("DecryptingQueue", e);
|
Log.w("DecryptingQueue", e);
|
||||||
sendResult(PushReceiver.RESULT_DECRYPT_DUPLICATE);
|
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 {
|
try {
|
||||||
String messageFrom = pdu.getFrom().getString();
|
String messageFrom = pdu.getFrom().getString();
|
||||||
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
|
Recipients recipients = RecipientFactory.getRecipientsFromString(context, messageFrom, false);
|
||||||
Recipient recipient = recipients.getPrimaryRecipient();
|
Recipient recipient = recipients.getPrimaryRecipient();
|
||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
||||||
@ -281,7 +277,7 @@ public class DecryptingQueue {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Session.hasSession(context, masterSecret, recipient)) {
|
if (!sessionStore.contains(recipientDevice.getRecipientId(), recipientDevice.getDeviceId())) {
|
||||||
Log.w("DecryptingQueue", "No such recipient session for MMS...");
|
Log.w("DecryptingQueue", "No such recipient session for MMS...");
|
||||||
database.markAsNoSession(messageId, threadId);
|
database.markAsNoSession(messageId, threadId);
|
||||||
return;
|
return;
|
||||||
@ -316,24 +312,15 @@ public class DecryptingQueue {
|
|||||||
Log.w("DecryptingQueue", "Successfully decrypted MMS!");
|
Log.w("DecryptingQueue", "Successfully decrypted MMS!");
|
||||||
database.insertSecureDecryptedMessageInbox(masterSecret, new IncomingMediaMessage(plaintextPdu), threadId);
|
database.insertSecureDecryptedMessageInbox(masterSecret, new IncomingMediaMessage(plaintextPdu), threadId);
|
||||||
database.delete(messageId);
|
database.delete(messageId);
|
||||||
} catch (RecipientFormattingException rfe) {
|
} catch (RecipientFormattingException | IOException | MmsException | InvalidMessageException rfe) {
|
||||||
Log.w("DecryptingQueue", rfe);
|
Log.w("DecryptingQueue", rfe);
|
||||||
database.markAsDecryptFailed(messageId, threadId);
|
database.markAsDecryptFailed(messageId, threadId);
|
||||||
} catch (InvalidMessageException ime) {
|
|
||||||
Log.w("DecryptingQueue", ime);
|
|
||||||
database.markAsDecryptFailed(messageId, threadId);
|
|
||||||
} catch (DuplicateMessageException dme) {
|
} catch (DuplicateMessageException dme) {
|
||||||
Log.w("DecryptingQueue", dme);
|
Log.w("DecryptingQueue", dme);
|
||||||
database.markAsDecryptDuplicate(messageId, threadId);
|
database.markAsDecryptDuplicate(messageId, threadId);
|
||||||
} catch (LegacyMessageException lme) {
|
} catch (LegacyMessageException lme) {
|
||||||
Log.w("DecryptingQueue", lme);
|
Log.w("DecryptingQueue", lme);
|
||||||
database.markAsLegacyVersion(messageId, threadId);
|
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;
|
String plaintextBody;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
|
Recipients recipients = RecipientFactory.getRecipientsFromString(context, originator, false);
|
||||||
Recipient recipient = recipients.getPrimaryRecipient();
|
Recipient recipient = recipients.getPrimaryRecipient();
|
||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId);
|
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), deviceId);
|
||||||
@ -380,7 +368,7 @@ public class DecryptingQueue {
|
|||||||
SmsTransportDetails transportDetails = new SmsTransportDetails();
|
SmsTransportDetails transportDetails = new SmsTransportDetails();
|
||||||
byte[] decodedCiphertext = transportDetails.getDecodedMessage(body.getBytes());
|
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);
|
if (WhisperMessage.isLegacy(decodedCiphertext)) database.markAsLegacyVersion(messageId);
|
||||||
else database.markAsNoSession(messageId);
|
else database.markAsNoSession(messageId);
|
||||||
return;
|
return;
|
||||||
@ -393,11 +381,11 @@ public class DecryptingQueue {
|
|||||||
|
|
||||||
if (isEndSession &&
|
if (isEndSession &&
|
||||||
"TERMINATE".equals(plaintextBody) &&
|
"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);
|
Log.w("DecryptionQueue", e);
|
||||||
database.markAsDecryptFailed(messageId);
|
database.markAsDecryptFailed(messageId);
|
||||||
return;
|
return;
|
||||||
@ -405,14 +393,6 @@ public class DecryptingQueue {
|
|||||||
Log.w("DecryptionQueue", lme);
|
Log.w("DecryptionQueue", lme);
|
||||||
database.markAsLegacyVersion(messageId);
|
database.markAsLegacyVersion(messageId);
|
||||||
return;
|
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) {
|
} catch (DuplicateMessageException e) {
|
||||||
Log.w("DecryptionQueue", e);
|
Log.w("DecryptionQueue", e);
|
||||||
database.markAsDecryptDuplicate(messageId);
|
database.markAsDecryptDuplicate(messageId);
|
||||||
|
@ -30,9 +30,11 @@ import org.thoughtcrime.securesms.util.Dialogs;
|
|||||||
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
import org.whispersystems.libaxolotl.IdentityKeyPair;
|
||||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
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.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
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.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@ -71,11 +73,11 @@ public class KeyExchangeInitiator {
|
|||||||
identityKey.getPublicKey());
|
identityKey.getPublicKey());
|
||||||
|
|
||||||
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize());
|
OutgoingKeyExchangeMessage textMessage = new OutgoingKeyExchangeMessage(recipient, message.serialize());
|
||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
|
SessionRecord sessionRecord = sessionStore.get(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
||||||
|
|
||||||
SessionRecordV2 sessionRecordV2 = new SessionRecordV2(context, masterSecret, recipientDevice);
|
sessionRecord.getSessionState().setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey);
|
||||||
sessionRecordV2.getSessionState().setPendingKeyExchange(sequence, baseKey, ephemeralKey, identityKey);
|
sessionStore.put(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID, sessionRecord);
|
||||||
sessionRecordV2.save();
|
|
||||||
|
|
||||||
MessageSender.send(context, masterSecret, textMessage, -1, false);
|
MessageSender.send(context, masterSecret, textMessage, -1, false);
|
||||||
}
|
}
|
||||||
@ -83,11 +85,10 @@ public class KeyExchangeInitiator {
|
|||||||
private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret,
|
private static boolean hasInitiatedSession(Context context, MasterSecret masterSecret,
|
||||||
Recipient recipient)
|
Recipient recipient)
|
||||||
{
|
{
|
||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
return
|
SessionRecord sessionRecord = sessionStore.get(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
||||||
new SessionRecordV2(context, masterSecret, recipientDevice)
|
|
||||||
.getSessionState()
|
return sessionRecord.getSessionState().hasPendingPreKey();
|
||||||
.hasPendingKeyExchange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int getRandomSequence() {
|
private static int getRandomSequence() {
|
||||||
|
@ -21,13 +21,14 @@ import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
|||||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||||
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||||
import org.whispersystems.libaxolotl.ratchet.RatchetingSession;
|
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.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.push.PreKeyEntity;
|
import org.whispersystems.textsecure.push.PreKeyEntity;
|
||||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||||
import org.whispersystems.textsecure.storage.PreKeyRecord;
|
import org.whispersystems.textsecure.storage.PreKeyRecord;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
import org.whispersystems.textsecure.storage.RecipientDevice;
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
|
||||||
import org.whispersystems.textsecure.storage.SessionRecordV2;
|
|
||||||
import org.whispersystems.textsecure.util.Medium;
|
import org.whispersystems.textsecure.util.Medium;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,14 +42,14 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
private Context context;
|
private Context context;
|
||||||
private RecipientDevice recipientDevice;
|
private RecipientDevice recipientDevice;
|
||||||
private MasterSecret masterSecret;
|
private MasterSecret masterSecret;
|
||||||
private SessionRecordV2 sessionRecord;
|
private SessionStore sessionStore;
|
||||||
|
|
||||||
public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice)
|
public KeyExchangeProcessorV2(Context context, MasterSecret masterSecret, RecipientDevice recipientDevice)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context;
|
||||||
this.recipientDevice = recipientDevice;
|
this.recipientDevice = recipientDevice;
|
||||||
this.masterSecret = masterSecret;
|
this.masterSecret = masterSecret;
|
||||||
this.sessionRecord = new SessionRecordV2(context, masterSecret, recipientDevice);
|
this.sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isTrusted(PreKeyWhisperMessage message) {
|
public boolean isTrusted(PreKeyWhisperMessage message) {
|
||||||
@ -71,6 +72,9 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
|
|
||||||
public boolean isStale(KeyExchangeMessage m) {
|
public boolean isStale(KeyExchangeMessage m) {
|
||||||
KeyExchangeMessageV2 message = (KeyExchangeMessageV2) m;
|
KeyExchangeMessageV2 message = (KeyExchangeMessageV2) m;
|
||||||
|
SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(),
|
||||||
|
recipientDevice.getDeviceId());
|
||||||
|
|
||||||
return
|
return
|
||||||
message.isResponse() &&
|
message.isResponse() &&
|
||||||
(!sessionRecord.getSessionState().hasPendingKeyExchange() ||
|
(!sessionRecord.getSessionState().hasPendingKeyExchange() ||
|
||||||
@ -88,7 +92,9 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
|
|
||||||
Log.w("KeyExchangeProcessor", "Received pre-key with local key ID: " + preKeyId);
|
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...");
|
Log.w("KeyExchangeProcessor", "We've already processed the prekey part, letting bundled message fall through...");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -96,13 +102,15 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
if (!PreKeyRecord.hasRecord(context, preKeyId))
|
if (!PreKeyRecord.hasRecord(context, preKeyId))
|
||||||
throw new InvalidKeyIdException("No such prekey: " + preKeyId);
|
throw new InvalidKeyIdException("No such prekey: " + preKeyId);
|
||||||
|
|
||||||
|
SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(),
|
||||||
|
recipientDevice.getDeviceId());
|
||||||
PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId);
|
PreKeyRecord preKeyRecord = new PreKeyRecord(context, masterSecret, preKeyId);
|
||||||
ECKeyPair ourBaseKey = preKeyRecord.getKeyPair();
|
ECKeyPair ourBaseKey = preKeyRecord.getKeyPair();
|
||||||
ECKeyPair ourEphemeralKey = ourBaseKey;
|
ECKeyPair ourEphemeralKey = ourBaseKey;
|
||||||
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
||||||
boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey();
|
boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey();
|
||||||
|
|
||||||
if (!simultaneousInitiate) sessionRecord.clear();
|
if (!simultaneousInitiate) sessionRecord.reset();
|
||||||
else sessionRecord.archiveCurrentState();
|
else sessionRecord.archiveCurrentState();
|
||||||
|
|
||||||
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
|
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
|
||||||
@ -110,13 +118,12 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
ourEphemeralKey, theirEphemeralKey,
|
ourEphemeralKey, theirEphemeralKey,
|
||||||
ourIdentityKey, theirIdentityKey);
|
ourIdentityKey, theirIdentityKey);
|
||||||
|
|
||||||
Session.clearV1SessionFor(context, recipientDevice.getRecipient());
|
|
||||||
sessionRecord.getSessionState().setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context));
|
sessionRecord.getSessionState().setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context));
|
||||||
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
|
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
|
||||||
|
|
||||||
if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true);
|
if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true);
|
||||||
|
|
||||||
sessionRecord.save();
|
sessionStore.put(recipientDevice.getRecipientId(), recipientDevice.getDeviceId(), sessionRecord);
|
||||||
|
|
||||||
if (preKeyId != Medium.MAX_VALUE) {
|
if (preKeyId != Medium.MAX_VALUE) {
|
||||||
PreKeyRecord.delete(context, preKeyId);
|
PreKeyRecord.delete(context, preKeyId);
|
||||||
@ -131,6 +138,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
public void processKeyExchangeMessage(PreKeyEntity message, long threadId)
|
public void processKeyExchangeMessage(PreKeyEntity message, long threadId)
|
||||||
throws InvalidKeyException
|
throws InvalidKeyException
|
||||||
{
|
{
|
||||||
|
SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(),
|
||||||
|
recipientDevice.getDeviceId());
|
||||||
ECKeyPair ourBaseKey = Curve.generateKeyPair(true);
|
ECKeyPair ourBaseKey = Curve.generateKeyPair(true);
|
||||||
ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true);
|
ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true);
|
||||||
ECPublicKey theirBaseKey = message.getPublicKey();
|
ECPublicKey theirBaseKey = message.getPublicKey();
|
||||||
@ -139,7 +148,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
IdentityKeyPair ourIdentityKey = IdentityKeyUtil.getIdentityKeyPair(context, masterSecret);
|
||||||
|
|
||||||
if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState();
|
if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState();
|
||||||
else sessionRecord.clear();
|
else sessionRecord.reset();
|
||||||
|
|
||||||
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
|
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
|
||||||
ourBaseKey, theirBaseKey, ourEphemeralKey,
|
ourBaseKey, theirBaseKey, ourEphemeralKey,
|
||||||
@ -149,7 +158,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
sessionRecord.getSessionState().setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context));
|
sessionRecord.getSessionState().setLocalRegistrationId(TextSecurePreferences.getLocalRegistrationId(context));
|
||||||
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
|
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
|
||||||
|
|
||||||
sessionRecord.save();
|
sessionStore.put(recipientDevice.getRecipientId(), recipientDevice.getDeviceId(), sessionRecord);
|
||||||
|
|
||||||
DatabaseFactory.getIdentityDatabase(context)
|
DatabaseFactory.getIdentityDatabase(context)
|
||||||
.saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey());
|
.saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey());
|
||||||
@ -165,6 +174,8 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
KeyExchangeMessageV2 message = (KeyExchangeMessageV2) _message;
|
KeyExchangeMessageV2 message = (KeyExchangeMessageV2) _message;
|
||||||
|
SessionRecord sessionRecord = sessionStore.get(recipientDevice.getRecipientId(),
|
||||||
|
recipientDevice.getDeviceId());
|
||||||
Recipient recipient = RecipientFactory.getRecipientsForIds(context,
|
Recipient recipient = RecipientFactory.getRecipientsForIds(context,
|
||||||
String.valueOf(recipientDevice.getRecipientId()),
|
String.valueOf(recipientDevice.getRecipientId()),
|
||||||
false)
|
false)
|
||||||
@ -219,7 +230,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
ECKeyPair ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey();
|
ECKeyPair ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey();
|
||||||
IdentityKeyPair ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey();
|
IdentityKeyPair ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey();
|
||||||
|
|
||||||
sessionRecord.clear();
|
sessionRecord.reset();
|
||||||
|
|
||||||
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
|
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
|
||||||
ourBaseKey, message.getBaseKey(),
|
ourBaseKey, message.getBaseKey(),
|
||||||
@ -227,8 +238,7 @@ public class KeyExchangeProcessorV2 extends KeyExchangeProcessor {
|
|||||||
ourIdentityKey, message.getIdentityKey());
|
ourIdentityKey, message.getIdentityKey());
|
||||||
|
|
||||||
sessionRecord.getSessionState().setSessionVersion(message.getVersion());
|
sessionRecord.getSessionState().setSessionVersion(message.getVersion());
|
||||||
Session.clearV1SessionFor(context, recipientDevice.getRecipient());
|
sessionStore.put(recipientDevice.getRecipientId(), recipientDevice.getDeviceId(), sessionRecord);
|
||||||
sessionRecord.save();
|
|
||||||
|
|
||||||
DatabaseFactory.getIdentityDatabase(context)
|
DatabaseFactory.getIdentityDatabase(context)
|
||||||
.saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey());
|
.saveIdentity(masterSecret, recipientDevice.getRecipientId(), message.getIdentityKey());
|
||||||
|
@ -33,7 +33,6 @@ import org.whispersystems.libaxolotl.IdentityKey;
|
|||||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
import org.whispersystems.textsecure.crypto.MasterCipher;
|
import org.whispersystems.textsecure.crypto.MasterCipher;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
|
||||||
import org.whispersystems.textsecure.util.Base64;
|
import org.whispersystems.textsecure.util.Base64;
|
||||||
import org.whispersystems.textsecure.util.Util;
|
import org.whispersystems.textsecure.util.Util;
|
||||||
|
|
||||||
@ -404,7 +403,14 @@ public class DatabaseFactory {
|
|||||||
|
|
||||||
if (name.matches("[0-9]+")) {
|
if (name.matches("[0-9]+")) {
|
||||||
long recipientId = Long.parseLong(name);
|
long recipientId = Long.parseLong(name);
|
||||||
IdentityKey identityKey = Session.getRemoteIdentityKey(context, masterSecret, recipientId);
|
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) {
|
if (identityKey != null) {
|
||||||
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
MasterCipher masterCipher = new MasterCipher(masterSecret);
|
||||||
|
@ -29,12 +29,13 @@ import org.whispersystems.libaxolotl.InvalidKeyException;
|
|||||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
import org.whispersystems.libaxolotl.InvalidVersionException;
|
import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||||
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.push.IncomingPushMessage;
|
import org.whispersystems.textsecure.push.IncomingPushMessage;
|
||||||
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
|
import org.whispersystems.textsecure.push.PushMessageProtos.PushMessageContent;
|
||||||
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
import org.whispersystems.textsecure.storage.InvalidKeyIdException;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
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 org.whispersystems.textsecure.util.Base64;
|
||||||
|
|
||||||
import ws.com.google.android.mms.MmsException;
|
import ws.com.google.android.mms.MmsException;
|
||||||
@ -131,19 +132,12 @@ public class PushReceiver {
|
|||||||
|
|
||||||
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
MessageNotifier.updateNotification(context, masterSecret, messageAndThreadId.second);
|
||||||
}
|
}
|
||||||
} catch (InvalidKeyException e) {
|
|
||||||
Log.w("PushReceiver", e);
|
|
||||||
handleReceivedCorruptedKey(masterSecret, message, false);
|
|
||||||
} catch (InvalidVersionException e) {
|
} catch (InvalidVersionException e) {
|
||||||
Log.w("PushReceiver", e);
|
Log.w("PushReceiver", e);
|
||||||
handleReceivedCorruptedKey(masterSecret, message, true);
|
handleReceivedCorruptedKey(masterSecret, message, true);
|
||||||
} catch (InvalidKeyIdException e) {
|
} catch (InvalidKeyException | InvalidKeyIdException | InvalidMessageException |
|
||||||
Log.w("PushReceiver", e);
|
RecipientFormattingException e)
|
||||||
handleReceivedCorruptedKey(masterSecret, message, false);
|
{
|
||||||
} catch (InvalidMessageException e) {
|
|
||||||
Log.w("PushReceiver", e);
|
|
||||||
handleReceivedCorruptedKey(masterSecret, message, false);
|
|
||||||
} catch (RecipientFormattingException e) {
|
|
||||||
Log.w("PushReceiver", e);
|
Log.w("PushReceiver", e);
|
||||||
handleReceivedCorruptedKey(masterSecret, message, false);
|
handleReceivedCorruptedKey(masterSecret, message, false);
|
||||||
}
|
}
|
||||||
@ -189,7 +183,9 @@ public class PushReceiver {
|
|||||||
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage);
|
Pair<Long, Long> messageAndThreadId = database.insertMessageInbox(masterSecret, incomingEndSessionMessage);
|
||||||
database.updateMessageBody(masterSecret, messageAndThreadId.first, messageContent.getBody());
|
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);
|
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, messageAndThreadId.second);
|
||||||
} catch (RecipientFormattingException e) {
|
} catch (RecipientFormattingException e) {
|
||||||
Log.w("PushReceiver", e);
|
Log.w("PushReceiver", e);
|
||||||
|
@ -42,8 +42,9 @@ import org.thoughtcrime.securesms.transport.SecureFallbackApprovalException;
|
|||||||
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
|
||||||
import org.thoughtcrime.securesms.transport.UniversalTransport;
|
import org.thoughtcrime.securesms.transport.UniversalTransport;
|
||||||
import org.thoughtcrime.securesms.transport.UntrustedIdentityException;
|
import org.thoughtcrime.securesms.transport.UntrustedIdentityException;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.storage.Session;
|
import org.whispersystems.textsecure.storage.TextSecureSessionStore;
|
||||||
|
|
||||||
public class SmsSender {
|
public class SmsSender {
|
||||||
|
|
||||||
@ -61,7 +62,7 @@ public class SmsSender {
|
|||||||
if (SendReceiveService.SEND_SMS_ACTION.equals(intent.getAction())) {
|
if (SendReceiveService.SEND_SMS_ACTION.equals(intent.getAction())) {
|
||||||
handleSendMessage(masterSecret, intent);
|
handleSendMessage(masterSecret, intent);
|
||||||
} else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) {
|
} else if (SendReceiveService.SENT_SMS_ACTION.equals(intent.getAction())) {
|
||||||
handleSentMessage(intent);
|
handleSentMessage(masterSecret, intent);
|
||||||
} else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) {
|
} else if (SendReceiveService.DELIVERED_SMS_ACTION.equals(intent.getAction())) {
|
||||||
handleDeliveredMessage(intent);
|
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);
|
long messageId = intent.getLongExtra("message_id", -1);
|
||||||
int result = intent.getIntExtra("ResultCode", -31337);
|
int result = intent.getIntExtra("ResultCode", -31337);
|
||||||
boolean upgraded = intent.getBooleanExtra("upgraded", false);
|
boolean upgraded = intent.getBooleanExtra("upgraded", false);
|
||||||
@ -138,7 +139,8 @@ public class SmsSender {
|
|||||||
|
|
||||||
if (record != null && record.isEndSession()) {
|
if (record != null && record.isEndSession()) {
|
||||||
Log.w("SmsSender", "Ending session...");
|
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());
|
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, record.getThreadId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
|||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
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 org.whispersystems.textsecure.util.Hex;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@ -168,13 +168,15 @@ public class MmsTransport {
|
|||||||
return encryptedPdu;
|
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 {
|
try {
|
||||||
TextTransport transportDetails = new TextTransport();
|
TextTransport transportDetails = new TextTransport();
|
||||||
Recipient recipient = RecipientFactory.getRecipientsFromString(context, recipientString, false).getPrimaryRecipient();
|
Recipient recipient = RecipientFactory.getRecipientsFromString(context, recipientString, false).getPrimaryRecipient();
|
||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(), RecipientDevice.DEFAULT_DEVICE_ID);
|
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.");
|
throw new InsecureFallbackApprovalException("No session exists for this secure message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||||||
import org.whispersystems.libaxolotl.InvalidKeyException;
|
import org.whispersystems.libaxolotl.InvalidKeyException;
|
||||||
import org.whispersystems.libaxolotl.SessionCipher;
|
import org.whispersystems.libaxolotl.SessionCipher;
|
||||||
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
||||||
|
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||||
import org.whispersystems.textsecure.crypto.AttachmentCipher;
|
import org.whispersystems.textsecure.crypto.AttachmentCipher;
|
||||||
import org.whispersystems.textsecure.crypto.MasterSecret;
|
import org.whispersystems.textsecure.crypto.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
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.StaleDevices;
|
||||||
import org.whispersystems.textsecure.push.StaleDevicesException;
|
import org.whispersystems.textsecure.push.StaleDevicesException;
|
||||||
import org.whispersystems.textsecure.push.UnregisteredUserException;
|
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.Base64;
|
||||||
import org.whispersystems.textsecure.util.InvalidNumberException;
|
import org.whispersystems.textsecure.util.InvalidNumberException;
|
||||||
|
|
||||||
@ -93,7 +95,8 @@ public class PushTransport extends BaseTransport {
|
|||||||
deliver(socket, recipient, threadId, plaintext);
|
deliver(socket, recipient, threadId, plaintext);
|
||||||
|
|
||||||
if (message.isEndSession()) {
|
if (message.isEndSession()) {
|
||||||
SessionRecordV2.deleteAll(context, recipient);
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
|
sessionStore.deleteAll(recipient.getRecipientId());
|
||||||
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, threadId);
|
KeyExchangeProcessor.broadcastSecurityUpdateEvent(context, threadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,8 +124,8 @@ public class PushTransport extends BaseTransport {
|
|||||||
recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
|
recipients = RecipientFactory.getRecipientsFromString(context, destination, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<UntrustedIdentityException>();
|
List<UntrustedIdentityException> untrustedIdentities = new LinkedList<>();
|
||||||
List<UnregisteredUserException> unregisteredUsers = new LinkedList<UnregisteredUserException>();
|
List<UnregisteredUserException> unregisteredUsers = new LinkedList<>();
|
||||||
|
|
||||||
for (Recipient recipient : recipients.getRecipientsList()) {
|
for (Recipient recipient : recipients.getRecipientsList()) {
|
||||||
try {
|
try {
|
||||||
@ -164,7 +167,7 @@ public class PushTransport extends BaseTransport {
|
|||||||
private List<PushAttachmentPointer> getPushAttachmentPointers(PushServiceSocket socket, PduBody body)
|
private List<PushAttachmentPointer> getPushAttachmentPointers(PushServiceSocket socket, PduBody body)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
List<PushAttachmentPointer> attachments = new LinkedList<PushAttachmentPointer>();
|
List<PushAttachmentPointer> attachments = new LinkedList<>();
|
||||||
|
|
||||||
for (int i=0;i<body.getPartsNum();i++) {
|
for (int i=0;i<body.getPartsNum();i++) {
|
||||||
String contentType = Util.toIsoString(body.getPart(i).getContentType());
|
String contentType = Util.toIsoString(body.getPart(i).getContentType());
|
||||||
@ -198,12 +201,12 @@ public class PushTransport extends BaseTransport {
|
|||||||
throws InvalidNumberException, IOException, UntrustedIdentityException
|
throws InvalidNumberException, IOException, UntrustedIdentityException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
|
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
|
||||||
long recipientId = recipient.getRecipientId();
|
long recipientId = recipient.getRecipientId();
|
||||||
|
|
||||||
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
|
for (int extraDeviceId : mismatchedDevices.getExtraDevices()) {
|
||||||
PushAddress address = PushAddress.create(context, recipientId, e164number, extraDeviceId);
|
sessionStore.delete(recipientId, extraDeviceId);
|
||||||
SessionRecordV2.delete(context, address);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
|
for (int missingDeviceId : mismatchedDevices.getMissingDevices()) {
|
||||||
@ -222,19 +225,12 @@ public class PushTransport extends BaseTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleStaleDevices(Recipient recipient, StaleDevices staleDevices)
|
private void handleStaleDevices(Recipient recipient, StaleDevices staleDevices) {
|
||||||
throws IOException
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
{
|
|
||||||
try {
|
|
||||||
long recipientId = recipient.getRecipientId();
|
long recipientId = recipient.getRecipientId();
|
||||||
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
|
|
||||||
|
|
||||||
for (int staleDeviceId : staleDevices.getStaleDevices()) {
|
for (int staleDeviceId : staleDevices.getStaleDevices()) {
|
||||||
PushAddress address = PushAddress.create(context, recipientId, e164number, staleDeviceId);
|
sessionStore.delete(recipientId, staleDeviceId);
|
||||||
SessionRecordV2.delete(context, address);
|
|
||||||
}
|
|
||||||
} catch (InvalidNumberException e) {
|
|
||||||
throw new IOException(e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,15 +302,16 @@ public class PushTransport extends BaseTransport {
|
|||||||
Recipient recipient, byte[] plaintext)
|
Recipient recipient, byte[] plaintext)
|
||||||
throws IOException, InvalidNumberException, UntrustedIdentityException
|
throws IOException, InvalidNumberException, UntrustedIdentityException
|
||||||
{
|
{
|
||||||
|
SessionStore sessionStore = new TextSecureSessionStore(context, masterSecret);
|
||||||
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
|
String e164number = Util.canonicalizeNumber(context, recipient.getNumber());
|
||||||
long recipientId = recipient.getRecipientId();
|
long recipientId = recipient.getRecipientId();
|
||||||
PushAddress masterDevice = PushAddress.create(context, recipientId, e164number, 1);
|
PushAddress masterDevice = PushAddress.create(context, recipientId, e164number, 1);
|
||||||
PushBody masterBody = getEncryptedMessage(socket, threadId, masterDevice, plaintext);
|
PushBody masterBody = getEncryptedMessage(socket, threadId, masterDevice, plaintext);
|
||||||
|
|
||||||
List<OutgoingPushMessage> messages = new LinkedList<OutgoingPushMessage>();
|
List<OutgoingPushMessage> messages = new LinkedList<>();
|
||||||
messages.add(new OutgoingPushMessage(masterDevice, masterBody));
|
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);
|
PushAddress device = PushAddress.create(context, recipientId, e164number, deviceId);
|
||||||
PushBody body = getEncryptedMessage(socket, threadId, device, plaintext);
|
PushBody body = getEncryptedMessage(socket, threadId, device, plaintext);
|
||||||
|
|
||||||
@ -328,9 +325,7 @@ public class PushTransport extends BaseTransport {
|
|||||||
PushAddress pushAddress, byte[] plaintext)
|
PushAddress pushAddress, byte[] plaintext)
|
||||||
throws IOException, UntrustedIdentityException
|
throws IOException, UntrustedIdentityException
|
||||||
{
|
{
|
||||||
if (!SessionRecordV2.hasSession(context, masterSecret, pushAddress) ||
|
if (!SessionUtil.hasEncryptCapableSession(context, masterSecret, pushAddress)) {
|
||||||
SessionRecordV2.needsRefresh(context, masterSecret, pushAddress))
|
|
||||||
{
|
|
||||||
try {
|
try {
|
||||||
List<PreKeyEntity> preKeys = socket.getPreKeys(pushAddress);
|
List<PreKeyEntity> preKeys = socket.getPreKeys(pushAddress);
|
||||||
|
|
||||||
|
@ -32,10 +32,13 @@ import org.thoughtcrime.securesms.util.NumberUtil;
|
|||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.whispersystems.libaxolotl.SessionCipher;
|
import org.whispersystems.libaxolotl.SessionCipher;
|
||||||
import org.whispersystems.libaxolotl.protocol.CiphertextMessage;
|
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.MasterSecret;
|
||||||
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
import org.whispersystems.textsecure.crypto.SessionCipherFactory;
|
||||||
import org.whispersystems.textsecure.storage.RecipientDevice;
|
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;
|
import java.util.ArrayList;
|
||||||
|
|
||||||
@ -174,7 +177,7 @@ public class SmsTransport extends BaseTransport {
|
|||||||
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(),
|
RecipientDevice recipientDevice = new RecipientDevice(recipient.getRecipientId(),
|
||||||
RecipientDevice.DEFAULT_DEVICE_ID);
|
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.");
|
throw new InsecureFallbackApprovalException("No session exists for this secure message.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ import org.whispersystems.textsecure.directory.NotInDirectoryException;
|
|||||||
import org.whispersystems.textsecure.push.ContactTokenDetails;
|
import org.whispersystems.textsecure.push.ContactTokenDetails;
|
||||||
import org.whispersystems.textsecure.push.PushServiceSocket;
|
import org.whispersystems.textsecure.push.PushServiceSocket;
|
||||||
import org.whispersystems.textsecure.push.UnregisteredUserException;
|
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.DirectoryUtil;
|
||||||
import org.whispersystems.textsecure.util.InvalidNumberException;
|
import org.whispersystems.textsecure.util.InvalidNumberException;
|
||||||
|
|
||||||
@ -177,7 +177,7 @@ public class UniversalTransport {
|
|||||||
Log.w("UniversalTransport", "Falling back to MMS");
|
Log.w("UniversalTransport", "Falling back to MMS");
|
||||||
DatabaseFactory.getMmsDatabase(context).markAsForcedSms(mediaMessage.getDatabaseMessageId());
|
DatabaseFactory.getMmsDatabase(context).markAsForcedSms(mediaMessage.getDatabaseMessageId());
|
||||||
return mmsTransport.deliver(mediaMessage);
|
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");
|
Log.w("UniversalTransport", "Marking message as pending insecure SMS fallback");
|
||||||
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
|
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
|
||||||
} else {
|
} else {
|
||||||
@ -199,7 +199,7 @@ public class UniversalTransport {
|
|||||||
Log.w("UniversalTransport", "Falling back to SMS");
|
Log.w("UniversalTransport", "Falling back to SMS");
|
||||||
DatabaseFactory.getSmsDatabase(context).markAsForcedSms(smsMessage.getId());
|
DatabaseFactory.getSmsDatabase(context).markAsForcedSms(smsMessage.getId());
|
||||||
smsTransport.deliver(smsMessage);
|
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.");
|
Log.w("UniversalTransport", "Marking message as pending insecure fallback.");
|
||||||
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
|
throw new InsecureFallbackApprovalException("Pending user approval for fallback to insecure SMS");
|
||||||
} else {
|
} else {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user