From f8f98ed95dbd14845d931e31bd0b4951da44428b Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Thu, 26 Apr 2018 18:00:57 -0300
Subject: [PATCH] Properly handle DECLINE messages in START state

Previously, DECLINE messages let directly to the START state
for introducer and introducees.
So incoming ACCEPT and DECLINE messages needed to be ignored in START state
introducing undefined behavior into the protocol.

This is fixed with this commit by adding two additional states
to the introducer state machine as well as making use of the existing
LOCAL_DECLINED state for the introducees.
---
 .../introduction/AbstractProtocolEngine.java  | 19 +++++
 .../IntroduceeProtocolEngine.java             | 76 ++++++++---------
 .../briar/introduction/IntroduceeSession.java |  4 +-
 .../IntroducerProtocolEngine.java             | 83 +++++++------------
 .../briar/introduction/IntroducerState.java   |  9 +-
 .../IntroductionIntegrationTest.java          | 41 ++++++---
 .../IntroductionIntegrationTestComponent.java |  1 +
 7 files changed, 123 insertions(+), 110 deletions(-)

diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
index 75af1b87ca..c9002bb0e6 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java
@@ -3,12 +3,14 @@ package org.briarproject.briar.introduction;
 import org.briarproject.bramble.api.FormatException;
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.client.ContactGroupFactory;
+import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.data.BdfDictionary;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorId;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.plugin.TransportId;
@@ -18,6 +20,8 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction.IntroductionResponse;
+import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
 
 import java.util.Map;
 
@@ -141,6 +145,21 @@ abstract class AbstractProtocolEngine<S extends Session>
 		}
 	}
 
