Merge branch '1232-handshake-manager' into 'master'

Implement handshake protocol

See merge request briar/briar!1118
This commit is contained in:
Torsten Grote
2019-06-04 11:49:11 +00:00
20 changed files with 894 additions and 47 deletions

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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<TransportId, TransportProperties> 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 {

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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<Record> 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<PublicKey, KeyPair> 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<Record> accept = rec ->
rec.getProtocolVersion() == PROTOCOL_VERSION &&
rec.getRecordType() == expectedType;
Record rec = r.readRecord(accept, IGNORE);
if (rec == null) throw new EOFException();
return rec;
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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<HandshakeResult> aliceResult = new AtomicReference<>();
AtomicReference<HandshakeResult> 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<Contact> 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<Pair<PendingContact, PendingContactState>> pairs =
local.getContactManager().getPendingContacts();
assertEquals(1, pairs.size());
Pair<PendingContact, PendingContactState> 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);
}
}

View File

@@ -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();
}

View File

@@ -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());
}
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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
}
});

View File

@@ -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);