From 0e04044ebb2b15d99cc9d82297bc38037a3c15bb Mon Sep 17 00:00:00 2001
From: Torsten Grote <t@grobox.de>
Date: Thu, 26 Apr 2018 11:18:04 -0300
Subject: [PATCH] Ensure that incoming messages are expected in the current
 state

Previously, the introducer would process and forward invalid messages by
the introducees. This commit adds the necessary checks and tests.
---
 .../IntroducerProtocolEngine.java             | 227 ++++++++++--------
 .../briar/introduction/IntroducerSession.java |  26 +-
 .../introduction/IntroductionConstants.java   |   4 +-
 .../introduction/IntroductionManagerImpl.java |  31 ++-
 .../introduction/SessionEncoderImpl.java      |   8 +-
 .../briar/introduction/SessionParserImpl.java |  14 +-
 .../IntroductionIntegrationTest.java          | 148 +++++++++++-
 .../SessionEncoderParserIntegrationTest.java  |  28 +--
 8 files changed, 328 insertions(+), 158 deletions(-)

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 98ad2fe53c..2fbe0cab8b 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
@@ -200,21 +200,21 @@ class IntroducerProtocolEngine
 			@Nullable String message, long timestamp) throws DbException {
 		// Send REQUEST messages
 		long localTimestamp =
-				Math.max(timestamp, getLocalTimestamp(s, s.getIntroducee1()));
-		Message sent1 = sendRequestMessage(txn, s.getIntroducee1(),
-				localTimestamp, s.getIntroducee2().author, message
+				Math.max(timestamp, getLocalTimestamp(s, s.getIntroduceeA()));
+		Message sentA = sendRequestMessage(txn, s.getIntroduceeA(),
+				localTimestamp, s.getIntroduceeB().author, message
 		);
-		Message sent2 = sendRequestMessage(txn, s.getIntroducee2(),
-				localTimestamp, s.getIntroducee1().author, message
+		Message sentB = sendRequestMessage(txn, s.getIntroduceeB(),
+				localTimestamp, s.getIntroduceeA().author, message
 		);
 		// Track the messages
-		messageTracker.trackOutgoingMessage(txn, sent1);
-		messageTracker.trackOutgoingMessage(txn, sent2);
+		messageTracker.trackOutgoingMessage(txn, sentA);
+		messageTracker.trackOutgoingMessage(txn, sentB);
 		// Move to the AWAIT_RESPONSES state
-		Introducee introducee1 = new Introducee(s.getIntroducee1(), sent1);
-		Introducee introducee2 = new Introducee(s.getIntroducee2(), sent2);
+		Introducee introduceeA = new Introducee(s.getIntroduceeA(), sentA);
+		Introducee introduceeB = new Introducee(s.getIntroduceeB(), sentB);
 		return new IntroducerSession(s.getSessionId(), AWAIT_RESPONSES,
-				localTimestamp, introducee1, introducee2);
+				localTimestamp, introduceeA, introduceeB);
 	}
 
 	private IntroducerSession onRemoteAccept(Transaction txn,
@@ -225,6 +225,14 @@ class IntroducerProtocolEngine
 		// The dependency, if any, must be the last remote message
 		if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
 			return abort(txn, s);
+		// The message must be expected in the current state
+		boolean senderIsAlice = senderIsAlice(s, m);
+		if (s.getState() != AWAIT_RESPONSES) {
+			if (senderIsAlice && s.getState() != AWAIT_RESPONSE_A)
+				return abort(txn, s);
+			else if (!senderIsAlice && s.getState() != AWAIT_RESPONSE_B)
+				return abort(txn, s);
+		}
 
 		// Mark the response visible in the UI
 		markMessageVisibleInUi(txn, m.getMessageId());
@@ -240,27 +248,24 @@ class IntroducerProtocolEngine
 						m.getAcceptTimestamp(), m.getTransportProperties(),
 						false);
 
-		// Move to the next state
+		// Create the next state
 		IntroducerState state = AWAIT_AUTHS;
-		Introducee introducee1, introducee2;
-		Contact c;
-		if (i.equals(s.getIntroducee1())) {
-			if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A;
-			introducee1 = new Introducee(s.getIntroducee1(), sent);
-			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-			c = contactManager
-					.getContact(txn, s.getIntroducee2().author.getId(),
-							identityManager.getLocalAuthor(txn).getId());
-		} else if (i.equals(s.getIntroducee2())) {
+		Introducee introduceeA, introduceeB;
+		if (senderIsAlice) {
 			if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B;
-			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
-			introducee2 = new Introducee(s.getIntroducee2(), sent);
-			c = contactManager
-					.getContact(txn, s.getIntroducee1().author.getId(),
-							identityManager.getLocalAuthor(txn).getId());
-		} else throw new AssertionError();
+			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
+			introduceeB = new Introducee(s.getIntroduceeB(), sent);
+		} else {
+			if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A;
+			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,
@@ -269,8 +274,14 @@ class IntroducerProtocolEngine
 				new IntroductionResponseReceivedEvent(c.getId(), request);
 		txn.attach(e);
 
+		// Move to the next state
 		return new IntroducerSession(s.getSessionId(), state,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
+	}
+
+	private boolean senderIsAlice(IntroducerSession s,
+			AbstractIntroductionMessage m) {
+		return m.getGroupId().equals(s.getIntroduceeA().groupId);
 	}
 
 	private IntroducerSession onRemoteDecline(Transaction txn,
@@ -281,6 +292,14 @@ class IntroducerProtocolEngine
 		// The dependency, if any, must be the last remote message
 		if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
 			return abort(txn, s);
+		// The message must be expected in the current state
+		boolean senderIsAlice = senderIsAlice(s, m);
+		if (s.getState() != AWAIT_RESPONSES) {
+			if (senderIsAlice && s.getState() != AWAIT_RESPONSE_A)
+				return abort(txn, s);
+			else if (!senderIsAlice && s.getState() != AWAIT_RESPONSE_B)
+				return abort(txn, s);
+		}
 
 		// Mark the response visible in the UI
 		markMessageVisibleInUi(txn, m.getMessageId());
@@ -293,25 +312,21 @@ class IntroducerProtocolEngine
 		long timestamp = getLocalTimestamp(s, i);
 		Message sent = sendDeclineMessage(txn, i, timestamp, false);
 
-		// Move to the START state
-		Introducee introducee1, introducee2;
-		AuthorId localAuthorId =identityManager.getLocalAuthor(txn).getId();
-		Contact c;
-		if (i.equals(s.getIntroducee1())) {
-			introducee1 = new Introducee(s.getIntroducee1(), sent);
-			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-			c = contactManager
-					.getContact(txn, s.getIntroducee2().author.getId(),
-							localAuthorId);
-		} else if (i.equals(s.getIntroducee2())) {
-			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
-			introducee2 = new Introducee(s.getIntroducee2(), sent);
-			c = contactManager
-					.getContact(txn, s.getIntroducee1().author.getId(),
-							localAuthorId);
-		} else throw new AssertionError();
+		// Update introducee state
+		Introducee introduceeA, introduceeB;
+		if (senderIsAlice) {
+			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
+			introduceeB = new Introducee(s.getIntroduceeB(), sent);
+		} else {
+			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,
@@ -321,7 +336,7 @@ class IntroducerProtocolEngine
 		txn.attach(e);
 
 		return new IntroducerSession(s.getSessionId(), START,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
 	private IntroducerSession onRemoteResponseInStart(Transaction txn,
@@ -341,20 +356,20 @@ class IntroducerProtocolEngine
 				.trackMessage(txn, m.getGroupId(), m.getTimestamp(), false);
 
 		Introducee i = getIntroducee(s, m.getGroupId());
-		Introducee introducee1, introducee2;
+		Introducee introduceeA, introduceeB;
 		AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId();
 		Contact c;
-		if (i.equals(s.getIntroducee1())) {
-			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
-			introducee2 = s.getIntroducee2();
+		if (i.equals(s.getIntroduceeA())) {
+			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
+			introduceeB = s.getIntroduceeB();
 			c = contactManager
-					.getContact(txn, s.getIntroducee1().author.getId(),
+					.getContact(txn, s.getIntroduceeA().author.getId(),
 							localAuthorId);
-		} else if (i.equals(s.getIntroducee2())) {
-			introducee1 = s.getIntroducee1();
-			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
+		} else if (i.equals(s.getIntroduceeB())) {
+			introduceeA = s.getIntroduceeA();
+			introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
 			c = contactManager
-					.getContact(txn, s.getIntroducee2().author.getId(),
+					.getContact(txn, s.getIntroduceeB().author.getId(),
 							localAuthorId);
 		} else throw new AssertionError();
 
@@ -369,7 +384,7 @@ class IntroducerProtocolEngine
 		txn.attach(e);
 
 		return new IntroducerSession(s.getSessionId(), START,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
 	private IntroducerSession onRemoteAuth(Transaction txn,
@@ -377,6 +392,14 @@ class IntroducerProtocolEngine
 		// The dependency, if any, must be the last remote message
 		if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
 			return abort(txn, s);
+		// The message must be expected in the current state
+		boolean senderIsAlice = senderIsAlice(s, m);
+		if (s.getState() != AWAIT_AUTHS) {
+			if (senderIsAlice && s.getState() != AWAIT_AUTH_A)
+				return abort(txn, s);
+			else if (!senderIsAlice && s.getState() != AWAIT_AUTH_B)
+				return abort(txn, s);
+		}
 
 		// Forward AUTH message
 		Introducee i = getOtherIntroducee(s, m.getGroupId());
@@ -386,18 +409,18 @@ class IntroducerProtocolEngine
 
 		// Move to the next state
 		IntroducerState state = AWAIT_ACTIVATES;
-		Introducee introducee1, introducee2;
-		if (i.equals(s.getIntroducee1())) {
-			if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_A;
-			introducee1 = new Introducee(s.getIntroducee1(), sent);
-			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-		} else if (i.equals(s.getIntroducee2())) {
+		Introducee introduceeA, introduceeB;
+		if (senderIsAlice) {
 			if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_B;
-			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
-			introducee2 = new Introducee(s.getIntroducee2(), sent);
-		} else throw new AssertionError();
+			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
+			introduceeB = new Introducee(s.getIntroduceeB(), sent);
+		} else {
+			if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_A;
+			introduceeA = new Introducee(s.getIntroduceeA(), sent);
+			introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+		}
 		return new IntroducerSession(s.getSessionId(), state,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
 	private IntroducerSession onRemoteActivate(Transaction txn,
@@ -405,26 +428,34 @@ class IntroducerProtocolEngine
 		// The dependency, if any, must be the last remote message
 		if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
 			return abort(txn, s);
+		// The message must be expected in the current state
+		boolean senderIsAlice = senderIsAlice(s, m);
+		if (s.getState() != AWAIT_ACTIVATES) {
+			if (senderIsAlice && s.getState() != AWAIT_ACTIVATE_A)
+				return abort(txn, s);
+			else if (!senderIsAlice && s.getState() != AWAIT_ACTIVATE_B)
+				return abort(txn, s);
+		}
 
-		// Forward AUTH message
+		// Forward ACTIVATE message
 		Introducee i = getOtherIntroducee(s, m.getGroupId());
 		long timestamp = getLocalTimestamp(s, i);
 		Message sent = sendActivateMessage(txn, i, timestamp);
 
 		// Move to the next state
 		IntroducerState state = START;
-		Introducee introducee1, introducee2;
-		if (i.equals(s.getIntroducee1())) {
-			if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_A;
-			introducee1 = new Introducee(s.getIntroducee1(), sent);
-			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-		} else if (i.equals(s.getIntroducee2())) {
+		Introducee introduceeA, introduceeB;
+		if (senderIsAlice) {
 			if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_B;
-			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
-			introducee2 = new Introducee(s.getIntroducee2(), sent);
-		} else throw new AssertionError();
+			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
+			introduceeB = new Introducee(s.getIntroduceeB(), sent);
+		} else {
+			if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_A;
+			introduceeA = new Introducee(s.getIntroduceeA(), sent);
+			introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+		}
 		return new IntroducerSession(s.getSessionId(), state,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
 	private IntroducerSession onRemoteAbort(Transaction txn,
@@ -438,16 +469,16 @@ class IntroducerProtocolEngine
 		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
 
 		// Reset the session back to initial state
-		Introducee introducee1, introducee2;
-		if (i.equals(s.getIntroducee1())) {
-			introducee1 = new Introducee(s.getIntroducee1(), sent);
-			introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
-		} else if (i.equals(s.getIntroducee2())) {
-			introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
-			introducee2 = new Introducee(s.getIntroducee2(), sent);
+		Introducee introduceeA, introduceeB;
+		if (i.equals(s.getIntroduceeA())) {
+			introduceeA = new Introducee(s.getIntroduceeA(), sent);
+			introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId());
+		} else if (i.equals(s.getIntroduceeB())) {
+			introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId());
+			introduceeB = new Introducee(s.getIntroduceeB(), sent);
 		} else throw new AssertionError();
 		return new IntroducerSession(s.getSessionId(), START,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
 	private IntroducerSession abort(Transaction txn,
@@ -456,28 +487,28 @@ class IntroducerProtocolEngine
 		txn.attach(new IntroductionAbortedEvent(s.getSessionId()));
 
 		// Send an ABORT message to both introducees
-		long timestamp1 = getLocalTimestamp(s, s.getIntroducee1());
-		Message sent1 = sendAbortMessage(txn, s.getIntroducee1(), timestamp1);
-		long timestamp2 = getLocalTimestamp(s, s.getIntroducee2());
-		Message sent2 = sendAbortMessage(txn, s.getIntroducee2(), timestamp2);
+		long timestampA = getLocalTimestamp(s, s.getIntroduceeA());
+		Message sentA = sendAbortMessage(txn, s.getIntroduceeA(), timestampA);
+		long timestampB = getLocalTimestamp(s, s.getIntroduceeB());
+		Message sentB = sendAbortMessage(txn, s.getIntroduceeB(), timestampB);
 		// Reset the session back to initial state
-		Introducee introducee1 = new Introducee(s.getIntroducee1(), sent1);
-		Introducee introducee2 = new Introducee(s.getIntroducee2(), sent2);
+		Introducee introduceeA = new Introducee(s.getIntroduceeA(), sentA);
+		Introducee introduceeB = new Introducee(s.getIntroduceeB(), sentB);
 		return new IntroducerSession(s.getSessionId(), START,
-				s.getRequestTimestamp(), introducee1, introducee2);
+				s.getRequestTimestamp(), introduceeA, introduceeB);
 	}
 
 	private Introducee getIntroducee(IntroducerSession s, GroupId g) {
-		if (s.getIntroducee1().groupId.equals(g)) return s.getIntroducee1();
-		else if (s.getIntroducee2().groupId.equals(g))
-			return s.getIntroducee2();
+		if (s.getIntroduceeA().groupId.equals(g)) return s.getIntroduceeA();
+		else if (s.getIntroduceeB().groupId.equals(g))
+			return s.getIntroduceeB();
 		else throw new AssertionError();
 	}
 
 	private Introducee getOtherIntroducee(IntroducerSession s, GroupId g) {
-		if (s.getIntroducee1().groupId.equals(g)) return s.getIntroducee2();
-		else if (s.getIntroducee2().groupId.equals(g))
-			return s.getIntroducee1();
+		if (s.getIntroduceeA().groupId.equals(g)) return s.getIntroduceeB();
+		else if (s.getIntroduceeB().groupId.equals(g))
+			return s.getIntroduceeA();
 		else throw new AssertionError();
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
index 9069479692..c26eb26d98 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java
@@ -17,21 +17,21 @@ import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
 @NotNullByDefault
 class IntroducerSession extends Session<IntroducerState> {
 
-	private final Introducee introducee1, introducee2;
+	private final Introducee introduceeA, introduceeB;
 
 	IntroducerSession(SessionId sessionId, IntroducerState state,
-			long requestTimestamp, Introducee introducee1,
-			Introducee introducee2) {
+			long requestTimestamp, Introducee introduceeA,
+			Introducee introduceeB) {
 		super(sessionId, state, requestTimestamp);
-		this.introducee1 = introducee1;
-		this.introducee2 = introducee2;
+		this.introduceeA = introduceeA;
+		this.introduceeB = introduceeB;
 	}
 
-	IntroducerSession(SessionId sessionId, GroupId groupId1, Author author1,
-			GroupId groupId2, Author author2) {
+	IntroducerSession(SessionId sessionId, GroupId groupIdA, Author authorA,
+			GroupId groupIdB, Author authorB) {
 		this(sessionId, IntroducerState.START, -1,
-				new Introducee(sessionId, groupId1, author1),
-				new Introducee(sessionId, groupId2, author2));
+				new Introducee(sessionId, groupIdA, authorA),
+				new Introducee(sessionId, groupIdB, authorB));
 	}
 
 	@Override
@@ -39,12 +39,12 @@ class IntroducerSession extends Session<IntroducerState> {
 		return INTRODUCER;
 	}
 
-	Introducee getIntroducee1() {
-		return introducee1;
+	Introducee getIntroduceeA() {
+		return introduceeA;
 	}
 
-	Introducee getIntroducee2() {
-		return introducee2;
+	Introducee getIntroduceeB() {
+		return introduceeB;
 	}
 
 	@Immutable
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
index af93fa955e..afb1ff3e70 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java
@@ -23,8 +23,8 @@ interface IntroductionConstants {
 	String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
 
 	// Session Keys Introducer
-	String SESSION_KEY_INTRODUCEE_1 = "introducee1";
-	String SESSION_KEY_INTRODUCEE_2 = "introducee2";
+	String SESSION_KEY_INTRODUCEE_A = "introduceeA";
+	String SESSION_KEY_INTRODUCEE_B = "introduceeB";
 	String SESSION_KEY_GROUP_ID = "groupId";
 	String SESSION_KEY_AUTHOR = "author";
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
index bfb81a4b8d..959d1d92bd 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java
@@ -308,8 +308,15 @@ class IntroductionManagerImpl extends ConversationClientImpl
 				// This is the first request - create a new session
 				GroupId groupId1 = getContactGroup(c1).getId();
 				GroupId groupId2 = getContactGroup(c2).getId();
-				session = new IntroducerSession(sessionId, groupId1,
-						c1.getAuthor(), groupId2, c2.getAuthor());
+				boolean alice = crypto.isAlice(c1.getAuthor().getId(),
+						c2.getAuthor().getId());
+				// use fixed deterministic roles for the introducees
+				session = new IntroducerSession(sessionId,
+						alice ? groupId1 : groupId2,
+						alice ? c1.getAuthor() : c2.getAuthor(),
+						alice ? groupId2 : groupId1,
+						alice ? c2.getAuthor() : c1.getAuthor()
+				);
 				storageId = createStorageId(txn);
 			} else {
 				// An earlier request exists, so we already have a session
@@ -425,10 +432,10 @@ class IntroductionManagerImpl extends ConversationClientImpl
 			IntroducerSession session =
 					sessionParser.parseIntroducerSession(bdfSession);
 			sessionId = session.getSessionId();
-			if (contactGroupId.equals(session.getIntroducee1().groupId)) {
-				author = session.getIntroducee2().author;
+			if (contactGroupId.equals(session.getIntroduceeA().groupId)) {
+				author = session.getIntroduceeB().author;
 			} else {
-				author = session.getIntroducee1().author;
+				author = session.getIntroduceeA().author;
 			}
 		} else if (role == INTRODUCEE) {
 			IntroduceeSession session = sessionParser
@@ -465,10 +472,10 @@ class IntroductionManagerImpl extends ConversationClientImpl
 					sessionParser.parseIntroducerSession(bdfSession);
 			sessionId = session.getSessionId();
 			LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
-			if (localAuthor.equals(session.getIntroducee1().author)) {
-				author = session.getIntroducee2().author;
+			if (localAuthor.equals(session.getIntroduceeA().author)) {
+				author = session.getIntroduceeB().author;
 			} else {
-				author = session.getIntroducee1().author;
+				author = session.getIntroduceeA().author;
 			}
 		} else if (role == INTRODUCEE) {
 			IntroduceeSession session = sessionParser
@@ -516,12 +523,12 @@ class IntroductionManagerImpl extends ConversationClientImpl
 			} catch (FormatException e) {
 				throw new AssertionError();
 			}
-			if (s.getIntroducee1().author.equals(c.getAuthor())) {
+			if (s.getIntroduceeA().author.equals(c.getAuthor())) {
 				abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
-						s.getIntroducee2(), localAuthor);
-			} else if (s.getIntroducee2().author.equals(c.getAuthor())) {
+						s.getIntroduceeB(), localAuthor);
+			} else if (s.getIntroduceeB().author.equals(c.getAuthor())) {
 				abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
-						s.getIntroducee1(), localAuthor);
+						s.getIntroduceeA(), localAuthor);
 			}
 		}
 	}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java
index 9cf17a2d0f..2ffee61d88 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java
@@ -23,8 +23,8 @@ import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID;
-import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
-import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
@@ -72,8 +72,8 @@ class SessionEncoderImpl implements SessionEncoder {
 	@Override
 	public BdfDictionary encodeIntroducerSession(IntroducerSession s) {
 		BdfDictionary d = encodeSession(s);
-		d.put(SESSION_KEY_INTRODUCEE_1, encodeIntroducee(s.getIntroducee1()));
-		d.put(SESSION_KEY_INTRODUCEE_2, encodeIntroducee(s.getIntroducee2()));
+		d.put(SESSION_KEY_INTRODUCEE_A, encodeIntroducee(s.getIntroduceeA()));
+		d.put(SESSION_KEY_INTRODUCEE_B, encodeIntroducee(s.getIntroduceeB()));
 		return d;
 	}
 
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
index f269b49730..7c5b219259 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java
@@ -27,8 +27,8 @@ import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID;
-import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
-import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
@@ -75,12 +75,12 @@ class SessionParserImpl implements SessionParser {
 		SessionId sessionId = getSessionId(d);
 		IntroducerState state = IntroducerState.fromValue(getState(d));
 		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
-		Introducee introducee1 = parseIntroducee(sessionId,
-				d.getDictionary(SESSION_KEY_INTRODUCEE_1));
-		Introducee introducee2 = parseIntroducee(sessionId,
-				d.getDictionary(SESSION_KEY_INTRODUCEE_2));
+		Introducee introduceeA = parseIntroducee(sessionId,
+				d.getDictionary(SESSION_KEY_INTRODUCEE_A));
+		Introducee introduceeB = parseIntroducee(sessionId,
+				d.getDictionary(SESSION_KEY_INTRODUCEE_B));
 		return new IntroducerSession(sessionId, state, requestTimestamp,
-				introducee1, introducee2);
+				introduceeA, introduceeB);
 	}
 
 	private Introducee parseIntroducee(SessionId sessionId, BdfDictionary d)
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 816957dd4a..1adfb9207e 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
@@ -54,11 +54,12 @@ import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT
 import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION;
 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_1;
-import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A;
+import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
 import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID;
 import static org.briarproject.briar.introduction.MessageType.ACCEPT;
+import static org.briarproject.briar.introduction.MessageType.AUTH;
 import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -610,6 +611,123 @@ public class IntroductionIntegrationTest
 		assertFalse(listener2.aborted);
 	}
 
+	/**
+	 * One introducee illegally sends two ACCEPT messages in a row.
+	 * The introducer should notice this and ABORT the session.
+	 */
+	@Test
+	public void testDoubleAccept() throws Exception {
+		addListeners(true, true);
+
+		// make the introduction
+		long time = clock.currentTimeMillis();
+		introductionManager0
+				.makeIntroduction(contact1From0, contact2From0, null, time);
+
+		// sync REQUEST to introducee1
+		sync0To1(1, true);
+
+		// save ACCEPT from introducee1
+		AcceptMessage m = (AcceptMessage) getMessageFor(c1.getClientHelper(),
+				contact0From1, ACCEPT);
+
+		// sync ACCEPT back to introducer
+		sync1To0(1, true);
+
+		// fake a second ACCEPT message from introducee1
+		Message msg = c1.getMessageEncoder()
+				.encodeAcceptMessage(m.getGroupId(), clock.currentTimeMillis(),
+						m.getMessageId(), m.getSessionId(),
+						m.getEphemeralPublicKey(), m.getAcceptTimestamp(),
+						m.getTransportProperties());
+		c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true);
+
+		// sync fake ACCEPT back to introducer
+		sync1To0(1, true);
+
+		assertTrue(listener0.aborted);
+	}
+
+	/**
+	 * One introducee sends an ACCEPT and then another DECLINE message.
+	 * The introducer should notice this and ABORT the session.
+	 */
+	@Test
+	public void testAcceptAndDecline() throws Exception {
+		addListeners(true, true);
+
+		// make the introduction
+		long time = clock.currentTimeMillis();
+		introductionManager0
+				.makeIntroduction(contact1From0, contact2From0, null, time);
+
+		// sync REQUEST to introducee1
+		sync0To1(1, true);
+
+		// save ACCEPT from introducee1
+		AcceptMessage m = (AcceptMessage) getMessageFor(c1.getClientHelper(),
+				contact0From1, ACCEPT);
+
+		// sync ACCEPT back to introducer
+		sync1To0(1, true);
+
+		// fake a second DECLINE message also from introducee1
+		Message msg = c1.getMessageEncoder()
+				.encodeDeclineMessage(m.getGroupId(), clock.currentTimeMillis(),
+						m.getMessageId(), m.getSessionId());
+		c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true);
+
+		// sync fake DECLINE back to introducer
+		sync1To0(1, true);
+
+		assertTrue(listener0.aborted);
+	}
+
+	/**
+	 * One introducee sends two AUTH messages.
+	 * The introducer should notice this and ABORT the session.
+	 */
+	@Test
+	public void testDoubleAuth() throws Exception {
+		addListeners(true, true);
+
+		// make the introduction
+		long time = clock.currentTimeMillis();
+		introductionManager0
+				.makeIntroduction(contact1From0, contact2From0, null, time);
+
+		// sync REQUEST messages
+		sync0To1(1, true);
+		sync0To2(1, true);
+
+		// sync ACCEPT messages
+		sync1To0(1, true);
+		sync2To0(1, true);
+
+		// sync forwarded ACCEPT messages to introducees
+		sync0To1(1, true);
+		sync0To2(1, true);
+
+		// save AUTH from introducee1
+		AuthMessage m = (AuthMessage) getMessageFor(c1.getClientHelper(),
+				contact0From1, AUTH);
+
+		// sync first AUTH message
+		sync1To0(1, true);
+
+		// fake a second AUTH message also from introducee1
+		Message msg = c1.getMessageEncoder()
+				.encodeAuthMessage(m.getGroupId(), clock.currentTimeMillis(),
+						m.getMessageId(), m.getSessionId(), m.getMac(),
+						m.getSignature());
+		c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true);
+
+		// sync second AUTH message
+		sync1To0(1, true);
+
+		assertTrue(listener0.aborted);
+	}
+
 	@Test
 	public void testIntroducerRemovedCleanup() throws Exception {
 		addListeners(true, true);
@@ -955,8 +1073,8 @@ public class IntroductionIntegrationTest
 
 	private void replacePreviousLocalMessageId(Author author,
 			BdfDictionary d, MessageId id) throws FormatException {
-		BdfDictionary i1 = d.getDictionary(SESSION_KEY_INTRODUCEE_1);
-		BdfDictionary i2 = d.getDictionary(SESSION_KEY_INTRODUCEE_2);
+		BdfDictionary i1 = d.getDictionary(SESSION_KEY_INTRODUCEE_A);
+		BdfDictionary i2 = d.getDictionary(SESSION_KEY_INTRODUCEE_B);
 		Author a1 = clientHelper
 				.parseAndValidateAuthor(i1.getList(SESSION_KEY_AUTHOR));
 		Author a2 = clientHelper
@@ -964,10 +1082,10 @@ public class IntroductionIntegrationTest
 
 		if (a1.equals(author)) {
 			i1.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id);
-			d.put(SESSION_KEY_INTRODUCEE_1, i1);
+			d.put(SESSION_KEY_INTRODUCEE_A, i1);
 		} else if (a2.equals(author)) {
 			i2.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id);
-			d.put(SESSION_KEY_INTRODUCEE_2, i2);
+			d.put(SESSION_KEY_INTRODUCEE_B, i2);
 		} else {
 			throw new AssertionError();
 		}
@@ -986,8 +1104,13 @@ public class IntroductionIntegrationTest
 		MessageId id = map.entrySet().iterator().next().getKey();
 		Message m = ch.getMessage(id);
 		BdfList body = ch.getMessageAsList(id);
-		//noinspection ConstantConditions
-		return c0.getMessageParser().parseAcceptMessage(m, body);
+		if (type == ACCEPT) {
+			//noinspection ConstantConditions
+			return c0.getMessageParser().parseAcceptMessage(m, body);
+		} else if (type == AUTH) {
+			//noinspection ConstantConditions
+			return c0.getMessageParser().parseAuthMessage(m, body);
+		} else throw new AssertionError("Not implemented");
 	}
 
 	private IntroductionRequest getIntroductionRequest(
@@ -1002,6 +1125,15 @@ public class IntroductionIntegrationTest
 		throw new AssertionError("No IntroductionRequest found");
 	}
 
+	private IntroducerSession getIntroducerSession()
+			throws DbException, FormatException {
+		Map<MessageId, BdfDictionary> dicts = c0.getClientHelper()
+				.getMessageMetadataAsDictionary(getLocalGroup().getId());
+		assertEquals(1, dicts.size());
+		BdfDictionary d = dicts.values().iterator().next();
+		return c0.getSessionParser().parseIntroducerSession(d);
+	}
+
 	private IntroduceeSession getIntroduceeSession(ClientHelper ch,
 			GroupId introducerGroup) throws DbException, FormatException {
 		Map<MessageId, BdfDictionary> dicts =
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
index 00bec26a83..9d773bac0c 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java
@@ -102,8 +102,8 @@ public class SessionEncoderParserIntegrationTest extends BrambleTestCase {
 		assertEquals(s1.getSessionId(), s2.getSessionId());
 		assertEquals(AWAIT_AUTHS, s1.getState());
 		assertEquals(s1.getState(), s2.getState());
-		assertIntroduceeEquals(s1.getIntroducee1(), s2.getIntroducee1());
-		assertIntroduceeEquals(s1.getIntroducee2(), s2.getIntroducee2());
+		assertIntroduceeEquals(s1.getIntroduceeA(), s2.getIntroduceeA());
+		assertIntroduceeEquals(s1.getIntroduceeB(), s2.getIntroduceeB());
 	}
 
 	@Test
@@ -121,19 +121,19 @@ public class SessionEncoderParserIntegrationTest extends BrambleTestCase {
 		BdfDictionary d = sessionEncoder.encodeIntroducerSession(s1);
 		IntroducerSession s2 = sessionParser.parseIntroducerSession(d);
 
-		assertNull(s1.getIntroducee1().lastLocalMessageId);
-		assertEquals(s1.getIntroducee1().lastLocalMessageId,
-				s2.getIntroducee1().lastLocalMessageId);
-		assertNull(s1.getIntroducee1().lastRemoteMessageId);
-		assertEquals(s1.getIntroducee1().lastRemoteMessageId,
-				s2.getIntroducee1().lastRemoteMessageId);
+		assertNull(s1.getIntroduceeA().lastLocalMessageId);
+		assertEquals(s1.getIntroduceeA().lastLocalMessageId,
+				s2.getIntroduceeA().lastLocalMessageId);
+		assertNull(s1.getIntroduceeA().lastRemoteMessageId);
+		assertEquals(s1.getIntroduceeA().lastRemoteMessageId,
+				s2.getIntroduceeA().lastRemoteMessageId);
 
-		assertNull(s1.getIntroducee2().lastLocalMessageId);
-		assertEquals(s1.getIntroducee2().lastLocalMessageId,
-				s2.getIntroducee2().lastLocalMessageId);
-		assertNull(s1.getIntroducee2().lastRemoteMessageId);
-		assertEquals(s1.getIntroducee2().lastRemoteMessageId,
-				s2.getIntroducee2().lastRemoteMessageId);
+		assertNull(s1.getIntroduceeB().lastLocalMessageId);
+		assertEquals(s1.getIntroduceeB().lastLocalMessageId,
+				s2.getIntroduceeB().lastLocalMessageId);
+		assertNull(s1.getIntroduceeB().lastRemoteMessageId);
+		assertEquals(s1.getIntroduceeB().lastRemoteMessageId,
+				s2.getIntroduceeB().lastRemoteMessageId);
 	}
 
 	@Test(expected = FormatException.class)
-- 
GitLab