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);