Skip to content

Commit

Permalink
Move identity key verification into libaxolotol. With tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
moxie0 committed Oct 20, 2014
1 parent 81ae9af commit 931605a
Show file tree
Hide file tree
Showing 13 changed files with 267 additions and 146 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,10 @@ public int getLocalRegistrationId() {
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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
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;
Expand All @@ -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();
Expand Down Expand Up @@ -74,9 +75,55 @@ public void testBasicPreKey()
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();
Expand Down Expand Up @@ -104,11 +151,28 @@ public void testBasicKeyExchange() throws InvalidKeyException, LegacyMessageExce
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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
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;
Expand Down Expand Up @@ -77,16 +78,19 @@ public SessionBuilder(SessionStore sessionStore,
* 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))
Expand Down Expand Up @@ -134,8 +138,16 @@ public void process(PreKeyWhisperMessage message)
* @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);
Expand Down Expand Up @@ -168,43 +180,34 @@ public void process(PreKey preKey) throws InvalidKeyException {
* @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();

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 (message.isResponse()) {
SessionState sessionState = sessionRecord.getSessionState();
boolean hasPendingKeyExchange = sessionState.hasPendingKeyExchange();
boolean isSimultaneousInitiateResponse = message.isResponseForSimultaneousInitiate();

responseMessage = new KeyExchangeMessage(message.getSequence(),
flags, ourBaseKey.getPublicKey(),
ourEphemeralKey.getPublicKey(),
ourIdentityKey.getPublicKey());
if ((!hasPendingKeyExchange || sessionState.getPendingKeyExchangeSequence() != message.getSequence()) &&
!isSimultaneousInitiateResponse)
{
throw new StaleKeyExchangeException();
}
}

if (message.getSequence() != sessionRecord.getSessionState().getPendingKeyExchangeSequence()) {
Expand Down Expand Up @@ -232,6 +235,39 @@ public KeyExchangeMessage process(KeyExchangeMessage message) throws InvalidKeyE
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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.whispersystems.libaxolotl;

public class StaleKeyExchangeException extends Throwable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package org.whispersystems.libaxolotl;

public class UntrustedIdentityException extends Exception {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}

0 comments on commit 931605a

Please sign in to comment.