diff --git a/briar-api/src/org/briarproject/api/ProtocolEngine.java b/briar-api/src/org/briarproject/api/ProtocolEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..65d9fe52e178ac54903c2694cae6d58bf88411e2
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/ProtocolEngine.java
@@ -0,0 +1,31 @@
+package org.briarproject.api;
+
+import org.briarproject.api.event.Event;
+
+import java.util.List;
+
+public interface ProtocolEngine<A, S, M> {
+	StateUpdate<S, M> onLocalAction(S localState, A action);
+
+	StateUpdate<S, M> onMessageReceived(S localState, M received);
+
+	StateUpdate<S, M> onMessageDelivered(S localState, M delivered);
+
+	class StateUpdate<S, M> {
+		public final boolean deleteMessages;
+		public final boolean deleteState;
+		public final S localState;
+		public final List<M> toSend;
+		public final List<Event> toBroadcast;
+
+		public StateUpdate(boolean deleteMessages, boolean deleteState,
+				S localState, List<M> toSend, List<Event> toBroadcast) {
+
+			this.deleteMessages = deleteMessages;
+			this.deleteState = deleteState;
+			this.localState = localState;
+			this.toSend = toSend;
+			this.toBroadcast = toBroadcast;
+		}
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/contact/ContactManager.java b/briar-api/src/org/briarproject/api/contact/ContactManager.java
index ba1d2c663025ed26a8dd04b0448616c5c6985d1c..a40a2b08c456db34f9b432795c2dfd01449fa54d 100644
--- a/briar-api/src/org/briarproject/api/contact/ContactManager.java
+++ b/briar-api/src/org/briarproject/api/contact/ContactManager.java
@@ -16,12 +16,21 @@ public interface ContactManager {
 	/** Registers a hook to be called whenever a contact is removed. */
 	void registerRemoveContactHook(RemoveContactHook hook);
 
+	/**
+	 * Stores a contact within the given transaction associated with the given
+	 * local and remote pseudonyms, and returns an ID for the contact.
+	 */
+	ContactId addContact(Transaction txn, Author remote, AuthorId local,
+			SecretKey master, long timestamp, boolean alice, boolean active)
+			throws DbException;
+
 	/**
 	 * Stores a contact associated with the given local and remote pseudonyms,
 	 * and returns an ID for the contact.
 	 */
-	ContactId addContact(Author remote, AuthorId local, SecretKey master,
-			long timestamp, boolean alice, boolean active) throws DbException;
+	ContactId addContact(Author remote, AuthorId local,
+			SecretKey master, long timestamp, boolean alice, boolean active)
+			throws DbException;
 
 	/** Returns the contact with the given ID. */
 	Contact getContact(ContactId c) throws DbException;
@@ -35,6 +44,14 @@ public interface ContactManager {
 	/** Marks a contact as active or inactive. */
 	void setContactActive(ContactId c, boolean active) throws DbException;
 
+	/** Return true if a contact with this name and public key already exists */
+	boolean contactExists(Transaction txn, AuthorId remoteAuthorID,
+			AuthorId localAuthorId) throws DbException;
+
+	/** Return true if a contact with this name and public key already exists */
+	boolean contactExists(AuthorId remoteAuthorID, AuthorId localAuthorId)
+			throws DbException;
+
 	interface AddContactHook {
 		void addingContact(Transaction txn, Contact c) throws DbException;
 	}
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index 2292bc0ba258a674251de8cdb5b6ebee64750251..a360fa98b61bb104e754a7bd42c6463a8e0918e0 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -162,6 +162,13 @@ public interface DatabaseComponent {
 	Collection<ContactId> getContacts(Transaction txn, AuthorId a)
 			throws DbException;
 
+	/**
+	 * Returns true if the database contains the given contact for the given
+	 * local pseudonym.
+	 */
+	boolean containsContact(Transaction txn, AuthorId remote, AuthorId local)
+			throws DbException;
+
 	/**
 	 * Returns the unique ID for this device.
 	 * <p/>
diff --git a/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java b/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java
index b8d29bf27cb526f830c371175e81db986b6eeb1f..0b2e5017cc88f8bacc762ed97d132773fe4b4093 100644
--- a/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java
@@ -6,12 +6,18 @@ import org.briarproject.api.contact.ContactId;
 public class ContactAddedEvent extends Event {
 
 	private final ContactId contactId;
+	private final boolean active;
 
-	public ContactAddedEvent(ContactId contactId) {
+	public ContactAddedEvent(ContactId contactId, boolean active) {
 		this.contactId = contactId;
+		this.active = active;
 	}
 
 	public ContactId getContactId() {
 		return contactId;
 	}
+
+	public boolean isActive() {
+		return active;
+	}
 }
diff --git a/briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..58473e08c81326327670b71cb5d25e54301a5014
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java
@@ -0,0 +1,26 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.introduction.IntroductionRequest;
+
+public class IntroductionRequestReceivedEvent extends Event {
+
+	private final ContactId contactId;
+	private final IntroductionRequest introductionRequest;
+
+	public IntroductionRequestReceivedEvent(ContactId contactId,
+			IntroductionRequest introductionRequest) {
+
+		this.contactId = contactId;
+		this.introductionRequest = introductionRequest;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public IntroductionRequest getIntroductionRequest() {
+		return introductionRequest;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..2938c40db6423f4683de5956d9fa372a3fd58be1
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java
@@ -0,0 +1,25 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.introduction.IntroductionResponse;
+
+public class IntroductionResponseReceivedEvent extends Event {
+
+	private final ContactId contactId;
+	private final IntroductionResponse introductionResponse;
+
+	public IntroductionResponseReceivedEvent(ContactId contactId,
+			IntroductionResponse introductionResponse) {
+
+		this.contactId = contactId;
+		this.introductionResponse = introductionResponse;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public IntroductionResponse getIntroductionResponse() {
+		return introductionResponse;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java
new file mode 100644
index 0000000000000000000000000000000000000000..dfbf3a3193748403a90d0455b7cc394fff4d479e
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java
@@ -0,0 +1,16 @@
+package org.briarproject.api.event;
+
+import org.briarproject.api.contact.Contact;
+
+public class IntroductionSucceededEvent extends Event {
+
+	private final Contact contact;
+
+	public IntroductionSucceededEvent(Contact contact) {
+		this.contact = contact;
+	}
+
+	public Contact getContact() {
+		return contact;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java
index c53a8363619ced08be3438d34026b9ba5aa1ef18..26216a873019ec9c6a1fcb5407fe06e718dd913f 100644
--- a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java
@@ -1,5 +1,6 @@
 package org.briarproject.api.event;
 
+import org.briarproject.api.db.Metadata;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Message;
 
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java b/briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..db50467f08b2fe29188f2c3b2352ab516bfd3652
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java
@@ -0,0 +1,43 @@
+package org.briarproject.api.introduction;
+
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+public enum IntroduceeAction {
+
+	LOCAL_ACCEPT,
+	LOCAL_DECLINE,
+	LOCAL_ABORT,
+	REMOTE_REQUEST,
+	REMOTE_ACCEPT,
+	REMOTE_DECLINE,
+	REMOTE_ABORT,
+	ACK;
+
+	public static IntroduceeAction getRemote(int type, boolean accept) {
+		if (type == TYPE_REQUEST) return REMOTE_REQUEST;
+		if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT;
+		if (type == TYPE_RESPONSE) return REMOTE_DECLINE;
+		if (type == TYPE_ACK) return ACK;
+		if (type == TYPE_ABORT) return REMOTE_ABORT;
+		return null;
+	}
+
+	public static IntroduceeAction getRemote(int type) {
+		return getRemote(type, true);
+	}
+
+	public static IntroduceeAction getLocal(int type, boolean accept) {
+		if (type == TYPE_RESPONSE && accept) return LOCAL_ACCEPT;
+		if (type == TYPE_RESPONSE) return LOCAL_DECLINE;
+		if (type == TYPE_ABORT) return LOCAL_ABORT;
+		return null;
+	}
+
+	public static IntroduceeAction getLocal(int type) {
+		return getLocal(type, true);
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java b/briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b46a0c1f6168b86b02cdd3c7520d262fe520384
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java
@@ -0,0 +1,76 @@
+package org.briarproject.api.introduction;
+
+import static org.briarproject.api.introduction.IntroduceeAction.ACK;
+import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ACCEPT;
+import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_DECLINE;
+import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ACCEPT;
+import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_DECLINE;
+import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_REQUEST;
+
+public enum IntroduceeProtocolState {
+
+	ERROR(0),
+	AWAIT_REQUEST(1) {
+		@Override
+		public IntroduceeProtocolState next(IntroduceeAction a) {
+			if (a == REMOTE_REQUEST) return AWAIT_RESPONSES;
+			return ERROR;
+		}
+	},
+	AWAIT_RESPONSES(2) {
+		@Override
+		public IntroduceeProtocolState next(IntroduceeAction a) {
+			if (a == REMOTE_ACCEPT) return AWAIT_LOCAL_RESPONSE;
+			if (a == REMOTE_DECLINE) return FINISHED;
+			if (a == LOCAL_ACCEPT) return AWAIT_REMOTE_RESPONSE;
+			if (a == LOCAL_DECLINE) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_REMOTE_RESPONSE(3) {
+		@Override
+		public IntroduceeProtocolState next(IntroduceeAction a) {
+			if (a == REMOTE_ACCEPT) return AWAIT_ACK;
+			if (a == REMOTE_DECLINE) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_LOCAL_RESPONSE(4) {
+		@Override
+		public IntroduceeProtocolState next(IntroduceeAction a) {
+			if (a == LOCAL_ACCEPT) return AWAIT_ACK;
+			if (a == LOCAL_DECLINE) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_ACK(5) {
+		@Override
+		public IntroduceeProtocolState next(IntroduceeAction a) {
+			if (a == ACK) return FINISHED;
+			return ERROR;
+		}
+	},
+	FINISHED(6);
+
+	private final int value;
+
+	IntroduceeProtocolState(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public static IntroduceeProtocolState fromValue(int value) {
+		for (IntroduceeProtocolState s : values()) {
+			if (s.value == value) return s;
+		}
+		throw new IllegalArgumentException();
+	}
+
+	public IntroduceeProtocolState next(IntroduceeAction a) {
+		return this;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroducerAction.java b/briar-api/src/org/briarproject/api/introduction/IntroducerAction.java
new file mode 100644
index 0000000000000000000000000000000000000000..d20433876aec53c41c3ce361807137668292bceb
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroducerAction.java
@@ -0,0 +1,46 @@
+package org.briarproject.api.introduction;
+
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+public enum IntroducerAction {
+
+	LOCAL_REQUEST,
+	LOCAL_ABORT,
+	REMOTE_ACCEPT_1,
+	REMOTE_ACCEPT_2,
+	REMOTE_DECLINE_1,
+	REMOTE_DECLINE_2,
+	REMOTE_ABORT,
+	ACK_1,
+	ACK_2;
+
+	public static IntroducerAction getLocal(int type) {
+		if (type == TYPE_REQUEST) return LOCAL_REQUEST;
+		if (type == TYPE_ABORT) return LOCAL_ABORT;
+		return null;
+	}
+
+	public static IntroducerAction getRemote(int type, boolean one,
+			boolean accept) {
+
+		if (one) {
+			if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT_1;
+			if (type == TYPE_RESPONSE) return REMOTE_DECLINE_1;
+			if (type == TYPE_ACK) return ACK_1;
+		} else {
+			if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT_2;
+			if (type == TYPE_RESPONSE) return REMOTE_DECLINE_2;
+			if (type == TYPE_ACK) return ACK_2;
+		}
+		if (type == TYPE_ABORT) return REMOTE_ABORT;
+		return null;
+	}
+
+	public static IntroducerAction getRemote(int type, boolean one) {
+		return getRemote(type, one, true);
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java b/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java
new file mode 100644
index 0000000000000000000000000000000000000000..d132e0d68a37bdd31dd25bc3f1e82a34d44a7105
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java
@@ -0,0 +1,94 @@
+package org.briarproject.api.introduction;
+
+import static org.briarproject.api.introduction.IntroducerAction.ACK_1;
+import static org.briarproject.api.introduction.IntroducerAction.ACK_2;
+import static org.briarproject.api.introduction.IntroducerAction.LOCAL_REQUEST;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_1;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_2;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_1;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_2;
+
+public enum IntroducerProtocolState {
+
+	ERROR(0),
+	PREPARE_REQUESTS(1) {
+		@Override
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == LOCAL_REQUEST) return AWAIT_RESPONSES;
+			return ERROR;
+		}
+	},
+	AWAIT_RESPONSES(2) {
+		@Override
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == REMOTE_ACCEPT_1) return AWAIT_RESPONSE_2;
+			if (a == REMOTE_ACCEPT_2) return AWAIT_RESPONSE_1;
+			if (a == REMOTE_DECLINE_1) return FINISHED;
+			if (a == REMOTE_DECLINE_2) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_RESPONSE_1(3) {
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == REMOTE_ACCEPT_1) return AWAIT_ACKS;
+			if (a == REMOTE_DECLINE_1) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_RESPONSE_2(4) {
+		@Override
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == REMOTE_ACCEPT_2) return AWAIT_ACKS;
+			if (a == REMOTE_DECLINE_2) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_ACKS(5) {
+		@Override
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == ACK_1) return AWAIT_ACK_2;
+			if (a == ACK_2) return AWAIT_ACK_1;
+			return ERROR;
+		}
+	},
+	AWAIT_ACK_1(6) {
+		@Override
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == ACK_1) return FINISHED;
+			return ERROR;
+		}
+	},
+	AWAIT_ACK_2(7) {
+		@Override
+		public IntroducerProtocolState next(IntroducerAction a) {
+			if (a == ACK_2) return FINISHED;
+			return ERROR;
+		}
+	},
+	FINISHED(8);
+
+	private final int value;
+
+	IntroducerProtocolState(int value) {
+		this.value = value;
+	}
+
+	public int getValue() {
+		return value;
+	}
+
+	public static IntroducerProtocolState fromValue(int value) {
+		for (IntroducerProtocolState s : values()) {
+			if (s.value == value) return s;
+		}
+		throw new IllegalArgumentException();
+	}
+
+	public static boolean isOngoing(IntroducerProtocolState state) {
+		return state != FINISHED && state != ERROR;
+	}
+
+	public IntroducerProtocolState next(IntroducerAction a) {
+		return this;
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java b/briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..64bc309830b2f18ea2e17d8f115a842c2970b6fe
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java
@@ -0,0 +1,68 @@
+package org.briarproject.api.introduction;
+
+public interface IntroductionConstants {
+
+	/* Protocol roles */
+	int ROLE_INTRODUCER = 0;
+	int ROLE_INTRODUCEE = 1;
+
+	/* Message types */
+	int TYPE_REQUEST = 1;
+	int TYPE_RESPONSE = 2;
+	int TYPE_ACK = 3;
+	int TYPE_ABORT = 4;
+
+	/* Message Constants */
+	String TYPE = "type";
+	String GROUP_ID = "groupId";
+	String SESSION_ID = "sessionId";
+	String CONTACT = "contactId";
+	String NAME = "name";
+	String PUBLIC_KEY = "publicKey";
+	String E_PUBLIC_KEY = "ephemeralPublicKey";
+	String MSG = "msg";
+	String ACCEPT = "accept";
+	String TIME = "time";
+	String DEVICE_ID = "deviceId";
+	String TRANSPORT = "transport";
+	String MESSAGE_ID = "messageId";
+	String MESSAGE_TIME = "timestamp";
+
+	/* Introducer Local State Metadata */
+	String STATE = "state";
+	String ROLE = "role";
+	String GROUP_ID_1 = "groupId1";
+	String GROUP_ID_2 = "groupId2";
+	String CONTACT_1 = "contact1";
+	String CONTACT_2 = "contact2";
+	String AUTHOR_ID_1 = "authorId1";
+	String AUTHOR_ID_2 = "authorId2";
+	String CONTACT_ID_1 = "contactId1";
+	String CONTACT_ID_2 = "contactId2";
+	String RESPONSE_1 = "response1";
+	String RESPONSE_2 = "response2";
+	String READ = "read";
+
+	/* Introduction Request Action */
+	String PUBLIC_KEY1 = "publicKey1";
+	String PUBLIC_KEY2 = "publicKey2";
+
+	/* Introducee Local State Metadata (without those already defined) */
+	String STORAGE_ID = "storageId";
+	String INTRODUCER = "introducer";
+	String LOCAL_AUTHOR_ID = "localAuthorId";
+	String REMOTE_AUTHOR_ID = "remoteAuthorId";
+	String OUR_PUBLIC_KEY = "ourEphemeralPublicKey";
+	String OUR_PRIVATE_KEY = "ourEphemeralPrivateKey";
+	String OUR_TIME = "ourTime";
+	String ADDED_CONTACT_ID = "addedContactId";
+	String NOT_OUR_RESPONSE = "notOurResponse";
+	String EXISTS = "contactExists";
+	String ANSWERED = "answered";
+
+	String TASK = "task";
+	int TASK_ADD_CONTACT = 0;
+	int TASK_ACTIVATE_CONTACT = 1;
+	int TASK_ABORT = 2;
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..882ccbf112a67be92c15f711fbc869384a3cb029
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java
@@ -0,0 +1,62 @@
+package org.briarproject.api.introduction;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.Collection;
+
+public interface IntroductionManager {
+
+	/** Returns the unique ID of the introduction client. */
+	ClientId getClientId();
+
+	/**
+	 * sends two initial introduction messages
+	 */
+	void makeIntroduction(Contact c1, Contact c2, String msg)
+			throws DbException, FormatException;
+
+	/**
+	 * Accept an introduction that had been made
+	 */
+	void acceptIntroduction(final SessionId sessionId)
+			throws DbException, FormatException;
+
+	/**
+	 * Decline an introduction that had been made
+	 */
+	void declineIntroduction(final SessionId sessionId)
+			throws DbException, FormatException;
+
+	/**
+	 * Get all introduction messages for the contact with this contactId
+	 */
+	Collection<IntroductionMessage> getIntroductionMessages(ContactId contactId)
+			throws DbException;
+
+	/** Marks an introduction message as read or unread. */
+	void setReadFlag(MessageId m, boolean read) throws DbException;
+
+
+	/** Get the session state for the given session ID */
+	BdfDictionary getSessionState(Transaction txn, byte[] sessionId)
+			throws DbException, FormatException;
+
+	/** Gets the group used for introductions with Contact c */
+	Group getIntroductionGroup(Contact c);
+
+	/** Get the local group used to store session states */
+	Group getLocalGroup();
+
+	/** Send an introduction message */
+	void sendMessage(Transaction txn, BdfDictionary message)
+			throws DbException, FormatException;
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ac98793f208356cd66a3b4f6c41004f70727bc6
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java
@@ -0,0 +1,54 @@
+package org.briarproject.api.introduction;
+
+import org.briarproject.api.sync.MessageId;
+
+abstract public class IntroductionMessage {
+
+	private final SessionId sessionId;
+	private final MessageId messageId;
+	private final long time;
+	private final boolean local, sent, seen, read;
+
+	public IntroductionMessage(SessionId sessionId, MessageId messageId,
+			long time, boolean local, boolean sent, boolean seen,
+			boolean read) {
+
+		this.sessionId = sessionId;
+		this.messageId = messageId;
+		this.time = time;
+		this.local = local;
+		this.sent = sent;
+		this.seen = seen;
+		this.read = read;
+	}
+
+	public SessionId getSessionId() {
+		return sessionId;
+	}
+
+	public long getTime() {
+		return time;
+	}
+
+	public MessageId getMessageId() {
+		return messageId;
+	}
+
+	public boolean isLocal() {
+		return local;
+	}
+
+	public boolean isSent() {
+		return sent;
+	}
+
+	public boolean isSeen() {
+		return seen;
+	}
+
+	public boolean isRead() {
+		return read;
+	}
+
+}
+
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
new file mode 100644
index 0000000000000000000000000000000000000000..facd7151fbf564b1afe1f1327c3b119781416dfe
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java
@@ -0,0 +1,36 @@
+package org.briarproject.api.introduction;
+
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.MessageId;
+
+public class IntroductionRequest extends IntroductionResponse {
+
+	private final String message;
+	private final boolean answered, exists;
+
+	public IntroductionRequest(SessionId sessionId, MessageId messageId,
+			long time, boolean local, boolean sent, boolean seen, boolean read,
+			AuthorId authorId, String name, boolean accepted, String message,
+			boolean answered, boolean exists) {
+
+		super(sessionId, messageId, time, local, sent, seen, read, authorId,
+				name, accepted);
+
+		this.message = message;
+		this.answered = answered;
+		this.exists = exists;
+	}
+
+	public String getMessage() {
+		return message;
+	}
+
+	public boolean wasAnswered() {
+		return answered;
+	}
+
+	public boolean doesExist() {
+		return exists;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..e73065bcc0800ea4d874521e84ed3fb8dafd2bcd
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java
@@ -0,0 +1,31 @@
+package org.briarproject.api.introduction;
+
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.sync.MessageId;
+
+public class IntroductionResponse extends IntroductionMessage {
+
+	private final AuthorId remoteAuthorId;
+	private final String name;
+	private final boolean accepted;
+
+	public IntroductionResponse(SessionId sessionId, MessageId messageId,
+			long time, boolean local, boolean sent, boolean seen, boolean read,
+			AuthorId remoteAuthorId, String name, boolean accepted) {
+
+		super(sessionId, messageId, time, local, sent, seen, read);
+
+		this.remoteAuthorId = remoteAuthorId;
+		this.name = name;
+		this.accepted = accepted;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public boolean wasAccepted() {
+		return accepted;
+	}
+
+}
diff --git a/briar-api/src/org/briarproject/api/introduction/SessionId.java b/briar-api/src/org/briarproject/api/introduction/SessionId.java
new file mode 100644
index 0000000000000000000000000000000000000000..d68bf9e0301ecf48ca1e61ab58a2772a7458846a
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/introduction/SessionId.java
@@ -0,0 +1,19 @@
+package org.briarproject.api.introduction;
+
+import org.briarproject.api.sync.MessageId;
+
+/**
+ * Type-safe wrapper for a byte array that uniquely identifies an
+ * introduction session.
+ */
+public class SessionId extends MessageId {
+
+	public SessionId(byte[] id) {
+		super(id);
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return o instanceof SessionId && super.equals(o);
+	}
+}
diff --git a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java
index 9dc655543950ec88500749ffc6c0d68c177d0cb9..445329ba95a6100c1def5d4a9f87363c5b3a3f94 100644
--- a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java
+++ b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java
@@ -4,6 +4,7 @@ import org.briarproject.api.DeviceId;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
 
 import java.util.Map;
 
@@ -13,7 +14,7 @@ public interface TransportPropertyManager {
 	 * Stores the given properties received while adding a contact - they will
 	 * be superseded by any properties synced from the contact.
 	 */
-	void addRemoteProperties(ContactId c, DeviceId dev,
+	void addRemoteProperties(Transaction txn, ContactId c, DeviceId dev,
 			Map<TransportId, TransportProperties> props) throws DbException;
 
 	/** Returns the local transport properties for all transports. */
diff --git a/briar-core/src/org/briarproject/CoreEagerSingletons.java b/briar-core/src/org/briarproject/CoreEagerSingletons.java
index 00d46b1da768305a8b0cf484befdb60015a1bf58..275b938a6f7f7d995ec312ea120527bd4d7cda19 100644
--- a/briar-core/src/org/briarproject/CoreEagerSingletons.java
+++ b/briar-core/src/org/briarproject/CoreEagerSingletons.java
@@ -4,6 +4,7 @@ import org.briarproject.contact.ContactModule;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.db.DatabaseModule;
 import org.briarproject.forum.ForumModule;
+import org.briarproject.introduction.IntroductionModule;
 import org.briarproject.lifecycle.LifecycleModule;
 import org.briarproject.messaging.MessagingModule;
 import org.briarproject.plugins.PluginsModule;
@@ -16,6 +17,7 @@ public interface CoreEagerSingletons {
 	void inject(CryptoModule.EagerSingletons init);
 	void inject(DatabaseModule.EagerSingletons init);
 	void inject(ForumModule.EagerSingletons init);
+	void inject(IntroductionModule.EagerSingletons init);
 	void inject(LifecycleModule.EagerSingletons init);
 	void inject(MessagingModule.EagerSingletons init);
 	void inject(PluginsModule.EagerSingletons init);
diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java
index 6303f210b3129e5a28e7121509aa423101c5c03c..d18e468f6ca31934a0565f50799c90e477a9c225 100644
--- a/briar-core/src/org/briarproject/CoreModule.java
+++ b/briar-core/src/org/briarproject/CoreModule.java
@@ -8,6 +8,7 @@ import org.briarproject.db.DatabaseModule;
 import org.briarproject.event.EventModule;
 import org.briarproject.forum.ForumModule;
 import org.briarproject.identity.IdentityModule;
+import org.briarproject.introduction.IntroductionModule;
 import org.briarproject.invitation.InvitationModule;
 import org.briarproject.keyagreement.KeyAgreementModule;
 import org.briarproject.lifecycle.LifecycleModule;
@@ -29,7 +30,7 @@ import dagger.Module;
 		IdentityModule.class, EventModule.class, DataModule.class,
 		ContactModule.class, PropertiesModule.class, TransportModule.class,
 		SyncModule.class, SettingsModule.class, ClientsModule.class,
-		SystemModule.class, PluginsModule.class})
+		SystemModule.class, PluginsModule.class, IntroductionModule.class})
 public class CoreModule {
 
 	public static void initEagerSingletons(CoreEagerSingletons c) {
@@ -43,5 +44,6 @@ public class CoreModule {
 		c.inject(new PropertiesModule.EagerSingletons());
 		c.inject(new SyncModule.EagerSingletons());
 		c.inject(new TransportModule.EagerSingletons());
+		c.inject(new IntroductionModule.EagerSingletons());
 	}
 }
diff --git a/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java b/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java
index b2503b74028128dab4e706e8a226a6efe90968ce..eb7b0fc6aa5a7b2d23fba8118f0464525aa01b83 100644
--- a/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java
+++ b/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java
@@ -21,6 +21,7 @@ import org.briarproject.api.sync.ValidationManager.IncomingMessageHook;
 import org.briarproject.util.ByteUtils;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map.Entry;
@@ -63,9 +64,20 @@ class MessageQueueManagerImpl implements MessageQueueManager {
 		QueueState queueState = loadQueueState(txn, queue.getId());
 		long queuePosition = queueState.outgoingPosition;
 		queueState.outgoingPosition++;
+		if (LOG.isLoggable(INFO)) {
+			LOG.info("Sending message with position " +
+					queuePosition + " in group " +
+					queue.getId().hashCode() + " with transaction " +
+					txn.hashCode());
+		}
 		saveQueueState(txn, queue.getId(), queueState);
 		QueueMessage q = queueMessageFactory.createMessage(queue.getId(),
 				timestamp, queuePosition, body);
+		if (LOG.isLoggable(INFO)) {
+			LOG.info("First bytes of message: " + Arrays.toString(
+					Arrays.copyOfRange(q.getRaw(), 0,
+							QUEUE_MESSAGE_HEADER_LENGTH)));
+		}
 		db.addLocalMessage(txn, q, queue.getClientId(), meta, true);
 		return q;
 	}
@@ -196,7 +208,12 @@ class MessageQueueManagerImpl implements MessageQueueManager {
 			if (LOG.isLoggable(INFO)) {
 				LOG.info("Received message with position  "
 						+ queuePosition + ", expecting "
-						+ queueState.incomingPosition);
+						+ queueState.incomingPosition + ". Received in group " +
+						m.getGroupId().hashCode() + " with transaction " +
+						txn.hashCode());
+				LOG.info("First bytes of message: " + Arrays.toString(
+						Arrays.copyOfRange(m.getRaw(), 0,
+								QUEUE_MESSAGE_HEADER_LENGTH)));
 			}
 			if (queuePosition < queueState.incomingPosition) {
 				// A message with this queue position has already been seen
diff --git a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
index 7ff709b70c9062f79d9cb29c72979a405daadae0..23ec838802d8ad75c512e970720a185b566e254c 100644
--- a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
+++ b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java
@@ -46,6 +46,18 @@ class ContactManagerImpl implements ContactManager, RemoveIdentityHook {
 		removeHooks.add(hook);
 	}
 
+	@Override
+	public ContactId addContact(Transaction txn, Author remote, AuthorId local,
+			SecretKey master,long timestamp, boolean alice, boolean active)
+			throws DbException {
+		ContactId c = db.addContact(txn, remote, local, active);
+		keyManager.addContact(txn, c, master, timestamp, alice);
+		Contact contact = db.getContact(txn, c);
+		for (AddContactHook hook : addHooks)
+			hook.addingContact(txn, contact);
+		return c;
+	}
+
 	@Override
 	public ContactId addContact(Author remote, AuthorId local, SecretKey master,
 			long timestamp, boolean alice, boolean active)
@@ -53,11 +65,8 @@ class ContactManagerImpl implements ContactManager, RemoveIdentityHook {
 		ContactId c;
 		Transaction txn = db.startTransaction(false);
 		try {
-			c = db.addContact(txn, remote, local, active);
-			keyManager.addContact(txn, c, master, timestamp, alice);
-			Contact contact = db.getContact(txn, c);
-			for (AddContactHook hook : addHooks)
-				hook.addingContact(txn, contact);
+			c = addContact(txn, remote, local, master, timestamp, alice,
+					active);
 			txn.setComplete();
 		} finally {
 			db.endTransaction(txn);
@@ -116,6 +125,26 @@ class ContactManagerImpl implements ContactManager, RemoveIdentityHook {
 		}
 	}
 
+	@Override
+	public boolean contactExists(Transaction txn, AuthorId remoteAuthorID,
+			AuthorId localAuthorId) throws DbException {
+		return db.containsContact(txn, remoteAuthorID, localAuthorId);
+	}
+
+	@Override
+	public boolean contactExists(AuthorId remoteAuthorID,
+			AuthorId localAuthorId) throws DbException {
+		boolean exists = false;
+		Transaction txn = db.startTransaction(true);
+		try {
+			exists = contactExists(txn, remoteAuthorID, localAuthorId);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+		return exists;
+	}
+
 	private void removeContact(Transaction txn, ContactId c)
 			throws DbException {
 		Contact contact = db.getContact(txn, c);
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index d00159903596c83da6d3750dc115571dcab6215e..d3e48748c6dea3c2fb745476cf30f9f470be8f5d 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -161,7 +161,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		if (db.containsContact(txn, remote.getId(), local))
 			throw new ContactExistsException();
 		ContactId c = db.addContact(txn, remote, local, active);
-		transaction.attach(new ContactAddedEvent(c));
+		transaction.attach(new ContactAddedEvent(c, active));
 		if (active) transaction.attach(new ContactStatusChangedEvent(c, true));
 		return c;
 	}
@@ -342,6 +342,14 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		return db.getContacts(txn, a);
 	}
 
+	public boolean containsContact(Transaction transaction, AuthorId remote,
+			AuthorId local) throws DbException {
+		T txn = unbox(transaction);
+		if (!db.containsLocalAuthor(txn, local))
+			throw new NoSuchLocalAuthorException();
+		return db.containsContact(txn, remote, local);
+	}
+
 	public DeviceId getDeviceId(Transaction transaction) throws DbException {
 		T txn = unbox(transaction);
 		return db.getDeviceId(txn);
diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..7be3184086617a1e34fff0aab621b3f390e3fb3f
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java
@@ -0,0 +1,371 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.ProtocolEngine;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.IntroductionRequestReceivedEvent;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.introduction.IntroduceeAction;
+import org.briarproject.api.introduction.IntroduceeProtocolState;
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.introduction.SessionId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ABORT;
+import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ACCEPT;
+import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_DECLINE;
+import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ABORT;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_ACK;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REMOTE_RESPONSE;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_RESPONSES;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.ERROR;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.FINISHED;
+import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
+import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
+import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.MSG;
+import static org.briarproject.api.introduction.IntroductionConstants.NAME;
+import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
+import static org.briarproject.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.STATE;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK_ADD_CONTACT;
+import static org.briarproject.api.introduction.IntroductionConstants.TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+public class IntroduceeEngine
+		implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
+
+	private static final Logger LOG =
+			Logger.getLogger(IntroduceeEngine.class.getName());
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
+			BdfDictionary localState, BdfDictionary localAction) {
+
+		try {
+			IntroduceeProtocolState currentState =
+					getState(localState.getLong(STATE));
+			int type = localAction.getLong(TYPE).intValue();
+			IntroduceeAction action;
+			if (localState.containsKey(ACCEPT)) action = IntroduceeAction
+					.getLocal(type, localState.getBoolean(ACCEPT));
+			else action = IntroduceeAction.getLocal(type);
+			IntroduceeProtocolState nextState = currentState.next(action);
+
+			if (action == LOCAL_ABORT && currentState != ERROR) {
+				return abortSession(currentState, localState);
+			}
+
+			if (nextState == ERROR) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Error: Invalid action in state " +
+							currentState.name());
+				}
+				if (currentState == ERROR) return noUpdate(localState);
+				else abortSession(currentState, localState);
+			}
+
+			if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) {
+				localState.put(STATE, nextState.getValue());
+				localState.put(ANSWERED, true);
+				List<BdfDictionary> messages = new ArrayList<BdfDictionary>(1);
+				// create the introduction response message
+				BdfDictionary msg = new BdfDictionary();
+				msg.put(TYPE, TYPE_RESPONSE);
+				msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+				msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+				msg.put(ACCEPT, localState.getBoolean(ACCEPT));
+				if (localState.getBoolean(ACCEPT)) {
+					msg.put(TIME, localState.getLong(OUR_TIME));
+					msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY));
+					msg.put(DEVICE_ID, localAction.getRaw(DEVICE_ID));
+					msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT));
+				}
+				messages.add(msg);
+				logAction(currentState, localState, msg);
+
+				if (nextState == AWAIT_ACK) {
+					localState.put(TASK, TASK_ADD_CONTACT);
+					// also send ACK, because we already have the other response
+					BdfDictionary ack = getAckMessage(localState);
+					messages.add(ack);
+				}
+				List<Event> events = Collections.emptyList();
+				return new StateUpdate<BdfDictionary, BdfDictionary>(false,
+						false,
+						localState, messages, events);
+			} else {
+				throw new IllegalArgumentException();
+			}
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
+			BdfDictionary localState, BdfDictionary msg) {
+
+		try {
+			IntroduceeProtocolState currentState =
+					getState(localState.getLong(STATE));
+			int type = msg.getLong(TYPE).intValue();
+			IntroduceeAction action = IntroduceeAction.getRemote(type);
+			IntroduceeProtocolState nextState = currentState.next(action);
+
+			logMessageReceived(currentState, nextState, localState, type, msg);
+
+			if (nextState == ERROR) {
+				if (currentState != ERROR && action != REMOTE_ABORT) {
+					return abortSession(currentState, localState);
+				} else {
+					return noUpdate(localState);
+				}
+			}
+
+			// update local session state with next protocol state
+			localState.put(STATE, nextState.getValue());
+			List<BdfDictionary> messages;
+			List<Event> events;
+			// we received the introduction request
+			if (currentState == AWAIT_REQUEST) {
+				// remember the session ID used by the introducer
+				localState.put(SESSION_ID, msg.getRaw(SESSION_ID));
+
+				addRequestData(localState, msg);
+				messages = Collections.emptyList();
+				events = Collections.singletonList(getEvent(localState, msg));
+			}
+			// we had the request and now one response came in _OR_
+			// we had sent our response already and now received the other one
+			else if (currentState == AWAIT_RESPONSES ||
+					currentState == AWAIT_REMOTE_RESPONSE) {
+				// update next state based on message content
+				action = IntroduceeAction
+						.getRemote(type, msg.getBoolean(ACCEPT));
+				nextState = currentState.next(action);
+				localState.put(STATE, nextState.getValue());
+
+				addResponseData(localState, msg);
+				if (nextState == AWAIT_ACK) {
+					localState.put(TASK, TASK_ADD_CONTACT);
+					messages = Collections
+							.singletonList(getAckMessage(localState));
+				} else {
+					messages = Collections.emptyList();
+				}
+				events = Collections.emptyList();
+			}
+			// we already sent our ACK and now received the other one
+			else if (currentState == AWAIT_ACK) {
+				localState.put(TASK, TASK_ACTIVATE_CONTACT);
+				messages = Collections.emptyList();
+				events = Collections.emptyList();
+			}
+			// we are done (probably declined response) and ignore this message
+			else if (currentState == FINISHED) {
+				return noUpdate(localState);
+			}
+			// this should not happen
+			else {
+				throw new IllegalArgumentException();
+			}
+			return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+					localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	private void addRequestData(BdfDictionary localState, BdfDictionary msg)
+			throws FormatException {
+
+		localState.put(NAME, msg.getString(NAME));
+		localState.put(PUBLIC_KEY, msg.getRaw(PUBLIC_KEY));
+		if (msg.containsKey(MSG)) {
+			localState.put(MSG, msg.getString(MSG));
+		}
+	}
+
+	private void addResponseData(BdfDictionary localState, BdfDictionary msg)
+			throws FormatException {
+
+		if (localState.containsKey(ACCEPT)) {
+			localState.put(ACCEPT,
+					localState.getBoolean(ACCEPT) && msg.getBoolean(ACCEPT));
+		} else {
+			localState.put(ACCEPT, msg.getBoolean(ACCEPT));
+		}
+		localState.put(NOT_OUR_RESPONSE, msg.getRaw(MESSAGE_ID));
+
+		if (msg.getBoolean(ACCEPT)) {
+			localState.put(TIME, msg.getLong(TIME));
+			localState.put(E_PUBLIC_KEY, msg.getRaw(E_PUBLIC_KEY));
+			localState.put(DEVICE_ID, msg.getRaw(DEVICE_ID));
+			localState.put(TRANSPORT, msg.getDictionary(TRANSPORT));
+		}
+	}
+
+	private BdfDictionary getAckMessage(BdfDictionary localState)
+			throws FormatException {
+
+		BdfDictionary m = new BdfDictionary();
+		m.put(TYPE, TYPE_ACK);
+		m.put(GROUP_ID, localState.getRaw(GROUP_ID));
+		m.put(SESSION_ID, localState.getRaw(SESSION_ID));
+
+		if (LOG.isLoggable(INFO)) {
+			LOG.info("Sending ACK " + " to " +
+					localState.getString(INTRODUCER) + " for " +
+					localState.getString(NAME) + " with session ID " +
+					Arrays.hashCode(m.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(m.getRaw(GROUP_ID)));
+		}
+		return m;
+	}
+
+	private void logAction(IntroduceeProtocolState state,
+			BdfDictionary localState, BdfDictionary msg) {
+
+		if (!LOG.isLoggable(INFO)) return;
+
+		try {
+			LOG.info("Sending " +
+					(localState.getBoolean(ACCEPT) ? "accept " : "decline ") +
+					"response in state " + state.name() +
+					" to " + localState.getString(INTRODUCER) +
+					" for " + localState.getString(NAME) + " with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " +
+					getState(localState.getLong(STATE)).name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private void logMessageReceived(IntroduceeProtocolState currentState,
+			IntroduceeProtocolState nextState, BdfDictionary localState,
+			int type, BdfDictionary msg) {
+
+		if (!LOG.isLoggable(INFO)) return;
+
+		try {
+			String t = "unknown";
+			if (type == TYPE_REQUEST) t = "Introduction";
+			else if (type == TYPE_RESPONSE) t = "Response";
+			else if (type == TYPE_ACK) t = "ACK";
+			else if (type == TYPE_ABORT) t = "Abort";
+
+			LOG.info("Received " + t + " in state " + currentState.name() +
+					" from " + localState.getString(INTRODUCER) +
+					(localState.containsKey(NAME) ?
+							" related to " + localState.getString(NAME) : "") +
+					" with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " + nextState.name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
+			BdfDictionary localState, BdfDictionary delivered) {
+		try {
+			return noUpdate(localState);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return null;
+		}
+	}
+
+	private IntroduceeProtocolState getState(Long state) {
+		return IntroduceeProtocolState.fromValue(state.intValue());
+	}
+
+	private Event getEvent(BdfDictionary localState, BdfDictionary msg)
+			throws FormatException {
+
+		ContactId contactId =
+				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
+		AuthorId authorId = new AuthorId(localState.getRaw(REMOTE_AUTHOR_ID));
+
+		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
+		MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
+		long time = msg.getLong(MESSAGE_TIME);
+		String name = msg.getString(NAME);
+		String message = msg.getOptionalString(MSG);
+		boolean exists = localState.getBoolean(EXISTS);
+
+		IntroductionRequest ir = new IntroductionRequest(sessionId, messageId,
+				time, false, false, false, false, authorId, name, false,
+				message, false, exists);
+		return new IntroductionRequestReceivedEvent(contactId, ir);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
+			IntroduceeProtocolState currentState, BdfDictionary localState)
+			throws FormatException {
+
+		if (LOG.isLoggable(WARNING)) {
+			LOG.warning("Aborting protocol session " +
+					Arrays.hashCode(localState.getRaw(SESSION_ID)) +
+					" in state " + currentState.name());
+		}
+
+		localState.put(STATE, ERROR.getValue());
+		localState.put(TASK, TASK_ABORT);
+		BdfDictionary msg = new BdfDictionary();
+		msg.put(TYPE, TYPE_ABORT);
+		msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
+		msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
+		List<BdfDictionary> messages = Collections.singletonList(msg);
+		// TODO inform about protocol abort via new Event?
+		List<Event> events = Collections.emptyList();
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, messages, events);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
+			BdfDictionary localState) throws FormatException {
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, new ArrayList<BdfDictionary>(0),
+				new ArrayList<Event>(0));
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..2049fba3ff5d636ee2acb8b48090184b3b77c715
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java
@@ -0,0 +1,406 @@
+package org.briarproject.introduction;
+
+
+import org.briarproject.api.Bytes;
+import org.briarproject.api.DeviceId;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.KeyParser;
+import org.briarproject.api.crypto.PrivateKey;
+import org.briarproject.api.crypto.PublicKey;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.IntroductionSucceededEvent;
+import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.introduction.SessionId;
+import org.briarproject.api.properties.TransportProperties;
+import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.system.Clock;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
+import static org.briarproject.api.introduction.IntroductionConstants.ADDED_CONTACT_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
+import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
+import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.NAME;
+import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
+import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
+import static org.briarproject.api.introduction.IntroductionConstants.STATE;
+import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT;
+import static org.briarproject.api.introduction.IntroductionConstants.TASK_ADD_CONTACT;
+import static org.briarproject.api.introduction.IntroductionConstants.TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+class IntroduceeManager {
+
+
+	private static final Logger LOG =
+			Logger.getLogger(IntroduceeManager.class.getName());
+
+	private final DatabaseComponent db;
+	private final IntroductionManager introductionManager;
+	private final ClientHelper clientHelper;
+	private final Clock clock;
+	private final CryptoComponent cryptoComponent;
+	private final TransportPropertyManager transportPropertyManager;
+	private final AuthorFactory authorFactory;
+	private final ContactManager contactManager;
+
+	IntroduceeManager(DatabaseComponent db,
+			IntroductionManager introductionManager, ClientHelper clientHelper,
+			Clock clock, CryptoComponent cryptoComponent,
+			TransportPropertyManager transportPropertyManager,
+			AuthorFactory authorFactory, ContactManager contactManager) {
+
+		this.db = db;
+		this.introductionManager = introductionManager;
+		this.clientHelper = clientHelper;
+		this.clock = clock;
+		this.cryptoComponent = cryptoComponent;
+		this.transportPropertyManager = transportPropertyManager;
+		this.authorFactory = authorFactory;
+		this.contactManager = contactManager;
+	}
+
+	public BdfDictionary initialize(Transaction txn, GroupId groupId,
+			BdfDictionary message) throws DbException, FormatException {
+
+		// create local message to keep engine state
+		long now = clock.currentTimeMillis();
+		Bytes salt = new Bytes(new byte[64]);
+		cryptoComponent.getSecureRandom().nextBytes(salt.getBytes());
+
+		Message localMsg = clientHelper
+				.createMessage(introductionManager.getLocalGroup().getId(), now,
+						BdfList.of(salt));
+		MessageId storageId = localMsg.getId();
+
+		// find out who is introducing us
+		BdfDictionary gd =
+				clientHelper.getGroupMetadataAsDictionary(txn, groupId);
+		ContactId introducerId =
+				new ContactId(gd.getLong(CONTACT).intValue());
+		Contact introducer = db.getContact(txn, introducerId);
+
+		BdfDictionary d = new BdfDictionary();
+		d.put(STORAGE_ID, storageId);
+		d.put(STATE, AWAIT_REQUEST.getValue());
+		d.put(ROLE, ROLE_INTRODUCEE);
+		d.put(GROUP_ID, groupId);
+		d.put(INTRODUCER, introducer.getAuthor().getName());
+		d.put(CONTACT_ID_1, introducer.getId().getInt());
+		d.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes());
+		d.put(NOT_OUR_RESPONSE, new byte[0]);
+		d.put(ANSWERED, false);
+
+		// check if the contact we are introduced to does already exist
+		AuthorId remoteAuthorId = authorFactory
+				.createAuthor(message.getString(NAME),
+						message.getRaw(PUBLIC_KEY)).getId();
+		boolean exists = contactManager.contactExists(txn, remoteAuthorId,
+				introducer.getLocalAuthorId());
+		d.put(EXISTS, exists);
+		d.put(REMOTE_AUTHOR_ID, remoteAuthorId);
+
+		// save local state to database
+		clientHelper.addLocalMessage(txn, localMsg,
+				introductionManager.getClientId(), d, false);
+
+		return d;
+	}
+
+	public void incomingMessage(Transaction txn, BdfDictionary state,
+			BdfDictionary message) throws DbException, FormatException {
+
+		IntroduceeEngine engine = new IntroduceeEngine();
+		processStateUpdate(txn, engine.onMessageReceived(state, message));
+	}
+
+	public void acceptIntroduction(Transaction txn,
+			final SessionId sessionId) throws DbException, FormatException {
+
+		BdfDictionary state =
+				introductionManager.getSessionState(txn, sessionId.getBytes());
+
+		// get data to connect and derive a shared secret later
+		long now = clock.currentTimeMillis();
+		byte[] deviceId = db.getDeviceId(txn).getBytes();
+		KeyPair keyPair = cryptoComponent.generateAgreementKeyPair();
+		byte[] publicKey = keyPair.getPublic().getEncoded();
+		byte[] privateKey = keyPair.getPrivate().getEncoded();
+		Map<TransportId, TransportProperties> transportProperties =
+				transportPropertyManager.getLocalProperties();
+
+		// update session state for later
+		state.put(ACCEPT, true);
+		state.put(OUR_TIME, now);
+		state.put(OUR_PUBLIC_KEY, publicKey);
+		state.put(OUR_PRIVATE_KEY, privateKey);
+
+		// define action
+		BdfDictionary localAction = new BdfDictionary();
+		localAction.put(TYPE, TYPE_RESPONSE);
+		localAction.put(DEVICE_ID, deviceId);
+		localAction.put(TRANSPORT,
+				encodeTransportProperties(transportProperties));
+
+		// start engine and process its state update
+		IntroduceeEngine engine = new IntroduceeEngine();
+		processStateUpdate(txn,
+				engine.onLocalAction(state, localAction));
+	}
+
+	public void declineIntroduction(Transaction txn, final SessionId sessionId)
+			throws DbException, FormatException {
+
+		BdfDictionary state =
+				introductionManager.getSessionState(txn, sessionId.getBytes());
+
+		// update session state
+		state.put(ACCEPT, false);
+
+		// define action
+		BdfDictionary localAction = new BdfDictionary();
+		localAction.put(TYPE, TYPE_RESPONSE);
+
+		// start engine and process its state update
+		IntroduceeEngine engine = new IntroduceeEngine();
+		processStateUpdate(txn,
+				engine.onLocalAction(state, localAction));
+	}
+
+	private void processStateUpdate(Transaction txn,
+			IntroduceeEngine.StateUpdate<BdfDictionary, BdfDictionary>
+					result) throws DbException, FormatException {
+
+		// perform actions based on new local state
+		performTasks(txn, result.localState);
+
+		// save new local state
+		MessageId storageId =
+				new MessageId(result.localState.getRaw(STORAGE_ID));
+		clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
+
+		// send messages
+		for (BdfDictionary d : result.toSend) {
+			introductionManager.sendMessage(txn, d);
+		}
+
+		// broadcast events
+		for (Event event : result.toBroadcast) {
+			txn.attach(event);
+		}
+	}
+
+	private void performTasks(Transaction txn, BdfDictionary localState)
+			throws FormatException, DbException {
+
+		if (!localState.containsKey(TASK)) return;
+
+		// remember task and remove it from localState
+		long task = localState.getLong(TASK);
+		localState.put(TASK, BdfDictionary.NULL_VALUE);
+
+
+
+		if (task == TASK_ADD_CONTACT) {
+			if (localState.getBoolean(EXISTS)) {
+				// we have this contact already, so do not perform actions
+				LOG.info("We have this contact already, do not add");
+				return;
+			}
+
+			LOG.info("Adding contact in inactive state");
+
+			// get all keys
+			KeyParser keyParser = cryptoComponent.getAgreementKeyParser();
+			byte[] publicKeyBytes;
+			PublicKey publicKey;
+			PrivateKey privateKey;
+			try {
+				publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY);
+				publicKey = keyParser
+						.parsePublicKey(publicKeyBytes);
+				privateKey = keyParser.parsePrivateKey(
+						localState.getRaw(OUR_PRIVATE_KEY));
+			} catch (GeneralSecurityException e) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.log(WARNING, e.toString(), e);
+				}
+				// we can not continue without the keys
+				throw new RuntimeException("Our own ephemeral key is invalid");
+			}
+			KeyPair keyPair = new KeyPair(publicKey, privateKey);
+			byte[] theirEphemeralKey = localState.getRaw(E_PUBLIC_KEY);
+
+			// figure out who takes which role by comparing public keys
+			int comp = Bytes.COMPARATOR.compare(new Bytes(publicKeyBytes),
+					new Bytes(theirEphemeralKey));
+			boolean alice = comp < 0;
+
+			// The master secret is derived from the local ephemeral key pair
+			// and the remote ephemeral public key
+			SecretKey secretKey;
+			try {
+				secretKey = cryptoComponent
+						.deriveMasterSecret(theirEphemeralKey, keyPair, alice);
+			} catch (GeneralSecurityException e) {
+				if (LOG.isLoggable(WARNING))
+					LOG.log(WARNING, e.toString(), e);
+				// we can not continue without the shared secret
+				throw new FormatException();
+			}
+
+			// The agreed timestamp is the minimum of the peers' timestamps
+			long ourTime = localState.getLong(OUR_TIME);
+			long theirTime = localState.getLong(TIME);
+			long timestamp = Math.min(ourTime, theirTime);
+
+			// Add the contact to the database
+			AuthorId localAuthorId =
+					new AuthorId(localState.getRaw(LOCAL_AUTHOR_ID));
+			Author remoteAuthor = authorFactory
+					.createAuthor(localState.getString(NAME),
+							localState.getRaw(PUBLIC_KEY));
+			ContactId contactId = contactManager
+					.addContact(txn, remoteAuthor, localAuthorId, secretKey,
+							timestamp, alice, false);
+
+			// Update local state with ContactId, so we know what to activate
+			localState.put(ADDED_CONTACT_ID, contactId.getInt());
+
+			// let the transport manager know how to connect to the contact
+			DeviceId deviceId = new DeviceId(localState.getRaw(DEVICE_ID));
+			Map<TransportId, TransportProperties> transportProperties =
+					parseTransportProperties(localState);
+			transportPropertyManager.addRemoteProperties(txn, contactId,
+					deviceId, transportProperties);
+
+			// delete the ephemeral private key by overwriting with NULL value
+			// this ensures future ephemeral keys can not be recovered when
+			// this device should gets compromised
+			localState.put(OUR_PRIVATE_KEY, BdfDictionary.NULL_VALUE);
+		}
+
+		// we sent and received an ACK, so activate contact
+		if (task == TASK_ACTIVATE_CONTACT) {
+			if (!localState.getBoolean(EXISTS) &&
+					localState.containsKey(ADDED_CONTACT_ID)) {
+
+				LOG.info("Activating Contact...");
+
+				ContactId contactId = new ContactId(
+						localState.getLong(ADDED_CONTACT_ID).intValue());
+
+				// activate and show contact in contact list
+				db.setContactActive(txn, contactId, true);
+
+				// broadcast event informing of successful introduction
+				Contact contact = db.getContact(txn, contactId);
+				Event event = new IntroductionSucceededEvent(contact);
+				txn.attach(event);
+			} else {
+				LOG.info(
+						"We must have had this contact already, not activating...");
+			}
+		}
+
+		// we need to abort the protocol, clean up what has been done
+		if (task == TASK_ABORT) {
+			if (localState.containsKey(ADDED_CONTACT_ID)) {
+				LOG.info("Deleting added contact due to abort...");
+				ContactId contactId = new ContactId(
+						localState.getLong(ADDED_CONTACT_ID).intValue());
+				contactManager.removeContact(contactId);
+			}
+		}
+
+	}
+
+	public void abort(Transaction txn, BdfDictionary state) {
+
+		IntroduceeEngine engine = new IntroduceeEngine();
+		BdfDictionary localAction = new BdfDictionary();
+		localAction.put(TYPE, TYPE_ABORT);
+		try {
+			processStateUpdate(txn,
+					engine.onLocalAction(state, localAction));
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private BdfDictionary encodeTransportProperties(
+			Map<TransportId, TransportProperties> map) {
+
+		BdfDictionary d = new BdfDictionary();
+		for (Map.Entry<TransportId, TransportProperties> e : map.entrySet()) {
+			d.put(e.getKey().getString(), e.getValue());
+		}
+		return d;
+	}
+
+	private Map<TransportId, TransportProperties> parseTransportProperties(
+			BdfDictionary d) throws FormatException {
+
+		Map<TransportId, TransportProperties> tpMap =
+				new HashMap<TransportId, TransportProperties>();
+		BdfDictionary tpMapDict = d.getDictionary(TRANSPORT);
+		for (String key : tpMapDict.keySet()) {
+			TransportId transportId = new TransportId(key);
+			TransportProperties transportProperties = new TransportProperties();
+			BdfDictionary tpDict = tpMapDict.getDictionary(key);
+			for (String tkey : tpDict.keySet()) {
+				transportProperties.put(tkey, tpDict.getString(tkey));
+			}
+			tpMap.put(transportId, transportProperties);
+		}
+		return tpMap;
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
new file mode 100644
index 0000000000000000000000000000000000000000..fb88c28df61de2cb1d8bb1a1eedbd0e2cf62a14e
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java
@@ -0,0 +1,379 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.ProtocolEngine;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.IntroductionResponseReceivedEvent;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.introduction.IntroducerAction;
+import org.briarproject.api.introduction.IntroducerProtocolState;
+import org.briarproject.api.introduction.IntroductionResponse;
+import org.briarproject.api.introduction.SessionId;
+import org.briarproject.api.sync.MessageId;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.introduction.IntroducerAction.LOCAL_ABORT;
+import static org.briarproject.api.introduction.IntroducerAction.LOCAL_REQUEST;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_1;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_2;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_1;
+import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_2;
+import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACKS;
+import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACK_1;
+import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACK_2;
+import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES;
+import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_1;
+import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_2;
+import static org.briarproject.api.introduction.IntroducerProtocolState.ERROR;
+import static org.briarproject.api.introduction.IntroducerProtocolState.FINISHED;
+import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
+import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.MSG;
+import static org.briarproject.api.introduction.IntroductionConstants.NAME;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
+import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1;
+import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
+import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.STATE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+public class IntroducerEngine
+		implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
+
+	private static final Logger LOG =
+			Logger.getLogger(IntroducerEngine.class.getName());
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
+			BdfDictionary localState, BdfDictionary localAction) {
+
+		try {
+			IntroducerProtocolState currentState =
+					getState(localState.getLong(STATE));
+			int type = localAction.getLong(TYPE).intValue();
+			IntroducerAction action = IntroducerAction.getLocal(type);
+			IntroducerProtocolState nextState = currentState.next(action);
+
+			if (action == LOCAL_ABORT && currentState != ERROR) {
+				return abortSession(currentState, localState);
+			}
+
+			if (nextState == ERROR) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Error: Invalid action in state " +
+							currentState.name());
+				}
+				return noUpdate(localState);
+			}
+
+			localState.put(STATE, nextState.getValue());
+			if (action == LOCAL_REQUEST) {
+				// create the introduction requests for both contacts
+				List<BdfDictionary> messages = new ArrayList<BdfDictionary>(2);
+				BdfDictionary msg1 = new BdfDictionary();
+				msg1.put(TYPE, TYPE_REQUEST);
+				msg1.put(SESSION_ID, localState.getRaw(SESSION_ID));
+				msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
+				msg1.put(NAME, localState.getString(CONTACT_2));
+				msg1.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY2));
+				if (localAction.containsKey(MSG)) {
+					msg1.put(MSG, localAction.getString(MSG));
+				}
+				messages.add(msg1);
+				logLocalAction(currentState, localState, msg1);
+				BdfDictionary msg2 = new BdfDictionary();
+				msg2.put(TYPE, TYPE_REQUEST);
+				msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
+				msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
+				msg2.put(NAME, localState.getString(CONTACT_1));
+				msg2.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY1));
+				if (localAction.containsKey(MSG)) {
+					msg2.put(MSG, localAction.getString(MSG));
+				}
+				messages.add(msg2);
+				logLocalAction(currentState, localState, msg2);
+
+				List<Event> events = Collections.emptyList();
+				return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+						localState, messages, events);
+			} else {
+				throw new IllegalArgumentException("Unknown Local Action");
+			}
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
+			BdfDictionary localState, BdfDictionary msg) {
+
+		try {
+			IntroducerProtocolState currentState =
+					getState(localState.getLong(STATE));
+			int type = msg.getLong(TYPE).intValue();
+			boolean one = isContact1(localState, msg);
+			IntroducerAction action = IntroducerAction.getRemote(type, one);
+			IntroducerProtocolState nextState = currentState.next(action);
+
+			logMessageReceived(currentState, nextState, localState, type, msg);
+
+			if (nextState == ERROR) {
+				if (currentState != ERROR) {
+					return abortSession(currentState, localState);
+				} else {
+					return noUpdate(localState);
+				}
+			}
+
+			List<BdfDictionary> messages;
+			List<Event> events;
+
+			// we have sent our requests and just got the 1st or 2nd response
+			if (currentState == AWAIT_RESPONSES ||
+					currentState == AWAIT_RESPONSE_1 ||
+					currentState == AWAIT_RESPONSE_2) {
+				// update next state based on message content
+				action = IntroducerAction
+						.getRemote(type, one, msg.getBoolean(ACCEPT));
+				nextState = currentState.next(action);
+				localState.put(STATE, nextState.getValue());
+				if (one) localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID));
+				else localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID));
+
+				messages = forwardMessage(localState, msg);
+				events = Collections.singletonList(getEvent(localState, msg));
+			}
+			// we have forwarded both responses and now received the 1st or 2nd ACK
+			else if (currentState == AWAIT_ACKS ||
+					currentState == AWAIT_ACK_1 ||
+					currentState == AWAIT_ACK_2) {
+				localState.put(STATE, nextState.getValue());
+				messages = forwardMessage(localState, msg);
+				events = Collections.emptyList();
+			}
+			// we probably received a response while already being FINISHED
+			else if (currentState == FINISHED) {
+				// if it was a response store it to be found later
+				if (action == REMOTE_ACCEPT_1 || action == REMOTE_DECLINE_1) {
+					localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID));
+					messages = Collections.emptyList();
+					events = Collections.singletonList(getEvent(localState, msg));
+				} else if (action == REMOTE_ACCEPT_2 ||
+						action == REMOTE_DECLINE_2) {
+					localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID));
+					messages = Collections.emptyList();
+					events = Collections.singletonList(getEvent(localState, msg));
+				} else return noUpdate(localState);
+			} else {
+				throw new IllegalArgumentException("Bad state");
+			}
+			return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+					localState, messages, events);
+		} catch (FormatException e) {
+			throw new IllegalArgumentException(e);
+		}
+	}
+
+	private void logLocalAction(IntroducerProtocolState state,
+			BdfDictionary localState, BdfDictionary msg) {
+
+		if (!LOG.isLoggable(INFO)) return;
+
+		try {
+			String to = getMessagePartner(localState, msg);
+			LOG.info("Sending introduction request in state " + state.name() +
+					" to " + to + " with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " +
+					getState(localState.getLong(STATE)).name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private void logMessageReceived(IntroducerProtocolState currentState,
+			IntroducerProtocolState nextState,
+			BdfDictionary localState, int type, BdfDictionary msg) {
+		if (!LOG.isLoggable(INFO)) return;
+
+		try {
+			String t = "unknown";
+			if (type == TYPE_REQUEST) t = "Introduction";
+			else if (type == TYPE_RESPONSE) t = "Response";
+			else if (type == TYPE_ACK) t = "ACK";
+			else if (type == TYPE_ABORT) t = "Abort";
+
+			String from = getMessagePartner(localState, msg);
+			String to = getOtherContact(localState, msg);
+
+			LOG.info("Received " + t + " in state " + currentState.name() + " from " +
+					from + " to " + to + " with session ID " +
+					Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
+					"Moving on to state " + nextState.name()
+			);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private List<BdfDictionary> forwardMessage(BdfDictionary localState,
+			BdfDictionary message) throws FormatException {
+
+		// clone the message here, because we still need the original
+		BdfDictionary msg = (BdfDictionary) message.clone();
+		if (isContact1(localState, msg)) {
+			msg.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
+		} else {
+			msg.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
+		}
+
+		if (LOG.isLoggable(INFO)) {
+			LOG.info("Forwarding message to group " +
+					Arrays.hashCode(msg.getRaw(GROUP_ID)));
+		}
+
+		return Collections.singletonList(msg);
+	}
+
+	@Override
+	public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
+			BdfDictionary localState, BdfDictionary delivered) {
+		try {
+			return noUpdate(localState);
+		}
+		catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return null;
+		}
+	}
+
+	private IntroducerProtocolState getState(Long state) {
+		 return IntroducerProtocolState.fromValue(state.intValue());
+	}
+
+	private Event getEvent(BdfDictionary localState, BdfDictionary msg)
+			throws FormatException {
+
+		ContactId contactId =
+				new ContactId(localState.getLong(CONTACT_ID_1).intValue());
+		AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1, new byte[32])); // TODO remove byte[]
+		if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
+			contactId =
+					new ContactId(localState.getLong(CONTACT_ID_2).intValue());
+			authorId = new AuthorId(localState.getRaw(AUTHOR_ID_2, new byte[32])); // TODO remove byte[]
+		}
+
+		SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
+		MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
+		long time = msg.getLong(MESSAGE_TIME);
+		String name = getOtherContact(localState, msg);
+		boolean accept = msg.getBoolean(ACCEPT);
+
+		IntroductionResponse ir =
+				new IntroductionResponse(sessionId, messageId, time, false,
+						false, false, false, authorId, name, accept);
+		return new IntroductionResponseReceivedEvent(contactId, ir);
+	}
+
+	private boolean isContact1(BdfDictionary localState, BdfDictionary msg)
+			throws FormatException {
+
+		byte[] group = msg.getRaw(GROUP_ID);
+		byte[] group1 = localState.getRaw(GROUP_ID_1);
+		byte[] group2 = localState.getRaw(GROUP_ID_2);
+
+		if (Arrays.equals(group, group1)) {
+			return true;
+		} else if (Arrays.equals(group, group2)) {
+			return false;
+		} else {
+			throw new FormatException();
+		}
+	}
+
+	private String getMessagePartner(BdfDictionary localState,
+			BdfDictionary msg) throws FormatException {
+
+		String from = localState.getString(CONTACT_1);
+		if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
+			from = localState.getString(CONTACT_2);
+		}
+		return from;
+	}
+
+	private String getOtherContact(BdfDictionary localState, BdfDictionary msg)
+			throws FormatException {
+
+		String to = localState.getString(CONTACT_2);
+		if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
+			to = localState.getString(CONTACT_1);
+		}
+		return to;
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
+			IntroducerProtocolState currentState, BdfDictionary localState)
+			throws FormatException {
+
+		if (LOG.isLoggable(WARNING)) {
+			LOG.warning("Aborting protocol session " +
+					Arrays.hashCode(localState.getRaw(SESSION_ID)) +
+					" in state " + currentState.name());
+		}
+
+		localState.put(STATE, ERROR.getValue());
+		List<BdfDictionary> messages = new ArrayList<BdfDictionary>(2);
+		BdfDictionary msg1 = new BdfDictionary();
+		msg1.put(TYPE, TYPE_ABORT);
+		msg1.put(SESSION_ID, localState.getRaw(SESSION_ID));
+		msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
+		messages.add(msg1);
+		BdfDictionary msg2 = new BdfDictionary();
+		msg2.put(TYPE, TYPE_ABORT);
+		msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
+		msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
+		messages.add(msg2);
+		// TODO inform about protocol abort via new Event?
+		List<Event> events = Collections.emptyList();
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, messages, events);
+	}
+
+	private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
+			BdfDictionary localState) throws FormatException {
+
+		return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
+				localState, new ArrayList<BdfDictionary>(0),
+				new ArrayList<Event>(0));
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroducerManager.java b/briar-core/src/org/briarproject/introduction/IntroducerManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..1b35fc26c0363c8ccbde45b93accb2cdf165e30a
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroducerManager.java
@@ -0,0 +1,169 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.Bytes;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.system.Clock;
+import org.briarproject.util.StringUtils;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS;
+import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.MSG;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
+import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.STATE;
+import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+
+class IntroducerManager {
+
+	private static final Logger LOG =
+		Logger.getLogger(IntroducerManager.class.getName());
+
+	private final IntroductionManager introductionManager;
+	private final ClientHelper clientHelper;
+	private final Clock clock;
+	private final CryptoComponent cryptoComponent;
+
+	IntroducerManager(IntroductionManager introductionManager,
+			ClientHelper clientHelper, Clock clock,
+			CryptoComponent cryptoComponent) {
+
+		this.introductionManager = introductionManager;
+		this.clientHelper = clientHelper;
+		this.clock = clock;
+		this.cryptoComponent = cryptoComponent;
+	}
+
+	public BdfDictionary initialize(Transaction txn, Contact c1, Contact c2)
+			throws FormatException, DbException {
+
+		// create local message to keep engine state
+		long now = clock.currentTimeMillis();
+		Bytes salt = new Bytes(new byte[64]);
+		cryptoComponent.getSecureRandom().nextBytes(salt.getBytes());
+
+		Message m = clientHelper
+				.createMessage(introductionManager.getLocalGroup().getId(), now,
+						BdfList.of(salt));
+		MessageId sessionId = m.getId();
+
+		Group g1 = introductionManager.getIntroductionGroup(c1);
+		Group g2 = introductionManager.getIntroductionGroup(c2);
+
+		BdfDictionary d = new BdfDictionary();
+		d.put(SESSION_ID, sessionId);
+		d.put(STORAGE_ID, sessionId);
+		d.put(STATE, PREPARE_REQUESTS.getValue());
+		d.put(ROLE, ROLE_INTRODUCER);
+		d.put(GROUP_ID_1, g1.getId());
+		d.put(GROUP_ID_2, g2.getId());
+		d.put(CONTACT_1, c1.getAuthor().getName());
+		d.put(CONTACT_2, c2.getAuthor().getName());
+		d.put(CONTACT_ID_1, c1.getId().getInt());
+		d.put(CONTACT_ID_2, c2.getId().getInt());
+		d.put(AUTHOR_ID_1, c1.getAuthor().getId());
+		d.put(AUTHOR_ID_2, c2.getAuthor().getId());
+
+		// save local state to database
+		clientHelper.addLocalMessage(txn, m, introductionManager.getClientId(), d, false);
+
+		return d;
+	}
+
+	public void makeIntroduction(Transaction txn, Contact c1, Contact c2,
+			String msg) throws DbException, FormatException {
+
+		// TODO check for existing session with those contacts?
+		//      deny new introduction under which conditions?
+
+		// initialize engine state
+		BdfDictionary localState = initialize(txn, c1, c2);
+
+		// define action
+		BdfDictionary localAction = new BdfDictionary();
+		localAction.put(TYPE, TYPE_REQUEST);
+		if (!StringUtils.isNullOrEmpty(msg)) {
+			localAction.put(MSG, msg);
+		}
+		localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey());
+		localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey());
+
+		// start engine and process its state update
+		IntroducerEngine engine = new IntroducerEngine();
+		processStateUpdate(txn,
+				engine.onLocalAction(localState, localAction));
+	}
+
+	public void incomingMessage(Transaction txn, BdfDictionary state,
+			BdfDictionary message) throws DbException, FormatException {
+
+		IntroducerEngine engine = new IntroducerEngine();
+		processStateUpdate(txn,
+				engine.onMessageReceived(state, message));
+	}
+
+	private void processStateUpdate(Transaction txn,
+			IntroducerEngine.StateUpdate<BdfDictionary, BdfDictionary>
+					result) throws DbException, FormatException {
+
+		// save new local state
+		MessageId storageId = new MessageId(result.localState.getRaw(STORAGE_ID));
+		clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
+
+		// send messages
+		for (BdfDictionary d : result.toSend) {
+			introductionManager.sendMessage(txn, d);
+		}
+
+		// broadcast events
+		for (Event event : result.toBroadcast) {
+			txn.attach(event);
+		}
+	}
+
+	public void abort(Transaction txn, BdfDictionary state) {
+
+		IntroducerEngine engine = new IntroducerEngine();
+		BdfDictionary localAction = new BdfDictionary();
+		localAction.put(TYPE, TYPE_ABORT);
+		try {
+			processStateUpdate(txn,
+					engine.onLocalAction(state, localAction));
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..a01c62b1010fc5b6b964e4d8471faca613bb6a72
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java
@@ -0,0 +1,509 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageQueueManager;
+import org.briarproject.api.clients.PrivateGroupFactory;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.contact.ContactManager.AddContactHook;
+import org.briarproject.api.contact.ContactManager.RemoveContactHook;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfEntry;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.db.NoSuchMessageException;
+import org.briarproject.api.db.Transaction;
+import org.briarproject.api.identity.AuthorFactory;
+import org.briarproject.api.identity.AuthorId;
+import org.briarproject.api.introduction.IntroducerProtocolState;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.introduction.IntroductionMessage;
+import org.briarproject.api.introduction.IntroductionRequest;
+import org.briarproject.api.introduction.IntroductionResponse;
+import org.briarproject.api.introduction.SessionId;
+import org.briarproject.api.properties.TransportPropertyManager;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.GroupFactory;
+import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageId;
+import org.briarproject.api.sync.MessageStatus;
+import org.briarproject.api.system.Clock;
+import org.briarproject.clients.BdfIncomingMessageHook;
+import org.briarproject.util.StringUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
+import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
+import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
+import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
+import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
+import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.MSG;
+import static org.briarproject.api.introduction.IntroductionConstants.NAME;
+import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
+import static org.briarproject.api.introduction.IntroductionConstants.READ;
+import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1;
+import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
+import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
+import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.STATE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+class IntroductionManagerImpl extends BdfIncomingMessageHook
+		implements IntroductionManager, AddContactHook, RemoveContactHook {
+
+	static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
+			"23b1897c198a90ae75b976ac023d0f32"
+					+ "80ca67b12f2346b2c23a34f34e2434c3"));
+
+	private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
+
+	private static final Logger LOG =
+			Logger.getLogger(IntroductionManagerImpl.class.getName());
+
+	private final DatabaseComponent db;
+	private final MessageQueueManager messageQueueManager;
+	private final PrivateGroupFactory privateGroupFactory;
+	private final MetadataEncoder metadataEncoder;
+	private final IntroducerManager introducerManager;
+	private final IntroduceeManager introduceeManager;
+	private final Group localGroup;
+
+	@Inject
+	IntroductionManagerImpl(DatabaseComponent db,
+			MessageQueueManager messageQueueManager,
+			ClientHelper clientHelper, GroupFactory groupFactory,
+			PrivateGroupFactory privateGroupFactory,
+			MetadataEncoder metadataEncoder, MetadataParser metadataParser,
+			CryptoComponent cryptoComponent,
+			TransportPropertyManager transportPropertyManager,
+			AuthorFactory authorFactory, ContactManager contactManager,
+			Clock clock) {
+
+		super(clientHelper, metadataParser, clock);
+		this.db = db;
+		this.messageQueueManager = messageQueueManager;
+		this.privateGroupFactory = privateGroupFactory;
+		this.metadataEncoder = metadataEncoder;
+		this.introducerManager =
+				new IntroducerManager(this, clientHelper, clock,
+						cryptoComponent);
+		this.introduceeManager =
+				new IntroduceeManager(db, this, clientHelper, clock,
+						cryptoComponent, transportPropertyManager,
+						authorFactory, contactManager);
+		localGroup =
+				groupFactory.createGroup(CLIENT_ID, LOCAL_GROUP_DESCRIPTOR);
+	}
+
+	@Override
+	public ClientId getClientId() {
+		return CLIENT_ID;
+	}
+
+	@Override
+	public void addingContact(Transaction txn, Contact c) throws DbException {
+		try {
+			// create an introduction group for sending introduction messages
+			Group g = getIntroductionGroup(c);
+			db.addGroup(txn, g);
+			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
+			// Attach the contact ID to the group
+			BdfDictionary gm = new BdfDictionary();
+			gm.put(CONTACT, c.getId().getInt());
+			clientHelper.mergeGroupMetadata(txn, g.getId(), gm);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override
+	public void removingContact(Transaction txn, Contact c) throws DbException {
+		// check for open sessions with that contact and abort those
+		Long id = (long) c.getId().getInt();
+		try {
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn, localGroup.getId());
+			for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
+				BdfDictionary d = entry.getValue();
+				long role = d.getLong(ROLE, -1L);
+				if (role != ROLE_INTRODUCER) continue;
+				if (d.getLong(CONTACT_ID_1).equals(id) ||
+						d.getLong(CONTACT_ID_2).equals(id)) {
+
+					IntroducerProtocolState state = IntroducerProtocolState
+							.fromValue(d.getLong(STATE).intValue());
+					if (IntroducerProtocolState.isOngoing(state)) {
+						introducerManager.abort(txn, d);
+					}
+				}
+
+			}
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+
+		// remove the group (all messages will be removed with it)
+		// this contact won't get our abort message, but the other will
+		db.removeGroup(txn, getIntroductionGroup(c));
+	}
+
+	/**
+	 * This is called when a new message arrived and is being validated.
+	 * It is the central method where we determine which role we play
+	 * in the introduction protocol and which engine we need to start.
+	 */
+	@Override
+	protected void incomingMessage(Transaction txn, Message m, BdfList body,
+			BdfDictionary message)	throws DbException {
+
+		// add local group for engine states to make sure it exists
+		db.addGroup(txn, localGroup);
+
+		// Get message data and type
+		GroupId groupId = m.getGroupId();
+		message.put(GROUP_ID, groupId);
+		long type = message.getLong(TYPE, -1L);
+
+		// we are an introducee, need to initialize new state
+		if (type == TYPE_REQUEST) {
+			BdfDictionary state;
+			try {
+				state = introduceeManager.initialize(txn, groupId, message);
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) {
+					LOG.warning("Could not initialize introducee state");
+					LOG.log(WARNING, e.toString(), e);
+				}
+				return;
+			}
+			try {
+				introduceeManager.incomingMessage(txn, state, message);
+			} catch (DbException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				introduceeManager.abort(txn, state);
+			} catch (FormatException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				introduceeManager.abort(txn, state);
+			}
+		}
+		// our role can be anything
+		else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
+			BdfDictionary state;
+			try {
+				state = getSessionState(txn,
+						message.getRaw(SESSION_ID, new byte[0]));
+			} catch (FormatException e) {
+				LOG.warning("Could not find state for message, deleting...");
+				deleteMessage(txn, m.getId());
+				return;
+			}
+
+			long role = state.getLong(ROLE, -1L);
+			try {
+				if (role == ROLE_INTRODUCER) {
+					introducerManager.incomingMessage(txn, state, message);
+				} else if (role == ROLE_INTRODUCEE) {
+					introduceeManager.incomingMessage(txn, state, message);
+				} else {
+					if(LOG.isLoggable(WARNING)) {
+						LOG.warning("Unknown role '" + role +
+								"'. Deleting message...");
+						deleteMessage(txn, m.getId());
+					}
+				}
+			} catch (DbException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
+				else introduceeManager.abort(txn, state);
+			} catch (IOException e) {
+				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
+				else introduceeManager.abort(txn, state);
+			}
+		} else {
+			// the message has been validated, so this should not happen
+			if(LOG.isLoggable(WARNING)) {
+				LOG.warning("Unknown message type '" + type + "', deleting...");
+			}
+		}
+	}
+
+	@Override
+	public void makeIntroduction(Contact c1, Contact c2, String msg)
+			throws DbException, FormatException {
+
+			Transaction txn = db.startTransaction(false);
+			try {
+				// add local group for session states to make sure it exists
+				db.addGroup(txn, getLocalGroup());
+				introducerManager.makeIntroduction(txn, c1, c2, msg);
+				txn.setComplete();
+			} finally {
+				db.endTransaction(txn);
+			}
+	}
+
+	@Override
+	public void acceptIntroduction(final SessionId sessionId)
+			throws DbException, FormatException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			introduceeManager.acceptIntroduction(txn, sessionId);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	@Override
+	public void declineIntroduction(final SessionId sessionId)
+			throws DbException, FormatException {
+
+		Transaction txn = db.startTransaction(false);
+		try {
+			introduceeManager.declineIntroduction(txn, sessionId);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
+		}
+	}
+
+	@Override
+	public Collection<IntroductionMessage> getIntroductionMessages(
+			ContactId contactId) throws DbException {
+
+		Collection<IntroductionMessage> list =
+				new ArrayList<IntroductionMessage>();
+
+		Map<MessageId, BdfDictionary> metadata;
+		Collection<MessageStatus> statuses;
+		Transaction txn = db.startTransaction(true);
+		try {
+			// get messages and their status
+			GroupId g =
+					getIntroductionGroup(db.getContact(txn, contactId)).getId();
+			metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
+			statuses = db.getMessageStatus(txn, contactId, g);
+
+			// turn messages into classes for the UI
+			Map<SessionId, BdfDictionary> sessionStates =
+					new HashMap<SessionId, BdfDictionary>();
+			for (MessageStatus s : statuses) {
+				MessageId messageId = s.getMessageId();
+				BdfDictionary msg = metadata.get(messageId);
+				if (msg == null) continue;
+
+				try {
+					long type = msg.getLong(TYPE);
+					if (type == TYPE_ACK || type == TYPE_ABORT) continue;
+
+					// get session state
+					SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
+					BdfDictionary state = sessionStates.get(sessionId);
+					if (state == null) {
+						state = getSessionState(txn, sessionId.getBytes());
+					}
+					sessionStates.put(sessionId, state);
+
+					boolean local;
+					long time = msg.getLong(MESSAGE_TIME);
+					boolean accepted = msg.getBoolean(ACCEPT, false);
+					boolean read = msg.getBoolean(READ, false);
+					AuthorId authorId;
+					String name;
+					if (type == TYPE_RESPONSE) {
+						if (state.getLong(ROLE) == ROLE_INTRODUCER) {
+							if (!concernsThisContact(contactId, messageId, state)) {
+								// this response is not from contactId
+								continue;
+							}
+							local = false;
+							authorId =
+									getAuthorIdForIntroducer(contactId, state);
+							name = getNameForIntroducer(contactId, state);
+						} else {
+							if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE),
+									messageId.getBytes())) {
+								// this response is not ours, don't include it
+								continue;
+							}
+							local = true;
+							authorId = new AuthorId(
+									state.getRaw(REMOTE_AUTHOR_ID));
+							name = state.getString(NAME);
+						}
+						IntroductionResponse ir = new IntroductionResponse(
+								sessionId, messageId, time, local, s.isSent(),
+								s.isSeen(), read, authorId, name, accepted);
+						list.add(ir);
+					} else if (type == TYPE_REQUEST) {
+						String message;
+						boolean answered, exists;
+						if (state.getLong(ROLE) == ROLE_INTRODUCER) {
+							local = true;
+							authorId =
+									getAuthorIdForIntroducer(contactId, state);
+							name = getNameForIntroducer(contactId, state);
+							message = msg.getOptionalString(MSG);
+							answered = false;
+							exists = false;
+						} else {
+							local = false;
+							authorId = new AuthorId(
+									state.getRaw(REMOTE_AUTHOR_ID));
+							name = state.getString(NAME);
+							message = state.getOptionalString(MSG);
+							answered = state.getBoolean(ANSWERED);
+							exists = state.getBoolean(EXISTS);
+						}
+						IntroductionRequest ir = new IntroductionRequest(
+								sessionId, messageId, time, local, s.isSent(),
+								s.isSeen(), read, authorId, name, accepted,
+								message, answered, exists);
+						list.add(ir);
+					}
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				}
+			}
+			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			db.endTransaction(txn);
+		}
+		return list;
+	}
+
+	private String getNameForIntroducer(ContactId contactId,
+			BdfDictionary state) throws FormatException {
+
+		if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
+			return state.getString(CONTACT_2);
+		if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
+			return state.getString(CONTACT_1);
+		throw new RuntimeException("Contact not part of this introduction session");
+	}
+
+	private AuthorId getAuthorIdForIntroducer(ContactId contactId,
+			BdfDictionary state) throws FormatException {
+
+		if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
+			return new AuthorId(state.getRaw(AUTHOR_ID_2));
+		if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
+			return new AuthorId(state.getRaw(AUTHOR_ID_1));
+		throw new RuntimeException("Contact not part of this introduction session");
+	}
+
+	private boolean concernsThisContact(ContactId contactId, MessageId messageId,
+			BdfDictionary state) throws FormatException {
+
+		if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) {
+			return Arrays.equals(state.getRaw(RESPONSE_1, new byte[0]),
+					messageId.getBytes());
+		} else {
+			return Arrays.equals(state.getRaw(RESPONSE_2, new byte[0]),
+					messageId.getBytes());
+		}
+	}
+
+	@Override
+	public void setReadFlag(MessageId m, boolean read) throws DbException {
+		try {
+			BdfDictionary meta = BdfDictionary.of(new BdfEntry(READ, read));
+			clientHelper.mergeMessageMetadata(m, meta);
+		} catch (FormatException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public BdfDictionary getSessionState(Transaction txn, byte[] sessionId)
+			throws DbException, FormatException {
+
+		try {
+			return clientHelper.getMessageMetadataAsDictionary(txn,
+					new MessageId(sessionId));
+		} catch (NoSuchMessageException e) {
+			Map<MessageId, BdfDictionary> map = clientHelper
+					.getMessageMetadataAsDictionary(txn,
+							localGroup.getId());
+			for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
+				if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) {
+					return m.getValue();
+				}
+			}
+			if (LOG.isLoggable(WARNING)) {
+				LOG.warning(
+						"No session state found for this message with session ID " +
+								Arrays.hashCode(sessionId));
+			}
+			throw new FormatException();
+		}
+	}
+
+	public Group getIntroductionGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	}
+
+	public Group getLocalGroup() {
+		return localGroup;
+	}
+
+	public void sendMessage(Transaction txn, BdfDictionary message)
+			throws DbException, FormatException {
+
+		BdfList bdfList = MessageEncoder.encodeMessage(message);
+		byte[] body = clientHelper.toByteArray(bdfList);
+		GroupId groupId = new GroupId(message.getRaw(GROUP_ID));
+		Group group = db.getGroup(txn, groupId);
+		long timestamp = System.currentTimeMillis();
+
+		message.put(MESSAGE_TIME, timestamp);
+		Metadata metadata = metadataEncoder.encode(message);
+
+		messageQueueManager
+				.sendMessage(txn, group, timestamp, body, metadata);
+	}
+
+	private void deleteMessage(Transaction txn, MessageId messageId)
+			throws DbException {
+
+		db.deleteMessage(txn, messageId);
+		db.deleteMessageMetadata(txn, messageId);
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionModule.java b/briar-core/src/org/briarproject/introduction/IntroductionModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..686a34edadb0d4451cc4162bcf79bac33345581b
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroductionModule.java
@@ -0,0 +1,56 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.clients.MessageQueueManager;
+import org.briarproject.api.contact.ContactManager;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.introduction.IntroductionManager;
+import org.briarproject.api.system.Clock;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class IntroductionModule {
+
+	public static class EagerSingletons {
+		@Inject IntroductionManager introductionManager;
+		@Inject IntroductionValidator introductionValidator;
+	}
+
+	@Provides
+	@Singleton
+	IntroductionValidator getValidator(MessageQueueManager messageQueueManager,
+			IntroductionManager introductionManager,
+			MetadataEncoder metadataEncoder, ClientHelper clientHelper,
+			Clock clock) {
+
+		IntroductionValidator introductionValidator = new IntroductionValidator(
+				clientHelper, metadataEncoder, clock);
+
+		messageQueueManager.registerMessageValidator(
+				introductionManager.getClientId(),
+				introductionValidator);
+
+		return introductionValidator;
+	}
+
+	@Provides
+	@Singleton
+	IntroductionManager getIntroductionManager(
+			ContactManager contactManager,
+			MessageQueueManager messageQueueManager,
+			IntroductionManagerImpl introductionManager) {
+
+		contactManager.registerAddContactHook(introductionManager);
+		contactManager.registerRemoveContactHook(introductionManager);
+		messageQueueManager
+				.registerIncomingMessageHook(introductionManager.getClientId(),
+						introductionManager);
+
+		return introductionManager;
+	}
+}
diff --git a/briar-core/src/org/briarproject/introduction/IntroductionValidator.java b/briar-core/src/org/briarproject/introduction/IntroductionValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..addf58ea49749522873ff662e506e42055d42dfa
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/IntroductionValidator.java
@@ -0,0 +1,173 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.DeviceId;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.introduction.SessionId;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.system.Clock;
+import org.briarproject.clients.BdfMessageValidator;
+
+import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
+import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.MSG;
+import static org.briarproject.api.introduction.IntroductionConstants.NAME;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
+import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
+
+class IntroductionValidator extends BdfMessageValidator {
+
+	IntroductionValidator(ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
+	}
+
+	@Override
+	protected BdfDictionary validateMessage(Message m, Group g, BdfList body)
+			throws FormatException {
+
+		BdfDictionary d;
+		long type = body.getLong(0);
+		byte[] id = body.getRaw(1);
+		checkLength(id, SessionId.LENGTH);
+
+		if (type == TYPE_REQUEST) {
+			d = validateRequest(body);
+		} else if (type == TYPE_RESPONSE) {
+			d = validateResponse(body);
+		} else if (type == TYPE_ACK) {
+			d = validateAck(body);
+		} else if (type == TYPE_ABORT) {
+			d = validateAbort(body);
+		} else {
+			throw new FormatException();
+		}
+
+		d.put(TYPE, type);
+		d.put(SESSION_ID, id);
+		d.put(MESSAGE_ID, m.getId());
+		d.put(MESSAGE_TIME, m.getTimestamp());
+		return d;
+	}
+
+	private BdfDictionary validateRequest(BdfList message)
+			throws FormatException {
+
+		checkSize(message, 4, 5);
+
+		// parse contact name
+		String name = message.getString(2);
+		checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
+
+		// parse contact's public key
+		byte[] key = message.getRaw(3);
+		checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH);
+
+		// parse (optional) message
+		String msg = null;
+		if (message.size() == 5) {
+			msg = message.getString(4);
+			checkLength(msg, 0, MAX_MESSAGE_BODY_LENGTH);
+		}
+
+		// Return the metadata
+		BdfDictionary d = new BdfDictionary();
+		d.put(NAME, name);
+		d.put(PUBLIC_KEY, key);
+		if (msg != null) {
+			d.put(MSG, msg);
+		}
+		return d;
+	}
+
+	private BdfDictionary validateResponse(BdfList message)
+			throws FormatException {
+
+		checkSize(message, 3, 7);
+
+		// parse accept/decline
+		boolean accept = message.getBoolean(2);
+
+		long time = 0;
+		byte[] pubkey = null;
+		byte[] deviceId = null;
+		BdfDictionary tp = new BdfDictionary();
+		if (accept) {
+			checkSize(message, 7);
+
+			// parse timestamp
+			time = message.getLong(3);
+
+			// parse ephemeral public key
+			pubkey = message.getRaw(4);
+			checkLength(pubkey, 0, MAX_PUBLIC_KEY_LENGTH);
+
+			// parse device ID
+			deviceId = message.getRaw(5);
+			checkLength(deviceId, DeviceId.LENGTH);
+
+			// parse transport properties
+			tp = message.getDictionary(6);
+			if (tp.size() < 1) throw new FormatException();
+			for (String tId : tp.keySet()) {
+				checkLength(tId, 1, TransportId.MAX_TRANSPORT_ID_LENGTH);
+				BdfDictionary tProps = tp.getDictionary(tId);
+				for (String propId : tProps.keySet()) {
+					checkLength(propId, 0, MAX_PROPERTY_LENGTH);
+					String prop = tProps.getString(propId);
+					checkLength(prop, 0, MAX_PROPERTY_LENGTH);
+				}
+			}
+		} else {
+			checkSize(message, 3);
+		}
+
+		// Return the metadata
+		BdfDictionary d = new BdfDictionary();
+		d.put(ACCEPT, accept);
+		if (accept) {
+			d.put(TIME, time);
+			d.put(E_PUBLIC_KEY, pubkey);
+			d.put(DEVICE_ID, deviceId);
+			d.put(TRANSPORT, tp);
+		}
+		return d;
+	}
+
+	private BdfDictionary validateAck(BdfList message)
+			throws FormatException {
+
+		checkSize(message, 2);
+
+		// Return the metadata
+		return new BdfDictionary();
+	}
+
+	private BdfDictionary validateAbort(BdfList message)
+			throws FormatException {
+
+		checkSize(message, 2);
+
+		// Return the metadata
+		return new BdfDictionary();
+	}
+}
diff --git a/briar-core/src/org/briarproject/introduction/MessageEncoder.java b/briar-core/src/org/briarproject/introduction/MessageEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..87bf4d5e6a4e63a0a5d35f793d7e30b653020437
--- /dev/null
+++ b/briar-core/src/org/briarproject/introduction/MessageEncoder.java
@@ -0,0 +1,74 @@
+package org.briarproject.introduction;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+
+import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
+import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.MSG;
+import static org.briarproject.api.introduction.IntroductionConstants.NAME;
+import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
+import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.api.introduction.IntroductionConstants.TIME;
+import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
+import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
+
+public class MessageEncoder {
+
+	public static BdfList encodeMessage(BdfDictionary d) throws FormatException {
+
+		BdfList body;
+		long type = d.getLong(TYPE);
+		if (type == TYPE_REQUEST) {
+			body = encodeRequest(d);
+		} else if (type == TYPE_RESPONSE) {
+			body = encodeResponse(d);
+		} else if (type == TYPE_ACK) {
+			body = encodeAck(d);
+		} else if (type == TYPE_ABORT) {
+			body = encodeAbort(d);
+		} else {
+			throw new FormatException();
+		}
+		return body;
+	}
+
+	private static BdfList encodeRequest(BdfDictionary d) throws FormatException {
+		BdfList list = BdfList.of(TYPE_REQUEST, d.getRaw(SESSION_ID),
+				d.getString(NAME), d.getRaw(PUBLIC_KEY));
+
+		if (d.containsKey(MSG)) {
+			list.add(d.getString(MSG));
+		}
+		return list;
+	}
+
+	private static BdfList encodeResponse(BdfDictionary d) throws FormatException {
+		BdfList list = BdfList.of(TYPE_RESPONSE, d.getRaw(SESSION_ID),
+				d.getBoolean(ACCEPT));
+
+		if (d.getBoolean(ACCEPT)) {
+			list.add(d.getLong(TIME));
+			list.add(d.getRaw(E_PUBLIC_KEY));
+			list.add(d.getRaw(DEVICE_ID));
+			list.add(d.getDictionary(TRANSPORT));
+		}
+		// TODO Sign the response, see #256
+		return list;
+	}
+
+	private static BdfList encodeAck(BdfDictionary d) throws FormatException {
+		return BdfList.of(TYPE_ACK, d.getRaw(SESSION_ID));
+	}
+
+	private static BdfList encodeAbort(BdfDictionary d) throws FormatException {
+		return BdfList.of(TYPE_ABORT, d.getRaw(SESSION_ID));
+	}
+
+}
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index 9910ff42fe9facb2d1a0a5eebebff2cc89b1f6da..4b10c48edeef1b79ea322826b2e3661bd3d7f32b 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -82,18 +82,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	}
 
 	@Override
-	public void addRemoteProperties(ContactId c, DeviceId dev,
+	public void addRemoteProperties(Transaction txn, ContactId c, DeviceId dev,
 			Map<TransportId, TransportProperties> props) throws DbException {
-		Transaction txn = db.startTransaction(false);
-		try {
-			Group g = getContactGroup(db.getContact(txn, c));
-			for (Entry<TransportId, TransportProperties> e : props.entrySet()) {
-				storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 0,
-						false, false);
-			}
-			txn.setComplete();
-		} finally {
-			db.endTransaction(txn);
+		Group g = getContactGroup(db.getContact(txn, c));
+		for (Entry<TransportId, TransportProperties> e : props.entrySet()) {
+			storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 0,
+					false, false);
 		}
 	}