diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeManager.java index 1f586ada5..538d775c9 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeManager.java @@ -24,6 +24,19 @@ public interface HandshakeManager { HandshakeResult handshake(PendingContactId p, InputStream in, StreamWriter out) throws DbException, IOException; + /** + * Handshakes with the given contact. Returns an ephemeral master key + * authenticated with both parties' handshake key pairs and a flag + * indicating whether the local peer is Alice or Bob. + * + * @param in An incoming stream for the handshake, which must be secured in + * handshake mode + * @param out An outgoing stream for the handshake, which must be secured + * in handshake mode + */ + HandshakeResult handshake(ContactId c, InputStream in, StreamWriter out) + throws DbException, IOException; + class HandshakeResult { private final SecretKey masterKey; diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java index 50f7d8aa0..b23bbb96a 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/KeyManager.java @@ -35,6 +35,20 @@ public interface KeyManager { ContactId c, SecretKey rootKey, long timestamp, boolean alice, boolean active) throws DbException; + /** + * Derives and stores a set of rotation mode transport keys for + * communicating with the given contact over each transport and returns the + * key set IDs. + *

+ * {@link StreamContext StreamContexts} for the contact can be created + * after this method has returned. + * + * @param alice True if the local party is Alice + * @param active Whether the derived keys can be used for outgoing streams + */ + Map addRotationKeys(ContactId c, SecretKey rootKey, + long timestamp, boolean alice, boolean active) throws DbException; + /** * Informs the key manager that a new contact has been added. Derives and * stores a set of handshake mode transport keys for communicating with the diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java index 2a50033b1..08542cac7 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/ConnectionManagerImpl.java @@ -101,7 +101,7 @@ class ConnectionManagerImpl implements ConnectionManager { ioExecutor.execute(new OutgoingDuplexSyncConnection(keyManager, connectionRegistry, streamReaderFactory, streamWriterFactory, syncSessionFactory, transportPropertyManager, ioExecutor, - secureRandom, c, t, d)); + secureRandom, handshakeManager, c, t, d)); } @Override diff --git a/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingDuplexSyncConnection.java b/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingDuplexSyncConnection.java index d7f777b7b..9ffb974c6 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingDuplexSyncConnection.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/connection/OutgoingDuplexSyncConnection.java @@ -2,6 +2,7 @@ package org.briarproject.bramble.connection; import org.briarproject.bramble.api.connection.ConnectionRegistry; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.HandshakeManager; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TransportId; @@ -14,9 +15,11 @@ import org.briarproject.bramble.api.sync.SyncSessionFactory; import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.api.transport.StreamContext; import org.briarproject.bramble.api.transport.StreamReaderFactory; +import org.briarproject.bramble.api.transport.StreamWriter; import org.briarproject.bramble.api.transport.StreamWriterFactory; import java.io.IOException; +import java.io.InputStream; import java.security.SecureRandom; import java.util.concurrent.Executor; @@ -28,21 +31,31 @@ import static org.briarproject.bramble.util.LogUtils.logException; class OutgoingDuplexSyncConnection extends DuplexSyncConnection implements Runnable { + // FIXME: Exchange timestamp as part of handshake protocol? + private static final long TIMESTAMP = 1617235200; // 1 April 2021 00:00 UTC + private final SecureRandom secureRandom; + private final HandshakeManager handshakeManager; private final ContactId contactId; - OutgoingDuplexSyncConnection(KeyManager keyManager, + OutgoingDuplexSyncConnection( + KeyManager keyManager, ConnectionRegistry connectionRegistry, StreamReaderFactory streamReaderFactory, StreamWriterFactory streamWriterFactory, SyncSessionFactory syncSessionFactory, TransportPropertyManager transportPropertyManager, - Executor ioExecutor, SecureRandom secureRandom, ContactId contactId, - TransportId transportId, DuplexTransportConnection connection) { + Executor ioExecutor, + SecureRandom secureRandom, + HandshakeManager handshakeManager, + ContactId contactId, + TransportId transportId, + DuplexTransportConnection connection) { super(keyManager, connectionRegistry, streamReaderFactory, streamWriterFactory, syncSessionFactory, transportPropertyManager, ioExecutor, transportId, connection); this.secureRandom = secureRandom; + this.handshakeManager = handshakeManager; this.contactId = contactId; } @@ -56,10 +69,22 @@ class OutgoingDuplexSyncConnection extends DuplexSyncConnection return; } if (ctx.isHandshakeMode()) { - // TODO: Support handshake mode for contacts - LOG.warning("Cannot use handshake mode stream context"); - onWriteError(); - return; + if (!performHandshake(ctx)) { + LOG.warning("Handshake failed"); + return; + } + // Allocate a rotation mode stream context + ctx = allocateStreamContext(contactId, transportId); + if (ctx == null) { + LOG.warning("Could not allocate stream context"); + onWriteError(); + return; + } + if (ctx.isHandshakeMode()) { + LOG.warning("Got handshake mode context after handshaking"); + onWriteError(); + return; + } } // Start the incoming session on another thread Priority priority = generatePriority(); @@ -127,6 +152,57 @@ class OutgoingDuplexSyncConnection extends DuplexSyncConnection } } + private boolean performHandshake(StreamContext ctxOut) { + // Flush the output stream to send the outgoing stream header + StreamWriter out; + try { + out = streamWriterFactory.createStreamWriter( + writer.getOutputStream(), ctxOut); + out.getOutputStream().flush(); + } catch (IOException e) { + logException(LOG, WARNING, e); + onWriteError(); + return false; + } + // Read and recognise the tag + StreamContext ctxIn = recogniseTag(reader, transportId); + // Unrecognised tags are suspicious in this case + if (ctxIn == null) { + LOG.warning("Unrecognised tag for returning stream"); + onReadError(); + return false; + } + // Check that the stream comes from the expected contact + ContactId inContactId = ctxIn.getContactId(); + if (contactId == null) { + LOG.warning("Expected contact tag, got rendezvous tag"); + onReadError(); + return false; + } + if (!inContactId.equals(contactId)) { + LOG.warning("Wrong contact ID for returning stream"); + onReadError(); + return false; + } + // TODO: Register the connection, close it if it's redundant + // Handshake and exchange contacts + try { + InputStream in = streamReaderFactory.createStreamReader( + reader.getInputStream(), ctxIn); + HandshakeManager.HandshakeResult result = + handshakeManager.handshake(contactId, in, out); + keyManager.addRotationKeys(contactId, result.getMasterKey(), + TIMESTAMP, result.isAlice(), true); + return true; + } catch (IOException | DbException e) { + logException(LOG, WARNING, e); + onWriteError(); + return false; + } finally { + // TODO: Unregister the connection + } + } + private void onReadError() { // 'Recognised' is always true for outgoing connections onReadError(true); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeManagerImpl.java index 7edeaf786..068685ff7 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeManagerImpl.java @@ -3,6 +3,8 @@ package org.briarproject.bramble.contact; import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.Pair; import org.briarproject.bramble.api.Predicate; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.HandshakeManager; import org.briarproject.bramble.api.contact.PendingContact; @@ -88,6 +90,27 @@ class HandshakeManagerImpl implements HandshakeManager { }); PublicKey theirStaticPublicKey = keys.getFirst(); KeyPair ourStaticKeyPair = keys.getSecond(); + return handshake(theirStaticPublicKey, ourStaticKeyPair, in, out); + } + + @Override + public HandshakeResult handshake(ContactId c, InputStream in, + StreamWriter out) throws DbException, IOException { + Pair keys = db.transactionWithResult(true, txn -> { + Contact contact = contactManager.getContact(txn, c); + PublicKey handshakePublicKey = contact.getHandshakePublicKey(); + if (handshakePublicKey == null) throw new DbException(); + KeyPair keyPair = identityManager.getHandshakeKeys(txn); + return new Pair<>(handshakePublicKey, keyPair); + }); + PublicKey theirStaticPublicKey = keys.getFirst(); + KeyPair ourStaticKeyPair = keys.getSecond(); + return handshake(theirStaticPublicKey, ourStaticKeyPair, in, out); + } + + private HandshakeResult handshake(PublicKey theirStaticPublicKey, + KeyPair ourStaticKeyPair, InputStream in, StreamWriter out) + throws IOException { boolean alice = transportCrypto.isAlice(theirStaticPublicKey, ourStaticKeyPair); RecordReader recordReader = recordReaderFactory.createRecordReader(in); diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java index 58aeb7cea..d240d644f 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/KeyManagerImpl.java @@ -101,9 +101,9 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { } @Override - public Map addRotationKeys( - Transaction txn, ContactId c, SecretKey rootKey, long timestamp, - boolean alice, boolean active) throws DbException { + public Map addRotationKeys(Transaction txn, + ContactId c, SecretKey rootKey, long timestamp, boolean alice, + boolean active) throws DbException { Map ids = new HashMap<>(); for (Entry e : managers.entrySet()) { TransportId t = e.getKey(); @@ -114,6 +114,14 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { return ids; } + @Override + public Map addRotationKeys(ContactId c, + SecretKey rootKey, long timestamp, boolean alice, boolean active) + throws DbException { + return db.transactionWithResult(false, txn -> + addRotationKeys(txn, c, rootKey, timestamp, alice, active)); + } + @Override public Map addContact(Transaction txn, ContactId c, PublicKey theirPublicKey, KeyPair ourKeyPair) @@ -137,7 +145,7 @@ class KeyManagerImpl implements KeyManager, Service, EventListener { PendingContactId p, PublicKey theirPublicKey, KeyPair ourKeyPair) throws DbException, GeneralSecurityException { SecretKey staticMasterKey = transportCrypto - .deriveStaticMasterKey(theirPublicKey, ourKeyPair); + .deriveStaticMasterKey(theirPublicKey, ourKeyPair); SecretKey rootKey = transportCrypto.deriveHandshakeRootKey(staticMasterKey, true); boolean alice = transportCrypto.isAlice(theirPublicKey, ourKeyPair);