Merge branch 'contact-exchange-refactoring' into 'master'

Contact exchange refactoring

See merge request briar/briar!1106
This commit is contained in:
Torsten Grote
2019-05-24 17:33:21 +00:00
19 changed files with 409 additions and 331 deletions

View File

@@ -0,0 +1,24 @@
package org.briarproject.bramble.api.contact;
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;
@NotNullByDefault
public interface ContactExchangeManager {
/**
* Exchanges contact information with a remote peer.
*
* @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;
}

View File

@@ -93,6 +93,11 @@ public interface ContactManager {
*/ */
Contact getContact(ContactId c) throws DbException; Contact getContact(ContactId c) throws DbException;
/**
* Returns the contact with the given ID.
*/
Contact getContact(Transaction txn, ContactId c) throws DbException;
/** /**
* Returns the contact with the given remoteAuthorId * Returns the contact with the given remoteAuthorId
* that was added by the LocalAuthor with the given localAuthorId * that was added by the LocalAuthor with the given localAuthorId

View File

@@ -1,32 +0,0 @@
package org.briarproject.bramble.api.contact.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import javax.annotation.Nullable;
@NotNullByDefault
public class ContactExchangeFailedEvent extends Event {
@Nullable
private final Author duplicateRemoteAuthor;
public ContactExchangeFailedEvent(@Nullable Author duplicateRemoteAuthor) {
this.duplicateRemoteAuthor = duplicateRemoteAuthor;
}
public ContactExchangeFailedEvent() {
this(null);
}
@Nullable
public Author getDuplicateRemoteAuthor() {
return duplicateRemoteAuthor;
}
public boolean wasDuplicateContact() {
return duplicateRemoteAuthor != null;
}
}

View File

@@ -1,20 +0,0 @@
package org.briarproject.bramble.api.contact.event;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
public class ContactExchangeSucceededEvent extends Event {
private final Author remoteAuthor;
public ContactExchangeSucceededEvent(Author remoteAuthor) {
this.remoteAuthor = remoteAuthor;
}
public Author getRemoteAuthor() {
return remoteAuthor;
}
}

View File

@@ -1,8 +1,27 @@
package org.briarproject.bramble.api.db; package org.briarproject.bramble.api.db;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.identity.AuthorId;
/** /**
* Thrown when a duplicate contact is added to the database. This exception may * Thrown when a duplicate contact is added to the database. This exception may
* occur due to concurrent updates and does not indicate a database error. * occur due to concurrent updates and does not indicate a database error.
*/ */
public class ContactExistsException extends DbException { public class ContactExistsException extends DbException {
private final AuthorId local;
private final Author remote;
public ContactExistsException(AuthorId local, Author remote) {
this.local = local;
this.remote = remote;
}
public AuthorId getLocalAuthorId() {
return local;
}
public Author getRemoteAuthor() {
return remote;
}
} }

View File

@@ -1,16 +1,6 @@
package org.briarproject.bramble.api.contact; package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.crypto.SecretKey; interface ContactExchangeConstants {
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
/**
* A task for conducting a contact information exchange with a remote peer.
*/
@NotNullByDefault
public interface ContactExchangeTask {
/** /**
* The current version of the contact exchange protocol. * The current version of the contact exchange protocol.
@@ -39,9 +29,7 @@ public interface ContactExchangeTask {
String BOB_NONCE_LABEL = "org.briarproject.bramble.contact/BOB_NONCE"; String BOB_NONCE_LABEL = "org.briarproject.bramble.contact/BOB_NONCE";
/** /**
* Exchanges contact information with a remote peer. * Label for signing key binding nonces.
*/ */
void startExchange(LocalAuthor localAuthor, SecretKey masterKey, String SIGNING_LABEL = "org.briarproject.briar.contact/EXCHANGE";
DuplexTransportConnection conn, TransportId transportId,
boolean alice);
} }

View File

@@ -0,0 +1,35 @@
package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@NotNullByDefault
interface ContactExchangeCrypto {
/**
* Derives the header key for a contact exchange stream from the master key.
*
* @param alice Whether the header key is for the stream sent by Alice
*/
SecretKey deriveHeaderKey(SecretKey masterKey, boolean alice);
/**
* Creates and returns a signature that proves ownership of a pseudonym.
*
* @param privateKey The pseudonym's signature private key
* @param alice Whether the pseudonym belongs to Alice
*/
byte[] sign(PrivateKey privateKey, SecretKey masterKey, boolean alice);
/**
* Verifies a signature that proves ownership of a pseudonym.
*
* @param publicKey The pseudonym's signature public key
* @param alice Whether the pseudonym belongs to Alice
* @return True if the signature is valid
*/
boolean verify(PublicKey publicKey, SecretKey masterKey, boolean alice,
byte[] signature);
}

View File

@@ -0,0 +1,66 @@
package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.PrivateKey;
import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import java.security.GeneralSecurityException;
import javax.inject.Inject;
import static org.briarproject.bramble.contact.ContactExchangeConstants.ALICE_KEY_LABEL;
import static org.briarproject.bramble.contact.ContactExchangeConstants.ALICE_NONCE_LABEL;
import static org.briarproject.bramble.contact.ContactExchangeConstants.BOB_KEY_LABEL;
import static org.briarproject.bramble.contact.ContactExchangeConstants.BOB_NONCE_LABEL;
import static org.briarproject.bramble.contact.ContactExchangeConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.contact.ContactExchangeConstants.SIGNING_LABEL;
@NotNullByDefault
class ContactExchangeCryptoImpl implements ContactExchangeCrypto {
private static final byte[] PROTOCOL_VERSION_BYTES =
new byte[] {PROTOCOL_VERSION};
private final CryptoComponent crypto;
@Inject
ContactExchangeCryptoImpl(CryptoComponent crypto) {
this.crypto = crypto;
}
@Override
public SecretKey deriveHeaderKey(SecretKey masterKey, boolean alice) {
String label = alice ? ALICE_KEY_LABEL : BOB_KEY_LABEL;
return crypto.deriveKey(label, masterKey, PROTOCOL_VERSION_BYTES);
}
@Override
public byte[] sign(PrivateKey privateKey, SecretKey masterKey,
boolean alice) {
byte[] nonce = deriveNonce(masterKey, alice);
try {
return crypto.sign(SIGNING_LABEL, nonce, privateKey);
} catch (GeneralSecurityException e) {
throw new AssertionError();
}
}
@Override
public boolean verify(PublicKey publicKey,
SecretKey masterKey, boolean alice, byte[] signature) {
byte[] nonce = deriveNonce(masterKey, alice);
try {
return crypto.verifySignature(signature, SIGNING_LABEL, nonce,
publicKey);
} catch (GeneralSecurityException e) {
return false;
}
}
private byte[] deriveNonce(SecretKey masterKey, boolean alice) {
String label = alice ? ALICE_NONCE_LABEL : BOB_NONCE_LABEL;
return crypto.mac(label, masterKey, PROTOCOL_VERSION_BYTES);
}
}

View File

@@ -3,24 +3,21 @@ package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.Predicate; import org.briarproject.bramble.api.Predicate;
import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactExchangeTask; 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.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.contact.event.ContactExchangeFailedEvent; import org.briarproject.bramble.api.crypto.PublicKey;
import org.briarproject.bramble.api.contact.event.ContactExchangeSucceededEvent;
import org.briarproject.bramble.api.crypto.CryptoComponent;
import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.crypto.SecretKey;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.BdfList;
import org.briarproject.bramble.api.db.ContactExistsException;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.EventBus;
import org.briarproject.bramble.api.identity.Author; 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.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.TransportId; import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection; import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportProperties;
@@ -39,28 +36,24 @@ import java.io.EOFException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static java.util.logging.Level.WARNING; import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.api.contact.RecordTypes.CONTACT_INFO; 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.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
import static org.briarproject.bramble.util.LogUtils.logException; import static org.briarproject.bramble.contact.ContactExchangeConstants.PROTOCOL_VERSION;
import static org.briarproject.bramble.util.ValidationUtils.checkLength; import static org.briarproject.bramble.util.ValidationUtils.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize; import static org.briarproject.bramble.util.ValidationUtils.checkSize;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask { class ContactExchangeManagerImpl implements ContactExchangeManager {
private static final Logger LOG = private static final Logger LOG =
Logger.getLogger(ContactExchangeTaskImpl.class.getName()); getLogger(ContactExchangeManagerImpl.class.getName());
private static final String SIGNING_LABEL_EXCHANGE =
"org.briarproject.briar.contact/EXCHANGE";
// Accept records with current protocol version, known record type // Accept records with current protocol version, known record type
private static final Predicate<Record> ACCEPT = r -> private static final Predicate<Record> ACCEPT = r ->
@@ -80,190 +73,108 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
private final ClientHelper clientHelper; private final ClientHelper clientHelper;
private final RecordReaderFactory recordReaderFactory; private final RecordReaderFactory recordReaderFactory;
private final RecordWriterFactory recordWriterFactory; private final RecordWriterFactory recordWriterFactory;
private final EventBus eventBus;
private final Clock clock; private final Clock clock;
private final ConnectionManager connectionManager;
private final ContactManager contactManager; private final ContactManager contactManager;
private final IdentityManager identityManager;
private final TransportPropertyManager transportPropertyManager; private final TransportPropertyManager transportPropertyManager;
private final CryptoComponent crypto; private final ContactExchangeCrypto contactExchangeCrypto;
private final StreamReaderFactory streamReaderFactory; private final StreamReaderFactory streamReaderFactory;
private final StreamWriterFactory streamWriterFactory; private final StreamWriterFactory streamWriterFactory;
private volatile LocalAuthor localAuthor;
private volatile DuplexTransportConnection conn;
private volatile TransportId transportId;
private volatile SecretKey masterKey;
private volatile boolean alice;
@Inject @Inject
ContactExchangeTaskImpl(DatabaseComponent db, ClientHelper clientHelper, ContactExchangeManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
RecordReaderFactory recordReaderFactory, RecordReaderFactory recordReaderFactory,
RecordWriterFactory recordWriterFactory, EventBus eventBus, RecordWriterFactory recordWriterFactory, Clock clock,
Clock clock, ConnectionManager connectionManager, ContactManager contactManager, IdentityManager identityManager,
ContactManager contactManager,
TransportPropertyManager transportPropertyManager, TransportPropertyManager transportPropertyManager,
CryptoComponent crypto, StreamReaderFactory streamReaderFactory, ContactExchangeCrypto contactExchangeCrypto,
StreamReaderFactory streamReaderFactory,
StreamWriterFactory streamWriterFactory) { StreamWriterFactory streamWriterFactory) {
this.db = db; this.db = db;
this.clientHelper = clientHelper; this.clientHelper = clientHelper;
this.recordReaderFactory = recordReaderFactory; this.recordReaderFactory = recordReaderFactory;
this.recordWriterFactory = recordWriterFactory; this.recordWriterFactory = recordWriterFactory;
this.eventBus = eventBus;
this.clock = clock; this.clock = clock;
this.connectionManager = connectionManager;
this.contactManager = contactManager; this.contactManager = contactManager;
this.identityManager = identityManager;
this.transportPropertyManager = transportPropertyManager; this.transportPropertyManager = transportPropertyManager;
this.crypto = crypto; this.contactExchangeCrypto = contactExchangeCrypto;
this.streamReaderFactory = streamReaderFactory; this.streamReaderFactory = streamReaderFactory;
this.streamWriterFactory = streamWriterFactory; this.streamWriterFactory = streamWriterFactory;
} }
@Override @Override
public void startExchange(LocalAuthor localAuthor, SecretKey masterKey, public Contact exchangeContacts(TransportId t,
DuplexTransportConnection conn, TransportId transportId, DuplexTransportConnection conn, SecretKey masterKey, boolean alice)
boolean alice) { throws IOException, DbException {
this.localAuthor = localAuthor;
this.conn = conn;
this.transportId = transportId;
this.masterKey = masterKey;
this.alice = alice;
start();
}
@Override
public void run() {
// Get the transport connection's input and output streams // Get the transport connection's input and output streams
InputStream in; InputStream in = conn.getReader().getInputStream();
OutputStream out; OutputStream out = conn.getWriter().getOutputStream();
try {
in = conn.getReader().getInputStream();
out = conn.getWriter().getOutputStream();
} catch (IOException e) {
logException(LOG, WARNING, e);
tryToClose(conn);
eventBus.broadcast(new ContactExchangeFailedEvent());
return;
}
// Get the local transport properties // Get the local author and transport properties
Map<TransportId, TransportProperties> localProperties; LocalAuthor localAuthor = identityManager.getLocalAuthor();
try { Map<TransportId, TransportProperties> localProperties =
localProperties = transportPropertyManager.getLocalProperties(); transportPropertyManager.getLocalProperties();
} catch (DbException e) {
logException(LOG, WARNING, e);
eventBus.broadcast(new ContactExchangeFailedEvent());
tryToClose(conn);
return;
}
// Derive the header keys for the transport streams // Derive the header keys for the transport streams
SecretKey aliceHeaderKey = crypto.deriveKey(ALICE_KEY_LABEL, masterKey, SecretKey localHeaderKey =
new byte[] {PROTOCOL_VERSION}); contactExchangeCrypto.deriveHeaderKey(masterKey, alice);
SecretKey bobHeaderKey = crypto.deriveKey(BOB_KEY_LABEL, masterKey, SecretKey remoteHeaderKey =
new byte[] {PROTOCOL_VERSION}); contactExchangeCrypto.deriveHeaderKey(masterKey, !alice);
// Create the readers // Create the readers
InputStream streamReader = InputStream streamReader = streamReaderFactory
streamReaderFactory.createContactExchangeStreamReader(in, .createContactExchangeStreamReader(in, remoteHeaderKey);
alice ? bobHeaderKey : aliceHeaderKey);
RecordReader recordReader = RecordReader recordReader =
recordReaderFactory.createRecordReader(streamReader); recordReaderFactory.createRecordReader(streamReader);
// Create the writers // Create the writers
StreamWriter streamWriter = StreamWriter streamWriter = streamWriterFactory
streamWriterFactory.createContactExchangeStreamWriter(out, .createContactExchangeStreamWriter(out, localHeaderKey);
alice ? aliceHeaderKey : bobHeaderKey); RecordWriter recordWriter = recordWriterFactory
RecordWriter recordWriter = .createRecordWriter(streamWriter.getOutputStream());
recordWriterFactory
.createRecordWriter(streamWriter.getOutputStream());
// Derive the nonces to be signed // Create our signature
byte[] aliceNonce = crypto.mac(ALICE_NONCE_LABEL, masterKey, byte[] localSignature = contactExchangeCrypto
new byte[] {PROTOCOL_VERSION}); .sign(localAuthor.getPrivateKey(), masterKey, alice);
byte[] bobNonce = crypto.mac(BOB_NONCE_LABEL, masterKey,
new byte[] {PROTOCOL_VERSION});
byte[] localNonce = alice ? aliceNonce : bobNonce;
byte[] remoteNonce = alice ? bobNonce : aliceNonce;
// Sign the nonce
byte[] localSignature = sign(localAuthor, localNonce);
// Exchange contact info // Exchange contact info
long localTimestamp = clock.currentTimeMillis(); long localTimestamp = clock.currentTimeMillis();
ContactInfo remoteInfo; ContactInfo remoteInfo;
try { if (alice) {
if (alice) { sendContactInfo(recordWriter, localAuthor, localProperties,
sendContactInfo(recordWriter, localAuthor, localProperties, localSignature, localTimestamp);
localSignature, localTimestamp); remoteInfo = receiveContactInfo(recordReader);
recordWriter.flush(); } else {
remoteInfo = receiveContactInfo(recordReader); remoteInfo = receiveContactInfo(recordReader);
} else { sendContactInfo(recordWriter, localAuthor, localProperties,
remoteInfo = receiveContactInfo(recordReader); localSignature, localTimestamp);
sendContactInfo(recordWriter, localAuthor, localProperties,
localSignature, localTimestamp);
recordWriter.flush();
}
// Send EOF on the outgoing stream
streamWriter.sendEndOfStream();
// Skip any remaining records from the incoming stream
recordReader.readRecord(r -> false, IGNORE);
} catch (IOException e) {
logException(LOG, WARNING, e);
eventBus.broadcast(new ContactExchangeFailedEvent());
tryToClose(conn);
return;
} }
// Send EOF on the outgoing stream
streamWriter.sendEndOfStream();
// Skip any remaining records from the incoming stream
recordReader.readRecord(r -> false, IGNORE);
// Verify the contact's signature // Verify the contact's signature
if (!verify(remoteInfo.author, remoteNonce, remoteInfo.signature)) { PublicKey remotePublicKey = remoteInfo.author.getPublicKey();
if (!contactExchangeCrypto.verify(remotePublicKey,
masterKey, !alice, remoteInfo.signature)) {
LOG.warning("Invalid signature"); LOG.warning("Invalid signature");
eventBus.broadcast(new ContactExchangeFailedEvent()); throw new FormatException();
tryToClose(conn);
return;
} }
// The agreed timestamp is the minimum of the peers' timestamps // The agreed timestamp is the minimum of the peers' timestamps
long timestamp = Math.min(localTimestamp, remoteInfo.timestamp); long timestamp = Math.min(localTimestamp, remoteInfo.timestamp);
try { // Add the contact
// Add the contact Contact contact = addContact(remoteInfo.author, localAuthor,
ContactId contactId = addContact(remoteInfo.author, timestamp, masterKey, timestamp, alice, remoteInfo.properties);
remoteInfo.properties);
// Reuse the connection as a transport connection
connectionManager.manageOutgoingConnection(contactId, transportId,
conn);
// Pseudonym exchange succeeded
LOG.info("Pseudonym exchange succeeded");
eventBus.broadcast(
new ContactExchangeSucceededEvent(remoteInfo.author));
} catch (ContactExistsException e) {
logException(LOG, WARNING, e);
tryToClose(conn);
eventBus.broadcast(
new ContactExchangeFailedEvent(remoteInfo.author));
} catch (DbException e) {
logException(LOG, WARNING, e);
tryToClose(conn);
eventBus.broadcast(new ContactExchangeFailedEvent());
}
}
private byte[] sign(LocalAuthor author, byte[] nonce) { // Contact exchange succeeded
try { LOG.info("Contact exchange succeeded");
return crypto.sign(SIGNING_LABEL_EXCHANGE, nonce, return contact;
author.getPrivateKey());
} catch (GeneralSecurityException e) {
throw new AssertionError();
}
}
private boolean verify(Author author, byte[] nonce, byte[] signature) {
try {
return crypto.verifySignature(signature, SIGNING_LABEL_EXCHANGE,
nonce, author.getPublicKey());
} catch (GeneralSecurityException e) {
return false;
}
} }
private void sendContactInfo(RecordWriter recordWriter, Author author, private void sendContactInfo(RecordWriter recordWriter, Author author,
@@ -274,6 +185,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
BdfList payload = BdfList.of(authorList, props, signature, timestamp); BdfList payload = BdfList.of(authorList, props, signature, timestamp);
recordWriter.writeRecord(new Record(PROTOCOL_VERSION, CONTACT_INFO, recordWriter.writeRecord(new Record(PROTOCOL_VERSION, CONTACT_INFO,
clientHelper.toByteArray(payload))); clientHelper.toByteArray(payload)));
recordWriter.flush();
LOG.info("Sent contact info"); LOG.info("Sent contact info");
} }
@@ -295,7 +207,8 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
return new ContactInfo(author, properties, signature, timestamp); return new ContactInfo(author, properties, signature, timestamp);
} }
private ContactId addContact(Author remoteAuthor, long timestamp, private Contact addContact(Author remoteAuthor, LocalAuthor localAuthor,
SecretKey masterKey, long timestamp, boolean alice,
Map<TransportId, TransportProperties> remoteProperties) Map<TransportId, TransportProperties> remoteProperties)
throws DbException { throws DbException {
return db.transactionWithResult(false, txn -> { return db.transactionWithResult(false, txn -> {
@@ -304,20 +217,10 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
true, true); true, true);
transportPropertyManager.addRemoteProperties(txn, contactId, transportPropertyManager.addRemoteProperties(txn, contactId,
remoteProperties); remoteProperties);
return contactId; return contactManager.getContact(txn, contactId);
}); });
} }
private void tryToClose(DuplexTransportConnection conn) {
try {
LOG.info("Closing connection");
conn.getReader().dispose(true, true);
conn.getWriter().dispose(true);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
private static class ContactInfo { private static class ContactInfo {
private final Author author; private final Author author;

View File

@@ -137,6 +137,11 @@ class ContactManagerImpl implements ContactManager {
return db.transactionWithResult(true, txn -> db.getContact(txn, c)); return db.transactionWithResult(true, txn -> db.getContact(txn, c));
} }
@Override
public Contact getContact(Transaction txn, ContactId c) throws DbException {
return db.getContact(txn, c);
}
@Override @Override
public Contact getContact(AuthorId remoteAuthorId, AuthorId localAuthorId) public Contact getContact(AuthorId remoteAuthorId, AuthorId localAuthorId)
throws DbException { throws DbException {

View File

@@ -1,6 +1,6 @@
package org.briarproject.bramble.contact; package org.briarproject.bramble.contact;
import org.briarproject.bramble.api.contact.ContactExchangeTask; import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import javax.inject.Inject; import javax.inject.Inject;
@@ -19,14 +19,14 @@ public class ContactModule {
@Provides @Provides
@Singleton @Singleton
ContactManager getContactManager(ContactManagerImpl contactManager) { ContactManager provideContactManager(ContactManagerImpl contactManager) {
return contactManager; return contactManager;
} }
@Provides @Provides
ContactExchangeTask provideContactExchangeTask( ContactExchangeManager provideContactExchangeManager(
ContactExchangeTaskImpl contactExchangeTask) { ContactExchangeManagerImpl contactExchangeManager) {
return contactExchangeTask; return contactExchangeManager;
} }
@Provides @Provides
@@ -34,4 +34,10 @@ public class ContactModule {
PendingContactFactoryImpl pendingContactFactory) { PendingContactFactoryImpl pendingContactFactory) {
return pendingContactFactory; return pendingContactFactory;
} }
@Provides
ContactExchangeCrypto provideContactExchangeCrypto(
ContactExchangeCryptoImpl contactExchangeCrypto) {
return contactExchangeCrypto;
}
} }

View File

@@ -234,16 +234,15 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
@Override @Override
public ContactId addContact(Transaction transaction, Author remote, public ContactId addContact(Transaction transaction, Author remote,
AuthorId local, boolean verified) AuthorId local, boolean verified) throws DbException {
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction); T txn = unbox(transaction);
if (!db.containsIdentity(txn, local)) if (!db.containsIdentity(txn, local))
throw new NoSuchIdentityException(); throw new NoSuchIdentityException();
if (db.containsIdentity(txn, remote.getId())) if (db.containsIdentity(txn, remote.getId()))
throw new ContactExistsException(); throw new ContactExistsException(local, remote);
if (db.containsContact(txn, remote.getId(), local)) if (db.containsContact(txn, remote.getId(), local))
throw new ContactExistsException(); throw new ContactExistsException(local, remote);
ContactId c = db.addContact(txn, remote, local, verified); ContactId c = db.addContact(txn, remote, local, verified);
transaction.attach(new ContactAddedEvent(c)); transaction.attach(new ContactAddedEvent(c));
return c; return c;

View File

@@ -1459,7 +1459,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
true)); true));
fail(); fail();
} catch (ContactExistsException expected) { } catch (ContactExistsException expected) {
// Expected assertEquals(localAuthor.getId(), expected.getLocalAuthorId());
assertEquals(author, expected.getRemoteAuthor());
} }
} }
@@ -1488,7 +1489,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
true)); true));
fail(); fail();
} catch (ContactExistsException expected) { } catch (ContactExistsException expected) {
// Expected assertEquals(localAuthor.getId(), expected.getLocalAuthorId());
assertEquals(author, expected.getRemoteAuthor());
} }
} }

