Move session construction and KeyExchangeMessage into libaxolotl.

1) Add plain two-way key exchange support libaxolotl by moving
   all the KeyExchangeMessage code there.

2) Move the bulk of KeyExchangeProcessor code to libaxolotl
   for setting up sessions based on retrieved prekeys, received
   prekeybundles, or exchanged key exchange messages.
This commit is contained in:
Moxie Marlinspike
2014-04-22 21:31:57 -07:00
parent a1db221caf
commit 72af8b11c2
25 changed files with 565 additions and 476 deletions

View File

@@ -0,0 +1,27 @@
/**
* Copyright (C) 2014 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.libaxolotl;
public class InvalidKeyIdException extends Exception {
public InvalidKeyIdException(String detailMessage) {
super(detailMessage);
}
public InvalidKeyIdException(Throwable throwable) {
super(throwable);
}
}

View File

@@ -0,0 +1,180 @@
package org.whispersystems.libaxolotl;
import android.util.Log;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.protocol.KeyExchangeMessage;
import org.whispersystems.libaxolotl.protocol.PreKeyWhisperMessage;
import org.whispersystems.libaxolotl.ratchet.RatchetingSession;
import org.whispersystems.libaxolotl.state.IdentityKeyStore;
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.SessionStore;
import org.whispersystems.libaxolotl.util.Medium;
public class SessionBuilder {
private static final String TAG = SessionBuilder.class.getSimpleName();
private final SessionStore sessionStore;
private final PreKeyStore preKeyStore;
private final IdentityKeyStore identityKeyStore;
private final long recipientId;
private final int deviceId;
public SessionBuilder(SessionStore sessionStore,
PreKeyStore preKeyStore,
IdentityKeyStore identityKeyStore,
long recipientId, int deviceId)
{
this.sessionStore = sessionStore;
this.preKeyStore = preKeyStore;
this.identityKeyStore = identityKeyStore;
this.recipientId = recipientId;
this.deviceId = deviceId;
}
public void process(PreKeyWhisperMessage message)
throws InvalidKeyIdException, InvalidKeyException
{
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 (!preKeyStore.contains(preKeyId) &&
sessionStore.contains(recipientId, deviceId))
{
Log.w(TAG, "We've already processed the prekey part, letting bundled message fall through...");
return;
}
if (!preKeyStore.contains(preKeyId))
throw new InvalidKeyIdException("No such prekey: " + preKeyId);
SessionRecord sessionRecord = sessionStore.get(recipientId, deviceId);
PreKeyRecord preKeyRecord = preKeyStore.load(preKeyId);
ECKeyPair ourBaseKey = preKeyRecord.getKeyPair();
ECKeyPair ourEphemeralKey = ourBaseKey;
IdentityKeyPair ourIdentityKey = identityKeyStore.getIdentityKeyPair();
boolean simultaneousInitiate = sessionRecord.getSessionState().hasPendingPreKey();
if (!simultaneousInitiate) sessionRecord.reset();
else sessionRecord.archiveCurrentState();
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
ourBaseKey, theirBaseKey,
ourEphemeralKey, theirEphemeralKey,
ourIdentityKey, theirIdentityKey);
sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId());
sessionRecord.getSessionState().setRemoteRegistrationId(message.getRegistrationId());
if (simultaneousInitiate) sessionRecord.getSessionState().setNeedsRefresh(true);
sessionStore.put(recipientId, deviceId, sessionRecord);
if (preKeyId != Medium.MAX_VALUE) {
preKeyStore.remove(preKeyId);
}
identityKeyStore.saveIdentity(recipientId, theirIdentityKey);
}
public void process(PreKey preKey) throws InvalidKeyException {
SessionRecord sessionRecord = sessionStore.get(recipientId, deviceId);
ECKeyPair ourBaseKey = Curve.generateKeyPair(true);
ECKeyPair ourEphemeralKey = Curve.generateKeyPair(true);
ECPublicKey theirBaseKey = preKey.getPublicKey();
ECPublicKey theirEphemeralKey = theirBaseKey;
IdentityKey theirIdentityKey = preKey.getIdentityKey();
IdentityKeyPair ourIdentityKey = identityKeyStore.getIdentityKeyPair();
if (sessionRecord.getSessionState().getNeedsRefresh()) sessionRecord.archiveCurrentState();
else sessionRecord.reset();
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
ourBaseKey, theirBaseKey, ourEphemeralKey,
theirEphemeralKey, ourIdentityKey, theirIdentityKey);
sessionRecord.getSessionState().setPendingPreKey(preKey.getKeyId(), ourBaseKey.getPublicKey());
sessionRecord.getSessionState().setLocalRegistrationId(identityKeyStore.getLocalRegistrationId());
sessionRecord.getSessionState().setRemoteRegistrationId(preKey.getRegistrationId());
sessionStore.put(recipientId, deviceId, sessionRecord);
identityKeyStore.saveIdentity(recipientId, preKey.getIdentityKey());
}
public KeyExchangeMessage process(KeyExchangeMessage message) throws InvalidKeyException {
KeyExchangeMessage responseMessage = null;
SessionRecord sessionRecord = sessionStore.get(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.");
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);
}
responseMessage = new KeyExchangeMessage(message.getSequence(),
flags, ourBaseKey.getPublicKey(),
ourEphemeralKey.getPublicKey(),
ourIdentityKey.getPublicKey());
}
if (message.getSequence() != sessionRecord.getSessionState().getPendingKeyExchangeSequence()) {
Log.w("KeyExchangeProcessor", "No matching sequence for response. " +
"Is simultaneous initiate response: " + message.isResponseForSimultaneousInitiate());
return responseMessage;
}
ECKeyPair ourBaseKey = sessionRecord.getSessionState().getPendingKeyExchangeBaseKey();
ECKeyPair ourEphemeralKey = sessionRecord.getSessionState().getPendingKeyExchangeEphemeralKey();
IdentityKeyPair ourIdentityKey = sessionRecord.getSessionState().getPendingKeyExchangeIdentityKey();
sessionRecord.reset();
RatchetingSession.initializeSession(sessionRecord.getSessionState(),
ourBaseKey, message.getBaseKey(),
ourEphemeralKey, message.getEphemeralKey(),
ourIdentityKey, message.getIdentityKey());
sessionRecord.getSessionState().setSessionVersion(message.getVersion());
sessionStore.put(recipientId, deviceId, sessionRecord);
identityKeyStore.saveIdentity(recipientId, message.getIdentityKey());
return responseMessage;
}
}

View File

@@ -0,0 +1,138 @@
package org.whispersystems.libaxolotl.protocol;
import com.google.protobuf.ByteString;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.InvalidKeyException;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.InvalidVersionException;
import org.whispersystems.libaxolotl.LegacyMessageException;
import org.whispersystems.libaxolotl.ecc.Curve;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
import org.whispersystems.libaxolotl.util.ByteUtil;
import java.io.IOException;
public class KeyExchangeMessage {
public static final int INITIATE_FLAG = 0x01;
public static final int RESPONSE_FLAG = 0X02;
public static final int SIMULTAENOUS_INITIATE_FLAG = 0x04;
private final int version;
private final int supportedVersion;
private final int sequence;
private final int flags;
private final ECPublicKey baseKey;
private final ECPublicKey ephemeralKey;
private final IdentityKey identityKey;
private final byte[] serialized;
public KeyExchangeMessage(int sequence, int flags,
ECPublicKey baseKey, ECPublicKey ephemeralKey,
IdentityKey identityKey)
{
this.supportedVersion = CiphertextMessage.CURRENT_VERSION;
this.version = CiphertextMessage.CURRENT_VERSION;
this.sequence = sequence;
this.flags = flags;
this.baseKey = baseKey;
this.ephemeralKey = ephemeralKey;
this.identityKey = identityKey;
byte[] version = {ByteUtil.intsToByteHighAndLow(this.version, this.supportedVersion)};
byte[] message = WhisperProtos.KeyExchangeMessage.newBuilder()
.setId((sequence << 5) | flags)
.setBaseKey(ByteString.copyFrom(baseKey.serialize()))
.setEphemeralKey(ByteString.copyFrom(ephemeralKey.serialize()))
.setIdentityKey(ByteString.copyFrom(identityKey.serialize()))
.build().toByteArray();
this.serialized = ByteUtil.combine(version, message);
}
public KeyExchangeMessage(byte[] serialized)
throws InvalidMessageException, InvalidVersionException, LegacyMessageException
{
try {
byte[][] parts = ByteUtil.split(serialized, 1, serialized.length - 1);
this.version = ByteUtil.highBitsToInt(parts[0][0]);
this.supportedVersion = ByteUtil.lowBitsToInt(parts[0][0]);
if (this.version <= CiphertextMessage.UNSUPPORTED_VERSION) {
throw new LegacyMessageException("Unsupported legacy version: " + this.version);
}
if (this.version > CiphertextMessage.CURRENT_VERSION) {
throw new InvalidVersionException("Unknown version: " + this.version);
}
WhisperProtos.KeyExchangeMessage message = WhisperProtos.KeyExchangeMessage.parseFrom(parts[1]);
if (!message.hasId() || !message.hasBaseKey() ||
!message.hasEphemeralKey() || !message.hasIdentityKey())
{
throw new InvalidMessageException("Some required fields missing!");
}
this.sequence = message.getId() >> 5;
this.flags = message.getId() & 0x1f;
this.serialized = serialized;
this.baseKey = Curve.decodePoint(message.getBaseKey().toByteArray(), 0);
this.ephemeralKey = Curve.decodePoint(message.getEphemeralKey().toByteArray(), 0);
this.identityKey = new IdentityKey(message.getIdentityKey().toByteArray(), 0);
} catch (InvalidKeyException | IOException e) {
throw new InvalidMessageException(e);
}
}
public int getVersion() {
return version;
}
public ECPublicKey getBaseKey() {
return baseKey;
}
public ECPublicKey getEphemeralKey() {
return ephemeralKey;
}
public IdentityKey getIdentityKey() {
return identityKey;
}
public boolean hasIdentityKey() {
return true;
}
public int getMaxVersion() {
return supportedVersion;
}
public boolean isResponse() {
return ((flags & RESPONSE_FLAG) != 0);
}
public boolean isInitiate() {
return (flags & INITIATE_FLAG) != 0;
}
public boolean isResponseForSimultaneousInitiate() {
return (flags & SIMULTAENOUS_INITIATE_FLAG) != 0;
}
public int getFlags() {
return flags;
}
public int getSequence() {
return sequence;
}
public byte[] serialize() {
return serialized;
}
}

View File

@@ -0,0 +1,12 @@
package org.whispersystems.libaxolotl.state;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.IdentityKeyPair;
public interface IdentityKeyStore {
public IdentityKeyPair getIdentityKeyPair();
public int getLocalRegistrationId();
public void saveIdentity(long recipientId, IdentityKey identityKey);
}

View File

@@ -0,0 +1,12 @@
package org.whispersystems.libaxolotl.state;
import org.whispersystems.libaxolotl.IdentityKey;
import org.whispersystems.libaxolotl.ecc.ECPublicKey;
public interface PreKey {
public int getDeviceId();
public int getKeyId();
public ECPublicKey getPublicKey();
public IdentityKey getIdentityKey();
public int getRegistrationId();
}

View File

@@ -0,0 +1,9 @@
package org.whispersystems.libaxolotl.state;
import org.whispersystems.libaxolotl.ecc.ECKeyPair;
public interface PreKeyRecord {
public int getId();
public ECKeyPair getKeyPair();
public byte[] serialize();
}

View File

@@ -0,0 +1,12 @@
package org.whispersystems.libaxolotl.state;
import org.whispersystems.libaxolotl.InvalidKeyIdException;
public interface PreKeyStore {
public PreKeyRecord load(int preKeyId) throws InvalidKeyIdException;
public void store(int preKeyId, PreKeyRecord record);
public boolean contains(int preKeyId);
public void remove(int preKeyId);
}

View File

@@ -0,0 +1,5 @@
package org.whispersystems.libaxolotl.util;
public class Medium {
public static int MAX_VALUE = 0xFFFFFF;
}