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;
/**
* Returns the contact with the given ID.
*/
Contact getContact(Transaction txn, ContactId c) throws DbException;
/**
* Returns the contact with the given remoteAuthorId
* 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;
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
* occur due to concurrent updates and does not indicate a database error.
*/
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;
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 {
interface ContactExchangeConstants {
/**
* 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";
/**
* Exchanges contact information with a remote peer.
* Label for signing key binding nonces.
*/
void startExchange(LocalAuthor localAuthor, SecretKey masterKey,
DuplexTransportConnection conn, TransportId transportId,
boolean alice);
String SIGNING_LABEL = "org.briarproject.briar.contact/EXCHANGE";
}

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

View File

@@ -137,6 +137,11 @@ class ContactManagerImpl implements ContactManager {
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
public Contact getContact(AuthorId remoteAuthorId, AuthorId localAuthorId)
throws DbException {

View File

@@ -1,6 +1,6 @@
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 javax.inject.Inject;
@@ -19,14 +19,14 @@ public class ContactModule {
@Provides
@Singleton
ContactManager getContactManager(ContactManagerImpl contactManager) {
ContactManager provideContactManager(ContactManagerImpl contactManager) {
return contactManager;
}
@Provides
ContactExchangeTask provideContactExchangeTask(
ContactExchangeTaskImpl contactExchangeTask) {
return contactExchangeTask;
ContactExchangeManager provideContactExchangeManager(
ContactExchangeManagerImpl contactExchangeManager) {
return contactExchangeManager;
}
@Provides
@@ -34,4 +34,10 @@ public class ContactModule {
PendingContactFactoryImpl pendingContactFactory) {
return pendingContactFactory;
}
@Provides
ContactExchangeCrypto provideContactExchangeCrypto(
ContactExchangeCryptoImpl contactExchangeCrypto) {
return contactExchangeCrypto;
}
}

View File

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

View File

@@ -1459,7 +1459,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
true));
fail();
} catch (ContactExistsException expected) {
// Expected
assertEquals(localAuthor.getId(), expected.getLocalAuthorId());
assertEquals(author, expected.getRemoteAuthor());
}
}
@@ -1488,7 +1489,8 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
true));
fail();
} 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.account.BriarAccountModule;
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.crypto.CryptoExecutor;
import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
@@ -128,7 +128,7 @@ public interface AndroidComponent
SettingsManager settingsManager();
ContactExchangeTask contactExchangeTask();
ContactExchangeManager contactExchangeManager();
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.StringUtils;
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.api.android.AndroidNotificationManager;
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_PUBLIC_KEY_HEX;
@Module(includes = ViewModelModule.class)
@Module(includes = {ContactExchangeModule.class, ViewModelModule.class})
public class AppModule {
static class EagerSingletons {

View File

@@ -1,46 +1,32 @@
package org.briarproject.briar.android.keyagreement;
import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.annotation.UiThread;
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.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.keyagreement.KeyAgreementResult;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import javax.inject.Inject;
import static android.widget.Toast.LENGTH_LONG;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static java.util.Objects.requireNonNull;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class ContactExchangeActivity extends KeyAgreementActivity implements
EventListener {
public class ContactExchangeActivity extends KeyAgreementActivity {
private static final Logger LOG =
Logger.getLogger(ContactExchangeActivity.class.getName());
@Inject
ViewModelProvider.Factory viewModelFactory;
// Fields that are accessed from background threads must be volatile
@Inject
volatile ContactExchangeTask contactExchangeTask;
@Inject
volatile IdentityManager identityManager;
private ContactExchangeViewModel viewModel;
@Override
public void injectActivity(ActivityComponent component) {
@@ -50,83 +36,50 @@ public class ContactExchangeActivity extends KeyAgreementActivity implements
@Override
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
getSupportActionBar().setTitle(R.string.add_contact_title);
}
@Override
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);
requireNonNull(getSupportActionBar())
.setTitle(R.string.add_contact_title);
viewModel = ViewModelProviders.of(this, viewModelFactory)
.get(ContactExchangeViewModel.class);
}
private void startContactExchange(KeyAgreementResult result) {
runOnDbThread(() -> {
LocalAuthor localAuthor;
// Load the local pseudonym
try {
localAuthor = identityManager.getLocalAuthor();
} 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());
viewModel.getSucceeded().observe(this, succeeded -> {
if (succeeded == null) return;
if (succeeded) {
Author remote = requireNonNull(viewModel.getRemoteAuthor());
contactExchangeSucceeded(remote);
} 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) {
runOnUiThreadUnlessDestroyed(() -> {
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_added_toast);
String text = String.format(format, contactName);
Toast.makeText(ContactExchangeActivity.this, text, LENGTH_LONG)
.show();
supportFinishAfterTransition();
});
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_added_toast);
String text = String.format(format, contactName);
Toast.makeText(this, text, LENGTH_LONG).show();
supportFinishAfterTransition();
}
@UiThread
private void duplicateContact(Author remoteAuthor) {
runOnUiThreadUnlessDestroyed(() -> {
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_already_exists);
String text = String.format(format, contactName);
Toast.makeText(ContactExchangeActivity.this, text, LENGTH_LONG)
.show();
finish();
});
String contactName = remoteAuthor.getName();
String format = getString(R.string.contact_already_exists);
String text = String.format(format, contactName);
Toast.makeText(this, text, LENGTH_LONG).show();
finish();
}
@UiThread
private void contactExchangeFailed() {
runOnUiThreadUnlessDestroyed(() -> {
showErrorFragment(R.string.connection_error_explanation);
});
showErrorFragment(R.string.connection_error_explanation);
}
@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})
@Retention(RetentionPolicy.RUNTIME)
@MapKey
@interface ViewModelKey {
public @interface ViewModelKey {
Class<? extends ViewModel> value();
}