diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/Role.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/Role.java
new file mode 100644
index 0000000000000000000000000000000000000000..bc7e4e3d907b92ca717e59519ea509c1a38222fb
--- /dev/null
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/Role.java
@@ -0,0 +1,29 @@
+package org.briarproject.briar.api.introduction2;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+public enum Role {
+
+	INTRODUCER(0), INTRODUCEE(1);
+
+	private final int value;
+
+	Role(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public static Role fromValue(int value) throws FormatException {
+		for (Role r : values()) if (r.value == value) return r;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..61e3014981034076c1a8cb92573af37f1358664f
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeSession.java
@@ -0,0 +1,227 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+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.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction2.Role;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_ACTIVATE;
+import static org.briarproject.briar.introduction2.IntroduceeState.START;
+import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
+
+@Immutable
+@NotNullByDefault
+class IntroduceeSession extends Session<IntroduceeState>
+		implements PeerSession {
+
+	private final GroupId contactGroupId;
+	private final long localTimestamp, acceptTimestamp, remoteAcceptTimestamp;
+	@Nullable
+	private final MessageId lastLocalMessageId, lastRemoteMessageId;
+	private final Author introducer, remoteAuthor;
+	@Nullable
+	private final byte[] ephemeralPublicKey, ephemeralPrivateKey;
+	@Nullable
+	private final byte[] masterKey, remoteEphemeralPublicKey;
+	@Nullable
+	private final Map<TransportId, TransportProperties> transportProperties;
+	@Nullable
+	private final Map<TransportId, TransportProperties>
+			remoteTransportProperties;
+	@Nullable
+	private final Map<TransportId, KeySetId> transportKeys;
+
+	IntroduceeSession(SessionId sessionId, IntroduceeState state,
+			long requestTimestamp, GroupId contactGroupId,
+			@Nullable MessageId lastLocalMessageId, long localTimestamp,
+			@Nullable MessageId lastRemoteMessageId, Author introducer,
+			@Nullable byte[] ephemeralPublicKey,
+			@Nullable byte[] ephemeralPrivateKey,
+			@Nullable Map<TransportId, TransportProperties> transportProperties,
+			long acceptTimestamp, @Nullable byte[] masterKey,
+			Author remoteAuthor,
+			@Nullable byte[] remoteEphemeralPublicKey, @Nullable
+			Map<TransportId, TransportProperties> remoteTransportProperties,
+			long remoteAcceptTimestamp,
+			@Nullable Map<TransportId, KeySetId> transportKeys) {
+		super(sessionId, state, requestTimestamp);
+		this.contactGroupId = contactGroupId;
+		this.lastLocalMessageId = lastLocalMessageId;
+		this.localTimestamp = localTimestamp;
+		this.lastRemoteMessageId = lastRemoteMessageId;
+		this.introducer = introducer;
+		this.ephemeralPublicKey = ephemeralPublicKey;
+		this.ephemeralPrivateKey = ephemeralPrivateKey;
+		this.transportProperties = transportProperties;
+		this.acceptTimestamp = acceptTimestamp;
+		this.masterKey = masterKey;
+		this.remoteAuthor = remoteAuthor;
+		this.remoteEphemeralPublicKey = remoteEphemeralPublicKey;
+		this.remoteTransportProperties = remoteTransportProperties;
+		this.remoteAcceptTimestamp = remoteAcceptTimestamp;
+		this.transportKeys = transportKeys;
+	}
+
+	static IntroduceeSession getInitial(GroupId contactGroupId,
+			SessionId sessionId, Author introducer, Author remoteAuthor) {
+		return new IntroduceeSession(sessionId, START, -1, contactGroupId, null,
+				-1, null, introducer, null, null, null, -1, null, remoteAuthor,
+				null, null, -1, null);
+	}
+
+	static IntroduceeSession addRemoteRequest(IntroduceeSession s,
+			IntroduceeState state, RequestMessage m) {
+		return new IntroduceeSession(s.getSessionId(), state, m.getTimestamp(),
+				s.contactGroupId, s.lastLocalMessageId, s.localTimestamp,
+				m.getMessageId(), s.introducer, s.ephemeralPublicKey,
+				s.ephemeralPrivateKey, s.transportProperties, s.acceptTimestamp,
+				s.masterKey, s.remoteAuthor, s.remoteEphemeralPublicKey,
+				s.remoteTransportProperties, s.remoteAcceptTimestamp,
+				s.transportKeys);
+	}
+
+	static IntroduceeSession addLocalAccept(IntroduceeSession s,
+			IntroduceeState state, Message acceptMessage,
+			byte[] ephemeralPublicKey, byte[] ephemeralPrivateKey,
+			long acceptTimestamp,
+			Map<TransportId, TransportProperties> transportProperties) {
+		return new IntroduceeSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId,
+				acceptMessage.getId(), acceptMessage.getTimestamp(),
+				s.lastRemoteMessageId, s.introducer, ephemeralPublicKey,
+				ephemeralPrivateKey, transportProperties,
+				acceptTimestamp, s.masterKey, s.remoteAuthor,
+				s.remoteEphemeralPublicKey, s.remoteTransportProperties,
+				s.remoteAcceptTimestamp, s.transportKeys);
+	}
+
+	static IntroduceeSession addRemoteAccept(IntroduceeSession s,
+			IntroduceeState state, AcceptMessage acceptMessage) {
+		return new IntroduceeSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, s.lastLocalMessageId,
+				s.localTimestamp, acceptMessage.getMessageId(), s.introducer,
+				s.ephemeralPublicKey, s.ephemeralPrivateKey,
+				s.transportProperties, s.acceptTimestamp, s.masterKey,
+				s.remoteAuthor, acceptMessage.getEphemeralPublicKey(),
+				acceptMessage.getTransportProperties(),
+				acceptMessage.getAcceptTimestamp(), s.transportKeys);
+	}
+
+	static IntroduceeSession addLocalAuth(IntroduceeSession s,
+			IntroduceeState state, SecretKey masterKey, Message m) {
+		return new IntroduceeSession(s.getSessionId(), state,
+				s.getRequestTimestamp(), s.contactGroupId, m.getId(),
+				m.getTimestamp(), s.lastRemoteMessageId, s.introducer,
+				s.ephemeralPublicKey, s.ephemeralPrivateKey,
+				s.transportProperties, s.acceptTimestamp, masterKey.getBytes(),
+				s.remoteAuthor, s.remoteEphemeralPublicKey,
+				s.remoteTransportProperties, s.remoteAcceptTimestamp,
+				s.transportKeys);
+	}
+
+	static IntroduceeSession awaitActivate(IntroduceeSession s, AuthMessage m,
+			Message sent, Map<TransportId, KeySetId> transportKeys) {
+		return new IntroduceeSession(s.getSessionId(), AWAIT_ACTIVATE,
+				s.getRequestTimestamp(), s.contactGroupId, sent.getId(),
+				sent.getTimestamp(), m.getMessageId(), s.introducer, null, null,
+				null, s.acceptTimestamp, null, s.getRemoteAuthor(), null, null,
+				s.remoteAcceptTimestamp, transportKeys);
+	}
+
+	static IntroduceeSession clear(IntroduceeSession s,
+			@Nullable MessageId lastLocalMessageId, long localTimestamp,
+			@Nullable MessageId lastRemoteMessageId) {
+		return new IntroduceeSession(s.getSessionId(), START,
+				s.getRequestTimestamp(), s.getContactGroupId(),
+				lastLocalMessageId, localTimestamp, lastRemoteMessageId,
+				s.getIntroducer(), null, null, null, -1, null,
+				s.getRemoteAuthor(), null, null, -1, null);
+	}
+
+	@Override
+	Role getRole() {
+		return INTRODUCEE;
+	}
+
+	public GroupId getContactGroupId() {
+		return contactGroupId;
+	}
+
+	public long getLocalTimestamp() {
+		return localTimestamp;
+	}
+
+	@Nullable
+	public MessageId getLastLocalMessageId() {
+		return lastLocalMessageId;
+	}
+
+	@Nullable
+	public MessageId getLastRemoteMessageId() {
+		return lastRemoteMessageId;
+	}
+
+	Author getIntroducer() {
+		return introducer;
+	}
+
+	@Nullable
+	byte[] getEphemeralPublicKey() {
+		return ephemeralPublicKey;
+	}
+
+	@Nullable
+	byte[] getEphemeralPrivateKey() {
+		return ephemeralPrivateKey;
+	}
+
+	@Nullable
+	Map<TransportId, TransportProperties> getTransportProperties() {
+		return transportProperties;
+	}
+
+	long getAcceptTimestamp() {
+		return acceptTimestamp;
+	}
+
+	@Nullable
+	byte[] getMasterKey() {
+		return masterKey;
+	}
+
+	Author getRemoteAuthor() {
+		return remoteAuthor;
+	}
+
+	@Nullable
+	byte[] getRemotePublicKey() {
+		return remoteEphemeralPublicKey;
+	}
+
+	@Nullable
+	Map<TransportId, TransportProperties> getRemoteTransportProperties() {
+		return remoteTransportProperties;
+	}
+
+	long getRemoteAcceptTimestamp() {
+		return remoteAcceptTimestamp;
+	}
+
+	@Nullable
+	Map<TransportId, KeySetId> getTransportKeys() {
+		return transportKeys;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeState.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeState.java
new file mode 100644
index 0000000000000000000000000000000000000000..4a901752df07d53d489945ca8a4229869e7ae815
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeState.java
@@ -0,0 +1,36 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+enum IntroduceeState implements State {
+
+	START(0),
+	AWAIT_RESPONSES(1),
+	LOCAL_DECLINED(2),
+	LOCAL_ACCEPTED(3),
+	REMOTE_ACCEPTED(4),
+	AWAIT_AUTH(5),
+	AWAIT_ACTIVATE(6);
+
+	private final int value;
+
+	IntroduceeState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static IntroduceeState fromValue(int value) throws FormatException {
+		for (IntroduceeState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..cbfc53923e47f22b4000f46c273b6a1fb4e88f36
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerSession.java
@@ -0,0 +1,115 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.Message;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction2.Role;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+
+import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
+
+@Immutable
+@NotNullByDefault
+class IntroducerSession extends Session<IntroducerState> {
+
+	private final Introducee introducee1, introducee2;
+
+	IntroducerSession(SessionId sessionId, IntroducerState state,
+			long requestTimestamp, Introducee introducee1,
+			Introducee introducee2) {
+		super(sessionId, state, requestTimestamp);
+		this.introducee1 = introducee1;
+		this.introducee2 = introducee2;
+	}
+
+	IntroducerSession(SessionId sessionId, GroupId groupId1, Author author1,
+			GroupId groupId2, Author author2) {
+		this(sessionId, IntroducerState.START, -1,
+				new Introducee(sessionId, groupId1, author1),
+				new Introducee(sessionId, groupId2, author2));
+	}
+
+	@Override
+	Role getRole() {
+		return INTRODUCER;
+	}
+
+	Introducee getIntroducee1() {
+		return introducee1;
+	}
+
+	Introducee getIntroducee2() {
+		return introducee2;
+	}
+
+	@Immutable
+	@NotNullByDefault
+	static class Introducee implements PeerSession {
+		final SessionId sessionId;
+		final GroupId groupId;
+		final Author author;
+		final long localTimestamp;
+		@Nullable
+		final MessageId lastLocalMessageId, lastRemoteMessageId;
+
+		Introducee(SessionId sessionId, GroupId groupId, Author author,
+				long localTimestamp,
+				@Nullable MessageId lastLocalMessageId,
+				@Nullable MessageId lastRemoteMessageId) {
+			this.sessionId = sessionId;
+			this.groupId = groupId;
+			this.localTimestamp = localTimestamp;
+			this.author = author;
+			this.lastLocalMessageId = lastLocalMessageId;
+			this.lastRemoteMessageId = lastRemoteMessageId;
+		}
+
+		Introducee(Introducee i, Message sent) {
+			this(i.sessionId, i.groupId, i.author, sent.getTimestamp(),
+					sent.getId(), i.lastRemoteMessageId);
+		}
+
+		Introducee(Introducee i, MessageId remoteMessageId) {
+			this(i.sessionId, i.groupId, i.author, i.localTimestamp,
+					i.lastLocalMessageId, remoteMessageId);
+		}
+
+		private Introducee(SessionId sessionId, GroupId groupId,
+				Author author) {
+			this(sessionId, groupId, author, -1, null, null);
+		}
+
+		public SessionId getSessionId() {
+			return sessionId;
+		}
+
+		@Override
+		public GroupId getContactGroupId() {
+			return groupId;
+		}
+
+		@Override
+		public long getLocalTimestamp() {
+			return localTimestamp;
+		}
+
+		@Nullable
+		@Override
+		public MessageId getLastLocalMessageId() {
+			return lastLocalMessageId;
+		}
+
+		@Nullable
+		@Override
+		public MessageId getLastRemoteMessageId() {
+			return lastRemoteMessageId;
+		}
+
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerState.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerState.java
new file mode 100644
index 0000000000000000000000000000000000000000..43e51d96af8e2b77abb57d92951f07f0cc2247f3
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerState.java
@@ -0,0 +1,36 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+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);
+
+	private final int value;
+
+	IntroducerState(int value) {
+		this.value = value;
+	}
+
+	@Override
+	public int getValue() {
+		return value;
+	}
+
+	static IntroducerState fromValue(int value) throws FormatException {
+		for (IntroducerState s : values()) if (s.value == value) return s;
+		throw new FormatException();
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java
index 4098b2421e84845e52f3743ffd7fd1bbca1de7f7..ea57344682f294364fd6418c23ebc08fcdddfb79 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java
@@ -11,4 +11,33 @@ interface IntroductionConstants {
 	String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
 	String MSG_KEY_INVITATION_ACCEPTED = "invitationAccepted";
 
+	// Session Keys
+	String SESSION_KEY_SESSION_ID = "sessionId";
+	String SESSION_KEY_ROLE = "role";
+	String SESSION_KEY_STATE = "state";
+	String SESSION_KEY_REQUEST_TIMESTAMP = "requestTimestamp";
+	String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
+	String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
+	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_GROUP_ID = "groupId";
+	String SESSION_KEY_AUTHOR = "author";
+
+	// Session Keys Introducee
+	String SESSION_KEY_INTRODUCER = "introducer";
+	String SESSION_KEY_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey";
+	String SESSION_KEY_EPHEMERAL_PRIVATE_KEY = "ephemeralPrivateKey";
+	String SESSION_KEY_TRANSPORT_PROPERTIES = "transportProperties";
+	String SESSION_KEY_ACCEPT_TIMESTAMP = "acceptTimestamp";
+	String SESSION_KEY_MASTER_KEY = "masterKey";
+	String SESSION_KEY_REMOTE_AUTHOR = "remoteAuthor";
+	String SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY = "remoteEphemeralPublicKey";
+	String SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES =
+			"remoteTransportProperties";
+	String SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP = "remoteAcceptTimestamp";
+	String SESSION_KEY_TRANSPORT_KEYS = "transportKeys";
+
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/PeerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/PeerSession.java
new file mode 100644
index 0000000000000000000000000000000000000000..e2f8b7c46fbb42f413a35c8a182a3de4e26c29c2
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/PeerSession.java
@@ -0,0 +1,25 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.briar.api.client.SessionId;
+
+import javax.annotation.Nullable;
+
+@NotNullByDefault
+interface PeerSession {
+
+	SessionId getSessionId();
+
+	GroupId getContactGroupId();
+
+	long getLocalTimestamp();
+
+	@Nullable
+	MessageId getLastLocalMessageId();
+
+	@Nullable
+	MessageId getLastRemoteMessageId();
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/Session.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/Session.java
new file mode 100644
index 0000000000000000000000000000000000000000..ca2a89c190119dc9aae058d900318abc00b7a259
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/Session.java
@@ -0,0 +1,37 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction2.Role;
+
+import javax.annotation.concurrent.Immutable;
+
+@Immutable
+@NotNullByDefault
+abstract class Session<S extends State> {
+
+	private final SessionId sessionId;
+	private final S state;
+	private long requestTimestamp;
+
+	Session(SessionId sessionId, S state, long requestTimestamp) {
+		this.sessionId = sessionId;
+		this.state = state;
+		this.requestTimestamp = requestTimestamp;
+	}
+
+	abstract Role getRole();
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	S getState() {
+		return state;
+	}
+
+	public long getRequestTimestamp() {
+		return requestTimestamp;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..4b45c39ca29b77057ac116e7bcc4aaee34a5f434
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoder.java
@@ -0,0 +1,13 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+
+@NotNullByDefault
+interface SessionEncoder {
+
+	BdfDictionary encodeIntroducerSession(IntroducerSession s);
+
+	BdfDictionary encodeIntroduceeSession(IntroduceeSession s);
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..3bce33d853d6cf57c8fc9cd7ade56e4718a2d501
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionEncoderImpl.java
@@ -0,0 +1,125 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
+
+@Immutable
+@NotNullByDefault
+class SessionEncoderImpl implements SessionEncoder {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	SessionEncoderImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@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()));
+		return d;
+	}
+
+	private BdfDictionary encodeIntroducee(Introducee i) {
+		BdfDictionary d = new BdfDictionary();
+		putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, i.lastLocalMessageId);
+		putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID,
+				i.lastRemoteMessageId);
+		d.put(SESSION_KEY_LOCAL_TIMESTAMP, i.localTimestamp);
+		d.put(SESSION_KEY_GROUP_ID, i.groupId);
+		d.put(SESSION_KEY_AUTHOR, clientHelper.toList(i.author));
+		return d;
+	}
+
+	@Override
+	public BdfDictionary encodeIntroduceeSession(IntroduceeSession s) {
+		BdfDictionary d = encodeSession(s);
+		d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp());
+		putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID,
+				s.getLastLocalMessageId());
+		putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID,
+				s.getLastRemoteMessageId());
+		d.put(SESSION_KEY_INTRODUCER, clientHelper.toList(s.getIntroducer()));
+		d.put(SESSION_KEY_REMOTE_AUTHOR,
+				clientHelper.toList(s.getRemoteAuthor()));
+		putNullable(d, SESSION_KEY_EPHEMERAL_PUBLIC_KEY,
+				s.getEphemeralPublicKey());
+		putNullable(d, SESSION_KEY_EPHEMERAL_PRIVATE_KEY,
+				s.getEphemeralPrivateKey());
+		putNullable(d, SESSION_KEY_TRANSPORT_PROPERTIES,
+				s.getTransportProperties() == null ? null :
+						clientHelper.toDictionary(s.getTransportProperties()));
+		d.put(SESSION_KEY_ACCEPT_TIMESTAMP, s.getAcceptTimestamp());
+		putNullable(d, SESSION_KEY_MASTER_KEY, s.getMasterKey());
+		putNullable(d, SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY,
+				s.getRemotePublicKey());
+		putNullable(d, SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES,
+				s.getRemoteTransportProperties() == null ? null : clientHelper
+						.toDictionary(s.getRemoteTransportProperties()));
+		d.put(SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP, s.getRemoteAcceptTimestamp());
+		putNullable(d, SESSION_KEY_TRANSPORT_KEYS,
+				encodeTransportKeys(s.getTransportKeys()));
+		return d;
+	}
+
+	private BdfDictionary encodeSession(Session s) {
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_KEY_SESSION_ID, s.getSessionId());
+		d.put(SESSION_KEY_ROLE, s.getRole().getValue());
+		d.put(SESSION_KEY_STATE, s.getState().getValue());
+		d.put(SESSION_KEY_REQUEST_TIMESTAMP, s.getRequestTimestamp());
+		return d;
+	}
+
+	@Nullable
+	private BdfDictionary encodeTransportKeys(
+			@Nullable Map<TransportId, KeySetId> keys) {
+		if (keys == null) return null;
+		BdfDictionary d = new BdfDictionary();
+		for (Map.Entry<TransportId, KeySetId> e : keys.entrySet()) {
+			d.put(e.getKey().getString(), e.getValue().getInt());
+		}
+		return d;
+	}
+
+	private void putNullable(BdfDictionary d, String key, @Nullable Object o) {
+		d.put(key, o == null ? NULL_VALUE : o);
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..dd3fba09cb69c17d94a0e04ec94ad921541471ae
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParser.java
@@ -0,0 +1,23 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction2.Role;
+
+@NotNullByDefault
+interface SessionParser {
+
+	BdfDictionary getSessionQuery(SessionId s);
+
+	Role getRole(BdfDictionary d) throws FormatException;
+
+	IntroducerSession parseIntroducerSession(BdfDictionary d)
+			throws FormatException;
+
+	IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException;
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParserImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..dbb609fc1a49ec362630a08282b3a7857db8a7f1
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/SessionParserImpl.java
@@ -0,0 +1,183 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.data.BdfEntry;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.api.introduction2.Role;
+import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import javax.inject.Inject;
+
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_AUTHOR;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_GROUP_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_1;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCEE_2;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_INTRODUCER;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_MASTER_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_SESSION_ID;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_STATE;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES;
+import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
+
+@Immutable
+@NotNullByDefault
+class SessionParserImpl implements SessionParser {
+
+	private final ClientHelper clientHelper;
+
+	@Inject
+	SessionParserImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
+	}
+
+	@Override
+	public BdfDictionary getSessionQuery(SessionId s) {
+		return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
+	}
+
+	@Override
+	public Role getRole(BdfDictionary d) throws FormatException {
+		return Role.fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
+	}
+
+	@Override
+	public IntroducerSession parseIntroducerSession(BdfDictionary d)
+			throws FormatException {
+		if (getRole(d) != INTRODUCER) throw new IllegalArgumentException();
+		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));
+		return new IntroducerSession(sessionId, state, requestTimestamp,
+				introducee1, introducee2);
+	}
+
+	private Introducee parseIntroducee(SessionId sessionId, BdfDictionary d)
+			throws FormatException {
+		MessageId lastLocalMessageId =
+				getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		MessageId lastRemoteMessageId =
+				getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+		GroupId groupId = getGroupId(d, SESSION_KEY_GROUP_ID);
+		Author author = getAuthor(d, SESSION_KEY_AUTHOR);
+		return new Introducee(sessionId, groupId, author, localTimestamp,
+				lastLocalMessageId, lastRemoteMessageId);
+	}
+
+	@Override
+	public IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId,
+			BdfDictionary d) throws FormatException {
+		if (getRole(d) != INTRODUCEE) throw new IllegalArgumentException();
+		SessionId sessionId = getSessionId(d);
+		IntroduceeState state = IntroduceeState.fromValue(getState(d));
+		long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP);
+		MessageId lastLocalMessageId =
+				getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
+		long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
+		MessageId lastRemoteMessageId =
+				getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
+		Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER);
+		byte[] ephemeralPublicKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY);
+		byte[] ephemeralPrivateKey =
+				d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PRIVATE_KEY);
+		BdfDictionary tpDict =
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES);
+		Map<TransportId, TransportProperties> transportProperties =
+				tpDict == null ? null : clientHelper
+						.parseAndValidateTransportPropertiesMap(tpDict);
+		long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP);
+		byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY);
+		Author remoteAuthor = getAuthor(d, SESSION_KEY_REMOTE_AUTHOR);
+		byte[] remoteEphemeralPublicKey =
+				d.getOptionalRaw(SESSION_KEY_REMOTE_EPHEMERAL_PUBLIC_KEY);
+		BdfDictionary rptDict = d.getOptionalDictionary(
+				SESSION_KEY_REMOTE_TRANSPORT_PROPERTIES);
+		Map<TransportId, TransportProperties> remoteTransportProperties =
+				rptDict == null ? null : clientHelper
+						.parseAndValidateTransportPropertiesMap(rptDict);
+		long remoteAcceptTimestamp =
+				d.getLong(SESSION_KEY_REMOTE_ACCEPT_TIMESTAMP);
+		Map<TransportId, KeySetId> transportKeys = parseTransportKeys(
+				d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS));
+		return new IntroduceeSession(sessionId, state, requestTimestamp,
+				introducerGroupId, lastLocalMessageId, localTimestamp,
+				lastRemoteMessageId, introducer, ephemeralPublicKey,
+				ephemeralPrivateKey, transportProperties, acceptTimestamp,
+				masterKey, remoteAuthor, remoteEphemeralPublicKey,
+				remoteTransportProperties, remoteAcceptTimestamp,
+				transportKeys);
+	}
+
+	private int getState(BdfDictionary d) throws FormatException {
+		return d.getLong(SESSION_KEY_STATE).intValue();
+	}
+
+	private SessionId getSessionId(BdfDictionary d) throws FormatException {
+		byte[] b = d.getRaw(SESSION_KEY_SESSION_ID);
+		return new SessionId(b);
+	}
+
+	@Nullable
+	private MessageId getMessageId(BdfDictionary d, String key)
+			throws FormatException {
+		byte[] b = d.getOptionalRaw(key);
+		return b == null ? null : new MessageId(b);
+	}
+
+	private GroupId getGroupId(BdfDictionary d, String key)
+			throws FormatException {
+		return new GroupId(d.getRaw(key));
+	}
+
+	private Author getAuthor(BdfDictionary d, String key)
+			throws FormatException {
+		return clientHelper.parseAndValidateAuthor(d.getList(key));
+	}
+
+	@Nullable
+	private Map<TransportId, KeySetId> parseTransportKeys(
+			@Nullable BdfDictionary d) throws FormatException {
+		if (d == null) return null;
+		Map<TransportId, KeySetId> map = new HashMap<>(d.size());
+		for (String key : d.keySet()) {
+			map.put(new TransportId(key),
+					new KeySetId(d.getLong(key).intValue())
+			);
+		}
+		return map;
+	}
+
+}
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/State.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/State.java
new file mode 100644
index 0000000000000000000000000000000000000000..1e1d46e0a6a352addf38f7cb524042a3b21fe08e
--- /dev/null
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/State.java
@@ -0,0 +1,7 @@
+package org.briarproject.briar.introduction2;
+
+interface State {
+
+	int getValue();
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction2/SessionEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction2/SessionEncoderParserIntegrationTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..24ddf208a3873ad9ac9b0c5642eb7f01c20a5d5d
--- /dev/null
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction2/SessionEncoderParserIntegrationTest.java
@@ -0,0 +1,281 @@
+package org.briarproject.briar.introduction2;
+
+import org.briarproject.bramble.api.FormatException;
+import org.briarproject.bramble.api.client.ClientHelper;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.data.BdfDictionary;
+import org.briarproject.bramble.api.identity.Author;
+import org.briarproject.bramble.api.identity.AuthorFactory;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.properties.TransportProperties;
+import org.briarproject.bramble.api.sync.GroupId;
+import org.briarproject.bramble.api.sync.MessageId;
+import org.briarproject.bramble.api.transport.KeySetId;
+import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.briar.api.client.SessionId;
+import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
+import org.briarproject.briar.test.BriarIntegrationTestComponent;
+import org.briarproject.briar.test.DaggerBriarIntegrationTestComponent;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
+import static org.briarproject.bramble.test.TestUtils.getRandomId;
+import static org.briarproject.bramble.test.TestUtils.getTransportId;
+import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap;
+import static org.briarproject.bramble.util.StringUtils.getRandomString;
+import static org.briarproject.briar.introduction2.IntroduceeState.LOCAL_ACCEPTED;
+import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTHS;
+import static org.briarproject.briar.introduction2.IntroductionConstants.SESSION_KEY_ROLE;
+import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
+import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+public class SessionEncoderParserIntegrationTest extends BrambleTestCase {
+
+	@Inject
+	ClientHelper clientHelper;
+	@Inject
+	AuthorFactory authorFactory;
+
+	private final SessionEncoder sessionEncoder;
+	private final SessionParser sessionParser;
+
+	private final GroupId groupId1 = new GroupId(getRandomId());
+	private final GroupId groupId2 = new GroupId(getRandomId());
+	private final SessionId sessionId = new SessionId(getRandomId());
+	private final long requestTimestamp = 42;
+	private final long localTimestamp = 1337;
+	private final long localTimestamp2 = 1338;
+	private final long acceptTimestamp = 123456;
+	private final long remoteAcceptTimestamp = 1234567;
+	private final MessageId lastLocalMessageId = new MessageId(getRandomId());
+	private final MessageId lastLocalMessageId2 = new MessageId(getRandomId());
+	private final MessageId lastRemoteMessageId = new MessageId(getRandomId());
+	private final MessageId lastRemoteMessageId2 = new MessageId(getRandomId());
+	private final Author author1;
+	private final Author author2;
+	private final byte[] ephemeralPublicKey =
+			getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final byte[] ephemeralPrivateKey =
+			getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final byte[] masterKey = getRandomBytes(SecretKey.LENGTH);
+	private final byte[] remoteEphemeralPublicKey =
+			getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
+	private final Map<TransportId, TransportProperties> transportProperties =
+			getTransportPropertiesMap(3);
+	private final Map<TransportId, TransportProperties>
+			remoteTransportProperties = getTransportPropertiesMap(3);
+	private final Map<TransportId, KeySetId> transportKeys = new HashMap<>();
+
+	public SessionEncoderParserIntegrationTest() {
+		BriarIntegrationTestComponent component =
+				DaggerBriarIntegrationTestComponent.builder().build();
+		component.inject(this);
+
+		sessionEncoder = new SessionEncoderImpl(clientHelper);
+		sessionParser = new SessionParserImpl(clientHelper);
+		author1 = getRealAuthor();
+		author2 = getRealAuthor();
+		transportKeys.put(getTransportId(), new KeySetId(1));
+		transportKeys.put(getTransportId(), new KeySetId(2));
+		transportKeys.put(getTransportId(), new KeySetId(3));
+	}
+
+	@Test
+	public void testIntroducerSession() throws FormatException {
+		IntroducerSession s1 = getIntroducerSession();
+
+		BdfDictionary d = sessionEncoder.encodeIntroducerSession(s1);
+		IntroducerSession s2 = sessionParser.parseIntroducerSession(d);
+
+		assertEquals(INTRODUCER, s1.getRole());
+		assertEquals(s1.getRole(), s2.getRole());
+		assertEquals(sessionId, s1.getSessionId());
+		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());
+	}
+
+	@Test
+	public void testIntroducerSessionWithNulls() throws FormatException {
+		Introducee introducee1 =
+				new Introducee(sessionId, groupId1, author1, localTimestamp,
+						null, null);
+		Introducee introducee2 =
+				new Introducee(sessionId, groupId2, author2, localTimestamp2,
+						null, null);
+		IntroducerSession s1 = new IntroducerSession(sessionId,
+				AWAIT_AUTHS, requestTimestamp, introducee1,
+				introducee2);
+
+		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.getIntroducee2().lastLocalMessageId);
+		assertEquals(s1.getIntroducee2().lastLocalMessageId,
+				s2.getIntroducee2().lastLocalMessageId);
+		assertNull(s1.getIntroducee2().lastRemoteMessageId);
+		assertEquals(s1.getIntroducee2().lastRemoteMessageId,
+				s2.getIntroducee2().lastRemoteMessageId);
+	}
+
+	@Test(expected = FormatException.class)
+	public void testIntroducerSessionUnknownRole() throws FormatException {
+		IntroducerSession s = getIntroducerSession();
+		BdfDictionary d = sessionEncoder.encodeIntroducerSession(s);
+		d.put(SESSION_KEY_ROLE, 1337);
+		sessionParser.parseIntroducerSession(d);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testIntroducerSessionWrongRole() throws FormatException {
+		IntroducerSession s = getIntroducerSession();
+		BdfDictionary d = sessionEncoder.encodeIntroducerSession(s);
+		d.put(SESSION_KEY_ROLE, INTRODUCEE.getValue());
+		sessionParser.parseIntroducerSession(d);
+	}
+
+	@Test
+	public void testIntroduceeSession() throws FormatException {
+		IntroduceeSession s1 = getIntroduceeSession();
+		BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s1);
+		IntroduceeSession s2 =
+				sessionParser.parseIntroduceeSession(groupId1, d);
+
+		assertEquals(LOCAL_ACCEPTED, s1.getState());
+		assertEquals(s1.getState(), s2.getState());
+		assertEquals(INTRODUCEE, s1.getRole());
+		assertEquals(s1.getRole(), s2.getRole());
+		assertEquals(sessionId, s1.getSessionId());
+		assertEquals(s1.getSessionId(), s2.getSessionId());
+		assertEquals(groupId1, s1.getContactGroupId());
+		assertEquals(s1.getContactGroupId(), s2.getContactGroupId());
+		assertEquals(localTimestamp, s1.getLocalTimestamp());
+		assertEquals(s1.getLocalTimestamp(), s2.getLocalTimestamp());
+		assertEquals(lastLocalMessageId, s1.getLastLocalMessageId());
+		assertEquals(s1.getLastLocalMessageId(), s2.getLastLocalMessageId());
+		assertEquals(lastRemoteMessageId, s1.getLastRemoteMessageId());
+		assertEquals(s1.getLastRemoteMessageId(), s2.getLastRemoteMessageId());
+		assertEquals(author1, s1.getIntroducer());
+		assertEquals(s1.getIntroducer(), s2.getIntroducer());
+		assertEquals(author2, s1.getRemoteAuthor());
+		assertEquals(s1.getRemoteAuthor(), s2.getRemoteAuthor());
+		assertArrayEquals(ephemeralPublicKey, s1.getEphemeralPublicKey());
+		assertArrayEquals(s1.getEphemeralPublicKey(),
+				s2.getEphemeralPublicKey());
+		assertArrayEquals(ephemeralPrivateKey, s1.getEphemeralPrivateKey());
+		assertArrayEquals(s1.getEphemeralPrivateKey(),
+				s2.getEphemeralPrivateKey());
+		assertEquals(acceptTimestamp, s1.getAcceptTimestamp());
+		assertEquals(s1.getAcceptTimestamp(), s2.getAcceptTimestamp());
+		assertArrayEquals(masterKey, s1.getMasterKey());
+		assertArrayEquals(s1.getMasterKey(), s2.getMasterKey());
+		assertArrayEquals(remoteEphemeralPublicKey, s1.getRemotePublicKey());
+		assertArrayEquals(s1.getRemotePublicKey(),
+				s2.getRemotePublicKey());
+		assertEquals(transportProperties, s1.getTransportProperties());
+		assertEquals(s1.getTransportProperties(), s2.getTransportProperties());
+		assertEquals(remoteTransportProperties,
+				s1.getRemoteTransportProperties());
+		assertEquals(s1.getRemoteTransportProperties(),
+				s2.getRemoteTransportProperties());
+		assertEquals(remoteAcceptTimestamp, s1.getRemoteAcceptTimestamp());
+		assertEquals(s1.getRemoteAcceptTimestamp(), s2.getRemoteAcceptTimestamp());
+		assertEquals(transportKeys, s1.getTransportKeys());
+		assertEquals(s1.getTransportKeys(), s2.getTransportKeys());
+	}
+
+	@Test
+	public void testIntroduceeSessionWithNulls() throws FormatException {
+		IntroduceeSession s1 =
+				new IntroduceeSession(sessionId, LOCAL_ACCEPTED,
+						requestTimestamp, groupId1, null, localTimestamp, null,
+						author1, null, null, null, acceptTimestamp, null,
+						author2, null, null, remoteAcceptTimestamp, null);
+
+		BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s1);
+		IntroduceeSession s2 =
+				sessionParser.parseIntroduceeSession(groupId1, d);
+
+		assertNull(s1.getLastLocalMessageId());
+		assertEquals(s1.getLastLocalMessageId(), s2.getLastLocalMessageId());
+		assertNull(s1.getLastRemoteMessageId());
+		assertEquals(s1.getLastRemoteMessageId(), s2.getLastRemoteMessageId());
+		assertNull(s1.getEphemeralPublicKey());
+		assertArrayEquals(s1.getEphemeralPublicKey(),
+				s2.getEphemeralPublicKey());
+		assertNull(s1.getEphemeralPrivateKey());
+		assertArrayEquals(s1.getEphemeralPrivateKey(),
+				s2.getEphemeralPrivateKey());
+		assertNull(s1.getTransportKeys());
+		assertEquals(s1.getTransportKeys(), s2.getTransportKeys());
+	}
+
+	@Test(expected = FormatException.class)
+	public void testIntroduceeSessionUnknownRole() throws FormatException {
+		IntroduceeSession s = getIntroduceeSession();
+		BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s);
+		d.put(SESSION_KEY_ROLE, 1337);
+		sessionParser.parseIntroduceeSession(groupId1, d);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testIntroduceeSessionWrongRole() throws FormatException {
+		IntroduceeSession s = getIntroduceeSession();
+		BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s);
+		d.put(SESSION_KEY_ROLE, INTRODUCER.getValue());
+		sessionParser.parseIntroduceeSession(groupId1, d);
+	}
+
+	private IntroducerSession getIntroducerSession() {
+		Introducee introducee1 =
+				new Introducee(sessionId, groupId1, author1, localTimestamp,
+						lastLocalMessageId, lastRemoteMessageId);
+		Introducee introducee2 =
+				new Introducee(sessionId, groupId2, author2, localTimestamp2,
+						lastLocalMessageId2, lastRemoteMessageId2);
+		return new IntroducerSession(sessionId, AWAIT_AUTHS,
+				requestTimestamp, introducee1, introducee2);
+	}
+
+	private IntroduceeSession getIntroduceeSession() {
+		return new IntroduceeSession(sessionId, LOCAL_ACCEPTED,
+				requestTimestamp, groupId1, lastLocalMessageId, localTimestamp,
+				lastRemoteMessageId, author1, ephemeralPublicKey,
+				ephemeralPrivateKey, transportProperties, acceptTimestamp,
+				masterKey, author2, remoteEphemeralPublicKey,
+				remoteTransportProperties, remoteAcceptTimestamp,
+				transportKeys);
+	}
+
+	private void assertIntroduceeEquals(Introducee i1, Introducee i2) {
+		assertEquals(i1.author, i2.author);
+		assertEquals(i1.groupId, i2.groupId);
+		assertEquals(i1.localTimestamp, i2.localTimestamp);
+		assertEquals(i1.lastLocalMessageId, i2.lastLocalMessageId);
+		assertEquals(i1.lastRemoteMessageId, i2.lastRemoteMessageId);
+	}
+
+	private Author getRealAuthor() {
+		return authorFactory.createAuthor(getRandomString(5),
+				getRandomBytes(MAX_PUBLIC_KEY_LENGTH));
+	}
+
+}
diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
index 1afcdea7bc683b0f16b948feee57f258124db929..54709815e451ae3b54bd76a8a515f36f3abd030e 100644
--- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
+++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java
@@ -38,6 +38,7 @@ import org.briarproject.briar.client.BriarClientModule;
 import org.briarproject.briar.forum.ForumModule;
 import org.briarproject.briar.introduction.IntroductionModule;
 import org.briarproject.briar.introduction2.MessageEncoderParserIntegrationTest;
+import org.briarproject.briar.introduction2.SessionEncoderParserIntegrationTest;
 import org.briarproject.briar.messaging.MessagingModule;
 import org.briarproject.briar.privategroup.PrivateGroupModule;
 import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;
@@ -78,6 +79,7 @@ public interface BriarIntegrationTestComponent {
 	void inject(BriarIntegrationTest<BriarIntegrationTestComponent> init);
 
 	void inject(MessageEncoderParserIntegrationTest init);
+	void inject(SessionEncoderParserIntegrationTest init);
 
 	void inject(BlogModule.EagerSingletons init);