diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeManager.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeManager.java index 0981158f0..dad6ae6b1 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeManager.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeManager.java @@ -4,7 +4,6 @@ import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.db.ContactExistsException; import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import java.io.IOException; @@ -13,12 +12,26 @@ import java.io.IOException; public interface ContactExchangeManager { /** - * Exchanges contact information with a remote peer. + * Exchanges contact information with a remote peer and adds the peer + * as a contact. * * @param alice Whether the local peer takes the role of Alice * @return The newly added contact * @throws ContactExistsException If the contact already exists */ - Contact exchangeContacts(TransportId t, DuplexTransportConnection conn, - SecretKey masterKey, boolean alice) throws IOException, DbException; + Contact exchangeContacts(DuplexTransportConnection conn, + SecretKey masterKey, boolean alice, boolean verified) + throws IOException, DbException; + + /** + * Exchanges contact information with a remote peer and adds the peer + * as a contact, replacing the given pending contact. + * + * @param alice Whether the local peer takes the role of Alice + * @return The newly added contact + * @throws ContactExistsException If the contact already exists + */ + Contact exchangeContacts(PendingContactId p, DuplexTransportConnection conn, + SecretKey masterKey, boolean alice, boolean verified) + throws IOException, DbException; } 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..1f586ada5 --- /dev/null +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/HandshakeManager.java @@ -0,0 +1,45 @@ +package org.briarproject.bramble.api.contact; + +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.transport.StreamWriter; + +import java.io.IOException; +import java.io.InputStream; + +@NotNullByDefault +public interface HandshakeManager { + + /** + * Handshakes with the given pending 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(PendingContactId p, InputStream in, + StreamWriter out) throws DbException, IOException; + + class HandshakeResult { + + private final SecretKey masterKey; + private final boolean alice; + + public HandshakeResult(SecretKey masterKey, boolean alice) { + this.masterKey = masterKey; + this.alice = alice; + } + + public SecretKey getMasterKey() { + return masterKey; + } + + public boolean isAlice() { + return alice; + } + } +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java index 49ed255f2..9e3bd03a7 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeManagerImpl.java @@ -7,17 +7,18 @@ import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactExchangeManager; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.PendingContactId; 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.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; +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.identity.LocalAuthor; -import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; -import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.properties.TransportProperties; @@ -36,20 +37,23 @@ import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.GeneralSecurityException; 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.util.logging.Logger.getLogger; -import static org.briarproject.bramble.api.contact.RecordTypes.CONTACT_INFO; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; import static org.briarproject.bramble.contact.ContactExchangeConstants.PROTOCOL_VERSION; +import static org.briarproject.bramble.contact.ContactExchangeRecordTypes.CONTACT_INFO; import static org.briarproject.bramble.util.ValidationUtils.checkLength; import static org.briarproject.bramble.util.ValidationUtils.checkSize; -@MethodsNotNullByDefault -@ParametersNotNullByDefault +@Immutable +@NotNullByDefault class ContactExchangeManagerImpl implements ContactExchangeManager { private static final Logger LOG = @@ -104,9 +108,22 @@ class ContactExchangeManagerImpl implements ContactExchangeManager { } @Override - public Contact exchangeContacts(TransportId t, - DuplexTransportConnection conn, SecretKey masterKey, boolean alice) - throws IOException, DbException { + public Contact exchangeContacts(DuplexTransportConnection conn, + SecretKey masterKey, boolean alice, + boolean verified) throws IOException, DbException { + return exchange(null, conn, masterKey, alice, verified); + } + + @Override + public Contact exchangeContacts(PendingContactId p, + DuplexTransportConnection conn, SecretKey masterKey, boolean alice, + boolean verified) throws IOException, DbException { + return exchange(p, conn, masterKey, alice, verified); + } + + private Contact exchange(@Nullable PendingContactId p, + DuplexTransportConnection conn, SecretKey masterKey, boolean alice, + boolean verified) throws IOException, DbException { // Get the transport connection's input and output streams InputStream in = conn.getReader().getInputStream(); OutputStream out = conn.getWriter().getOutputStream(); @@ -169,8 +186,8 @@ class ContactExchangeManagerImpl implements ContactExchangeManager { long timestamp = Math.min(localTimestamp, remoteInfo.timestamp); // Add the contact - Contact contact = addContact(remoteInfo.author, localAuthor, - masterKey, timestamp, alice, remoteInfo.properties); + Contact contact = addContact(p, remoteInfo.author, localAuthor, + masterKey, timestamp, alice, verified, remoteInfo.properties); // Contact exchange succeeded LOG.info("Contact exchange succeeded"); @@ -207,18 +224,34 @@ class ContactExchangeManagerImpl implements ContactExchangeManager { return new ContactInfo(author, properties, signature, timestamp); } - private Contact addContact(Author remoteAuthor, LocalAuthor localAuthor, - SecretKey masterKey, long timestamp, boolean alice, + private Contact addContact(@Nullable PendingContactId pendingContactId, + Author remoteAuthor, LocalAuthor localAuthor, SecretKey masterKey, + long timestamp, boolean alice, boolean verified, Map remoteProperties) - throws DbException { - return db.transactionWithResult(false, txn -> { - ContactId contactId = contactManager.addContact(txn, remoteAuthor, - localAuthor.getId(), masterKey, timestamp, alice, - true, true); + throws DbException, FormatException { + Transaction txn = db.startTransaction(false); + try { + ContactId contactId; + if (pendingContactId == null) { + contactId = contactManager.addContact(txn, remoteAuthor, + localAuthor.getId(), masterKey, timestamp, alice, + verified, true); + } else { + contactId = contactManager.addContact(txn, pendingContactId, + remoteAuthor, localAuthor.getId(), masterKey, + timestamp, alice, verified, true); + } transportPropertyManager.addRemoteProperties(txn, contactId, remoteProperties); - return contactManager.getContact(txn, contactId); - }); + Contact contact = contactManager.getContact(txn, contactId); + db.commitTransaction(txn); + return contact; + } catch (GeneralSecurityException e) { + // Pending contact's public key is invalid + throw new FormatException(); + } finally { + db.endTransaction(txn); + } } private static class ContactInfo { diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeRecordTypes.java similarity index 51% rename from bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java rename to bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeRecordTypes.java index bd24dcf67..750cd7e53 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/RecordTypes.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeRecordTypes.java @@ -1,9 +1,9 @@ -package org.briarproject.bramble.api.contact; +package org.briarproject.bramble.contact; /** * Record types for the contact exchange protocol. */ -public interface RecordTypes { +interface ContactExchangeRecordTypes { byte CONTACT_INFO = 0; } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java index 56a0694b0..821cfdc26 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java @@ -33,23 +33,18 @@ import javax.annotation.Nullable; import javax.annotation.concurrent.ThreadSafe; import javax.inject.Inject; -import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES; import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED; -import static org.briarproject.bramble.util.StringUtils.getRandomBase32String; import static org.briarproject.bramble.util.StringUtils.toUtf8; @ThreadSafe @NotNullByDefault class ContactManagerImpl implements ContactManager { - private static final String REMOTE_CONTACT_LINK = - "briar://" + getRandomBase32String(BASE32_LINK_BYTES); - private final DatabaseComponent db; private final KeyManager keyManager; private final IdentityManager identityManager; @@ -120,9 +115,10 @@ class ContactManagerImpl implements ContactManager { } @Override - public String getHandshakeLink() { - // TODO replace with real implementation - return REMOTE_CONTACT_LINK; + public String getHandshakeLink() throws DbException { + KeyPair keyPair = db.transactionWithResult(true, + identityManager::getHandshakeKeys); + return pendingContactFactory.createHandshakeLink(keyPair.getPublic()); } @Override 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..37e7a41d3 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeConstants.java @@ -0,0 +1,31 @@ +package org.briarproject.bramble.contact; + +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES; + +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"; + + /** + * The length of the proof of ownership in bytes. + */ + int PROOF_BYTES = MAC_BYTES; +} 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..bab35e6d1 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCrypto.java @@ -0,0 +1,40 @@ +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 { + + 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..199bf16af --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeCryptoImpl.java @@ -0,0 +1,66 @@ +package org.briarproject.bramble.contact; + +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 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..7edeaf786 --- /dev/null +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/HandshakeManagerImpl.java @@ -0,0 +1,163 @@ +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.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.crypto.TransportCrypto; +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.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.PROOF_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; +import static org.briarproject.bramble.util.ValidationUtils.checkLength; + +@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 TransportCrypto transportCrypto; + private final HandshakeCrypto handshakeCrypto; + private final RecordReaderFactory recordReaderFactory; + private final RecordWriterFactory recordWriterFactory; + + @Inject + HandshakeManagerImpl(DatabaseComponent db, + IdentityManager identityManager, + ContactManager contactManager, + TransportCrypto transportCrypto, + HandshakeCrypto handshakeCrypto, + RecordReaderFactory recordReaderFactory, + RecordWriterFactory recordWriterFactory) { + this.db = db; + this.identityManager = identityManager; + this.contactManager = contactManager; + this.transportCrypto = transportCrypto; + this.handshakeCrypto = handshakeCrypto; + this.recordReaderFactory = recordReaderFactory; + this.recordWriterFactory = recordWriterFactory; + } + + @Override + public HandshakeResult handshake(PendingContactId p, InputStream in, + StreamWriter out) 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 = transportCrypto.isAlice(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 new HandshakeResult(masterKey, alice); + } + + 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 { + byte[] key = readRecord(r, EPHEMERAL_PUBLIC_KEY).getPayload(); + checkLength(key, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES); + return new AgreementPublicKey(key); + } + + 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 { + byte[] proof = readRecord(r, PROOF_OF_OWNERSHIP).getPayload(); + checkLength(proof, PROOF_BYTES, PROOF_BYTES); + return proof; + } + + 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; +} diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java index cfa555565..a5e0bc675 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactory.java @@ -3,6 +3,7 @@ package org.briarproject.bramble.contact; import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.UnsupportedVersionException; import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.crypto.PublicKey; interface PendingContactFactory { @@ -15,4 +16,9 @@ interface PendingContactFactory { */ PendingContact createPendingContact(String link, String alias) throws FormatException; + + /** + * Creates a handshake link from the given public key. + */ + String createHandshakeLink(PublicKey k); } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java index ae9d1c0cb..68dd24cca 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/PendingContactFactoryImpl.java @@ -20,6 +20,7 @@ import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.FORMAT import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.ID_LABEL; import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.LINK_REGEX; import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.RAW_LINK_BYTES; +import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT; class PendingContactFactoryImpl implements PendingContactFactory { @@ -41,18 +42,31 @@ class PendingContactFactoryImpl implements PendingContactFactory { return new PendingContact(id, publicKey, alias, timestamp); } + @Override + public String createHandshakeLink(PublicKey k) { + if (!k.getKeyType().equals(KEY_TYPE_AGREEMENT)) + throw new IllegalArgumentException(); + byte[] encoded = k.getEncoded(); + if (encoded.length != RAW_LINK_BYTES - 1) + throw new IllegalArgumentException(); + byte[] raw = new byte[RAW_LINK_BYTES]; + raw[0] = FORMAT_VERSION; + arraycopy(encoded, 0, raw, 1, encoded.length); + return "briar://" + Base32.encode(raw).toLowerCase(); + } + private PublicKey parseHandshakeLink(String link) throws FormatException { Matcher matcher = LINK_REGEX.matcher(link); if (!matcher.find()) throw new FormatException(); // Discard 'briar://' and anything before or after the link link = matcher.group(2); - byte[] base32 = Base32.decode(link, false); - if (base32.length != RAW_LINK_BYTES) throw new AssertionError(); - byte version = base32[0]; + byte[] raw = Base32.decode(link, false); + if (raw.length != RAW_LINK_BYTES) throw new AssertionError(); + byte version = raw[0]; if (version != FORMAT_VERSION) throw new UnsupportedVersionException(version < FORMAT_VERSION); - byte[] publicKeyBytes = new byte[base32.length - 1]; - arraycopy(base32, 1, publicKeyBytes, 0, publicKeyBytes.length); + byte[] publicKeyBytes = new byte[raw.length - 1]; + arraycopy(raw, 1, publicKeyBytes, 0, publicKeyBytes.length); try { KeyParser parser = crypto.getAgreementKeyParser(); return parser.parsePublicKey(publicKeyBytes); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java new file mode 100644 index 000000000..8b461c669 --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java @@ -0,0 +1,298 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.api.Pair; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.contact.HandshakeManager.HandshakeResult; +import org.briarproject.bramble.api.contact.PendingContact; +import org.briarproject.bramble.api.contact.PendingContactState; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.identity.Identity; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.bramble.test.TestDatabaseConfigModule; +import org.briarproject.bramble.test.TestDuplexTransportConnection; +import org.briarproject.bramble.test.TestStreamWriter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.io.File; +import java.io.OutputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Collection; +import java.util.Random; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static junit.framework.TestCase.assertNotNull; +import static junit.framework.TestCase.assertNull; +import static junit.framework.TestCase.fail; +import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION; +import static org.briarproject.bramble.test.TestDuplexTransportConnection.createPair; +import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory; +import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.test.TestUtils.getTestDirectory; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class ContactExchangeIntegrationTest extends BrambleTestCase { + + private static final int TIMEOUT = 15_000; + + private final File testDir = getTestDirectory(); + private final File aliceDir = new File(testDir, "alice"); + private final File bobDir = new File(testDir, "bob"); + private final SecretKey masterKey = getSecretKey(); + private final Random random = new Random(); + + private ContactExchangeIntegrationTestComponent alice, bob; + private Identity aliceIdentity, bobIdentity; + + @Before + public void setUp() throws Exception { + assertTrue(testDir.mkdirs()); + // Create the devices + alice = DaggerContactExchangeIntegrationTestComponent.builder() + .testDatabaseConfigModule( + new TestDatabaseConfigModule(aliceDir)).build(); + alice.injectBrambleCoreEagerSingletons(); + bob = DaggerContactExchangeIntegrationTestComponent.builder() + .testDatabaseConfigModule(new TestDatabaseConfigModule(bobDir)) + .build(); + bob.injectBrambleCoreEagerSingletons(); + // Set up the devices and get the identities + aliceIdentity = setUp(alice, "Alice"); + bobIdentity = setUp(bob, "Bob"); + } + + private Identity setUp(ContactExchangeIntegrationTestComponent device, + String name) throws Exception { + // Add an identity for the user + IdentityManager identityManager = device.getIdentityManager(); + Identity identity = identityManager.createIdentity(name); + identityManager.registerIdentity(identity); + // Start the lifecycle manager + LifecycleManager lifecycleManager = device.getLifecycleManager(); + lifecycleManager.startServices(getSecretKey()); + lifecycleManager.waitForStartup(); + // Check the initial conditions + ContactManager contactManager = device.getContactManager(); + assertEquals(0, contactManager.getPendingContacts().size()); + assertEquals(0, contactManager.getContacts().size()); + return identity; + } + + @Test + public void testExchangeContacts() throws Exception { + TestDuplexTransportConnection[] pair = createPair(); + TestDuplexTransportConnection aliceConnection = pair[0]; + TestDuplexTransportConnection bobConnection = pair[1]; + CountDownLatch aliceFinished = new CountDownLatch(1); + CountDownLatch bobFinished = new CountDownLatch(1); + boolean verified = random.nextBoolean(); + + alice.getIoExecutor().execute(() -> { + try { + alice.getContactExchangeManager().exchangeContacts( + aliceConnection, masterKey, true, verified); + aliceFinished.countDown(); + } catch (Exception e) { + fail(); + } + }); + bob.getIoExecutor().execute(() -> { + try { + bob.getContactExchangeManager().exchangeContacts(bobConnection, + masterKey, false, verified); + bobFinished.countDown(); + } catch (Exception e) { + fail(); + } + }); + aliceFinished.await(TIMEOUT, MILLISECONDS); + bobFinished.await(TIMEOUT, MILLISECONDS); + assertContacts(verified, false); + assertNoPendingContacts(); + } + + @Test + public void testExchangeContactsFromPendingContacts() throws Exception { + PendingContact bobFromAlice = addPendingContact(alice, bob); + PendingContact aliceFromBob = addPendingContact(bob, alice); + assertPendingContacts(); + + TestDuplexTransportConnection[] pair = createPair(); + TestDuplexTransportConnection aliceConnection = pair[0]; + TestDuplexTransportConnection bobConnection = pair[1]; + CountDownLatch aliceFinished = new CountDownLatch(1); + CountDownLatch bobFinished = new CountDownLatch(1); + boolean verified = random.nextBoolean(); + + alice.getIoExecutor().execute(() -> { + try { + alice.getContactExchangeManager().exchangeContacts( + bobFromAlice.getId(), aliceConnection, masterKey, true, + verified); + aliceFinished.countDown(); + } catch (Exception e) { + fail(); + } + }); + bob.getIoExecutor().execute(() -> { + try { + bob.getContactExchangeManager().exchangeContacts( + aliceFromBob.getId(), bobConnection, masterKey, false, + verified); + bobFinished.countDown(); + } catch (Exception e) { + fail(); + } + }); + aliceFinished.await(TIMEOUT, MILLISECONDS); + bobFinished.await(TIMEOUT, MILLISECONDS); + assertContacts(verified, true); + assertNoPendingContacts(); + } + + @Test + public void testHandshakeAndExchangeContactsFromPendingContacts() + throws Exception { + PendingContact bobFromAlice = addPendingContact(alice, bob); + PendingContact aliceFromBob = addPendingContact(bob, alice); + assertPendingContacts(); + + PipedInputStream aliceHandshakeIn = new PipedInputStream(); + PipedInputStream bobHandshakeIn = new PipedInputStream(); + OutputStream aliceHandshakeOut = new PipedOutputStream(bobHandshakeIn); + OutputStream bobHandshakeOut = new PipedOutputStream(aliceHandshakeIn); + AtomicReference aliceResult = new AtomicReference<>(); + AtomicReference bobResult = new AtomicReference<>(); + + TestDuplexTransportConnection[] pair = createPair(); + TestDuplexTransportConnection aliceConnection = pair[0]; + TestDuplexTransportConnection bobConnection = pair[1]; + CountDownLatch aliceFinished = new CountDownLatch(1); + CountDownLatch bobFinished = new CountDownLatch(1); + boolean verified = random.nextBoolean(); + + alice.getIoExecutor().execute(() -> { + try { + HandshakeResult result = alice.getHandshakeManager().handshake( + bobFromAlice.getId(), aliceHandshakeIn, + new TestStreamWriter(aliceHandshakeOut)); + aliceResult.set(result); + alice.getContactExchangeManager().exchangeContacts( + bobFromAlice.getId(), aliceConnection, + result.getMasterKey(), result.isAlice(), verified); + aliceFinished.countDown(); + } catch (Exception e) { + fail(); + } + }); + bob.getIoExecutor().execute(() -> { + try { + HandshakeResult result = bob.getHandshakeManager().handshake( + aliceFromBob.getId(), bobHandshakeIn, + new TestStreamWriter(bobHandshakeOut)); + bobResult.set(result); + bob.getContactExchangeManager().exchangeContacts( + aliceFromBob.getId(), bobConnection, + result.getMasterKey(), result.isAlice(), verified); + bobFinished.countDown(); + } catch (Exception e) { + fail(); + } + }); + aliceFinished.await(TIMEOUT, MILLISECONDS); + bobFinished.await(TIMEOUT, MILLISECONDS); + assertArrayEquals(aliceResult.get().getMasterKey().getBytes(), + bobResult.get().getMasterKey().getBytes()); + assertNotEquals(aliceResult.get().isAlice(), bobResult.get().isAlice()); + assertContacts(verified, true); + assertNoPendingContacts(); + } + + private PendingContact addPendingContact( + ContactExchangeIntegrationTestComponent local, + ContactExchangeIntegrationTestComponent remote) throws Exception { + String link = remote.getContactManager().getHandshakeLink(); + String alias = remote.getIdentityManager().getLocalAuthor().getName(); + return local.getContactManager().addPendingContact(link, alias); + } + + private void assertContacts(boolean verified, + boolean withHandshakeKeys) throws Exception { + assertContact(alice, bobIdentity, verified, withHandshakeKeys); + assertContact(bob, aliceIdentity, verified, withHandshakeKeys); + } + + private void assertContact(ContactExchangeIntegrationTestComponent local, + Identity expectedIdentity, boolean verified, + boolean withHandshakeKey) throws Exception { + Collection contacts = local.getContactManager().getContacts(); + assertEquals(1, contacts.size()); + Contact contact = contacts.iterator().next(); + assertEquals(expectedIdentity.getLocalAuthor(), contact.getAuthor()); + assertEquals(verified, contact.isVerified()); + PublicKey expectedPublicKey = expectedIdentity.getHandshakePublicKey(); + PublicKey actualPublicKey = contact.getHandshakePublicKey(); + assertNotNull(expectedPublicKey); + if (withHandshakeKey) { + assertNotNull(actualPublicKey); + assertArrayEquals(expectedPublicKey.getEncoded(), + actualPublicKey.getEncoded()); + } else { + assertNull(actualPublicKey); + } + } + + private void assertNoPendingContacts() throws Exception { + assertEquals(0, alice.getContactManager().getPendingContacts().size()); + assertEquals(0, bob.getContactManager().getPendingContacts().size()); + } + + private void assertPendingContacts() throws Exception { + assertPendingContact(alice, bobIdentity); + assertPendingContact(bob, aliceIdentity); + } + + private void assertPendingContact( + ContactExchangeIntegrationTestComponent local, + Identity expectedIdentity) throws Exception { + Collection> pairs = + local.getContactManager().getPendingContacts(); + assertEquals(1, pairs.size()); + Pair pair = + pairs.iterator().next(); + assertEquals(WAITING_FOR_CONNECTION, pair.getSecond()); + PendingContact pendingContact = pair.getFirst(); + assertEquals(expectedIdentity.getLocalAuthor().getName(), + pendingContact.getAlias()); + PublicKey expectedPublicKey = expectedIdentity.getHandshakePublicKey(); + assertNotNull(expectedPublicKey); + assertArrayEquals(expectedPublicKey.getEncoded(), + pendingContact.getPublicKey().getEncoded()); + } + + private void tearDown(ContactExchangeIntegrationTestComponent device) + throws Exception { + // Stop the lifecycle manager + LifecycleManager lifecycleManager = device.getLifecycleManager(); + lifecycleManager.stopServices(); + lifecycleManager.waitForShutdown(); + } + + @After + public void tearDown() throws Exception { + tearDown(alice); + tearDown(bob); + deleteTestDirectory(testDir); + } +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java new file mode 100644 index 000000000..d3d7dae2d --- /dev/null +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTestComponent.java @@ -0,0 +1,39 @@ +package org.briarproject.bramble.contact; + +import org.briarproject.bramble.BrambleCoreEagerSingletons; +import org.briarproject.bramble.BrambleCoreModule; +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.identity.IdentityManager; +import org.briarproject.bramble.api.lifecycle.IoExecutor; +import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.test.BrambleCoreIntegrationTestModule; + +import java.util.concurrent.Executor; + +import javax.inject.Singleton; + +import dagger.Component; + +@Singleton +@Component(modules = { + BrambleCoreIntegrationTestModule.class, + BrambleCoreModule.class +}) +interface ContactExchangeIntegrationTestComponent + extends BrambleCoreEagerSingletons { + + ContactExchangeManager getContactExchangeManager(); + + ContactManager getContactManager(); + + HandshakeManager getHandshakeManager(); + + IdentityManager getIdentityManager(); + + @IoExecutor + Executor getIoExecutor(); + + LifecycleManager getLifecycleManager(); +} diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java index 195de4ef3..1eab88e88 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactManagerImplTest.java @@ -3,6 +3,9 @@ package org.briarproject.bramble.contact; 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.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.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; @@ -17,7 +20,6 @@ import org.briarproject.bramble.api.transport.KeyManager; import org.briarproject.bramble.test.BrambleMockTestCase; import org.briarproject.bramble.test.DbExpectations; import org.jmock.Expectations; -import org.jmock.Mockery; import org.junit.Test; import java.util.Collection; @@ -25,16 +27,20 @@ import java.util.Random; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32_LINK_BYTES; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.OURSELVES; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNKNOWN; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.UNVERIFIED; import static org.briarproject.bramble.api.identity.AuthorInfo.Status.VERIFIED; +import static org.briarproject.bramble.test.TestUtils.getAgreementPrivateKey; +import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey; import static org.briarproject.bramble.test.TestUtils.getAuthor; import static org.briarproject.bramble.test.TestUtils.getContact; import static org.briarproject.bramble.test.TestUtils.getLocalAuthor; import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.util.StringUtils.getRandomBase32String; import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; @@ -42,7 +48,6 @@ import static org.junit.Assert.assertTrue; public class ContactManagerImplTest extends BrambleMockTestCase { - private final Mockery context = new Mockery(); private final DatabaseComponent db = context.mock(DatabaseComponent.class); private final KeyManager keyManager = context.mock(KeyManager.class); private final IdentityManager identityManager = @@ -196,7 +201,6 @@ public class ContactManagerImplTest extends BrambleMockTestCase { Transaction txn = new Transaction(null, true); context.checking(new DbExpectations() {{ - oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); oneOf(identityManager).getLocalAuthor(txn); will(returnValue(localAuthor)); oneOf(db).getContactsByAuthorId(txn, remote.getId()); @@ -258,4 +262,22 @@ public class ContactManagerImplTest extends BrambleMockTestCase { }}); } + @Test + public void testGetHandshakeLink() throws Exception { + Transaction txn = new Transaction(null, true); + PublicKey publicKey = getAgreementPublicKey(); + PrivateKey privateKey = getAgreementPrivateKey(); + KeyPair keyPair = new KeyPair(publicKey, privateKey); + String link = "briar://" + getRandomBase32String(BASE32_LINK_BYTES); + + context.checking(new DbExpectations() {{ + oneOf(db).transactionWithResult(with(true), withDbCallable(txn)); + oneOf(identityManager).getHandshakeKeys(txn); + will(returnValue(keyPair)); + oneOf(pendingContactFactory).createHandshakeLink(publicKey); + will(returnValue(link)); + }}); + + assertEquals(link, contactManager.getHandshakeLink()); + } } diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java index 39ee19324..879bd5db1 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/PendingContactFactoryImplTest.java @@ -19,8 +19,12 @@ import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.BASE32 import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.FORMAT_VERSION; import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.ID_LABEL; import static org.briarproject.bramble.api.contact.HandshakeLinkConstants.RAW_LINK_BYTES; +import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_AGREEMENT; +import static org.briarproject.bramble.api.crypto.CryptoConstants.KEY_TYPE_SIGNATURE; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; import static org.briarproject.bramble.test.TestUtils.getAgreementPublicKey; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.junit.Assert.assertArrayEquals; @@ -110,6 +114,57 @@ public class PendingContactFactoryImplTest extends BrambleMockTestCase { assertEquals(timestamp, p.getTimestamp()); } + @Test(expected = IllegalArgumentException.class) + public void testCreateHandshakeLinkRejectsInvalidKeyType() { + PublicKey invalidPublicKey = context.mock(PublicKey.class); + + context.checking(new Expectations() {{ + oneOf(invalidPublicKey).getKeyType(); + will(returnValue(KEY_TYPE_SIGNATURE)); + }}); + + pendingContactFactory.createHandshakeLink(invalidPublicKey); + } + + @Test(expected = IllegalArgumentException.class) + public void testCreateHandshakeLinkRejectsInvalidKeyLength() { + PublicKey invalidPublicKey = context.mock(PublicKey.class); + byte[] invalidPublicKeyBytes = + getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES + 1); + + context.checking(new Expectations() {{ + oneOf(invalidPublicKey).getKeyType(); + will(returnValue(KEY_TYPE_AGREEMENT)); + oneOf(invalidPublicKey).getEncoded(); + will(returnValue(invalidPublicKeyBytes)); + }}); + + pendingContactFactory.createHandshakeLink(invalidPublicKey); + } + + @Test + public void testCreateAndParseLink() throws Exception { + context.checking(new Expectations() {{ + oneOf(crypto).getAgreementKeyParser(); + will(returnValue(keyParser)); + oneOf(keyParser).parsePublicKey(publicKey.getEncoded()); + will(returnValue(publicKey)); + oneOf(crypto).hash(ID_LABEL, publicKey.getEncoded()); + will(returnValue(idBytes)); + oneOf(clock).currentTimeMillis(); + will(returnValue(timestamp)); + }}); + + String link = pendingContactFactory.createHandshakeLink(publicKey); + PendingContact p = + pendingContactFactory.createPendingContact(link, alias); + assertArrayEquals(idBytes, p.getId().getBytes()); + assertArrayEquals(publicKey.getEncoded(), + p.getPublicKey().getEncoded()); + assertEquals(alias, p.getAlias()); + assertEquals(timestamp, p.getTimestamp()); + } + private String encodeLink() { return encodeLink(FORMAT_VERSION); } diff --git a/briar-core/src/test/java/org/briarproject/briar/test/TestStreamWriter.java b/bramble-core/src/test/java/org/briarproject/bramble/test/TestStreamWriter.java similarity index 68% rename from briar-core/src/test/java/org/briarproject/briar/test/TestStreamWriter.java rename to bramble-core/src/test/java/org/briarproject/bramble/test/TestStreamWriter.java index ba95dbb9c..12d325ff5 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/TestStreamWriter.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/TestStreamWriter.java @@ -1,15 +1,15 @@ -package org.briarproject.briar.test; +package org.briarproject.bramble.test; import org.briarproject.bramble.api.transport.StreamWriter; import java.io.IOException; import java.io.OutputStream; -class TestStreamWriter implements StreamWriter { +public class TestStreamWriter implements StreamWriter { private final OutputStream out; - TestStreamWriter(OutputStream out) { + public TestStreamWriter(OutputStream out) { this.out = out; } @@ -21,5 +21,6 @@ class TestStreamWriter implements StreamWriter { @Override public void sendEndOfStream() throws IOException { out.flush(); + out.close(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java index 00a9da2a9..d51f1547b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/AddContactViewModel.java @@ -62,7 +62,7 @@ public class AddContactViewModel extends AndroidViewModel { handshakeLink.postValue(contactManager.getHandshakeLink()); } catch (DbException e) { logException(LOG, WARNING, e); - // the UI should stay disable in this case, + // the UI should stay disabled in this case, // leaving the user unable to proceed } }); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java index a7ada2cbe..7a274bdc9 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ContactExchangeViewModel.java @@ -58,8 +58,8 @@ class ContactExchangeViewModel extends AndroidViewModel { SecretKey masterKey, boolean alice) { ioExecutor.execute(() -> { try { - Contact contact = contactExchangeManager.exchangeContacts(t, - conn, masterKey, alice); + Contact contact = contactExchangeManager.exchangeContacts(conn, + masterKey, alice, true); // Reuse the connection as a transport connection connectionManager.manageOutgoingConnection(contact.getId(), t, conn);