diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java index 0de25f31f9f4542b8c9a5c2a9278ff804b38ef17..564db75c6f11797e47ec0dca6cca03c0739888d4 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java @@ -1,7 +1,10 @@ package org.briarproject; +import android.support.annotation.Nullable; + import net.jodah.concurrentunit.Waiter; +import org.briarproject.api.FormatException; import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.SessionId; import org.briarproject.api.contact.Contact; @@ -35,6 +38,7 @@ import org.briarproject.api.properties.TransportProperties; import org.briarproject.api.properties.TransportPropertyManager; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.SyncSessionFactory; @@ -59,6 +63,7 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; @@ -69,22 +74,15 @@ import javax.inject.Inject; import static org.briarproject.TestPluginsModule.MAX_LATENCY; import static org.briarproject.TestPluginsModule.TRANSPORT_ID; +import static org.briarproject.api.clients.MessageQueueManager.QUEUE_STATE_KEY; import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; -import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY; import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.api.introduction.IntroductionConstants.MAC_LENGTH; import static org.briarproject.api.introduction.IntroductionConstants.NAME; import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.api.introduction.IntroductionConstants.SIGNATURE; import static org.briarproject.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.api.introduction.IntroductionConstants.TIME; import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT; import static org.briarproject.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; @@ -95,11 +93,13 @@ import static org.junit.Assert.assertTrue; public class IntroductionIntegrationTest extends BriarTestCase { - private LifecycleManager lifecycleManager0, lifecycleManager1, lifecycleManager2; + private LifecycleManager lifecycleManager0, lifecycleManager1, + lifecycleManager2; private SyncSessionFactory sync0, sync1, sync2; private ContactManager contactManager0, contactManager1, contactManager2; private ContactId contactId0, contactId1, contactId2; - private IdentityManager identityManager0, identityManager1, identityManager2; + private IdentityManager identityManager0, identityManager1, + identityManager2; private LocalAuthor author0, author1, author2; @Inject @@ -108,6 +108,8 @@ public class IntroductionIntegrationTest extends BriarTestCase { CryptoComponent crypto; @Inject AuthorFactory authorFactory; + @Inject + IntroductionGroupFactory introductionGroupFactory; // objects accessed from background threads need to be volatile private volatile IntroductionManager introductionManager0; @@ -834,7 +836,7 @@ public class IntroductionIntegrationTest extends BriarTestCase { } @Test - public void testFakeResponse() throws Exception { + public void testModifiedResponse() throws Exception { startLifecycles(); try { addDefaultIdentities(); @@ -856,122 +858,106 @@ public class IntroductionIntegrationTest extends BriarTestCase { introductionManager0 .makeIntroduction(introducee1, introducee2, "Hi!", time); - // sync first request message + // sync request messages deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener1.requestReceived); + deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); + eventWaiter.await(TIMEOUT, 2); // sync first response deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); eventWaiter.await(TIMEOUT, 1); - assertTrue(listener0.response1Received); - // get SessionId - List<IntroductionMessage> list = new ArrayList<>( - introductionManager1.getIntroductionMessages(contactId0)); - assertEquals(2, list.size()); - assertTrue(list.get(0) instanceof IntroductionRequest); - IntroductionRequest msg = (IntroductionRequest) list.get(0); - SessionId sessionId = msg.getSessionId(); - - // get contact group - IntroductionGroupFactory groupFactory = - t0.getIntroductionGroupFactory(); - Group group = groupFactory.createIntroductionGroup(introducee1); + // get response to be forwarded + MessageId responseId = null; + BdfDictionary response = null; + Group g2 = introductionGroupFactory + .createIntroductionGroup(introducee2); + ClientHelper clientHelper0 = t0.getClientHelper(); + Map<MessageId, BdfDictionary> map = + clientHelper0.getMessageMetadataAsDictionary(g2.getId()); + for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) { + if (entry.getValue().getLong(TYPE) == TYPE_RESPONSE) { + responseId = entry.getKey(); + response = entry.getValue(); + } + } + assertTrue(responseId != null && response != null); - // get data for contact2 - long timestamp = clock.currentTimeMillis(); - KeyPair eKeyPair = crypto.generateAgreementKeyPair(); - byte[] ePublicKey = eKeyPair.getPublic().getEncoded(); - TransportProperties tp = new TransportProperties( - Collections.singletonMap("key", "value")); - BdfDictionary tpDict = BdfDictionary.of(new BdfEntry("fake", tp)); + // adapt outgoing message queue to removed message + decreaseOutgoingMessageCounter(clientHelper0, g2.getId(), 1); - // create a fake response - BdfDictionary d = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_RESPONSE), - new BdfEntry(SESSION_ID, sessionId), - new BdfEntry(GROUP_ID, group.getId()), - new BdfEntry(ACCEPT, true), - new BdfEntry(TIME, timestamp), - new BdfEntry(E_PUBLIC_KEY, ePublicKey), - new BdfEntry(TRANSPORT, tpDict) - ); + // modify response by changing transport properties + BdfDictionary tp = response.getDictionary(TRANSPORT); + tp.put("fakeId", BdfDictionary.of(new BdfEntry("fake", "fake"))); + response.put(TRANSPORT, tp); - // add the message to the queue - DatabaseComponent db0 = t0.getDatabaseComponent(); + // replace original response with modified one MessageSender sender0 = t0.getMessageSender(); + DatabaseComponent db0 = t0.getDatabaseComponent(); Transaction txn = db0.startTransaction(false); try { - sender0.sendMessage(txn, d); + db0.deleteMessage(txn, responseId); + sender0.sendMessage(txn, response); txn.setComplete(); } finally { db0.endTransaction(txn); } - // send the fake response + // sync second response + deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); + eventWaiter.await(TIMEOUT, 1); + + // sync forwarded responses to introducees deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); - // fake session state for introducer, so she doesn't abort - ClientHelper clientHelper0 = t0.getClientHelper(); - BdfDictionary state = - clientHelper0.getMessageMetadataAsDictionary(sessionId); - state.put(STATE, IntroducerProtocolState.AWAIT_ACKS.getValue()); - clientHelper0.mergeMessageMetadata(sessionId, state); + // sync first ACK and its forward + deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); + deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); - // sync back the ACK + // sync second ACK and forward it deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); - // create a fake ACK - // TODO do we need to actually calculate a MAC and signature here? - byte[] mac = TestUtils.getRandomBytes(MAC_LENGTH); - byte[] sig = TestUtils.getRandomBytes(MAX_SIGNATURE_LENGTH); - d = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_ACK), - new BdfEntry(SESSION_ID, sessionId), - new BdfEntry(GROUP_ID, group.getId()), - new BdfEntry(MAC, mac), - new BdfEntry(SIGNATURE, sig) - ); - - // add the fake ACK to the message queue - txn = db0.startTransaction(false); + // introducee2 should have detected the fake now + // and deleted introducee1 again + Collection<Contact> contacts2; + DatabaseComponent db2 = t2.getDatabaseComponent(); + txn = db2.startTransaction(true); try { - sender0.sendMessage(txn, d); + contacts2 = db2.getContacts(txn); txn.setComplete(); } finally { - db0.endTransaction(txn); + db2.endTransaction(txn); } + assertEquals(1, contacts2.size()); - // make sure the contact was already added (as inactive) - DatabaseComponent db1 = t1.getDatabaseComponent(); - txn = db1.startTransaction(true); - try { - assertEquals(2, db1.getContacts(txn).size()); - txn.setComplete(); - } finally { - db1.endTransaction(txn); - } + // sync abort message to introducer + deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); + + // ensure introducer got the abort + SessionId sessionId = new SessionId(response.getRaw(SESSION_ID)); + BdfDictionary state = + clientHelper0.getMessageMetadataAsDictionary(sessionId); + assertEquals(IntroducerProtocolState.ERROR.getValue(), + state.getLong(STATE).intValue()); - // send the fake ACK + // sync abort messages to introducees deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); - // make sure session was aborted and contact deleted again + // although aborted, introducee1 keeps the contact, + // so introducer can not make contacts disappear by sending abort + Collection<Contact> contacts1; + DatabaseComponent db1 = t1.getDatabaseComponent(); txn = db1.startTransaction(true); try { - assertEquals(1, db1.getContacts(txn).size()); + contacts1 = db1.getContacts(txn); txn.setComplete(); } finally { db1.endTransaction(txn); } - - // there should now be an abort message to sync back - deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); - - // ensure introducer got the abort - state = clientHelper0.getMessageMetadataAsDictionary(sessionId); - assertEquals(IntroducerProtocolState.ERROR.getValue(), - state.getLong(STATE).intValue()); + assertEquals(2, contacts1.size()); } finally { stopLifecycles(); } @@ -1067,7 +1053,7 @@ public class IntroductionIntegrationTest extends BriarTestCase { } private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, - SyncSessionFactory toSync, ContactId toId, String debug) + SyncSessionFactory toSync, ContactId toId, @Nullable String debug) throws IOException, TimeoutException { if (debug != null) LOG.info("TEST: Sending message from " + debug); @@ -1214,6 +1200,16 @@ public class IntroductionIntegrationTest extends BriarTestCase { } } + private void decreaseOutgoingMessageCounter(ClientHelper clientHelper, + GroupId g, int num) throws FormatException, DbException { + BdfDictionary gD = clientHelper.getGroupMetadataAsDictionary(g); + LOG.warning(gD.toString()); + BdfDictionary queue = gD.getDictionary(QUEUE_STATE_KEY); + queue.put("nextOut", queue.getLong("nextOut") - num); + gD.put(QUEUE_STATE_KEY, queue); + clientHelper.mergeGroupMetadata(g, gD); + } + private void injectEagerSingletons( IntroductionIntegrationTestComponent component) { diff --git a/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java b/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java index d132e0d68a37bdd31dd25bc3f1e82a34d44a7105..61bcf383274c6b5e6fee7f2bcf5d0c4c60350c25 100644 --- a/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java +++ b/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java @@ -3,6 +3,7 @@ package org.briarproject.api.introduction; import static org.briarproject.api.introduction.IntroducerAction.ACK_1; import static org.briarproject.api.introduction.IntroducerAction.ACK_2; import static org.briarproject.api.introduction.IntroducerAction.LOCAL_REQUEST; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ABORT; import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_1; import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_2; import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_1; @@ -65,7 +66,13 @@ public enum IntroducerProtocolState { return ERROR; } }, - FINISHED(8); + FINISHED(8) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == REMOTE_ABORT) return ERROR; + return FINISHED; + } + }; private final int value;