+	void broadcastIntroductionResponseReceivedEvent(Transaction txn,
+			Session s, AuthorId sender, AbstractIntroductionMessage m)
+			throws DbException {
+		AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
+		Contact c = contactManager.getContact(txn, sender, localAuthorId);
+		IntroductionResponse response =
+				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
+						m.getGroupId(), s.getRole(), m.getTimestamp(), false,
+						false, false, false, c.getAuthor().getName(),
+						m instanceof AcceptMessage);
+		IntroductionResponseReceivedEvent e =
+				new IntroductionResponseReceivedEvent(c.getId(), response);
+		txn.attach(e);
+	}
+
 	void markMessageVisibleInUi(Transaction txn, MessageId m)
 			throws DbException {
 		BdfDictionary meta = new BdfDictionary();
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
index ce7d5f76c2..df0a719005 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java
@@ -27,10 +27,8 @@ import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
 import org.briarproject.briar.api.client.SessionId;
 import org.briarproject.briar.api.introduction.IntroductionRequest;
-import org.briarproject.briar.api.introduction.IntroductionResponse;
 import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
 import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent;
-import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
 import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent;
 
 import java.security.GeneralSecurityException;
@@ -44,7 +42,9 @@ import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
 import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_AUTH;
 import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES;
 import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED;
 import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_ACCEPTED;
+import static org.briarproject.briar.introduction.IntroduceeState.START;
 
 @Immutable
 @NotNullByDefault
@@ -142,12 +142,12 @@ class IntroduceeProtocolEngine
 	public IntroduceeSession onAcceptMessage(Transaction txn,
 			IntroduceeSession session, AcceptMessage m) throws DbException {
 		switch (session.getState()) {
-			case START:
-				return onRemoteResponseInStart(txn, session, m);
+			case LOCAL_DECLINED:
+				return onRemoteResponseWhenDeclined(txn, session, m);
 			case AWAIT_RESPONSES:
 			case LOCAL_ACCEPTED:
 				return onRemoteAccept(txn, session, m);
-			case LOCAL_DECLINED:
+			case START:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
 			case AWAIT_ACTIVATE:
@@ -161,12 +161,12 @@ class IntroduceeProtocolEngine
 	public IntroduceeSession onDeclineMessage(Transaction txn,
 			IntroduceeSession session, DeclineMessage m) throws DbException {
 		switch (session.getState()) {
-			case START:
-				return onRemoteResponseInStart(txn, session, m);
-			case AWAIT_RESPONSES:
 			case LOCAL_DECLINED:
+				return onRemoteResponseWhenDeclined(txn, session, m);
+			case AWAIT_RESPONSES:
 			case LOCAL_ACCEPTED:
 				return onRemoteDecline(txn, session, m);
+			case START:
 			case REMOTE_ACCEPTED:
 			case AWAIT_AUTH:
 			case AWAIT_ACTIVATE:
@@ -255,8 +255,7 @@ class IntroduceeProtocolEngine
 	}
 
 	private IntroduceeSession onLocalAccept(Transaction txn,
-			IntroduceeSession s, long timestamp)
-			throws DbException {
+			IntroduceeSession s, long timestamp) throws DbException {
 		// Mark the request message unavailable to answer
 		markRequestsUnavailableToAnswer(txn, s);
 
@@ -291,20 +290,23 @@ class IntroduceeProtocolEngine
 	}
 
 	private IntroduceeSession onLocalDecline(Transaction txn,
-			IntroduceeSession s, long timestamp)
-			throws DbException {
+			IntroduceeSession s, long timestamp) throws DbException {
 		// Mark the request message unavailable to answer
 		markRequestsUnavailableToAnswer(txn, s);
 
 		// Send a DECLINE message
 		long localTimestamp = Math.max(timestamp + 1, getLocalTimestamp(s));
 		Message sent = sendDeclineMessage(txn, s, localTimestamp, true);
+
 		// Track the message
 		messageTracker.trackOutgoingMessage(txn, sent);
 
-		// Move to the START state
-		return IntroduceeSession.clear(s, sent.getId(), sent.getTimestamp(),
-				s.getLastRemoteMessageId());
+		// Move to the START or LOCAL_DECLINED state, if still awaiting response
+		IntroduceeState state =
+				s.getState() == REMOTE_ACCEPTED ? START : LOCAL_DECLINED;
+		return IntroduceeSession
+				.clear(s, state, sent.getId(), sent.getTimestamp(),
+						s.getLastRemoteMessageId());
 	}
 
 	private IntroduceeSession onRemoteAccept(Transaction txn,
@@ -347,25 +349,17 @@ class IntroduceeProtocolEngine
 				.trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
 
 		// Broadcast IntroductionResponseReceivedEvent
-		Contact c = contactManager.getContact(txn, s.getIntroducer().getId(),
-				identityManager.getLocalAuthor(txn).getId());
-		IntroductionResponse request =
-				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
-						m.getGroupId(), INTRODUCEE, m.getTimestamp(), false,
-						false, false, false, s.getRemote().author.getName(),
-						false);
-		IntroductionResponseReceivedEvent e =
-				new IntroductionResponseReceivedEvent(c.getId(), request);
-		txn.attach(e);
+		broadcastIntroductionResponseReceivedEvent(txn, s,
+				s.getIntroducer().getId(), m);
 
 		// Move back to START state
-		return IntroduceeSession
-				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
-						m.getMessageId());
+		return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
+				s.getLocalTimestamp(), m.getMessageId());
 	}
 
-	private IntroduceeSession onRemoteResponseInStart(Transaction txn,
-			IntroduceeSession s, AbstractIntroductionMessage m) throws DbException {
+	private IntroduceeSession onRemoteResponseWhenDeclined(Transaction txn,
+			IntroduceeSession s, AbstractIntroductionMessage m)
+			throws DbException {
 		// The timestamp must be higher than the last request message
 		if (m.getTimestamp() <= s.getRequestTimestamp())
 			return abort(txn, s);
@@ -373,10 +367,9 @@ class IntroduceeProtocolEngine
 		if (isInvalidDependency(s, m.getPreviousMessageId()))
 			return abort(txn, s);
 
-		// Stay in START state
-		return IntroduceeSession
-				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
-						m.getMessageId());
+		// Move to START state
+		return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
+				s.getLocalTimestamp(), m.getMessageId());
 	}
 
 	private IntroduceeSession onLocalAuth(Transaction txn, IntroduceeSession s)
