Add database methods for converting a pending contact.

This commit is contained in:
akwizgran
2019-05-24 14:16:38 +01:00
parent 84060a57da
commit 677728b9ae
8 changed files with 241 additions and 10 deletions

View File

@@ -32,12 +32,23 @@ public interface ContactManager {
* derives and stores transport keys for each transport, and returns an ID * derives and stores transport keys for each transport, and returns an ID
* for the contact. * for the contact.
* *
* @param alice true if the local party is Alice * @param alice True if the local party is Alice
*/ */
ContactId addContact(Transaction txn, Author remote, AuthorId local, ContactId addContact(Transaction txn, Author remote, AuthorId local,
SecretKey rootKey, long timestamp, boolean alice, boolean verified, SecretKey rootKey, long timestamp, boolean alice, boolean verified,
boolean active) throws DbException; boolean active) throws DbException;
/**
* Stores a contact associated with the given local and remote pseudonyms,
* replacing the given pending contact, derives and stores transport keys
* for each transport, and returns an ID for the contact.
*
* @param alice True if the local party is Alice
*/
ContactId addContact(Transaction txn, PendingContactId p, Author remote,
AuthorId local, SecretKey rootKey, long timestamp, boolean alice,
boolean verified, boolean active) throws DbException;
/** /**
* Stores a contact associated with the given local and remote pseudonyms * Stores a contact associated with the given local and remote pseudonyms
* and returns an ID for the contact. * and returns an ID for the contact.
@@ -50,7 +61,7 @@ public interface ContactManager {
* derives and stores transport keys for each transport, and returns an ID * derives and stores transport keys for each transport, and returns an ID
* for the contact. * for the contact.
* *
* @param alice true if the local party is Alice * @param alice True if the local party is Alice
*/ */
ContactId addContact(Author remote, AuthorId local, SecretKey rootKey, ContactId addContact(Author remote, AuthorId local, SecretKey rootKey,
long timestamp, boolean alice, boolean verified, boolean active) long timestamp, boolean alice, boolean verified, boolean active)
@@ -132,13 +143,13 @@ public interface ContactManager {
void removeContact(Transaction txn, ContactId c) throws DbException; void removeContact(Transaction txn, ContactId c) throws DbException;
/** /**
* Sets an alias name for the contact or unsets it if alias is null. * Sets an alias for the contact or unsets it if alias is null.
*/ */
void setContactAlias(Transaction txn, ContactId c, @Nullable String alias) void setContactAlias(Transaction txn, ContactId c, @Nullable String alias)
throws DbException; throws DbException;
/** /**
* Sets an alias name for the contact or unsets it if alias is null. * Sets an alias for the contact or unsets it if alias is null.
*/ */
void setContactAlias(ContactId c, @Nullable String alias) void setContactAlias(ContactId c, @Nullable String alias)
throws DbException; throws DbException;

View File

@@ -63,6 +63,13 @@ public interface DatabaseComponent extends TransactionManager {
ContactId addContact(Transaction txn, Author remote, AuthorId local, ContactId addContact(Transaction txn, Author remote, AuthorId local,
boolean verified) throws DbException; boolean verified) throws DbException;
/**
* Stores a contact associated with the given local and remote pseudonyms,
* replacing the given pending contact, and returns an ID for the contact.
*/
ContactId addContact(Transaction txn, PendingContactId p, Author remote,
AuthorId local, boolean verified) throws DbException;
/** /**
* Stores a group. * Stores a group.
*/ */

View File

@@ -81,6 +81,19 @@ class ContactManagerImpl implements ContactManager {
return c; return c;
} }
@Override
public ContactId addContact(Transaction txn, PendingContactId p,
Author remote, AuthorId local, SecretKey rootKey, long timestamp,
boolean alice, boolean verified, boolean active)
throws DbException {
ContactId c = db.addContact(txn, p, remote, local, verified);
keyManager.addContactWithRotationKeys(txn, c, rootKey, timestamp,
alice, active);
Contact contact = db.getContact(txn, c);
for (ContactHook hook : hooks) hook.addingContact(txn, contact);
return c;
}
@Override @Override
public ContactId addContact(Transaction txn, Author remote, AuthorId local, public ContactId addContact(Transaction txn, Author remote, AuthorId local,
boolean verified) throws DbException { boolean verified) throws DbException {

View File

@@ -630,7 +630,8 @@ interface Database<T> {
/** /**
* Removes the given transport keys from the database. * Removes the given transport keys from the database.
*/ */
void removeTransportKeys(T txn, TransportId t, KeySetId k) throws DbException; void removeTransportKeys(T txn, TransportId t, KeySetId k)
throws DbException;
/** /**
* Resets the transmission count and expiry time of the given message with * Resets the transmission count and expiry time of the given message with
@@ -686,6 +687,14 @@ interface Database<T> {
void setTransportKeysActive(T txn, TransportId t, KeySetId k) void setTransportKeysActive(T txn, TransportId t, KeySetId k)
throws DbException; throws DbException;
/**
* Transfers ownership of any transport keys from the given pending contact
* to the given contact and copies the pending contact's handshake public
* key to the contact.
*/
void transferKeys(T txn, PendingContactId p, ContactId c)
throws DbException;
/** /**
* Updates the transmission count, expiry time and estimated time of arrival * Updates the transmission count, expiry time and estimated time of arrival
* of the given message with respect to the given contact, using the latency * of the given message with respect to the given contact, using the latency

View File

@@ -248,6 +248,28 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
return c; return c;
} }
@Override
public ContactId addContact(Transaction transaction, PendingContactId p,
Author remote, AuthorId local, boolean verified)
throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException();
T txn = unbox(transaction);
if (!db.containsPendingContact(txn, p))
throw new NoSuchPendingContactException();
if (!db.containsIdentity(txn, local))
throw new NoSuchIdentityException();
if (db.containsIdentity(txn, remote.getId()))
throw new ContactExistsException(local, remote);
if (db.containsContact(txn, remote.getId(), local))
throw new ContactExistsException(local, remote);
ContactId c = db.addContact(txn, remote, local, verified);
db.transferKeys(txn, p, c);
db.removePendingContact(txn, p);
transaction.attach(new ContactAddedEvent(c));
transaction.attach(new PendingContactRemovedEvent(p));
return c;
}
@Override @Override
public void addGroup(Transaction transaction, Group g) throws DbException { public void addGroup(Transaction transaction, Group g) throws DbException {
if (transaction.isReadOnly()) throw new IllegalArgumentException(); if (transaction.isReadOnly()) throw new IllegalArgumentException();

View File

@@ -3115,6 +3115,48 @@ abstract class JdbcDatabase implements Database<Connection> {
} }
} }
@Override
public void transferKeys(Connection txn, PendingContactId p, ContactId c)
throws DbException {
PreparedStatement ps = null;
ResultSet rs = null;
try {
// Transfer the handshake public key
String sql = "SELECT publicKey from pendingContacts"
+ " WHERE pendingContactId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, p.getBytes());
rs = ps.executeQuery();
if (!rs.next()) throw new DbStateException();
byte[] publicKey = rs.getBytes(1);
if (rs.next()) throw new DbStateException();
rs.close();
ps.close();
sql = "UPDATE contacts SET handshakePublicKey = ?"
+ " WHERE contactId = ?";
ps = txn.prepareStatement(sql);
ps.setBytes(1, publicKey);
ps.setInt(2, c.getInt());
int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException();
ps.close();
// Transfer the transport keys
sql = "UPDATE outgoingKeys"
+ " SET contactId = ?, pendingContactId = NULL"
+ " WHERE pendingContactId = ?";
ps = txn.prepareStatement(sql);
ps.setInt(1, c.getInt());
ps.setBytes(2, p.getBytes());
affected = ps.executeUpdate();
if (affected < 0) throw new DbStateException();
ps.close();
} catch (SQLException e) {
tryToClose(rs, LOG, WARNING);
tryToClose(ps, LOG, WARNING);
throw new DbException(e);
}
}
@Override @Override
public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m, public void updateExpiryTimeAndEta(Connection txn, ContactId c, MessageId m,
int maxLatency) throws DbException { int maxLatency) throws DbException {

View File

@@ -763,16 +763,25 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
throws Exception { throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
// Check whether the pending contact is in the DB (which it's not) // Check whether the pending contact is in the DB (which it's not)
exactly(2).of(database).startTransaction(); exactly(3).of(database).startTransaction();
will(returnValue(txn)); will(returnValue(txn));
exactly(2).of(database).containsPendingContact(txn, exactly(3).of(database).containsPendingContact(txn,
pendingContactId); pendingContactId);
will(returnValue(false)); will(returnValue(false));
exactly(2).of(database).abortTransaction(txn); exactly(3).of(database).abortTransaction(txn);
}}); }});
DatabaseComponent db = createDatabaseComponent(database, eventBus, DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager); eventExecutor, shutdownManager);
try {
db.transaction(false, transaction ->
db.addContact(transaction, pendingContactId, author,
localAuthor.getId(), true));
fail();
} catch (NoSuchPendingContactException expected) {
// Expected
}
try { try {
db.transaction(false, transaction -> db.transaction(false, transaction ->
db.addTransportKeys(transaction, pendingContactId, db.addTransportKeys(transaction, pendingContactId,
@@ -1494,6 +1503,69 @@ public class DatabaseComponentImplTest extends BrambleMockTestCase {
} }
} }
@Test
public void testCannotAddLocalIdentityAsContactFromPendingContact()
throws Exception {
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsPendingContact(txn, pendingContactId);
will(returnValue(true));
oneOf(database).containsIdentity(txn, localAuthor.getId());
will(returnValue(true));
// Contact is a local identity
oneOf(database).containsIdentity(txn, author.getId());
will(returnValue(true));
oneOf(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
try {
db.transaction(false, transaction ->
db.addContact(transaction, pendingContactId, author,
localAuthor.getId(), true));
fail();
} catch (ContactExistsException expected) {
assertEquals(localAuthor.getId(), expected.getLocalAuthorId());
assertEquals(author, expected.getRemoteAuthor());
}
}
@Test
public void testCannotAddDuplicateContactFromPendingContact()
throws Exception {
context.checking(new Expectations() {{
oneOf(database).startTransaction();
will(returnValue(txn));
oneOf(database).containsPendingContact(txn, pendingContactId);
will(returnValue(true));
oneOf(database).containsIdentity(txn, localAuthor.getId());
will(returnValue(true));
oneOf(database).containsIdentity(txn, author.getId());
will(returnValue(false));
// Contact already exists for this local identity
oneOf(database).containsContact(txn, author.getId(),
localAuthor.getId());
will(returnValue(true));
oneOf(database).abortTransaction(txn);
}});
DatabaseComponent db = createDatabaseComponent(database, eventBus,
eventExecutor, shutdownManager);
try {
db.transaction(false, transaction ->
db.addContact(transaction, pendingContactId, author,
localAuthor.getId(), true));
fail();
} catch (ContactExistsException expected) {
assertEquals(localAuthor.getId(), expected.getLocalAuthorId());
assertEquals(author, expected.getRemoteAuthor());
}
}
@Test @Test
public void testMessageDependencies() throws Exception { public void testMessageDependencies() throws Exception {
int shutdownHandle = 12345; int shutdownHandle = 12345;

View File

@@ -2204,12 +2204,13 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
assertEquals(emptyList(), db.getPendingContacts(txn)); assertEquals(emptyList(), db.getPendingContacts(txn));
db.addPendingContact(txn, pendingContact); db.addPendingContact(txn, pendingContact);
Collection<PendingContact> pendingContacts = Collection<PendingContact> pendingContacts = db.getPendingContacts(txn);
db.getPendingContacts(txn);
assertEquals(1, pendingContacts.size()); assertEquals(1, pendingContacts.size());
PendingContact retrieved = pendingContacts.iterator().next(); PendingContact retrieved = pendingContacts.iterator().next();
assertEquals(pendingContact.getId(), retrieved.getId()); assertEquals(pendingContact.getId(), retrieved.getId());
assertEquals(pendingContact.getAlias(), retrieved.getAlias()); assertEquals(pendingContact.getAlias(), retrieved.getAlias());
assertArrayEquals(pendingContact.getPublicKey().getEncoded(),
retrieved.getPublicKey().getEncoded());
assertEquals(pendingContact.getTimestamp(), retrieved.getTimestamp()); assertEquals(pendingContact.getTimestamp(), retrieved.getTimestamp());
db.removePendingContact(txn, pendingContact.getId()); db.removePendingContact(txn, pendingContact.getId());
@@ -2219,6 +2220,60 @@ public abstract class JdbcDatabaseTest extends BrambleTestCase {
db.close(); db.close();
} }
@Test
public void testTransferKeys() throws Exception {
boolean alice = random.nextBoolean();
TransportKeys transportKeys =
createHandshakeKeys(1000, getSecretKey(), alice);
Database<Connection> db = open(false);
Connection txn = db.startTransaction();
// Add the pending contact, the transport and the handshake keys
db.addPendingContact(txn, pendingContact);
db.addTransport(txn, transportId, 123);
assertEquals(keySetId, db.addTransportKeys(txn, pendingContact.getId(),
transportKeys));
Collection<TransportKeySet> allKeys =
db.getTransportKeys(txn, transportId);
assertEquals(1, allKeys.size());
TransportKeySet ks = allKeys.iterator().next();
assertEquals(keySetId, ks.getKeySetId());
assertNull(ks.getContactId());
assertEquals(pendingContact.getId(), ks.getPendingContactId());
// Add a contact
db.addIdentity(txn, identity);
assertEquals(contactId,
db.addContact(txn, author, localAuthor.getId(), true));
// The contact shouldn't have a handshake public key
Contact contact = db.getContact(txn, contactId);
assertNull(contact.getHandshakePublicKey());
// Transfer the keys to the contact
db.transferKeys(txn, pendingContact.getId(), contactId);
// The handshake public key should have been copied to the contact
contact = db.getContact(txn, contactId);
PublicKey handshakePublicKey = contact.getHandshakePublicKey();
assertNotNull(handshakePublicKey);
assertArrayEquals(pendingContact.getPublicKey().getEncoded(),
handshakePublicKey.getEncoded());
// The transport keys should have been transferred to the contact
allKeys = db.getTransportKeys(txn, transportId);
assertEquals(1, allKeys.size());
ks = allKeys.iterator().next();
assertEquals(keySetId, ks.getKeySetId());
assertEquals(contactId, ks.getContactId());
assertNull(ks.getPendingContactId());
db.commitTransaction(txn);
db.close();
}
@Test @Test
public void testSetHandshakeKeyPair() throws Exception { public void testSetHandshakeKeyPair() throws Exception {
Identity withoutKeys = new Identity(localAuthor, null, null, Identity withoutKeys = new Identity(localAuthor, null, null,