Correctly handle formatting when "one time PreKey" is absent.

pull/1/head
Moxie Marlinspike 10 years ago
parent 07c61394e9
commit 9dce376780

@ -3,6 +3,7 @@ package org.whispersystems.test;
import android.test.AndroidTestCase;
import org.whispersystems.libaxolotl.DuplicateMessageException;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
import org.whispersystems.libaxolotl.InvalidMessageException;
@ -568,6 +569,60 @@ public class SessionBuilderTest extends AndroidTestCase {
bobSessionStore, bobPreKeyStore, bobSignedPreKeyStore, bobIdentityKeyStore);
}
public void testOptionalOneTimePreKey() throws Exception {
SessionStore aliceSessionStore = new InMemorySessionStore();
SignedPreKeyStore aliceSignedPreKeyStore = new InMemorySignedPreKeyStore();
PreKeyStore alicePreKeyStore = new InMemoryPreKeyStore();
IdentityKeyStore aliceIdentityKeyStore = new InMemoryIdentityKeyStore();
SessionBuilder aliceSessionBuilder = new SessionBuilder(aliceSessionStore, alicePreKeyStore,
aliceSignedPreKeyStore,
aliceIdentityKeyStore,
BOB_RECIPIENT_ID, 1);
SessionStore bobSessionStore = new InMemorySessionStore();
PreKeyStore bobPreKeyStore = new InMemoryPreKeyStore();
SignedPreKeyStore bobSignedPreKeyStore = new InMemorySignedPreKeyStore();
IdentityKeyStore bobIdentityKeyStore = new InMemoryIdentityKeyStore();
ECKeyPair bobPreKeyPair = Curve.generateKeyPair();
ECKeyPair bobSignedPreKeyPair = Curve.generateKeyPair();
byte[] bobSignedPreKeySignature = Curve.calculateSignature(bobIdentityKeyStore.getIdentityKeyPair().getPrivateKey(),
bobSignedPreKeyPair.getPublicKey().serialize());
PreKeyBundle bobPreKey = new PreKeyBundle(bobIdentityKeyStore.getLocalRegistrationId(), 1,
0, null,
22, bobSignedPreKeyPair.getPublicKey(),
bobSignedPreKeySignature,
bobIdentityKeyStore.getIdentityKeyPair().getPublicKey());
aliceSessionBuilder.process(bobPreKey);
assertTrue(aliceSessionStore.containsSession(BOB_RECIPIENT_ID, 1));
assertTrue(!aliceSessionStore.loadSession(BOB_RECIPIENT_ID, 1).getSessionState().getNeedsRefresh());
assertTrue(aliceSessionStore.loadSession(BOB_RECIPIENT_ID, 1).getSessionState().getSessionVersion() == 3);
String originalMessage = "L'homme est condamné à être libre";
SessionCipher aliceSessionCipher = new SessionCipher(aliceSessionStore, alicePreKeyStore, aliceSignedPreKeyStore, aliceIdentityKeyStore, BOB_RECIPIENT_ID, 1);
CiphertextMessage outgoingMessage = aliceSessionCipher.encrypt(originalMessage.getBytes());
assertTrue(outgoingMessage.getType() == CiphertextMessage.PREKEY_TYPE);
PreKeyWhisperMessage incomingMessage = new PreKeyWhisperMessage(outgoingMessage.serialize());
assertTrue(!incomingMessage.getPreKeyId().isPresent());
bobPreKeyStore.storePreKey(31337, new PreKeyRecord(bobPreKey.getPreKeyId(), bobPreKeyPair));
bobSignedPreKeyStore.storeSignedPreKey(22, new SignedPreKeyRecord(22, System.currentTimeMillis(), bobSignedPreKeyPair, bobSignedPreKeySignature));
SessionCipher bobSessionCipher = new SessionCipher(bobSessionStore, bobPreKeyStore, bobSignedPreKeyStore, bobIdentityKeyStore, ALICE_RECIPIENT_ID, 1);
byte[] plaintext = bobSessionCipher.decrypt(incomingMessage);
assertTrue(bobSessionStore.containsSession(ALICE_RECIPIENT_ID, 1));
assertTrue(bobSessionStore.loadSession(ALICE_RECIPIENT_ID, 1).getSessionState().getSessionVersion() == 3);
assertTrue(bobSessionStore.loadSession(ALICE_RECIPIENT_ID, 1).getSessionState().getAliceBaseKey() != null);
assertTrue(originalMessage.equals(new String(plaintext)));
}
private void runInteraction(SessionStore aliceSessionStore,
PreKeyStore alicePreKeyStore,
SignedPreKeyStore aliceSignedPreKeyStore,

@ -88,12 +88,13 @@ public class SessionBuilder {
* @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.
*/
/*package*/ int process(SessionRecord sessionRecord, PreKeyWhisperMessage message)
/*package*/ Optional<Integer> process(SessionRecord sessionRecord, PreKeyWhisperMessage message)
throws InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException
{
int messageVersion = message.getMessageVersion();
IdentityKey theirIdentityKey = message.getIdentityKey();
int unsignedPreKeyId;
Optional<Integer> unsignedPreKeyId;
if (!identityKeyStore.isTrustedIdentity(recipientId, theirIdentityKey)) {
throw new UntrustedIdentityException();
@ -109,13 +110,13 @@ public class SessionBuilder {
return unsignedPreKeyId;
}
private int processV3(SessionRecord sessionRecord, PreKeyWhisperMessage message)
private Optional<Integer> processV3(SessionRecord sessionRecord, PreKeyWhisperMessage message)
throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException
{
if (sessionRecord.hasSessionState(message.getMessageVersion(), message.getBaseKey().serialize())) {
Log.w(TAG, "We've already setup a session for this V3 message, letting bundled message fall through...");
return -1;
return Optional.absent();
}
boolean simultaneousInitiate = sessionRecord.getSessionState().hasUnacknowledgedPreKeyMessage();
@ -129,8 +130,8 @@ public class SessionBuilder {
.setOurSignedPreKey(ourSignedPreKey)
.setOurRatchetKey(ourSignedPreKey);
if (message.getPreKeyId() >= 0) {
parameters.setOurOneTimePreKey(Optional.of(preKeyStore.loadPreKey(message.getPreKeyId()).getKeyPair()));
if (message.getPreKeyId().isPresent()) {
parameters.setOurOneTimePreKey(Optional.of(preKeyStore.loadPreKey(message.getPreKeyId().get()).getKeyPair()));
} else {
parameters.setOurOneTimePreKey(Optional.<ECKeyPair>absent());
}
@ -146,25 +147,28 @@ public class SessionBuilder {
if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true);
if (message.getPreKeyId() >= 0 && message.getPreKeyId() != Medium.MAX_VALUE) {
if (message.getPreKeyId().isPresent() && message.getPreKeyId().get() != Medium.MAX_VALUE) {
return message.getPreKeyId();
} else {
return -1;
return Optional.absent();
}
}
private int processV2(SessionRecord sessionRecord, PreKeyWhisperMessage message)
private Optional<Integer> processV2(SessionRecord sessionRecord, PreKeyWhisperMessage message)
throws UntrustedIdentityException, InvalidKeyIdException, InvalidKeyException
{
if (!message.getPreKeyId().isPresent()) {
throw new InvalidKeyIdException("V2 message requires one time prekey id!");
}
if (!preKeyStore.containsPreKey(message.getPreKeyId()) &&
if (!preKeyStore.containsPreKey(message.getPreKeyId().get()) &&
sessionStore.containsSession(recipientId, deviceId))
{
Log.w(TAG, "We've already processed the prekey part of this V2 session, letting bundled message fall through...");
return -1;
return Optional.absent();
}
ECKeyPair ourPreKey = preKeyStore.loadPreKey(message.getPreKeyId()).getKeyPair();
ECKeyPair ourPreKey = preKeyStore.loadPreKey(message.getPreKeyId().get()).getKeyPair();
boolean simultaneousInitiate = sessionRecord.getSessionState().hasUnacknowledgedPreKeyMessage();
BobAxolotlParameters.Builder parameters = BobAxolotlParameters.newBuilder();
@ -186,10 +190,10 @@ public class SessionBuilder {
if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true);
if (message.getPreKeyId() != Medium.MAX_VALUE) {
if (message.getPreKeyId().get() != Medium.MAX_VALUE) {
return message.getPreKeyId();
} else {
return -1;
return Optional.absent();
}
}
@ -222,11 +226,14 @@ public class SessionBuilder {
throw new InvalidKeyException("Both signed and unsigned prekeys are absent!");
}
boolean isExistingSession = sessionStore.containsSession(recipientId, deviceId);
SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId);
ECKeyPair ourBaseKey = Curve.generateKeyPair();
ECPublicKey theirSignedPreKey = preKey.getSignedPreKey() != null ? preKey.getSignedPreKey() :
preKey.getPreKey();
boolean supportsV3 = preKey.getSignedPreKey() != null;
boolean isExistingSession = sessionStore.containsSession(recipientId, deviceId);
SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId);
ECKeyPair ourBaseKey = Curve.generateKeyPair();
ECPublicKey theirSignedPreKey = supportsV3 ? preKey.getSignedPreKey() : preKey.getPreKey();
Optional<ECPublicKey> theirOneTimePreKey = Optional.fromNullable(preKey.getPreKey());
Optional<Integer> theirOneTimePreKeyId = theirOneTimePreKey.isPresent() ? Optional.of(preKey.getPreKeyId()) :
Optional.<Integer>absent();
AliceAxolotlParameters.Builder parameters = AliceAxolotlParameters.newBuilder();
@ -235,18 +242,16 @@ public class SessionBuilder {
.setTheirIdentityKey(preKey.getIdentityKey())
.setTheirSignedPreKey(theirSignedPreKey)
.setTheirRatchetKey(theirSignedPreKey)
.setTheirOneTimePreKey(preKey.getSignedPreKey() != null ?
Optional.fromNullable(preKey.getPreKey()) :
Optional.<ECPublicKey>absent());
.setTheirOneTimePreKey(supportsV3 ? theirOneTimePreKey : Optional.<ECPublicKey>absent());
if (isExistingSession) sessionRecord.archiveCurrentState();
else sessionRecord.reset();
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
preKey.getSignedPreKey() == null ? 2 : 3,
supportsV3 ? 3 : 2,
parameters.create());
sessionRecord.getSessionState().setUnacknowledgedPreKeyMessage(preKey.getPreKeyId(), preKey.getSignedPreKeyId(), ourBaseKey.getPublicKey());
sessionRecord.getSessionState().setUnacknowledgedPreKeyMessage(theirOneTimePreKeyId, preKey.getSignedPreKeyId(), ourBaseKey.getPublicKey());
sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId());
sessionRecord.getSessionState().setRemoteRegistrationId(preKey.getRegistrationId());

@ -33,6 +33,7 @@ import org.whispersystems.libaxolotl.state.SessionStore;
import org.whispersystems.libaxolotl.state.SignedPreKeyStore;
import org.whispersystems.libaxolotl.util.ByteUtil;
import org.whispersystems.libaxolotl.util.Pair;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
@ -147,14 +148,14 @@ public class SessionCipher {
InvalidKeyIdException, InvalidKeyException, UntrustedIdentityException
{
synchronized (SESSION_LOCK) {
SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId);
int unsignedPreKeyId = sessionBuilder.process(sessionRecord, ciphertext);
byte[] plaintext = decrypt(sessionRecord, ciphertext.getWhisperMessage());
SessionRecord sessionRecord = sessionStore.loadSession(recipientId, deviceId);
Optional<Integer> unsignedPreKeyId = sessionBuilder.process(sessionRecord, ciphertext);
byte[] plaintext = decrypt(sessionRecord, ciphertext.getWhisperMessage());
sessionStore.storeSession(recipientId, deviceId, sessionRecord);
if (unsignedPreKeyId >=0) {
preKeyStore.removePreKey(unsignedPreKeyId);
if (unsignedPreKeyId.isPresent()) {
preKeyStore.removePreKey(unsignedPreKeyId.get());
}
return plaintext;

@ -27,18 +27,19 @@ import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.util.ByteUtil;
import org.whispersystems.libaxolotl.util.guava.Optional;
public class PreKeyWhisperMessage implements CiphertextMessage {
private final int version;
private final int registrationId;
private final int preKeyId;
private final int signedPreKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
private final WhisperMessage message;
private final byte[] serialized;
private final int version;
private final int registrationId;
private final Optional<Integer> preKeyId;
private final int signedPreKeyId;
private final ECPublicKey baseKey;
private final IdentityKey identityKey;
private final WhisperMessage message;
private final byte[] serialized;
public PreKeyWhisperMessage(byte[] serialized)
throws InvalidMessageException, InvalidVersionException
@ -65,7 +66,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
this.serialized = serialized;
this.registrationId = preKeyWhisperMessage.getRegistrationId();
this.preKeyId = preKeyWhisperMessage.hasPreKeyId() ? preKeyWhisperMessage.getPreKeyId() : -1;
this.preKeyId = preKeyWhisperMessage.hasPreKeyId() ? Optional.of(preKeyWhisperMessage.getPreKeyId()) : Optional.<Integer>absent();
this.signedPreKeyId = preKeyWhisperMessage.hasSignedPreKeyId() ? preKeyWhisperMessage.getSignedPreKeyId() : -1;
this.baseKey = Curve.decodePoint(preKeyWhisperMessage.getBaseKey().toByteArray(), 0);
this.identityKey = new IdentityKey(Curve.decodePoint(preKeyWhisperMessage.getIdentityKey().toByteArray(), 0));
@ -75,8 +76,9 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
}
}
public PreKeyWhisperMessage(int messageVersion, int registrationId, int preKeyId, int signedPreKeyId,
ECPublicKey baseKey, IdentityKey identityKey, WhisperMessage message)
public PreKeyWhisperMessage(int messageVersion, int registrationId, Optional<Integer> preKeyId,
int signedPreKeyId, ECPublicKey baseKey, IdentityKey identityKey,
WhisperMessage message)
{
this.version = messageVersion;
this.registrationId = registrationId;
@ -86,15 +88,20 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
this.identityKey = identityKey;
this.message = message;
WhisperProtos.PreKeyWhisperMessage.Builder builder =
WhisperProtos.PreKeyWhisperMessage.newBuilder()
.setSignedPreKeyId(signedPreKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setMessage(ByteString.copyFrom(message.serialize()))
.setRegistrationId(registrationId);
if (preKeyId.isPresent()) {
builder.setPreKeyId(preKeyId.get());
}
byte[] versionBytes = {ByteUtil.intsToByteHighAndLow(this.version, CURRENT_VERSION)};
byte[] messageBytes = WhisperProtos.PreKeyWhisperMessage.newBuilder()
.setPreKeyId(preKeyId)
.setSignedPreKeyId(signedPreKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.setMessage(ByteString.copyFrom(message.serialize()))
.setRegistrationId(registrationId)
.build().toByteArray();
byte[] messageBytes = builder.build().toByteArray();
this.serialized = ByteUtil.combine(versionBytes, messageBytes);
}
@ -111,7 +118,7 @@ public class PreKeyWhisperMessage implements CiphertextMessage {
return registrationId;
}
public int getPreKeyId() {
public Optional<Integer> getPreKeyId() {
return preKeyId;
}

@ -36,6 +36,7 @@ import org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure.Chain;
import org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure.PendingKeyExchange;
import org.whispersystems.libaxolotl.state.StorageProtos.SessionStructure.PendingPreKey;
import org.whispersystems.libaxolotl.util.Pair;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.util.Iterator;
import java.util.LinkedList;
@ -415,15 +416,17 @@ public class SessionState {
return sessionStructure.hasPendingKeyExchange();
}
public void setUnacknowledgedPreKeyMessage(int preKeyId, int signedPreKeyId, ECPublicKey baseKey) {
PendingPreKey pending = PendingPreKey.newBuilder()
.setPreKeyId(preKeyId)
.setSignedPreKeyId(signedPreKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.build();
public void setUnacknowledgedPreKeyMessage(Optional<Integer> preKeyId, int signedPreKeyId, ECPublicKey baseKey) {
PendingPreKey.Builder pending = PendingPreKey.newBuilder()
.setSignedPreKeyId(signedPreKeyId)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()));
if (preKeyId.isPresent()) {
pending.setPreKeyId(preKeyId.get());
}
this.sessionStructure = this.sessionStructure.toBuilder()
.setPendingPreKey(pending)
.setPendingPreKey(pending.build())
.build();
}
@ -433,8 +436,16 @@ public class SessionState {
public UnacknowledgedPreKeyMessageItems getUnacknowledgedPreKeyMessageItems() {
try {
Optional<Integer> preKeyId;
if (sessionStructure.getPendingPreKey().hasPreKeyId()) {
preKeyId = Optional.of(sessionStructure.getPendingPreKey().getPreKeyId());
} else {
preKeyId = Optional.absent();
}
return
new UnacknowledgedPreKeyMessageItems(sessionStructure.getPendingPreKey().getPreKeyId(),
new UnacknowledgedPreKeyMessageItems(preKeyId,
sessionStructure.getPendingPreKey().getSignedPreKeyId(),
Curve.decodePoint(sessionStructure.getPendingPreKey()
.getBaseKey()
@ -475,18 +486,21 @@ public class SessionState {
}
public static class UnacknowledgedPreKeyMessageItems {
private final int preKeyId;
private final int signedPreKeyId;
private final ECPublicKey baseKey;
public UnacknowledgedPreKeyMessageItems(int preKeyId, int signedPreKeyId, ECPublicKey baseKey) {
private final Optional<Integer> preKeyId;
private final int signedPreKeyId;
private final ECPublicKey baseKey;
public UnacknowledgedPreKeyMessageItems(Optional<Integer> preKeyId,
int signedPreKeyId,
ECPublicKey baseKey)
{
this.preKeyId = preKeyId;
this.signedPreKeyId = signedPreKeyId;
this.baseKey = baseKey;
}
public int getPreKeyId() {
public Optional<Integer> getPreKeyId() {
return preKeyId;
}

Loading…
Cancel
Save