View File

@@ -8,7 +8,7 @@ import org.briarproject.bramble.BrambleCoreEagerSingletons;
import org.briarproject.bramble.BrambleCoreModule; import org.briarproject.bramble.BrambleCoreModule;
import org.briarproject.bramble.account.BriarAccountModule; import org.briarproject.bramble.account.BriarAccountModule;
import org.briarproject.bramble.api.account.AccountManager; import org.briarproject.bramble.api.account.AccountManager;
import org.briarproject.bramble.api.contact.ContactExchangeTask; import org.briarproject.bramble.api.contact.ContactExchangeManager;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.crypto.CryptoExecutor; import org.briarproject.bramble.api.crypto.CryptoExecutor;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator; import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
@@ -128,7 +128,7 @@ public interface AndroidComponent
SettingsManager settingsManager(); SettingsManager settingsManager();
ContactExchangeTask contactExchangeTask(); ContactExchangeManager contactExchangeManager();
KeyAgreementTask keyAgreementTask(); KeyAgreementTask keyAgreementTask();

View File

@@ -33,6 +33,7 @@ import org.briarproject.bramble.plugin.tor.CircumventionProvider;
import org.briarproject.bramble.util.AndroidUtils; import org.briarproject.bramble.util.AndroidUtils;
import org.briarproject.bramble.util.StringUtils; import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.android.account.LockManagerImpl; import org.briarproject.briar.android.account.LockManagerImpl;
import org.briarproject.briar.android.keyagreement.ContactExchangeModule;
import org.briarproject.briar.android.viewmodel.ViewModelModule; import org.briarproject.briar.android.viewmodel.ViewModelModule;
import org.briarproject.briar.api.android.AndroidNotificationManager; import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.android.DozeWatchdog; import org.briarproject.briar.api.android.DozeWatchdog;
@@ -59,7 +60,7 @@ import static java.util.Collections.emptyList;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_ONION_ADDRESS;
import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX; import static org.briarproject.bramble.api.reporting.ReportingConstants.DEV_PUBLIC_KEY_HEX;
@Module(includes = ViewModelModule.class) @Module(includes = {ContactExchangeModule.class, ViewModelModule.class})
public class AppModule { public class AppModule {
static class EagerSingletons { static class EagerSingletons {

View File

@@ -1,46 +1,32 @@
package org.briarproject.briar.android.keyagreement; package org.briarproject.briar.android.keyagreement;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.UiThread; import android.support.annotation.UiThread;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.contact.ContactExchangeTask;
import org.briarproject.bramble.api.contact.event.ContactExchangeFailedEvent;
import org.briarproject.bramble.api.contact.event.ContactExchangeSucceededEvent;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.event.Event;
import org.briarproject.bramble.api.event.EventListener;
import org.briarproject.bramble.api.identity.Author; 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.keyagreement.KeyAgreementResult; import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R; import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent; import org.briarproject.briar.android.activity.ActivityComponent;
import java.util.logging.Logger;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import static android.widget.Toast.LENGTH_LONG; import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING; import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.LogUtils.logException;
@MethodsNotNullByDefault @MethodsNotNullByDefault
@ParametersNotNullByDefault @ParametersNotNullByDefault
public class ContactExchangeActivity extends KeyAgreementActivity implements public class ContactExchangeActivity extends KeyAgreementActivity {
EventListener {
private static final Logger LOG = @Inject
Logger.getLogger(ContactExchangeActivity.class.getName()); ViewModelProvider.Factory viewModelFactory;
// Fields that are accessed from background threads must be volatile private ContactExchangeViewModel viewModel;
@Inject
volatile ContactExchangeTask contactExchangeTask;
@Inject
volatile IdentityManager identityManager;
@Override @Override
public void injectActivity(ActivityComponent component) { public void injectActivity(ActivityComponent component) {
@@ -50,83 +36,50 @@ public class ContactExchangeActivity extends KeyAgreementActivity implements
@Override @Override
public void onCreate(@Nullable Bundle state) { public void onCreate(@Nullable Bundle state) {
super.onCreate(state); super.onCreate(state);
getSupportActionBar().setTitle(R.string.add_contact_title); requireNonNull(getSupportActionBar())
} .setTitle(R.string.add_contact_title);
viewModel = ViewModelProviders.of(this, viewModelFactory)
@Override .get(ContactExchangeViewModel.class);
public void onStart() {
super.onStart();
// Listen to updates from contactExchangeTask
eventBus.addListener(this);
}
@Override
protected void onStop() {
super.onStop();
// Stop listen to updates from contactExchangeTask
eventBus.addListener(this);
} }
private void startContactExchange(KeyAgreementResult result) { private void startContactExchange(KeyAgreementResult result) {
runOnDbThread(() -> { viewModel.getSucceeded().observe(this, succeeded -> {
LocalAuthor localAuthor; if (succeeded == null) return;
// Load the local pseudonym if (succeeded) {
try { Author remote = requireNonNull(viewModel.getRemoteAuthor());
localAuthor = identityManager.getLocalAuthor(); contactExchangeSucceeded(remote);
} catch (DbException e) {
logException(LOG, WARNING, e);
contactExchangeFailed();
return;
}
// Exchange contact details
contactExchangeTask.startExchange(localAuthor,
result.getMasterKey(), result.getConnection(),
result.getTransportId(), result.wasAlice());
});
}
@Override
public void eventOccurred(Event e) {
if (e instanceof ContactExchangeSucceededEvent) {
contactExchangeSucceeded(
((ContactExchangeSucceededEvent) e).getRemoteAuthor());
} else if (e instanceof ContactExchangeFailedEvent) {
ContactExchangeFailedEvent fe = (ContactExchangeFailedEvent) e;
if (fe.wasDuplicateContact()) {
duplicateContact(fe.getDuplicateRemoteAuthor());
} else { } else {
contactExchangeFailed(); Author duplicate = viewModel.getDuplicateAuthor();
if (duplicate == null) contactExchangeFailed();
else duplicateContact(duplicate);
} }
} });
viewModel.startContactExchange(result.getTransportId(),
result.getConnection(), result.getMasterKey(),
result.wasAlice());
} }
@UiThread
private void contactExchangeSucceeded(Author remoteAuthor) { private void contactExchangeSucceeded(Author remoteAuthor) {
runOnUiThreadUnlessDestroyed(() -> { String contactName = remoteAuthor.getName();
String contactName = remoteAuthor.getName(); String format = getString(R.string.contact_added_toast);
String format = getString(R.string.contact_added_toast); String text = String.format(format, contactName);
String text = String.format(format, contactName); Toast.makeText(this, text, LENGTH_LONG).show();
Toast.makeText(ContactExchangeActivity.this, text, LENGTH_LONG) supportFinishAfterTransition();
.show();
supportFinishAfterTransition();
});
} }
@UiThread
private void duplicateContact(Author remoteAuthor) { private void duplicateContact(Author remoteAuthor) {
runOnUiThreadUnlessDestroyed(() -> { String contactName = remoteAuthor.getName();
String contactName = remoteAuthor.getName(); String format = getString(R.string.contact_already_exists);
String format = getString(R.string.contact_already_exists); String text = String.format(format, contactName);
String text = String.format(format, contactName); Toast.makeText(this, text, LENGTH_LONG).show();
Toast.makeText(ContactExchangeActivity.this, text, LENGTH_LONG) finish();
.show();
finish();
});
} }
@UiThread
private void contactExchangeFailed() { private void contactExchangeFailed() {
runOnUiThreadUnlessDestroyed(() -> { showErrorFragment(R.string.connection_error_explanation);
showErrorFragment(R.string.connection_error_explanation);
});
} }
@UiThread @UiThread

View File

@@ -0,0 +1,20 @@
package org.briarproject.briar.android.keyagreement;
import android.arch.lifecycle.ViewModel;
import org.briarproject.briar.android.viewmodel.ViewModelKey;
import dagger.Binds;
import dagger.Module;
import dagger.multibindings.IntoMap;
@Module
public abstract class ContactExchangeModule {
@Binds
@IntoMap
@ViewModelKey(ContactExchangeViewModel.class)
abstract ViewModel bindContactExchangeViewModel(
ContactExchangeViewModel contactExchangeViewModel);
}

View File

@@ -0,0 +1,104 @@
package org.briarproject.briar.android.keyagreement;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.arch.lifecycle.LiveData;
import android.arch.lifecycle.MutableLiveData;
import android.support.annotation.UiThread;
import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactExchangeManager;
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.identity.Author;
import org.briarproject.bramble.api.lifecycle.IoExecutor;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.plugin.ConnectionManager;
import org.briarproject.bramble.api.plugin.TransportId;
import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
import java.io.IOException;
import java.util.concurrent.Executor;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static java.util.logging.Level.WARNING;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.bramble.util.LogUtils.logException;
@NotNullByDefault
class ContactExchangeViewModel extends AndroidViewModel {
private static final Logger LOG =
getLogger(ContactExchangeViewModel.class.getName());
private final Executor ioExecutor;
private final ContactExchangeManager contactExchangeManager;
private final ConnectionManager connectionManager;
private final MutableLiveData<Boolean> succeeded = new MutableLiveData<>();
@Nullable
private volatile Author remoteAuthor, duplicateAuthor;
@Inject
ContactExchangeViewModel(Application app, @IoExecutor Executor ioExecutor,
ContactExchangeManager contactExchangeManager,
ConnectionManager connectionManager) {
super(app);
this.ioExecutor = ioExecutor;
this.contactExchangeManager = contactExchangeManager;
this.connectionManager = connectionManager;
}
@UiThread
void startContactExchange(TransportId t, DuplexTransportConnection conn,
SecretKey masterKey, boolean alice) {
ioExecutor.execute(() -> {
try {
Contact contact = contactExchangeManager.exchangeContacts(t,
conn, masterKey, alice);
// Reuse the connection as a transport connection
connectionManager.manageOutgoingConnection(contact.getId(),
t, conn);
remoteAuthor = contact.getAuthor();
succeeded.postValue(true);
} catch (ContactExistsException e) {
tryToClose(conn);
duplicateAuthor = e.getRemoteAuthor();
succeeded.postValue(false);
} catch (DbException | IOException e) {
tryToClose(conn);
logException(LOG, WARNING, e);
succeeded.postValue(false);
}
});
}
private void tryToClose(DuplexTransportConnection conn) {
try {
conn.getReader().dispose(true, true);
conn.getWriter().dispose(true);
} catch (IOException e) {
logException(LOG, WARNING, e);
}
}
@UiThread
@Nullable
Author getRemoteAuthor() {
return remoteAuthor;
}
@UiThread
@Nullable
Author getDuplicateAuthor() {
return duplicateAuthor;
}
LiveData<Boolean> getSucceeded() {
return succeeded;
}
}

View File

@@ -14,6 +14,6 @@ import dagger.MapKey;
@Target({ElementType.METHOD}) @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
@MapKey @MapKey
@interface ViewModelKey { public @interface ViewModelKey {
Class<? extends ViewModel> value(); Class<? extends ViewModel> value();
} }