From 9345b5c71b459ce3004bd450d2e83572b42cfe6a Mon Sep 17 00:00:00 2001 From: akwizgran <akwizgran@users.sourceforge.net> Date: Thu, 24 Nov 2011 22:09:04 +0000 Subject: [PATCH] Avoid DB lookups where possible. --- .../event/RemoteTransportsUpdatedEvent.java | 12 +- .../sf/briar/db/DatabaseComponentImpl.java | 2 +- .../transport/ConnectionRecogniserImpl.java | 68 +-- .../ConnectionRecogniserImplTest.java | 530 +++++++++++++++--- 4 files changed, 489 insertions(+), 123 deletions(-) diff --git a/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java b/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java index 8418d23595..501eb886d3 100644 --- a/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java +++ b/api/net/sf/briar/api/db/event/RemoteTransportsUpdatedEvent.java @@ -1,17 +1,27 @@ package net.sf.briar.api.db.event; +import java.util.Collection; + import net.sf.briar.api.ContactId; +import net.sf.briar.api.protocol.Transport; /** An event that is broadcast when a contact's transports are updated. */ public class RemoteTransportsUpdatedEvent extends DatabaseEvent { private final ContactId contactId; + private final Collection<Transport> transports; - public RemoteTransportsUpdatedEvent(ContactId contactId) { + public RemoteTransportsUpdatedEvent(ContactId contactId, + Collection<Transport> transports) { this.contactId = contactId; + this.transports = transports; } public ContactId getContactId() { return contactId; } + + public Collection<Transport> getTransports() { + return transports; + } } diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java index 56b9f82532..03f2f8a74b 100644 --- a/components/net/sf/briar/db/DatabaseComponentImpl.java +++ b/components/net/sf/briar/db/DatabaseComponentImpl.java @@ -1226,7 +1226,7 @@ DatabaseCleaner.Callback { contactLock.readLock().unlock(); } // Call the listeners outside the lock - callListeners(new RemoteTransportsUpdatedEvent(c)); + callListeners(new RemoteTransportsUpdatedEvent(c, t.getTransports())); } public void removeContact(ContactId c) throws DbException { diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java index 6be76b664a..744333eca7 100644 --- a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java +++ b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java @@ -51,6 +51,7 @@ DatabaseListener { private final DatabaseComponent db; private final Executor executor; private final Cipher ivCipher; // Locking: this + private final Set<TransportId> localTransportIds; // Locking: this private final Map<Bytes, Context> expected; // Locking: this private boolean initialised = false; // Locking: this @@ -62,9 +63,15 @@ DatabaseListener { this.db = db; this.executor = executor; ivCipher = crypto.getIvCipher(); + localTransportIds = new HashSet<TransportId>(); expected = new HashMap<Bytes, Context>(); } + // Package access for testing + synchronized boolean isInitialised() { + return initialised; + } + // Locking: this private void initialise() throws DbException { assert !initialised; @@ -76,7 +83,7 @@ DatabaseListener { try { for(TransportId t : transports) { TransportIndex i = db.getRemoteIndex(c, t); - if(i == null) continue; + if(i == null) continue; // Contact doesn't support transport ConnectionWindow w = db.getConnectionWindow(c, i); for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) { Context ctx = new Context(c, t, i, e.getKey()); @@ -89,6 +96,7 @@ DatabaseListener { continue; } } + localTransportIds.addAll(transports); expected.putAll(ivs); initialised = true; } @@ -100,7 +108,8 @@ DatabaseListener { ErasableKey ivKey = crypto.deriveIvKey(secret, true); try { ivCipher.init(Cipher.ENCRYPT_MODE, ivKey); - return new Bytes(ivCipher.doFinal(iv)); + byte[] encryptedIv = ivCipher.doFinal(iv); + return new Bytes(encryptedIv); } catch(BadPaddingException badCipher) { throw new RuntimeException(badCipher); } catch(IllegalBlockSizeException badCipher) { @@ -127,8 +136,9 @@ DatabaseListener { }); } - private ConnectionContext acceptConnection(TransportId t, - byte[] encryptedIv) throws DbException { + // Package access for testing + ConnectionContext acceptConnection(TransportId t, byte[] encryptedIv) + throws DbException { if(encryptedIv.length != IV_LENGTH) throw new IllegalArgumentException(); synchronized(this) { @@ -189,11 +199,12 @@ DatabaseListener { }); } else if(e instanceof RemoteTransportsUpdatedEvent) { // Update the expected IVs for the contact - final ContactId c = - ((RemoteTransportsUpdatedEvent) e).getContactId(); + RemoteTransportsUpdatedEvent r = (RemoteTransportsUpdatedEvent) e; + final ContactId c = r.getContactId(); + final Collection<Transport> transports = r.getTransports(); executor.execute(new Runnable() { public void run() { - updateContact(c); + updateContact(c, transports); } }); } @@ -212,7 +223,7 @@ DatabaseListener { for(ContactId c : db.getContacts()) { try { TransportIndex i = db.getRemoteIndex(c, t); - if(i == null) continue; + if(i == null) continue; // Contact doesn't support transport ConnectionWindow w = db.getConnectionWindow(c, i); for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) { Context ctx = new Context(c, t, i, e.getKey()); @@ -228,33 +239,26 @@ DatabaseListener { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage()); return; } + localTransportIds.add(t); expected.putAll(ivs); } - private synchronized void updateContact(ContactId c) { + private synchronized void updateContact(ContactId c, + Collection<Transport> transports) { if(!initialised) return; - // Don't recalculate IVs for transports that are already known - Set<TransportIndex> known = new HashSet<TransportIndex>(); - for(Context ctx : expected.values()) { - if(ctx.contactId.equals(c)) known.add(ctx.transportIndex); - } - Set<TransportIndex> current = new HashSet<TransportIndex>(); + // The ID <-> index mappings may have changed, so recalculate everything Map<Bytes, Context> ivs = new HashMap<Bytes, Context>(); try { - for(Transport transport : db.getLocalTransports()) { + for(Transport transport: transports) { TransportId t = transport.getId(); - TransportIndex i = db.getRemoteIndex(c, t); - if(i == null) continue; - current.add(i); - // If the transport is not already known, calculate the IVs - if(!known.contains(i)) { - ConnectionWindow w = db.getConnectionWindow(c, i); - for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) { - Context ctx = new Context(c, t, i, e.getKey()); - ivs.put(calculateIv(ctx, e.getValue()), ctx); - } - w.erase(); + if(!localTransportIds.contains(t)) continue; + TransportIndex i = transport.getIndex(); + ConnectionWindow w = db.getConnectionWindow(c, i); + for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) { + Context ctx = new Context(c, t, i, e.getKey()); + ivs.put(calculateIv(ctx, e.getValue()), ctx); } + w.erase(); } } catch(NoSuchContactException e) { // The contact was removed - clean up in removeContact() @@ -263,14 +267,10 @@ DatabaseListener { if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.getMessage()); return; } - // Remove any IVs that are no longer current + // Remove the old IVs Iterator<Context> it = expected.values().iterator(); - while(it.hasNext()) { - Context ctx = it.next(); - if(ctx.contactId.equals(c) && !current.contains(ctx.transportIndex)) - it.remove(); - } - // Add any IVs that were not previously known + while(it.hasNext()) if(it.next().contactId.equals(c)) it.remove(); + // Store the new IVs expected.putAll(ivs); } diff --git a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java index b7ae59a940..efd5cf0337 100644 --- a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java +++ b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java @@ -2,6 +2,7 @@ package net.sf.briar.transport; import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Map; @@ -16,13 +17,13 @@ import net.sf.briar.api.ContactId; import net.sf.briar.api.crypto.CryptoComponent; import net.sf.briar.api.crypto.ErasableKey; import net.sf.briar.api.db.DatabaseComponent; -import net.sf.briar.api.db.DbException; +import net.sf.briar.api.db.event.ContactRemovedEvent; +import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent; +import net.sf.briar.api.db.event.TransportAddedEvent; import net.sf.briar.api.protocol.Transport; import net.sf.briar.api.protocol.TransportId; import net.sf.briar.api.protocol.TransportIndex; import net.sf.briar.api.transport.ConnectionContext; -import net.sf.briar.api.transport.ConnectionRecogniser; -import net.sf.briar.api.transport.ConnectionRecogniser.Callback; import net.sf.briar.api.transport.ConnectionWindow; import net.sf.briar.crypto.CryptoModule; import net.sf.briar.plugins.ImmediateExecutor; @@ -41,8 +42,7 @@ public class ConnectionRecogniserImplTest extends TestCase { private final byte[] inSecret; private final TransportId transportId; private final TransportIndex localIndex, remoteIndex; - private final Collection<Transport> transports; - private final ConnectionWindow connectionWindow; + private final Collection<Transport> localTransports, remoteTransports; public ConnectionRecogniserImplTest() { super(); @@ -54,140 +54,496 @@ public class ConnectionRecogniserImplTest extends TestCase { transportId = new TransportId(TestUtils.getRandomId()); localIndex = new TransportIndex(13); remoteIndex = new TransportIndex(7); - Transport transport = new Transport(transportId, localIndex, - Collections.singletonMap("foo", "bar")); - transports = Collections.singletonList(transport); - connectionWindow = new ConnectionWindowImpl(crypto, remoteIndex, - inSecret); + Map<String, String> properties = Collections.singletonMap("foo", "bar"); + Transport localTransport = new Transport(transportId, localIndex, + properties); + localTransports = Collections.singletonList(localTransport); + Transport remoteTransport = new Transport(transportId, remoteIndex, + properties); + remoteTransports = Collections.singletonList(remoteTransport); } @Test public void testUnexpectedIv() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); Mockery context = new Mockery(); final DatabaseComponent db = context.mock(DatabaseComponent.class); context.checking(new Expectations() {{ - oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); // Initialise + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); oneOf(db).getLocalTransports(); - will(returnValue(transports)); + will(returnValue(localTransports)); oneOf(db).getContacts(); will(returnValue(Collections.singletonList(contactId))); oneOf(db).getRemoteIndex(contactId, transportId); will(returnValue(remoteIndex)); oneOf(db).getConnectionWindow(contactId, remoteIndex); - will(returnValue(connectionWindow)); + will(returnValue(window)); }}); Executor executor = new ImmediateExecutor(); - ConnectionRecogniser c = new ConnectionRecogniserImpl(crypto, db, + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, executor); - c.acceptConnection(transportId, new byte[IV_LENGTH], new Callback() { + assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH])); + context.assertIsSatisfied(); + } - public void connectionAccepted(ConnectionContext ctx) { - fail(); - } + @Test + public void testExpectedIv() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(remoteIndex)); + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + // Update the window + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).setConnectionWindow(contactId, remoteIndex, window); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // The IV should not be expected by the wrong transport + TransportId wrong = new TransportId(TestUtils.getRandomId()); + assertNull(c.acceptConnection(wrong, encryptedIv)); + // The IV should be expected by the right transport + ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(remoteIndex, ctx.getTransportIndex()); + assertEquals(3, ctx.getConnectionNumber()); + // The IV should no longer be expected + assertNull(c.acceptConnection(transportId, encryptedIv)); + // The window should have advanced + Map<Long, byte[]> unseen = window.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 19; i++) { + assertEquals(i != 3, unseen.containsKey(Long.valueOf(i))); + } + context.assertIsSatisfied(); + } - public void connectionRejected() { - // Expected - } + @Test + public void testContactRemovedAfterInit() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise before removing contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(remoteIndex)); + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // Ensure the recogniser is initialised + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH])); + assertTrue(c.isInitialised()); + // Remove the contact + c.eventOccurred(new ContactRemovedEvent(contactId)); + // The IV should not be expected + assertNull(c.acceptConnection(transportId, encryptedIv)); + context.assertIsSatisfied(); + } - public void handleException(DbException e) { - fail(); - } - }); + @Test + public void testContactRemovedBeforeInit() throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise after removing contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.emptyList())); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // Remove the contact + c.eventOccurred(new ContactRemovedEvent(contactId)); + // The IV should not be expected + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, encryptedIv)); + assertTrue(c.isInitialised()); context.assertIsSatisfied(); } @Test - public void testExpectedIv() throws Exception { - // Calculate the shared secret for connection number 3 - byte[] secret = inSecret; - for(int i = 0; i < 4; i++) { - secret = crypto.deriveNextSecret(secret, remoteIndex.getInt(), i); + public void testLocalTransportAddedAfterInit() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise before adding transport + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(Collections.emptyList())); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + // Add the transport + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(remoteIndex)); + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + // Update the window + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).setConnectionWindow(contactId, remoteIndex, window); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // The IV should not be expected + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, encryptedIv)); + assertTrue(c.isInitialised()); + // Add the transport + c.eventOccurred(new TransportAddedEvent(transportId)); + // The IV should be expected + ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(remoteIndex, ctx.getTransportIndex()); + assertEquals(3, ctx.getConnectionNumber()); + // The IV should no longer be expected + assertNull(c.acceptConnection(transportId, encryptedIv)); + // The window should have advanced + Map<Long, byte[]> unseen = window.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 19; i++) { + assertEquals(i != 3, unseen.containsKey(Long.valueOf(i))); } - // Calculate the expected IV for connection number 3 - ErasableKey ivKey = crypto.deriveIvKey(secret, true); - Cipher ivCipher = crypto.getIvCipher(); - ivCipher.init(Cipher.ENCRYPT_MODE, ivKey); - byte[] iv = IvEncoder.encodeIv(true, remoteIndex.getInt(), 3); - byte[] encryptedIv = ivCipher.doFinal(iv); + context.assertIsSatisfied(); + } + @Test + public void testLocalTransportAddedBeforeInit() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); Mockery context = new Mockery(); final DatabaseComponent db = context.mock(DatabaseComponent.class); context.checking(new Expectations() {{ + // Initialise after adding transport oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); - // Initialise oneOf(db).getLocalTransports(); - will(returnValue(transports)); + will(returnValue(localTransports)); oneOf(db).getContacts(); will(returnValue(Collections.singletonList(contactId))); oneOf(db).getRemoteIndex(contactId, transportId); will(returnValue(remoteIndex)); oneOf(db).getConnectionWindow(contactId, remoteIndex); - will(returnValue(connectionWindow)); + will(returnValue(window)); // Update the window oneOf(db).getConnectionWindow(contactId, remoteIndex); - will(returnValue(connectionWindow)); - oneOf(db).setConnectionWindow(contactId, remoteIndex, - connectionWindow); + will(returnValue(window)); + oneOf(db).setConnectionWindow(contactId, remoteIndex, window); }}); Executor executor = new ImmediateExecutor(); - ConnectionRecogniser c = new ConnectionRecogniserImpl(crypto, db, + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, executor); - // The IV should not be expected by the wrong transport - TransportId wrong = new TransportId(TestUtils.getRandomId()); - c.acceptConnection(wrong, encryptedIv, new Callback() { - - public void connectionAccepted(ConnectionContext ctx) { - fail(); - } + byte[] encryptedIv = calculateIv(); + // Add the transport + c.eventOccurred(new TransportAddedEvent(transportId)); + // The IV should be expected + assertFalse(c.isInitialised()); + ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv); + assertTrue(c.isInitialised()); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(remoteIndex, ctx.getTransportIndex()); + assertEquals(3, ctx.getConnectionNumber()); + // The IV should no longer be expected + assertNull(c.acceptConnection(transportId, encryptedIv)); + // The window should have advanced + Map<Long, byte[]> unseen = window.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 19; i++) { + assertEquals(i != 3, unseen.containsKey(Long.valueOf(i))); + } + context.assertIsSatisfied(); + } - public void connectionRejected() { - // Expected - } + @Test + public void testRemoteTransportAddedAfterInit() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise before updating the contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(null)); + // Update the contact + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + // Update the window + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).setConnectionWindow(contactId, remoteIndex, window); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // The IV should not be expected + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, encryptedIv)); + assertTrue(c.isInitialised()); + // Update the contact + c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId, + remoteTransports)); + // The IV should be expected + ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(remoteIndex, ctx.getTransportIndex()); + assertEquals(3, ctx.getConnectionNumber()); + // The IV should no longer be expected + assertNull(c.acceptConnection(transportId, encryptedIv)); + // The window should have advanced + Map<Long, byte[]> unseen = window.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 19; i++) { + assertEquals(i != 3, unseen.containsKey(Long.valueOf(i))); + } + context.assertIsSatisfied(); + } - public void handleException(DbException e) { - fail(); - } - }); - // The IV should be expected by the right transport - c.acceptConnection(transportId, encryptedIv, new Callback() { - - public void connectionAccepted(ConnectionContext ctx) { - assertNotNull(ctx); - assertEquals(contactId, ctx.getContactId()); - assertEquals(remoteIndex, ctx.getTransportIndex()); - assertEquals(3L, ctx.getConnectionNumber()); - } - - public void connectionRejected() { - fail(); - } - - public void handleException(DbException e) { - fail(); - } - }); + @Test + public void testRemoteTransportAddedBeforeInit() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise after updating the contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(remoteIndex)); + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + // Update the window + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).setConnectionWindow(contactId, remoteIndex, window); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // Update the contact + c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId, + remoteTransports)); + // The IV should be expected + assertFalse(c.isInitialised()); + ConnectionContext ctx = c.acceptConnection(transportId, encryptedIv); + assertTrue(c.isInitialised()); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(remoteIndex, ctx.getTransportIndex()); + assertEquals(3, ctx.getConnectionNumber()); // The IV should no longer be expected - c.acceptConnection(transportId, encryptedIv, new Callback() { + assertNull(c.acceptConnection(transportId, encryptedIv)); + // The window should have advanced + Map<Long, byte[]> unseen = window.getUnseen(); + assertEquals(19, unseen.size()); + for(int i = 0; i < 19; i++) { + assertEquals(i != 3, unseen.containsKey(Long.valueOf(i))); + } + context.assertIsSatisfied(); + } + + @Test + public void testRemoteTransportRemovedAfterInit() throws Exception { + final ConnectionWindow window = createConnectionWindow(remoteIndex); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise before updating the contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(remoteIndex)); + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // Ensure the recogniser is initialised + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH])); + assertTrue(c.isInitialised()); + // Update the contact + c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId, + Collections.<Transport>emptyList())); + // The IV should not be expected + assertNull(c.acceptConnection(transportId, encryptedIv)); + context.assertIsSatisfied(); + } - public void connectionAccepted(ConnectionContext ctx) { - fail(); - } + @Test + public void testRemoteTransportRemovedBeforeInit() throws Exception { + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise after updating the contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(null)); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // Update the contact + c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId, + Collections.<Transport>emptyList())); + // The IV should not be expected + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, encryptedIv)); + assertTrue(c.isInitialised()); + context.assertIsSatisfied(); + } - public void connectionRejected() { - // Expected - } + @Test + public void testRemoteTransportIndexChangedAfterInit() throws Exception { + // The contact changes the transport ID <-> index relationships + final TransportId transportId1 = + new TransportId(TestUtils.getRandomId()); + final TransportIndex remoteIndex1 = new TransportIndex(11); + Map<String, String> properties = Collections.singletonMap("foo", "bar"); + Transport remoteTransport = new Transport(transportId, remoteIndex1, + properties); + Transport remoteTransport1 = new Transport(transportId1, remoteIndex, + properties); + Collection<Transport> remoteTransports1 = Arrays.asList( + new Transport[] {remoteTransport, remoteTransport1}); + // Use two local transports for this test + TransportIndex localIndex1 = new TransportIndex(17); + Transport localTransport = new Transport(transportId, localIndex, + properties); + Transport localTransport1 = new Transport(transportId1, localIndex1, + properties); + final Collection<Transport> localTransports1 = Arrays.asList( + new Transport[] {localTransport, localTransport1}); - public void handleException(DbException e) { - fail(); - } - }); + final ConnectionWindow window = createConnectionWindow(remoteIndex); + final ConnectionWindow window1 = createConnectionWindow(remoteIndex1); + Mockery context = new Mockery(); + final DatabaseComponent db = context.mock(DatabaseComponent.class); + context.checking(new Expectations() {{ + // Initialise before updating the contact + oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class))); + oneOf(db).getLocalTransports(); + will(returnValue(localTransports1)); + oneOf(db).getContacts(); + will(returnValue(Collections.singletonList(contactId))); + // First, transportId <-> remoteIndex, transportId1 <-> remoteIndex + oneOf(db).getRemoteIndex(contactId, transportId); + will(returnValue(remoteIndex)); + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).getRemoteIndex(contactId, transportId1); + will(returnValue(remoteIndex1)); + oneOf(db).getConnectionWindow(contactId, remoteIndex1); + will(returnValue(window1)); + // Later, transportId <-> remoteIndex1, transportId1 <-> remoteIndex + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).getConnectionWindow(contactId, remoteIndex1); + will(returnValue(window1)); + // Update the window + oneOf(db).getConnectionWindow(contactId, remoteIndex); + will(returnValue(window)); + oneOf(db).setConnectionWindow(contactId, remoteIndex, window); + }}); + Executor executor = new ImmediateExecutor(); + ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(crypto, db, + executor); + byte[] encryptedIv = calculateIv(); + // Ensure the recogniser is initialised + assertFalse(c.isInitialised()); + assertNull(c.acceptConnection(transportId, new byte[IV_LENGTH])); + assertTrue(c.isInitialised()); + // Update the contact + c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId, + remoteTransports1)); + // The IV should not be expected by the old transport + assertNull(c.acceptConnection(transportId, encryptedIv)); + // The IV should be expected by the new transport + ConnectionContext ctx = c.acceptConnection(transportId1, encryptedIv); + assertNotNull(ctx); + assertEquals(contactId, ctx.getContactId()); + assertEquals(remoteIndex, ctx.getTransportIndex()); + assertEquals(3, ctx.getConnectionNumber()); + // The IV should no longer be expected + assertNull(c.acceptConnection(transportId1, encryptedIv)); // The window should have advanced - Map<Long, byte[]> unseen = connectionWindow.getUnseen(); + Map<Long, byte[]> unseen = window.getUnseen(); assertEquals(19, unseen.size()); for(int i = 0; i < 19; i++) { assertEquals(i != 3, unseen.containsKey(Long.valueOf(i))); } context.assertIsSatisfied(); } + + private ConnectionWindow createConnectionWindow(TransportIndex index) { + return new ConnectionWindowImpl(crypto, index, inSecret) { + @Override + public void erase() {} + }; + } + + private byte[] calculateIv() throws Exception { + // Calculate the shared secret for connection number 3 + byte[] secret = inSecret; + for(int i = 0; i < 4; i++) { + secret = crypto.deriveNextSecret(secret, remoteIndex.getInt(), i); + } + // Calculate the expected IV for connection number 3 + ErasableKey ivKey = crypto.deriveIvKey(secret, true); + Cipher ivCipher = crypto.getIvCipher(); + ivCipher.init(Cipher.ENCRYPT_MODE, ivKey); + byte[] iv = IvEncoder.encodeIv(true, remoteIndex.getInt(), 3); + return ivCipher.doFinal(iv); + } } -- GitLab