@@ -479,9 +472,8 @@ class IntroduceeProtocolEngine
 		keyManager.activateKeys(txn, s.getTransportKeys());
 
 		// Move back to START state
-		return IntroduceeSession
-				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
-						m.getMessageId());
+		return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
+				s.getLocalTimestamp(), m.getMessageId());
 	}
 
 	private IntroduceeSession onRemoteAbort(Transaction txn,
@@ -494,9 +486,8 @@ class IntroduceeProtocolEngine
 		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
 
 		// Reset the session back to initial state
-		return IntroduceeSession
-				.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
-						m.getMessageId());
+		return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(),
+				s.getLocalTimestamp(), m.getMessageId());
 	}
 
 	private IntroduceeSession abort(Transaction txn, IntroduceeSession s)
@@ -511,8 +502,9 @@ class IntroduceeProtocolEngine
 		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
 
 		// Reset the session back to initial state
-		return IntroduceeSession.clear(s, sent.getId(), sent.getTimestamp(),
-				s.getLastRemoteMessageId());
+		return IntroduceeSession
+				.clear(s, START, sent.getId(), sent.getTimestamp(),
+						s.getLastRemoteMessageId());
 	}
 
 	private boolean isInvalidDependency(IntroduceeSession s,
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
index 92b06abc05..b952b3dc52 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java
@@ -121,7 +121,7 @@ class IntroduceeSession extends Session<IntroduceeState>
 				remote, null, transportKeys);
 	}
 
