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 new file mode 100644 index 000000000..6a9eb6eda --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeManager.java @@ -0,0 +1,27 @@ +package org.briarproject.bramble.api.contact; + +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; +import org.briarproject.bramble.api.transport.StreamWriter; + +import java.io.IOException; +import java.io.InputStream; + +@NotNullByDefault +public interface HandshakeManager { + + /** + * Handshakes and exchanges pseudonyms with the given pending contact, + * converts the pending contact to a contact and returns the contact. + * + * @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 + * @param conn The connection to use for the handshake and contact exchange + */ + Contact handshakeAndAddContact(PendingContactId p, + InputStream in, StreamWriter out, DuplexTransportConnection conn) + throws DbException, IOException; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java index 97af570b6..f4d6df9f3 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactModule.java @@ -2,6 +2,7 @@ package org.briarproject.bramble.contact; import org.briarproject.bramble.api.contact.ContactExchangeManager; import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.HandshakeManager; import javax.inject.Inject; import javax.inject.Singleton; @@ -40,4 +41,17 @@ public class ContactModule { ContactExchangeCryptoImpl contactExchangeCrypto) { return contactExchangeCrypto; } + + @Provides + @Singleton + HandshakeManager provideHandshakeManager( + HandshakeManagerImpl handshakeManager) { + return handshakeManager; + } + + @Provides + HandshakeCrypto provideHandshakeCrypto( + HandshakeCryptoImpl handshakeCrypto) { + return handshakeCrypto; + } } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeConstants.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeConstants.java new file mode 100644 index 000000000..1fba77722 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeConstants.java @@ -0,0 +1,25 @@ +package org.briarproject.bramble.contact; + +interface HandshakeConstants { + + /** + * The current version of the handshake protocol. + */ + byte PROTOCOL_VERSION = 0; + + /** + * Label for deriving the master key. + */ + String MASTER_KEY_LABEL = "org.briarproject.bramble.handshake/MASTER_KEY"; + + /** + * Label for deriving Alice's proof of ownership from the master key. + */ + String ALICE_PROOF_LABEL = "org.briarproject.bramble.handshake/ALICE_PROOF"; + + /** + * Label for deriving Bob's proof of ownership from the master key. + */ + String BOB_PROOF_LABEL = "org.briarproject.bramble.handshake/BOB_PROOF"; + +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCrypto.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCrypto.java new file mode 100644 index 000000000..88d5a6fd5 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCrypto.java @@ -0,0 +1,43 @@ +package org.briarproject.bramble.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.nullsafety.NotNullByDefault; + +import java.security.GeneralSecurityException; + +@NotNullByDefault +interface HandshakeCrypto { + + boolean isLocalPeerAlice(PublicKey theirStaticPublicKey, + KeyPair ourStaticKeyPair); + + KeyPair generateEphemeralKeyPair(); + + /** + * Derives the master key from the given static and ephemeral keys. + * + * @param alice Whether the local peer is Alice + */ + SecretKey deriveMasterKey(PublicKey theirStaticPublicKey, + PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair, + KeyPair ourEphemeralKeyPair, boolean alice) + throws GeneralSecurityException; + + /** + * Returns proof that the local peer knows the master key and therefore + * owns the static and ephemeral public keys sent by the local peer. + * + * @param alice Whether the proof is being created by Alice + */ + byte[] proveOwnership(SecretKey masterKey, boolean alice); + + /** + * Verifies the given proof that the remote peer knows the master key and + * therefore owns the static and ephemeral keys sent by the remote peer. + * + * @param alice Whether the proof was created by Alice + */ + boolean verifyOwnership(SecretKey masterKey, boolean alice, byte[] proof); +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCryptoImpl.java new file mode 100644 index 000000000..d01c6f1a2 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCryptoImpl.java @@ -0,0 +1,75 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.Bytes; +import org.briarproject.bramble.api.crypto.CryptoComponent; +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.nullsafety.NotNullByDefault; + +import java.security.GeneralSecurityException; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.contact.HandshakeConstants.ALICE_PROOF_LABEL; +import static org.briarproject.bramble.contact.HandshakeConstants.BOB_PROOF_LABEL; +import static org.briarproject.bramble.contact.HandshakeConstants.MASTER_KEY_LABEL; + +@Immutable +@NotNullByDefault +class HandshakeCryptoImpl implements HandshakeCrypto { + + private final CryptoComponent crypto; + + @Inject + HandshakeCryptoImpl(CryptoComponent crypto) { + this.crypto = crypto; + } + + @Override + public boolean isLocalPeerAlice(PublicKey theirStaticPublicKey, + KeyPair ourStaticKeyPair) { + byte[] ours = ourStaticKeyPair.getPublic().getEncoded(); + byte[] theirs = theirStaticPublicKey.getEncoded(); + return new Bytes(ours).compareTo(new Bytes(theirs)) < 0; + } + + @Override + public KeyPair generateEphemeralKeyPair() { + return crypto.generateAgreementKeyPair(); + } + + @Override + public SecretKey deriveMasterKey(PublicKey theirStaticPublicKey, + PublicKey theirEphemeralPublicKey, KeyPair ourStaticKeyPair, + KeyPair ourEphemeralKeyPair, boolean alice) throws + GeneralSecurityException { + byte[] theirStatic = theirStaticPublicKey.getEncoded(); + byte[] theirEphemeral = theirEphemeralPublicKey.getEncoded(); + byte[] ourStatic = ourStaticKeyPair.getPublic().getEncoded(); + byte[] ourEphemeral = ourEphemeralKeyPair.getPublic().getEncoded(); + byte[][] inputs = { + alice ? ourStatic : theirStatic, + alice ? theirStatic : ourStatic, + alice ? ourEphemeral : theirEphemeral, + alice ? theirEphemeral : ourEphemeral + }; + return crypto.deriveSharedSecret(MASTER_KEY_LABEL, theirStaticPublicKey, + theirEphemeralPublicKey, ourStaticKeyPair, ourEphemeralKeyPair, + alice, inputs); + } + + @Override + public byte[] proveOwnership(SecretKey masterKey, boolean alice) { + String label = alice ? ALICE_PROOF_LABEL : BOB_PROOF_LABEL; + return crypto.mac(label, masterKey); + } + + @Override + public boolean verifyOwnership(SecretKey masterKey, boolean alice, + byte[] proof) { + String label = alice ? ALICE_PROOF_LABEL : BOB_PROOF_LABEL; + return crypto.verifyMac(proof, label, masterKey); + } +} 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 new file mode 100644 index 000000000..bb4cb5dbb --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeManagerImpl.java @@ -0,0 +1,165 @@ +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.ContactExchangeManager; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.HandshakeManager; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.PendingContactId; +import org.briarproject.bramble.api.crypto.AgreementPublicKey; +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.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.TransactionManager; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; +import org.briarproject.bramble.api.record.Record; +import org.briarproject.bramble.api.record.RecordReader; +import org.briarproject.bramble.api.record.RecordReaderFactory; +import org.briarproject.bramble.api.record.RecordWriter; +import org.briarproject.bramble.api.record.RecordWriterFactory; +import org.briarproject.bramble.api.transport.StreamWriter; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.security.GeneralSecurityException; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; +import static org.briarproject.bramble.contact.HandshakeConstants.PROTOCOL_VERSION; +import static org.briarproject.bramble.contact.HandshakeRecordTypes.EPHEMERAL_PUBLIC_KEY; +import static org.briarproject.bramble.contact.HandshakeRecordTypes.PROOF_OF_OWNERSHIP; + +@Immutable +@NotNullByDefault +class HandshakeManagerImpl implements HandshakeManager { + + // Ignore records with current protocol version, unknown record type + private static final Predicate IGNORE = r -> + r.getProtocolVersion() == PROTOCOL_VERSION && + !isKnownRecordType(r.getRecordType()); + + private static boolean isKnownRecordType(byte type) { + return type == EPHEMERAL_PUBLIC_KEY || type == PROOF_OF_OWNERSHIP; + } + + private final TransactionManager db; + private final IdentityManager identityManager; + private final ContactManager contactManager; + private final HandshakeCrypto handshakeCrypto; + private final RecordReaderFactory recordReaderFactory; + private final RecordWriterFactory recordWriterFactory; + private final ContactExchangeManager contactExchangeManager; + + @Inject + HandshakeManagerImpl(DatabaseComponent db, + IdentityManager identityManager, + ContactManager contactManager, + HandshakeCrypto handshakeCrypto, + RecordReaderFactory recordReaderFactory, + RecordWriterFactory recordWriterFactory, + ContactExchangeManager contactExchangeManager) { + this.db = db; + this.identityManager = identityManager; + this.contactManager = contactManager; + this.handshakeCrypto = handshakeCrypto; + this.recordReaderFactory = recordReaderFactory; + this.recordWriterFactory = recordWriterFactory; + this.contactExchangeManager = contactExchangeManager; + } + + @Override + public Contact handshakeAndAddContact(PendingContactId p, + InputStream in, StreamWriter out, DuplexTransportConnection conn) + throws DbException, IOException { + Pair keys = db.transactionWithResult(true, txn -> { + PendingContact pendingContact = + contactManager.getPendingContact(txn, p); + KeyPair keyPair = identityManager.getHandshakeKeys(txn); + return new Pair<>(pendingContact.getPublicKey(), keyPair); + }); + PublicKey theirStaticPublicKey = keys.getFirst(); + KeyPair ourStaticKeyPair = keys.getSecond(); + boolean alice = handshakeCrypto.isLocalPeerAlice(theirStaticPublicKey, + ourStaticKeyPair); + RecordReader recordReader = recordReaderFactory.createRecordReader(in); + RecordWriter recordWriter = recordWriterFactory + .createRecordWriter(out.getOutputStream()); + KeyPair ourEphemeralKeyPair = + handshakeCrypto.generateEphemeralKeyPair(); + PublicKey theirEphemeralPublicKey; + if (alice) { + sendPublicKey(recordWriter, ourEphemeralKeyPair.getPublic()); + theirEphemeralPublicKey = receivePublicKey(recordReader); + } else { + theirEphemeralPublicKey = receivePublicKey(recordReader); + sendPublicKey(recordWriter, ourEphemeralKeyPair.getPublic()); + } + SecretKey masterKey; + try { + masterKey = handshakeCrypto.deriveMasterKey(theirStaticPublicKey, + theirEphemeralPublicKey, ourStaticKeyPair, + ourEphemeralKeyPair, alice); + } catch (GeneralSecurityException e) { + throw new FormatException(); + } + byte[] ourProof = handshakeCrypto.proveOwnership(masterKey, alice); + byte[] theirProof; + if (alice) { + sendProof(recordWriter, ourProof); + theirProof = receiveProof(recordReader); + } else { + theirProof = receiveProof(recordReader); + sendProof(recordWriter, ourProof); + } + out.sendEndOfStream(); + recordReader.readRecord(r -> false, IGNORE); + if (!handshakeCrypto.verifyOwnership(masterKey, !alice, theirProof)) + throw new FormatException(); + return contactExchangeManager.exchangeContacts(p, conn, masterKey, + alice, false); + } + + private void sendPublicKey(RecordWriter w, PublicKey k) throws IOException { + w.writeRecord(new Record(PROTOCOL_VERSION, EPHEMERAL_PUBLIC_KEY, + k.getEncoded())); + w.flush(); + } + + private PublicKey receivePublicKey(RecordReader r) throws IOException { + Record rec = readRecord(r, EPHEMERAL_PUBLIC_KEY); + int length = rec.getPayload().length; + if (length == 0 || length > MAX_AGREEMENT_PUBLIC_KEY_BYTES) + throw new FormatException(); + return new AgreementPublicKey(rec.getPayload()); + } + + private void sendProof(RecordWriter w, byte[] proof) throws IOException { + w.writeRecord(new Record(PROTOCOL_VERSION, PROOF_OF_OWNERSHIP, proof)); + w.flush(); + } + + private byte[] receiveProof(RecordReader r) throws IOException { + return readRecord(r, PROOF_OF_OWNERSHIP).getPayload(); + } + + private Record readRecord(RecordReader r, byte expectedType) + throws IOException { + // Accept records with current protocol version, expected type only + Predicate accept = rec -> + rec.getProtocolVersion() == PROTOCOL_VERSION && + rec.getRecordType() == expectedType; + Record rec = r.readRecord(accept, IGNORE); + if (rec == null) throw new EOFException(); + return rec; + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeRecordTypes.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeRecordTypes.java new file mode 100644 index 000000000..7c38d9a84 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeRecordTypes.java @@ -0,0 +1,11 @@ +package org.briarproject.bramble.contact; + +/** + * Record types for the handshake protocol. + */ +interface HandshakeRecordTypes { + + byte EPHEMERAL_PUBLIC_KEY = 0; + + byte PROOF_OF_OWNERSHIP = 1; +}