diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java similarity index 79% rename from briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java rename to briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java index c92d588f0e34ac4983e6559081d9f1115cd3a51e..af71da19ecf105111c1f560dca79575e14b14f5e 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTest.java @@ -1,9 +1,12 @@ -package org.briarproject; +package org.briarproject.introduction; import android.support.annotation.Nullable; import net.jodah.concurrentunit.Waiter; +import org.briarproject.BriarTestCase; +import org.briarproject.TestDatabaseModule; +import org.briarproject.TestUtils; import org.briarproject.api.FormatException; import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.SessionId; @@ -13,8 +16,10 @@ import org.briarproject.api.contact.ContactManager; import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.KeyPair; import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.crypto.Signature; import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.data.BdfList; import org.briarproject.api.db.DatabaseComponent; import org.briarproject.api.db.DbException; import org.briarproject.api.db.Metadata; @@ -29,7 +34,6 @@ import org.briarproject.api.event.MessageStateChangedEvent; import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; -import org.briarproject.api.introduction.IntroducerProtocolState; import org.briarproject.api.introduction.IntroductionManager; import org.briarproject.api.introduction.IntroductionMessage; import org.briarproject.api.introduction.IntroductionRequest; @@ -45,9 +49,6 @@ import org.briarproject.api.sync.ValidationManager.State; import org.briarproject.api.system.Clock; import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; -import org.briarproject.introduction.IntroductionGroupFactory; -import org.briarproject.introduction.IntroductionModule; -import org.briarproject.introduction.MessageSender; import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.properties.PropertiesModule; import org.briarproject.sync.SyncModule; @@ -61,11 +62,13 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; +import java.security.GeneralSecurityException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; @@ -75,11 +78,16 @@ 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.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_KEY; import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.NONCE; 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.STATE; +import static org.briarproject.api.introduction.IntroductionConstants.SIGNATURE; +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_REQUEST; @@ -89,6 +97,7 @@ import static org.briarproject.api.sync.ValidationManager.State.INVALID; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class IntroductionIntegrationTest extends BriarTestCase { @@ -123,12 +132,19 @@ public class IntroductionIntegrationTest extends BriarTestCase { private final String INTRODUCER = "Introducer"; private final String INTRODUCEE1 = "Introducee1"; private final String INTRODUCEE2 = "Introducee2"; + private IntroducerListener listener0; + private IntroduceeListener listener1; + private IntroduceeListener listener2; private static final Logger LOG = Logger.getLogger(IntroductionIntegrationTest.class.getName()); private IntroductionIntegrationTestComponent t0, t1, t2; + interface StateVisitor { + boolean visit(BdfDictionary response); + } + @Before public void setUp() { IntroductionIntegrationTestComponent component = @@ -172,21 +188,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroductionSession() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(true, true); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, true); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -258,21 +264,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroductionSessionFirstDecline() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(false, true); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, false); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, true); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -335,21 +331,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroductionSessionSecondDecline() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(true, false); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, false); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -407,21 +393,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroductionSessionDelayedFirstDecline() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(false, false); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, false); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, false); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -470,19 +446,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroductionToSameContact() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(true, false); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -521,9 +489,6 @@ public class IntroductionIntegrationTest extends BriarTestCase { TestUtils.getRandomBytes(123)); identityManager1.addLocalAuthor(author2); - // Add Transport Properties - addTransportProperties(); - // Add introducees' authors as contacts contactId1 = contactManager0.addContact(author1, author0.getId(), master, clock.currentTimeMillis(), true, @@ -543,12 +508,20 @@ public class IntroductionIntegrationTest extends BriarTestCase { author2.getId(), master, clock.currentTimeMillis(), false, true, true ); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); + addListeners(true, false); + + // Add Transport Properties + TransportPropertyManager tpm0 = t0.getTransportPropertyManager(); + TransportPropertyManager tpm1 = t1.getTransportPropertyManager(); + TransportProperties tp = new TransportProperties( + Collections.singletonMap("key", "value")); + tpm0.mergeLocalProperties(TRANSPORT_ID, tp); + deliverMessage(sync0, contactId01, sync1, contactId1, "0 to 11"); + deliverMessage(sync0, contactId02, sync1, contactId2, "0 to 12"); + tpm1.mergeLocalProperties(TRANSPORT_ID, tp); + deliverMessage(sync1, contactId1, sync0, contactId01, "1 to 01"); + deliverMessage(sync1, contactId2, sync0, contactId02, "1 to 02"); // make introduction long time = clock.currentTimeMillis(); @@ -612,21 +585,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testSessionIdReuse() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(true, true); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, true); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -690,21 +653,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroducerRemovedCleanup() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(true, true); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, true); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -757,21 +710,11 @@ public class IntroductionIntegrationTest extends BriarTestCase { public void testIntroduceesRemovedCleanup() throws Exception { startLifecycles(); try { - // Add Identities And Contacts addDefaultIdentities(); addDefaultContacts(); - - // Add Transport Properties + addListeners(true, true); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, true); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -834,22 +777,15 @@ public class IntroductionIntegrationTest extends BriarTestCase { } } - @Test - public void testModifiedResponse() throws Exception { + private void testModifiedResponse(StateVisitor visitor) + throws Exception { startLifecycles(); try { addDefaultIdentities(); addDefaultContacts(); + addListeners(true, true); addTransportProperties(); - // listen to events - IntroducerListener listener0 = new IntroducerListener(); - t0.getEventBus().addListener(listener0); - IntroduceeListener listener1 = new IntroduceeListener(1, true); - t1.getEventBus().addListener(listener1); - IntroduceeListener listener2 = new IntroduceeListener(2, true); - t2.getEventBus().addListener(listener2); - // make introduction long time = clock.currentTimeMillis(); Contact introducee1 = contactManager0.getContact(contactId1); @@ -867,28 +803,19 @@ public class IntroductionIntegrationTest extends BriarTestCase { eventWaiter.await(TIMEOUT, 1); // 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); + Entry<MessageId, BdfDictionary> resp = + getMessageFor(introducee2, TYPE_RESPONSE); + MessageId responseId = resp.getKey(); + BdfDictionary response = resp.getValue(); // adapt outgoing message queue to removed message + ClientHelper clientHelper0 = t0.getClientHelper(); + Group g2 = introductionGroupFactory + .createIntroductionGroup(introducee2); decreaseOutgoingMessageCounter(clientHelper0, g2.getId(), 1); - // modify response by changing transport properties - BdfDictionary tp = response.getDictionary(TRANSPORT); - tp.put("fakeId", BdfDictionary.of(new BdfEntry("fake", "fake"))); - response.put(TRANSPORT, tp); + // allow visitor to modify response + boolean earlyAbort = visitor.visit(response); // replace original response with modified one MessageSender sender0 = t0.getMessageSender(); @@ -910,11 +837,7 @@ public class IntroductionIntegrationTest extends BriarTestCase { deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); - // sync first ACK and its forward - deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); - deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); - - // sync second ACK and forward it + // sync first ACK and forward it deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); @@ -931,37 +854,167 @@ public class IntroductionIntegrationTest extends BriarTestCase { } assertEquals(1, contacts2.size()); - // sync abort message to introducer - deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); + // sync introducee2's ack and following abort + deliverMessage(sync2, contactId2, sync0, contactId0, 2, "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()); + assertTrue(listener0.aborted); // sync abort messages to introducees - deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + deliverMessage(sync0, contactId0, sync1, contactId1, 2, "0 to 1"); deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); - // 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 { - contacts1 = db1.getContacts(txn); - txn.setComplete(); - } finally { - db1.endTransaction(txn); + if (earlyAbort) { + assertTrue(listener1.aborted); + assertTrue(listener2.aborted); + } else { + assertTrue(listener2.aborted); + // when aborted late, introducee1 keeps the contact, + // so introducer can not make contacts disappear by aborting + Collection<Contact> contacts1; + DatabaseComponent db1 = t1.getDatabaseComponent(); + txn = db1.startTransaction(true); + try { + contacts1 = db1.getContacts(txn); + txn.setComplete(); + } finally { + db1.endTransaction(txn); + } + assertEquals(2, contacts1.size()); } - assertEquals(2, contacts1.size()); } finally { stopLifecycles(); } } + @Test + public void testModifiedTransportProperties() throws Exception { + testModifiedResponse(new StateVisitor() { + @Override + public boolean visit(BdfDictionary response) { + BdfDictionary tp = response.getDictionary(TRANSPORT, null); + tp.put("fakeId", + BdfDictionary.of(new BdfEntry("fake", "fake"))); + response.put(TRANSPORT, tp); + return false; + } + }); + } + + @Test + public void testModifiedTimestamp() throws Exception { + testModifiedResponse(new StateVisitor() { + @Override + public boolean visit(BdfDictionary response) { + long timestamp = response.getLong(TIME, 0L); + response.put(TIME, timestamp + 1); + return false; + } + }); + } + + @Test + public void testModifiedEphemeralPublicKey() throws Exception { + testModifiedResponse(new StateVisitor() { + @Override + public boolean visit(BdfDictionary response) { + KeyPair keyPair = crypto.generateSignatureKeyPair(); + response.put(E_PUBLIC_KEY, keyPair.getPublic().getEncoded()); + return true; + } + }); + } + + @Test + public void testModifiedEphemeralPublicKeyWithFakeMac() + throws Exception { + // initialize a real introducee manager + MessageSender messageSender = t2.getMessageSender(); + DatabaseComponent db = t2.getDatabaseComponent(); + ClientHelper clientHelper = t2.getClientHelper(); + TransportPropertyManager tpManager = t2.getTransportPropertyManager(); + ContactManager contactManager = t2.getContactManager(); + IdentityManager identityManager = t2.getIdentityManager(); + IntroduceeManager manager2 = + new IntroduceeManager(messageSender, db, clientHelper, clock, + crypto, tpManager, authorFactory, contactManager, + identityManager, introductionGroupFactory); + + // create keys + KeyPair keyPair1 = crypto.generateSignatureKeyPair(); + KeyPair eKeyPair1 = crypto.generateAgreementKeyPair(); + byte[] ePublicKeyBytes1 = eKeyPair1.getPublic().getEncoded(); + KeyPair eKeyPair2 = crypto.generateAgreementKeyPair(); + byte[] ePublicKeyBytes2 = eKeyPair2.getPublic().getEncoded(); + + // Nonce 1 + SecretKey secretKey = + crypto.deriveMasterSecret(ePublicKeyBytes2, eKeyPair1, true); + byte[] nonce1 = crypto.deriveSignatureNonce(secretKey, true); + + // Signature 1 + Signature signature = crypto.getSignature(); + signature.initSign(keyPair1.getPrivate()); + signature.update(nonce1); + byte[] sig1 = signature.sign(); + + // MAC 1 + SecretKey macKey1 = crypto.deriveMacKey(secretKey, true); + BdfDictionary tp1 = BdfDictionary.of(new BdfEntry("fake", "fake")); + long time1 = clock.currentTimeMillis(); + BdfList toMacList = BdfList.of(keyPair1.getPublic().getEncoded(), + ePublicKeyBytes1, tp1, time1); + byte[] toMac = clientHelper.toByteArray(toMacList); + byte[] mac1 = crypto.mac(macKey1, toMac); + + // create only relevant part of state for introducee2 + BdfDictionary state = new BdfDictionary(); + state.put(PUBLIC_KEY, keyPair1.getPublic().getEncoded()); + state.put(TRANSPORT, tp1); + state.put(TIME, time1); + state.put(E_PUBLIC_KEY, ePublicKeyBytes1); + state.put(MAC, mac1); + state.put(MAC_KEY, macKey1.getBytes()); + state.put(NONCE, nonce1); + state.put(SIGNATURE, sig1); + + // MAC and signature verification should pass + manager2.verifyMac(state); + manager2.verifySignature(state); + + // replace ephemeral key pair and recalculate matching keys and nonce + KeyPair eKeyPair1f = crypto.generateAgreementKeyPair(); + byte[] ePublicKeyBytes1f = eKeyPair1f.getPublic().getEncoded(); + secretKey = + crypto.deriveMasterSecret(ePublicKeyBytes2, eKeyPair1f, true); + nonce1 = crypto.deriveSignatureNonce(secretKey, true); + + // recalculate MAC + macKey1 = crypto.deriveMacKey(secretKey, true); + toMacList = BdfList.of(keyPair1.getPublic().getEncoded(), + ePublicKeyBytes1f, tp1, time1); + toMac = clientHelper.toByteArray(toMacList); + mac1 = crypto.mac(macKey1, toMac); + + // update state with faked information + state.put(E_PUBLIC_KEY, ePublicKeyBytes1f); + state.put(MAC, mac1); + state.put(MAC_KEY, macKey1.getBytes()); + state.put(NONCE, nonce1); + + // MAC verification should still pass + manager2.verifyMac(state); + + // Signature can not be verified, because we don't have private + // long-term key to fake it + try { + manager2.verifySignature(state); + fail(); + } catch(GeneralSecurityException e) { + // expected + } + } + @After public void tearDown() throws InterruptedException { TestUtils.deleteTestDirectory(testDir); @@ -990,16 +1043,33 @@ public class IntroductionIntegrationTest extends BriarTestCase { lifecycleManager2.waitForShutdown(); } - private void addTransportProperties() throws DbException { + private void addTransportProperties() + throws DbException, IOException, TimeoutException { TransportPropertyManager tpm0 = t0.getTransportPropertyManager(); TransportPropertyManager tpm1 = t1.getTransportPropertyManager(); TransportPropertyManager tpm2 = t2.getTransportPropertyManager(); - TransportProperties tp = new TransportProperties( Collections.singletonMap("key", "value")); + tpm0.mergeLocalProperties(TRANSPORT_ID, tp); + deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + deliverMessage(sync0, contactId0, sync2, contactId2, "0 to 2"); + tpm1.mergeLocalProperties(TRANSPORT_ID, tp); + deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + tpm2.mergeLocalProperties(TRANSPORT_ID, tp); + deliverMessage(sync2, contactId2, sync0, contactId0, "2 to 0"); + } + + private void addListeners(boolean accept1, boolean accept2) { + // listen to events + listener0 = new IntroducerListener(); + t0.getEventBus().addListener(listener0); + listener1 = new IntroduceeListener(1, accept1); + t1.getEventBus().addListener(listener1); + listener2 = new IntroduceeListener(2, accept2); + t2.getEventBus().addListener(listener2); } private void addDefaultIdentities() throws DbException { @@ -1045,14 +1115,21 @@ public class IntroductionIntegrationTest extends BriarTestCase { assertTrue(contactId0.equals(contactId02)); } + private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, + SyncSessionFactory toSync, ContactId toId, String debug) + throws IOException, TimeoutException { + deliverMessage(fromSync, fromId, toSync, toId, 1, debug); + } + private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, SyncSessionFactory toSync, ContactId toId) throws IOException, TimeoutException { - deliverMessage(fromSync, fromId, toSync, toId, null); + deliverMessage(fromSync, fromId, toSync, toId, 1, null); } private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, - SyncSessionFactory toSync, ContactId toId, @Nullable String debug) + SyncSessionFactory toSync, ContactId toId, int num, + @Nullable String debug) throws IOException, TimeoutException { if (debug != null) LOG.info("TEST: Sending message from " + debug); @@ -1072,8 +1149,8 @@ public class IntroductionIntegrationTest extends BriarTestCase { sessionTo.run(); in.close(); - // wait for message to actually arrive - msgWaiter.await(TIMEOUT, 1); + // wait for [num] message(s) to actually arrive + msgWaiter.await(TIMEOUT, num); } private void assertDefaultUiMessages() throws DbException { @@ -1176,15 +1253,12 @@ public class IntroductionIntegrationTest extends BriarTestCase { } else if (e instanceof IntroductionResponseReceivedEvent) { ContactId c = ((IntroductionResponseReceivedEvent) e).getContactId(); - try { - if (c.equals(contactId1)) { - response1Received = true; - } else if (c.equals(contactId2)) { - response2Received = true; - } - } finally { - eventWaiter.resume(); + if (c.equals(contactId1)) { + response1Received = true; + } else if (c.equals(contactId2)) { + response2Received = true; } + eventWaiter.resume(); } else if (e instanceof IntroductionAbortedEvent) { aborted = true; eventWaiter.resume(); @@ -1202,6 +1276,23 @@ public class IntroductionIntegrationTest extends BriarTestCase { clientHelper.mergeGroupMetadata(g, gD); } + private Entry<MessageId, BdfDictionary> getMessageFor(Contact contact, + long type) throws FormatException, DbException { + Entry<MessageId, BdfDictionary> response = null; + Group g = introductionGroupFactory + .createIntroductionGroup(contact); + ClientHelper clientHelper0 = t0.getClientHelper(); + Map<MessageId, BdfDictionary> map = + clientHelper0.getMessageMetadataAsDictionary(g.getId()); + for (Entry<MessageId, BdfDictionary> entry : map.entrySet()) { + if (entry.getValue().getLong(TYPE) == type) { + response = entry; + } + } + assertTrue(response != null); + return response; + } + private void injectEagerSingletons( IntroductionIntegrationTestComponent component) { diff --git a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java similarity index 92% rename from briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTestComponent.java rename to briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java index 8e3e770420efdd5bdf6c6133f25926b55cd971c4..c1f7077a497fa2d7c51269cda0ec504e15a75ebe 100644 --- a/briar-android-tests/src/test/java/org/briarproject/IntroductionIntegrationTestComponent.java +++ b/briar-android-tests/src/test/java/org/briarproject/introduction/IntroductionIntegrationTestComponent.java @@ -1,5 +1,8 @@ -package org.briarproject; +package org.briarproject.introduction; +import org.briarproject.TestDatabaseModule; +import org.briarproject.TestPluginsModule; +import org.briarproject.TestSeedProviderModule; import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.contact.ContactManager; import org.briarproject.api.db.DatabaseComponent; @@ -16,9 +19,6 @@ import org.briarproject.data.DataModule; import org.briarproject.db.DatabaseModule; import org.briarproject.event.EventModule; import org.briarproject.identity.IdentityModule; -import org.briarproject.introduction.IntroductionGroupFactory; -import org.briarproject.introduction.IntroductionModule; -import org.briarproject.introduction.MessageSender; import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.properties.PropertiesModule; import org.briarproject.sync.SyncModule; diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java index c5df31c108d4eb60fbb82921ab873a8eea4a634c..a01734cb13a82b16ff42c0185c02c3efd91edd2d 100644 --- a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java +++ b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java @@ -288,93 +288,40 @@ class IntroduceeManager { return; } - LOG.info("Adding contact in inactive state"); - - // get all keys - KeyParser keyParser = cryptoComponent.getAgreementKeyParser(); - byte[] publicKeyBytes; - PublicKey publicKey; - PrivateKey privateKey; - try { - publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY); - publicKey = keyParser - .parsePublicKey(publicKeyBytes); - privateKey = keyParser.parsePrivateKey( - localState.getRaw(OUR_PRIVATE_KEY)); - } catch (GeneralSecurityException e) { - if (LOG.isLoggable(WARNING)) { - LOG.log(WARNING, e.toString(), e); - } - // we can not continue without the keys - throw new RuntimeException("Our own ephemeral key is invalid"); - } - KeyPair keyPair = new KeyPair(publicKey, privateKey); - byte[] theirEphemeralKey = localState.getRaw(E_PUBLIC_KEY); - // figure out who takes which role by comparing public keys + byte[] publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY); + byte[] theirEphemeralKey = localState.getRaw(E_PUBLIC_KEY); int comp = Bytes.COMPARATOR.compare(new Bytes(publicKeyBytes), new Bytes(theirEphemeralKey)); boolean alice = comp < 0; - // The master secret is derived from the local ephemeral key pair - // and the remote ephemeral public key - SecretKey secretKey; - try { - secretKey = cryptoComponent - .deriveMasterSecret(theirEphemeralKey, keyPair, alice); - } catch (GeneralSecurityException e) { - // we can not continue without the shared secret - throw new DbException(e); - } - - // Derive two nonces and a MAC key from the secret master key - byte[] ourNonce = - cryptoComponent.deriveSignatureNonce(secretKey, alice); - byte[] theirNonce = - cryptoComponent.deriveSignatureNonce(secretKey, !alice); - SecretKey macKey = cryptoComponent.deriveMacKey(secretKey, alice); - SecretKey theirMacKey = - cryptoComponent.deriveMacKey(secretKey, !alice); - - // Save the other nonce and MAC key for the verification - localState.put(NONCE, theirNonce); - localState.put(MAC_KEY, theirMacKey.getBytes()); - - // Sign our nonce with our long-term identity public key + // get our local author AuthorId localAuthorId = new AuthorId(localState.getRaw(LOCAL_AUTHOR_ID)); LocalAuthor author = identityManager.getLocalAuthor(txn, localAuthorId); - Signature signature = cryptoComponent.getSignature(); - KeyParser sigParser = cryptoComponent.getSignatureKeyParser(); + + SecretKey secretKey; + byte[] privateKeyBytes = localState.getRaw(OUR_PRIVATE_KEY); try { - PrivateKey privKey = - sigParser.parsePrivateKey(author.getPrivateKey()); - signature.initSign(privKey); + // derive secret master key + secretKey = + deriveSecretKey(publicKeyBytes, privateKeyBytes, alice, + theirEphemeralKey); + // derive MAC keys and nonces, sign our nonce and calculate MAC + deriveMacKeysAndNonces(localState, author, secretKey, alice); } catch (GeneralSecurityException e) { // we can not continue without the signature throw new DbException(e); } - signature.update(ourNonce); - byte[] sig = signature.sign(); + + LOG.info("Adding contact in inactive state"); // The agreed timestamp is the minimum of the peers' timestamps long ourTime = localState.getLong(OUR_TIME); long theirTime = localState.getLong(TIME); long timestamp = Math.min(ourTime, theirTime); - // Calculate a MAC over identity public key, ephemeral public key, - // transport properties and timestamp. - BdfDictionary tp = localState.getDictionary(OUR_TRANSPORT); - BdfList toSignList = BdfList.of(author.getPublicKey(), - publicKeyBytes, tp, ourTime); - byte[] toSign = clientHelper.toByteArray(toSignList); - byte[] mac = cryptoComponent.mac(macKey, toSign); - - // Add MAC and signature to localState, so it can be included in ACK - localState.put(OUR_MAC, mac); - localState.put(OUR_SIGNATURE, sig); - // Add the contact to the database as inactive Author remoteAuthor = authorFactory .createAuthor(localState.getString(NAME), @@ -411,51 +358,15 @@ class IntroduceeManager { if (task == TASK_ACTIVATE_CONTACT) { if (!localState.getBoolean(EXISTS) && localState.containsKey(ADDED_CONTACT_ID)) { - - LOG.info("Verifying Signature..."); - - byte[] nonce = localState.getRaw(NONCE); - byte[] sig = localState.getRaw(SIGNATURE); - byte[] keyBytes = localState.getRaw(PUBLIC_KEY); try { - // Parse the public key - KeyParser keyParser = cryptoComponent.getSignatureKeyParser(); - PublicKey key = keyParser.parsePublicKey(keyBytes); - // Verify the signature - Signature signature = cryptoComponent.getSignature(); - signature.initVerify(key); - signature.update(nonce); - if (!signature.verify(sig)) { - LOG.warning("Invalid nonce signature in ACK"); - throw new GeneralSecurityException(); - } + LOG.info("Verifying Signature..."); + verifySignature(localState); + LOG.info("Verifying MAC..."); + verifyMac(localState); } catch (GeneralSecurityException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); - // we can not continue without verifying the signature throw new DbException(e); } - LOG.info("Verifying MAC..."); - - // get MAC and MAC key from session state - byte[] mac = localState.getRaw(MAC); - byte[] macKeyBytes = localState.getRaw(MAC_KEY); - SecretKey macKey = new SecretKey(macKeyBytes); - - // get MAC data and calculate a new MAC with stored key - byte[] pubKey = localState.getRaw(PUBLIC_KEY); - byte[] ePubKey = localState.getRaw(E_PUBLIC_KEY); - BdfDictionary tp = localState.getDictionary(TRANSPORT); - long timestamp = localState.getLong(TIME); - BdfList toSignList = BdfList.of(pubKey, ePubKey, tp, timestamp); - byte[] toSign = clientHelper.toByteArray(toSignList); - byte[] calculatedMac = cryptoComponent.mac(macKey, toSign); - if (!Arrays.equals(mac, calculatedMac)) { - LOG.warning("Received ACK with invalid MAC"); - throw new DbException(); - } - LOG.info("Activating Contact..."); ContactId contactId = new ContactId( @@ -483,11 +394,122 @@ class IntroduceeManager { contactManager.removeContact(txn, contactId); } } + } + + private SecretKey deriveSecretKey(byte[] publicKeyBytes, + byte[] privateKeyBytes, boolean alice, byte[] theirPublicKey) + throws GeneralSecurityException { + // parse the local ephemeral key pair + KeyParser keyParser = cryptoComponent.getAgreementKeyParser(); + PublicKey publicKey; + PrivateKey privateKey; + try { + publicKey = keyParser.parsePublicKey(publicKeyBytes); + privateKey = keyParser.parsePrivateKey(privateKeyBytes); + } catch (GeneralSecurityException e) { + if (LOG.isLoggable(WARNING)) { + LOG.log(WARNING, e.toString(), e); + } + throw new RuntimeException("Our own ephemeral key is invalid"); + } + KeyPair keyPair = new KeyPair(publicKey, privateKey); + // The master secret is derived from the local ephemeral key pair + // and the remote ephemeral public key + return cryptoComponent + .deriveMasterSecret(theirPublicKey, keyPair, alice); } - public void abort(Transaction txn, BdfDictionary state) { + /** + * Derives nonces, signs our nonce and calculates MAC + * + * Derives two nonces and two mac keys from the secret master key. + * The other introducee's nonce and MAC key are added to the localState. + * + * Our nonce is signed with the local author's long-term private key. + * The signature is added to the localState. + * + * Calculates a MAC and stores it in the localState. + */ + private void deriveMacKeysAndNonces(BdfDictionary localState, + LocalAuthor author, SecretKey secretKey, boolean alice) + throws FormatException, GeneralSecurityException { + // Derive two nonces and a MAC key from the secret master key + byte[] ourNonce = + cryptoComponent.deriveSignatureNonce(secretKey, alice); + byte[] theirNonce = + cryptoComponent.deriveSignatureNonce(secretKey, !alice); + SecretKey macKey = cryptoComponent.deriveMacKey(secretKey, alice); + SecretKey theirMacKey = cryptoComponent.deriveMacKey(secretKey, !alice); + + // Save the other nonce and MAC key for the verification + localState.put(NONCE, theirNonce); + localState.put(MAC_KEY, theirMacKey.getBytes()); + + // Sign our nonce with our long-term identity public key + Signature signature = cryptoComponent.getSignature(); + KeyParser sigParser = cryptoComponent.getSignatureKeyParser(); + PrivateKey privKey = sigParser.parsePrivateKey(author.getPrivateKey()); + signature.initSign(privKey); + signature.update(ourNonce); + byte[] sig = signature.sign(); + + // Calculate a MAC over identity public key, ephemeral public key, + // transport properties and timestamp. + byte[] publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY); + BdfDictionary tp = localState.getDictionary(OUR_TRANSPORT); + long ourTime = localState.getLong(OUR_TIME); + BdfList toMacList = BdfList.of(author.getPublicKey(), + publicKeyBytes, tp, ourTime); + byte[] toMac = clientHelper.toByteArray(toMacList); + byte[] mac = cryptoComponent.mac(macKey, toMac); + + // Add MAC and signature to localState, so it can be included in ACK + localState.put(OUR_MAC, mac); + localState.put(OUR_SIGNATURE, sig); + } + + void verifySignature(BdfDictionary localState) + throws FormatException, GeneralSecurityException { + byte[] nonce = localState.getRaw(NONCE); + byte[] sig = localState.getRaw(SIGNATURE); + byte[] keyBytes = localState.getRaw(PUBLIC_KEY); + + // Parse the public key + KeyParser keyParser = cryptoComponent.getSignatureKeyParser(); + PublicKey key = keyParser.parsePublicKey(keyBytes); + // Verify the signature + Signature signature = cryptoComponent.getSignature(); + signature.initVerify(key); + signature.update(nonce); + if (!signature.verify(sig)) { + LOG.warning("Invalid nonce signature in ACK"); + throw new GeneralSecurityException(); + } + } + void verifyMac(BdfDictionary localState) + throws FormatException, GeneralSecurityException { + // get MAC and MAC key from session state + byte[] mac = localState.getRaw(MAC); + byte[] macKeyBytes = localState.getRaw(MAC_KEY); + SecretKey macKey = new SecretKey(macKeyBytes); + + // get MAC data and calculate a new MAC with stored key + byte[] pubKey = localState.getRaw(PUBLIC_KEY); + byte[] ePubKey = localState.getRaw(E_PUBLIC_KEY); + BdfDictionary tp = localState.getDictionary(TRANSPORT); + long timestamp = localState.getLong(TIME); + BdfList toMacList = BdfList.of(pubKey, ePubKey, tp, timestamp); + byte[] toMac = clientHelper.toByteArray(toMacList); + byte[] calculatedMac = cryptoComponent.mac(macKey, toMac); + if (!Arrays.equals(mac, calculatedMac)) { + LOG.warning("Received ACK with invalid MAC"); + throw new GeneralSecurityException(); + } + } + + public void abort(Transaction txn, BdfDictionary state) { IntroduceeEngine engine = new IntroduceeEngine(); BdfDictionary localAction = new BdfDictionary(); localAction.put(TYPE, TYPE_ABORT); diff --git a/briar-tests/src/org/briarproject/introduction/IntroduceeManagerTest.java b/briar-tests/src/org/briarproject/introduction/IntroduceeManagerTest.java index a7ee386a5df0000be2d665c09d6a8a9246d03660..b5c4ff68414bf9a1850a7c2792dc958eb07afb93 100644 --- a/briar-tests/src/org/briarproject/introduction/IntroduceeManagerTest.java +++ b/briar-tests/src/org/briarproject/introduction/IntroduceeManagerTest.java @@ -10,6 +10,10 @@ import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactManager; import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.PublicKey; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.crypto.Signature; import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfEntry; import org.briarproject.api.data.BdfList; @@ -33,11 +37,14 @@ import org.jmock.Mockery; import org.jmock.lib.legacy.ClassImposteriser; import org.junit.Test; +import java.security.GeneralSecurityException; import java.security.SecureRandom; 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.IntroduceeProtocolState.AWAIT_REQUEST; import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.ADDED_CONTACT_ID; import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED; import static org.briarproject.api.introduction.IntroductionConstants.CONTACT; import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1; @@ -46,9 +53,13 @@ import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_K import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER; import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID; +import static org.briarproject.api.introduction.IntroductionConstants.MAC; +import static org.briarproject.api.introduction.IntroductionConstants.MAC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.MAC_LENGTH; import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID; import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.NONCE; import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; @@ -56,15 +67,21 @@ import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUT import static org.briarproject.api.introduction.IntroductionConstants.ROLE; import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; 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.STORAGE_ID; 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.SyncConstants.MESSAGE_HEADER_LENGTH; +import static org.hamcrest.Matchers.array; +import static org.hamcrest.Matchers.samePropertyValuesAs; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; public class IntroduceeManagerTest extends BriarTestCase { @@ -221,6 +238,152 @@ public class IntroduceeManagerTest extends BriarTestCase { assertFalse(txn.isComplete()); } + @Test + public void testDetectReplacedEphemeralPublicKey() + throws DbException, FormatException, GeneralSecurityException { + + // TODO MR !237 should use its new default initialization method here + final BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, TYPE_RESPONSE); + msg.put(GROUP_ID, introductionGroup1.getId()); + msg.put(SESSION_ID, sessionId); + msg.put(MESSAGE_ID, message1.getId()); + msg.put(MESSAGE_TIME, time); + msg.put(NAME, introducee2.getAuthor().getName()); + msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); + final BdfDictionary state = + initializeSessionState(txn, introductionGroup1.getId(), msg); + + // prepare state for incoming ACK + state.put(STATE, IntroduceeProtocolState.AWAIT_ACK.ordinal()); + state.put(ADDED_CONTACT_ID, 2); + final byte[] nonce = TestUtils.getRandomBytes(42); + state.put(NONCE, nonce); + state.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); + + // create incoming ACK message + final byte[] mac = TestUtils.getRandomBytes(MAC_LENGTH); + final byte[] sig = TestUtils.getRandomBytes(MAX_SIGNATURE_LENGTH); + BdfDictionary ack = BdfDictionary.of( + new BdfEntry(TYPE, TYPE_ACK), + new BdfEntry(SESSION_ID, sessionId), + new BdfEntry(GROUP_ID, introductionGroup1.getId()), + new BdfEntry(MAC, mac), + new BdfEntry(SIGNATURE, sig) + ); + + final KeyParser keyParser = context.mock(KeyParser.class); + final PublicKey publicKey = context.mock(PublicKey.class); + final Signature signature = context.mock(Signature.class); + context.checking(new Expectations() {{ + oneOf(cryptoComponent).getSignatureKeyParser(); + will(returnValue(keyParser)); + oneOf(keyParser) + .parsePublicKey(introducee2.getAuthor().getPublicKey()); + will(returnValue(publicKey)); + oneOf(cryptoComponent).getSignature(); + will(returnValue(signature)); + oneOf(signature).initVerify(publicKey); + oneOf(signature).update(nonce); + oneOf(signature).verify(sig); + will(returnValue(false)); + }}); + + try { + introduceeManager.incomingMessage(txn, state, ack); + fail(); + } catch (DbException e) { + // expected + assertTrue(e.getCause() instanceof GeneralSecurityException); + } + context.assertIsSatisfied(); + assertFalse(txn.isComplete()); + } + + @Test + public void testSignatureVerification() + throws FormatException, DbException, GeneralSecurityException { + + final byte[] publicKeyBytes = introducee2.getAuthor().getPublicKey(); + final byte[] nonce = TestUtils.getRandomBytes(MAC_LENGTH); + final byte[] sig = TestUtils.getRandomBytes(MAC_LENGTH); + + BdfDictionary state = new BdfDictionary(); + state.put(PUBLIC_KEY, publicKeyBytes); + state.put(NONCE, nonce); + state.put(SIGNATURE, sig); + + final KeyParser keyParser = context.mock(KeyParser.class); + final Signature signature = context.mock(Signature.class); + final PublicKey publicKey = context.mock(PublicKey.class); + context.checking(new Expectations() {{ + oneOf(cryptoComponent).getSignatureKeyParser(); + will(returnValue(keyParser)); + oneOf(keyParser).parsePublicKey(publicKeyBytes); + will(returnValue(publicKey)); + oneOf(cryptoComponent).getSignature(); + will(returnValue(signature)); + oneOf(signature).initVerify(publicKey); + oneOf(signature).update(nonce); + oneOf(signature).verify(sig); + will(returnValue(true)); + }}); + introduceeManager.verifySignature(state); + context.assertIsSatisfied(); + } + + @Test + public void testMacVerification() + throws FormatException, DbException, GeneralSecurityException { + + final byte[] publicKeyBytes = introducee2.getAuthor().getPublicKey(); + final BdfDictionary tp = BdfDictionary.of(new BdfEntry("fake", "fake")); + final byte[] ePublicKeyBytes = + TestUtils.getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + final byte[] mac = TestUtils.getRandomBytes(MAC_LENGTH); + final SecretKey macKey = TestUtils.getSecretKey(); + + // move state to where it would be after an ACK arrived + BdfDictionary state = new BdfDictionary(); + state.put(PUBLIC_KEY, publicKeyBytes); + state.put(TRANSPORT, tp); + state.put(TIME, time); + state.put(E_PUBLIC_KEY, ePublicKeyBytes); + state.put(MAC, mac); + state.put(MAC_KEY, macKey.getBytes()); + + final byte[] signBytes = TestUtils.getRandomBytes(42); + context.checking(new Expectations() {{ + oneOf(clientHelper).toByteArray( + BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time)); + will(returnValue(signBytes)); + //noinspection unchecked + oneOf(cryptoComponent).mac(with(samePropertyValuesAs(macKey)), + with(array(equal(signBytes)))); + will(returnValue(mac)); + }}); + introduceeManager.verifyMac(state); + context.assertIsSatisfied(); + + // now produce wrong MAC + context.checking(new Expectations() {{ + oneOf(clientHelper).toByteArray( + BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time)); + will(returnValue(signBytes)); + //noinspection unchecked + oneOf(cryptoComponent).mac(with(samePropertyValuesAs(macKey)), + with(array(equal(signBytes)))); + will(returnValue(TestUtils.getRandomBytes(MAC_LENGTH))); + }}); + try { + introduceeManager.verifyMac(state); + fail(); + } catch(GeneralSecurityException e) { + // expected + } + context.assertIsSatisfied(); + } + private BdfDictionary initializeSessionState(final Transaction txn, final GroupId groupId, final BdfDictionary msg) throws DbException, FormatException { @@ -230,7 +393,7 @@ public class IntroduceeManagerTest extends BriarTestCase { final BdfDictionary groupMetadata = BdfDictionary.of( new BdfEntry(CONTACT, introducee1.getId().getInt()) ); - final boolean contactExists = true; + final boolean contactExists = false; final BdfDictionary state = new BdfDictionary(); state.put(STORAGE_ID, localStateMessage.getId()); state.put(STATE, AWAIT_REQUEST.getValue()); @@ -241,7 +404,7 @@ public class IntroduceeManagerTest extends BriarTestCase { state.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes()); state.put(NOT_OUR_RESPONSE, localStateMessage.getId()); state.put(ANSWERED, false); - state.put(EXISTS, true); + state.put(EXISTS, contactExists); state.put(REMOTE_AUTHOR_ID, introducee2.getAuthor().getId()); state.put(REMOTE_AUTHOR_IS_US, false);