-	static IntroduceeSession clear(IntroduceeSession s,
+	static IntroduceeSession clear(IntroduceeSession s, IntroduceeState state,
 			@Nullable MessageId lastLocalMessageId, long localTimestamp,
 			@Nullable MessageId lastRemoteMessageId) {
 		Local local =
@@ -130,7 +130,7 @@ class IntroduceeSession extends Session<IntroduceeState>
 		Remote remote =
 				new Remote(s.remote.alice, s.remote.author, lastRemoteMessageId,
 						null, null, -1, null);
-		return new IntroduceeSession(s.getSessionId(), START,
+		return new IntroduceeSession(s.getSessionId(), state,
 				s.getRequestTimestamp(), s.contactGroupId, s.introducer, local,
 				remote, null, null);
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
index 7115ec0892..8aeb28277a 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java
@@ -2,12 +2,11 @@ package org.briarproject.briar.introduction;
 
 import org.briarproject.bramble.api.client.ClientHelper;
 import org.briarproject.bramble.api.client.ContactGroupFactory;
-import org.briarproject.bramble.api.contact.Contact;
 import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
-import org.briarproject.bramble.api.identity.AuthorId;
+import org.briarproject.bramble.api.identity.Author;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.sync.GroupId;
@@ -16,16 +15,13 @@ import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.api.system.Clock;
 import org.briarproject.briar.api.client.MessageTracker;
 import org.briarproject.briar.api.client.ProtocolStateException;
-import org.briarproject.briar.api.introduction.IntroductionResponse;
 import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent;
-import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent;
 import org.briarproject.briar.introduction.IntroducerSession.Introducee;
 
 import javax.annotation.Nullable;
 import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
-import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATES;
 import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATE_A;
 import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATE_B;
@@ -35,6 +31,8 @@ import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTH_B;
 import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSES;
 import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_A;
 import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_B;
+import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED;
+import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED;
 import static org.briarproject.briar.introduction.IntroducerState.START;
 
 @Immutable
@@ -68,6 +66,8 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSES:
 			case AWAIT_RESPONSE_A:
 			case AWAIT_RESPONSE_B:
+			case A_DECLINED:
+			case B_DECLINED:
 			case AWAIT_AUTHS:
 			case AWAIT_AUTH_A:
 			case AWAIT_AUTH_B:
@@ -111,8 +111,10 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSE_A:
 			case AWAIT_RESPONSE_B:
 				return onRemoteAccept(txn, s, m);
+			case A_DECLINED:
+			case B_DECLINED:
+				return onRemoteResponseWhenDeclined(txn, s, m);
 			case START:
-				return onRemoteResponseInStart(txn, s, m);
 			case AWAIT_AUTHS:
 			case AWAIT_AUTH_A:
 			case AWAIT_AUTH_B:
@@ -133,8 +135,10 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSE_A:
 			case AWAIT_RESPONSE_B:
 				return onRemoteDecline(txn, s, m);
+			case A_DECLINED:
+			case B_DECLINED:
+				return onRemoteResponseWhenDeclined(txn, s, m);
 			case START:
-				return onRemoteResponseInStart(txn, s, m);
 			case AWAIT_AUTHS:
 			case AWAIT_AUTH_A:
 			case AWAIT_AUTH_B:
@@ -159,6 +163,8 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSES:
 			case AWAIT_RESPONSE_A:
 			case AWAIT_RESPONSE_B:
+			case A_DECLINED:
+			case B_DECLINED:
 			case AWAIT_ACTIVATES:
 			case AWAIT_ACTIVATE_A:
 			case AWAIT_ACTIVATE_B:
@@ -180,6 +186,8 @@ class IntroducerProtocolEngine
 			case AWAIT_RESPONSES:
 			case AWAIT_RESPONSE_A:
 			case AWAIT_RESPONSE_B:
+			case A_DECLINED:
+			case B_DECLINED:
 			case AWAIT_AUTHS:
 			case AWAIT_AUTH_A:
 			case AWAIT_AUTH_B:
@@ -262,17 +270,8 @@ class IntroducerProtocolEngine
 		}
 
 		// Broadcast IntroductionResponseReceivedEvent
-		AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
-		Contact c = contactManager.getContact(txn,
-				senderIsAlice ? introduceeA.author.getId() :
-						introduceeB.author.getId(), localAuthorId);
-		IntroductionResponse request =
-				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
-						m.getGroupId(), INTRODUCER, m.getTimestamp(), false,
-						false, false, false, c.getAuthor().getName(), true);
-		IntroductionResponseReceivedEvent e =
-				new IntroductionResponseReceivedEvent(c.getId(), request);
-		txn.attach(e);
+		Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
+		broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
 
 		// Move to the next state
 		return new IntroducerSession(s.getSessionId(), state,
@@ -312,34 +311,28 @@ class IntroducerProtocolEngine
 		long timestamp = getLocalTimestamp(s, i);
 		Message sent = sendDeclineMessage(txn, i, timestamp, false);
 
-		// Update introducee state
+		// Create the next state
+		IntroducerState state = START;
 		Introducee introduceeA, introduceeB;
 		if (senderIsAlice) {
+			if (s.getState() == AWAIT_RESPONSES) state = A_DECLINED;
 			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
 			introduceeB = new Introducee(s.getIntroduceeB(), sent);
 		} else {
+			if (s.getState() == AWAIT_RESPONSES) state = B_DECLINED;
 			introduceeA = new Introducee(s.getIntroduceeA(), sent);
 			introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
 		}
 
 		// Broadcast IntroductionResponseReceivedEvent
-		AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
-		Contact c = contactManager.getContact(txn,
-				senderIsAlice ? introduceeA.author.getId() :
-						introduceeB.author.getId(), localAuthorId);
-		IntroductionResponse request =
-				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
-						m.getGroupId(), INTRODUCER, m.getTimestamp(), false,
-						false, false, false, c.getAuthor().getName(), false);
-		IntroductionResponseReceivedEvent e =
-				new IntroductionResponseReceivedEvent(c.getId(), request);
-		txn.attach(e);
+		Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
+		broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
 
-		return new IntroducerSession(s.getSessionId(), START,
+		return new IntroducerSession(s.getSessionId(), state,
 				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
-	private IntroducerSession onRemoteResponseInStart(Transaction txn,
+	private IntroducerSession onRemoteResponseWhenDeclined(Transaction txn,
 			IntroducerSession s, AbstractIntroductionMessage m)
 			throws DbException {
 		// The timestamp must be higher than the last request message
@@ -355,33 +348,19 @@ class IntroducerProtocolEngine
 		messageTracker
 				.trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
 
-		Introducee i = getIntroducee(s, m.getGroupId());
+		boolean senderIsAlice = senderIsAlice(s, m);
 		Introducee introduceeA, introduceeB;
-		AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
-		Contact c;
-		if (i.equals(s.getIntroduceeA())) {
+		if (senderIsAlice) {
 			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
 			introduceeB = s.getIntroduceeB();
-			c = contactManager
-					.getContact(txn, s.getIntroduceeA().author.getId(),
-							localAuthorId);
-		} else if (i.equals(s.getIntroduceeB())) {
+		} else {
 			introduceeA = s.getIntroduceeA();
 			introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
-			c = contactManager
-					.getContact(txn, s.getIntroduceeB().author.getId(),
-							localAuthorId);
-		} else throw new AssertionError();
+		}
 
 		// Broadcast IntroductionResponseReceivedEvent
-		IntroductionResponse request =
-				new IntroductionResponse(s.getSessionId(), m.getMessageId(),
-						m.getGroupId(), INTRODUCER, m.getTimestamp(), false,
-						false, false, false, c.getAuthor().getName(),
-						m instanceof AcceptMessage);
-		IntroductionResponseReceivedEvent e =
-				new IntroductionResponseReceivedEvent(c.getId(), request);
-		txn.attach(e);
+		Author sender = senderIsAlice ? introduceeA.author : introduceeB.author;
+		broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m);
 
 		return new IntroducerSession(s.getSessionId(), START,
 				s.getRequestTimestamp(), introduceeA, introduceeB);
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java
index 99c3fbf86c..6514eca16f 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java
@@ -12,10 +12,11 @@ enum IntroducerState implements State {
 	START(0),
 	AWAIT_RESPONSES(1),
 	AWAIT_RESPONSE_A(2), AWAIT_RESPONSE_B(3),
-	AWAIT_AUTHS(4),
-	AWAIT_AUTH_A(5), AWAIT_AUTH_B(6),
-	AWAIT_ACTIVATES(7),
-	AWAIT_ACTIVATE_A(8), AWAIT_ACTIVATE_B(9);
+	A_DECLINED(4), B_DECLINED(5),
+	AWAIT_AUTHS(6),
+	AWAIT_AUTH_A(7), AWAIT_AUTH_B(8),
+	AWAIT_ACTIVATES(9),
+	AWAIT_ACTIVATE_A(10), AWAIT_ACTIVATE_B(11);
 
 	private final int value;
 
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index 3dccbbc8de..3c602a2e3f 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -20,7 +20,6 @@ import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.properties.TransportProperties;
 import org.briarproject.bramble.api.properties.TransportPropertyManager;
 import org.briarproject.bramble.api.sync.Group;
-import org.briarproject.bramble.api.sync.GroupId;
 import org.briarproject.bramble.api.sync.Message;
 import org.briarproject.bramble.api.sync.MessageId;
 import org.briarproject.bramble.test.TestDatabaseModule;
@@ -52,6 +51,10 @@ import static org.briarproject.bramble.test.TestUtils.getTransportProperties;
 import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
+import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED;
+import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED;
+import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED;
+import static org.briarproject.briar.introduction.IntroducerState.START;
 import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
@@ -171,8 +174,7 @@ public class IntroductionIntegrationTest
 		sync0To2(1, true);
 
 		// assert that introducee2 did add the transport keys
-		IntroduceeSession session2 = getIntroduceeSession(c2.getClientHelper(),
-				introductionManager2.getContactGroup(contact0From2).getId());
+		IntroduceeSession session2 = getIntroduceeSession(c2);
 		assertNotNull(session2.getTransportKeys());
 		assertFalse(session2.getTransportKeys().isEmpty());
 
@@ -181,8 +183,7 @@ public class IntroductionIntegrationTest
 		sync0To1(2, true);
 
 		// assert that introducee1 really purged the key material
-		IntroduceeSession session1 = getIntroduceeSession(c1.getClientHelper(),
-				introductionManager1.getContactGroup(contact0From1).getId());
+		IntroduceeSession session1 = getIntroduceeSession(c1);
 		assertNull(session1.getMasterKey());
 		assertNull(session1.getLocal().ephemeralPrivateKey);
 		assertNull(session1.getTransportKeys());
@@ -240,16 +241,32 @@ public class IntroductionIntegrationTest
 		assertTrue(listener1.requestReceived);
 		assertTrue(listener2.requestReceived);
 
+		// assert that introducee is in correct state
+		IntroduceeSession introduceeSession = getIntroduceeSession(c1);
+		assertEquals(LOCAL_DECLINED, introduceeSession.getState());
+
 		// sync first response
 		sync1To0(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.response1Received);
 
+		// assert that introducer is in correct state
+		boolean alice = c0.getIntroductionCrypto()
+				.isAlice(introducee1.getAuthor().getId(),
+						introducee2.getAuthor().getId());
+		IntroducerSession introducerSession = getIntroducerSession();
+		assertEquals(alice ? A_DECLINED : B_DECLINED,
+				introducerSession.getState());
+
 		// sync second response
 		sync2To0(1, true);
 		eventWaiter.await(TIMEOUT, 1);
 		assertTrue(listener0.response2Received);
 
+		// assert that introducer now moved to START state
+		introducerSession = getIntroducerSession();
+		assertEquals(START, introducerSession.getState());
+
 		// sync first forwarded response
 		sync0To2(1, true);
 
@@ -1114,13 +1131,17 @@ public class IntroductionIntegrationTest
 		return c0.getSessionParser().parseIntroducerSession(d);
 	}
 
-	private IntroduceeSession getIntroduceeSession(ClientHelper ch,
-			GroupId introducerGroup) throws DbException, FormatException {
-		Map<MessageId, BdfDictionary> dicts =
-				ch.getMessageMetadataAsDictionary(getLocalGroup().getId());
+	private IntroduceeSession getIntroduceeSession(
+			IntroductionIntegrationTestComponent c)
+			throws DbException, FormatException {
+		Map<MessageId, BdfDictionary> dicts = c.getClientHelper()
+				.getMessageMetadataAsDictionary(getLocalGroup().getId());
 		assertEquals(1, dicts.size());
 		BdfDictionary d = dicts.values().iterator().next();
-		return c0.getSessionParser().parseIntroduceeSession(introducerGroup, d);
+		Group introducerGroup =
+				introductionManager2.getContactGroup(contact0From2);
+		return c.getSessionParser()
+				.parseIntroduceeSession(introducerGroup.getId(), d);
 	}
 
 	private Group getLocalGroup() {
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
index b8d5dedaa4..afd6d4394a 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java
@@ -62,5 +62,6 @@ interface IntroductionIntegrationTestComponent
 	MessageEncoder getMessageEncoder();
 	MessageParser getMessageParser();
 	SessionParser getSessionParser();
+	IntroductionCrypto getIntroductionCrypto();
 
 }
-- 
GitLab