mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-28 06:50:55 +00:00
Move identity key verification into libaxolotol. With tests.
This commit is contained in:
@@ -44,4 +44,10 @@ public class InMemoryIdentityKeyStore implements IdentityKeyStore {
|
||||
public void saveIdentity(long recipientId, IdentityKey identityKey) {
|
||||
trustedKeys.put(recipientId, identityKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTrustedIdentity(long recipientId, IdentityKey identityKey) {
|
||||
IdentityKey trusted = trustedKeys.get(recipientId);
|
||||
return (trusted == null || trusted.equals(identityKey));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import org.whispersystems.libaxolotl.InvalidVersionException;
|
||||
import org.whispersystems.libaxolotl.LegacyMessageException;
|
||||
import org.whispersystems.libaxolotl.SessionBuilder;
|
||||
import org.whispersystems.libaxolotl.SessionCipher;
|
||||
import org.whispersystems.libaxolotl.StaleKeyExchangeException;
|
||||
import org.whispersystems.libaxolotl.UntrustedIdentityException;
|
||||
import org.whispersystems.libaxolotl.ecc.Curve;
|
||||
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
|
||||
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
|
||||
@@ -33,8 +35,7 @@ public class SessionBuilderTest extends AndroidTestCase {
|
||||
private static final long BOB_RECIPIENT_ID = 2L;
|
||||
|
||||
public void testBasicPreKey()
|
||||
throws InvalidKeyException, InvalidVersionException, InvalidMessageException, InvalidKeyIdException, DuplicateMessageException, LegacyMessageException
|
||||
{
|
||||
throws InvalidKeyException, InvalidVersionException, InvalidMessageException, InvalidKeyIdException, DuplicateMessageException, LegacyMessageException, UntrustedIdentityException {
|
||||
SessionStore aliceSessionStore = new InMemorySessionStore();
|
||||
PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore();
|
||||
IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore();
|
||||
@@ -74,9 +75,55 @@ public class SessionBuilderTest extends AndroidTestCase {
|
||||
byte[] plaintext = bobSessionCipher.decrypt(incomingMessage.getWhisperMessage().serialize());
|
||||
|
||||
assertTrue(originalMessage.equals(new String(plaintext)));
|
||||
|
||||
CiphertextMessage bobOutgoingMessage = bobSessionCipher.encrypt(originalMessage.getBytes());
|
||||
assertTrue(bobOutgoingMessage.getType() == CiphertextMessage.WHISPER_TYPE);
|
||||
|
||||
byte[] alicePlaintext = aliceSessionCipher.decrypt(bobOutgoingMessage.serialize());
|
||||
assertTrue(new String(alicePlaintext).equals(originalMessage));
|
||||
|
||||
runInteraction(aliceSessionStore, bobSessionStore);
|
||||
|
||||
aliceSessionStore = new InMemorySessionStore();
|
||||
aliceIdentityKeyStore = new InMemoryIdentityKeyStore();
|
||||
aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore,
|
||||
aliceIdentityKeyStore,
|
||||
BOB_RECIPIENT_ID, 1);
|
||||
aliceSessionCipher = new SessionCipher(aliceSessionStore, BOB_RECIPIENT_ID, 1);
|
||||
|
||||
bobPreKey = new InMemoryPreKey(31338, Curve.generateKeyPair(true),
|
||||
bobIdentityKeyStore.getIdentityKeyPair().getPublicKey(),
|
||||
bobIdentityKeyStore.getLocalRegistrationId());
|
||||
|
||||
bobPreKeyStore.store(31338, bobPreKey);
|
||||
aliceSessionBuilder.process(bobPreKey);
|
||||
|
||||
outgoingMessage = aliceSessionCipher.encrypt(originalMessage.getBytes());
|
||||
|
||||
try {
|
||||
bobSessionBuilder.process(new PreKeyWhisperMessage(outgoingMessage.serialize()));
|
||||
throw new AssertionError("shouldn't be trusted!");
|
||||
} catch (UntrustedIdentityException uie) {
|
||||
bobIdentityKeyStore.saveIdentity(ALICE_RECIPIENT_ID, new PreKeyWhisperMessage(outgoingMessage.serialize()).getIdentityKey());
|
||||
bobSessionBuilder.process(new PreKeyWhisperMessage(outgoingMessage.serialize()));
|
||||
}
|
||||
|
||||
plaintext = bobSessionCipher.decrypt(new PreKeyWhisperMessage(outgoingMessage.serialize()).getWhisperMessage().serialize());
|
||||
assertTrue(new String(plaintext).equals(originalMessage));
|
||||
|
||||
bobPreKey = new InMemoryPreKey(31337, Curve.generateKeyPair(true),
|
||||
aliceIdentityKeyStore.getIdentityKeyPair().getPublicKey(),
|
||||
bobIdentityKeyStore.getLocalRegistrationId());
|
||||
|
||||
try {
|
||||
aliceSessionBuilder.process(bobPreKey);
|
||||
throw new AssertionError("shoulnd't be trusted!");
|
||||
} catch (UntrustedIdentityException uie) {
|
||||
// good
|
||||
}
|
||||
}
|
||||
|
||||
public void testBasicKeyExchange() throws InvalidKeyException, LegacyMessageException, InvalidMessageException, DuplicateMessageException {
|
||||
public void testBasicKeyExchange() throws InvalidKeyException, LegacyMessageException, InvalidMessageException, DuplicateMessageException, UntrustedIdentityException, StaleKeyExchangeException {
|
||||
SessionStore aliceSessionStore = new InMemorySessionStore();
|
||||
PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore();
|
||||
IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore();
|
||||
@@ -104,11 +151,28 @@ public class SessionBuilderTest extends AndroidTestCase {
|
||||
assertTrue(bobSessionStore.contains(ALICE_RECIPIENT_ID, 1));
|
||||
|
||||
runInteraction(aliceSessionStore, bobSessionStore);
|
||||
|
||||
aliceSessionStore = new InMemorySessionStore();
|
||||
aliceIdentityKeyStore = new InMemoryIdentityKeyStore();
|
||||
aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore,
|
||||
aliceIdentityKeyStore, BOB_RECIPIENT_ID, 1);
|
||||
aliceKeyExchangeMessage = aliceSessionBuilder.process();
|
||||
|
||||
try {
|
||||
bobKeyExchangeMessage = bobSessionBuilder.process(aliceKeyExchangeMessage);
|
||||
throw new AssertionError("This identity shouldn't be trusted!");
|
||||
} catch (UntrustedIdentityException uie) {
|
||||
bobIdentityKeyStore.saveIdentity(ALICE_RECIPIENT_ID, aliceKeyExchangeMessage.getIdentityKey());
|
||||
bobKeyExchangeMessage = bobSessionBuilder.process(aliceKeyExchangeMessage);
|
||||
}
|
||||
|
||||
assertTrue(aliceSessionBuilder.process(bobKeyExchangeMessage) == null);
|
||||
|
||||
runInteraction(aliceSessionStore, bobSessionStore);
|
||||
}
|
||||
|
||||
public void testSimultaneousKeyExchange()
|
||||
throws InvalidKeyException, DuplicateMessageException, LegacyMessageException, InvalidMessageException
|
||||
{
|
||||
throws InvalidKeyException, DuplicateMessageException, LegacyMessageException, InvalidMessageException, UntrustedIdentityException, StaleKeyExchangeException {
|
||||
SessionStore aliceSessionStore = new InMemorySessionStore();
|
||||
PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore();
|
||||
IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore();
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.whispersystems.libaxolotl.state.PreKey;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyRecord;
|
||||
import org.whispersystems.libaxolotl.state.PreKeyStore;
|
||||
import org.whispersystems.libaxolotl.state.SessionRecord;
|
||||
import org.whispersystems.libaxolotl.state.SessionState;
|
||||
import org.whispersystems.libaxolotl.state.SessionStore;
|
||||
import org.whispersystems.libaxolotl.util.KeyHelper;
|
||||
import org.whispersystems.libaxolotl.util.Medium;
|
||||
@@ -77,16 +78,19 @@ public class SessionBuilder {
|
||||
* that corresponds to the PreKey ID in
|
||||
* the message.
|
||||
* @throws org.whispersystems.libaxolotl.InvalidKeyException when the message is formatted incorrectly.
|
||||
* @throws org.whispersystems.libaxolotl.UntrustedIdentityException when the {@link IdentityKey} of the sender is untrusted.
|
||||
*/
|
||||
public void process(PreKeyWhisperMessage message)
|
||||
throws InvalidKeyIdException, InvalidKeyException
|
||||
throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException
|
||||
{
|
||||
int preKeyId = message.getPreKeyId();
|
||||
ECPublicKey theirBaseKey = message.getBaseKey();
|
||||
ECPublicKey theirEphemeralKey = message.getWhisperMessage().getSenderEphemeral();
|
||||
IdentityKey theirIdentityKey = message.getIdentityKey();
|
||||
|
||||
Log.w(TAG, "Received pre-key with local key ID: " + preKeyId);
|
||||
if (!identityKeyStore.isTrustedIdentity(recipientId, theirIdentityKey)) {
|
||||
throw new UntrustedIdentityException();
|
||||
}
|
||||
|
||||
if (!preKeyStore.contains(preKeyId) &&
|
||||
sessionStore.contains(recipientId, deviceId))
|
||||
@@ -134,8 +138,16 @@ public class SessionBuilder {
|
||||
* @param preKey A PreKey for the destination recipient, retrieved from a server.
|
||||
* @throws InvalidKeyException when the {@link org.whispersystems.libaxolotl.state.PreKey} is
|
||||
* badly formatted.
|
||||
* @throws org.whispersystems.libaxolotl.UntrustedIdentityException when the sender's
|
||||
* {@link IdentityKey} is not
|
||||
* trusted.
|
||||
*/
|
||||
public void process(PreKey preKey) throws InvalidKeyException {
|
||||
public void process(PreKey preKey) throws InvalidKeyException, UntrustedIdentityException {
|
||||
|
||||
if (!identityKeyStore.isTrustedIdentity(recipientId, preKey.getIdentityKey())) {
|
||||
throw new UntrustedIdentityException();
|
||||
}
|
||||
|
||||
SessionRecord sessionRecord = sessionStore.load(recipientId, deviceId);
|
||||
ECKeyPair ourBaseKey = Curve.generateKeyPair(true);
|
||||
ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true);
|
||||
@@ -168,43 +180,34 @@ public class SessionBuilder {
|
||||
* @return The KeyExchangeMessage to respond with, or null if no response is necessary.
|
||||
* @throws InvalidKeyException if the received KeyExchangeMessage is badly formatted.
|
||||
*/
|
||||
public KeyExchangeMessage process(KeyExchangeMessage message) throws InvalidKeyException {
|
||||
public KeyExchangeMessage process(KeyExchangeMessage message)
|
||||
throws InvalidKeyException, UntrustedIdentityException, StaleKeyExchangeException
|
||||
{
|
||||
|
||||
if (!identityKeyStore.isTrustedIdentity(recipientId, message.getIdentityKey())) {
|
||||
throw new UntrustedIdentityException();
|
||||
}
|
||||
|
||||
KeyExchangeMessage responseMessage = null;
|
||||
SessionRecord sessionRecord = sessionStore.load(recipientId, deviceId);
|
||||
|
||||
Log.w(TAG, "Received key exchange with sequence: " + message.getSequence());
|
||||
|
||||
if (message.isInitiate()) {
|
||||
ECKeyPair ourBaseKey, ourEphemeralKey;
|
||||
IdentityKeyPair ourIdentityKey;
|
||||
|
||||
int flags = KeyExchangeMessage.RESPONSE_FLAG;
|
||||
|
||||
Log.w(TAG, "KeyExchange is an initiate.");
|
||||
responseMessage = processInitiate(sessionRecord, message);
|
||||
}
|
||||
|
||||
if (!sessionRecord.getSessionState().hasPendingKeyExchange()) {
|
||||
Log.w(TAG, "We don't have a pending initiate...");
|
||||
ourBaseKey = Curve.generateKeyPair(true);
|
||||
ourEphemeralKey = Curve.generateKeyPair(true);
|
||||
ourIdentityKey = identityKeyStore.getIdentityKeyPair();
|
||||
if (message.isResponse()) {
|
||||
SessionState sessionState = sessionRecord.getSessionState();
|
||||
boolean hasPendingKeyExchange = sessionState.hasPendingKeyExchange();
|
||||
boolean isSimultaneousInitiateResponse = message.isResponseForSimultaneousInitiate();
|
||||
|
||||
sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey,
|
||||
ourEphemeralKey, ourIdentityKey);
|
||||
} else {
|
||||
Log.w(TAG, "We already have a pending initiate, responding as simultaneous initiate...");
|
||||
ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey();
|
||||
ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey();
|
||||
ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey();
|
||||
flags |= KeyExchangeMessage.SIMULTAENOUS_INITIATE_FLAG;
|
||||
|
||||
sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey,
|
||||
ourEphemeralKey, ourIdentityKey);
|
||||
if ((!hasPendingKeyExchange || sessionState.getPendingKeyExchangeSequence() != message.getSequence()) &&
|
||||
!isSimultaneousInitiateResponse)
|
||||
{
|
||||
throw new StaleKeyExchangeException();
|
||||
}
|
||||
|
||||
responseMessage = new KeyExchangeMessage(message.getSequence(),
|
||||
flags, ourBaseKey.getPublicKey(),
|
||||
ourEphemeralKey.getPublicKey(),
|
||||
ourIdentityKey.getPublicKey());
|
||||
}
|
||||
|
||||
if (message.getSequence() != sessionRecord.getSessionState().getPendingKeyExchangeSequence()) {
|
||||
@@ -232,6 +235,39 @@ public class SessionBuilder {
|
||||
return responseMessage;
|
||||
}
|
||||
|
||||
private KeyExchangeMessage processInitiate(SessionRecord sessionRecord, KeyExchangeMessage message)
|
||||
throws InvalidKeyException
|
||||
{
|
||||
ECKeyPair ourBaseKey, ourEphemeralKey;
|
||||
IdentityKeyPair ourIdentityKey;
|
||||
|
||||
int flags = KeyExchangeMessage.RESPONSE_FLAG;
|
||||
|
||||
if (!sessionRecord.getSessionState().hasPendingKeyExchange()) {
|
||||
Log.w(TAG, "We don't have a pending initiate...");
|
||||
ourBaseKey = Curve.generateKeyPair(true);
|
||||
ourEphemeralKey = Curve.generateKeyPair(true);
|
||||
ourIdentityKey = identityKeyStore.getIdentityKeyPair();
|
||||
|
||||
sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey,
|
||||
ourEphemeralKey, ourIdentityKey);
|
||||
} else {
|
||||
Log.w(TAG, "We already have a pending initiate, responding as simultaneous initiate...");
|
||||
ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey();
|
||||
ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey();
|
||||
ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey();
|
||||
flags |= KeyExchangeMessage.SIMULTAENOUS_INITIATE_FLAG;
|
||||
|
||||
sessionRecord.getSessionState().setPendingKeyExchange(message.getSequence(), ourBaseKey,
|
||||
ourEphemeralKey, ourIdentityKey);
|
||||
}
|
||||
|
||||
return new KeyExchangeMessage(message.getSequence(),
|
||||
flags, ourBaseKey.getPublicKey(),
|
||||
ourEphemeralKey.getPublicKey(),
|
||||
ourIdentityKey.getPublicKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a new session by sending an initial KeyExchangeMessage to the recipient.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.whispersystems.libaxolotl;
|
||||
|
||||
public class StaleKeyExchangeException extends Throwable {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.whispersystems.libaxolotl;
|
||||
|
||||
public class UntrustedIdentityException extends Exception {
|
||||
}
|
||||
@@ -37,4 +37,21 @@ public interface IdentityKeyStore {
|
||||
*/
|
||||
public void saveIdentity(long recipientId, IdentityKey identityKey);
|
||||
|
||||
|
||||
/**
|
||||
* Verify a remote client's identity key.
|
||||
* <p>
|
||||
* Determine whether a remote client's identity is trusted. Convention is
|
||||
* that the TextSecure protocol is 'trust on first use.' This means that
|
||||
* an identity key is considered 'trusted' if there is no entry for the recipient
|
||||
* in the local store, or if it matches the saved key for a recipient in the local
|
||||
* store. Only if it mismatches an entry in the local store is it considered
|
||||
* 'untrusted.'
|
||||
*
|
||||
* @param recipientId The recipient ID of the remote client.
|
||||
* @param identityKey The identity key to verify.
|
||||
* @return true if trusted, false if untrusted.
|
||||
*/
|
||||
public boolean isTrustedIdentity(long recipientId, IdentityKey identityKey);
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user