diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java
index 11d6c87b0ac55d85e9a16c3e2de12a872c8b41d4..6303f210b3129e5a28e7121509aa423101c5c03c 100644
--- a/briar-core/src/org/briarproject/CoreModule.java
+++ b/briar-core/src/org/briarproject/CoreModule.java
@@ -9,6 +9,7 @@ import org.briarproject.event.EventModule;
 import org.briarproject.forum.ForumModule;
 import org.briarproject.identity.IdentityModule;
 import org.briarproject.invitation.InvitationModule;
+import org.briarproject.keyagreement.KeyAgreementModule;
 import org.briarproject.lifecycle.LifecycleModule;
 import org.briarproject.messaging.MessagingModule;
 import org.briarproject.plugins.PluginsModule;
@@ -23,7 +24,8 @@ import dagger.Module;
 
 @Module(includes = {DatabaseModule.class,
 		CryptoModule.class, LifecycleModule.class, ReliabilityModule.class,
-		MessagingModule.class, InvitationModule.class, ForumModule.class,
+		MessagingModule.class, InvitationModule.class, KeyAgreementModule.class,
+		ForumModule.class,
 		IdentityModule.class, EventModule.class, DataModule.class,
 		ContactModule.class, PropertiesModule.class, TransportModule.class,
 		SyncModule.class, SettingsModule.class, ClientsModule.class,
diff --git a/briar-core/src/org/briarproject/keyagreement/AbortException.java b/briar-core/src/org/briarproject/keyagreement/AbortException.java
new file mode 100644
index 0000000000000000000000000000000000000000..670bbc3ee14f63d6bd1d543d58d06652576c4437
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/AbortException.java
@@ -0,0 +1,23 @@
+package org.briarproject.keyagreement;
+
+class AbortException extends Exception {
+	public boolean receivedAbort;
+
+	public AbortException() {
+		this(false);
+	}
+
+	public AbortException(boolean receivedAbort) {
+		super();
+		this.receivedAbort = receivedAbort;
+	}
+
+	public AbortException(Exception e) {
+		this(e, false);
+	}
+
+	public AbortException(Exception e, boolean receivedAbort) {
+		super(e);
+		this.receivedAbort = receivedAbort;
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java
new file mode 100644
index 0000000000000000000000000000000000000000..e87297af63402215582b79de7ad5bd98f63ec31e
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java
@@ -0,0 +1,236 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.keyagreement.KeyAgreementConnection;
+import org.briarproject.api.keyagreement.KeyAgreementListener;
+import org.briarproject.api.keyagreement.Payload;
+import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.plugins.duplex.DuplexPlugin;
+import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
+import org.briarproject.api.system.Clock;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.Future;
+import java.util.logging.Logger;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.CONNECTION_TIMEOUT;
+
+class KeyAgreementConnector {
+
+	interface Callbacks {
+		void connectionWaiting();
+	}
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyAgreementConnector.class.getName());
+
+	private final Callbacks callbacks;
+	private final Clock clock;
+	private final CryptoComponent crypto;
+	private final PluginManager pluginManager;
+	private final CompletionService<KeyAgreementConnection> connect;
+
+	private final List<KeyAgreementListener> listeners =
+			new ArrayList<KeyAgreementListener>();
+	private final List<Future<KeyAgreementConnection>> pending =
+			new ArrayList<Future<KeyAgreementConnection>>();
+
+	private volatile boolean connecting = false;
+	private volatile boolean alice = false;
+
+	public KeyAgreementConnector(Callbacks callbacks, Clock clock,
+			CryptoComponent crypto, PluginManager pluginManager,
+			Executor ioExecutor) {
+		this.callbacks = callbacks;
+		this.clock = clock;
+		this.crypto = crypto;
+		this.pluginManager = pluginManager;
+		connect = new ExecutorCompletionService<KeyAgreementConnection>(
+				ioExecutor);
+	}
+
+	public Payload listen(KeyPair localKeyPair) {
+		LOG.info("Starting BQP listeners");
+		// Derive commitment
+		byte[] commitment = crypto.deriveKeyCommitment(
+				localKeyPair.getPublic().getEncoded());
+		// Start all listeners and collect their descriptors
+		List<TransportDescriptor> descriptors =
+				new ArrayList<TransportDescriptor>();
+		for (DuplexPlugin plugin : pluginManager.getKeyAgreementPlugins()) {
+			KeyAgreementListener l = plugin.createKeyAgreementListener(
+					commitment);
+			if (l != null) {
+				TransportDescriptor d = l.getDescriptor();
+				descriptors.add(d);
+				pending.add(connect.submit(new ReadableTask(l.listen())));
+				listeners.add(l);
+			}
+		}
+		return new Payload(commitment, descriptors);
+	}
+
+	public void stopListening() {
+		LOG.info("Stopping BQP listeners");
+		for (KeyAgreementListener l : listeners) {
+			l.close();
+		}
+		listeners.clear();
+	}
+
+	public KeyAgreementTransport connect(Payload remotePayload,
+			boolean alice) {
+		// Let the listeners know if we are Alice
+		this.connecting = true;
+		this.alice = alice;
+		long end = clock.currentTimeMillis() + CONNECTION_TIMEOUT;
+
+		// Start connecting over supported transports
+		LOG.info("Starting outgoing BQP connections");
+		for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
+			DuplexPlugin plugin = (DuplexPlugin) pluginManager.getPlugin(
+					d.getIdentifier());
+			if (plugin != null)
+				pending.add(connect.submit(new ReadableTask(
+						new ConnectorTask(plugin, remotePayload.getCommitment(),
+								d, end))));
+		}
+
+		// Get chosen connection
+		KeyAgreementConnection chosen = null;
+		try {
+			long now = clock.currentTimeMillis();
+			Future<KeyAgreementConnection> f =
+					connect.poll(end - now, MILLISECONDS);
+			if (f == null)
+				return null; // No task completed within the timeout.
+			chosen = f.get();
+			return new KeyAgreementTransport(chosen);
+		} catch (InterruptedException e) {
+			LOG.info("Interrupted while waiting for connection");
+			Thread.currentThread().interrupt();
+			return null;
+		} catch (ExecutionException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return null;
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return null;
+		} finally {
+			stopListening();
+			// Close all other connections
+			closePending(chosen);
+		}
+	}
+
+	private void closePending(KeyAgreementConnection chosen) {
+		for (Future<KeyAgreementConnection> f : pending) {
+			try {
+				if (f.cancel(true))
+					LOG.info("Cancelled task");
+				else if (!f.isCancelled()) {
+					KeyAgreementConnection c = f.get();
+					if (c != null && c != chosen)
+						tryToClose(c.getConnection(), false);
+				}
+			} catch (InterruptedException e) {
+				LOG.info("Interrupted while closing sockets");
+				Thread.currentThread().interrupt();
+				return;
+			} catch (ExecutionException e) {
+				if (LOG.isLoggable(INFO)) LOG.info(e.toString());
+			}
+		}
+	}
+
+	private void tryToClose(DuplexTransportConnection conn, boolean exception) {
+		try {
+			if (LOG.isLoggable(INFO))
+				LOG.info("Closing connection, exception: " + exception);
+			conn.getReader().dispose(exception, true);
+			conn.getWriter().dispose(exception);
+		} catch (IOException e) {
+			if (LOG.isLoggable(INFO)) LOG.info(e.toString());
+		}
+	}
+
+	private class ConnectorTask implements Callable<KeyAgreementConnection> {
+
+		private final byte[] commitment;
+		private final TransportDescriptor descriptor;
+		private final long end;
+		private final DuplexPlugin plugin;
+
+		private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
+				TransportDescriptor descriptor, long end) {
+			this.plugin = plugin;
+			this.commitment = commitment;
+			this.descriptor = descriptor;
+			this.end = end;
+		}
+
+		@Override
+		public KeyAgreementConnection call() throws Exception {
+			// Repeat attempts until we connect or get interrupted
+			while (true) {
+				long now = clock.currentTimeMillis();
+				DuplexTransportConnection conn =
+						plugin.createKeyAgreementConnection(commitment,
+								descriptor, end - now);
+				if (conn != null) {
+					if (LOG.isLoggable(INFO))
+						LOG.info(plugin.getId().getString() +
+								": Outgoing connection");
+					return new KeyAgreementConnection(conn, plugin.getId());
+				}
+				// Wait 2s before retry (to circumvent transient failures)
+				Thread.sleep(2000);
+			}
+		}
+	}
+
+	private class ReadableTask
+			implements Callable<KeyAgreementConnection> {
+
+		private final Callable<KeyAgreementConnection> connectionTask;
+
+		private ReadableTask(Callable<KeyAgreementConnection> connectionTask) {
+			this.connectionTask = connectionTask;
+		}
+
+		@Override
+		public KeyAgreementConnection call()
+				throws Exception {
+			KeyAgreementConnection c = connectionTask.call();
+			InputStream in = c.getConnection().getReader().getInputStream();
+			boolean waitingSent = false;
+			while (!alice && in.available() == 0) {
+				if (!waitingSent && connecting && !alice) {
+					// Bob waits here until Alice obtains his payload.
+					callbacks.connectionWaiting();
+					waitingSent = true;
+				}
+				if (LOG.isLoggable(INFO))
+					LOG.info(c.getTransportId().toString() +
+							": Waiting for connection");
+				Thread.sleep(1000);
+			}
+			if (!alice && LOG.isLoggable(INFO))
+				LOG.info(c.getTransportId().toString() + ": Data available");
+			return c;
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f6a529028a668fc4cbde1231a3a61201730f847
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java
@@ -0,0 +1,43 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.data.BdfWriterFactory;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.keyagreement.KeyAgreementTaskFactory;
+import org.briarproject.api.keyagreement.PayloadEncoder;
+import org.briarproject.api.keyagreement.PayloadParser;
+import org.briarproject.api.lifecycle.IoExecutor;
+import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.system.Clock;
+
+import java.util.concurrent.Executor;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+
+@Module
+public class KeyAgreementModule {
+
+	@Provides
+	@Singleton
+	KeyAgreementTaskFactory provideKeyAgreementTaskFactory(Clock clock,
+			CryptoComponent crypto, EventBus eventBus,
+			@IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder,
+			PluginManager pluginManager) {
+		return new KeyAgreementTaskFactoryImpl(clock, crypto, eventBus,
+				ioExecutor, payloadEncoder, pluginManager);
+	}
+
+	@Provides
+	PayloadEncoder providePayloadEncoder(BdfWriterFactory bdfWriterFactory) {
+		return new PayloadEncoderImpl(bdfWriterFactory);
+	}
+
+	@Provides
+	PayloadParser providePayloadParser(BdfReaderFactory bdfReaderFactory) {
+		return new PayloadParserImpl(bdfReaderFactory);
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..c832edd092e10c3227eeed2e538c7ed25b901f88
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java
@@ -0,0 +1,157 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.keyagreement.Payload;
+import org.briarproject.api.keyagreement.PayloadEncoder;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+
+/**
+ * Implementation of the BQP protocol.
+ * <p/>
+ * Alice:
+ * <ul>
+ *     <li>Send A_KEY</li>
+ *     <li>Receive B_KEY
+ *     <ul>
+ *         <li>Check B_KEY matches B_COMMIT</li>
+ *     </ul></li>
+ *     <li>Calculate s</li>
+ *     <li>Send A_CONFIRM</li>
+ *     <li>Receive B_CONFIRM
+ *     <ul>
+ *         <li>Check B_CONFIRM matches expected</li>
+ *     </ul></li>
+ *     <li>Derive master</li>
+ * </ul><p/>
+ * Bob:
+ * <ul>
+ *     <li>Receive A_KEY
+ *     <ul>
+ *         <li>Check A_KEY matches A_COMMIT</li>
+ *     </ul></li>
+ *     <li>Send B_KEY</li>
+ *     <li>Calculate s</li>
+ *     <li>Receive A_CONFIRM
+ *     <ul>
+ *         <li>Check A_CONFIRM matches expected</li>
+ *     </ul></li>
+ *     <li>Send B_CONFIRM</li>
+ *     <li>Derive master</li>
+ * </ul>
+ */
+class KeyAgreementProtocol {
+
+	interface Callbacks {
+		void connectionWaiting();
+		void initialPacketReceived();
+	}
+
+	private Callbacks callbacks;
+	private CryptoComponent crypto;
+	private PayloadEncoder payloadEncoder;
+	private KeyAgreementTransport transport;
+	private Payload theirPayload, ourPayload;
+	private KeyPair ourKeyPair;
+	private boolean alice;
+
+	public KeyAgreementProtocol(Callbacks callbacks, CryptoComponent crypto,
+			PayloadEncoder payloadEncoder, KeyAgreementTransport transport,
+			Payload theirPayload, Payload ourPayload, KeyPair ourKeyPair,
+			boolean alice) {
+		this.callbacks = callbacks;
+		this.crypto = crypto;
+		this.payloadEncoder = payloadEncoder;
+		this.transport = transport;
+		this.theirPayload = theirPayload;
+		this.ourPayload = ourPayload;
+		this.ourKeyPair = ourKeyPair;
+		this.alice = alice;
+	}
+
+	/**
+	 * Perform the BQP protocol.
+	 *
+	 * @return the negotiated master secret.
+	 * @throws AbortException when the protocol may have been tampered with.
+	 * @throws IOException for all other other connection errors.
+	 */
+	public SecretKey perform() throws AbortException, IOException {
+		try {
+			byte[] theirPublicKey;
+			if (alice) {
+				sendKey();
+				// Alice waits here until Bob obtains her payload.
+				callbacks.connectionWaiting();
+				theirPublicKey = receiveKey();
+			} else {
+				theirPublicKey = receiveKey();
+				sendKey();
+			}
+			SecretKey s = deriveSharedSecret(theirPublicKey);
+			if (alice) {
+				sendConfirm(s, theirPublicKey);
+				receiveConfirm(s, theirPublicKey);
+			} else {
+				receiveConfirm(s, theirPublicKey);
+				sendConfirm(s, theirPublicKey);
+			}
+			return crypto.deriveMasterSecret(s);
+		} catch (AbortException e) {
+			sendAbort(e.getCause() != null);
+			throw e;
+		}
+	}
+
+	private void sendKey() throws IOException {
+		transport.sendKey(ourKeyPair.getPublic().getEncoded());
+	}
+
+	private byte[] receiveKey() throws AbortException {
+		byte[] publicKey = transport.receiveKey();
+		callbacks.initialPacketReceived();
+		byte[] expected = crypto.deriveKeyCommitment(publicKey);
+		if (!Arrays.equals(expected, theirPayload.getCommitment()))
+			throw new AbortException();
+		return publicKey;
+	}
+
+	private SecretKey deriveSharedSecret(byte[] theirPublicKey)
+			throws AbortException {
+		try {
+			return crypto.deriveSharedSecret(theirPublicKey, ourKeyPair, alice);
+		} catch (GeneralSecurityException e) {
+			throw new AbortException(e);
+		}
+	}
+
+	private void sendConfirm(SecretKey s, byte[] theirPublicKey)
+			throws IOException {
+		byte[] confirm = crypto.deriveConfirmationRecord(s,
+				payloadEncoder.encode(theirPayload),
+				payloadEncoder.encode(ourPayload),
+				theirPublicKey, ourKeyPair,
+				alice, alice);
+		transport.sendConfirm(confirm);
+	}
+
+	private void receiveConfirm(SecretKey s, byte[] theirPublicKey)
+			throws AbortException {
+		byte[] confirm = transport.receiveConfirm();
+		byte[] expected = crypto.deriveConfirmationRecord(s,
+				payloadEncoder.encode(theirPayload),
+				payloadEncoder.encode(ourPayload),
+				theirPublicKey, ourKeyPair,
+				alice, !alice);
+		if (!Arrays.equals(expected, confirm))
+			throw new AbortException();
+	}
+
+	private void sendAbort(boolean exception) {
+		transport.sendAbort(exception);
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..aafde49b3b1acb32aa01c995b9402a96cf615e41
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java
@@ -0,0 +1,46 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.KeyAgreementAbortedEvent;
+import org.briarproject.api.event.KeyAgreementFailedEvent;
+import org.briarproject.api.event.KeyAgreementFinishedEvent;
+import org.briarproject.api.keyagreement.KeyAgreementTask;
+import org.briarproject.api.keyagreement.KeyAgreementTaskFactory;
+import org.briarproject.api.keyagreement.PayloadEncoder;
+import org.briarproject.api.lifecycle.IoExecutor;
+import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.system.Clock;
+
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory {
+
+	private final Clock clock;
+	private final CryptoComponent crypto;
+	private final EventBus eventBus;
+	private final Executor ioExecutor;
+	private final PayloadEncoder payloadEncoder;
+	private final PluginManager pluginManager;
+
+	@Inject
+	KeyAgreementTaskFactoryImpl(Clock clock, CryptoComponent crypto,
+			EventBus eventBus, @IoExecutor Executor ioExecutor,
+			PayloadEncoder payloadEncoder, PluginManager pluginManager) {
+		this.clock = clock;
+		this.crypto = crypto;
+		this.eventBus = eventBus;
+		this.ioExecutor = ioExecutor;
+		this.payloadEncoder = payloadEncoder;
+		this.pluginManager = pluginManager;
+	}
+
+	public KeyAgreementTask getTask() {
+		return new KeyAgreementTaskImpl(clock, crypto, eventBus, payloadEncoder,
+				pluginManager, ioExecutor);
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..12f7678740421fbae7f1251c7f7a107cb6b967f8
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java
@@ -0,0 +1,135 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.KeyAgreementAbortedEvent;
+import org.briarproject.api.event.KeyAgreementFailedEvent;
+import org.briarproject.api.event.KeyAgreementFinishedEvent;
+import org.briarproject.api.event.KeyAgreementListeningEvent;
+import org.briarproject.api.event.KeyAgreementStartedEvent;
+import org.briarproject.api.event.KeyAgreementWaitingEvent;
+import org.briarproject.api.keyagreement.KeyAgreementResult;
+import org.briarproject.api.keyagreement.KeyAgreementTask;
+import org.briarproject.api.keyagreement.Payload;
+import org.briarproject.api.keyagreement.PayloadEncoder;
+import org.briarproject.api.plugins.PluginManager;
+import org.briarproject.api.system.Clock;
+
+import java.io.IOException;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+
+class KeyAgreementTaskImpl extends Thread implements
+		KeyAgreementTask, KeyAgreementConnector.Callbacks,
+		KeyAgreementProtocol.Callbacks {
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyAgreementTaskImpl.class.getName());
+
+	private final CryptoComponent crypto;
+	private final EventBus eventBus;
+	private final PayloadEncoder payloadEncoder;
+	private final KeyPair localKeyPair;
+	private final KeyAgreementConnector connector;
+
+	private Payload localPayload;
+	private Payload remotePayload;
+
+	public KeyAgreementTaskImpl(Clock clock, CryptoComponent crypto,
+			EventBus eventBus, PayloadEncoder payloadEncoder,
+			PluginManager pluginManager, Executor ioExecutor) {
+		this.crypto = crypto;
+		this.eventBus = eventBus;
+		this.payloadEncoder = payloadEncoder;
+		localKeyPair = crypto.generateAgreementKeyPair();
+		connector = new KeyAgreementConnector(this, clock, crypto,
+				pluginManager, ioExecutor);
+	}
+
+	@Override
+	public synchronized void listen() {
+		if (localPayload == null) {
+			localPayload = connector.listen(localKeyPair);
+			eventBus.broadcast(new KeyAgreementListeningEvent(localPayload));
+		}
+	}
+
+	@Override
+	public synchronized void stopListening() {
+		if (localPayload != null) {
+			if (remotePayload == null)
+				connector.stopListening();
+			else
+				interrupt();
+		}
+	}
+
+	@Override
+	public synchronized void connectAndRunProtocol(Payload remotePayload) {
+		if (this.localPayload == null)
+			throw new IllegalStateException(
+					"Must listen before connecting");
+		if (this.remotePayload != null)
+			throw new IllegalStateException(
+					"Already provided remote payload for this task");
+		this.remotePayload = remotePayload;
+		start();
+	}
+
+	@Override
+	public void run() {
+		boolean alice = localPayload.compareTo(remotePayload) < 0;
+
+		// Open connection to remote device
+		KeyAgreementTransport transport =
+				connector.connect(remotePayload, alice);
+		if (transport == null) {
+			// Notify caller that the connection failed
+			eventBus.broadcast(new KeyAgreementFailedEvent());
+			return;
+		}
+
+		// Run BQP protocol over the connection
+		LOG.info("Starting BQP protocol");
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(this, crypto,
+				payloadEncoder, transport, remotePayload, localPayload,
+				localKeyPair, alice);
+		try {
+			SecretKey master = protocol.perform();
+			KeyAgreementResult result =
+					new KeyAgreementResult(master, transport.getConnection(),
+							transport.getTransportId(), alice);
+			LOG.info("Finished BQP protocol");
+			// Broadcast result to caller
+			eventBus.broadcast(new KeyAgreementFinishedEvent(result));
+		} catch (AbortException e) {
+			if (LOG.isLoggable(WARNING))
+				LOG.log(WARNING, e.toString(), e);
+			// Notify caller that the protocol was aborted
+			eventBus.broadcast(new KeyAgreementAbortedEvent(e.receivedAbort));
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING))
+				LOG.log(WARNING, e.toString(), e);
+			// Notify caller that the connection failed
+			eventBus.broadcast(new KeyAgreementFailedEvent());
+		}
+	}
+
+	@Override
+	public void connectionWaiting() {
+		eventBus.broadcast(new KeyAgreementWaitingEvent());
+	}
+
+	@Override
+	public void initialPacketReceived() {
+		// We send this here instead of when we create the protocol, so that
+		// if device A makes a connection after getting device B's payload and
+		// starts its protocol, device A's UI doesn't change to prevent device B
+		// from getting device A's payload.
+		eventBus.broadcast(new KeyAgreementStartedEvent());
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java
new file mode 100644
index 0000000000000000000000000000000000000000..25a18962bad176a7d58535d2fd5009d6977b1a51
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java
@@ -0,0 +1,131 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.TransportId;
+import org.briarproject.api.keyagreement.KeyAgreementConnection;
+import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
+import org.briarproject.util.ByteUtils;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_LENGTH;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_PAYLOAD_LENGTH_OFFSET;
+import static org.briarproject.api.keyagreement.RecordTypes.ABORT;
+import static org.briarproject.api.keyagreement.RecordTypes.CONFIRM;
+import static org.briarproject.api.keyagreement.RecordTypes.KEY;
+
+/**
+ * Handles the sending and receiving of BQP records.
+ */
+class KeyAgreementTransport {
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyAgreementTransport.class.getName());
+
+	private final KeyAgreementConnection kac;
+	private final InputStream in;
+	private final OutputStream out;
+
+	public KeyAgreementTransport(KeyAgreementConnection kac)
+			throws IOException {
+		this.kac = kac;
+		in = kac.getConnection().getReader().getInputStream();
+		out = kac.getConnection().getWriter().getOutputStream();
+	}
+
+	public DuplexTransportConnection getConnection() {
+		return kac.getConnection();
+	}
+
+	public TransportId getTransportId() {
+		return kac.getTransportId();
+	}
+
+	public void sendKey(byte[] key) throws IOException {
+		writeRecord(KEY, key);
+	}
+
+	public byte[] receiveKey() throws AbortException {
+		return readRecord(KEY);
+	}
+
+	public void sendConfirm(byte[] confirm) throws IOException {
+		writeRecord(CONFIRM, confirm);
+	}
+
+	public byte[] receiveConfirm() throws AbortException {
+		return readRecord(CONFIRM);
+	}
+
+	public void sendAbort(boolean exception) {
+		try {
+			writeRecord(ABORT, new byte[0]);
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			exception = true;
+		}
+		tryToClose(exception);
+	}
+
+	public void tryToClose(boolean exception) {
+		try {
+			LOG.info("Closing connection");
+			kac.getConnection().getReader().dispose(exception, true);
+			kac.getConnection().getWriter().dispose(exception);
+		} catch (IOException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		}
+	}
+
+	private void writeRecord(byte type, byte[] payload) throws IOException {
+		byte[] recordHeader = new byte[RECORD_HEADER_LENGTH];
+		recordHeader[0] = PROTOCOL_VERSION;
+		recordHeader[1] = type;
+		ByteUtils.writeUint16(payload.length, recordHeader,
+				RECORD_HEADER_PAYLOAD_LENGTH_OFFSET);
+		out.write(recordHeader);
+		out.write(payload);
+		out.flush();
+	}
+
+	private byte[] readRecord(byte type) throws AbortException {
+		byte[] header = readHeader();
+		if (header[0] != PROTOCOL_VERSION)
+			throw new AbortException(); // TODO handle?
+		if (header[1] != type) {
+			// Unexpected packet
+			throw new AbortException(header[1] == ABORT);
+		}
+		int len = ByteUtils.readUint16(header,
+				RECORD_HEADER_PAYLOAD_LENGTH_OFFSET);
+		try {
+			return readData(len);
+		} catch (IOException e) {
+			throw new AbortException(e);
+		}
+	}
+
+	private byte[] readHeader() throws AbortException {
+		try {
+			return readData(RECORD_HEADER_LENGTH);
+		} catch (IOException e) {
+			throw new AbortException(e);
+		}
+	}
+
+	private byte[] readData(int len) throws IOException {
+		byte[] data = new byte[len];
+		int offset = 0;
+		while (offset < data.length) {
+			int read = in.read(data, offset, data.length - offset);
+			if (read == -1) throw new EOFException();
+			offset += read;
+		}
+		return data;
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..8a26f9405bd17128dc7cfde4ca89bb90859f13d0
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java
@@ -0,0 +1,48 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.data.BdfWriter;
+import org.briarproject.api.data.BdfWriterFactory;
+import org.briarproject.api.keyagreement.Payload;
+import org.briarproject.api.keyagreement.PayloadEncoder;
+import org.briarproject.api.keyagreement.TransportDescriptor;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.inject.Inject;
+
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
+
+class PayloadEncoderImpl implements PayloadEncoder {
+
+	private final BdfWriterFactory bdfWriterFactory;
+
+	@Inject
+	public PayloadEncoderImpl(BdfWriterFactory bdfWriterFactory) {
+		this.bdfWriterFactory = bdfWriterFactory;
+	}
+
+	@Override
+	public byte[] encode(Payload p) {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter w = bdfWriterFactory.createWriter(out);
+		try {
+			w.writeListStart(); // Payload start
+			w.writeLong(PROTOCOL_VERSION);
+			w.writeRaw(p.getCommitment());
+			w.writeListStart(); // Descriptors start
+			for (TransportDescriptor d : p.getTransportDescriptors()) {
+				w.writeListStart();
+				w.writeString(d.getIdentifier().getString());
+				w.writeDictionary(d.getProperties());
+				w.writeListEnd();
+			}
+			w.writeListEnd(); // Descriptors end
+			w.writeListEnd(); // Payload end
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayOutputStream
+			throw new RuntimeException(e);
+		}
+		return out.toByteArray();
+	}
+}
diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..d13f9ff73228af96b26a814a6d55e19a3e4d9a2b
--- /dev/null
+++ b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java
@@ -0,0 +1,68 @@
+package org.briarproject.keyagreement;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.data.BdfReader;
+import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.keyagreement.Payload;
+import org.briarproject.api.keyagreement.PayloadParser;
+import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.properties.TransportProperties;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
+import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
+
+class PayloadParserImpl implements PayloadParser {
+
+	private final BdfReaderFactory bdfReaderFactory;
+
+	@Inject
+	public PayloadParserImpl(BdfReaderFactory bdfReaderFactory) {
+		this.bdfReaderFactory = bdfReaderFactory;
+	}
+
+	@Override
+	public Payload parse(byte[] raw) throws IOException {
+		ByteArrayInputStream in = new ByteArrayInputStream(raw);
+		BdfReader r = bdfReaderFactory.createReader(in);
+		r.readListStart(); // Payload start
+		int proto = (int) r.readLong();
+		if (proto != PROTOCOL_VERSION)
+			throw new FormatException();
+		byte[] commitment = r.readRaw(COMMIT_LENGTH);
+		if (commitment.length != COMMIT_LENGTH)
+			throw new FormatException();
+		List<TransportDescriptor> descriptors = new ArrayList<TransportDescriptor>();
+		r.readListStart(); // Descriptors start
+		while (r.hasList()) {
+			r.readListStart();
+			while (!r.hasListEnd()) {
+				TransportId id =
+						new TransportId(r.readString(MAX_PROPERTY_LENGTH));
+				TransportProperties p = new TransportProperties();
+				r.readDictionaryStart();
+				while (!r.hasDictionaryEnd()) {
+					String key = r.readString(MAX_PROPERTY_LENGTH);
+					String value = r.readString(MAX_PROPERTY_LENGTH);
+					p.put(key, value);
+				}
+				r.readDictionaryEnd();
+				descriptors.add(new TransportDescriptor(id, p));
+			}
+			r.readListEnd();
+		}
+		r.readListEnd(); // Descriptors end
+		r.readListEnd(); // Payload end
+		if (!r.eof())
+			throw new FormatException();
+		return new Payload(commitment, descriptors);
+	}
+}