diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/agreement/TransportKeyAgreementManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/agreement/TransportKeyAgreementManager.java new file mode 100644 index 000000000..1b8fc6bc1 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/agreement/TransportKeyAgreementManager.java @@ -0,0 +1,24 @@ +package org.briarproject.bramble.api.transport.agreement; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.ClientId; + +@NotNullByDefault +public interface TransportKeyAgreementManager { + + /** + * The unique ID of the transport key agreement client. + */ + ClientId CLIENT_ID = + new ClientId("org.briarproject.bramble.transport.agreement"); + + /** + * The current major version of the transport key agreement client. + */ + int MAJOR_VERSION = 0; + + /** + * The current minor version of the transport key agreement client. + */ + int MINOR_VERSION = 0; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java index c256759ff..6732f8001 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreEagerSingletons.java @@ -11,6 +11,7 @@ import org.briarproject.bramble.properties.PropertiesModule; import org.briarproject.bramble.rendezvous.RendezvousModule; import org.briarproject.bramble.sync.validation.ValidationModule; import org.briarproject.bramble.transport.TransportModule; +import org.briarproject.bramble.transport.agreement.TransportKeyAgreementModule; import org.briarproject.bramble.versioning.VersioningModule; public interface BrambleCoreEagerSingletons { @@ -33,6 +34,8 @@ public interface BrambleCoreEagerSingletons { void inject(RendezvousModule.EagerSingletons init); + void inject(TransportKeyAgreementModule.EagerSingletons init); + void inject(TransportModule.EagerSingletons init); void inject(ValidationModule.EagerSingletons init); @@ -51,6 +54,7 @@ public interface BrambleCoreEagerSingletons { c.inject(new RendezvousModule.EagerSingletons()); c.inject(new PluginModule.EagerSingletons()); c.inject(new PropertiesModule.EagerSingletons()); + c.inject(new TransportKeyAgreementModule.EagerSingletons()); c.inject(new TransportModule.EagerSingletons()); c.inject(new ValidationModule.EagerSingletons()); c.inject(new VersioningModule.EagerSingletons()); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java index 447bd5cb6..51069c526 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/BrambleCoreModule.java @@ -23,6 +23,7 @@ import org.briarproject.bramble.settings.SettingsModule; import org.briarproject.bramble.sync.SyncModule; import org.briarproject.bramble.sync.validation.ValidationModule; import org.briarproject.bramble.transport.TransportModule; +import org.briarproject.bramble.transport.agreement.TransportKeyAgreementModule; import org.briarproject.bramble.versioning.VersioningModule; import dagger.Module; @@ -49,6 +50,7 @@ import dagger.Module; RendezvousModule.class, SettingsModule.class, SyncModule.class, + TransportKeyAgreementModule.class, TransportModule.class, ValidationModule.class, VersioningModule.class diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoder.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoder.java new file mode 100644 index 000000000..058fc894d --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoder.java @@ -0,0 +1,22 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; + +@NotNullByDefault +interface MessageEncoder { + + Message encodeKeyMessage(GroupId contactGroupId, + TransportId transportId, PublicKey publicKey); + + Message encodeActivateMessage(GroupId contactGroupId, + TransportId transportId, MessageId previousMessageId); + + BdfDictionary encodeMessageMetadata(TransportId transportId, + MessageType type, boolean local); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoderImpl.java new file mode 100644 index 000000000..35d019a79 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageEncoderImpl.java @@ -0,0 +1,77 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_IS_SESSION; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_LOCAL; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; + +@Immutable +@NotNullByDefault +class MessageEncoderImpl implements MessageEncoder { + + private final ClientHelper clientHelper; + private final Clock clock; + + @Inject + MessageEncoderImpl(ClientHelper clientHelper, Clock clock) { + this.clientHelper = clientHelper; + this.clock = clock; + } + + @Override + public Message encodeKeyMessage(GroupId contactGroupId, + TransportId transportId, PublicKey publicKey) { + BdfList body = BdfList.of( + KEY.getValue(), + transportId.getString(), + publicKey.getEncoded()); + return encodeMessage(contactGroupId, body); + } + + @Override + public Message encodeActivateMessage(GroupId contactGroupId, + TransportId transportId, MessageId previousMessageId) { + BdfList body = BdfList.of( + ACTIVATE.getValue(), + transportId.getString(), + previousMessageId); + return encodeMessage(contactGroupId, body); + } + + @Override + public BdfDictionary encodeMessageMetadata(TransportId transportId, + MessageType type, boolean local) { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_IS_SESSION, false), + new BdfEntry(MSG_KEY_TRANSPORT_ID, transportId.getString()), + new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue()), + new BdfEntry(MSG_KEY_LOCAL, local)); + } + + private Message encodeMessage(GroupId contactGroupId, BdfList body) { + try { + return clientHelper.createMessage(contactGroupId, + clock.currentTimeMillis(), clientHelper.toByteArray(body)); + } catch (FormatException e) { + throw new AssertionError(); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageType.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageType.java new file mode 100644 index 000000000..1e63464e3 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/MessageType.java @@ -0,0 +1,29 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum MessageType { + + KEY(0), + ACTIVATE(1); + + private final int value; + + MessageType(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static MessageType fromValue(int value) throws FormatException { + for (MessageType t : values()) if (t.value == value) return t; + throw new FormatException(); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/Session.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/Session.java new file mode 100644 index 000000000..e8d03e5a6 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/Session.java @@ -0,0 +1,65 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class Session { + + private final State state; + @Nullable + private final MessageId lastLocalMessageId; + @Nullable + private final KeyPair localKeyPair; + @Nullable + private final Long localTimestamp, remoteTimestamp; + @Nullable + private final KeySetId keySetId; + + Session(State state, @Nullable MessageId lastLocalMessageId, + @Nullable KeyPair localKeyPair, + @Nullable Long localTimestamp, @Nullable Long remoteTimestamp, + @Nullable KeySetId keySetId) { + this.state = state; + this.lastLocalMessageId = lastLocalMessageId; + this.localKeyPair = localKeyPair; + this.localTimestamp = localTimestamp; + this.remoteTimestamp = remoteTimestamp; + this.keySetId = keySetId; + } + + State getState() { + return state; + } + + @Nullable + MessageId getLastLocalMessageId() { + return lastLocalMessageId; + } + + @Nullable + KeyPair getLocalKeyPair() { + return localKeyPair; + } + + @Nullable + Long getLocalTimestamp() { + return localTimestamp; + } + + @Nullable + Long getRemoteTimestamp() { + return remoteTimestamp; + } + + @Nullable + KeySetId getKeySetId() { + return keySetId; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoder.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoder.java new file mode 100644 index 000000000..949ed977b --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoder.java @@ -0,0 +1,13 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; + +@NotNullByDefault +interface SessionEncoder { + + BdfDictionary encodeSession(Session s, TransportId transportId); + + BdfDictionary getSessionQuery(TransportId transportId); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoderImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoderImpl.java new file mode 100644 index 000000000..ce68c42be --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionEncoderImpl.java @@ -0,0 +1,70 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.transport.KeySetId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_IS_SESSION; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_KEY_SET_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PRIVATE_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_REMOTE_TIMESTAMP; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_STATE; + +@Immutable +@NotNullByDefault +class SessionEncoderImpl implements SessionEncoder { + + @Inject + SessionEncoderImpl() { + } + + @Override + public BdfDictionary encodeSession(Session s, TransportId transportId) { + BdfDictionary meta = new BdfDictionary(); + meta.put(MSG_KEY_IS_SESSION, true); + meta.put(MSG_KEY_TRANSPORT_ID, transportId.getString()); + meta.put(SESSION_KEY_STATE, s.getState().getValue()); + putNullable(meta, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, + s.getLastLocalMessageId()); + KeyPair localKeyPair = s.getLocalKeyPair(); + if (localKeyPair == null) { + meta.put(SESSION_KEY_LOCAL_PUBLIC_KEY, NULL_VALUE); + meta.put(SESSION_KEY_LOCAL_PRIVATE_KEY, NULL_VALUE); + } else { + meta.put(SESSION_KEY_LOCAL_PUBLIC_KEY, + localKeyPair.getPublic().getEncoded()); + meta.put(SESSION_KEY_LOCAL_PRIVATE_KEY, + localKeyPair.getPrivate().getEncoded()); + } + putNullable(meta, SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp()); + putNullable(meta, SESSION_KEY_REMOTE_TIMESTAMP, s.getRemoteTimestamp()); + KeySetId keySetId = s.getKeySetId(); + if (keySetId == null) meta.put(SESSION_KEY_KEY_SET_ID, NULL_VALUE); + else meta.put(SESSION_KEY_KEY_SET_ID, keySetId.getInt()); + return meta; + } + + @Override + public BdfDictionary getSessionQuery(TransportId transportId) { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_IS_SESSION, true), + new BdfEntry(MSG_KEY_TRANSPORT_ID, transportId.getString())); + } + + private void putNullable(BdfDictionary meta, String key, + @Nullable Object o) { + meta.put(key, o == null ? NULL_VALUE : o); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParser.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParser.java new file mode 100644 index 000000000..7b9e2a3e8 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParser.java @@ -0,0 +1,11 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface SessionParser { + + Session parseSession(BdfDictionary meta) throws FormatException; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParserImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParserImpl.java new file mode 100644 index 000000000..34736e9da --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/SessionParserImpl.java @@ -0,0 +1,70 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_KEY_SET_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PRIVATE_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_REMOTE_TIMESTAMP; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.SESSION_KEY_STATE; + +@Immutable +@NotNullByDefault +class SessionParserImpl implements SessionParser { + + private final TransportKeyAgreementCrypto crypto; + + @Inject + SessionParserImpl(TransportKeyAgreementCrypto crypto) { + this.crypto = crypto; + } + + @Override + public Session parseSession(BdfDictionary meta) throws FormatException { + State state = + State.fromValue(meta.getLong(SESSION_KEY_STATE).intValue()); + + MessageId lastLocalMessageId = null; + byte[] lastLocalMessageIdBytes = + meta.getOptionalRaw(SESSION_KEY_LAST_LOCAL_MESSAGE_ID); + if (lastLocalMessageIdBytes != null) { + lastLocalMessageId = new MessageId(lastLocalMessageIdBytes); + } + + KeyPair localKeyPair = null; + byte[] localPublicKeyBytes = + meta.getOptionalRaw(SESSION_KEY_LOCAL_PUBLIC_KEY); + byte[] localPrivateKeyBytes = + meta.getOptionalRaw(SESSION_KEY_LOCAL_PRIVATE_KEY); + if (localPublicKeyBytes != null && localPrivateKeyBytes != null) { + PublicKey pub = crypto.parsePublicKey(localPublicKeyBytes); + PrivateKey priv = crypto.parsePrivateKey(localPrivateKeyBytes); + localKeyPair = new KeyPair(pub, priv); + } + + Long localTimestamp = meta.getOptionalLong(SESSION_KEY_LOCAL_TIMESTAMP); + Long remoteTimestamp = + meta.getOptionalLong(SESSION_KEY_REMOTE_TIMESTAMP); + + KeySetId keySetId = null; + Long keySetIdLong = meta.getOptionalLong(SESSION_KEY_KEY_SET_ID); + if (keySetIdLong != null) { + keySetId = new KeySetId(keySetIdLong.intValue()); + } + + return new Session(state, lastLocalMessageId, localKeyPair, + localTimestamp, remoteTimestamp, keySetId); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/State.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/State.java new file mode 100644 index 000000000..9ab3f97da --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/State.java @@ -0,0 +1,43 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum State { + + /** + * We've sent a key message and are awaiting the contact's key message. + */ + AWAIT_KEY(0), + + /** + * We've exchanged key messages, derived the transport keys and sent an + * activate message, and now we're awaiting the contact's activate message. + */ + AWAIT_ACTIVATE(1), + + /** + * We've exchanged key messages and activate messages, and have derived and + * activated the transport keys. This is the end state. + */ + ACTIVATED(2); + + private final int value; + + State(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static State fromValue(int value) throws FormatException { + for (State s : values()) if (s.value == value) return s; + throw new FormatException(); + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementConstants.java new file mode 100644 index 000000000..278e67303 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementConstants.java @@ -0,0 +1,28 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface TransportKeyAgreementConstants { + + String MSG_KEY_IS_SESSION = "isSession"; + String MSG_KEY_MESSAGE_TYPE = "messageType"; + String MSG_KEY_TRANSPORT_ID = "transportId"; + String MSG_KEY_PUBLIC_KEY = "publicKey"; + String MSG_KEY_LOCAL = "local"; + + String SESSION_KEY_STATE = "state"; + String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId"; + String SESSION_KEY_LOCAL_PUBLIC_KEY = "localPublicKey"; + String SESSION_KEY_LOCAL_PRIVATE_KEY = "localPrivateKey"; + String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp"; + String SESSION_KEY_REMOTE_TIMESTAMP = "remoteTimestamp"; + String SESSION_KEY_KEY_SET_ID = "keySetId"; + + /** + * Label for deriving the root key from key pairs. + */ + String ROOT_KEY_LABEL = + "org.briarproject.bramble.transport.agreement/ROOT_KEY"; + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCrypto.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCrypto.java new file mode 100644 index 000000000..fc9b8a575 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCrypto.java @@ -0,0 +1,23 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.security.GeneralSecurityException; + +@NotNullByDefault +interface TransportKeyAgreementCrypto { + + KeyPair generateKeyPair(); + + SecretKey deriveRootKey(KeyPair localKeyPair, PublicKey remotePublicKey, + long timestamp) throws GeneralSecurityException; + + PublicKey parsePublicKey(byte[] encoded) throws FormatException; + + PrivateKey parsePrivateKey(byte[] encoded) throws FormatException; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCryptoImpl.java new file mode 100644 index 000000000..5ae01dccc --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementCryptoImpl.java @@ -0,0 +1,67 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import java.security.GeneralSecurityException; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.Bytes.compare; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.ROOT_KEY_LABEL; + +@Immutable +@NotNullByDefault +class TransportKeyAgreementCryptoImpl implements TransportKeyAgreementCrypto { + + private final CryptoComponent crypto; + + @Inject + TransportKeyAgreementCryptoImpl(CryptoComponent crypto) { + this.crypto = crypto; + } + + @Override + public KeyPair generateKeyPair() { + return crypto.generateAgreementKeyPair(); + } + + @Override + public SecretKey deriveRootKey(KeyPair localKeyPair, + PublicKey remotePublicKey, long timestamp) + throws GeneralSecurityException { + byte[] theirPublic = remotePublicKey.getEncoded(); + byte[] ourPublic = localKeyPair.getPublic().getEncoded(); + boolean alice = compare(ourPublic, theirPublic) < 0; + byte[][] inputs = { + alice ? ourPublic : theirPublic, + alice ? theirPublic : ourPublic + }; + return crypto.deriveSharedSecret(ROOT_KEY_LABEL, remotePublicKey, + localKeyPair, inputs); + } + + @Override + public PublicKey parsePublicKey(byte[] encoded) throws FormatException { + try { + return crypto.getAgreementKeyParser().parsePublicKey(encoded); + } catch (GeneralSecurityException e) { + throw new FormatException(); + } + } + + @Override + public PrivateKey parsePrivateKey(byte[] encoded) throws FormatException { + try { + return crypto.getAgreementKeyParser().parsePrivateKey(encoded); + } catch (GeneralSecurityException e) { + throw new FormatException(); + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImpl.java new file mode 100644 index 000000000..e937f98b7 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImpl.java @@ -0,0 +1,411 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.BdfIncomingMessageHook; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager.ContactHook; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataParser; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager.OpenDatabaseHook; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.PluginConfig; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; +import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Group.Visibility; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager; +import org.briarproject.bramble.api.versioning.ClientVersioningManager; +import org.briarproject.bramble.api.versioning.ClientVersioningManager.ClientVersioningHook; + +import java.security.GeneralSecurityException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static java.lang.Math.min; +import static java.util.Collections.singletonMap; +import static java.util.logging.Level.INFO; +import static java.util.logging.Logger.getLogger; +import static org.briarproject.bramble.api.Bytes.compare; +import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNonNull; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.DEFER; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.REJECT; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.State.ACTIVATED; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_ACTIVATE; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; + +@Immutable +@NotNullByDefault +class TransportKeyAgreementManagerImpl extends BdfIncomingMessageHook + implements TransportKeyAgreementManager, OpenDatabaseHook, ContactHook, + ClientVersioningHook { + + private static final Logger LOG = + getLogger(TransportKeyAgreementManagerImpl.class.getName()); + + private final ContactGroupFactory contactGroupFactory; + private final ClientVersioningManager clientVersioningManager; + private final IdentityManager identityManager; + private final KeyManager keyManager; + private final MessageEncoder messageEncoder; + private final SessionEncoder sessionEncoder; + private final SessionParser sessionParser; + private final TransportKeyAgreementCrypto crypto; + + private final List transports; + private final Group localGroup; + + @Inject + TransportKeyAgreementManagerImpl( + DatabaseComponent db, + ClientHelper clientHelper, + MetadataParser metadataParser, + ContactGroupFactory contactGroupFactory, + ClientVersioningManager clientVersioningManager, + IdentityManager identityManager, + KeyManager keyManager, + MessageEncoder messageEncoder, + SessionEncoder sessionEncoder, + SessionParser sessionParser, + TransportKeyAgreementCrypto crypto, + PluginConfig config) { + super(db, clientHelper, metadataParser); + this.contactGroupFactory = contactGroupFactory; + this.clientVersioningManager = clientVersioningManager; + this.identityManager = identityManager; + this.keyManager = keyManager; + this.messageEncoder = messageEncoder; + this.sessionEncoder = sessionEncoder; + this.sessionParser = sessionParser; + this.crypto = crypto; + transports = new ArrayList<>(); + for (DuplexPluginFactory duplex : config.getDuplexFactories()) { + transports.add(duplex.getId()); + } + for (SimplexPluginFactory simplex : config.getSimplexFactories()) { + transports.add(simplex.getId()); + } + localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID, + MAJOR_VERSION); + } + + @Override + public void onDatabaseOpened(Transaction txn) throws DbException { + Collection contacts = db.getContacts(txn); + if (!db.containsGroup(txn, localGroup.getId())) { + db.addGroup(txn, localGroup); + // Set things up for any pre-existing contacts + for (Contact c : contacts) addingContact(txn, c); + } + // Find any contacts and transports that need keys + Map> transportsWithKeys = + db.getTransportsWithKeys(txn); + for (Contact c : contacts) { + Collection withKeys = + transportsWithKeys.get(c.getId()); + for (TransportId t : transports) { + if (withKeys == null || !withKeys.contains(t)) { + // We need keys for this contact and transport + GroupId contactGroupId = getContactGroup(c).getId(); + SavedSession ss = loadSession(txn, contactGroupId, t); + if (ss == null) { + // Start a session by sending our key message + startSession(txn, contactGroupId, t); + } + } + } + } + } + + @Override + public void addingContact(Transaction txn, Contact c) throws DbException { + // Create a group to share with the contact + Group g = getContactGroup(c); + db.addGroup(txn, g); + // Attach the contact ID to the group + clientHelper.setContactId(txn, g.getId(), c.getId()); + // Apply the client's visibility to the contact group + Visibility client = clientVersioningManager.getClientVisibility(txn, + c.getId(), CLIENT_ID, MAJOR_VERSION); + db.setGroupVisibility(txn, c.getId(), g.getId(), client); + } + + @Override + public void removingContact(Transaction txn, Contact c) throws DbException { + db.removeGroup(txn, getContactGroup(c)); + } + + @Override + public void onClientVisibilityChanging(Transaction txn, Contact c, + Visibility v) throws DbException { + // Apply the client's visibility to the contact group + Group g = getContactGroup(c); + db.setGroupVisibility(txn, c.getId(), g.getId(), v); + } + + @Override + protected DeliveryAction incomingMessage(Transaction txn, Message m, + BdfList body, BdfDictionary meta) + throws DbException, FormatException { + MessageType type = MessageType.fromValue( + meta.getLong(MSG_KEY_MESSAGE_TYPE).intValue()); + TransportId t = new TransportId(meta.getString(MSG_KEY_TRANSPORT_ID)); + if (LOG.isLoggable(INFO)) { + LOG.info("Received " + type + " message for " + t); + } + if (!transports.contains(t)) { + // Defer handling the message until we support the transport + return DEFER; + } + SavedSession ss = loadSession(txn, m.getGroupId(), t); + if (type == KEY) return handleKeyMessage(txn, t, m, meta, ss); + else if (type == ACTIVATE) return handleActivateMessage(txn, t, ss); + else throw new AssertionError(); + } + + private DeliveryAction handleKeyMessage(Transaction txn, TransportId t, + Message m, BdfDictionary meta, @Nullable SavedSession ss) + throws DbException, FormatException { + ContactId c = clientHelper.getContactId(txn, m.getGroupId()); + boolean haveKeys = db.containsTransportKeys(txn, c, t); + if (ss == null) { + if (haveKeys) { + // We have keys but no session, so we must have derived keys + // when adding the contact. If the contact didn't support + // the transport when they added us, they wouldn't have + // derived keys at that time. If they later added support for + // the transport then they would have started a session, so a + // key message is valid in this case + return handleKeyMessageForNewSession(txn, c, t, m, meta); + } else { + // We don't have keys, so we should have created a session at + // startup + throw new IllegalStateException(); + } + } else if (ss.session.getState() == AWAIT_KEY) { + if (haveKeys) { + // We have keys, so we shouldn't be in the AWAIT_KEY state, + // even if the contact didn't derive keys when adding us and + // later started a session + throw new IllegalStateException(); + } else { + // This is the key message we're waiting for + return handleKeyMessageForExistingSession(txn, c, t, m, meta, + ss); + } + } else { + return REJECT; // Not valid in this state + } + } + + private DeliveryAction handleActivateMessage(Transaction txn, + TransportId t, @Nullable SavedSession ss) throws DbException { + if (ss != null && ss.session.getState() == AWAIT_ACTIVATE) { + // Activate the keys and finish the session + KeySetId keySetId = requireNonNull(ss.session.getKeySetId()); + keyManager.activateKeys(txn, singletonMap(t, keySetId)); + Session session = new Session(ACTIVATED, + ss.session.getLastLocalMessageId(), null, null, null, null); + saveSession(txn, t, ss.storageId, session); + return ACCEPT_DO_NOT_SHARE; + } else { + return REJECT; // Not valid in this state + } + } + + private DeliveryAction handleKeyMessageForNewSession(Transaction txn, + ContactId c, TransportId t, Message m, BdfDictionary meta) + throws DbException, FormatException { + KeyPair localKeyPair = crypto.generateKeyPair(); + PublicKey remotePublicKey = + crypto.parsePublicKey(meta.getRaw(MSG_KEY_PUBLIC_KEY)); + Message keyMessage = sendKeyMessage(txn, m.getGroupId(), t, + localKeyPair.getPublic()); + long minTimestamp = min(keyMessage.getTimestamp(), m.getTimestamp()); + SecretKey rootKey; + try { + rootKey = crypto.deriveRootKey(localKeyPair, remotePublicKey, + minTimestamp); + } catch (GeneralSecurityException e) { + return REJECT; // Invalid public key + } + boolean alice = isLocalPartyAlice(txn, db.getContact(txn, c)); + KeySetId keySetId = keyManager.addRotationKeys(txn, c, t, rootKey, + minTimestamp, alice, false); + Message activateMessage = + sendActivateMessage(txn, m.getGroupId(), t, keyMessage.getId()); + Session session = new Session(AWAIT_ACTIVATE, activateMessage.getId(), + null, null, null, keySetId); + saveNewSession(txn, m.getGroupId(), t, session); + return ACCEPT_DO_NOT_SHARE; + } + + private DeliveryAction handleKeyMessageForExistingSession(Transaction txn, + ContactId c, TransportId t, Message m, BdfDictionary meta, + SavedSession ss) throws DbException, FormatException { + KeyPair localKeyPair = requireNonNull(ss.session.getLocalKeyPair()); + PublicKey remotePublicKey = + crypto.parsePublicKey(meta.getRaw(MSG_KEY_PUBLIC_KEY)); + long localTimestamp = requireNonNull(ss.session.getLocalTimestamp()); + long minTimestamp = min(localTimestamp, m.getTimestamp()); + SecretKey rootKey; + try { + rootKey = crypto.deriveRootKey(localKeyPair, remotePublicKey, + minTimestamp); + } catch (GeneralSecurityException e) { + return REJECT; // Invalid public key + } + boolean alice = isLocalPartyAlice(txn, db.getContact(txn, c)); + KeySetId keySetId = keyManager.addRotationKeys(txn, c, t, rootKey, + minTimestamp, alice, false); + MessageId previousMessageId = + requireNonNull(ss.session.getLastLocalMessageId()); + Message activateMessage = + sendActivateMessage(txn, m.getGroupId(), t, previousMessageId); + Session session = new Session(AWAIT_ACTIVATE, activateMessage.getId(), + null, null, null, keySetId); + saveSession(txn, t, ss.storageId, session); + return ACCEPT_DO_NOT_SHARE; + } + + private void startSession(Transaction txn, GroupId contactGroupId, + TransportId t) throws DbException { + KeyPair localKeyPair = crypto.generateKeyPair(); + Message keyMessage = sendKeyMessage(txn, contactGroupId, t, + localKeyPair.getPublic()); + Session session = new Session(AWAIT_KEY, keyMessage.getId(), + localKeyPair, keyMessage.getTimestamp(), null, null); + saveNewSession(txn, contactGroupId, t, session); + } + + @Nullable + private SavedSession loadSession(Transaction txn, GroupId contactGroupId, + TransportId t) throws DbException { + try { + BdfDictionary query = sessionEncoder.getSessionQuery(t); + Collection ids = + clientHelper.getMessageIds(txn, contactGroupId, query); + if (ids.size() > 1) throw new DbException(); + if (ids.isEmpty()) { + if (LOG.isLoggable(INFO)) LOG.info("No session for " + t); + return null; + } + MessageId storageId = ids.iterator().next(); + BdfDictionary bdfSession = + clientHelper.getMessageMetadataAsDictionary(txn, storageId); + Session session = sessionParser.parseSession(bdfSession); + if (LOG.isLoggable(INFO)) { + LOG.info("Loaded session in state " + session.getState() + + " for " + t); + } + return new SavedSession(session, storageId); + } catch (FormatException e) { + throw new DbException(e); + } + } + + private void saveNewSession(Transaction txn, GroupId contactGroupId, + TransportId t, Session session) throws DbException { + Message m = + clientHelper.createMessageForStoringMetadata(contactGroupId); + db.addLocalMessage(txn, m, new Metadata(), false, false); + MessageId storageId = m.getId(); + saveSession(txn, t, storageId, session); + } + + private void saveSession(Transaction txn, TransportId t, + MessageId storageId, Session session) throws DbException { + if (LOG.isLoggable(INFO)) { + LOG.info("Saving session in state " + session.getState() + + " for " + t); + } + BdfDictionary meta = sessionEncoder.encodeSession(session, t); + try { + clientHelper.mergeMessageMetadata(txn, storageId, meta); + } catch (FormatException e) { + throw new AssertionError(); + } + } + + private Message sendKeyMessage(Transaction txn, GroupId contactGroupId, + TransportId t, PublicKey publicKey) throws DbException { + Message m = messageEncoder.encodeKeyMessage(contactGroupId, t, + publicKey); + sendMessage(txn, t, m, KEY); + return m; + } + + private Message sendActivateMessage(Transaction txn, + GroupId contactGroupId, TransportId t, MessageId previousMessageId) + throws DbException { + Message m = messageEncoder.encodeActivateMessage(contactGroupId, t, + previousMessageId); + sendMessage(txn, t, m, ACTIVATE); + return m; + } + + private void sendMessage(Transaction txn, TransportId t, Message m, + MessageType type) throws DbException { + BdfDictionary meta = + messageEncoder.encodeMessageMetadata(t, type, true); + try { + clientHelper.addLocalMessage(txn, m, meta, true, false); + } catch (FormatException e) { + throw new AssertionError(); + } + } + + private Group getContactGroup(Contact c) { + return contactGroupFactory.createContactGroup(CLIENT_ID, + MAJOR_VERSION, c); + } + + private boolean isLocalPartyAlice(Transaction txn, Contact c) + throws DbException { + Author local = identityManager.getLocalAuthor(txn); + Author remote = c.getAuthor(); + return compare(local.getId().getBytes(), remote.getId().getBytes()) < 0; + } + + private static class SavedSession { + + private final Session session; + private final MessageId storageId; + + private SavedSession(Session session, MessageId storageId) { + this.session = session; + this.storageId = storageId; + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementModule.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementModule.java new file mode 100644 index 000000000..8bff228fa --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementModule.java @@ -0,0 +1,83 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.sync.validation.ValidationManager; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager; +import org.briarproject.bramble.api.versioning.ClientVersioningManager; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.CLIENT_ID; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MAJOR_VERSION; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MINOR_VERSION; + +@Module +public class TransportKeyAgreementModule { + + public static class EagerSingletons { + @Inject + TransportKeyAgreementManager transportKeyAgreementManager; + @Inject + TransportKeyAgreementValidator transportKeyAgreementValidator; + } + + @Provides + @Singleton + TransportKeyAgreementManager provideTransportKeyAgreementManager( + LifecycleManager lifecycleManager, + ValidationManager validationManager, + ContactManager contactManager, + ClientVersioningManager clientVersioningManager, + TransportKeyAgreementManagerImpl transportKeyAgreementManager) { + lifecycleManager.registerOpenDatabaseHook(transportKeyAgreementManager); + validationManager.registerIncomingMessageHook(CLIENT_ID, + MAJOR_VERSION, transportKeyAgreementManager); + contactManager.registerContactHook(transportKeyAgreementManager); + clientVersioningManager.registerClient(CLIENT_ID, MAJOR_VERSION, + MINOR_VERSION, transportKeyAgreementManager); + return transportKeyAgreementManager; + } + + @Provides + @Singleton + TransportKeyAgreementValidator provideTransportKeyAgreementValidator( + ClientHelper clientHelper, MetadataEncoder metadataEncoder, + Clock clock, MessageEncoder messageEncoder, + ValidationManager validationManager) { + TransportKeyAgreementValidator validator = + new TransportKeyAgreementValidator(clientHelper, + metadataEncoder, clock, messageEncoder); + validationManager.registerMessageValidator(CLIENT_ID, MAJOR_VERSION, + validator); + return validator; + } + + @Provides + MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) { + return messageEncoder; + } + + @Provides + SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) { + return sessionEncoder; + } + + @Provides + SessionParser provideSessionParser(SessionParserImpl sessionParser) { + return sessionParser; + } + + @Provides + TransportKeyAgreementCrypto provideTransportKeyAgreementCrypto( + TransportKeyAgreementCryptoImpl transportKeyAgreementCrypto) { + return transportKeyAgreementCrypto; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidator.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidator.java new file mode 100644 index 000000000..1f4053b2f --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementValidator.java @@ -0,0 +1,77 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.BdfMessageContext; +import org.briarproject.bramble.api.client.BdfMessageValidator; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; + +import javax.annotation.concurrent.Immutable; + +import static java.util.Collections.singletonList; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; +import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.util.ValidationUtils.checkLength; +import static org.briarproject.bramble.util.ValidationUtils.checkSize; + +@Immutable +@NotNullByDefault +class TransportKeyAgreementValidator extends BdfMessageValidator { + + private final MessageEncoder messageEncoder; + + TransportKeyAgreementValidator(ClientHelper clientHelper, + MetadataEncoder metadataEncoder, Clock clock, + MessageEncoder messageEncoder) { + super(clientHelper, metadataEncoder, clock); + this.messageEncoder = messageEncoder; + } + + @Override + protected BdfMessageContext validateMessage(Message m, Group g, + BdfList body) throws FormatException { + MessageType type = MessageType.fromValue(body.getLong(0).intValue()); + if (type == KEY) return validateKeyMessage(body); + else if (type == ACTIVATE) return validateActivateMessage(body); + else throw new AssertionError(); + } + + private BdfMessageContext validateKeyMessage(BdfList body) + throws FormatException { + // Message type, transport ID, public key + checkSize(body, 3); + String transportId = body.getString(1); + checkLength(transportId, 1, MAX_TRANSPORT_ID_LENGTH); + byte[] publicKey = body.getRaw(2); + checkLength(publicKey, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES); + BdfDictionary meta = messageEncoder.encodeMessageMetadata( + new TransportId(transportId), KEY, false); + meta.put(MSG_KEY_PUBLIC_KEY, publicKey); + return new BdfMessageContext(meta); + } + + private BdfMessageContext validateActivateMessage(BdfList body) + throws FormatException { + // Message type, transport ID, previous message ID + checkSize(body, 3); + String transportId = body.getString(1); + checkLength(transportId, 1, MAX_TRANSPORT_ID_LENGTH); + byte[] previousMessageId = body.getRaw(2); + checkLength(previousMessageId, MessageId.LENGTH); + BdfDictionary meta = messageEncoder.encodeMessageMetadata( + new TransportId(transportId), ACTIVATE, false); + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, singletonList(dependency)); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImplTest.java new file mode 100644 index 000000000..01c8dabf7 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/agreement/TransportKeyAgreementManagerImplTest.java @@ -0,0 +1,589 @@ +package org.briarproject.bramble.transport.agreement; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataParser; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.plugin.PluginConfig; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.plugin.duplex.DuplexPluginFactory; +import org.briarproject.bramble.api.plugin.simplex.SimplexPluginFactory; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.bramble.api.versioning.ClientVersioningManager; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.bramble.test.CaptureArgumentAction; +import org.jmock.Expectations; +import org.junit.Before; +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicReference; + +import static java.lang.Math.min; +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.briarproject.bramble.api.Bytes.compare; +import static org.briarproject.bramble.api.sync.Group.Visibility.VISIBLE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.ACCEPT_DO_NOT_SHARE; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.DEFER; +import static org.briarproject.bramble.api.sync.validation.IncomingMessageHook.DeliveryAction.REJECT; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.CLIENT_ID; +import static org.briarproject.bramble.api.transport.agreement.TransportKeyAgreementManager.MAJOR_VERSION; +import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey; +import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey; +import static org.briarproject.bramble.test.TestUtils.getContact; +import static org.briarproject.bramble.test.TestUtils.getGroup; +import static org.briarproject.bramble.test.TestUtils.getLocalAuthor; +import static org.briarproject.bramble.test.TestUtils.getMessage; +import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.test.TestUtils.getTransportId; +import static org.briarproject.bramble.transport.agreement.MessageType.ACTIVATE; +import static org.briarproject.bramble.transport.agreement.MessageType.KEY; +import static org.briarproject.bramble.transport.agreement.State.ACTIVATED; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_ACTIVATE; +import static org.briarproject.bramble.transport.agreement.State.AWAIT_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_PUBLIC_KEY; +import static org.briarproject.bramble.transport.agreement.TransportKeyAgreementConstants.MSG_KEY_TRANSPORT_ID; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class TransportKeyAgreementManagerImplTest extends BrambleMockTestCase { + + private final DatabaseComponent db = context.mock(DatabaseComponent.class); + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + private final MetadataParser metadataParser = + context.mock(MetadataParser.class); + private final ContactGroupFactory contactGroupFactory = + context.mock(ContactGroupFactory.class); + private final ClientVersioningManager clientVersioningManager = + context.mock(ClientVersioningManager.class); + private final IdentityManager identityManager = + context.mock(IdentityManager.class); + private final KeyManager keyManager = context.mock(KeyManager.class); + private final MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + private final SessionEncoder sessionEncoder = + context.mock(SessionEncoder.class); + private final SessionParser sessionParser = + context.mock(SessionParser.class); + private final TransportKeyAgreementCrypto crypto = + context.mock(TransportKeyAgreementCrypto.class); + private final PluginConfig pluginConfig = context.mock(PluginConfig.class); + private final SimplexPluginFactory simplexFactory = + context.mock(SimplexPluginFactory.class); + private final DuplexPluginFactory duplexFactory = + context.mock(DuplexPluginFactory.class); + + private final TransportId simplexTransportId = getTransportId(); + private final TransportId duplexTransportId = getTransportId(); + private final Group localGroup = getGroup(CLIENT_ID, MAJOR_VERSION); + private final Group contactGroup = getGroup(CLIENT_ID, MAJOR_VERSION); + private final Contact contact = getContact(); + private final LocalAuthor localAuthor = getLocalAuthor(); + private final boolean alice = compare(localAuthor.getId().getBytes(), + contact.getAuthor().getId().getBytes()) < 0; + private final KeyPair localKeyPair = + new KeyPair(getAgreementPublicKey(), getAgreementPrivateKey()); + private final PublicKey remotePublicKey = getAgreementPublicKey(); + private final SecretKey rootKey = getSecretKey(); + private final KeySetId keySetId = new KeySetId(123); + + private final Message storageMessage = getMessage(contactGroup.getId()); + private final Message localKeyMessage = getMessage(contactGroup.getId()); + private final Message localActivateMessage = + getMessage(contactGroup.getId()); + private final Message remoteKeyMessage = getMessage(contactGroup.getId()); + private final Message remoteActivateMessage = + getMessage(contactGroup.getId()); + private final long localTimestamp = localKeyMessage.getTimestamp(); + private final long remoteTimestamp = remoteKeyMessage.getTimestamp(); + + // These query and metadata dictionaries are handled by the manager without + // inspecting their contents, so we can use empty dictionaries for testing + private final BdfDictionary sessionQuery = new BdfDictionary(); + private final BdfDictionary sessionMeta = new BdfDictionary(); + private final BdfDictionary localKeyMeta = new BdfDictionary(); + private final BdfDictionary localActivateMeta = new BdfDictionary(); + + // The manager doesn't use the incoming message body, so it can be empty + private final BdfList remoteMessageBody = new BdfList(); + + private final BdfDictionary remoteKeyMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, KEY.getValue()), + new BdfEntry(MSG_KEY_TRANSPORT_ID, + simplexTransportId.getString()), + new BdfEntry(MSG_KEY_PUBLIC_KEY, remotePublicKey.getEncoded())); + + private final BdfDictionary remoteActivateMeta = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, ACTIVATE.getValue()), + new BdfEntry(MSG_KEY_TRANSPORT_ID, + simplexTransportId.getString())); + + private TransportKeyAgreementManagerImpl manager; + + @Before + public void setUp() { + context.checking(new Expectations() {{ + oneOf(pluginConfig).getSimplexFactories(); + will(returnValue(singletonList(simplexFactory))); + oneOf(simplexFactory).getId(); + will(returnValue(simplexTransportId)); + oneOf(pluginConfig).getDuplexFactories(); + will(returnValue(singletonList(duplexFactory))); + oneOf(duplexFactory).getId(); + will(returnValue(duplexTransportId)); + oneOf(contactGroupFactory) + .createLocalGroup(CLIENT_ID, MAJOR_VERSION); + will(returnValue(localGroup)); + }}); + + manager = new TransportKeyAgreementManagerImpl(db, clientHelper, + metadataParser, contactGroupFactory, clientVersioningManager, + identityManager, keyManager, messageEncoder, sessionEncoder, + sessionParser, crypto, pluginConfig); + } + + @Test + public void testCreatesContactGroupAtStartupIfLocalGroupDoesNotExist() + throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + oneOf(db).getContacts(txn); + will(returnValue(singletonList(contact))); + // The local group doesn't exist so we need to create contact groups + oneOf(db).containsGroup(txn, localGroup.getId()); + will(returnValue(false)); + oneOf(db).addGroup(txn, localGroup); + // Create the contact group and set it up + oneOf(contactGroupFactory).createContactGroup(CLIENT_ID, + MAJOR_VERSION, contact); + will(returnValue(contactGroup)); + oneOf(db).addGroup(txn, contactGroup); + oneOf(clientHelper) + .setContactId(txn, contactGroup.getId(), contact.getId()); + oneOf(clientVersioningManager).getClientVisibility(txn, + contact.getId(), CLIENT_ID, MAJOR_VERSION); + will(returnValue(VISIBLE)); + oneOf(db).setGroupVisibility(txn, contact.getId(), + contactGroup.getId(), VISIBLE); + // We already have keys for both transports + oneOf(db).getTransportsWithKeys(txn); + will(returnValue(singletonMap(contact.getId(), + asList(simplexTransportId, duplexTransportId)))); + }}); + + manager.onDatabaseOpened(txn); + } + + @Test + public void testDoesNotCreateContactGroupAtStartupIfLocalGroupExists() + throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + oneOf(db).getContacts(txn); + will(returnValue(singletonList(contact))); + // The local group exists so we don't need to create contact groups + oneOf(db).containsGroup(txn, localGroup.getId()); + will(returnValue(true)); + // We already have keys for both transports + oneOf(db).getTransportsWithKeys(txn); + will(returnValue(singletonMap(contact.getId(), + asList(simplexTransportId, duplexTransportId)))); + }}); + + manager.onDatabaseOpened(txn); + } + + @Test + public void testStartsSessionAtStartup() throws Exception { + Transaction txn = new Transaction(null, false); + AtomicReference savedSession = new AtomicReference<>(); + + context.checking(new Expectations() {{ + oneOf(db).getContacts(txn); + will(returnValue(singletonList(contact))); + // The local group exists so we don't need to create contact groups + oneOf(db).containsGroup(txn, localGroup.getId()); + will(returnValue(true)); + // We need keys for the simplex transport + oneOf(db).getTransportsWithKeys(txn); + will(returnValue(singletonMap(contact.getId(), + singletonList(duplexTransportId)))); + // Check whether a session exists - it doesn't + oneOf(contactGroupFactory) + .createContactGroup(CLIENT_ID, MAJOR_VERSION, contact); + will(returnValue(contactGroup)); + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(emptyList())); + // Send a key message + oneOf(crypto).generateKeyPair(); + will(returnValue(localKeyPair)); + oneOf(messageEncoder).encodeKeyMessage(contactGroup.getId(), + simplexTransportId, localKeyPair.getPublic()); + will(returnValue(localKeyMessage)); + oneOf(messageEncoder) + .encodeMessageMetadata(simplexTransportId, KEY, true); + will(returnValue(localKeyMeta)); + oneOf(clientHelper).addLocalMessage(txn, localKeyMessage, + localKeyMeta, true, false); + // Save the session + oneOf(clientHelper) + .createMessageForStoringMetadata(contactGroup.getId()); + will(returnValue(storageMessage)); + oneOf(db).addLocalMessage(txn, storageMessage, new Metadata(), + false, false); + oneOf(sessionEncoder).encodeSession(with(any(Session.class)), + with(simplexTransportId)); + will(doAll( + new CaptureArgumentAction<>(savedSession, Session.class, 0), + returnValue(sessionMeta))); + oneOf(clientHelper).mergeMessageMetadata(txn, + storageMessage.getId(), sessionMeta); + }}); + + manager.onDatabaseOpened(txn); + + assertEquals(AWAIT_KEY, savedSession.get().getState()); + assertEquals(localKeyMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertEquals(localKeyPair, savedSession.get().getLocalKeyPair()); + assertEquals(Long.valueOf(localTimestamp), + savedSession.get().getLocalTimestamp()); + assertNull(savedSession.get().getRemoteTimestamp()); + assertNull(savedSession.get().getKeySetId()); + } + + @Test + public void testDefersMessageIfTransportIsNotSupported() throws Exception { + Transaction txn = new Transaction(null, false); + TransportId unknownTransportId = getTransportId(); + BdfDictionary meta = new BdfDictionary(remoteKeyMeta); + meta.put(MSG_KEY_TRANSPORT_ID, unknownTransportId.getString()); + + assertEquals(DEFER, manager.incomingMessage(txn, remoteKeyMessage, + remoteMessageBody, meta)); + } + + @Test + public void testAcceptsKeyMessageInAwaitKeyState() throws Exception { + Transaction txn = new Transaction(null, false); + Session loadedSession = new Session(AWAIT_KEY, + localKeyMessage.getId(), localKeyPair, localTimestamp, + null, null); + AtomicReference savedSession = new AtomicReference<>(); + + context.checking(new Expectations() {{ + // Check whether a session exists - it does + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(singletonList(storageMessage.getId()))); + // Load the session + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + storageMessage.getId()); + will(returnValue(sessionMeta)); + oneOf(sessionParser).parseSession(sessionMeta); + will(returnValue(loadedSession)); + // Load the contact ID + oneOf(clientHelper).getContactId(txn, contactGroup.getId()); + will(returnValue(contact.getId())); + // Check whether we already have keys - we don't + oneOf(db).containsTransportKeys(txn, contact.getId(), + simplexTransportId); + will(returnValue(false)); + // Parse the remote public key + oneOf(crypto).parsePublicKey(remotePublicKey.getEncoded()); + will(returnValue(remotePublicKey)); + // Derive and store the transport keys + oneOf(crypto).deriveRootKey(localKeyPair, remotePublicKey, + min(localTimestamp, remoteTimestamp)); + will(returnValue(rootKey)); + oneOf(db).getContact(txn, contact.getId()); + will(returnValue(contact)); + oneOf(identityManager).getLocalAuthor(txn); + will(returnValue(localAuthor)); + oneOf(keyManager).addRotationKeys(txn, contact.getId(), + simplexTransportId, rootKey, + min(localTimestamp, remoteTimestamp), alice, false); + will(returnValue(keySetId)); + // Send an activate message + oneOf(messageEncoder).encodeActivateMessage(contactGroup.getId(), + simplexTransportId, localKeyMessage.getId()); + will(returnValue(localActivateMessage)); + oneOf(messageEncoder) + .encodeMessageMetadata(simplexTransportId, ACTIVATE, true); + will(returnValue(localActivateMeta)); + oneOf(clientHelper).addLocalMessage(txn, localActivateMessage, + localActivateMeta, true, false); + // Save the session + oneOf(sessionEncoder).encodeSession(with(any(Session.class)), + with(simplexTransportId)); + will(doAll( + new CaptureArgumentAction<>(savedSession, Session.class, 0), + returnValue(sessionMeta))); + oneOf(clientHelper).mergeMessageMetadata(txn, + storageMessage.getId(), sessionMeta); + }}); + + assertEquals(ACCEPT_DO_NOT_SHARE, manager.incomingMessage(txn, + remoteKeyMessage, remoteMessageBody, remoteKeyMeta)); + + assertEquals(AWAIT_ACTIVATE, savedSession.get().getState()); + assertEquals(localActivateMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertNull(savedSession.get().getLocalKeyPair()); + assertNull(savedSession.get().getLocalTimestamp()); + assertNull(savedSession.get().getRemoteTimestamp()); + assertEquals(keySetId, savedSession.get().getKeySetId()); + } + + @Test + public void testAcceptsKeyMessageIfWeHaveTransportKeysButNoSession() + throws Exception { + Transaction txn = new Transaction(null, false); + AtomicReference savedSession = new AtomicReference<>(); + + context.checking(new Expectations() {{ + // Check whether a session exists - it doesn't + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(emptyList())); + // Load the contact ID + oneOf(clientHelper).getContactId(txn, contactGroup.getId()); + will(returnValue(contact.getId())); + // Check whether we already have keys - we do + oneOf(db).containsTransportKeys(txn, contact.getId(), + simplexTransportId); + will(returnValue(true)); + // Generate the local key pair + oneOf(crypto).generateKeyPair(); + will(returnValue(localKeyPair)); + // Parse the remote public key + oneOf(crypto).parsePublicKey(remotePublicKey.getEncoded()); + will(returnValue(remotePublicKey)); + // Send a key message + oneOf(messageEncoder).encodeKeyMessage(contactGroup.getId(), + simplexTransportId, localKeyPair.getPublic()); + will(returnValue(localKeyMessage)); + oneOf(messageEncoder) + .encodeMessageMetadata(simplexTransportId, KEY, true); + will(returnValue(localKeyMeta)); + oneOf(clientHelper).addLocalMessage(txn, localKeyMessage, + localKeyMeta, true, false); + // Derive and store the transport keys + oneOf(crypto).deriveRootKey(localKeyPair, remotePublicKey, + min(localTimestamp, remoteTimestamp)); + will(returnValue(rootKey)); + oneOf(db).getContact(txn, contact.getId()); + will(returnValue(contact)); + oneOf(identityManager).getLocalAuthor(txn); + will(returnValue(localAuthor)); + oneOf(keyManager).addRotationKeys(txn, contact.getId(), + simplexTransportId, rootKey, + min(localTimestamp, remoteTimestamp), alice, false); + will(returnValue(keySetId)); + // Send an activate message + oneOf(messageEncoder).encodeActivateMessage(contactGroup.getId(), + simplexTransportId, localKeyMessage.getId()); + will(returnValue(localActivateMessage)); + oneOf(messageEncoder) + .encodeMessageMetadata(simplexTransportId, ACTIVATE, true); + will(returnValue(localActivateMeta)); + oneOf(clientHelper).addLocalMessage(txn, localActivateMessage, + localActivateMeta, true, false); + // Save the session + oneOf(clientHelper) + .createMessageForStoringMetadata(contactGroup.getId()); + will(returnValue(storageMessage)); + oneOf(db).addLocalMessage(txn, storageMessage, new Metadata(), + false, false); + oneOf(sessionEncoder).encodeSession(with(any(Session.class)), + with(simplexTransportId)); + will(doAll( + new CaptureArgumentAction<>(savedSession, Session.class, 0), + returnValue(sessionMeta))); + oneOf(clientHelper).mergeMessageMetadata(txn, + storageMessage.getId(), sessionMeta); + }}); + + assertEquals(ACCEPT_DO_NOT_SHARE, manager.incomingMessage(txn, + remoteKeyMessage, remoteMessageBody, remoteKeyMeta)); + + assertEquals(AWAIT_ACTIVATE, savedSession.get().getState()); + assertEquals(localActivateMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertNull(savedSession.get().getLocalKeyPair()); + assertNull(savedSession.get().getLocalTimestamp()); + assertNull(savedSession.get().getRemoteTimestamp()); + assertEquals(keySetId, savedSession.get().getKeySetId()); + } + + @Test + public void testRejectsKeyMessageInAwaitActivateState() throws Exception { + Session loadedSession = new Session(AWAIT_ACTIVATE, + localActivateMessage.getId(), null, null, null, keySetId); + testRejectsKeyMessageWithExistingSession(loadedSession); + } + + @Test + public void testRejectsKeyMessageInActivatedState() throws Exception { + Session loadedSession = new Session(ACTIVATED, + localActivateMessage.getId(), null, null, null, null); + testRejectsKeyMessageWithExistingSession(loadedSession); + } + + private void testRejectsKeyMessageWithExistingSession(Session loadedSession) + throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + // Check whether a session exists - it does + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(singletonList(storageMessage.getId()))); + // Load the session + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + storageMessage.getId()); + will(returnValue(sessionMeta)); + oneOf(sessionParser).parseSession(sessionMeta); + will(returnValue(loadedSession)); + // Load the contact ID + oneOf(clientHelper).getContactId(txn, contactGroup.getId()); + will(returnValue(contact.getId())); + // Check whether we already have keys - we don't + oneOf(db).containsTransportKeys(txn, contact.getId(), + simplexTransportId); + will(returnValue(false)); + }}); + + assertEquals(REJECT, manager.incomingMessage(txn, + remoteKeyMessage, remoteMessageBody, remoteKeyMeta)); + } + + @Test + public void testAcceptsActivateMessageInAwaitActivateState() + throws Exception { + Transaction txn = new Transaction(null, false); + Session loadedSession = new Session(AWAIT_ACTIVATE, + localActivateMessage.getId(), null, null, null, keySetId); + AtomicReference savedSession = new AtomicReference<>(); + + context.checking(new Expectations() {{ + // Check whether a session exists - it does + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(singletonList(storageMessage.getId()))); + // Load the session + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + storageMessage.getId()); + will(returnValue(sessionMeta)); + oneOf(sessionParser).parseSession(sessionMeta); + will(returnValue(loadedSession)); + // Activate the transport keys + oneOf(keyManager).activateKeys(txn, + singletonMap(simplexTransportId, keySetId)); + // Save the session + oneOf(sessionEncoder).encodeSession(with(any(Session.class)), + with(simplexTransportId)); + will(doAll( + new CaptureArgumentAction<>(savedSession, Session.class, 0), + returnValue(sessionMeta))); + oneOf(clientHelper).mergeMessageMetadata(txn, + storageMessage.getId(), sessionMeta); + }}); + + assertEquals(ACCEPT_DO_NOT_SHARE, manager.incomingMessage(txn, + remoteActivateMessage, remoteMessageBody, remoteActivateMeta)); + + assertEquals(ACTIVATED, savedSession.get().getState()); + assertEquals(localActivateMessage.getId(), + savedSession.get().getLastLocalMessageId()); + assertNull(savedSession.get().getLocalKeyPair()); + assertNull(savedSession.get().getLocalTimestamp()); + assertNull(savedSession.get().getRemoteTimestamp()); + assertNull(savedSession.get().getKeySetId()); + } + + @Test + public void testRejectsActivateMessageWithNoSession() throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + // Check whether a session exists - it doesn't + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(emptyList())); + }}); + + assertEquals(REJECT, manager.incomingMessage(txn, + remoteActivateMessage, remoteMessageBody, remoteActivateMeta)); + } + + @Test + public void testRejectsActivateMessageInAwaitKeyState() throws Exception { + Session loadedSession = new Session(AWAIT_KEY, + localActivateMessage.getId(), localKeyPair, localTimestamp, + null, null); + testRejectsActivateMessageWithExistingSession(loadedSession); + } + + @Test + public void testRejectsActivateMessageInActivatedState() throws Exception { + Session loadedSession = new Session(ACTIVATED, + localActivateMessage.getId(), null, null, null, null); + testRejectsActivateMessageWithExistingSession(loadedSession); + } + + private void testRejectsActivateMessageWithExistingSession( + Session loadedSession) throws Exception { + Transaction txn = new Transaction(null, false); + + context.checking(new Expectations() {{ + // Check whether a session exists - it does + oneOf(sessionEncoder).getSessionQuery(simplexTransportId); + will(returnValue(sessionQuery)); + oneOf(clientHelper) + .getMessageIds(txn, contactGroup.getId(), sessionQuery); + will(returnValue(singletonList(storageMessage.getId()))); + // Load the session + oneOf(clientHelper).getMessageMetadataAsDictionary(txn, + storageMessage.getId()); + will(returnValue(sessionMeta)); + oneOf(sessionParser).parseSession(sessionMeta); + will(returnValue(loadedSession)); + }}); + + assertEquals(REJECT, manager.incomingMessage(txn, + remoteActivateMessage, remoteMessageBody, remoteActivateMeta)); + } +}