diff --git a/api/net/sf/briar/api/ContactTransport.java b/api/net/sf/briar/api/ContactTransport.java
new file mode 100644
index 0000000000000000000000000000000000000000..c1d25893fe099fee92d4c48cc56ac054f2663831
--- /dev/null
+++ b/api/net/sf/briar/api/ContactTransport.java
@@ -0,0 +1,36 @@
+package net.sf.briar.api;
+
+import net.sf.briar.api.protocol.TransportId;
+
+public class ContactTransport extends TemporarySecret {
+
+	private final long epoch, clockDiff, latency;
+	private final boolean alice;
+
+	public ContactTransport(ContactId contactId, TransportId transportId,
+			long epoch, long clockDiff, long latency, boolean alice,
+			long period, byte[] secret, long outgoing, long centre,
+			byte[] bitmap) {
+		super(contactId, transportId, period, secret, outgoing, centre, bitmap);
+		this.epoch = epoch;
+		this.clockDiff = clockDiff;
+		this.latency = latency;
+		this.alice = alice;
+	}
+
+	public long getEpoch() {
+		return epoch;
+	}
+
+	public long getClockDifference() {
+		return clockDiff;
+	}
+
+	public long getLatency() {
+		return latency;
+	}
+
+	public boolean getAlice() {
+		return alice;
+	}
+}
diff --git a/api/net/sf/briar/api/TemporarySecret.java b/api/net/sf/briar/api/TemporarySecret.java
new file mode 100644
index 0000000000000000000000000000000000000000..2ea4f26ccf38a5c0f6cb1ffacb9279fc66e839d3
--- /dev/null
+++ b/api/net/sf/briar/api/TemporarySecret.java
@@ -0,0 +1,51 @@
+package net.sf.briar.api;
+
+import net.sf.briar.api.protocol.TransportId;
+
+public class TemporarySecret {
+
+	protected final ContactId contactId;
+	protected final TransportId transportId;
+	protected final long period, outgoing, centre;
+	protected final byte[] secret, bitmap;
+
+	public TemporarySecret(ContactId contactId, TransportId transportId,
+			long period, byte[] secret, long outgoing, long centre,
+			byte[] bitmap) {
+		this.contactId = contactId;
+		this.transportId = transportId;
+		this.period = period;
+		this.secret = secret;
+		this.outgoing = outgoing;
+		this.centre = centre;
+		this.bitmap = bitmap;
+	}
+
+	public ContactId getContactId() {
+		return contactId;
+	}
+
+	public TransportId getTransportId() {
+		return transportId;
+	}
+
+	public long getPeriod() {
+		return period;
+	}
+
+	public byte[] getSecret() {
+		return secret;
+	}
+
+	public long getOutgoingConnectionCounter() {
+		return outgoing;
+	}
+
+	public long getWindowCentre() {
+		return centre;
+	}
+
+	public byte[] getWindowBitmap() {
+		return bitmap;
+	}
+}
diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java
index 90de9bc4f71abf6a843d230e8b6518998e256e65..83e5da292edb7899e763ebe3a3e649be76bf6423 100644
--- a/api/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/api/net/sf/briar/api/crypto/CryptoComponent.java
@@ -9,16 +9,49 @@ import javax.crypto.Cipher;
 
 public interface CryptoComponent {
 
-	ErasableKey deriveTagKey(byte[] secret, boolean initiator);
-
-	ErasableKey deriveFrameKey(byte[] secret, boolean initiator);
-
-	byte[][] deriveInitialSecrets(byte[] ourPublicKey, byte[] theirPublicKey,
-			PrivateKey ourPrivateKey, int invitationCode, boolean initiator);
-
-	int deriveConfirmationCode(byte[] secret);
-
-	byte[] deriveNextSecret(byte[] secret, int index, long connection);
+	/**
+	 * Derives a tag key from the given temporary secret.
+	 * @param alice Indicates whether the key is for connections initiated by
+	 * Alice or Bob.
+	 */
+	ErasableKey deriveTagKey(byte[] secret, boolean alice);
+
+	/**
+	 * Derives a frame key from the given temporary secret and connection
+	 * number.
+	 * @param alice Indicates whether the key is for a connection initiated by
+	 * Alice or Bob.
+	 * @param initiator Indicates whether the key is for the initiator's or the
+	 * responder's side of the connection.
+	 */
+	ErasableKey deriveFrameKey(byte[] secret, long connection, boolean alice,
+			boolean initiator);
+
+	/**
+	 * Derives an initial shared secret from two public keys and one of the
+	 * corresponding private keys.
+	 * @param alice Indicates whether the private key belongs to Alice or Bob.
+	 */
+	byte[] deriveInitialSecret(byte[] ourPublicKey, byte[] theirPublicKey,
+			PrivateKey ourPrivateKey, boolean alice);
+
+	/**
+	 * Generates a random invitation code.
+	 */
+	int generateInvitationCode();
+
+	/**
+	 * Derives two confirmation codes from the given initial shared secret. The
+	 * first code is for Alice to give to Bob; the second is for Bob to give to
+	 * Alice.
+	 */
+	int[] deriveConfirmationCodes(byte[] secret);
+
+	/**
+	 * Derives a temporary secret for the given period from the previous
+	 * period's temporary secret.
+	 */
+	byte[] deriveNextSecret(byte[] secret, long period);
 
 	KeyPair generateAgreementKeyPair();
 
diff --git a/api/net/sf/briar/api/crypto/KeyManager.java b/api/net/sf/briar/api/crypto/KeyManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..7d71eb199dd85d8229fc6f126e53b3c9dacaed08
--- /dev/null
+++ b/api/net/sf/briar/api/crypto/KeyManager.java
@@ -0,0 +1,15 @@
+package net.sf.briar.api.crypto;
+
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.transport.ConnectionContext;
+
+public interface KeyManager {
+
+	/**
+	 * Returns a connection context for connecting to the given contact over
+	 * the given transport, or null if the contact does not support the
+	 * transport.
+	 */
+	ConnectionContext getConnectionContext(ContactId c, TransportId t);
+}
diff --git a/api/net/sf/briar/api/db/DatabaseComponent.java b/api/net/sf/briar/api/db/DatabaseComponent.java
index 85b35016d697acc5561b74221d1462b0470cae41..2581773ec36f927669361162861dc741d0673ce0 100644
--- a/api/net/sf/briar/api/db/DatabaseComponent.java
+++ b/api/net/sf/briar/api/db/DatabaseComponent.java
@@ -5,7 +5,9 @@ import java.util.Collection;
 import java.util.Map;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.ContactTransport;
 import net.sf.briar.api.Rating;
+import net.sf.briar.api.TemporarySecret;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.event.DatabaseListener;
@@ -22,10 +24,7 @@ import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionWindow;
 
 /**
  * Encapsulates the database implementation and exposes high-level operations
@@ -50,10 +49,9 @@ public interface DatabaseComponent {
 	void removeListener(DatabaseListener d);
 
 	/**
-	 * Adds a new contact to the database with the given secrets and returns an
-	 * ID for the contact.
+	 * Adds a new contact to the database and returns an ID for the contact.
 	 */
-	ContactId addContact(byte[] inSecret, byte[] outSecret) throws DbException;
+	ContactId addContact() throws DbException;
 
 	/** Adds a locally generated group message to the database. */
 	void addLocalGroupMessage(Message m) throws DbException;
@@ -62,10 +60,10 @@ public interface DatabaseComponent {
 	void addLocalPrivateMessage(Message m, ContactId c) throws DbException;
 
 	/**
-	 * Allocates and returns a local index for the given transport. Returns
-	 * null if all indices have been allocated.
+	 * Stores the given temporary secrets and deletes any secrets that have
+	 * been made obsolete.
 	 */
-	TransportIndex addTransport(TransportId t) throws DbException;
+	void addSecrets(Collection<TemporarySecret> secrets) throws DbException;
 
 	/**
 	 * Generates an acknowledgement for the given contact. Returns null if
@@ -101,7 +99,7 @@ public interface DatabaseComponent {
 	 * an update is not due.
 	 */
 	SubscriptionUpdate generateSubscriptionUpdate(ContactId c)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Generates a transport update for the given contact. Returns null if an
@@ -112,28 +110,11 @@ public interface DatabaseComponent {
 	/** Returns the configuration for the given transport. */
 	TransportConfig getConfig(TransportId t) throws DbException;
 
-	/**
-	 * Returns an outgoing connection context for the given contact and
-	 * transport.
-	 */
-	ConnectionContext getConnectionContext(ContactId c, TransportIndex i)
-	throws DbException;
-
-	/**
-	 * Returns the connection reordering window for the given contact and
-	 * transport.
-	 */
-	ConnectionWindow getConnectionWindow(ContactId c, TransportIndex i)
-	throws DbException;
-
 	/** Returns the IDs of all contacts. */
 	Collection<ContactId> getContacts() throws DbException;
 
-	/**
-	 * Returns the local index for the given transport, or null if no index
-	 * has been allocated.
-	 */
-	TransportIndex getLocalIndex(TransportId t) throws DbException;
+	/** Returns all contact transports. */
+	Collection<ContactTransport> getContactTransports() throws DbException;
 
 	/** Returns the local transport properties for the given transport. */
 	TransportProperties getLocalProperties(TransportId t) throws DbException;
@@ -147,16 +128,9 @@ public interface DatabaseComponent {
 	/** Returns the user's rating for the given author. */
 	Rating getRating(AuthorId a) throws DbException;
 
-	/**
-	 * Returns the given contact's index for the given transport, or null if
-	 * the contact does not support the transport.
-	 */
-	TransportIndex getRemoteIndex(ContactId c, TransportId t)
-	throws DbException;
-
 	/** Returns all remote transport properties for the given transport. */
 	Map<ContactId, TransportProperties> getRemoteProperties(TransportId t)
-	throws DbException;
+			throws DbException;
 
 	/** Returns the set of groups to which the user subscribes. */
 	Collection<Group> getSubscriptions() throws DbException;
@@ -170,6 +144,13 @@ public interface DatabaseComponent {
 	/** Returns true if any messages are sendable to the given contact. */
 	boolean hasSendableMessages(ContactId c) throws DbException;
 
+	/**
+	 * Increments the outgoing connection counter for the given contact
+	 * transport in the given rotation period.
+	 */
+	void incrementConnectionCounter(ContactId c, TransportId t, long period)
+			throws DbException;
+
 	/** Processes an acknowledgement from the given contact. */
 	void receiveAck(ContactId c, Ack a) throws DbException;
 
@@ -188,11 +169,11 @@ public interface DatabaseComponent {
 
 	/** Processes a subscription update from the given contact. */
 	void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate s)
-	throws DbException;
+			throws DbException;
 
 	/** Processes a transport update from the given contact. */
 	void receiveTransportUpdate(ContactId c, TransportUpdate t)
-	throws DbException;
+			throws DbException;
 
 	/** Removes a contact (and all associated state) from the database. */
 	void removeContact(ContactId c) throws DbException;
@@ -204,18 +185,18 @@ public interface DatabaseComponent {
 	void setConfig(TransportId t, TransportConfig c) throws DbException;
 
 	/**
-	 * Sets the connection reordering window for the given contact and
-	 * transport.
+	 * Sets the connection reordering window for the given contact transport
+	 * in the given rotation period.
 	 */
-	void setConnectionWindow(ContactId c, TransportIndex i,
-			ConnectionWindow w) throws DbException;
+	void setConnectionWindow(ContactId c, TransportId t, long period,
+			long centre, byte[] bitmap) throws DbException;
 
 	/**
 	 * Sets the local transport properties for the given transport, replacing
 	 * any existing properties for that transport.
 	 */
 	void setLocalProperties(TransportId t, TransportProperties p)
-	throws DbException;
+			throws DbException;
 
 	/** Records the user's rating for the given author. */
 	void setRating(AuthorId a, Rating r) throws DbException;
@@ -228,7 +209,7 @@ public interface DatabaseComponent {
 	 * to any other contacts.
 	 */
 	void setVisibility(GroupId g, Collection<ContactId> visible)
-	throws DbException;
+			throws DbException;
 
 	/** Subscribes to the given group. */
 	void subscribe(Group g) throws DbException;
diff --git a/api/net/sf/briar/api/db/NoSuchContactException.java b/api/net/sf/briar/api/db/NoSuchContactException.java
index 72b735493fc7020f3128d295ebdb79af04735ab8..8813cafa70687562ac36ac619719cd114c7e56f1 100644
--- a/api/net/sf/briar/api/db/NoSuchContactException.java
+++ b/api/net/sf/briar/api/db/NoSuchContactException.java
@@ -7,8 +7,4 @@ package net.sf.briar.api.db;
 public class NoSuchContactException extends DbException {
 
 	private static final long serialVersionUID = -7048538231308207386L;
-
-	public NoSuchContactException() {
-		super();
-	}
 }
diff --git a/api/net/sf/briar/api/db/NoSuchContactTransportException.java b/api/net/sf/briar/api/db/NoSuchContactTransportException.java
new file mode 100644
index 0000000000000000000000000000000000000000..0bbf6828f320848cd9e6689ee0452fde36ecf50d
--- /dev/null
+++ b/api/net/sf/briar/api/db/NoSuchContactTransportException.java
@@ -0,0 +1,10 @@
+package net.sf.briar.api.db;
+
+/**
+ * Thrown when a database operation is attempted for a contact transport that
+ * is not in the database.
+ */
+public class NoSuchContactTransportException extends DbException {
+
+	private static final long serialVersionUID = -6274982612759573100L;
+}
diff --git a/api/net/sf/briar/api/db/event/TransportAddedEvent.java b/api/net/sf/briar/api/db/event/TransportAddedEvent.java
deleted file mode 100644
index 75156882292478155b0ef2b3c29e30511c5e6f8b..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/db/event/TransportAddedEvent.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package net.sf.briar.api.db.event;
-
-import net.sf.briar.api.protocol.TransportId;
-
-/** An event that is broadcast when a transport is added. */
-public class TransportAddedEvent extends DatabaseEvent {
-
-	private final TransportId transportId;
-
-	public TransportAddedEvent(TransportId transportId) {
-		this.transportId = transportId;
-	}
-
-	public TransportId getTransportId() {
-		return transportId;
-	}
-}
diff --git a/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java b/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java
deleted file mode 100644
index 64199a59c1e8e43357d0f7d7ff172898fec2e8a0..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.sf.briar.api.plugins;
-
-public interface IncomingInvitationCallback extends InvitationCallback {
-
-	int enterInvitationCode();
-}
diff --git a/api/net/sf/briar/api/plugins/InvitationCallback.java b/api/net/sf/briar/api/plugins/InvitationCallback.java
deleted file mode 100644
index fb37cf40dd6b840d805d0f4c750f53218d31611c..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/plugins/InvitationCallback.java
+++ /dev/null
@@ -1,14 +0,0 @@
-package net.sf.briar.api.plugins;
-
-public interface InvitationCallback {
-
-	boolean isCancelled();
-
-	int enterConfirmationCode(int code);
-
-	void showProgress(String... message);
-
-	void showFailure(String... message);
-
-	void showSuccess();
-}
diff --git a/api/net/sf/briar/api/plugins/InvitationStarter.java b/api/net/sf/briar/api/plugins/InvitationStarter.java
deleted file mode 100644
index 92c9acd65ade2b900bd8ee7808af5b4dc3d54e51..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/plugins/InvitationStarter.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package net.sf.briar.api.plugins;
-
-import net.sf.briar.api.plugins.duplex.DuplexPlugin;
-
-public interface InvitationStarter {
-
-	void startIncomingInvitation(DuplexPlugin plugin,
-			IncomingInvitationCallback callback);
-
-	void startOutgoingInvitation(DuplexPlugin plugin,
-			OutgoingInvitationCallback callback);
-}
diff --git a/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java b/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java
deleted file mode 100644
index b80e82bc7c3beafd349d3e3deceb7962cebb0fb7..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package net.sf.briar.api.plugins;
-
-public interface OutgoingInvitationCallback extends InvitationCallback {
-
-	void showInvitationCode(int code);
-}
diff --git a/api/net/sf/briar/api/protocol/Transport.java b/api/net/sf/briar/api/protocol/Transport.java
index 37c9ab021c65e35050ece282d044820e4bd4bbfb..707d84fa7ef8e5c95e934b0024ac24fd089f8d51 100644
--- a/api/net/sf/briar/api/protocol/Transport.java
+++ b/api/net/sf/briar/api/protocol/Transport.java
@@ -8,29 +8,21 @@ public class Transport extends TreeMap<String, String> {
 	private static final long serialVersionUID = 4900420175715429560L;
 
 	private final TransportId id;
-	private final TransportIndex index;
 
-	public Transport(TransportId id, TransportIndex index,
-			Map<String, String> p) {
+	public Transport(TransportId id, Map<String, String> p) {
 		super(p);
 		this.id = id;
-		this.index = index;
 	}
 
-	public Transport(TransportId id, TransportIndex index) {
+	public Transport(TransportId id) {
 		super();
 		this.id = id;
-		this.index = index;
 	}
 
 	public TransportId getId() {
 		return id;
 	}
 
-	public TransportIndex getIndex() {
-		return index;
-	}
-
 	@Override
 	public int hashCode() {
 		return id.hashCode();
@@ -40,7 +32,7 @@ public class Transport extends TreeMap<String, String> {
 	public boolean equals(Object o) {
 		if(o instanceof Transport) {
 			Transport t = (Transport) o;
-			return id.equals(t.id) && index.equals(t.index) && super.equals(o);
+			return id.equals(t.id) && super.equals(o);
 		}
 		return false;
 	}
diff --git a/api/net/sf/briar/api/protocol/TransportIndex.java b/api/net/sf/briar/api/protocol/TransportIndex.java
deleted file mode 100644
index c167b49c69ac23498bea7870aae293d216a2674d..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/protocol/TransportIndex.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package net.sf.briar.api.protocol;
-
-
-/**
- * Type-safe wrapper for an integer that uniquely identifies a transport plugin
- * within the scope of a single node.
- */
-public class TransportIndex {
-
-	private final int index;
-
-	public TransportIndex(int index) {
-		if(index < 0 || index >= ProtocolConstants.MAX_TRANSPORTS)
-			throw new IllegalArgumentException();
-		this.index = index;
-	}
-
-	public int getInt() {
-		return index;
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if(o instanceof TransportIndex)
-			return index == ((TransportIndex) o).index;
-		return false;
-	}
-
-	@Override
-	public int hashCode() {
-		return index;
-	}
-}
diff --git a/api/net/sf/briar/api/protocol/duplex/DuplexConnectionFactory.java b/api/net/sf/briar/api/protocol/duplex/DuplexConnectionFactory.java
index 0d8e1dce2d5ef2f7371a863e7bba2f7a07cf3b38..c7504f376e81ea019e7a8145ed65e66ce0706c5c 100644
--- a/api/net/sf/briar/api/protocol/duplex/DuplexConnectionFactory.java
+++ b/api/net/sf/briar/api/protocol/duplex/DuplexConnectionFactory.java
@@ -3,14 +3,12 @@ package net.sf.briar.api.protocol.duplex;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionContext;
 
 public interface DuplexConnectionFactory {
 
-	void createIncomingConnection(ConnectionContext ctx, TransportId t,
-			DuplexTransportConnection d);
+	void createIncomingConnection(ConnectionContext ctx, DuplexTransportConnection d);
 
-	void createOutgoingConnection(ContactId c, TransportId t, TransportIndex i,
+	void createOutgoingConnection(ContactId c, TransportId t,
 			DuplexTransportConnection d);
 }
diff --git a/api/net/sf/briar/api/protocol/simplex/SimplexConnectionFactory.java b/api/net/sf/briar/api/protocol/simplex/SimplexConnectionFactory.java
index 63c33a290b7bbb6560a08266155a3313732350a8..fa8d2c021f25de08e604ed0803de38c77d41352f 100644
--- a/api/net/sf/briar/api/protocol/simplex/SimplexConnectionFactory.java
+++ b/api/net/sf/briar/api/protocol/simplex/SimplexConnectionFactory.java
@@ -4,14 +4,12 @@ import net.sf.briar.api.ContactId;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionContext;
 
 public interface SimplexConnectionFactory {
 
-	void createIncomingConnection(ConnectionContext ctx, TransportId t,
-			SimplexTransportReader r);
+	void createIncomingConnection(ConnectionContext ctx, SimplexTransportReader r);
 
-	void createOutgoingConnection(ContactId c, TransportId t, TransportIndex i,
+	void createOutgoingConnection(ContactId c, TransportId t,
 			SimplexTransportWriter w);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionContext.java b/api/net/sf/briar/api/transport/ConnectionContext.java
index 1a5dbd551954a4c08197a9177e60837a99494403..f9f547a4fdfbc4889acadb4b81416f4bd1d80db8 100644
--- a/api/net/sf/briar/api/transport/ConnectionContext.java
+++ b/api/net/sf/briar/api/transport/ConnectionContext.java
@@ -1,15 +1,47 @@
 package net.sf.briar.api.transport;
 
 import net.sf.briar.api.ContactId;
-import net.sf.briar.api.protocol.TransportIndex;
+import net.sf.briar.api.protocol.TransportId;
 
-public interface ConnectionContext {
+public class ConnectionContext {
 
-	ContactId getContactId();
+	private final ContactId contactId;
+	private final TransportId transportId;
+	private final byte[] tag, secret;
+	private final long connection;
+	private final boolean alice;
 
-	TransportIndex getTransportIndex();
+	public ConnectionContext(ContactId contactId, TransportId transportId,
+			byte[] tag, byte[] secret, long connection, boolean alice) {
+		this.contactId = contactId;
+		this.transportId = transportId;
+		this.tag = tag;
+		this.secret = secret;
+		this.connection = connection;
+		this.alice = alice;
+	}
 
-	long getConnectionNumber();
+	public ContactId getContactId() {
+		return contactId;
+	}
 
-	byte[] getSecret();
+	public TransportId getTransportId() {
+		return transportId;
+	}
+
+	public byte[] getTag() {
+		return tag;
+	}
+
+	public byte[] getSecret() {
+		return secret;
+	}
+
+	public long getConnectionNumber() {
+		return connection;
+	}
+
+	public boolean getAlice() {
+		return alice;
+	}
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionContextFactory.java b/api/net/sf/briar/api/transport/ConnectionContextFactory.java
deleted file mode 100644
index 374a357ef8515b38757d1c2903e2a9c96da1c4e4..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/ConnectionContextFactory.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.api.transport;
-
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.protocol.TransportIndex;
-
-public interface ConnectionContextFactory {
-
-	ConnectionContext createConnectionContext(ContactId c, TransportIndex i,
-			long connection, byte[] secret);
-
-	ConnectionContext createNextConnectionContext(ContactId c, TransportIndex i,
-			long connection, byte[] previousSecret);
-}
diff --git a/api/net/sf/briar/api/transport/ConnectionDispatcher.java b/api/net/sf/briar/api/transport/ConnectionDispatcher.java
index 4d2e144136511e1c186faec36b83521b932ab40e..4e7dbd793dab7a8bc2cc71557258d33f8d8c07b8 100644
--- a/api/net/sf/briar/api/transport/ConnectionDispatcher.java
+++ b/api/net/sf/briar/api/transport/ConnectionDispatcher.java
@@ -5,17 +5,16 @@ import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 
 public interface ConnectionDispatcher {
 
 	void dispatchReader(TransportId t, SimplexTransportReader r);
 
-	void dispatchWriter(ContactId c, TransportId t, TransportIndex i,
+	void dispatchWriter(ContactId c, TransportId t,
 			SimplexTransportWriter w);
 
 	void dispatchIncomingConnection(TransportId t, DuplexTransportConnection d);
 
 	void dispatchOutgoingConnection(ContactId c, TransportId t,
-			TransportIndex i, DuplexTransportConnection d);
+			DuplexTransportConnection d);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionReaderFactory.java b/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
index c688ca9852b5be11c35830c60195e7473b98af1a..171a594bd92f6b85fea7f534b4cfbb62f4643a18 100644
--- a/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
+++ b/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
@@ -8,6 +8,6 @@ public interface ConnectionReaderFactory {
 	 * Creates a connection reader for a simplex connection or one side of a
 	 * duplex connection. The secret is erased before this method returns.
 	 */
-	ConnectionReader createConnectionReader(InputStream in, byte[] secret,
-			boolean initiator);
+	ConnectionReader createConnectionReader(InputStream in,
+			ConnectionContext ctx, boolean initiator);
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionWindow.java b/api/net/sf/briar/api/transport/ConnectionWindow.java
index 794846fcd408a7b15e6890766f0100ae29872eda..e2ecd949033c134251a9305d123e03f803b7a083 100644
--- a/api/net/sf/briar/api/transport/ConnectionWindow.java
+++ b/api/net/sf/briar/api/transport/ConnectionWindow.java
@@ -1,14 +1,12 @@
 package net.sf.briar.api.transport;
 
-import java.util.Map;
+import java.util.Set;
 
 public interface ConnectionWindow {
 
 	boolean isSeen(long connection);
 
-	byte[] setSeen(long connection);
+	void setSeen(long connection);
 
-	Map<Long, byte[]> getUnseen();
-
-	void erase();
+	Set<Long> getUnseen();
 }
diff --git a/api/net/sf/briar/api/transport/ConnectionWindowFactory.java b/api/net/sf/briar/api/transport/ConnectionWindowFactory.java
deleted file mode 100644
index 110df0e99cc206df3d9bb26c422043ca1ef9c728..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/ConnectionWindowFactory.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.sf.briar.api.transport;
-
-import java.util.Map;
-
-import net.sf.briar.api.protocol.TransportIndex;
-
-public interface ConnectionWindowFactory {
-
-	ConnectionWindow createConnectionWindow(TransportIndex i, byte[] secret);
-
-	ConnectionWindow createConnectionWindow(TransportIndex i,
-			Map<Long, byte[]> unseen);
-}
diff --git a/api/net/sf/briar/api/transport/ConnectionWriterFactory.java b/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
index b5996f3f5f9a6d9e799349537579410a3b5488f3..f2fd8aaf0727e9909fa1f7bd8adccb2e0ce1c6b8 100644
--- a/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
+++ b/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
@@ -9,5 +9,5 @@ public interface ConnectionWriterFactory {
 	 * duplex connection. The secret is erased before this method returns.
 	 */
 	ConnectionWriter createConnectionWriter(OutputStream out, long capacity,
-			byte[] secret, boolean initiator);
+			ConnectionContext ctx, boolean initiator);
 }
diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java
index 8436319d220c426191180d0421f528a0f30bcd70..58a7a1e5cba835c5ad149e929da1303866b4d49b 100644
--- a/components/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -1,6 +1,7 @@
 package net.sf.briar.crypto;
 
 import static net.sf.briar.api.plugins.InvitationConstants.CODE_BITS;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
@@ -48,18 +49,23 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final int GCM_MAC_LENGTH = 16; // 128 bits
 
 	// Labels for key derivation
-	private static final byte[] TAG = { 'T', 'A', 'G' };
-	private static final byte[] FRAME = { 'F', 'R', 'A', 'M', 'E' };
+	private static final byte[] A_TAG = { 'A', '_', 'T', 'A', 'G', '\0' };
+	private static final byte[] B_TAG = { 'B', '_', 'T', 'A', 'G', '\0' };
+	private static final byte[] A_FRAME_A =
+		{ 'A', '_', 'F', 'R', 'A', 'M', 'E', '_', 'A', '\0' };
+	private static final byte[] A_FRAME_B =
+		{ 'A', '_', 'F', 'R', 'A', 'M', 'E', '_', 'B', '\0' };
+	private static final byte[] B_FRAME_A =
+		{ 'B', '_', 'F', 'R', 'A', 'M', 'E', '_', 'A', '\0' };
+	private static final byte[] B_FRAME_B =
+		{ 'B', '_', 'F', 'R', 'A', 'M', 'E', '_', 'B', '\0' };
 	// Labels for secret derivation
-	private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T' };
-	private static final byte[] NEXT = { 'N', 'E', 'X', 'T' };
+	private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T', '\0' };
+	private static final byte[] ROTATE = { 'R', 'O', 'T', 'A', 'T', 'E', '\0' };
 	// Label for confirmation code derivation
-	private static final byte[] CODE = { 'C', 'O', 'D', 'E' };
-	// Context strings for key and confirmation code derivation
-	private static final byte[] INITIATOR = { 'I' };
-	private static final byte[] RESPONDER = { 'R' };
+	private static final byte[] CODE = { 'C', 'O', 'D', 'E', '\0' };
 	// Blank plaintext for key derivation
-	private static final byte[] KEY_DERIVATION_INPUT =
+	private static final byte[] KEY_DERIVATION_BLANK_PLAINTEXT =
 			new byte[SECRET_KEY_BYTES];
 
 	private final KeyParser agreementKeyParser, signatureKeyParser;
@@ -87,43 +93,46 @@ class CryptoComponentImpl implements CryptoComponent {
 		secureRandom = new SecureRandom();
 	}
 
-	public ErasableKey deriveTagKey(byte[] secret, boolean initiator) {
-		if(initiator) return deriveKey(secret, TAG, INITIATOR);
-		else return deriveKey(secret, TAG, RESPONDER);
+	public ErasableKey deriveTagKey(byte[] secret, boolean alice) {
+		if(alice) return deriveKey(secret, A_TAG, 0L);
+		else return deriveKey(secret, B_TAG, 0L);
 	}
 
-	public ErasableKey deriveFrameKey(byte[] secret, boolean initiator) {
-		if(initiator) return deriveKey(secret, FRAME, INITIATOR);
-		else return deriveKey(secret, FRAME, RESPONDER);
+	public ErasableKey deriveFrameKey(byte[] secret, long connection,
+			boolean alice, boolean initiator) {
+		if(alice) {
+			if(initiator) return deriveKey(secret, A_FRAME_A, connection);
+			else return deriveKey(secret, A_FRAME_B, connection);
+		} else {
+			if(initiator) return deriveKey(secret, B_FRAME_A, connection);
+			else return deriveKey(secret, B_FRAME_B, connection);
+		}
 	}
 
-	private ErasableKey deriveKey(byte[] secret, byte[] label, byte[] context) {
+	private ErasableKey deriveKey(byte[] secret, byte[] label, long context) {
 		byte[] key = counterModeKdf(secret, label, context);
 		return new ErasableKeyImpl(key, SECRET_KEY_ALGO);
 	}
 
 	// Key derivation function based on a block cipher in CTR mode - see
 	// NIST SP 800-108, section 5.1
-	private byte[] counterModeKdf(byte[] secret, byte[] label, byte[] context) {
+	private byte[] counterModeKdf(byte[] secret, byte[] label, long context) {
 		// The secret must be usable as a key
 		if(secret.length != SECRET_KEY_BYTES)
 			throw new IllegalArgumentException();
 		// The label and context must leave a byte free for the counter
-		if(label.length + context.length + 2 >= KEY_DERIVATION_IV_BYTES)
+		if(label.length + 4 >= KEY_DERIVATION_IV_BYTES)
 			throw new IllegalArgumentException();
-		// The IV contains the length-prefixed label and context
 		byte[] ivBytes = new byte[KEY_DERIVATION_IV_BYTES];
-		ByteUtils.writeUint8(label.length, ivBytes, 0);
-		System.arraycopy(label, 0, ivBytes, 1, label.length);
-		ByteUtils.writeUint8(context.length, ivBytes, label.length + 1);
-		System.arraycopy(context, 0, ivBytes, label.length + 2, context.length);
+		System.arraycopy(label, 0, ivBytes, 0, label.length);
+		ByteUtils.writeUint32(context, ivBytes, label.length);
 		// Use the secret and the IV to encrypt a blank plaintext
 		IvParameterSpec iv = new IvParameterSpec(ivBytes);
 		ErasableKey key = new ErasableKeyImpl(secret, SECRET_KEY_ALGO);
 		try {
 			Cipher cipher = Cipher.getInstance(KEY_DERIVATION_ALGO, PROVIDER);
 			cipher.init(Cipher.ENCRYPT_MODE, key, iv);
-			byte[] output = cipher.doFinal(KEY_DERIVATION_INPUT);
+			byte[] output = cipher.doFinal(KEY_DERIVATION_BLANK_PLAINTEXT);
 			assert output.length == SECRET_KEY_BYTES;
 			return output;
 		} catch(GeneralSecurityException e) {
@@ -131,27 +140,22 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
-	public byte[][] deriveInitialSecrets(byte[] ourPublicKey,
-			byte[] theirPublicKey, PrivateKey ourPrivateKey, int invitationCode,
-			boolean initiator) {
+	public byte[] deriveInitialSecret(byte[] ourPublicKey,
+			byte[] theirPublicKey, PrivateKey ourPrivateKey, boolean alice) {
 		try {
 			PublicKey theirPublic = agreementKeyParser.parsePublicKey(
 					theirPublicKey);
 			MessageDigest messageDigest = getMessageDigest();
 			byte[] ourHash = messageDigest.digest(ourPublicKey);
 			byte[] theirHash = messageDigest.digest(theirPublicKey);
-			// The initiator and responder info are hashes of the public keys
-			byte[] initiatorInfo, responderInfo;
-			if(initiator) {
-				initiatorInfo = ourHash;
-				responderInfo = theirHash;
+			byte[] aliceInfo, bobInfo;
+			if(alice) {
+				aliceInfo = ourHash;
+				bobInfo = theirHash;
 			} else {
-				initiatorInfo = theirHash;
-				responderInfo = ourHash;
+				aliceInfo = theirHash;
+				bobInfo = ourHash;
 			}
-			// The public info is the invitation code as a uint32
-			byte[] publicInfo = new byte[4];
-			ByteUtils.writeUint32(invitationCode, publicInfo, 0);
 			// The raw secret comes from the key agreement algorithm
 			KeyAgreement keyAgreement = KeyAgreement.getInstance(AGREEMENT_ALGO,
 					PROVIDER);
@@ -160,17 +164,12 @@ class CryptoComponentImpl implements CryptoComponent {
 			byte[] rawSecret = keyAgreement.generateSecret();
 			// Derive the cooked secret from the raw secret using the
 			// concatenation KDF
-			byte[] cookedSecret = concatenationKdf(rawSecret, FIRST,
-					initiatorInfo, responderInfo, publicInfo);
+			byte[] cookedSecret = concatenationKdf(rawSecret, FIRST, aliceInfo,
+					bobInfo);
 			ByteUtils.erase(rawSecret);
-			// Derive the incoming and outgoing secrets from the cooked secret
-			// using the CTR mode KDF
-			byte[][] secrets = new byte[2][];
-			secrets[0] = counterModeKdf(cookedSecret, FIRST, INITIATOR);
-			secrets[1] = counterModeKdf(cookedSecret, FIRST, RESPONDER);
-			ByteUtils.erase(cookedSecret);
-			return secrets;
+			return cookedSecret;
 		} catch(GeneralSecurityException e) {
+			// FIXME: Throw instead of returning null?
 			return null;
 		}
 	}
@@ -178,7 +177,7 @@ class CryptoComponentImpl implements CryptoComponent {
 	// Key derivation function based on a hash function - see NIST SP 800-56A,
 	// section 5.8
 	private byte[] concatenationKdf(byte[] rawSecret, byte[] label,
-			byte[] initiatorInfo, byte[] responderInfo, byte[] publicInfo) {
+			byte[] initiatorInfo, byte[] responderInfo) {
 		// The output of the hash function must be long enough to use as a key
 		MessageDigest messageDigest = getMessageDigest();
 		if(messageDigest.getDigestLength() < SECRET_KEY_BYTES)
@@ -197,9 +196,6 @@ class CryptoComponentImpl implements CryptoComponent {
 		ByteUtils.writeUint8(responderInfo.length, length, 0);
 		messageDigest.update(length);
 		messageDigest.update(responderInfo);
-		ByteUtils.writeUint8(publicInfo.length, length, 0);
-		messageDigest.update(length);
-		messageDigest.update(publicInfo);
 		byte[] hash = messageDigest.digest();
 		// The secret is the first SECRET_KEY_BYTES bytes of the hash
 		byte[] output = new byte[SECRET_KEY_BYTES];
@@ -208,22 +204,28 @@ class CryptoComponentImpl implements CryptoComponent {
 		return output;
 	}
 
-	public byte[] deriveNextSecret(byte[] secret, int index, long connection) {
-		if(index < 0 || index > ByteUtils.MAX_16_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		if(connection < 0 || connection > ByteUtils.MAX_32_BIT_UNSIGNED)
+	public byte[] deriveNextSecret(byte[] secret, long period) {
+		if(period < 0 || period > MAX_32_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
-		byte[] context = new byte[6];
-		ByteUtils.writeUint16(index, context, 0);
-		ByteUtils.writeUint32(connection, context, 2);
-		return counterModeKdf(secret, NEXT, context);
+		return counterModeKdf(secret, ROTATE, period);
+	}
+
+	public int generateInvitationCode() {
+		int codeBytes = (int) Math.ceil(CODE_BITS / 8.0);
+		byte[] random = new byte[codeBytes];
+		secureRandom.nextBytes(random);
+		return ByteUtils.readUint(random, CODE_BITS);
 	}
 
-	public int deriveConfirmationCode(byte[] secret) {
-		byte[] output = counterModeKdf(secret, CODE, CODE);
-		int code =  ByteUtils.readUint(output, CODE_BITS);
-		ByteUtils.erase(output);
-		return code;
+	public int[] deriveConfirmationCodes(byte[] secret) {
+		byte[] alice = counterModeKdf(secret, CODE, 0);
+		byte[] bob = counterModeKdf(secret, CODE, 1);
+		int[] codes = new int[2];
+		codes[0] = ByteUtils.readUint(alice, CODE_BITS);
+		codes[1] = ByteUtils.readUint(bob, CODE_BITS);
+		ByteUtils.erase(alice);
+		ByteUtils.erase(bob);
+		return codes;
 	}
 
 	public KeyPair generateAgreementKeyPair() {
diff --git a/components/net/sf/briar/db/KeyRotator.java b/components/net/sf/briar/crypto/KeyRotator.java
similarity index 94%
rename from components/net/sf/briar/db/KeyRotator.java
rename to components/net/sf/briar/crypto/KeyRotator.java
index 200a1ed24f1f3f9b1f074aa3ebda8954558b0796..71dd43d7e2028fe3b31cab8d578f3242c86e21b4 100644
--- a/components/net/sf/briar/db/KeyRotator.java
+++ b/components/net/sf/briar/crypto/KeyRotator.java
@@ -1,4 +1,4 @@
-package net.sf.briar.db;
+package net.sf.briar.crypto;
 
 import net.sf.briar.api.db.DbException;
 
diff --git a/components/net/sf/briar/db/KeyRotatorImpl.java b/components/net/sf/briar/crypto/KeyRotatorImpl.java
similarity index 97%
rename from components/net/sf/briar/db/KeyRotatorImpl.java
rename to components/net/sf/briar/crypto/KeyRotatorImpl.java
index 26db0228b3b5392ba856b6b99135a16e550344f9..0d653561fc9fa4d95a2abbf92c2368143168907a 100644
--- a/components/net/sf/briar/db/KeyRotatorImpl.java
+++ b/components/net/sf/briar/crypto/KeyRotatorImpl.java
@@ -1,4 +1,4 @@
-package net.sf.briar.db;
+package net.sf.briar.crypto;
 
 import java.util.Timer;
 import java.util.TimerTask;
diff --git a/components/net/sf/briar/db/Database.java b/components/net/sf/briar/db/Database.java
index cc6be9b2fbd03aa4d78f4ae4b556251a3f44278f..75a6bd15eb291db251bf4182ce945075c2abde0a 100644
--- a/components/net/sf/briar/db/Database.java
+++ b/components/net/sf/briar/db/Database.java
@@ -5,7 +5,9 @@ import java.util.Collection;
 import java.util.Map;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.ContactTransport;
 import net.sf.briar.api.Rating;
+import net.sf.briar.api.TemporarySecret;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DbException;
@@ -19,9 +21,6 @@ import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionWindow;
 
 /**
  * A low-level interface to the database (DatabaseComponent provides a
@@ -81,18 +80,19 @@ interface Database<T> {
 	void addBatchToAck(T txn, ContactId c, BatchId b) throws DbException;
 
 	/**
-	 * Adds a new contact to the database with the given secrets and returns an
-	 * ID for the contact.
+	 * Adds a new contact to the database and returns an ID for the contact.
 	 * <p>
-	 * Any secrets generated by the method are stored in the given collection
-	 * and should be erased by the caller once the transaction has been
-	 * committed or aborted.
+	 * Locking: contact write, subscription write, transport write.
+	 */
+	ContactId addContact(T txn) throws DbException;
+
+	/**
+	 * Adds a contact transport to the database.
 	 * <p>
-	 * Locking: contact write, subscription write, transport write,
-	 * window write.
+	 * Locking: contact read, window write.
 	 */
-	ContactId addContact(T txn, byte[] inSecret, byte[] outSecret,
-			Collection<byte[]> erase) throws DbException;
+	void addContactTransport(T txn, ContactTransport ct)
+			throws DbException;
 
 	/**
 	 * Returns false if the given message is already in the database. Otherwise
@@ -118,6 +118,15 @@ interface Database<T> {
 	 */
 	boolean addPrivateMessage(T txn, Message m, ContactId c) throws DbException;
 
+	/**
+	 * Stores the given temporary secrets and deletes any secrets that have
+	 * been made obsolete.
+	 * <p>
+	 * Locking: contact read, window write.
+	 */
+	void addSecrets(T txn, Collection<TemporarySecret> secrets)
+			throws DbException;
+
 	/**
 	 * Subscribes to the given group.
 	 * <p>
@@ -132,15 +141,7 @@ interface Database<T> {
 	 * Locking: contact read, subscription write.
 	 */
 	void addSubscription(T txn, ContactId c, Group g, long start)
-	throws DbException;
-
-	/**
-	 * Allocates and returns a local index for the given transport. Returns
-	 * null if all indices have been allocated.
-	 * <p>
-	 * Locking: transport write.
-	 */
-	TransportIndex addTransport(T txn, TransportId t) throws DbException;
+			throws DbException;
 
 	/**
 	 * Makes the given group visible to the given contact.
@@ -156,6 +157,14 @@ interface Database<T> {
 	 */
 	boolean containsContact(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Returns true if the database contains the given contact transport.
+	 * <p>
+	 * Locking: contact read, window read.
+	 */
+	boolean containsContactTransport(T txn, ContactId c, TransportId t)
+			throws DbException;
+
 	/**
 	 * Returns true if the database contains the given message.
 	 * <p>
@@ -177,7 +186,7 @@ interface Database<T> {
 	 * Locking: subscription read.
 	 */
 	boolean containsSubscription(T txn, GroupId g, long time)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns true if the user is subscribed to the given group, the group is
@@ -196,7 +205,7 @@ interface Database<T> {
 	 * Locking: contact read, messageStatus read.
 	 */
 	Collection<BatchId> getBatchesToAck(T txn, ContactId c, int maxBatches)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns the configuration for the given transport.
@@ -206,33 +215,18 @@ interface Database<T> {
 	TransportConfig getConfig(T txn, TransportId t) throws DbException;
 
 	/**
-	 * Returns an outgoing connection context for the given contact and
-	 * transport.
-	 * <p>
-	 * Any secrets generated by the method are stored in the given collection
-	 * and should be erased by the caller once the transaction has been
-	 * committed or aborted.
+	 * Returns the IDs of all contacts.
 	 * <p>
-	 * Locking: contact read, window write.
+	 * Locking: contact read.
 	 */
-	ConnectionContext getConnectionContext(T txn, ContactId c, TransportIndex i,
-			Collection<byte[]> erase) throws DbException;
+	Collection<ContactId> getContacts(T txn) throws DbException;
 
 	/**
-	 * Returns the connection reordering window for the given contact and
-	 * transport.
+	 * Returns all contact transports.
 	 * <p>
 	 * Locking: contact read, window read.
 	 */
-	ConnectionWindow getConnectionWindow(T txn, ContactId c, TransportIndex i)
-	throws DbException;
-
-	/**
-	 * Returns the IDs of all contacts.
-	 * <p>
-	 * Locking: contact read.
-	 */
-	Collection<ContactId> getContacts(T txn) throws DbException;
+	Collection<ContactTransport> getContactTransports(T txn) throws DbException;
 
 	/**
 	 * Returns the approximate expiry time of the database.
@@ -259,21 +253,13 @@ interface Database<T> {
 	 */
 	MessageId getGroupMessageParent(T txn, MessageId m) throws DbException;
 
-	/**
-	 * Returns the local index for the given transport, or null if no index
-	 * has been allocated.
-	 * <p>
-	 * Locking: transport read.
-	 */
-	TransportIndex getLocalIndex(T txn, TransportId t) throws DbException;
-
 	/**
 	 * Returns the local transport properties for the given transport.
 	 * <p>
 	 * Locking: transport read.
 	 */
 	TransportProperties getLocalProperties(T txn, TransportId t)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns all local transports.
@@ -310,7 +296,7 @@ interface Database<T> {
 	 * Locking: message read, messageFlag read.
 	 */
 	Collection<MessageHeader> getMessageHeaders(T txn, GroupId g)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns the message identified by the given ID, in raw format, or null
@@ -321,7 +307,7 @@ interface Database<T> {
 	 * subscription read.
 	 */
 	byte[] getMessageIfSendable(T txn, ContactId c, MessageId m)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns the IDs of all messages signed by the given author.
@@ -329,7 +315,7 @@ interface Database<T> {
 	 * Locking: message read.
 	 */
 	Collection<MessageId> getMessagesByAuthor(T txn, AuthorId a)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns the number of children of the message identified by the given
@@ -372,15 +358,6 @@ interface Database<T> {
 	 */
 	boolean getRead(T txn, MessageId m) throws DbException;
 
-	/**
-	 * Returns the given contact's index for the given transport, or null if
-	 * the contact does not support the transport.
-	 * <p>
-	 * Locking: contact read, window read.
-	 */
-	TransportIndex getRemoteIndex(T txn, ContactId c, TransportId t)
-	throws DbException;
-
 	/**
 	 * Returns all remote properties for the given transport.
 	 * <p>
@@ -404,7 +381,7 @@ interface Database<T> {
 	 * subscription read.
 	 */
 	Collection<MessageId> getSendableMessages(T txn, ContactId c, int capacity)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns true if the given message has been starred.
@@ -464,7 +441,7 @@ interface Database<T> {
 	 * Locking: contact read, subscription read.
 	 */
 	Map<GroupId, GroupId> getVisibleHoles(T txn, ContactId c, long timestamp)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns any subscriptions that are visible to the given contact,
@@ -474,7 +451,7 @@ interface Database<T> {
 	 * Locking: contact read, subscription read.
 	 */
 	Map<Group, Long> getVisibleSubscriptions(T txn, ContactId c, long timestamp)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Returns true if any messages are sendable to the given contact.
@@ -483,6 +460,15 @@ interface Database<T> {
 	 */
 	boolean hasSendableMessages(T txn, ContactId c) throws DbException;
 
+	/**
+	 * Increments the outgoing connection counter for the given contact
+	 * transport in the given rotation period.
+	 * <p>
+	 * Locking: contact read, window write.
+	 */
+	void incrementConnectionCounter(T txn, ContactId c, TransportId t,
+			long period) throws DbException;
+
 	/**
 	 * Removes an outstanding batch that has been acknowledged. Any messages in
 	 * the batch that are still considered outstanding (Status.SENT) with
@@ -499,7 +485,7 @@ interface Database<T> {
 	 * Locking: contact read, messageStatus write.
 	 */
 	void removeBatchesToAck(T txn, ContactId c, Collection<BatchId> sent)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Removes a contact (and all associated state) from the database.
@@ -543,7 +529,7 @@ interface Database<T> {
 	 * with IDs greater than the first are removed.
 	 */
 	void removeSubscriptions(T txn, ContactId c, GroupId start, GroupId end)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Makes the given group invisible to the given contact.
@@ -559,16 +545,16 @@ interface Database<T> {
 	 * Locking: transport write.
 	 */
 	void setConfig(T txn, TransportId t, TransportConfig config)
-	throws DbException;
+			throws DbException;
 
 	/**
-	 * Sets the connection reordering window for the given contact and
-	 * transport.
+	 * Sets the connection reordering window for the given contact transport in
+	 * the given rotation period.
 	 * <p>
 	 * Locking: contact read, window write.
 	 */
-	void setConnectionWindow(T txn, ContactId c, TransportIndex i,
-			ConnectionWindow w) throws DbException;
+	void setConnectionWindow(T txn, ContactId c, TransportId t, long period,
+			long centre, byte[] bitmap) throws DbException;
 
 	/**
 	 * Sets the given contact's database expiry time.
@@ -584,7 +570,7 @@ interface Database<T> {
 	 * Locking: transport write.
 	 */
 	void setLocalProperties(T txn, TransportId t, TransportProperties p)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Sets the user's rating for the given author.
@@ -622,7 +608,7 @@ interface Database<T> {
 	 * Locking: contact read, message read, messageStatus write.
 	 */
 	void setStatus(T txn, ContactId c, MessageId m, Status s)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * If the database contains the given message and it belongs to a group
@@ -634,7 +620,7 @@ interface Database<T> {
 	 * subscription read.
 	 */
 	boolean setStatusSeenIfVisible(T txn, ContactId c, MessageId m)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Records the time of the latest subscription update acknowledged by the
@@ -643,7 +629,7 @@ interface Database<T> {
 	 * Locking: contact read, subscription write.
 	 */
 	void setSubscriptionsAcked(T txn, ContactId c, long timestamp)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Records the time of the latest subscription update received from the
@@ -652,7 +638,7 @@ interface Database<T> {
 	 * Locking: contact read, subscription write.
 	 */
 	void setSubscriptionsReceived(T txn, ContactId c, long timestamp)
-	throws DbException;
+			throws DbException;
 
 	/**
 	 * Sets the transports for the given contact, replacing any existing
@@ -677,5 +663,5 @@ interface Database<T> {
 	 * Locking: contact read, transport write.
 	 */
 	void setTransportsSent(T txn, ContactId c, long timestamp)
-	throws DbException;
+			throws DbException;
 }
diff --git a/components/net/sf/briar/db/DatabaseComponentImpl.java b/components/net/sf/briar/db/DatabaseComponentImpl.java
index ebe17a3097950d204503744ee12830df68567ebc..5828b36a7daf4d0382f9bf31518117f0ebb8441b 100644
--- a/components/net/sf/briar/db/DatabaseComponentImpl.java
+++ b/components/net/sf/briar/db/DatabaseComponentImpl.java
@@ -22,7 +22,9 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.ContactTransport;
 import net.sf.briar.api.Rating;
+import net.sf.briar.api.TemporarySecret;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.clock.Clock;
@@ -30,6 +32,7 @@ import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.db.MessageHeader;
 import net.sf.briar.api.db.NoSuchContactException;
+import net.sf.briar.api.db.NoSuchContactTransportException;
 import net.sf.briar.api.db.Status;
 import net.sf.briar.api.db.event.BatchReceivedEvent;
 import net.sf.briar.api.db.event.ContactAddedEvent;
@@ -41,7 +44,6 @@ import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
-import net.sf.briar.api.db.event.TransportAddedEvent;
 import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
@@ -58,10 +60,7 @@ import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionWindow;
 import net.sf.briar.util.ByteUtils;
 
 import com.google.inject.Inject;
@@ -76,7 +75,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent,
 DatabaseCleaner.Callback {
 
 	private static final Logger LOG =
-		Logger.getLogger(DatabaseComponentImpl.class.getName());
+			Logger.getLogger(DatabaseComponentImpl.class.getName());
 
 	/*
 	 * Locks must always be acquired in alphabetical order. See the Database
@@ -84,21 +83,21 @@ DatabaseCleaner.Callback {
 	 */
 
 	private final ReentrantReadWriteLock contactLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock messageLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock messageFlagLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock messageStatusLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock ratingLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock subscriptionLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock transportLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 	private final ReentrantReadWriteLock windowLock =
-		new ReentrantReadWriteLock(true);
+			new ReentrantReadWriteLock(true);
 
 	private final Database<T> db;
 	private final DatabaseCleaner cleaner;
@@ -107,7 +106,7 @@ DatabaseCleaner.Callback {
 	private final Clock clock;
 
 	private final Collection<DatabaseListener> listeners =
-		new CopyOnWriteArrayList<DatabaseListener>();
+			new CopyOnWriteArrayList<DatabaseListener>();
 
 	private final Object spaceLock = new Object();
 	private long bytesStoredSinceLastCheck = 0L; // Locking: spaceLock
@@ -172,8 +171,7 @@ DatabaseCleaner.Callback {
 		listeners.remove(d);
 	}
 
-	public ContactId addContact(byte[] inSecret, byte[] outSecret)
-	throws DbException {
+	public ContactId addContact() throws DbException {
 		ContactId c;
 		Collection<byte[]> erase = new ArrayList<byte[]>();
 		contactLock.writeLock().lock();
@@ -186,7 +184,7 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
-							c = db.addContact(txn, inSecret, outSecret, erase);
+							c = db.addContact(txn);
 							db.commitTransaction(txn);
 						} catch(DbException e) {
 							db.abortTransaction(txn);
@@ -266,7 +264,7 @@ DatabaseCleaner.Callback {
 	 * @param sender may be null for a locally generated message.
 	 */
 	private boolean storeGroupMessage(T txn, Message m, ContactId sender)
-	throws DbException {
+			throws DbException {
 		if(m.getGroup() == null) throw new IllegalArgumentException();
 		boolean stored = db.addGroupMessage(txn, m);
 		// Mark the message as seen by the sender
@@ -315,7 +313,7 @@ DatabaseCleaner.Callback {
 	 * greater than 0, or false if it has changed from greater than 0 to 0.
 	 */
 	private int updateAncestorSendability(T txn, MessageId m, boolean increment)
-	throws DbException {
+			throws DbException {
 		int affected = 0;
 		boolean changed = true;
 		while(changed) {
@@ -343,17 +341,18 @@ DatabaseCleaner.Callback {
 	}
 
 	public void addLocalPrivateMessage(Message m, ContactId c)
-	throws DbException {
+			throws DbException {
 		boolean added = false;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.writeLock().lock();
 			try {
 				messageStatusLock.writeLock().lock();
 				try {
 					T txn = db.startTransaction();
 					try {
+						if(!db.containsContact(txn, c))
+							throw new NoSuchContactException();
 						added = storePrivateMessage(txn, m, c, false);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
@@ -373,6 +372,36 @@ DatabaseCleaner.Callback {
 		if(added) callListeners(new MessagesAddedEvent());
 	}
 
+	public void addSecrets(Collection<TemporarySecret> secrets)
+			throws DbException {
+		contactLock.readLock().lock();
+		try {
+			windowLock.writeLock().lock();
+			try {
+				T txn = db.startTransaction();
+				try {
+					Collection<TemporarySecret> relevant =
+							new ArrayList<TemporarySecret>();
+					for(TemporarySecret s : secrets) {
+						ContactId c = s.getContactId();
+						TransportId t = s.getTransportId();
+						if(db.containsContactTransport(txn, c, t))
+							relevant.add(s);
+					}
+					if(!secrets.isEmpty()) db.addSecrets(txn, relevant);
+					db.commitTransaction(txn);
+				} catch(DbException e) {
+					db.abortTransaction(txn);
+					throw e;
+				}
+			} finally {
+				windowLock.writeLock().unlock();
+			}
+		} finally {
+			contactLock.writeLock().unlock();
+		}
+	}
+
 	/**
 	 * If the given message is already in the database, returns false.
 	 * Otherwise stores the message and marks it as new or seen with respect to
@@ -396,52 +425,16 @@ DatabaseCleaner.Callback {
 		return true;
 	}
 
-	/**
-	 * Returns true if the database contains the given contact.
-	 * <p>
-	 * Locking: contact read.
-	 */
-	private boolean containsContact(ContactId c) throws DbException {
-		T txn = db.startTransaction();
-		try {
-			boolean contains = db.containsContact(txn, c);
-			db.commitTransaction(txn);
-			return contains;
-		} catch(DbException e) {
-			db.abortTransaction(txn);
-			throw e;
-		}
-	}
-
-	public TransportIndex addTransport(TransportId t) throws DbException {
-		TransportIndex i;
-		transportLock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				i = db.addTransport(txn, t);
-				db.commitTransaction(txn);
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			transportLock.writeLock().unlock();
-		}
-		// Call the listeners outside the lock
-		if(i != null) callListeners(new TransportAddedEvent(t));
-		return i;
-	}
-
 	public Ack generateAck(ContactId c, int maxBatches) throws DbException {
 		Collection<BatchId> acked;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageStatusLock.readLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
+					if(!db.containsContact(txn, c))
+						throw new NoSuchContactException();
 					acked = db.getBatchesToAck(txn, c, maxBatches);
 					db.commitTransaction(txn);
 				} catch(DbException e) {
@@ -473,14 +466,13 @@ DatabaseCleaner.Callback {
 	}
 
 	public RawBatch generateBatch(ContactId c, int capacity)
-	throws DbException {
+			throws DbException {
 		Collection<MessageId> ids;
 		List<byte[]> messages = new ArrayList<byte[]>();
 		RawBatch b;
 		// Get some sendable messages from the database
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.readLock().lock();
@@ -489,6 +481,8 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
+							if(!db.containsContact(txn, c))
+								throw new NoSuchContactException();
 							ids = db.getSendableMessages(txn, c, capacity);
 							for(MessageId m : ids) {
 								messages.add(db.getMessage(txn, m));
@@ -537,7 +531,6 @@ DatabaseCleaner.Callback {
 		// Get some sendable messages from the database
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.readLock().lock();
@@ -546,6 +539,8 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
+							if(!db.containsContact(txn, c))
+								throw new NoSuchContactException();
 							Iterator<MessageId> it = requested.iterator();
 							while(it.hasNext()) {
 								MessageId m = it.next();
@@ -595,17 +590,18 @@ DatabaseCleaner.Callback {
 	}
 
 	public Offer generateOffer(ContactId c, int maxMessages)
-	throws DbException {
+			throws DbException {
 		Collection<MessageId> offered;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.readLock().lock();
 				try {
 					T txn = db.startTransaction();
 					try {
+						if(!db.containsContact(txn, c))
+							throw new NoSuchContactException();
 						offered = db.getOfferableMessages(txn, c, maxMessages);
 						db.commitTransaction(txn);
 					} catch(DbException e) {
@@ -625,17 +621,18 @@ DatabaseCleaner.Callback {
 	}
 
 	public SubscriptionUpdate generateSubscriptionUpdate(ContactId c)
-	throws DbException {
+			throws DbException {
 		Map<GroupId, GroupId> holes;
 		Map<Group, Long> subs;
 		long expiry, timestamp;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			subscriptionLock.readLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
+					if(!db.containsContact(txn, c))
+						throw new NoSuchContactException();
 					timestamp = clock.currentTimeMillis() - 1;
 					holes = db.getVisibleHoles(txn, c, timestamp);
 					subs = db.getVisibleSubscriptions(txn, c, timestamp);
@@ -661,17 +658,18 @@ DatabaseCleaner.Callback {
 	}
 
 	public TransportUpdate generateTransportUpdate(ContactId c)
-	throws DbException {
+			throws DbException {
 		boolean due;
 		Collection<Transport> transports;
 		long timestamp;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			transportLock.readLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
+					if(!db.containsContact(txn, c))
+						throw new NoSuchContactException();
 					// Work out whether an update is due
 					long modified = db.getTransportsModified(txn);
 					long sent = db.getTransportsSent(txn, c);
@@ -723,46 +721,35 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public ConnectionContext getConnectionContext(ContactId c, TransportIndex i)
-	throws DbException {
-		Collection<byte[]> erase = new ArrayList<byte[]>();
+	public Collection<ContactId> getContacts() throws DbException {
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
-			windowLock.writeLock().lock();
+			T txn = db.startTransaction();
 			try {
-				T txn = db.startTransaction();
-				try {
-					ConnectionContext ctx =
-						db.getConnectionContext(txn, c, i, erase);
-					db.commitTransaction(txn);
-					return ctx;
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				windowLock.writeLock().unlock();
+				Collection<ContactId> contacts = db.getContacts(txn);
+				db.commitTransaction(txn);
+				return contacts;
+			} catch(DbException e) {
+				db.abortTransaction(txn);
+				throw e;
 			}
 		} finally {
 			contactLock.readLock().unlock();
-			// Erase the secrets after committing or aborting the transaction
-			for(byte[] b : erase) ByteUtils.erase(b);
 		}
 	}
 
-	public ConnectionWindow getConnectionWindow(ContactId c, TransportIndex i)
-	throws DbException {
+	public Collection<ContactTransport> getContactTransports()
+			throws DbException {
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			windowLock.readLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
-					ConnectionWindow w = db.getConnectionWindow(txn, c, i);
+					Collection<ContactTransport> contactTransports =
+							db.getContactTransports(txn);
 					db.commitTransaction(txn);
-					return w;
+					return contactTransports;
 				} catch(DbException e) {
 					db.abortTransaction(txn);
 					throw e;
@@ -775,49 +762,15 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Collection<ContactId> getContacts() throws DbException {
-		contactLock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				Collection<ContactId> contacts = db.getContacts(txn);
-				db.commitTransaction(txn);
-				return contacts;
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
-	public TransportIndex getLocalIndex(TransportId t) throws DbException {
-		transportLock.readLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				TransportIndex i = db.getLocalIndex(txn, t);
-				db.commitTransaction(txn);
-				return i;
-			} catch(DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			transportLock.readLock().unlock();
-		}
-	}
-
 	public TransportProperties getLocalProperties(TransportId t)
-	throws DbException {
+			throws DbException {
 		transportLock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				TransportProperties p = db.getLocalProperties(txn, t);
+				TransportProperties properties = db.getLocalProperties(txn, t);
 				db.commitTransaction(txn);
-				return p;
+				return properties;
 			} catch(DbException e) {
 				db.abortTransaction(txn);
 				throw e;
@@ -845,7 +798,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public Collection<MessageHeader> getMessageHeaders(GroupId g)
-	throws DbException {
+			throws DbException {
 		messageLock.readLock().lock();
 		try {
 			messageFlagLock.readLock().lock();
@@ -853,7 +806,7 @@ DatabaseCleaner.Callback {
 				T txn = db.startTransaction();
 				try {
 					Collection<MessageHeader> headers =
-						db.getMessageHeaders(txn, g);
+							db.getMessageHeaders(txn, g);
 					db.commitTransaction(txn);
 					return headers;
 				} catch(DbException e) {
@@ -885,30 +838,6 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public TransportIndex getRemoteIndex(ContactId c, TransportId t)
-	throws DbException {
-		contactLock.readLock().lock();
-		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
-			transportLock.readLock().lock();
-			try {
-				T txn = db.startTransaction();
-				try {
-					TransportIndex i = db.getRemoteIndex(txn, c, t);
-					db.commitTransaction(txn);
-					return i;
-				} catch(DbException e) {
-					db.abortTransaction(txn);
-					throw e;
-				}
-			} finally {
-				transportLock.readLock().unlock();
-			}
-		} finally {
-			contactLock.readLock().unlock();
-		}
-	}
-
 	public Map<ContactId, TransportProperties> getRemoteProperties(
 			TransportId t) throws DbException {
 		contactLock.readLock().lock();
@@ -918,7 +847,7 @@ DatabaseCleaner.Callback {
 				T txn = db.startTransaction();
 				try {
 					Map<ContactId, TransportProperties> properties =
-						db.getRemoteProperties(txn, t);
+							db.getRemoteProperties(txn, t);
 					db.commitTransaction(txn);
 					return properties;
 				} catch(DbException e) {
@@ -960,7 +889,7 @@ DatabaseCleaner.Callback {
 					T txn = db.startTransaction();
 					try {
 						Map<GroupId, Integer> counts =
-							db.getUnreadMessageCounts(txn);
+								db.getUnreadMessageCounts(txn);
 						db.commitTransaction(txn);
 						return counts;
 					} catch(DbException e) {
@@ -1003,7 +932,6 @@ DatabaseCleaner.Callback {
 	public boolean hasSendableMessages(ContactId c) throws DbException {
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.readLock().lock();
@@ -1012,6 +940,8 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
+							if(!db.containsContact(txn, c))
+								throw new NoSuchContactException();
 							boolean has = db.hasSendableMessages(txn, c);
 							db.commitTransaction(txn);
 							return has;
@@ -1033,17 +963,41 @@ DatabaseCleaner.Callback {
 		}
 	}
 
+	public void incrementConnectionCounter(ContactId c, TransportId t,
+			long period) throws DbException {
+		contactLock.readLock().lock();
+		try {
+			windowLock.writeLock().lock();
+			try {
+				T txn = db.startTransaction();
+				try {
+					if(!db.containsContactTransport(txn, c, t))
+						throw new NoSuchContactTransportException();
+					db.incrementConnectionCounter(txn, c, t, period);
+					db.commitTransaction(txn);
+				} catch(DbException e) {
+					db.abortTransaction(txn);
+				}
+			} finally {
+				windowLock.writeLock().unlock();
+			}
+		} finally {
+			contactLock.readLock().unlock();
+		}
+	}
+
 	public void receiveAck(ContactId c, Ack a) throws DbException {
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.writeLock().lock();
 				try {
-					Collection<BatchId> acks = a.getBatchIds();
 					T txn = db.startTransaction();
 					try {
+						if(!db.containsContact(txn, c))
+							throw new NoSuchContactException();
+						Collection<BatchId> acks = a.getBatchIds();
 						// Mark all messages in acked batches as seen
 						for(BatchId b : acks) db.removeAckedBatch(txn, c, b);
 						// Find any lost batches that need to be retransmitted
@@ -1069,7 +1023,6 @@ DatabaseCleaner.Callback {
 		boolean anyAdded = false;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.writeLock().lock();
 			try {
 				messageStatusLock.writeLock().lock();
@@ -1078,6 +1031,8 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
+							if(!db.containsContact(txn, c))
+								throw new NoSuchContactException();
 							anyAdded = storeMessages(txn, c, b.getMessages());
 							db.addBatchToAck(txn, c, b.getId());
 							db.commitTransaction(txn);
@@ -1131,7 +1086,6 @@ DatabaseCleaner.Callback {
 		BitSet request;
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.writeLock().lock();
@@ -1140,6 +1094,8 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
+							if(!db.containsContact(txn, c))
+								throw new NoSuchContactException();
 							offered = o.getMessageIds();
 							request = new BitSet(offered.size());
 							Iterator<MessageId> it = offered.iterator();
@@ -1171,15 +1127,16 @@ DatabaseCleaner.Callback {
 	}
 
 	public void receiveSubscriptionUpdate(ContactId c, SubscriptionUpdate s)
-	throws DbException {
+			throws DbException {
 		// Update the contact's subscriptions
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			subscriptionLock.writeLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
+					if(!db.containsContact(txn, c))
+						throw new NoSuchContactException();
 					Map<GroupId, GroupId> holes = s.getHoles();
 					for(Entry<GroupId, GroupId> e : holes.entrySet()) {
 						GroupId start = e.getKey(), end = e.getValue();
@@ -1208,16 +1165,17 @@ DatabaseCleaner.Callback {
 	}
 
 	public void receiveTransportUpdate(ContactId c, TransportUpdate t)
-	throws DbException {
+			throws DbException {
 		Collection<Transport> transports;
 		// Update the contact's transport properties
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			transportLock.writeLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
+					if(!db.containsContact(txn, c))
+						throw new NoSuchContactException();
 					transports = t.getTransports();
 					db.setTransports(txn, c, transports, t.getTimestamp());
 					db.commitTransaction(txn);
@@ -1238,7 +1196,6 @@ DatabaseCleaner.Callback {
 	public void removeContact(ContactId c) throws DbException {
 		contactLock.writeLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.writeLock().lock();
 			try {
 				messageFlagLock.writeLock().lock();
@@ -1253,6 +1210,8 @@ DatabaseCleaner.Callback {
 								try {
 									T txn = db.startTransaction();
 									try {
+										if(!db.containsContact(txn, c))
+											throw new NoSuchContactException();
 										db.removeContact(txn, c);
 										db.commitTransaction(txn);
 									} catch(DbException e) {
@@ -1285,7 +1244,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public void setConfig(TransportId t, TransportConfig c)
-	throws DbException {
+			throws DbException {
 		transportLock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
@@ -1301,16 +1260,17 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void setConnectionWindow(ContactId c, TransportIndex i,
-			ConnectionWindow w) throws DbException {
+	public void setConnectionWindow(ContactId c, TransportId t, long period,
+			long centre, byte[] bitmap) throws DbException {
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			windowLock.writeLock().lock();
 			try {
 				T txn = db.startTransaction();
 				try {
-					db.setConnectionWindow(txn, c, i, w);
+					if(!db.containsContactTransport(txn, c, t))
+						throw new NoSuchContactTransportException();
+					db.setConnectionWindow(txn, c, t, period, centre, bitmap);
 					db.commitTransaction(txn);
 				} catch(DbException e) {
 					db.abortTransaction(txn);
@@ -1324,7 +1284,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public void setLocalProperties(TransportId t, TransportProperties p)
-	throws DbException {
+			throws DbException {
 		boolean changed = false;
 		transportLock.writeLock().lock();
 		try {
@@ -1378,10 +1338,9 @@ DatabaseCleaner.Callback {
 	}
 
 	public void setSeen(ContactId c, Collection<MessageId> seen)
-	throws DbException {
+			throws DbException {
 		contactLock.readLock().lock();
 		try {
-			if(!containsContact(c)) throw new NoSuchContactException();
 			messageLock.readLock().lock();
 			try {
 				messageStatusLock.writeLock().lock();
@@ -1390,6 +1349,8 @@ DatabaseCleaner.Callback {
 					try {
 						T txn = db.startTransaction();
 						try {
+							if(!db.containsContact(txn, c))
+								throw new NoSuchContactException();
 							for(MessageId m : seen) {
 								db.setStatusSeenIfVisible(txn, c, m);
 							}
@@ -1421,7 +1382,7 @@ DatabaseCleaner.Callback {
 	 * from not good to good, or false if it has changed from good to not good.
 	 */
 	private void updateAuthorSendability(T txn, AuthorId a, boolean increment)
-	throws DbException {
+			throws DbException {
 		for(MessageId id : db.getMessagesByAuthor(txn, a)) {
 			int sendability = db.getSendability(txn, id);
 			if(increment) {
@@ -1438,7 +1399,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public void setVisibility(GroupId g, Collection<ContactId> visible)
-	throws DbException {
+			throws DbException {
 		List<ContactId> affected = new ArrayList<ContactId>();
 		contactLock.readLock().lock();
 		try {
@@ -1619,4 +1580,8 @@ DatabaseCleaner.Callback {
 		}
 		return false;
 	}
+
+	public void rotateKeys() throws DbException {
+
+	}
 }
diff --git a/components/net/sf/briar/db/DatabaseModule.java b/components/net/sf/briar/db/DatabaseModule.java
index bb51aa89c0c34495084be456c6519c409a91a9a8..7532896e281e0db25e065b8a79ad70da63536840 100644
--- a/components/net/sf/briar/db/DatabaseModule.java
+++ b/components/net/sf/briar/db/DatabaseModule.java
@@ -14,8 +14,6 @@ import net.sf.briar.api.db.DatabasePassword;
 import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.PacketFactory;
-import net.sf.briar.api.transport.ConnectionContextFactory;
-import net.sf.briar.api.transport.ConnectionWindowFactory;
 import net.sf.briar.util.BoundedExecutor;
 
 import com.google.inject.AbstractModule;
@@ -50,11 +48,8 @@ public class DatabaseModule extends AbstractModule {
 	@Provides
 	Database<Connection> getDatabase(@DatabaseDirectory File dir,
 			@DatabasePassword Password password, @DatabaseMaxSize long maxSize,
-			ConnectionContextFactory connectionContextFactory,
-			ConnectionWindowFactory connectionWindowFactory,
 			GroupFactory groupFactory, Clock clock) {
-		return new H2Database(dir, password, maxSize, connectionContextFactory,
-				connectionWindowFactory, groupFactory, clock);
+		return new H2Database(dir, password, maxSize, groupFactory, clock);
 	}
 
 	@Provides @Singleton
diff --git a/components/net/sf/briar/db/H2Database.java b/components/net/sf/briar/db/H2Database.java
index ed1dd1d47e27fd6cba951f61e62206d8e90a468d..fb91b6696513654c370bc671953728e6cb49143e 100644
--- a/components/net/sf/briar/db/H2Database.java
+++ b/components/net/sf/briar/db/H2Database.java
@@ -15,8 +15,6 @@ import net.sf.briar.api.db.DatabaseMaxSize;
 import net.sf.briar.api.db.DatabasePassword;
 import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.protocol.GroupFactory;
-import net.sf.briar.api.transport.ConnectionContextFactory;
-import net.sf.briar.api.transport.ConnectionWindowFactory;
 
 import org.apache.commons.io.FileSystemUtils;
 
@@ -39,11 +37,9 @@ class H2Database extends JdbcDatabase {
 	H2Database(@DatabaseDirectory File dir,
 			@DatabasePassword Password password,
 			@DatabaseMaxSize long maxSize,
-			ConnectionContextFactory connectionContextFactory,
-			ConnectionWindowFactory connectionWindowFactory,
 			GroupFactory groupFactory, Clock clock) {
-		super(connectionContextFactory, connectionWindowFactory, groupFactory,
-				clock, HASH_TYPE, BINARY_TYPE, COUNTER_TYPE, SECRET_TYPE);
+		super(groupFactory, clock, HASH_TYPE, BINARY_TYPE, COUNTER_TYPE,
+				SECRET_TYPE);
 		home = new File(dir, "db");
 		this.password = password;
 		url = "jdbc:h2:split:" + home.getPath()
diff --git a/components/net/sf/briar/db/JdbcDatabase.java b/components/net/sf/briar/db/JdbcDatabase.java
index b3a42ac5e5b3dc78b541e94b95b1139b9df3af31..fde56a67e6053464df5168e61a5bd5ecbc4bc48b 100644
--- a/components/net/sf/briar/db/JdbcDatabase.java
+++ b/components/net/sf/briar/db/JdbcDatabase.java
@@ -1,5 +1,8 @@
 package net.sf.briar.db;
 
+import static net.sf.briar.db.DatabaseConstants.EXPIRY_MODULUS;
+import static net.sf.briar.db.DatabaseConstants.RETRANSMIT_THRESHOLD;
+
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
@@ -21,7 +24,9 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.ContactTransport;
 import net.sf.briar.api.Rating;
+import net.sf.briar.api.TemporarySecret;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.clock.Clock;
@@ -35,14 +40,8 @@ import net.sf.briar.api.protocol.GroupFactory;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
-import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionContextFactory;
-import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.api.transport.ConnectionWindowFactory;
 import net.sf.briar.util.FileUtils;
 
 /**
@@ -52,248 +51,232 @@ import net.sf.briar.util.FileUtils;
 abstract class JdbcDatabase implements Database<Connection> {
 
 	private static final String CREATE_SUBSCRIPTIONS =
-		"CREATE TABLE subscriptions"
-		+ " (groupId HASH NOT NULL,"
-		+ " groupName VARCHAR NOT NULL,"
-		+ " groupKey BINARY," // Null for unrestricted groups
-		+ " start BIGINT NOT NULL,"
-		+ " PRIMARY KEY (groupId))";
+			"CREATE TABLE subscriptions"
+					+ " (groupId HASH NOT NULL,"
+					+ " groupName VARCHAR NOT NULL,"
+					+ " groupKey BINARY," // Null for unrestricted groups
+					+ " start BIGINT NOT NULL,"
+					+ " PRIMARY KEY (groupId))";
 
 	private static final String CREATE_CONTACTS =
-		"CREATE TABLE contacts"
-		+ " (contactId COUNTER,"
-		+ " PRIMARY KEY (contactId))";
+			"CREATE TABLE contacts"
+					+ " (contactId COUNTER,"
+					+ " PRIMARY KEY (contactId))";
 
 	private static final String CREATE_MESSAGES =
-		"CREATE TABLE messages"
-		+ " (messageId HASH NOT NULL,"
-		+ " parentId HASH," // Null for the first message in a thread
-		+ " groupId HASH," // Null for private messages
-		+ " authorId HASH," // Null for private or anonymous messages
-		+ " subject VARCHAR NOT NULL,"
-		+ " timestamp BIGINT NOT NULL,"
-		+ " length INT NOT NULL,"
-		+ " bodyStart INT NOT NULL,"
-		+ " bodyLength INT NOT NULL,"
-		+ " raw BLOB NOT NULL,"
-		+ " sendability INT," // Null for private messages
-		+ " contactId INT," // Null for group messages
-		+ " PRIMARY KEY (messageId),"
-		+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
-		+ " ON DELETE CASCADE,"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE messages"
+					+ " (messageId HASH NOT NULL,"
+					+ " parentId HASH," // Null for the first message in a thread
+					+ " groupId HASH," // Null for private messages
+					+ " authorId HASH," // Null for private or anonymous messages
+					+ " subject VARCHAR NOT NULL,"
+					+ " timestamp BIGINT NOT NULL,"
+					+ " length INT NOT NULL,"
+					+ " bodyStart INT NOT NULL,"
+					+ " bodyLength INT NOT NULL,"
+					+ " raw BLOB NOT NULL,"
+					+ " sendability INT," // Null for private messages
+					+ " contactId INT," // Null for group messages
+					+ " PRIMARY KEY (messageId),"
+					+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
+					+ " ON DELETE CASCADE,"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String INDEX_MESSAGES_BY_PARENT =
-		"CREATE INDEX messagesByParent ON messages (parentId)";
+			"CREATE INDEX messagesByParent ON messages (parentId)";
 
 	private static final String INDEX_MESSAGES_BY_AUTHOR =
-		"CREATE INDEX messagesByAuthor ON messages (authorId)";
+			"CREATE INDEX messagesByAuthor ON messages (authorId)";
 
 	private static final String INDEX_MESSAGES_BY_TIMESTAMP =
-		"CREATE INDEX messagesByTimestamp ON messages (timestamp)";
+			"CREATE INDEX messagesByTimestamp ON messages (timestamp)";
 
 	private static final String INDEX_MESSAGES_BY_SENDABILITY =
-		"CREATE INDEX messagesBySendability ON messages (sendability)";
+			"CREATE INDEX messagesBySendability ON messages (sendability)";
 
 	private static final String CREATE_VISIBILITIES =
-		"CREATE TABLE visibilities"
-		+ " (contactId INT NOT NULL,"
-		+ " groupId HASH," // Null for the head of the linked list
-		+ " nextId HASH," // Null for the tail of the linked list
-		+ " deleted BIGINT NOT NULL,"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE,"
-		+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE visibilities"
+					+ " (contactId INT NOT NULL,"
+					+ " groupId HASH," // Null for the head of the linked list
+					+ " nextId HASH," // Null for the tail of the linked list
+					+ " deleted BIGINT NOT NULL,"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE,"
+					+ " FOREIGN KEY (groupId) REFERENCES subscriptions (groupId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String INDEX_VISIBILITIES_BY_GROUP =
-		"CREATE INDEX visibilitiesByGroup ON visibilities (groupId)";
+			"CREATE INDEX visibilitiesByGroup ON visibilities (groupId)";
 
 	private static final String INDEX_VISIBILITIES_BY_NEXT =
-		"CREATE INDEX visibilitiesByNext on visibilities (nextId)";
+			"CREATE INDEX visibilitiesByNext on visibilities (nextId)";
 
 	private static final String CREATE_BATCHES_TO_ACK =
-		"CREATE TABLE batchesToAck"
-		+ " (batchId HASH NOT NULL,"
-		+ " contactId INT NOT NULL,"
-		+ " PRIMARY KEY (batchId, contactId),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE batchesToAck"
+					+ " (batchId HASH NOT NULL,"
+					+ " contactId INT NOT NULL,"
+					+ " PRIMARY KEY (batchId, contactId),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_CONTACT_SUBSCRIPTIONS =
-		"CREATE TABLE contactSubscriptions"
-		+ " (contactId INT NOT NULL,"
-		+ " groupId HASH NOT NULL,"
-		+ " groupName VARCHAR NOT NULL,"
-		+ " groupKey BINARY," // Null for unrestricted groups
-		+ " start BIGINT NOT NULL,"
-		+ " PRIMARY KEY (contactId, groupId),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE contactSubscriptions"
+					+ " (contactId INT NOT NULL,"
+					+ " groupId HASH NOT NULL,"
+					+ " groupName VARCHAR NOT NULL,"
+					+ " groupKey BINARY," // Null for unrestricted groups
+					+ " start BIGINT NOT NULL,"
+					+ " PRIMARY KEY (contactId, groupId),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_OUTSTANDING_BATCHES =
-		"CREATE TABLE outstandingBatches"
-		+ " (batchId HASH NOT NULL,"
-		+ " contactId INT NOT NULL,"
-		+ " timestamp BIGINT NOT NULL,"
-		+ " passover INT NOT NULL,"
-		+ " PRIMARY KEY (batchId, contactId),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE outstandingBatches"
+					+ " (batchId HASH NOT NULL,"
+					+ " contactId INT NOT NULL,"
+					+ " timestamp BIGINT NOT NULL,"
+					+ " passover INT NOT NULL,"
+					+ " PRIMARY KEY (batchId, contactId),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_OUTSTANDING_MESSAGES =
-		"CREATE TABLE outstandingMessages"
-		+ " (batchId HASH NOT NULL,"
-		+ " contactId INT NOT NULL,"
-		+ " messageId HASH NOT NULL,"
-		+ " PRIMARY KEY (batchId, contactId, messageId),"
-		+ " FOREIGN KEY (batchId, contactId)"
-		+ " REFERENCES outstandingBatches (batchId, contactId)"
-		+ " ON DELETE CASCADE,"
-		+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE outstandingMessages"
+					+ " (batchId HASH NOT NULL,"
+					+ " contactId INT NOT NULL,"
+					+ " messageId HASH NOT NULL,"
+					+ " PRIMARY KEY (batchId, contactId, messageId),"
+					+ " FOREIGN KEY (batchId, contactId)"
+					+ " REFERENCES outstandingBatches (batchId, contactId)"
+					+ " ON DELETE CASCADE,"
+					+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String INDEX_OUTSTANDING_MESSAGES_BY_BATCH =
-		"CREATE INDEX outstandingMessagesByBatch"
-		+ " ON outstandingMessages (batchId)";
+			"CREATE INDEX outstandingMessagesByBatch"
+					+ " ON outstandingMessages (batchId)";
 
 	private static final String CREATE_RATINGS =
-		"CREATE TABLE ratings"
-		+ " (authorId HASH NOT NULL,"
-		+ " rating SMALLINT NOT NULL,"
-		+ " PRIMARY KEY (authorId))";
+			"CREATE TABLE ratings"
+					+ " (authorId HASH NOT NULL,"
+					+ " rating SMALLINT NOT NULL,"
+					+ " PRIMARY KEY (authorId))";
 
 	private static final String CREATE_STATUSES =
-		"CREATE TABLE statuses"
-		+ " (messageId HASH NOT NULL,"
-		+ " contactId INT NOT NULL,"
-		+ " status SMALLINT NOT NULL,"
-		+ " PRIMARY KEY (messageId, contactId),"
-		+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
-		+ " ON DELETE CASCADE,"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE statuses"
+					+ " (messageId HASH NOT NULL,"
+					+ " contactId INT NOT NULL,"
+					+ " status SMALLINT NOT NULL,"
+					+ " PRIMARY KEY (messageId, contactId),"
+					+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
+					+ " ON DELETE CASCADE,"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String INDEX_STATUSES_BY_MESSAGE =
-		"CREATE INDEX statusesByMessage ON statuses (messageId)";
+			"CREATE INDEX statusesByMessage ON statuses (messageId)";
 
 	private static final String INDEX_STATUSES_BY_CONTACT =
-		"CREATE INDEX statusesByContact ON statuses (contactId)";
-
-	private static final String CREATE_TRANSPORTS =
-		"CREATE TABLE transports"
-		+ " (transportId HASH NOT NULL,"
-		+ " index COUNTER,"
-		+ " PRIMARY KEY (transportId))";
+			"CREATE INDEX statusesByContact ON statuses (contactId)";
 
 	private static final String CREATE_TRANSPORT_CONFIGS =
-		"CREATE TABLE transportConfigs"
-		+ " (transportId HASH NOT NULL,"
-		+ " key VARCHAR NOT NULL,"
-		+ " value VARCHAR NOT NULL,"
-		+ " PRIMARY KEY (transportId, key))";
+			"CREATE TABLE transportConfigs"
+					+ " (transportId HASH NOT NULL,"
+					+ " key VARCHAR NOT NULL,"
+					+ " value VARCHAR NOT NULL,"
+					+ " PRIMARY KEY (transportId, key))";
 
 	private static final String CREATE_TRANSPORT_PROPS =
-		"CREATE TABLE transportProperties"
-		+ " (transportId HASH NOT NULL,"
-		+ " key VARCHAR NOT NULL,"
-		+ " value VARCHAR NOT NULL,"
-		+ " PRIMARY KEY (transportId, key))";
-
-	private static final String CREATE_CONTACT_TRANSPORTS =
-		"CREATE TABLE contactTransports"
-		+ " (contactId INT NOT NULL,"
-		+ " transportId HASH NOT NULL,"
-		+ " index INT NOT NULL,"
-		+ " UNIQUE (contactId, index),"
-		+ " PRIMARY KEY (contactId, transportId),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE transportProperties"
+					+ " (transportId HASH NOT NULL,"
+					+ " key VARCHAR NOT NULL,"
+					+ " value VARCHAR NOT NULL,"
+					+ " PRIMARY KEY (transportId, key))";
 
 	private static final String CREATE_CONTACT_TRANSPORT_PROPS =
-		"CREATE TABLE contactTransportProperties"
-		+ " (contactId INT NOT NULL,"
-		+ " transportId HASH NOT NULL,"
-		+ " key VARCHAR NOT NULL,"
-		+ " value VARCHAR NOT NULL,"
-		+ " PRIMARY KEY (contactId, transportId, key),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
-
-	private static final String CREATE_CONNECTION_CONTEXTS =
-		"CREATE TABLE connectionContexts"
-		+ " (contactId INT NOT NULL,"
-		+ " index INT NOT NULL,"
-		+ " connection BIGINT NOT NULL,"
-		+ " secret SECRET NOT NULL,"
-		+ " PRIMARY KEY (contactId, index),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
-
-	private static final String CREATE_CONNECTION_WINDOWS =
-		"CREATE TABLE connectionWindows"
-		+ " (contactId INT NOT NULL,"
-		+ " index INT NOT NULL,"
-		+ " connection BIGINT NOT NULL,"
-		+ " secret SECRET NOT NULL,"
-		+ " PRIMARY KEY (contactId, index, connection),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE contactTransportProperties"
+					+ " (contactId INT NOT NULL,"
+					+ " transportId HASH NOT NULL,"
+					+ " key VARCHAR NOT NULL,"
+					+ " value VARCHAR NOT NULL,"
+					+ " PRIMARY KEY (contactId, transportId, key),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
+
+	private static final String CREATE_CONTACT_TRANSPORTS =
+			"CREATE TABLE contactTransports"
+					+ " (contactId INT NOT NULL,"
+					+ " transportId HASH NOT NULL,"
+					+ " epoch BIGINT NOT NULL,"
+					+ " clockDiff BIGINT NOT NULL,"
+					+ " latency BIGINT NOT NULL,"
+					+ " alice BOOLEAN NOT NULL,"
+					+ " PRIMARY KEY (contactId, transportId),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
+
+	private static final String CREATE_SECRETS =
+			"CREATE TABLE secrets"
+					+ " (contactId INT NOT NULL,"
+					+ " transportId HASH NOT NULL,"
+					+ " period BIGINT NOT NULL,"
+					+ " secret SECRET NOT NULL,"
+					+ " outgoing BIGINT NOT NULL,"
+					+ " centre BIGINT NOT NULL,"
+					+ " bitmap BINARY NOT NULL,"
+					+ " PRIMARY KEY (contactId, transportId, period),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_SUBSCRIPTION_TIMES =
-		"CREATE TABLE subscriptionTimes"
-		+ " (contactId INT NOT NULL,"
-		+ " received BIGINT NOT NULL,"
-		+ " acked BIGINT NOT NULL,"
-		+ " expiry BIGINT NOT NULL,"
-		+ " PRIMARY KEY (contactId),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE subscriptionTimes"
+					+ " (contactId INT NOT NULL,"
+					+ " received BIGINT NOT NULL,"
+					+ " acked BIGINT NOT NULL,"
+					+ " expiry BIGINT NOT NULL,"
+					+ " PRIMARY KEY (contactId),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_TRANSPORT_TIMESTAMPS =
-		"CREATE TABLE transportTimestamps"
-		+ " (contactId INT NOT NULL,"
-		+ " sent BIGINT NOT NULL,"
-		+ " received BIGINT NOT NULL,"
-		+ " modified BIGINT NOT NULL,"
-		+ " PRIMARY KEY (contactId),"
-		+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE transportTimestamps"
+					+ " (contactId INT NOT NULL,"
+					+ " sent BIGINT NOT NULL,"
+					+ " received BIGINT NOT NULL,"
+					+ " modified BIGINT NOT NULL,"
+					+ " PRIMARY KEY (contactId),"
+					+ " FOREIGN KEY (contactId) REFERENCES contacts (contactId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final String CREATE_FLAGS =
-		"CREATE TABLE flags"
-		+ " (messageId HASH NOT NULL,"
-		+ " read BOOLEAN NOT NULL,"
-		+ " starred BOOLEAN NOT NULL,"
-		+ " PRIMARY KEY (messageId),"
-		+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
-		+ " ON DELETE CASCADE)";
+			"CREATE TABLE flags"
+					+ " (messageId HASH NOT NULL,"
+					+ " read BOOLEAN NOT NULL,"
+					+ " starred BOOLEAN NOT NULL,"
+					+ " PRIMARY KEY (messageId),"
+					+ " FOREIGN KEY (messageId) REFERENCES messages (messageId)"
+					+ " ON DELETE CASCADE)";
 
 	private static final Logger LOG =
-		Logger.getLogger(JdbcDatabase.class.getName());
+			Logger.getLogger(JdbcDatabase.class.getName());
 
-	private final ConnectionContextFactory connectionContextFactory;
-	private final ConnectionWindowFactory connectionWindowFactory;
+	// FIXME: Can this factory be done away with?
 	private final GroupFactory groupFactory;
 	private final Clock clock;
 	// Different database libraries use different names for certain types
 	private final String hashType, binaryType, counterType, secretType;
 
 	private final LinkedList<Connection> connections =
-		new LinkedList<Connection>(); // Locking: self
+			new LinkedList<Connection>(); // Locking: self
 
 	private int openConnections = 0; // Locking: connections
 	private boolean closed = false; // Locking: connections
 
 	protected abstract Connection createConnection() throws SQLException;
 
-	JdbcDatabase(ConnectionContextFactory connectionContextFactory,
-			ConnectionWindowFactory connectionWindowFactory,
-			GroupFactory groupFactory, Clock clock, String hashType,
+	JdbcDatabase(GroupFactory groupFactory, Clock clock, String hashType,
 			String binaryType, String counterType, String secretType) {
-		this.connectionContextFactory = connectionContextFactory;
-		this.connectionWindowFactory = connectionWindowFactory;
 		this.groupFactory = groupFactory;
 		this.clock = clock;
 		this.hashType = hashType;
@@ -303,7 +286,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	protected void open(boolean resume, File dir, String driverClass)
-	throws DbException, IOException {
+			throws DbException, IOException {
 		if(resume) {
 			if(!dir.exists()) throw new FileNotFoundException();
 			if(!dir.isDirectory()) throw new FileNotFoundException();
@@ -351,13 +334,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_STATUSES));
 			s.executeUpdate(INDEX_STATUSES_BY_MESSAGE);
 			s.executeUpdate(INDEX_STATUSES_BY_CONTACT);
-			s.executeUpdate(insertTypeNames(CREATE_TRANSPORTS));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_CONFIGS));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_PROPS));
-			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORTS));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORT_PROPS));
-			s.executeUpdate(insertTypeNames(CREATE_CONNECTION_CONTEXTS));
-			s.executeUpdate(insertTypeNames(CREATE_CONNECTION_WINDOWS));
+			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORTS));
+			s.executeUpdate(insertTypeNames(CREATE_SECRETS));
 			s.executeUpdate(insertTypeNames(CREATE_SUBSCRIPTION_TIMES));
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_TIMESTAMPS));
 			s.executeUpdate(insertTypeNames(CREATE_FLAGS));
@@ -475,12 +456,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void addBatchToAck(Connection txn, ContactId c, BatchId b)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT NULL FROM batchesToAck"
-				+ " WHERE batchId = ? AND contactId = ?";
+					+ " WHERE batchId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, b.getBytes());
 			ps.setInt(2, c.getInt());
@@ -491,7 +472,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			if(found) return;
 			sql = "INSERT INTO batchesToAck (batchId, contactId)"
-				+ " VALUES (?, ?)";
+					+ " VALUES (?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, b.getBytes());
 			ps.setInt(2, c.getInt());
@@ -505,8 +486,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public ContactId addContact(Connection txn, byte[] inSecret,
-			byte[] outSecret, Collection<byte[]> erase) throws DbException {
+	public ContactId addContact(Connection txn) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -518,7 +498,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Get the new (highest) contact ID
 			sql = "SELECT contactId FROM contacts"
-				+ " ORDER BY contactId DESC LIMIT ?";
+					+ " ORDER BY contactId DESC LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, 1);
 			rs = ps.executeQuery();
@@ -529,7 +509,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Create the head-of-list pointer for the visibility list
 			sql = "INSERT INTO visibilities (contactId, deleted)"
-				+ " VALUES (?, ZERO())";
+					+ " VALUES (?, ZERO())";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			affected = ps.executeUpdate();
@@ -537,8 +517,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Initialise the subscription timestamps
 			sql = "INSERT INTO subscriptionTimes"
-				+ " (contactId, received, acked, expiry)"
-				+ " VALUES (?, ZERO(), ZERO(), ZERO())";
+					+ " (contactId, received, acked, expiry)"
+					+ " VALUES (?, ZERO(), ZERO(), ZERO())";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			affected = ps.executeUpdate();
@@ -546,81 +526,70 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Initialise the transport timestamps
 			sql = "INSERT INTO transportTimestamps"
-				+ " (contactId, sent, received, modified)"
-				+ " VALUES (?, ZERO(), ZERO(), ZERO())";
+					+ " (contactId, sent, received, modified)"
+					+ " VALUES (?, ZERO(), ZERO(), ZERO())";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			affected = ps.executeUpdate();
 			if(affected != 1) throw new DbStateException();
 			ps.close();
-			// Initialise the outgoing connection contexts for all transports
-			sql = "INSERT INTO connectionContexts"
-				+ " (contactId, index, connection, secret)"
-				+ " VALUES (?, ?, ZERO(), ?)";
+			return c;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
+	public void addContactTransport(Connection txn, ContactTransport ct)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "INSERT INTO contactTransports"
+					+ " (contactId, transportId, epoch, clockDiff, latency,"
+					+ " alice)"
+					+ " VALUES (?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			for(int i = 0; i < ProtocolConstants.MAX_TRANSPORTS; i++) {
-				ps.setInt(2, i);
-				ConnectionContext ctx =
-					connectionContextFactory.createNextConnectionContext(c,
-							new TransportIndex(i), 0L, outSecret);
-				byte[] secret = ctx.getSecret();
-				erase.add(secret);
-				ps.setBytes(3, secret);
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != ProtocolConstants.MAX_TRANSPORTS)
-				throw new DbStateException();
-			for(int i = 0; i < batchAffected.length; i++) {
-				if(batchAffected[i] != 1) throw new DbStateException();
-			}
+			ps.setInt(1, ct.getContactId().getInt());
+			ps.setBytes(2, ct.getTransportId().getBytes());
+			ps.setLong(3, ct.getEpoch());
+			ps.setLong(4, ct.getClockDifference());
+			ps.setLong(5, ct.getLatency());
+			ps.setBoolean(6, ct.getAlice());
+			int affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
 			ps.close();
-			// Initialise the incoming connection windows for all transports
-			sql = "INSERT INTO connectionWindows"
-				+ " (contactId, index, connection, secret)"
-				+ " VALUES (?, ?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			int batchSize = 0;
-			for(int i = 0; i < ProtocolConstants.MAX_TRANSPORTS; i++) {
-				ps.setInt(2, i);
-				ConnectionWindow w =
-					connectionWindowFactory.createConnectionWindow(
-							new TransportIndex(i), inSecret);
-				for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
-					ps.setLong(3, e.getKey());
-					byte[] secret = e.getValue();
-					erase.add(secret);
-					ps.setBytes(4, secret);
-					ps.addBatch();
-					batchSize++;
-				}
-			}
-			batchAffected = ps.executeBatch();
-			if(batchAffected.length != batchSize) throw new DbStateException();
-			for(int i = 0; i < batchAffected.length; i++) {
-				if(batchAffected[i] != 1) throw new DbStateException();
-			}
+			sql = "INSERT INTO secrets"
+					+ " (contactId, transportId, period, secret, outgoing,"
+					+ " centre, bitmap)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, ct.getContactId().getInt());
+			ps.setBytes(2, ct.getTransportId().getBytes());
+			ps.setLong(3, ct.getPeriod());
+			ps.setBytes(4, ct.getSecret());
+			ps.setLong(5, ct.getOutgoingConnectionCounter());
+			ps.setLong(6, ct.getWindowCentre());
+			ps.setBytes(7, ct.getWindowBitmap());
+			affected = ps.executeUpdate();
+			if(affected != 1) throw new DbStateException();
 			ps.close();
-			return c;
 		} catch(SQLException e) {
-			tryToClose(rs);
 			tryToClose(ps);
 			throw new DbException(e);
 		}
 	}
 
 	public boolean addGroupMessage(Connection txn, Message m)
-	throws DbException {
+			throws DbException {
 		assert m.getGroup() != null;
 		if(containsMessage(txn, m.getId())) return false;
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO messages (messageId, parentId, groupId,"
-				+ " authorId, subject, timestamp, length, bodyStart,"
-				+ " bodyLength, raw, sendability)"
-				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ZERO())";
+					+ " authorId, subject, timestamp, length, bodyStart,"
+					+ " bodyLength, raw, sendability)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ZERO())";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getId().getBytes());
 			if(m.getParent() == null) ps.setNull(2, Types.BINARY);
@@ -652,8 +621,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Create an outstanding batch row
 			String sql = "INSERT INTO outstandingBatches"
-				+ " (batchId, contactId, timestamp, passover)"
-				+ " VALUES (?, ?, ?, ZERO())";
+					+ " (batchId, contactId, timestamp, passover)"
+					+ " VALUES (?, ?, ?, ZERO())";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, b.getBytes());
 			ps.setInt(2, c.getInt());
@@ -663,8 +632,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Create an outstanding message row for each message in the batch
 			sql = "INSERT INTO outstandingMessages"
-				+ " (batchId, contactId, messageId)"
-				+ " VALUES (?, ?, ?)";
+					+ " (batchId, contactId, messageId)"
+					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, b.getBytes());
 			ps.setInt(2, c.getInt());
@@ -681,7 +650,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Set the status of each message in the batch to SENT
 			sql = "UPDATE statuses SET status = ?"
-				+ " WHERE messageId = ? AND contactId = ? AND status = ?";
+					+ " WHERE messageId = ? AND contactId = ? AND status = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setShort(1, (short) Status.SENT.ordinal());
 			ps.setInt(3, c.getInt());
@@ -705,14 +674,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean addPrivateMessage(Connection txn, Message m, ContactId c)
-	throws DbException {
+			throws DbException {
 		assert m.getGroup() == null;
 		if(containsMessage(txn, m.getId())) return false;
 		PreparedStatement ps = null;
 		try {
-			String sql = "INSERT INTO messages (messageId, parentId, subject,"
-				+ " timestamp, length, bodyStart, bodyLength, raw, contactId)"
-				+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
+			String sql = "INSERT INTO messages"
+					+ " (messageId, parentId, subject, timestamp, length,"
+					+ " bodyStart, bodyLength, raw, contactId)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getId().getBytes());
 			if(m.getParent() == null) ps.setNull(2, Types.BINARY);
@@ -739,7 +709,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			String sql = "INSERT INTO subscriptions"
-				+ " (groupId, groupName, groupKey, start) VALUES (?, ?, ?, ?)";
+					+ " (groupId, groupName, groupKey, start)"
+					+ " VALUES (?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getId().getBytes());
 			ps.setString(2, g.getName());
@@ -755,6 +726,55 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void addSecrets(Connection txn, Collection<TemporarySecret> secrets)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			// Store the new secrets
+			String sql = "INSERT INTO secrets"
+					+ " (contactId, transportId, period, secret, outgoing,"
+					+ " centre, bitmap)";
+			ps = txn.prepareStatement(sql);
+			for(TemporarySecret s : secrets) {
+				ps.setInt(1, s.getContactId().getInt());
+				ps.setBytes(2, s.getTransportId().getBytes());
+				ps.setLong(3, s.getPeriod());
+				ps.setBytes(4, s.getSecret());
+				ps.setLong(5, s.getOutgoingConnectionCounter());
+				ps.setLong(6, s.getWindowCentre());
+				ps.setBytes(7, s.getWindowBitmap());
+				ps.addBatch();
+			}
+			int[] batchAffected = ps.executeBatch();
+			if(batchAffected.length != secrets.size())
+				throw new DbStateException();
+			for(int i = 0; i < batchAffected.length; i++) {
+				if(batchAffected[i] != 1) throw new DbStateException();
+			}
+			ps.close();
+			// Delete any obsolete secrets
+			sql = "DELETE FROM secrets"
+					+ " WHERE contactId = ? AND transportId = ? AND period < ?";
+			ps = txn.prepareStatement(sql);
+			for(TemporarySecret s : secrets) {
+				ps.setInt(1, s.getContactId().getInt());
+				ps.setBytes(2, s.getTransportId().getBytes());
+				ps.setLong(3, s.getPeriod() - 1);
+				ps.addBatch();
+			}
+			batchAffected = ps.executeBatch();
+			if(batchAffected.length != secrets.size())
+				throw new DbStateException();
+			for(int i = 0; i < batchAffected.length; i++) {
+				if(batchAffected[i] > 1) throw new DbStateException();
+			}
+			ps.close();
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void addSubscription(Connection txn, ContactId c, Group g,
 			long start) throws DbException {
 		PreparedStatement ps = null;
@@ -762,7 +782,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Check whether the subscription already exists
 			String sql = "SELECT NULL FROM contactSubscriptions"
-				+ " WHERE contactId = ? AND groupId = ?";
+					+ " WHERE contactId = ? AND groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getId().getBytes());
@@ -774,8 +794,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(found) return;
 			// Add the subscription
 			sql = "INSERT INTO contactSubscriptions"
-				+ " (contactId, groupId, groupName, groupKey, start)"
-				+ " VALUES (?, ?, ?, ?, ?)";
+					+ " (contactId, groupId, groupName, groupKey, start)"
+					+ " VALUES (?, ?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getId().getBytes());
@@ -792,46 +812,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public TransportIndex addTransport(Connection txn, TransportId t)
-	throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			// Allocate a new index
-			String sql = "INSERT INTO transports (transportId) VALUES (?)";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			int affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			ps.close();
-			// If the new index is in range, return it
-			sql = "SELECT index FROM transports WHERE transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			rs = ps.executeQuery();
-			if(!rs.next()) throw new DbStateException();
-			int i = rs.getInt(1);
-			if(rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			if(i < ProtocolConstants.MAX_TRANSPORTS)
-				return new TransportIndex(i);
-			// Too many transports - delete the new index and return null
-			sql = "DELETE FROM transports WHERE transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			return null;
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public void addVisibility(Connection txn, ContactId c, GroupId g)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -839,7 +821,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			byte[] groupId = null, nextId = null;
 			long deleted = 0L;
 			String sql = "SELECT groupId, nextId, deleted FROM visibilities"
-				+ " WHERE contactId = ? AND nextId > ? ORDER BY nextId LIMIT ?";
+					+ " WHERE contactId = ? AND nextId > ?"
+					+ " ORDER BY nextId LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getBytes());
@@ -850,7 +833,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				rs.close();
 				ps.close();
 				sql = "SELECT groupId, nextId, deleted FROM visibilities"
-					+ " WHERE contactId = ? AND nextId IS NULL";
+						+ " WHERE contactId = ? AND nextId IS NULL";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
 				rs = ps.executeQuery();
@@ -866,14 +849,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(groupId == null) {
 				// Inserting at the head of the list
 				sql = "UPDATE visibilities SET nextId = ?"
-					+ " WHERE contactId = ? AND groupId IS NULL";
+						+ " WHERE contactId = ? AND groupId IS NULL";
 				ps = txn.prepareStatement(sql);
 				ps.setBytes(1, g.getBytes());
 				ps.setInt(2, c.getInt());
 			} else {
 				// Inserting in the middle or at the tail of the list
 				sql = "UPDATE visibilities SET nextId = ?"
-					+ " WHERE contactId = ? AND groupId = ?";
+						+ " WHERE contactId = ? AND groupId = ?";
 				ps = txn.prepareStatement(sql);
 				ps.setBytes(1, g.getBytes());
 				ps.setInt(2, c.getInt());
@@ -884,7 +867,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Insert the new element
 			sql = "INSERT INTO visibilities"
-				+ " (contactId, groupId, nextId, deleted) VALUES (?, ?, ?, ?)";
+					+ " (contactId, groupId, nextId, deleted)"
+					+ " VALUES (?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getBytes());
@@ -902,7 +886,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean containsContact(Connection txn, ContactId c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -922,8 +906,31 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public boolean containsContactTransport(Connection txn, ContactId c,
+			TransportId t) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			String sql = "SELECT NULL FROM contactTransports"
+					+ " WHERE contactId = ? AND transportId = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, t.getBytes());
+			rs = ps.executeQuery();
+			boolean found = rs.next();
+			if(rs.next()) throw new DbStateException();
+			rs.close();
+			ps.close();
+			return found;
+		} catch(SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public boolean containsMessage(Connection txn, MessageId m)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -944,7 +951,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean containsSubscription(Connection txn, GroupId g)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -965,12 +972,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean containsSubscription(Connection txn, GroupId g, long time)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT NULL FROM subscriptions"
-				+ " WHERE groupId = ? AND start <= ?";
+					+ " WHERE groupId = ? AND start <= ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			ps.setLong(2, time);
@@ -993,9 +1000,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT start FROM subscriptions JOIN visibilities"
-				+ " ON subscriptions.groupId = visibilities.groupId"
-				+ " WHERE subscriptions.groupId = ? AND contactId = ?";
+			String sql = "SELECT start FROM subscriptions AS s"
+					+ " JOIN visibilities AS v"
+					+ " ON s.groupId = v.groupId"
+					+ " WHERE s.groupId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1021,8 +1029,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT batchId FROM batchesToAck"
-				+ " WHERE contactId = ?"
-				+ " LIMIT ?";
+					+ " WHERE contactId = ?"
+					+ " LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setInt(2, maxBatches);
@@ -1040,12 +1048,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public TransportConfig getConfig(Connection txn, TransportId t)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT key, value FROM transportConfigs"
-				+ " WHERE transportId = ?";
+					+ " WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, t.getBytes());
 			rs = ps.executeQuery();
@@ -1061,67 +1069,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public ConnectionContext getConnectionContext(Connection txn, ContactId c,
-			TransportIndex i, Collection<byte[]> erase) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			// Retrieve the current context
-			String sql = "SELECT connection, secret FROM connectionContexts"
-				+ " WHERE contactId = ? AND index = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setInt(2, i.getInt());
-			rs = ps.executeQuery();
-			if(!rs.next()) throw new DbStateException();
-			long connection = rs.getLong(1);
-			byte[] secret = rs.getBytes(2);
-			if(rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			ConnectionContext ctx =
-				connectionContextFactory.createConnectionContext(c, i,
-						connection, secret);
-			// Calculate and store the next context
-			ConnectionContext next =
-				connectionContextFactory.createNextConnectionContext(c, i,
-						connection + 1, secret);
-			byte[] nextSecret = next.getSecret();
-			erase.add(nextSecret);
-			sql = "UPDATE connectionContexts"
-				+ " SET connection = connection + 1, secret = ?"
-				+ " WHERE contactId = ? AND index = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, nextSecret);
-			ps.setInt(2, c.getInt());
-			ps.setInt(3, i.getInt());
-			int affected = ps.executeUpdate();
-			if(affected != 1) throw new DbStateException();
-			ps.close();
-			return ctx;
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
-	public ConnectionWindow getConnectionWindow(Connection txn, ContactId c,
-			TransportIndex i) throws DbException {
+	public Collection<ContactId> getContacts(Connection txn)
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT connection, secret FROM connectionWindows"
-				+ " WHERE contactId = ? AND index = ?";
+			String sql = "SELECT contactId FROM contacts";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setInt(2, i.getInt());
 			rs = ps.executeQuery();
-			Map<Long, byte[]> unseen = new HashMap<Long, byte[]>();
-			while(rs.next()) unseen.put(rs.getLong(1), rs.getBytes(2));
+			List<ContactId> ids = new ArrayList<ContactId>();
+			while(rs.next()) ids.add(new ContactId(rs.getInt(1)));
 			rs.close();
 			ps.close();
-			return connectionWindowFactory.createConnectionWindow(i, unseen);
+			return Collections.unmodifiableList(ids);
 		} catch(SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1129,19 +1089,39 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<ContactId> getContacts(Connection txn)
-	throws DbException {
+	public Collection<ContactTransport> getContactTransports(Connection txn)
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT contactId FROM contacts";
+			String sql = "SELECT c.contactId, c.transportId, epoch,"
+					+ " clockDiff, latency, alice, period, secret,"
+					+ " outgoing, centre, bitmap"
+					+ " FROM contactTransports AS c"
+					+ " JOIN secrets AS s"
+					+ " ON c.contactId = s.contactId"
+					+ " AND c.transportId = s.transportId";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
-			List<ContactId> ids = new ArrayList<ContactId>();
-			while(rs.next()) ids.add(new ContactId(rs.getInt(1)));
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(ids);
+			List<ContactTransport> cts = new ArrayList<ContactTransport>();
+			while(rs.next()) {
+				ContactId contactId = new ContactId(rs.getInt(1));
+				TransportId transportId = new TransportId(rs.getBytes(2));
+				long epoch = rs.getLong(3);
+				long clockDiff = rs.getLong(4);
+				long latency = rs.getLong(5);
+				boolean alice = rs.getBoolean(6);
+				long period = rs.getLong(7);
+				byte[] secret = rs.getBytes(8);
+				long outgoing = rs.getLong(9);
+				long centre = rs.getLong(10);
+				byte[] bitmap = rs.getBytes(11);
+				ContactTransport ct = new ContactTransport(contactId,
+						transportId, epoch, clockDiff, latency, alice, period,
+						secret, outgoing, centre, bitmap);
+				cts.add(ct);
+			}
+			return Collections.unmodifiableList(cts);
 		} catch(SQLException e) {
 			tryToClose(rs);
 			tryToClose(ps);
@@ -1163,13 +1143,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			long timestamp = 0L;
 			String sql = "SELECT timestamp FROM messages"
-				+ " ORDER BY timestamp LIMIT ?";
+					+ " ORDER BY timestamp LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, 1);
 			rs = ps.executeQuery();
 			if(rs.next()) {
 				timestamp = rs.getLong(1);
-				timestamp -= timestamp % DatabaseConstants.EXPIRY_MODULUS;
+				timestamp -= timestamp % EXPIRY_MODULUS;
 			}
 			if(rs.next()) throw new DbStateException();
 			rs.close();
@@ -1183,15 +1163,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public MessageId getGroupMessageParent(Connection txn, MessageId m)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT m1.parentId FROM messages AS m1"
-				+ " JOIN messages AS m2"
-				+ " ON m1.parentId = m2.messageId"
-				+ " AND m1.groupId = m2.groupId"
-				+ " WHERE m1.messageId = ?";
+					+ " JOIN messages AS m2"
+					+ " ON m1.parentId = m2.messageId"
+					+ " AND m1.groupId = m2.groupId"
+					+ " WHERE m1.messageId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			rs = ps.executeQuery();
@@ -1210,37 +1190,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public TransportIndex getLocalIndex(Connection txn, TransportId t)
-	throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT index FROM transports WHERE transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBytes(1, t.getBytes());
-			rs = ps.executeQuery();
-			TransportIndex index = null;
-			if(rs.next()) {
-				index = new TransportIndex(rs.getInt(1));
-				if(rs.next()) throw new DbStateException();
-			}
-			rs.close();
-			ps.close();
-			return index;
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public TransportProperties getLocalProperties(Connection txn, TransportId t)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT key, value FROM transportProperties"
-				+ " WHERE transportId = ?";
+					+ " WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, t.getBytes());
 			rs = ps.executeQuery();
@@ -1257,14 +1213,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<Transport> getLocalTransports(Connection txn)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT transports.transportId, index, key, value"
-				+ " FROM transports LEFT OUTER JOIN transportProperties"
-				+ " ON transports.transportId = transportProperties.transportId"
-				+ " ORDER BY transports.transportId";
+			String sql = "SELECT transportId, key, value"
+					+ " FROM transportProperties"
+					+ " ORDER BY transportId";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
 			List<Transport> transports = new ArrayList<Transport>();
@@ -1273,13 +1228,10 @@ abstract class JdbcDatabase implements Database<Connection> {
 			while(rs.next()) {
 				TransportId id = new TransportId(rs.getBytes(1));
 				if(!id.equals(lastId)) {
-					t = new Transport(id, new TransportIndex(rs.getInt(2)));
+					t = new Transport(id);
 					transports.add(t);
 				}
-				// Key and value may be null due to the left outer join
-				String key = rs.getString(3);
-				String value = rs.getString(4);
-				if(key != null && value != null) t.put(key, value);
+				t.put(rs.getString(2), rs.getString(3));
 			}
 			rs.close();
 			ps.close();
@@ -1292,15 +1244,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<BatchId> getLostBatches(Connection txn, ContactId c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT batchId FROM outstandingBatches"
-				+ " WHERE contactId = ? AND passover >= ?";
+					+ " WHERE contactId = ? AND passover >= ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
-			ps.setInt(2, DatabaseConstants.RETRANSMIT_THRESHOLD);
+			ps.setInt(2, RETRANSMIT_THRESHOLD);
 			rs = ps.executeQuery();
 			List<BatchId> ids = new ArrayList<BatchId>();
 			while(rs.next()) ids.add(new BatchId(rs.getBytes(1)));
@@ -1338,12 +1290,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public byte[] getMessageBody(Connection txn, MessageId m)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT bodyStart, bodyLength, raw FROM messages"
-				+ " WHERE messageId = ?";
+					+ " WHERE messageId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			rs = ps.executeQuery();
@@ -1368,11 +1320,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT messages.messageId, parentId, authorId,"
-				+ " subject, timestamp, read, starred"
-				+ " FROM messages LEFT OUTER JOIN flags"
-				+ " ON messages.messageId = flags.messageId"
-				+ " WHERE groupId = ?";
+			String sql = "SELECT m.messageId, parentId, authorId,"
+					+ " subject, timestamp, read, starred"
+					+ " FROM messages AS m"
+					+ " LEFT OUTER JOIN flags AS f"
+					+ " ON m.messageId = f.messageId"
+					+ " WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			rs = ps.executeQuery();
@@ -1400,15 +1353,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public byte[] getMessageIfSendable(Connection txn, ContactId c, MessageId m)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			// Do we have a sendable private message with the given ID?
-			String sql = "SELECT length, raw FROM messages"
-				+ " JOIN statuses ON messages.messageId = statuses.messageId"
-				+ " WHERE messages.messageId = ? AND messages.contactId = ?"
-				+ " AND status = ?";
+			String sql = "SELECT length, raw FROM messages AS m"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " WHERE m.messageId = ? AND m.contactId = ?"
+					+ " AND status = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1425,23 +1379,23 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			if(raw != null) return raw;
 			// Do we have a sendable group message with the given ID?
-			sql = "SELECT length, raw FROM messages"
-				+ " JOIN contactSubscriptions AS cs"
-				+ " ON messages.groupId = cs.groupId"
-				+ " JOIN visibilities"
-				+ " ON messages.groupId = visibilities.groupId"
-				+ " AND cs.contactId = visibilities.contactId"
-				+ " JOIN statuses"
-				+ " ON messages.messageId = statuses.messageId"
-				+ " AND cs.contactId = statuses.contactId"
-				+ " JOIN subscriptionTimes"
-				+ " ON cs.contactId = subscriptionTimes.contactId"
-				+ " WHERE messages.messageId = ?"
-				+ " AND cs.contactId = ?"
-				+ " AND timestamp >= start"
-				+ " AND timestamp >= expiry"
-				+ " AND status = ?"
-				+ " AND sendability > ZERO()";
+			sql = "SELECT length, raw FROM messages AS m"
+					+ " JOIN contactSubscriptions AS cs"
+					+ " ON m.groupId = cs.groupId"
+					+ " JOIN visibilities AS v"
+					+ " ON m.groupId = v.groupId"
+					+ " AND cs.contactId = v.contactId"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " AND cs.contactId = s.contactId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON cs.contactId = st.contactId"
+					+ " WHERE m.messageId = ?"
+					+ " AND cs.contactId = ?"
+					+ " AND timestamp >= start"
+					+ " AND timestamp >= expiry"
+					+ " AND status = ?"
+					+ " AND sendability > ZERO()";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -1464,7 +1418,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<MessageId> getMessagesByAuthor(Connection txn, AuthorId a)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1485,7 +1439,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public int getNumberOfSendableChildren(Connection txn, MessageId m)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1500,8 +1454,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs.close();
 			ps.close();
 			sql = "SELECT COUNT(messageId) FROM messages"
-				+ " WHERE parentId = ? AND groupId = ?"
-				+ " AND sendability > ZERO()";
+					+ " WHERE parentId = ? AND groupId = ?"
+					+ " AND sendability > ZERO()";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setBytes(2, groupId);
@@ -1520,12 +1474,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<MessageId> getOldMessages(Connection txn, int capacity)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT length, messageId FROM messages"
-				+ " ORDER BY timestamp";
+					+ " ORDER BY timestamp";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
 			List<MessageId> ids = new ArrayList<MessageId>();
@@ -1589,48 +1543,20 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public TransportIndex getRemoteIndex(Connection txn, ContactId c,
-			TransportId t) throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT index FROM contactTransports"
-				+ " WHERE contactId = ? AND transportId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setBytes(2, t.getBytes());
-			rs = ps.executeQuery();
-			TransportIndex index = null;
-			if(rs.next()) {
-				index = new TransportIndex(rs.getInt(1));
-				if(rs.next()) throw new DbStateException();
-			}
-			rs.close();
-			ps.close();
-			return index;
-		} catch(SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Map<ContactId, TransportProperties> getRemoteProperties(
 			Connection txn, TransportId t) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT contactTransports.contactId, key, value"
-				+ " FROM contactTransports"
-				+ " LEFT OUTER JOIN contactTransportProperties AS ctp"
-				+ " ON contactTransports.transportId = ctp.transportId"
-				+ " WHERE contactTransports.transportId = ?"
-				+ " ORDER BY contactTransports.contactId";
+			String sql = "SELECT contactId, key, value"
+					+ " FROM contactTransportProperties"
+					+ " WHERE transportId = ?"
+					+ " ORDER BY contactId";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, t.getBytes());
 			rs = ps.executeQuery();
 			Map<ContactId, TransportProperties> properties =
-				new HashMap<ContactId, TransportProperties>();
+					new HashMap<ContactId, TransportProperties>();
 			ContactId lastId = null;
 			TransportProperties p = null;
 			while(rs.next()) {
@@ -1639,10 +1565,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					p = new TransportProperties();
 					properties.put(id, p);
 				}
-				// Key and value may be null due to the left outer join
-				String key = rs.getString(2);
-				String value = rs.getString(3);
-				if(key != null && value != null) p.put(key, value);
+				p.put(rs.getString(2), rs.getString(3));
 			}
 			rs.close();
 			ps.close();
@@ -1681,11 +1604,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			// Do we have any sendable private messages?
-			String sql = "SELECT messages.messageId FROM messages"
-				+ " JOIN statuses ON messages.messageId = statuses.messageId"
-				+ " WHERE messages.contactId = ? AND status = ?"
-				+ " ORDER BY timestamp"
-				+ " LIMIT ?";
+			String sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " WHERE m.contactId = ? AND status = ?"
+					+ " ORDER BY timestamp"
+					+ " LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setShort(2, (short) Status.NEW.ordinal());
@@ -1698,24 +1622,24 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(ids.size() == maxMessages)
 				return Collections.unmodifiableList(ids);
 			// Do we have any sendable group messages?
-			sql = "SELECT messages.messageId FROM messages"
-				+ " JOIN contactSubscriptions AS cs"
-				+ " ON m.groupId = cs.groupId"
-				+ " JOIN visibilities"
-				+ " ON messages.groupId = visibilities.groupId"
-				+ " AND cs.contactId = visibilities.contactId"
-				+ " JOIN statuses"
-				+ " ON messages.messageId = statuses.messageId"
-				+ " AND cs.contactId = statuses.contactId"
-				+ " JOIN subscriptionTimes"
-				+ " ON cs.contactId = subscriptionTimes.contactId"
-				+ " WHERE cs.contactId = ?"
-				+ " AND timestamp >= start"
-				+ " AND timestamp >= expiry"
-				+ " AND status = ?"
-				+ " AND sendability > ZERO()"
-				+ " ORDER BY timestamp"
-				+ " LIMIT ?";
+			sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN contactSubscriptions AS cs"
+					+ " ON m.groupId = cs.groupId"
+					+ " JOIN visibilities AS v"
+					+ " ON m.groupId = v.groupId"
+					+ " AND cs.contactId = v.contactId"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " AND cs.contactId = s.contactId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON cs.contactId = st.contactId"
+					+ " WHERE cs.contactId = ?"
+					+ " AND timestamp >= start"
+					+ " AND timestamp >= expiry"
+					+ " AND status = ?"
+					+ " AND sendability > ZERO()"
+					+ " ORDER BY timestamp"
+					+ " LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setShort(2, (short) Status.NEW.ordinal());
@@ -1738,10 +1662,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			// Do we have any sendable private messages?
-			String sql = "SELECT length, messages.messageId FROM messages"
-				+ " JOIN statuses ON messages.messageId = statuses.messageId"
-				+ " WHERE messages.contactId = ? AND status = ?"
-				+ " ORDER BY timestamp";
+			String sql = "SELECT length, m.messageId FROM messages AS m"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " WHERE m.contactId = ? AND status = ?"
+					+ " ORDER BY timestamp";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setShort(2, (short) Status.NEW.ordinal());
@@ -1758,23 +1683,23 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			if(total == capacity) return Collections.unmodifiableList(ids);
 			// Do we have any sendable group messages?
-			sql = "SELECT length, messages.messageId FROM messages"
-				+ " JOIN contactSubscriptions AS cs"
-				+ " ON messages.groupId = cs.groupId"
-				+ " JOIN visibilities"
-				+ " ON messages.groupId = visibilities.groupId"
-				+ " AND cs.contactId = visibilities.contactId"
-				+ " JOIN statuses"
-				+ " ON messages.messageId = statuses.messageId"
-				+ " AND cs.contactId = statuses.contactId"
-				+ " JOIN subscriptionTimes"
-				+ " ON cs.contactId = subscriptionTimes.contactId"
-				+ " WHERE cs.contactId = ?"
-				+ " AND timestamp >= start"
-				+ " AND timestamp >= expiry"
-				+ " AND status = ?"
-				+ " AND sendability > ZERO()"
-				+ " ORDER BY timestamp";
+			sql = "SELECT length, m.messageId FROM messages AS m"
+					+ " JOIN contactSubscriptions AS cs"
+					+ " ON m.groupId = cs.groupId"
+					+ " JOIN visibilities AS v"
+					+ " ON m.groupId = v.groupId"
+					+ " AND cs.contactId = v.contactId"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " AND cs.contactId = s.contactId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON cs.contactId = st.contactId"
+					+ " WHERE cs.contactId = ?"
+					+ " AND timestamp >= start"
+					+ " AND timestamp >= expiry"
+					+ " AND status = ?"
+					+ " AND sendability > ZERO()"
+					+ " ORDER BY timestamp";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setShort(2, (short) Status.NEW.ordinal());
@@ -1817,12 +1742,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<Group> getSubscriptions(Connection txn)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT groupId, groupName, groupKey"
-				+ " FROM subscriptions";
+					+ " FROM subscriptions";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
 			List<Group> subs = new ArrayList<Group>();
@@ -1843,13 +1768,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<Group> getSubscriptions(Connection txn, ContactId c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT groupId, groupName, groupKey"
-				+ " FROM contactSubscriptions"
-				+ " WHERE contactId = ?";
+					+ " FROM contactSubscriptions"
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			rs = ps.executeQuery();
@@ -1891,12 +1816,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public long getTransportsSent(Connection txn, ContactId c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT sent FROM transportTimestamps"
-				+ " WHERE contactId = ?";
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			rs = ps.executeQuery();
@@ -1914,15 +1839,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Map<GroupId, Integer> getUnreadMessageCounts(Connection txn)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT groupId, COUNT(*)"
-				+ " FROM messages LEFT OUTER JOIN flags"
-				+ " ON messages.messageId = flags.messageId"
-				+ " WHERE (NOT read) OR (read IS NULL)"
-				+ " GROUP BY groupId";
+					+ " FROM messages AS m"
+					+ " LEFT OUTER JOIN flags AS f"
+					+ " ON m.messageId = f.messageId"
+					+ " WHERE (NOT read) OR (read IS NULL)"
+					+ " GROUP BY groupId";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
 			Map<GroupId, Integer> counts = new HashMap<GroupId, Integer>();
@@ -1941,7 +1867,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<ContactId> getVisibility(Connection txn, GroupId g)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -1966,11 +1892,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT groupId, nextId FROM visibilities"
-				+ " JOIN subscriptionTimes"
-				+ " ON visibilities.contactId = subscriptionTimes.contactId"
-				+ " WHERE visibilities.contactId = ?"
-				+ " AND deleted > acked AND deleted < ?";
+			String sql = "SELECT groupId, nextId FROM visibilities AS v"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON v.contactId = st.contactId"
+					+ " WHERE v.contactId = ?"
+					+ " AND deleted > acked AND deleted < ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setLong(2, timestamp);
@@ -2000,14 +1926,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql =
-				"SELECT subscriptions.groupId, groupName, groupKey, start"
-				+ " FROM subscriptions JOIN visibilities"
-				+ " ON subscriptions.groupId = visibilities.groupId"
-				+ " JOIN subscriptionTimes"
-				+ " ON visibilities.contactId = subscriptionTimes.contactId"
-				+ " WHERE visibilities.contactId = ?"
-				+ " AND start > acked AND start < ?";
+			String sql = "SELECT s.groupId, groupName, groupKey, start"
+					+ " FROM subscriptions AS s"
+					+ " JOIN visibilities AS v"
+					+ " ON s.groupId = v.groupId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON v.contactId = st.contactId"
+					+ " WHERE v.contactId = ?"
+					+ " AND start > acked AND start < ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setLong(2, timestamp);
@@ -2033,15 +1959,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean hasSendableMessages(Connection txn, ContactId c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			// Do we have any sendable private messages?
-			String sql = "SELECT messages.messageId FROM messages"
-				+ " JOIN statuses ON messages.messageId = statuses.messageId"
-				+ " WHERE messages.contactId = ? AND status = ?"
-				+ " LIMIT ?";
+			String sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " WHERE m.contactId = ? AND status = ?"
+					+ " LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setShort(2, (short) Status.NEW.ordinal());
@@ -2053,23 +1980,23 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			if(found) return true;
 			// Do we have any sendable group messages?
-			sql = "SELECT messages.messageId FROM messages"
-				+ " JOIN contactSubscriptions AS cs"
-				+ " ON messages.groupId = cs.groupId"
-				+ " JOIN visibilities"
-				+ " ON messages.groupId = visibilities.groupId"
-				+ " AND cs.contactId = visibilities.contactId"
-				+ " JOIN statuses"
-				+ " ON messages.messageId = statuses.messageId"
-				+ " AND cs.contactId = statuses.contactId"
-				+ " JOIN subscriptionTimes"
-				+ " ON cs.contactId = subscriptionTimes.contactId"
-				+ " WHERE cs.contactId = ?"
-				+ " AND timestamp >= start"
-				+ " AND timestamp >= expiry"
-				+ " AND status = ?"
-				+ " AND sendability > ZERO()"
-				+ " LIMIT ?";
+			sql = "SELECT m.messageId FROM messages AS m"
+					+ " JOIN contactSubscriptions AS cs"
+					+ " ON m.groupId = cs.groupId"
+					+ " JOIN visibilities AS v"
+					+ " ON m.groupId = v.groupId"
+					+ " AND cs.contactId = v.contactId"
+					+ " JOIN statuses AS s"
+					+ " ON m.messageId = s.messageId"
+					+ " AND cs.contactId = s.contactId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON cs.contactId = st.contactId"
+					+ " WHERE cs.contactId = ?"
+					+ " AND timestamp >= start"
+					+ " AND timestamp >= expiry"
+					+ " AND status = ?"
+					+ " AND sendability > ZERO()"
+					+ " LIMIT ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setShort(2, (short) Status.NEW.ordinal());
@@ -2087,13 +2014,32 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void incrementConnectionCounter(Connection txn, ContactId c,
+			TransportId t, long period) throws DbException {
+		PreparedStatement ps = null;
+		try {
+			String sql = "UPDATE secrets SET outgoing = outgoing + 1"
+					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setBytes(2, t.getBytes());
+			ps.setLong(3, period);
+			int affected = ps.executeUpdate();
+			if(affected > 1) throw new DbStateException();
+			ps.close();
+		} catch(SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void removeAckedBatch(Connection txn, ContactId c, BatchId b)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT timestamp FROM outstandingBatches"
-				+ " WHERE contactId = ? AND batchId = ?";
+					+ " WHERE contactId = ? AND batchId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, b.getBytes());
@@ -2105,7 +2051,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Increment the passover count of all older outstanding batches
 			sql = "UPDATE outstandingBatches SET passover = passover + ?"
-				+ " WHERE contactId = ? AND timestamp < ?";
+					+ " WHERE contactId = ? AND timestamp < ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, 1);
 			ps.setInt(2, c.getInt());
@@ -2126,13 +2072,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT messageId FROM outstandingMessages"
-				+ " WHERE contactId = ? AND batchId = ?";
+					+ " WHERE contactId = ? AND batchId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, b.getBytes());
 			rs = ps.executeQuery();
 			sql = "UPDATE statuses SET status = ?"
-				+ " WHERE messageId = ? AND contactId = ? AND status = ?";
+					+ " WHERE messageId = ? AND contactId = ? AND status = ?";
 			ps1 = txn.prepareStatement(sql);
 			ps1.setShort(1, (short) newStatus.ordinal());
 			ps1.setInt(3, c.getInt());
@@ -2171,7 +2117,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			String sql = "DELETE FROM batchesToAck"
-				+ " WHERE contactId = ? and batchId = ?";
+					+ " WHERE contactId = ? and batchId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			for(BatchId b : sent) {
@@ -2192,7 +2138,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void removeContact(Connection txn, ContactId c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "DELETE FROM contacts WHERE contactId = ?";
@@ -2208,7 +2154,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void removeLostBatch(Connection txn, ContactId c, BatchId b)
-	throws DbException {
+			throws DbException {
 		removeBatch(txn, c, b, Status.NEW);
 	}
 
@@ -2228,14 +2174,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void removeSubscription(Connection txn, GroupId g)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null, ps1 = null;
 		ResultSet rs = null;
 		try {
 			// Remove the group ID from the visibility lists
 			long now = clock.currentTimeMillis();
 			String sql = "SELECT contactId, nextId FROM visibilities"
-				+ " WHERE groupId = ?";
+					+ " WHERE groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getBytes());
 			rs = ps.executeQuery();
@@ -2243,7 +2189,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				int contactId = rs.getInt(1);
 				byte[] nextId = rs.getBytes(2);
 				sql = "UPDATE visibilities SET nextId = ?, deleted = ?"
-					+ " WHERE contactId = ? AND nextId = ?";
+						+ " WHERE contactId = ? AND nextId = ?";
 				ps1 = txn.prepareStatement(sql);
 				if(nextId == null) ps1.setNull(1, Types.BINARY); // At the tail
 				else ps1.setBytes(1, nextId); // At the head or in the middle
@@ -2279,28 +2225,28 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(start == null && end == null) {
 				// Delete everything
 				String sql = "DELETE FROM contactSubscriptions"
-					+ " WHERE contactId = ?";
+						+ " WHERE contactId = ?";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
 			} else if(start == null) {
 				// Delete everything before end
 				String sql = "DELETE FROM contactSubscriptions"
-					+ " WHERE contactId = ? AND groupId < ?";
+						+ " WHERE contactId = ? AND groupId < ?";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
 				ps.setBytes(2, end.getBytes());
 			} else if(end == null) {
 				// Delete everything after start
 				String sql = "DELETE FROM contactSubscriptions"
-					+ " WHERE contactId = ? AND groupId > ?";
+						+ " WHERE contactId = ? AND groupId > ?";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
 				ps.setBytes(2, start.getBytes());
 			} else {
 				// Delete everything between start and end
 				String sql = "DELETE FROM contactSubscriptions"
-					+ " WHERE contactId = ?"
-					+ " AND groupId > ? AND groupId < ?";
+						+ " WHERE contactId = ?"
+						+ " AND groupId > ? AND groupId < ?";
 				ps = txn.prepareStatement(sql);
 				ps.setInt(1, c.getInt());
 				ps.setBytes(2, start.getBytes());
@@ -2316,13 +2262,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void removeVisibility(Connection txn, ContactId c, GroupId g)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			// Remove the group ID from the linked list
 			String sql = "SELECT nextId FROM visibilities"
-				+ " WHERE contactId = ? AND groupId = ?";
+					+ " WHERE contactId = ? AND groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getBytes());
@@ -2333,7 +2279,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs.close();
 			ps.close();
 			sql = "DELETE FROM visibilities"
-				+ " WHERE contactId = ? AND groupId = ?";
+					+ " WHERE contactId = ? AND groupId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setBytes(2, g.getBytes());
@@ -2341,7 +2287,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			if(affected != 1) throw new DbStateException();
 			ps.close();
 			sql = "UPDATE visibilities SET nextId = ?, deleted = ?"
-				+ " WHERE contactId = ? AND nextId = ?";
+					+ " WHERE contactId = ? AND nextId = ?";
 			ps = txn.prepareStatement(sql);
 			if(nextId == null) ps.setNull(1, Types.BINARY); // At the tail
 			else ps.setBytes(1, nextId); // At the head or in the middle
@@ -2359,7 +2305,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setConfig(Connection txn, TransportId t, TransportConfig c)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			// Delete any existing config for the given transport
@@ -2370,7 +2316,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Store the new config
 			sql = "INSERT INTO transportConfigs (transportId, key, value)"
-				+ " VALUES (?, ?, ?)";
+					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, t.getBytes());
 			for(Entry<String, String> e : c.entrySet()) {
@@ -2379,8 +2325,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				ps.addBatch();
 			}
 			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != c.size())
-				throw new DbStateException();
+			if(batchAffected.length != c.size()) throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] != 1) throw new DbStateException();
 			}
@@ -2391,37 +2336,20 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setConnectionWindow(Connection txn, ContactId c,
-			TransportIndex i, ConnectionWindow w) throws DbException {
+	public void setConnectionWindow(Connection txn, ContactId c, TransportId t,
+			long period, long centre, byte[] bitmap) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			// Delete any existing connection window
-			String sql = "DELETE FROM connectionWindows"
-				+ " WHERE contactId = ? AND index = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setInt(2, i.getInt());
-			ps.executeUpdate();
-			ps.close();
-			// Store the new connection window
-			sql = "INSERT INTO connectionWindows"
-				+ " (contactId, index, connection, secret)"
-				+ " VALUES(?, ?, ?, ?)";
+			String sql = "UPDATE secrets SET centre = ? AND bitmap = ?"
+					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setInt(2, i.getInt());
-			Map<Long, byte[]> unseen = w.getUnseen();
-			for(Entry<Long, byte[]> e : unseen.entrySet()) {
-				ps.setLong(3, e.getKey());
-				ps.setBytes(4, e.getValue());
-				ps.addBatch();
-			}
-			int[] affectedBatch = ps.executeBatch();
-			if(affectedBatch.length != unseen.size())
-				throw new DbStateException();
-			for(int j = 0; j < affectedBatch.length; j++) {
-				if(affectedBatch[j] != 1) throw new DbStateException();
-			}
+			ps.setLong(1, centre);
+			ps.setBytes(2, bitmap);
+			ps.setInt(3, c.getInt());
+			ps.setBytes(4, t.getBytes());
+			ps.setLong(5, period);
+			int affected = ps.executeUpdate();
+			if(affected > 1) throw new DbStateException();
 			ps.close();
 		} catch(SQLException e) {
 			tryToClose(ps);
@@ -2430,11 +2358,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setExpiryTime(Connection txn, ContactId c, long expiry)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE subscriptionTimes SET expiry = ?"
-				+ " WHERE contactId = ?";
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setLong(1, expiry);
 			ps.setInt(2, c.getInt());
@@ -2453,14 +2381,14 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			// Delete any existing properties for the given transport
 			String sql = "DELETE FROM transportProperties"
-				+ " WHERE transportId = ?";
+					+ " WHERE transportId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, t.getBytes());
 			ps.executeUpdate();
 			ps.close();
 			// Store the new properties
 			sql = "INSERT INTO transportProperties (transportId, key, value)"
-				+ " VALUES (?, ?, ?)";
+					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, t.getBytes());
 			for(Entry<String, String> e : p.entrySet()) {
@@ -2469,8 +2397,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				ps.addBatch();
 			}
 			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != p.size())
-				throw new DbStateException();
+			if(batchAffected.length != p.size()) throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] != 1) throw new DbStateException();
 			}
@@ -2482,7 +2409,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Rating setRating(Connection txn, AuthorId a, Rating r)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -2513,7 +2440,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				old = Rating.UNRATED;
 				if(!old.equals(r)) {
 					sql = "INSERT INTO ratings (authorId, rating)"
-						+ " VALUES (?, ?)";
+							+ " VALUES (?, ?)";
 					ps = txn.prepareStatement(sql);
 					ps.setBytes(1, a.getBytes());
 					ps.setShort(2, (short) r.ordinal());
@@ -2531,7 +2458,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean setRead(Connection txn, MessageId m, boolean read)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -2562,7 +2489,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				old = false;
 				if(old != read) {
 					sql = "INSERT INTO flags (messageId, read, starred)"
-						+ " VALUES (?, ?, ?)";
+							+ " VALUES (?, ?, ?)";
 					ps = txn.prepareStatement(sql);
 					ps.setBytes(1, m.getBytes());
 					ps.setBoolean(2, read);
@@ -2581,11 +2508,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setSendability(Connection txn, MessageId m, int sendability)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE messages SET sendability = ?"
-				+ " WHERE messageId = ?";
+					+ " WHERE messageId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, sendability);
 			ps.setBytes(2, m.getBytes());
@@ -2599,7 +2526,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public boolean setStarred(Connection txn, MessageId m, boolean starred)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -2630,7 +2557,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				old = false;
 				if(old != starred) {
 					sql = "INSERT INTO flags (messageId, read, starred)"
-						+ " VALUES (?, ?, ?)";
+							+ " VALUES (?, ?, ?)";
 					ps = txn.prepareStatement(sql);
 					ps.setBytes(1, m.getBytes());
 					ps.setBoolean(2, false);
@@ -2649,12 +2576,12 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setStatus(Connection txn, ContactId c, MessageId m, Status s)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			String sql = "SELECT status FROM statuses"
-				+ " WHERE messageId = ? AND contactId = ?";
+					+ " WHERE messageId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -2667,7 +2594,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				ps.close();
 				if(!old.equals(Status.SEEN) && !old.equals(s)) {
 					sql = "UPDATE statuses SET status = ?"
-						+ " WHERE messageId = ? AND contactId = ?";
+							+ " WHERE messageId = ? AND contactId = ?";
 					ps = txn.prepareStatement(sql);
 					ps.setShort(1, (short) s.ordinal());
 					ps.setBytes(2, m.getBytes());
@@ -2681,7 +2608,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				rs.close();
 				ps.close();
 				sql = "INSERT INTO statuses (messageId, contactId, status)"
-					+ " VALUES (?, ?, ?)";
+						+ " VALUES (?, ?, ?)";
 				ps = txn.prepareStatement(sql);
 				ps.setBytes(1, m.getBytes());
 				ps.setInt(2, c.getInt());
@@ -2702,18 +2629,18 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
-			String sql = "SELECT NULL FROM messages"
-				+ " JOIN contactSubscriptions AS cs"
-				+ " ON messages.groupId = cs.groupId"
-				+ " JOIN visibilities"
-				+ " ON messages.groupId = visibilities.groupId"
-				+ " AND cs.contactId = visibilities.contactId"
-				+ " JOIN subscriptionTimes"
-				+ " ON cs.contactId = subscriptionTimes.contactId"
-				+ " WHERE messageId = ?"
-				+ " AND cs.contactId = ?"
-				+ " AND timestamp >= start"
-				+ " AND timestamp >= expiry";
+			String sql = "SELECT NULL FROM messages AS m"
+					+ " JOIN contactSubscriptions AS cs"
+					+ " ON m.groupId = cs.groupId"
+					+ " JOIN visibilities AS v"
+					+ " ON m.groupId = v.groupId"
+					+ " AND cs.contactId = v.contactId"
+					+ " JOIN subscriptionTimes AS st"
+					+ " ON cs.contactId = st.contactId"
+					+ " WHERE messageId = ?"
+					+ " AND cs.contactId = ?"
+					+ " AND timestamp >= start"
+					+ " AND timestamp >= expiry";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, m.getBytes());
 			ps.setInt(2, c.getInt());
@@ -2724,7 +2651,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			if(!found) return false;
 			sql = "UPDATE statuses SET status = ?"
-				+ " WHERE messageId = ? AND contactId = ?";
+					+ " WHERE messageId = ? AND contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setShort(1, (short) Status.SEEN.ordinal());
 			ps.setBytes(2, m.getBytes());
@@ -2745,7 +2672,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE subscriptionTimes SET acked = ?"
-				+ " WHERE contactId = ?";
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setLong(1, timestamp);
 			ps.setInt(2, c.getInt());
@@ -2763,7 +2690,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE subscriptionTimes SET received = ?"
-				+ " WHERE contactId = ?";
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setLong(1, timestamp);
 			ps.setInt(2, c.getInt());
@@ -2778,13 +2705,13 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public void setTransports(Connection txn, ContactId c,
 			Collection<Transport> transports, long timestamp)
-	throws DbException {
+					throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
 			// Return if the timestamp isn't fresh
 			String sql = "SELECT received FROM transportTimestamps"
-				+ " WHERE contactId = ?";
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			rs = ps.executeQuery();
@@ -2794,38 +2721,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs.close();
 			ps.close();
 			if(lastTimestamp >= timestamp) return;
-			// Delete any existing transports
-			sql = "DELETE FROM contactTransports WHERE contactId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.executeUpdate();
-			ps.close();
 			// Delete any existing transport properties
 			sql = "DELETE FROM contactTransportProperties WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.executeUpdate();
 			ps.close();
-			// Store the new transports
-			sql = "INSERT INTO contactTransports"
-				+ " (contactId, transportId, index) VALUES (?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			for(Transport t : transports) {
-				ps.setBytes(2, t.getId().getBytes());
-				ps.setInt(3, t.getIndex().getInt());
-				ps.addBatch();
-			}
-			int[] batchAffected = ps.executeBatch();
-			if(batchAffected.length != transports.size())
-				throw new DbStateException();
-			for(int i = 0; i < batchAffected.length; i++) {
-				if(batchAffected[i] != 1) throw new DbStateException();
-			}
-			ps.close();
 			// Store the new transport properties
 			sql = "INSERT INTO contactTransportProperties"
-				+ " (contactId, transportId, key, value) VALUES (?, ?, ?, ?)";
+					+ " (contactId, transportId, key, value)"
+					+ " VALUES (?, ?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			int batchSize = 0;
@@ -2838,7 +2743,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 					batchSize++;
 				}
 			}
-			batchAffected = ps.executeBatch();
+			int[] batchAffected = ps.executeBatch();
 			if(batchAffected.length != batchSize) throw new DbStateException();
 			for(int i = 0; i < batchAffected.length; i++) {
 				if(batchAffected[i] != 1) throw new DbStateException();
@@ -2846,7 +2751,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			ps.close();
 			// Update the timestamp
 			sql = "UPDATE transportTimestamps SET received = ?"
-				+ " WHERE contactId = ?";
+					+ " WHERE contactId = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setLong(1, timestamp);
 			ps.setInt(2, c.getInt());
@@ -2861,7 +2766,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setTransportsModified(Connection txn, long timestamp)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE transportTimestamps set modified = ?";
@@ -2876,11 +2781,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void setTransportsSent(Connection txn, ContactId c, long timestamp)
-	throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		try {
 			String sql = "UPDATE transportTimestamps SET sent = ?"
-				+ " WHERE contactId = ? AND sent < ?";
+					+ " WHERE contactId = ? AND sent < ?";
 			ps = txn.prepareStatement(sql);
 			ps.setLong(1, timestamp);
 			ps.setInt(2, c.getInt());
diff --git a/components/net/sf/briar/plugins/InvitationStarterImpl.java b/components/net/sf/briar/plugins/InvitationStarterImpl.java
deleted file mode 100644
index f67be85716375930e82b7ecff9c4624a2779a917..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/plugins/InvitationStarterImpl.java
+++ /dev/null
@@ -1,220 +0,0 @@
-package net.sf.briar.plugins;
-
-import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
-import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
-import static net.sf.briar.api.plugins.InvitationConstants.MAX_CODE;
-import static net.sf.briar.api.plugins.InvitationConstants.MAX_PUBLIC_KEY_LENGTH;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.KeyPair;
-import java.util.Arrays;
-import java.util.concurrent.Executor;
-
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.crypto.MessageDigest;
-import net.sf.briar.api.crypto.PseudoRandom;
-import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.db.DbException;
-import net.sf.briar.api.plugins.IncomingInvitationCallback;
-import net.sf.briar.api.plugins.InvitationCallback;
-import net.sf.briar.api.plugins.InvitationStarter;
-import net.sf.briar.api.plugins.OutgoingInvitationCallback;
-import net.sf.briar.api.plugins.PluginExecutor;
-import net.sf.briar.api.plugins.duplex.DuplexPlugin;
-import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
-import net.sf.briar.api.serial.Reader;
-import net.sf.briar.api.serial.ReaderFactory;
-import net.sf.briar.api.serial.Writer;
-import net.sf.briar.api.serial.WriterFactory;
-import net.sf.briar.util.ByteUtils;
-
-import com.google.inject.Inject;
-
-class InvitationStarterImpl implements InvitationStarter {
-
-	private static final String TIMED_OUT = "INVITATION_TIMED_OUT";
-	private static final String IO_EXCEPTION = "INVITATION_IO_EXCEPTION";
-	private static final String INVALID_KEY = "INVITATION_INVALID_KEY";
-	private static final String WRONG_CODE = "INVITATION_WRONG_CODE";
-	private static final String DB_EXCEPTION = "INVITATION_DB_EXCEPTION";
-
-	private final Executor pluginExecutor;
-	private final CryptoComponent crypto;
-	private final DatabaseComponent db;
-	private final ReaderFactory readerFactory;
-	private final WriterFactory writerFactory;
-
-	@Inject
-	InvitationStarterImpl(@PluginExecutor Executor pluginExecutor,
-			CryptoComponent crypto, DatabaseComponent db,
-			ReaderFactory readerFactory, WriterFactory writerFactory) {
-		this.pluginExecutor = pluginExecutor;
-		this.crypto = crypto;
-		this.db = db;
-		this.readerFactory = readerFactory;
-		this.writerFactory = writerFactory;
-	}
-
-	public void startIncomingInvitation(DuplexPlugin plugin,
-			IncomingInvitationCallback callback) {
-		pluginExecutor.execute(new IncomingInvitationWorker(plugin, callback));
-	}
-
-	public void startOutgoingInvitation(DuplexPlugin plugin,
-			OutgoingInvitationCallback callback) {
-		pluginExecutor.execute(new OutgoingInvitationWorker(plugin, callback));
-	}
-
-	private abstract class InvitationWorker implements Runnable {
-
-		private final DuplexPlugin plugin;
-		private final InvitationCallback callback;
-		private final boolean initiator;
-
-		protected InvitationWorker(DuplexPlugin plugin,
-				InvitationCallback callback, boolean initiator) {
-			this.plugin = plugin;
-			this.callback = callback;
-			this.initiator = initiator;
-		}
-
-		protected abstract int getInvitationCode();
-
-		public void run() {
-			long end = System.currentTimeMillis() + INVITATION_TIMEOUT;
-			// Use the invitation code to seed the PRNG
-			int code = getInvitationCode();
-			if(code == -1) return; // Cancelled
-			PseudoRandom r = crypto.getPseudoRandom(code);
-			long timeout = end - System.currentTimeMillis();
-			if(timeout <= 0) {
-				callback.showFailure(TIMED_OUT);
-				return;
-			}
-			// Create a connection
-			DuplexTransportConnection conn;
-			if(initiator) conn = plugin.sendInvitation(r, timeout);
-			else conn = plugin.acceptInvitation(r, timeout);
-			if(callback.isCancelled()) {
-				if(conn != null) conn.dispose(false, false);
-				return;
-			}
-			if(conn == null) {
-				callback.showFailure(TIMED_OUT);
-				return;
-			}
-			// Use an ephemeral key pair for key agreement
-			KeyPair ourKeyPair = crypto.generateAgreementKeyPair();
-			MessageDigest messageDigest = crypto.getMessageDigest();
-			byte[] ourKey = ourKeyPair.getPublic().getEncoded();
-			byte[] ourHash = messageDigest.digest(ourKey);
-			byte[] theirKey, theirHash;
-			try {
-				OutputStream out = conn.getOutputStream();
-				Writer writer = writerFactory.createWriter(out);
-				InputStream in = conn.getInputStream();
-				Reader reader = readerFactory.createReader(in);
-				if(initiator) {
-					// Send the public key hash
-					writer.writeBytes(ourHash);
-					out.flush();
-					// Receive the public key hash
-					theirHash = reader.readBytes(HASH_LENGTH);
-					// Send the public key
-					writer.writeBytes(ourKey);
-					out.flush();
-					// Receive the public key
-					theirKey = reader.readBytes(MAX_PUBLIC_KEY_LENGTH);
-				} else {
-					// Receive the public key hash
-					theirHash = reader.readBytes(HASH_LENGTH);
-					// Send the public key hash
-					writer.writeBytes(ourHash);
-					out.flush();
-					// Receive the public key
-					theirKey = reader.readBytes(MAX_PUBLIC_KEY_LENGTH);
-					// Send the public key
-					writer.writeBytes(ourKey);
-					out.flush();
-				}
-			} catch(IOException e) {
-				conn.dispose(true, false);
-				callback.showFailure(IO_EXCEPTION);
-				return;
-			}
-			conn.dispose(false, false);
-			if(callback.isCancelled()) return;
-			// Check that the received hash matches the received key
-			if(!Arrays.equals(theirHash, messageDigest.digest(theirKey))) {
-				callback.showFailure(INVALID_KEY);
-				return;
-			}
-			// Derive the initial shared secrets and the confirmation codes
-			byte[][] secrets = crypto.deriveInitialSecrets(ourKey, theirKey,
-					ourKeyPair.getPrivate(), code, initiator);
-			if(secrets == null) {
-				callback.showFailure(INVALID_KEY);
-				return;
-			}
-			int initCode = crypto.deriveConfirmationCode(secrets[0]);
-			int respCode = crypto.deriveConfirmationCode(secrets[1]);
-			int ourCode = initiator ? initCode : respCode;
-			int theirCode = initiator ? respCode : initCode;
-			// Compare the confirmation codes
-			if(callback.enterConfirmationCode(ourCode) != theirCode) {
-				callback.showFailure(WRONG_CODE);
-				ByteUtils.erase(secrets[0]);
-				ByteUtils.erase(secrets[1]);
-				return;
-			}
-			// Add the contact to the database
-			byte[] inSecret = initiator ? secrets[1] : secrets[0];
-			byte[] outSecret = initiator ? secrets[0] : secrets[1];
-			try {
-				db.addContact(inSecret, outSecret);
-			} catch(DbException e) {
-				callback.showFailure(DB_EXCEPTION);
-				ByteUtils.erase(secrets[0]);
-				ByteUtils.erase(secrets[1]);
-				return;
-			}
-			callback.showSuccess();
-		}
-	}
-
-	private class IncomingInvitationWorker extends InvitationWorker {
-
-		private final IncomingInvitationCallback callback;
-
-		IncomingInvitationWorker(DuplexPlugin plugin,
-				IncomingInvitationCallback callback) {
-			super(plugin, callback, false);
-			this.callback = callback;
-		}
-
-		@Override
-		protected int getInvitationCode() {
-			return callback.enterInvitationCode();
-		}
-	}
-
-	private class OutgoingInvitationWorker extends InvitationWorker {
-
-		private final OutgoingInvitationCallback callback;
-
-		OutgoingInvitationWorker(DuplexPlugin plugin,
-				OutgoingInvitationCallback callback) {
-			super(plugin, callback, true);
-			this.callback = callback;
-		}
-
-		@Override
-		protected int getInvitationCode() {
-			int code = crypto.getSecureRandom().nextInt(MAX_CODE + 1);
-			callback.showInvitationCode(code);
-			return code;
-		}
-	}
-}
diff --git a/components/net/sf/briar/plugins/PluginManagerImpl.java b/components/net/sf/briar/plugins/PluginManagerImpl.java
index ab05b6de48f47c5fe0bec1a36ec412a67c3f0c4e..79782b912030a70a9c6b27256a78abb0d6dd5151 100644
--- a/components/net/sf/briar/plugins/PluginManagerImpl.java
+++ b/components/net/sf/briar/plugins/PluginManagerImpl.java
@@ -32,7 +32,6 @@ import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionDispatcher;
 import net.sf.briar.api.ui.UiCallback;
 
@@ -102,14 +101,7 @@ class PluginManagerImpl implements PluginManager {
 						LOG.warning("Duplicate transport ID: " + id);
 					continue;
 				}
-				TransportIndex index = db.getLocalIndex(id);
-				if(index == null) index = db.addTransport(id);
-				if(index == null) {
-					if(LOG.isLoggable(Level.WARNING))
-						LOG.warning("Could not allocate index for ID: " + id);
-					continue;
-				}
-				callback.init(id, index);
+				callback.init(id);
 				plugin.start();
 				simplexPlugins.add(plugin);
 			} catch(ClassCastException e) {
@@ -142,14 +134,7 @@ class PluginManagerImpl implements PluginManager {
 						LOG.warning("Duplicate transport ID: " + id);
 					continue;
 				}
-				TransportIndex index = db.getLocalIndex(id);
-				if(index == null) index = db.addTransport(id);
-				if(index == null) {
-					if(LOG.isLoggable(Level.WARNING))
-						LOG.warning("Could not allocate index for ID: " + id);
-					continue;
-				}
-				callback.init(id, index);
+				callback.init(id);
 				plugin.start();
 				duplexPlugins.add(plugin);
 			} catch(ClassCastException e) {
@@ -222,12 +207,10 @@ class PluginManagerImpl implements PluginManager {
 	private abstract class PluginCallbackImpl implements PluginCallback {
 
 		protected volatile TransportId id = null;
-		protected volatile TransportIndex index = null;
 
-		protected void init(TransportId id, TransportIndex index) {
-			assert this.id == null && this.index == null;
+		protected void init(TransportId id) {
+			assert this.id == null;
 			this.id = id;
-			this.index = index;
 		}
 
 		public TransportConfig getConfig() {
@@ -320,8 +303,7 @@ class PluginManagerImpl implements PluginManager {
 		}
 
 		public void writerCreated(ContactId c, SimplexTransportWriter w) {
-			assert index != null;
-			dispatcher.dispatchWriter(c, id, index, w);
+			dispatcher.dispatchWriter(c, id, w);
 		}
 	}
 
@@ -335,8 +317,7 @@ class PluginManagerImpl implements PluginManager {
 
 		public void outgoingConnectionCreated(ContactId c,
 				DuplexTransportConnection d) {
-			assert index != null;
-			dispatcher.dispatchOutgoingConnection(c, id, index, d);
+			dispatcher.dispatchOutgoingConnection(c, id, d);
 		}
 	}
 }
\ No newline at end of file
diff --git a/components/net/sf/briar/plugins/email/GmailPlugin.java b/components/net/sf/briar/plugins/email/GmailPlugin.java
index 323afd885df4da6cec3a0efb10a2c2c1059f348d..32bda238e56ebdf5df4c9ef0c4e0d42012535b34 100644
--- a/components/net/sf/briar/plugins/email/GmailPlugin.java
+++ b/components/net/sf/briar/plugins/email/GmailPlugin.java
@@ -30,7 +30,6 @@ import javax.mail.internet.MimeMessage;
 import javax.mail.internet.MimeMultipart;
 import javax.mail.search.FlagTerm;
 import javax.mail.util.ByteArrayDataSource;
-import javax.microedition.io.StreamConnection;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.TransportConfig;
diff --git a/components/net/sf/briar/plugins/email/PipeDataSource.java b/components/net/sf/briar/plugins/email/PipeDataSource.java
deleted file mode 100644
index 06f23cd896521979a5393bb7a4b78f46e390e657..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/plugins/email/PipeDataSource.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.sf.briar.plugins.email;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.PipedInputStream;
-import java.io.PipedOutputStream;
-
-import javax.activation.DataSource;
-
-public class PipeDataSource implements DataSource{
-
-	public String getContentType() {
-		return "application/octet-stream";
-	}
-
-	public PipedInputStream getInputStream() throws IOException {
-		return null;
-	}
-
-	public String getName() {
-		return "foo";
-	}
-
-	public PipedOutputStream getOutputStream() throws UnsupportedOperationException {
-		return null;
-	}
-
-	
-
-}
diff --git a/components/net/sf/briar/protocol/ProtocolWriterImpl.java b/components/net/sf/briar/protocol/ProtocolWriterImpl.java
index 862ee880bd584fa6e728d8388e9da0781e54d434..d7bebf4e2e64c5edb3b39aa82e735ab919272605 100644
--- a/components/net/sf/briar/protocol/ProtocolWriterImpl.java
+++ b/components/net/sf/briar/protocol/ProtocolWriterImpl.java
@@ -148,7 +148,6 @@ class ProtocolWriterImpl implements ProtocolWriter {
 		for(Transport p : t.getTransports()) {
 			w.writeStructId(Types.TRANSPORT);
 			w.writeBytes(p.getId().getBytes());
-			w.writeInt32(p.getIndex().getInt());
 			w.writeMap(p);
 		}
 		w.writeListEnd();
diff --git a/components/net/sf/briar/protocol/TransportUpdateReader.java b/components/net/sf/briar/protocol/TransportUpdateReader.java
index 63e6b899c402a258e6baebb3be203bae51cd7648..685a00b1463273740428a9572d56e0fe7357564b 100644
--- a/components/net/sf/briar/protocol/TransportUpdateReader.java
+++ b/components/net/sf/briar/protocol/TransportUpdateReader.java
@@ -15,14 +15,13 @@ import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.PacketFactory;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.Types;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.serial.Consumer;
 import net.sf.briar.api.serial.CountingConsumer;
-import net.sf.briar.api.serial.StructReader;
 import net.sf.briar.api.serial.Reader;
+import net.sf.briar.api.serial.StructReader;
 
 class TransportUpdateReader implements StructReader<TransportUpdate> {
 
@@ -46,12 +45,10 @@ class TransportUpdateReader implements StructReader<TransportUpdate> {
 		if(transports.size() > MAX_TRANSPORTS) throw new FormatException();
 		long timestamp = r.readInt64();
 		r.removeConsumer(counting);
-		// Check for duplicate IDs or indices
+		// Check for duplicate IDs
 		Set<TransportId> ids = new HashSet<TransportId>();
-		Set<TransportIndex> indices = new HashSet<TransportIndex>();
 		for(Transport t : transports) {
 			if(!ids.add(t.getId())) throw new FormatException();
-			if(!indices.add(t.getIndex())) throw new FormatException();
 		}
 		// Build and return the transport update
 		return packetFactory.createTransportUpdate(transports, timestamp);
@@ -65,17 +62,13 @@ class TransportUpdateReader implements StructReader<TransportUpdate> {
 			byte[] b = r.readBytes(UniqueId.LENGTH);
 			if(b.length != UniqueId.LENGTH) throw new FormatException();
 			TransportId id = new TransportId(b);
-			// Read the index
-			int i = r.readInt32();
-			if(i < 0 || i >= MAX_TRANSPORTS) throw new FormatException();
-			TransportIndex index = new TransportIndex(i);
 			// Read the properties
 			r.setMaxStringLength(MAX_PROPERTY_LENGTH);
 			Map<String, String> m = r.readMap(String.class, String.class);
 			r.resetMaxStringLength();
 			if(m.size() > MAX_PROPERTIES_PER_TRANSPORT)
 				throw new FormatException();
-			return new Transport(id, index, m);
+			return new Transport(id, m);
 		}
 	}
 }
diff --git a/components/net/sf/briar/protocol/duplex/DuplexConnection.java b/components/net/sf/briar/protocol/duplex/DuplexConnection.java
index bb172db2804891122e528474d30c35d0e5cc087a..6cb64768d3a9cd8a259f0c73dce4bdbfe8b28779 100644
--- a/components/net/sf/briar/protocol/duplex/DuplexConnection.java
+++ b/components/net/sf/briar/protocol/duplex/DuplexConnection.java
@@ -45,6 +45,7 @@ import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.UnverifiedBatch;
 import net.sf.briar.api.protocol.VerificationExecutor;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
 import net.sf.briar.api.transport.ConnectionRegistry;
@@ -54,7 +55,7 @@ import net.sf.briar.api.transport.ConnectionWriterFactory;
 abstract class DuplexConnection implements DatabaseListener {
 
 	private static final Logger LOG =
-		Logger.getLogger(DuplexConnection.class.getName());
+			Logger.getLogger(DuplexConnection.class.getName());
 
 	private static final Runnable CLOSE = new Runnable() {
 		public void run() {}
@@ -66,9 +67,10 @@ abstract class DuplexConnection implements DatabaseListener {
 	protected final ConnectionWriterFactory connWriterFactory;
 	protected final ProtocolReaderFactory protoReaderFactory;
 	protected final ProtocolWriterFactory protoWriterFactory;
+	protected final ConnectionContext ctx;
+	protected final DuplexTransportConnection transport;
 	protected final ContactId contactId;
 	protected final TransportId transportId;
-	protected final DuplexTransportConnection transport;
 
 	private final Executor dbExecutor, verificationExecutor;
 	private final AtomicBoolean canSendOffer, disposed;
@@ -84,8 +86,8 @@ abstract class DuplexConnection implements DatabaseListener {
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
-			ProtocolWriterFactory protoWriterFactory, ContactId contactId,
-			TransportId transportId, DuplexTransportConnection transport) {
+			ProtocolWriterFactory protoWriterFactory, ConnectionContext ctx,
+			DuplexTransportConnection transport) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
 		this.db = db;
@@ -94,19 +96,20 @@ abstract class DuplexConnection implements DatabaseListener {
 		this.connWriterFactory = connWriterFactory;
 		this.protoReaderFactory = protoReaderFactory;
 		this.protoWriterFactory = protoWriterFactory;
-		this.contactId = contactId;
-		this.transportId = transportId;
+		this.ctx = ctx;
 		this.transport = transport;
+		contactId = ctx.getContactId();
+		transportId = ctx.getTransportId();
 		canSendOffer = new AtomicBoolean(false);
 		disposed = new AtomicBoolean(false);
 		writerTasks = new LinkedBlockingQueue<Runnable>();
 	}
 
 	protected abstract ConnectionReader createConnectionReader()
-	throws DbException, IOException;
+			throws IOException;
 
 	protected abstract ConnectionWriter createConnectionWriter()
-	throws DbException, IOException;
+			throws IOException;
 
 	public void eventOccurred(DatabaseEvent e) {
 		if(e instanceof BatchReceivedEvent) {
@@ -121,7 +124,7 @@ abstract class DuplexConnection implements DatabaseListener {
 				dbExecutor.execute(new GenerateOffer());
 		} else if(e instanceof SubscriptionsUpdatedEvent) {
 			Collection<ContactId> affected =
-				((SubscriptionsUpdatedEvent) e).getAffectedContacts();
+					((SubscriptionsUpdatedEvent) e).getAffectedContacts();
 			if(affected.contains(contactId)) {
 				dbExecutor.execute(new GenerateSubscriptionUpdate());
 			}
@@ -176,9 +179,6 @@ abstract class DuplexConnection implements DatabaseListener {
 			}
 			// The writer will dispose of the transport
 			writerTasks.add(CLOSE);
-		} catch(DbException e) {
-			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
-			if(!disposed.getAndSet(true)) transport.dispose(true, true);
 		} catch(IOException e) {
 			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
 			if(!disposed.getAndSet(true)) transport.dispose(true, true);
@@ -217,9 +217,6 @@ abstract class DuplexConnection implements DatabaseListener {
 			writer.flush();
 			writer.close();
 			if(!disposed.getAndSet(true)) transport.dispose(false, true);
-		} catch(DbException e) {
-			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
-			if(!disposed.getAndSet(true)) transport.dispose(true, true);
 		} catch(InterruptedException e) {
 			if(LOG.isLoggable(Level.INFO))
 				LOG.info("Interrupted while waiting for task");
diff --git a/components/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java b/components/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java
index ed4f08f1fb52b2f332f5e5c8852c0a8e105a48bb..73aa74d16c7a2a2d54412505bdf93ecdc09df965 100644
--- a/components/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java
+++ b/components/net/sf/briar/protocol/duplex/DuplexConnectionFactoryImpl.java
@@ -1,15 +1,17 @@
 package net.sf.briar.protocol.duplex;
 
 import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.KeyManager;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.protocol.duplex.DuplexConnectionFactory;
 import net.sf.briar.api.transport.ConnectionContext;
@@ -21,8 +23,12 @@ import com.google.inject.Inject;
 
 class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 
+	private static final Logger LOG =
+			Logger.getLogger(DuplexConnectionFactoryImpl.class.getName());
+
 	private final Executor dbExecutor, verificationExecutor;
 	private final DatabaseComponent db;
+	private final KeyManager keyManager;
 	private final ConnectionRegistry connRegistry;
 	private final ConnectionReaderFactory connReaderFactory;
 	private final ConnectionWriterFactory connWriterFactory;
@@ -32,14 +38,15 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 	@Inject
 	DuplexConnectionFactoryImpl(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, ConnectionRegistry connRegistry,
+			DatabaseComponent db, KeyManager keyManager,
+			ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
-			ProtocolReaderFactory protoReaderFactory,
-			ProtocolWriterFactory protoWriterFactory) {
+			ProtocolReaderFactory protoReaderFactory, ProtocolWriterFactory protoWriterFactory) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
 		this.db = db;
+		this.keyManager = keyManager;
 		this.connRegistry = connRegistry;
 		this.connReaderFactory = connReaderFactory;
 		this.connWriterFactory = connWriterFactory;
@@ -47,12 +54,12 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 		this.protoWriterFactory = protoWriterFactory;
 	}
 
-	public void createIncomingConnection(ConnectionContext ctx, TransportId t,
-			DuplexTransportConnection d) {
+	public void createIncomingConnection(ConnectionContext ctx,
+			DuplexTransportConnection transport) {
 		final DuplexConnection conn = new IncomingDuplexConnection(dbExecutor,
 				verificationExecutor, db, connRegistry, connReaderFactory,
-				connWriterFactory, protoReaderFactory, protoWriterFactory,
-				ctx, t, d);
+				connWriterFactory, protoReaderFactory, protoWriterFactory, ctx,
+				transport);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
@@ -68,11 +75,17 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 	}
 
 	public void createOutgoingConnection(ContactId c, TransportId t,
-			TransportIndex i, DuplexTransportConnection d) {
+			DuplexTransportConnection transport) {
+		ConnectionContext ctx = keyManager.getConnectionContext(c, t);
+		if(ctx == null) {
+			if(LOG.isLoggable(Level.WARNING))
+				LOG.warning("Could not create outgoing connection context");
+			return;
+		}
 		final DuplexConnection conn = new OutgoingDuplexConnection(dbExecutor,
 				verificationExecutor, db, connRegistry, connReaderFactory,
-				connWriterFactory, protoReaderFactory, protoWriterFactory,
-				c, t, i, d);
+				connWriterFactory, protoReaderFactory, protoWriterFactory, ctx,
+				transport);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
@@ -86,5 +99,4 @@ class DuplexConnectionFactoryImpl implements DuplexConnectionFactory {
 		};
 		new Thread(read).start();
 	}
-
 }
diff --git a/components/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java b/components/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java
index 3a888eaa7fa5f487830fe53111119449762c921a..75fc8932aa650146def75c0ea84291baab60464d 100644
--- a/components/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java
+++ b/components/net/sf/briar/protocol/duplex/IncomingDuplexConnection.java
@@ -8,7 +8,6 @@ import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.TransportId;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
@@ -28,24 +27,22 @@ class IncomingDuplexConnection extends DuplexConnection {
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
 			ProtocolWriterFactory protoWriterFactory,
-			ConnectionContext ctx, TransportId transportId,
-			DuplexTransportConnection transport) {
+			ConnectionContext ctx, DuplexTransportConnection transport) {
 		super(dbExecutor, verificationExecutor, db, connRegistry,
 				connReaderFactory, connWriterFactory, protoReaderFactory,
-				protoWriterFactory, ctx.getContactId(), transportId, transport);
+				protoWriterFactory, ctx, transport);
 		this.ctx = ctx;
 	}
 
 	@Override
 	protected ConnectionReader createConnectionReader() throws IOException {
 		return connReaderFactory.createConnectionReader(
-				transport.getInputStream(), ctx.getSecret(), true);
+				transport.getInputStream(), ctx, true);
 	}
 
 	@Override
 	protected ConnectionWriter createConnectionWriter() throws IOException {
 		return connWriterFactory.createConnectionWriter(
-				transport.getOutputStream(), Long.MAX_VALUE, ctx.getSecret(),
-				false);
+				transport.getOutputStream(), Long.MAX_VALUE, ctx, false);
 	}
 }
diff --git a/components/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java b/components/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java
index b784b8115eda9f7c67273e5dd6f6ad51cd2d7c4c..e3f9c8c723c0a97109e03501f2bc3786706f7330 100644
--- a/components/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java
+++ b/components/net/sf/briar/protocol/duplex/OutgoingDuplexConnection.java
@@ -3,15 +3,11 @@ package net.sf.briar.protocol.duplex;
 import java.io.IOException;
 import java.util.concurrent.Executor;
 
-import net.sf.briar.api.ContactId;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
-import net.sf.briar.api.db.DbException;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
-import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
@@ -22,45 +18,28 @@ import net.sf.briar.api.transport.ConnectionWriterFactory;
 
 class OutgoingDuplexConnection extends DuplexConnection {
 
-	private final TransportIndex transportIndex;
-
-	private ConnectionContext ctx = null; // Locking: this
-
 	OutgoingDuplexConnection(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
 			DatabaseComponent db, ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
-			ProtocolWriterFactory protoWriterFactory, ContactId contactId,
-			TransportId transportId, TransportIndex transportIndex,
+			ProtocolWriterFactory protoWriterFactory, ConnectionContext ctx,
 			DuplexTransportConnection transport) {
 		super(dbExecutor, verificationExecutor, db, connRegistry,
 				connReaderFactory, connWriterFactory, protoReaderFactory,
-				protoWriterFactory, contactId, transportId, transport);
-		this.transportIndex = transportIndex;
+				protoWriterFactory, ctx, transport);
 	}
 
 	@Override
-	protected ConnectionReader createConnectionReader() throws DbException,
-	IOException {
-		synchronized(this) {
-			if(ctx == null)
-				ctx = db.getConnectionContext(contactId, transportIndex);
-		}
+	protected ConnectionReader createConnectionReader() throws IOException {
 		return connReaderFactory.createConnectionReader(
-				transport.getInputStream(), ctx.getSecret(), false);
+				transport.getInputStream(), ctx, false);
 	}
 
 	@Override
-	protected ConnectionWriter createConnectionWriter() throws DbException,
-	IOException {
-		synchronized(this) {
-			if(ctx == null)
-				ctx = db.getConnectionContext(contactId, transportIndex);
-		}
+	protected ConnectionWriter createConnectionWriter() throws IOException {
 		return connWriterFactory.createConnectionWriter(
-				transport.getOutputStream(), Long.MAX_VALUE, ctx.getSecret(),
-				true);
+				transport.getOutputStream(), Long.MAX_VALUE, ctx, true);
 	}
 }
diff --git a/components/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java b/components/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java
index a15c7bd8dd80d60a3adea1d2f09cec1613ac79da..e83b36e1bf71ade162cc4b265a6562683cd2eea5 100644
--- a/components/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java
+++ b/components/net/sf/briar/protocol/simplex/IncomingSimplexConnection.java
@@ -30,7 +30,7 @@ import net.sf.briar.api.transport.ConnectionRegistry;
 class IncomingSimplexConnection {
 
 	private static final Logger LOG =
-		Logger.getLogger(IncomingSimplexConnection.class.getName());
+			Logger.getLogger(IncomingSimplexConnection.class.getName());
 
 	private final Executor dbExecutor, verificationExecutor;
 	private final DatabaseComponent db;
@@ -38,16 +38,16 @@ class IncomingSimplexConnection {
 	private final ConnectionReaderFactory connFactory;
 	private final ProtocolReaderFactory protoFactory;
 	private final ConnectionContext ctx;
-	private final TransportId transportId;
 	private final SimplexTransportReader transport;
 	private final ContactId contactId;
+	private final TransportId transportId;
 
 	IncomingSimplexConnection(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
 			DatabaseComponent db, ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connFactory,
 			ProtocolReaderFactory protoFactory, ConnectionContext ctx,
-			TransportId transportId, SimplexTransportReader transport) {
+			SimplexTransportReader transport) {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
 		this.db = db;
@@ -55,16 +55,16 @@ class IncomingSimplexConnection {
 		this.connFactory = connFactory;
 		this.protoFactory = protoFactory;
 		this.ctx = ctx;
-		this.transportId = transportId;
 		this.transport = transport;
 		contactId = ctx.getContactId();
+		transportId = ctx.getTransportId();
 	}
 
 	void read() {
 		connRegistry.registerConnection(contactId, transportId);
 		try {
 			ConnectionReader conn = connFactory.createConnectionReader(
-					transport.getInputStream(), ctx.getSecret(), true);
+					transport.getInputStream(), ctx, true);
 			InputStream in = conn.getInputStream();
 			ProtocolReader reader = protoFactory.createProtocolReader(in);
 			// Read packets until EOF
diff --git a/components/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java b/components/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java
index 6333a700ed8de6937a866d5e3d69e05b6bbdbac4..fc0e926e4d886420d4c43475380cb016ec7ac2fc 100644
--- a/components/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java
+++ b/components/net/sf/briar/protocol/simplex/OutgoingSimplexConnection.java
@@ -18,7 +18,6 @@ import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionRegistry;
@@ -34,35 +33,32 @@ class OutgoingSimplexConnection {
 	private final ConnectionRegistry connRegistry;
 	private final ConnectionWriterFactory connFactory;
 	private final ProtocolWriterFactory protoFactory;
+	private final ConnectionContext ctx;
+	private final SimplexTransportWriter transport;
 	private final ContactId contactId;
 	private final TransportId transportId;
-	private final TransportIndex transportIndex;
-	private final SimplexTransportWriter transport;
 
 	OutgoingSimplexConnection(DatabaseComponent db,
 			ConnectionRegistry connRegistry,
 			ConnectionWriterFactory connFactory,
-			ProtocolWriterFactory protoFactory, ContactId contactId,
-			TransportId transportId, TransportIndex transportIndex,
+			ProtocolWriterFactory protoFactory, ConnectionContext ctx,
 			SimplexTransportWriter transport) {
 		this.db = db;
 		this.connRegistry = connRegistry;
 		this.connFactory = connFactory;
 		this.protoFactory = protoFactory;
-		this.contactId = contactId;
-		this.transportId = transportId;
-		this.transportIndex = transportIndex;
+		this.ctx = ctx;
 		this.transport = transport;
+		contactId = ctx.getContactId();
+		transportId = ctx.getTransportId();
 	}
 
 	void write() {
 		connRegistry.registerConnection(contactId, transportId);
 		try {
-			ConnectionContext ctx = db.getConnectionContext(contactId,
-					transportIndex);
 			ConnectionWriter conn = connFactory.createConnectionWriter(
 					transport.getOutputStream(), transport.getCapacity(),
-					ctx.getSecret(), true);
+					ctx, true);
 			OutputStream out = conn.getOutputStream();
 			ProtocolWriter writer = protoFactory.createProtocolWriter(out,
 					transport.shouldFlush());
diff --git a/components/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java b/components/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java
index 1c08ffe8942e1658f31c7f47065e90b4418fc7a7..c20d8b15bd601ae54ce25f66bac56d5ce70dd1f8 100644
--- a/components/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java
+++ b/components/net/sf/briar/protocol/simplex/SimplexConnectionFactoryImpl.java
@@ -1,8 +1,11 @@
 package net.sf.briar.protocol.simplex;
 
 import java.util.concurrent.Executor;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.KeyManager;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
@@ -10,7 +13,6 @@ import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.VerificationExecutor;
 import net.sf.briar.api.protocol.simplex.SimplexConnectionFactory;
 import net.sf.briar.api.transport.ConnectionContext;
@@ -22,8 +24,12 @@ import com.google.inject.Inject;
 
 class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 
+	private static final Logger LOG =
+			Logger.getLogger(SimplexConnectionFactoryImpl.class.getName());
+
 	private final Executor dbExecutor, verificationExecutor;
 	private final DatabaseComponent db;
+	private final KeyManager keyManager;
 	private final ConnectionRegistry connRegistry;
 	private final ConnectionReaderFactory connReaderFactory;
 	private final ConnectionWriterFactory connWriterFactory;
@@ -33,7 +39,8 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 	@Inject
 	SimplexConnectionFactoryImpl(@DatabaseExecutor Executor dbExecutor,
 			@VerificationExecutor Executor verificationExecutor,
-			DatabaseComponent db, ConnectionRegistry connRegistry,
+			DatabaseComponent db, KeyManager keyManager,
+			ConnectionRegistry connRegistry,
 			ConnectionReaderFactory connReaderFactory,
 			ConnectionWriterFactory connWriterFactory,
 			ProtocolReaderFactory protoReaderFactory,
@@ -41,6 +48,7 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 		this.dbExecutor = dbExecutor;
 		this.verificationExecutor = verificationExecutor;
 		this.db = db;
+		this.keyManager = keyManager;
 		this.connRegistry = connRegistry;
 		this.connReaderFactory = connReaderFactory;
 		this.connWriterFactory = connWriterFactory;
@@ -48,11 +56,10 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 		this.protoWriterFactory = protoWriterFactory;
 	}
 
-	public void createIncomingConnection(ConnectionContext ctx, TransportId t,
-			SimplexTransportReader r) {
+	public void createIncomingConnection(ConnectionContext ctx, SimplexTransportReader r) {
 		final IncomingSimplexConnection conn = new IncomingSimplexConnection(
 				dbExecutor, verificationExecutor, db, connRegistry,
-				connReaderFactory, protoReaderFactory, ctx, t, r);
+				connReaderFactory, protoReaderFactory, ctx, r);
 		Runnable read = new Runnable() {
 			public void run() {
 				conn.read();
@@ -62,10 +69,15 @@ class SimplexConnectionFactoryImpl implements SimplexConnectionFactory {
 	}
 
 	public void createOutgoingConnection(ContactId c, TransportId t,
-			TransportIndex i, SimplexTransportWriter w) {
+			SimplexTransportWriter w) {
+		ConnectionContext ctx = keyManager.getConnectionContext(c, t);
+		if(ctx == null) {
+			if(LOG.isLoggable(Level.WARNING))
+				LOG.warning("Could not create outgoing connection context");
+			return;
+		}		
 		final OutgoingSimplexConnection conn = new OutgoingSimplexConnection(db,
-				connRegistry, connWriterFactory, protoWriterFactory,
-				c, t, i, w);
+				connRegistry, connWriterFactory, protoWriterFactory, ctx, w);
 		Runnable write = new Runnable() {
 			public void run() {
 				conn.write();
diff --git a/components/net/sf/briar/transport/ConnectionContextFactoryImpl.java b/components/net/sf/briar/transport/ConnectionContextFactoryImpl.java
deleted file mode 100644
index f50d5193c6e79221fc1cdd57c5a8f763209e81d1..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/ConnectionContextFactoryImpl.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package net.sf.briar.transport;
-
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionContextFactory;
-
-import com.google.inject.Inject;
-
-class ConnectionContextFactoryImpl implements ConnectionContextFactory {
-
-	private final CryptoComponent crypto;
-
-	@Inject
-	ConnectionContextFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
-	}
-
-	public ConnectionContext createConnectionContext(ContactId c,
-			TransportIndex i, long connection, byte[] secret) {
-		return new ConnectionContextImpl(c, i, connection, secret);
-	}
-
-	public ConnectionContext createNextConnectionContext(ContactId c,
-			TransportIndex i, long connection, byte[] previousSecret) {
-		byte[] secret = crypto.deriveNextSecret(previousSecret, i.getInt(),
-				connection);
-		return new ConnectionContextImpl(c, i, connection, secret);
-	}
-}
diff --git a/components/net/sf/briar/transport/ConnectionContextImpl.java b/components/net/sf/briar/transport/ConnectionContextImpl.java
deleted file mode 100644
index 27e8caee68a3680890f329aaf3cb5e4fd14f3c81..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/ConnectionContextImpl.java
+++ /dev/null
@@ -1,37 +0,0 @@
-package net.sf.briar.transport;
-
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContext;
-
-class ConnectionContextImpl implements ConnectionContext {
-
-	private final ContactId contactId;
-	private final TransportIndex transportIndex;
-	private final long connectionNumber;
-	private final byte[] secret;
-
-	ConnectionContextImpl(ContactId contactId, TransportIndex transportIndex,
-			long connectionNumber, byte[] secret) {
-		this.contactId = contactId;
-		this.transportIndex = transportIndex;
-		this.connectionNumber = connectionNumber;
-		this.secret = secret;
-	}
-
-	public ContactId getContactId() {
-		return contactId;
-	}
-
-	public TransportIndex getTransportIndex() {
-		return transportIndex;
-	}
-
-	public long getConnectionNumber() {
-		return connectionNumber;
-	}
-
-	public byte[] getSecret() {
-		return secret;
-	}
-}
diff --git a/components/net/sf/briar/transport/ConnectionDispatcherImpl.java b/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
index 1ac49b86b79ad9a37bf4c2831bed09518578fb32..2eb5849de53c9cd0f879baddf877e970eb95b84d 100644
--- a/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
+++ b/components/net/sf/briar/transport/ConnectionDispatcherImpl.java
@@ -13,7 +13,6 @@ import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.duplex.DuplexConnectionFactory;
 import net.sf.briar.api.protocol.simplex.SimplexConnectionFactory;
 import net.sf.briar.api.transport.ConnectionContext;
@@ -31,27 +30,27 @@ class ConnectionDispatcherImpl implements ConnectionDispatcher {
 
 	private final Executor connExecutor;
 	private final ConnectionRecogniser recogniser;
-	private final SimplexConnectionFactory batchConnFactory;
-	private final DuplexConnectionFactory streamConnFactory;
+	private final SimplexConnectionFactory simplexConnFactory;
+	private final DuplexConnectionFactory duplexConnFactory;
 
 	@Inject
 	ConnectionDispatcherImpl(@IncomingConnectionExecutor Executor connExecutor,
 			ConnectionRecogniser recogniser,
-			SimplexConnectionFactory batchConnFactory,
-			DuplexConnectionFactory streamConnFactory) {
+			SimplexConnectionFactory simplexConnFactory,
+			DuplexConnectionFactory duplexConnFactory) {
 		this.connExecutor = connExecutor;
 		this.recogniser = recogniser;
-		this.batchConnFactory = batchConnFactory;
-		this.streamConnFactory = streamConnFactory;
+		this.simplexConnFactory = simplexConnFactory;
+		this.duplexConnFactory = duplexConnFactory;
 	}
 
 	public void dispatchReader(TransportId t, SimplexTransportReader r) {
 		connExecutor.execute(new DispatchSimplexConnection(t, r));
 	}
 
-	public void dispatchWriter(ContactId c, TransportId t, TransportIndex i,
+	public void dispatchWriter(ContactId c, TransportId t,
 			SimplexTransportWriter w) {
-		batchConnFactory.createOutgoingConnection(c, t, i, w);
+		simplexConnFactory.createOutgoingConnection(c, t, w);
 	}
 
 	public void dispatchIncomingConnection(TransportId t,
@@ -60,8 +59,8 @@ class ConnectionDispatcherImpl implements ConnectionDispatcher {
 	}
 
 	public void dispatchOutgoingConnection(ContactId c, TransportId t,
-			TransportIndex i, DuplexTransportConnection d) {
-		streamConnFactory.createOutgoingConnection(c, t, i, d);
+			DuplexTransportConnection d) {
+		duplexConnFactory.createOutgoingConnection(c, t, d);
 	}
 
 	private byte[] readTag(InputStream in) throws IOException {
@@ -91,9 +90,12 @@ class ConnectionDispatcherImpl implements ConnectionDispatcher {
 				byte[] tag = readTag(transport.getInputStream());
 				ConnectionContext ctx = recogniser.acceptConnection(transportId,
 						tag);
-				if(ctx == null) transport.dispose(false, false);
-				else batchConnFactory.createIncomingConnection(ctx, transportId,
-						transport);
+				if(ctx == null) {
+					transport.dispose(false, false);
+				} else {
+					simplexConnFactory.createIncomingConnection(ctx,
+							transport);
+				}
 			} catch(DbException e) {
 				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
 				try {
@@ -128,9 +130,11 @@ class ConnectionDispatcherImpl implements ConnectionDispatcher {
 				byte[] tag = readTag(transport.getInputStream());
 				ConnectionContext ctx = recogniser.acceptConnection(transportId,
 						tag);
-				if(ctx == null) transport.dispose(false, false);
-				else streamConnFactory.createIncomingConnection(ctx,
-						transportId, transport);
+				if(ctx == null) {
+					transport.dispose(false, false);
+				} else {
+					duplexConnFactory.createIncomingConnection(ctx, transport);
+				}
 			} catch(DbException e) {
 				if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
 				transport.dispose(true, false);
diff --git a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
index fc951da24a9ab393516db85b478e25a1bf0f9e32..c6fae40198acdf33190d326bdc1094bcb962c26a 100644
--- a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
+++ b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
@@ -4,14 +4,11 @@ import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 
 import java.io.InputStream;
 
-import javax.crypto.Cipher;
-
-import net.sf.briar.api.crypto.AuthenticatedCipher;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
-import net.sf.briar.util.ByteUtils;
 
 import com.google.inject.Inject;
 
@@ -25,27 +22,14 @@ class ConnectionReaderFactoryImpl implements ConnectionReaderFactory {
 	}
 
 	public ConnectionReader createConnectionReader(InputStream in,
-			byte[] secret, boolean initiator) {
-		if(initiator) {
-			// Derive the frame key and erase the secret
-			ErasableKey frameKey = crypto.deriveFrameKey(secret, initiator);
-			ByteUtils.erase(secret);
-			// Create a reader for the responder's side of the connection
-			AuthenticatedCipher frameCipher = crypto.getFrameCipher();
-			FrameReader encryption = new IncomingEncryptionLayer(in,
-					frameCipher, frameKey, MAX_FRAME_LENGTH);
-			return new ConnectionReaderImpl(encryption, MAX_FRAME_LENGTH);
-		} else {
-			// Derive the tag and frame keys and erase the secret
-			ErasableKey tagKey = crypto.deriveTagKey(secret, initiator);
-			ErasableKey frameKey = crypto.deriveFrameKey(secret, initiator);
-			ByteUtils.erase(secret);
-			// Create a reader for the initiator's side of the connection
-			Cipher tagCipher = crypto.getTagCipher();
-			AuthenticatedCipher frameCipher = crypto.getFrameCipher();
-			FrameReader encryption = new IncomingEncryptionLayer(in, tagCipher,
-					frameCipher, tagKey, frameKey, MAX_FRAME_LENGTH);
-			return new ConnectionReaderImpl(encryption, MAX_FRAME_LENGTH);
-		}
+			ConnectionContext ctx, boolean initiator) {
+		byte[] secret = ctx.getSecret();
+		long connection = ctx.getConnectionNumber();
+		boolean alice = ctx.getAlice();
+		ErasableKey frameKey = crypto.deriveFrameKey(secret, connection, alice,
+				initiator);
+		FrameReader encryption = new IncomingEncryptionLayer(in,
+				crypto.getFrameCipher(), frameKey, MAX_FRAME_LENGTH);
+		return new ConnectionReaderImpl(encryption, MAX_FRAME_LENGTH);
 	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
deleted file mode 100644
index 5babc2278cd3c3f1d33e9fe9e53b2b4cb97f504c..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
+++ /dev/null
@@ -1,263 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.Executor;
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import javax.crypto.Cipher;
-
-import net.sf.briar.api.Bytes;
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.crypto.ErasableKey;
-import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.db.DbException;
-import net.sf.briar.api.db.NoSuchContactException;
-import net.sf.briar.api.db.event.ContactRemovedEvent;
-import net.sf.briar.api.db.event.DatabaseEvent;
-import net.sf.briar.api.db.event.DatabaseListener;
-import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
-import net.sf.briar.api.db.event.TransportAddedEvent;
-import net.sf.briar.api.protocol.Transport;
-import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionRecogniser;
-import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.api.transport.IncomingConnectionExecutor;
-import net.sf.briar.util.ByteUtils;
-
-import com.google.inject.Inject;
-
-class ConnectionRecogniserImpl implements ConnectionRecogniser,
-DatabaseListener {
-
-	private static final Logger LOG =
-		Logger.getLogger(ConnectionRecogniserImpl.class.getName());
-
-	private final Executor connExecutor;
-	private final DatabaseComponent db;
-	private final CryptoComponent crypto;
-	private final Cipher tagCipher; // Locking: this
-	private final Set<TransportId> localTransportIds; // Locking: this
-	private final Map<Bytes, Context> expected; // Locking: this
-
-	private boolean initialised = false; // Locking: this
-
-	@Inject
-	ConnectionRecogniserImpl(@IncomingConnectionExecutor Executor connExecutor,
-			DatabaseComponent db, CryptoComponent crypto) {
-		this.connExecutor = connExecutor;
-		this.db = db;
-		this.crypto = crypto;
-		tagCipher = crypto.getTagCipher();
-		localTransportIds = new HashSet<TransportId>();
-		expected = new HashMap<Bytes, Context>();
-	}
-
-	// Package access for testing
-	synchronized boolean isInitialised() {
-		return initialised;
-	}
-
-	// Locking: this
-	private void initialise() throws DbException {
-		assert !initialised;
-		db.addListener(this);
-		Map<Bytes, Context> ivs = new HashMap<Bytes, Context>();
-		Collection<TransportId> transports = new ArrayList<TransportId>();
-		for(Transport t : db.getLocalTransports()) transports.add(t.getId());
-		for(ContactId c : db.getContacts()) {
-			try {
-				for(TransportId t : transports) {
-					TransportIndex i = db.getRemoteIndex(c, t);
-					if(i == null) continue; // Contact doesn't support transport
-					ConnectionWindow w = db.getConnectionWindow(c, i);
-					for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
-						Context ctx = new Context(c, t, i, e.getKey());
-						ivs.put(calculateTag(ctx, e.getValue()), ctx);
-					}
-					w.erase();
-				}
-			} catch(NoSuchContactException e) {
-				// The contact was removed - clean up in removeContact()
-				continue;
-			}
-		}
-		localTransportIds.addAll(transports);
-		expected.putAll(ivs);
-		initialised = true;
-	}
-
-	// Locking: this
-	private Bytes calculateTag(Context ctx, byte[] secret) {
-		ErasableKey tagKey = crypto.deriveTagKey(secret, true);
-		byte[] tag = new byte[TAG_LENGTH];
-		TagEncoder.encodeTag(tag, tagCipher, tagKey);
-		tagKey.erase();
-		return new Bytes(tag);
-	}
-
-	public ConnectionContext acceptConnection(TransportId t, byte[] tag)
-	throws DbException {
-		if(tag.length != TAG_LENGTH)
-			throw new IllegalArgumentException();
-		synchronized(this) {
-			if(!initialised) initialise();
-			Bytes b = new Bytes(tag);
-			Context ctx = expected.get(b);
-			if(ctx == null || !ctx.transportId.equals(t)) return null;
-			// The IV was expected
-			expected.remove(b);
-			ContactId c = ctx.contactId;
-			TransportIndex i = ctx.transportIndex;
-			long connection = ctx.connection;
-			ConnectionWindow w = null;
-			byte[] secret = null;
-			// Get the secret and update the connection window
-			try {
-				w = db.getConnectionWindow(c, i);
-				secret = w.setSeen(connection);
-				db.setConnectionWindow(c, i, w);
-			} catch(NoSuchContactException e) {
-				// The contact was removed - reject the connection
-				if(w != null) w.erase();
-				if(secret != null) ByteUtils.erase(secret);
-				return null;
-			}
-			// Update the connection window's expected IVs
-			Iterator<Context> it = expected.values().iterator();
-			while(it.hasNext()) {
-				Context ctx1 = it.next();
-				if(ctx1.contactId.equals(c) && ctx1.transportIndex.equals(i))
-					it.remove();
-			}
-			for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
-				Context ctx1 = new Context(c, t, i, e.getKey());
-				expected.put(calculateTag(ctx1, e.getValue()), ctx1);
-			}
-			w.erase();
-			return new ConnectionContextImpl(c, i, connection, secret);
-		}
-	}
-
-	public void eventOccurred(DatabaseEvent e) {
-		if(e instanceof ContactRemovedEvent) {
-			// Remove the expected IVs for the ex-contact
-			final ContactId c = ((ContactRemovedEvent) e).getContactId();
-			connExecutor.execute(new Runnable() {
-				public void run() {
-					removeContact(c);
-				}
-			});
-		} else if(e instanceof TransportAddedEvent) {
-			// Add the expected IVs for the new transport
-			final TransportId t = ((TransportAddedEvent) e).getTransportId();
-			connExecutor.execute(new Runnable() {
-				public void run() {
-					addTransport(t);
-				}
-			});
-		} else if(e instanceof RemoteTransportsUpdatedEvent) {
-			// Update the expected IVs for the contact
-			RemoteTransportsUpdatedEvent r = (RemoteTransportsUpdatedEvent) e;
-			final ContactId c = r.getContactId();
-			final Collection<Transport> transports = r.getTransports();
-			connExecutor.execute(new Runnable() {
-				public void run() {
-					updateContact(c, transports);
-				}
-			});
-		}
-	}
-
-	private synchronized void removeContact(ContactId c) {
-		if(!initialised) return;
-		Iterator<Context> it = expected.values().iterator();
-		while(it.hasNext()) if(it.next().contactId.equals(c)) it.remove();
-	}
-
-	private synchronized void addTransport(TransportId t) {
-		if(!initialised) return;
-		Map<Bytes, Context> ivs = new HashMap<Bytes, Context>();
-		try {
-			for(ContactId c : db.getContacts()) {
-				try {
-					TransportIndex i = db.getRemoteIndex(c, t);
-					if(i == null) continue; // Contact doesn't support transport
-					ConnectionWindow w = db.getConnectionWindow(c, i);
-					for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
-						Context ctx = new Context(c, t, i, e.getKey());
-						ivs.put(calculateTag(ctx, e.getValue()), ctx);
-					}
-					w.erase();
-				} catch(NoSuchContactException e) {
-					// The contact was removed - clean up in removeContact()
-					continue;
-				}
-			}
-		} catch(DbException e) {
-			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
-			return;
-		}
-		localTransportIds.add(t);
-		expected.putAll(ivs);
-	}
-
-	private synchronized void updateContact(ContactId c,
-			Collection<Transport> transports) {
-		if(!initialised) return;
-		// The ID <-> index mappings may have changed, so recalculate everything
-		Map<Bytes, Context> ivs = new HashMap<Bytes, Context>();
-		try {
-			for(Transport transport: transports) {
-				TransportId t = transport.getId();
-				if(!localTransportIds.contains(t)) continue;
-				TransportIndex i = transport.getIndex();
-				ConnectionWindow w = db.getConnectionWindow(c, i);
-				for(Entry<Long, byte[]> e : w.getUnseen().entrySet()) {
-					Context ctx = new Context(c, t, i, e.getKey());
-					ivs.put(calculateTag(ctx, e.getValue()), ctx);
-				}
-				w.erase();
-			}
-		} catch(NoSuchContactException e) {
-			// The contact was removed - clean up in removeContact()
-			return;
-		} catch(DbException e) {
-			if(LOG.isLoggable(Level.WARNING)) LOG.warning(e.toString());
-			return;
-		}
-		// Remove the old IVs
-		Iterator<Context> it = expected.values().iterator();
-		while(it.hasNext()) if(it.next().contactId.equals(c)) it.remove();
-		// Store the new IVs
-		expected.putAll(ivs);
-	}
-
-	private static class Context {
-
-		private final ContactId contactId;
-		private final TransportId transportId;
-		private final TransportIndex transportIndex;
-		private final long connection;
-
-		private Context(ContactId contactId, TransportId transportId,
-				TransportIndex transportIndex, long connection) {
-			this.contactId = contactId;
-			this.transportId = transportId;
-			this.transportIndex = transportIndex;
-			this.connection = connection;
-		}
-	}
-}
diff --git a/components/net/sf/briar/transport/ConnectionWindowFactoryImpl.java b/components/net/sf/briar/transport/ConnectionWindowFactoryImpl.java
deleted file mode 100644
index 2890e04272535ce2ba558a189f2dce08274ad898..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/ConnectionWindowFactoryImpl.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package net.sf.briar.transport;
-
-import java.util.Map;
-
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.api.transport.ConnectionWindowFactory;
-
-import com.google.inject.Inject;
-
-class ConnectionWindowFactoryImpl implements ConnectionWindowFactory {
-
-	private final CryptoComponent crypto;
-
-	@Inject
-	ConnectionWindowFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
-	}
-
-	public ConnectionWindow createConnectionWindow(TransportIndex i,
-			byte[] secret) {
-		return new ConnectionWindowImpl(crypto, i, secret);
-	}
-
-	public ConnectionWindow createConnectionWindow(TransportIndex i,
-			Map<Long, byte[]> unseen) {
-		return new ConnectionWindowImpl(crypto, i, unseen);
-	}
-}
diff --git a/components/net/sf/briar/transport/ConnectionWindowImpl.java b/components/net/sf/briar/transport/ConnectionWindowImpl.java
index d20eca411de139ec13e7a44e25e29101c86ee498..057cd9335a380f8dd81fe2b52d6478e374717c7f 100644
--- a/components/net/sf/briar/transport/ConnectionWindowImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWindowImpl.java
@@ -1,41 +1,30 @@
 package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.CONNECTION_WINDOW_SIZE;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
-import java.util.HashMap;
-import java.util.Map;
+import java.util.HashSet;
+import java.util.Set;
 
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.util.ByteUtils;
 
 // This class is not thread-safe
 class ConnectionWindowImpl implements ConnectionWindow {
 
-	private final CryptoComponent crypto;
-	private final int index;
-	private final Map<Long, byte[]> unseen;
+	private final Set<Long> unseen;
 
 	private long centre;
 
-	ConnectionWindowImpl(CryptoComponent crypto, TransportIndex i,
-			byte[] secret) {
-		this.crypto = crypto;
-		index = i.getInt();
-		unseen = new HashMap<Long, byte[]>();
-		for(long l = 0; l < CONNECTION_WINDOW_SIZE / 2; l++) {
-			secret = crypto.deriveNextSecret(secret, index, l);
-			unseen.put(l, secret);
-		}
+	ConnectionWindowImpl() {
+		unseen = new HashSet<Long>();
+		for(long l = 0; l < CONNECTION_WINDOW_SIZE / 2; l++) unseen.add(l);
 		centre = 0;
 	}
 
-	ConnectionWindowImpl(CryptoComponent crypto, TransportIndex i,
-			Map<Long, byte[]> unseen) {
+	ConnectionWindowImpl(Set<Long> unseen) {
 		long min = Long.MAX_VALUE, max = Long.MIN_VALUE;
-		for(long l : unseen.keySet()) {
-			if(l < 0 || l > ByteUtils.MAX_32_BIT_UNSIGNED)
+		for(long l : unseen) {
+			if(l < 0 || l > MAX_32_BIT_UNSIGNED)
 				throw new IllegalArgumentException();
 			if(l < min) min = l;
 			if(l > max) max = l;
@@ -44,42 +33,29 @@ class ConnectionWindowImpl implements ConnectionWindow {
 			throw new IllegalArgumentException();
 		centre = max - CONNECTION_WINDOW_SIZE / 2 + 1;
 		for(long l = centre; l <= max; l++) {
-			if(!unseen.containsKey(l)) throw new IllegalArgumentException();
+			if(!unseen.contains(l)) throw new IllegalArgumentException();
 		}
-		this.crypto = crypto;
-		index = i.getInt();
 		this.unseen = unseen;
 	}
 
 	public boolean isSeen(long connection) {
-		return !unseen.containsKey(connection);
+		return !unseen.contains(connection);
 	}
 
-	public byte[] setSeen(long connection) {
+	public void setSeen(long connection) {
 		long bottom = getBottom(centre);
 		long top = getTop(centre);
 		if(connection < bottom || connection > top)
 			throw new IllegalArgumentException();
-		if(!unseen.containsKey(connection))
+		if(!unseen.remove(connection))
 			throw new IllegalArgumentException();
 		if(connection >= centre) {
 			centre = connection + 1;
 			long newBottom = getBottom(centre);
 			long newTop = getTop(centre);
-			for(long l = bottom; l < newBottom; l++) {
-				byte[] expired = unseen.remove(l);
-				if(expired != null) ByteUtils.erase(expired);
-			}
-			byte[] topSecret = unseen.get(top);
-			assert topSecret != null;
-			for(long l = top + 1; l <= newTop; l++) {
-				topSecret = crypto.deriveNextSecret(topSecret, index, l);
-				unseen.put(l, topSecret);
-			}
+			for(long l = bottom; l < newBottom; l++) unseen.remove(l);
+			for(long l = top + 1; l <= newTop; l++) unseen.add(l);
 		}
-		byte[] seen = unseen.remove(connection);
-		assert seen != null;
-		return seen;
 	}
 
 	// Returns the lowest value contained in a window with the given centre
@@ -89,15 +65,11 @@ class ConnectionWindowImpl implements ConnectionWindow {
 
 	// Returns the highest value contained in a window with the given centre
 	private static long getTop(long centre) {
-		return Math.min(ByteUtils.MAX_32_BIT_UNSIGNED,
+		return Math.min(MAX_32_BIT_UNSIGNED,
 				centre + CONNECTION_WINDOW_SIZE / 2 - 1);
 	}
 
-	public Map<Long, byte[]> getUnseen() {
+	public Set<Long> getUnseen() {
 		return unseen;
 	}
-
-	public void erase() {
-		for(byte[] secret : unseen.values()) ByteUtils.erase(secret);
-	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
index 415eb2fdf74ece8fa3f2be2f1cfc1309b50c854f..d853371d8a28e5818a69033d107acbb505b8ccc8 100644
--- a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
@@ -4,14 +4,11 @@ import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 
 import java.io.OutputStream;
 
-import javax.crypto.Cipher;
-
-import net.sf.briar.api.crypto.AuthenticatedCipher;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
-import net.sf.briar.util.ByteUtils;
 
 import com.google.inject.Inject;
 
@@ -25,27 +22,21 @@ class ConnectionWriterFactoryImpl implements ConnectionWriterFactory {
 	}
 
 	public ConnectionWriter createConnectionWriter(OutputStream out,
-			long capacity, byte[] secret, boolean initiator) {
+			long capacity, ConnectionContext ctx, boolean initiator) {
+		byte[] secret = ctx.getSecret();
+		long connection = ctx.getConnectionNumber();
+		boolean alice = ctx.getAlice();
+		ErasableKey frameKey = crypto.deriveFrameKey(secret, connection, alice,
+				initiator);
+		FrameWriter encryption;
 		if(initiator) {
-			// Derive the tag and frame keys and erase the secret
-			ErasableKey tagKey = crypto.deriveTagKey(secret, initiator);
-			ErasableKey frameKey = crypto.deriveFrameKey(secret, initiator);
-			ByteUtils.erase(secret);
-			// Create a writer for the initiator's side of the connection
-			Cipher tagCipher = crypto.getTagCipher();
-			AuthenticatedCipher frameCipher = crypto.getFrameCipher();
-			FrameWriter encryption = new OutgoingEncryptionLayer(out, capacity,
-					tagCipher, frameCipher, tagKey, frameKey, MAX_FRAME_LENGTH);
-			return new ConnectionWriterImpl(encryption, MAX_FRAME_LENGTH);
+			encryption = new OutgoingEncryptionLayer(out, capacity,
+					crypto.getFrameCipher(), frameKey, MAX_FRAME_LENGTH,
+					ctx.getTag());
 		} else {
-			// Derive the frame key and erase the secret
-			ErasableKey frameKey = crypto.deriveFrameKey(secret, initiator);
-			ByteUtils.erase(secret);
-			// Create a writer for the responder's side of the connection
-			AuthenticatedCipher frameCipher = crypto.getFrameCipher();
-			FrameWriter encryption = new OutgoingEncryptionLayer(out, capacity,
-					frameCipher, frameKey, MAX_FRAME_LENGTH);
-			return new ConnectionWriterImpl(encryption, MAX_FRAME_LENGTH);
+			encryption = new OutgoingEncryptionLayer(out, capacity,
+					crypto.getFrameCipher(), frameKey, MAX_FRAME_LENGTH);
 		}
+		return new ConnectionWriterImpl(encryption, MAX_FRAME_LENGTH);
 	}
 }
\ No newline at end of file
diff --git a/components/net/sf/briar/transport/IncomingEncryptionLayer.java b/components/net/sf/briar/transport/IncomingEncryptionLayer.java
index 4b36a89d26bb633fc078e1a90f29a1970fa9613b..6d446a8a5faeec0795092fc09acd3c0b33c74c08 100644
--- a/components/net/sf/briar/transport/IncomingEncryptionLayer.java
+++ b/components/net/sf/briar/transport/IncomingEncryptionLayer.java
@@ -5,15 +5,12 @@ import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 
-import javax.crypto.Cipher;
-
 import net.sf.briar.api.FormatException;
 import net.sf.briar.api.crypto.AuthenticatedCipher;
 import net.sf.briar.api.crypto.ErasableKey;
@@ -21,70 +18,29 @@ import net.sf.briar.api.crypto.ErasableKey;
 class IncomingEncryptionLayer implements FrameReader {
 
 	private final InputStream in;
-	private final Cipher tagCipher;
 	private final AuthenticatedCipher frameCipher;
-	private final ErasableKey tagKey, frameKey;
+	private final ErasableKey frameKey;
 	private final byte[] iv, aad, ciphertext;
 	private final int frameLength;
 
 	private long frameNumber;
-	private boolean readTag, finalFrame;
+	private boolean finalFrame;
 
-	/** Constructor for the initiator's side of a connection. */
-	IncomingEncryptionLayer(InputStream in, Cipher tagCipher,
-			AuthenticatedCipher frameCipher, ErasableKey tagKey,
-			ErasableKey frameKey, int frameLength) {
-		this.in = in;
-		this.tagCipher = tagCipher;
-		this.frameCipher = frameCipher;
-		this.tagKey = tagKey;
-		this.frameKey = frameKey;
-		this.frameLength = frameLength;
-		iv = new byte[IV_LENGTH];
-		aad = new byte[AAD_LENGTH];
-		ciphertext = new byte[frameLength];
-		frameNumber = 0L;
-		readTag = true;
-		finalFrame = false;
-	}
-
-	/** Constructor for the responder's side of a connection. */
 	IncomingEncryptionLayer(InputStream in, AuthenticatedCipher frameCipher,
 			ErasableKey frameKey, int frameLength) {
 		this.in = in;
 		this.frameCipher = frameCipher;
 		this.frameKey = frameKey;
 		this.frameLength = frameLength;
-		tagCipher = null;
-		tagKey = null;
 		iv = new byte[IV_LENGTH];
 		aad = new byte[AAD_LENGTH];
 		ciphertext = new byte[frameLength];
 		frameNumber = 0L;
-		readTag = false;
 		finalFrame = false;
 	}
 
 	public int readFrame(byte[] frame) throws IOException {
 		if(finalFrame) return -1;
-		// Read the tag if required
-		if(readTag) {
-			int offset = 0;
-			try {
-				while(offset < TAG_LENGTH) {
-					int read = in.read(ciphertext, offset, TAG_LENGTH - offset);
-					if(read == -1) throw new EOFException();
-					offset += read;
-				}
-			} catch(IOException e) {
-				frameKey.erase();
-				tagKey.erase();
-				throw e;
-			}
-			if(!TagEncoder.decodeTag(ciphertext, tagCipher, tagKey))
-				throw new FormatException();
-			readTag = false;
-		}
 		// Read the frame
 		int ciphertextLength = 0;
 		try {
@@ -96,7 +52,6 @@ class IncomingEncryptionLayer implements FrameReader {
 			}
 		} catch(IOException e) {
 			frameKey.erase();
-			tagKey.erase();
 			throw e;
 		}
 		int plaintextLength = ciphertextLength - MAC_LENGTH;
diff --git a/components/net/sf/briar/transport/OutgoingEncryptionLayer.java b/components/net/sf/briar/transport/OutgoingEncryptionLayer.java
index d691bd39bccccf836674835964af43c97162d2bd..a9c45fd961db375bfa0eb2e07148d9c52deaf94e 100644
--- a/components/net/sf/briar/transport/OutgoingEncryptionLayer.java
+++ b/components/net/sf/briar/transport/OutgoingEncryptionLayer.java
@@ -5,41 +5,36 @@ import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
 import java.io.IOException;
 import java.io.OutputStream;
 import java.security.GeneralSecurityException;
 
-import javax.crypto.Cipher;
-
 import net.sf.briar.api.crypto.AuthenticatedCipher;
 import net.sf.briar.api.crypto.ErasableKey;
 
 class OutgoingEncryptionLayer implements FrameWriter {
 
 	private final OutputStream out;
-	private final Cipher tagCipher;
 	private final AuthenticatedCipher frameCipher;
-	private final ErasableKey tagKey, frameKey;
-	private final byte[] iv, aad, ciphertext;
+	private final ErasableKey frameKey;
+	private final byte[] tag, iv, aad, ciphertext;
 	private final int frameLength, maxPayloadLength;
 
 	private long capacity, frameNumber;
 	private boolean writeTag;
 
 	/** Constructor for the initiator's side of a connection. */
-	OutgoingEncryptionLayer(OutputStream out, long capacity, Cipher tagCipher,
-			AuthenticatedCipher frameCipher, ErasableKey tagKey,
-			ErasableKey frameKey, int frameLength) {
+	OutgoingEncryptionLayer(OutputStream out, long capacity,
+			AuthenticatedCipher frameCipher, ErasableKey frameKey,
+			int frameLength, byte[] tag) {
 		this.out = out;
 		this.capacity = capacity;
-		this.tagCipher = tagCipher;
 		this.frameCipher = frameCipher;
-		this.tagKey = tagKey;
 		this.frameKey = frameKey;
 		this.frameLength = frameLength;
+		this.tag = tag;
 		maxPayloadLength = frameLength - HEADER_LENGTH - MAC_LENGTH;
 		iv = new byte[IV_LENGTH];
 		aad = new byte[AAD_LENGTH];
@@ -57,9 +52,8 @@ class OutgoingEncryptionLayer implements FrameWriter {
 		this.frameCipher = frameCipher;
 		this.frameKey = frameKey;
 		this.frameLength = frameLength;
+		tag = null;
 		maxPayloadLength = frameLength - HEADER_LENGTH - MAC_LENGTH;
-		tagCipher = null;
-		tagKey = null;
 		iv = new byte[IV_LENGTH];
 		aad = new byte[AAD_LENGTH];
 		ciphertext = new byte[frameLength];
@@ -75,15 +69,13 @@ class OutgoingEncryptionLayer implements FrameWriter {
 		if(writeTag && finalFrame && payloadLength == 0) return;
 		// Write the tag if required
 		if(writeTag) {
-			TagEncoder.encodeTag(ciphertext, tagCipher, tagKey);
 			try {
-				out.write(ciphertext, 0, TAG_LENGTH);
+				out.write(tag, 0, tag.length);
 			} catch(IOException e) {
 				frameKey.erase();
-				tagKey.erase();
 				throw e;
 			}
-			capacity -= TAG_LENGTH;
+			capacity -= tag.length;
 			writeTag = false;
 		}
 		// Encode the header
@@ -117,7 +109,6 @@ class OutgoingEncryptionLayer implements FrameWriter {
 			out.write(ciphertext, 0, ciphertextLength);
 		} catch(IOException e) {
 			frameKey.erase();
-			tagKey.erase();
 			throw e;
 		}
 		capacity -= ciphertextLength;
@@ -132,7 +123,7 @@ class OutgoingEncryptionLayer implements FrameWriter {
 		// How many frame numbers can we use?
 		long frameNumbers = MAX_32_BIT_UNSIGNED - frameNumber + 1;
 		// How many full frames do we have space for?
-		long bytes = writeTag ? capacity - TAG_LENGTH : capacity;
+		long bytes = writeTag ? capacity - tag.length : capacity;
 		long fullFrames = bytes / frameLength;
 		// Are we limited by frame numbers or space?
 		if(frameNumbers > fullFrames) {
diff --git a/components/net/sf/briar/transport/TransportModule.java b/components/net/sf/briar/transport/TransportModule.java
index 89db1a259b7455e298f7202456a253928e339cb6..daceddd3b11022d6b7a6151e205e6df34cfe2875 100644
--- a/components/net/sf/briar/transport/TransportModule.java
+++ b/components/net/sf/briar/transport/TransportModule.java
@@ -3,12 +3,9 @@ package net.sf.briar.transport;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 
-import net.sf.briar.api.transport.ConnectionContextFactory;
 import net.sf.briar.api.transport.ConnectionDispatcher;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
-import net.sf.briar.api.transport.ConnectionRecogniser;
 import net.sf.briar.api.transport.ConnectionRegistry;
-import net.sf.briar.api.transport.ConnectionWindowFactory;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.api.transport.IncomingConnectionExecutor;
 
@@ -18,15 +15,10 @@ public class TransportModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
-		bind(ConnectionContextFactory.class).to(
-				ConnectionContextFactoryImpl.class);
 		bind(ConnectionDispatcher.class).to(ConnectionDispatcherImpl.class);
 		bind(ConnectionReaderFactory.class).to(
 				ConnectionReaderFactoryImpl.class);
-		bind(ConnectionRecogniser.class).to(ConnectionRecogniserImpl.class);
 		bind(ConnectionRegistry.class).toInstance(new ConnectionRegistryImpl());
-		bind(ConnectionWindowFactory.class).to(
-				ConnectionWindowFactoryImpl.class);
 		bind(ConnectionWriterFactory.class).to(
 				ConnectionWriterFactoryImpl.class);
 		// The executor is unbounded, so tasks can be dependent or long-lived
diff --git a/test/build.xml b/test/build.xml
index 1720cf75662cf75baefe82790924bf61ab92a639..b006dad3127a6a7ae3517660abd1c6697ab4f73a 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -19,6 +19,7 @@
 			<test name='net.sf.briar.crypto.CounterModeTest'/>
 			<test name='net.sf.briar.crypto.ErasableKeyTest'/>
 			<test name='net.sf.briar.crypto.KeyDerivationTest'/>
+			<test name='net.sf.briar.crypto.KeyRotatorImplTest'/>
 			<test name='net.sf.briar.db.BasicH2Test'/>
 			<test name='net.sf.briar.db.DatabaseCleanerImplTest'/>
 			<test name='net.sf.briar.db.DatabaseComponentImplTest'/>
@@ -46,7 +47,6 @@
 			<test name='net.sf.briar.serial.ReaderImplTest'/>
 			<test name='net.sf.briar.serial.WriterImplTest'/>
 			<test name='net.sf.briar.transport.ConnectionReaderImplTest'/>
-			<test name='net.sf.briar.transport.ConnectionRecogniserImplTest'/>
 			<test name='net.sf.briar.transport.ConnectionRegistryImplTest'/>
 			<test name='net.sf.briar.transport.ConnectionWindowImplTest'/>
 			<test name='net.sf.briar.transport.ConnectionWriterImplTest'/>
diff --git a/test/net/sf/briar/ProtocolIntegrationTest.java b/test/net/sf/briar/ProtocolIntegrationTest.java
index 5f742831710fefde2f4b36d7a840c1ddc1adf490..e03a3389dd80959c95e2ec23c688e8ffe38cc26f 100644
--- a/test/net/sf/briar/ProtocolIntegrationTest.java
+++ b/test/net/sf/briar/ProtocolIntegrationTest.java
@@ -17,6 +17,7 @@ import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Random;
 
+import net.sf.briar.api.ContactId;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.Author;
@@ -40,8 +41,8 @@ import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
 import net.sf.briar.api.transport.ConnectionWriter;
@@ -72,8 +73,9 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 	private final ProtocolWriterFactory protocolWriterFactory;
 	private final PacketFactory packetFactory;
 	private final CryptoComponent crypto;
+	private final ContactId contactId;
+	private final TransportId transportId;
 	private final byte[] secret;
-	private final TransportIndex transportIndex = new TransportIndex(13);
 	private final Author author;
 	private final Group group, group1;
 	private final Message message, message1, message2, message3;
@@ -95,6 +97,8 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class);
 		packetFactory = i.getInstance(PacketFactory.class);
 		crypto = i.getInstance(CryptoComponent.class);
+		contactId = new ContactId(234);
+		transportId = new TransportId(TestUtils.getRandomId());
 		// Create a shared secret
 		Random r = new Random();
 		secret = new byte[32];
@@ -125,7 +129,7 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 				subject, messageBody.getBytes("UTF-8"));
 		// Create some transports
 		TransportId transportId = new TransportId(TestUtils.getRandomId());
-		Transport transport = new Transport(transportId, transportIndex,
+		Transport transport = new Transport(transportId,
 				Collections.singletonMap("bar", "baz"));
 		transports = Collections.singletonList(transport);
 	}
@@ -137,8 +141,11 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 
 	private byte[] write() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		byte[] tag = new byte[TAG_LENGTH];
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, secret.clone(), 0L, true);
 		ConnectionWriter conn = connectionWriterFactory.createConnectionWriter(
-				out, Long.MAX_VALUE, secret.clone(), true);
+				out, Long.MAX_VALUE, ctx, true);
 		OutputStream out1 = conn.getOutputStream();
 		ProtocolWriter writer = protocolWriterFactory.createProtocolWriter(out1,
 				false);
@@ -190,8 +197,11 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		InputStream in = new ByteArrayInputStream(connectionData);
 		byte[] tag = new byte[TAG_LENGTH];
 		assertEquals(TAG_LENGTH, in.read(tag, 0, TAG_LENGTH));
+		assertArrayEquals(new byte[TAG_LENGTH], tag);
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, secret.clone(), 0L, true);
 		ConnectionReader conn = connectionReaderFactory.createConnectionReader(
-				in, secret.clone(), true);
+				in, ctx, true);
 		InputStream in1 = conn.getInputStream();
 		ProtocolReader reader = protocolReaderFactory.createProtocolReader(in1);
 
diff --git a/test/net/sf/briar/crypto/KeyDerivationTest.java b/test/net/sf/briar/crypto/KeyDerivationTest.java
index cc4335c9ae36e2843451b7d1fd8f28516da4abb0..49dda92c8e4625052c11e759d91a0a723b83fc29 100644
--- a/test/net/sf/briar/crypto/KeyDerivationTest.java
+++ b/test/net/sf/briar/crypto/KeyDerivationTest.java
@@ -8,7 +8,6 @@ import java.util.Random;
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
-import net.sf.briar.api.protocol.ProtocolConstants;
 
 import org.junit.Test;
 
@@ -27,8 +26,10 @@ public class KeyDerivationTest extends BriarTestCase {
 	@Test
 	public void testKeysAreDistinct() {
 		List<ErasableKey> keys = new ArrayList<ErasableKey>();
-		keys.add(crypto.deriveFrameKey(secret, true));
-		keys.add(crypto.deriveFrameKey(secret, false));
+		keys.add(crypto.deriveFrameKey(secret, 0, false, false));
+		keys.add(crypto.deriveFrameKey(secret, 0, false, true));
+		keys.add(crypto.deriveFrameKey(secret, 0, true, false));
+		keys.add(crypto.deriveFrameKey(secret, 0, true, true));
 		keys.add(crypto.deriveTagKey(secret, true));
 		keys.add(crypto.deriveTagKey(secret, false));
 		for(int i = 0; i < 4; i++) {
@@ -47,7 +48,7 @@ public class KeyDerivationTest extends BriarTestCase {
 		for(int i = 0; i < 20; i++) {
 			byte[] b = new byte[32];
 			r.nextBytes(b);
-			secrets.add(crypto.deriveNextSecret(b, 0, 0));
+			secrets.add(crypto.deriveNextSecret(b, 0));
 		}
 		for(int i = 0; i < 20; i++) {
 			byte[] secretI = secrets.get(i);
@@ -58,26 +59,11 @@ public class KeyDerivationTest extends BriarTestCase {
 		}
 	}
 
-	@Test
-	public void testTransportIndexAffectsDerivation() {
-		List<byte[]> secrets = new ArrayList<byte[]>();
-		for(int i = 0; i < ProtocolConstants.MAX_TRANSPORTS; i++) {
-			secrets.add(crypto.deriveNextSecret(secret, i, 0));
-		}
-		for(int i = 0; i < ProtocolConstants.MAX_TRANSPORTS; i++) {
-			byte[] secretI = secrets.get(i);
-			for(int j = 0; j < ProtocolConstants.MAX_TRANSPORTS; j++) {
-				byte[] secretJ = secrets.get(j);
-				assertEquals(i == j, Arrays.equals(secretI, secretJ));
-			}
-		}
-	}
-
 	@Test
 	public void testConnectionNumberAffectsDerivation() {
 		List<byte[]> secrets = new ArrayList<byte[]>();
 		for(int i = 0; i < 20; i++) {
-			secrets.add(crypto.deriveNextSecret(secret, 0, i));
+			secrets.add(crypto.deriveNextSecret(secret, i));
 		}
 		for(int i = 0; i < 20; i++) {
 			byte[] secretI = secrets.get(i);
diff --git a/test/net/sf/briar/db/KeyRotatorImplTest.java b/test/net/sf/briar/crypto/KeyRotatorImplTest.java
similarity index 92%
rename from test/net/sf/briar/db/KeyRotatorImplTest.java
rename to test/net/sf/briar/crypto/KeyRotatorImplTest.java
index 2470e534e6c1fb7da634552a375888451a96e12e..3e124b2e8e4bd8c123d31c7182a86568dde253d1 100644
--- a/test/net/sf/briar/db/KeyRotatorImplTest.java
+++ b/test/net/sf/briar/crypto/KeyRotatorImplTest.java
@@ -1,11 +1,12 @@
-package net.sf.briar.db;
+package net.sf.briar.crypto;
 
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.db.DbException;
-import net.sf.briar.db.KeyRotator.Callback;
+import net.sf.briar.crypto.KeyRotatorImpl;
+import net.sf.briar.crypto.KeyRotator.Callback;
 
 import org.junit.Test;
 
diff --git a/test/net/sf/briar/db/DatabaseComponentTest.java b/test/net/sf/briar/db/DatabaseComponentTest.java
index 013fcf2c080a966ab0092b88718c89bb371d189f..3918650be8e6fd957612e5cb18b31366995394c6 100644
--- a/test/net/sf/briar/db/DatabaseComponentTest.java
+++ b/test/net/sf/briar/db/DatabaseComponentTest.java
@@ -23,7 +23,6 @@ import net.sf.briar.api.db.event.DatabaseListener;
 import net.sf.briar.api.db.event.MessagesAddedEvent;
 import net.sf.briar.api.db.event.RatingChangedEvent;
 import net.sf.briar.api.db.event.SubscriptionsUpdatedEvent;
-import net.sf.briar.api.db.event.TransportAddedEvent;
 import net.sf.briar.api.lifecycle.ShutdownManager;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.AuthorId;
@@ -40,9 +39,7 @@ import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
-import net.sf.briar.api.transport.ConnectionWindow;
 
 import org.jmock.Expectations;
 import org.jmock.Mockery;
@@ -63,7 +60,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	private final Message message, privateMessage;
 	private final Group group;
 	private final TransportId transportId;
-	private final TransportIndex localIndex, remoteIndex;
 	private final Collection<Transport> transports;
 	private final Map<ContactId, TransportProperties> remoteProperties;
 	private final byte[] inSecret, outSecret;
@@ -72,7 +68,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		super();
 		authorId = new AuthorId(TestUtils.getRandomId());
 		batchId = new BatchId(TestUtils.getRandomId());
-		contactId = new ContactId(123);
+		contactId = new ContactId(234);
 		groupId = new GroupId(TestUtils.getRandomId());
 		messageId = new MessageId(TestUtils.getRandomId());
 		parentId = new MessageId(TestUtils.getRandomId());
@@ -86,13 +82,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				timestamp, raw);
 		group = new TestGroup(groupId, "The really exciting group", null);
 		transportId = new TransportId(TestUtils.getRandomId());
-		localIndex = new TransportIndex(0);
-		remoteIndex = new TransportIndex(13);
 		TransportProperties properties = new TransportProperties(
 				Collections.singletonMap("foo", "bar"));
 		remoteProperties = Collections.singletonMap(contactId, properties);
-		Transport transport = new Transport(transportId, localIndex,
-				properties);
+		Transport transport = new Transport(transportId, properties);
 		transports = Collections.singletonList(transport);
 		Random r = new Random();
 		inSecret = new byte[32];
@@ -108,14 +101,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	@Test
 	@SuppressWarnings("unchecked")
 	public void testSimpleCalls() throws Exception {
+		// FIXME: Test new methods
 		final int shutdownHandle = 12345;
 		Mockery context = new Mockery();
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final ConnectionWindow connectionWindow =
-			context.mock(ConnectionWindow.class);
 		final Group group = context.mock(Group.class);
 		final DatabaseListener listener = context.mock(DatabaseListener.class);
 		context.checking(new Expectations() {{
@@ -142,18 +134,12 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setRating(txn, authorId, Rating.GOOD);
 			will(returnValue(Rating.GOOD));
 			// addContact()
-			oneOf(database).addContact(with(txn), with(inSecret),
-					with(outSecret), with(any(Collection.class)));
+			oneOf(database).addContact(txn);
 			will(returnValue(contactId));
 			oneOf(listener).eventOccurred(with(any(ContactAddedEvent.class)));
 			// getContacts()
 			oneOf(database).getContacts(txn);
 			will(returnValue(Collections.singletonList(contactId)));
-			// getConnectionWindow(contactId, 13)
-			oneOf(database).containsContact(txn, contactId);
-			will(returnValue(true));
-			oneOf(database).getConnectionWindow(txn, contactId, remoteIndex);
-			will(returnValue(connectionWindow));
 			// getTransportProperties(transportId)
 			oneOf(database).getRemoteProperties(txn, transportId);
 			will(returnValue(remoteProperties));
@@ -183,11 +169,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			// unsubscribe(groupId) again
 			oneOf(database).containsSubscription(txn, groupId);
 			will(returnValue(false));
-			// setConnectionWindow(contactId, 13, connectionWindow)
-			oneOf(database).containsContact(txn, contactId);
-			will(returnValue(true));
-			oneOf(database).setConnectionWindow(txn, contactId, remoteIndex,
-					connectionWindow);
 			// removeContact(contactId)
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
@@ -206,10 +187,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		assertEquals(Rating.UNRATED, db.getRating(authorId));
 		db.setRating(authorId, Rating.GOOD); // First time - listeners called
 		db.setRating(authorId, Rating.GOOD); // Second time - not called
-		assertEquals(contactId, db.addContact(inSecret, outSecret));
+		assertEquals(contactId, db.addContact());
 		assertEquals(Collections.singletonList(contactId), db.getContacts());
-		assertEquals(connectionWindow,
-				db.getConnectionWindow(contactId, remoteIndex));
 		assertEquals(remoteProperties, db.getRemoteProperties(transportId));
 		db.subscribe(group); // First time - listeners called
 		db.subscribe(group); // Second time - not called
@@ -217,7 +196,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		assertEquals(Collections.singletonList(groupId), db.getSubscriptions());
 		db.unsubscribe(groupId); // First time - listeners called
 		db.unsubscribe(groupId); // Second time - not called
-		db.setConnectionWindow(contactId, remoteIndex, connectionWindow);
 		db.removeContact(contactId);
 		db.removeListener(listener);
 		db.close();
@@ -297,7 +275,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testAffectedParentContinuesBackwardInclusion()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -338,7 +316,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testGroupMessagesAreNotStoredUnlessSubscribed()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -424,7 +402,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testAddingSendableMessageTriggersBackwardInclusion()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -516,7 +494,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testVariousMethodsThrowExceptionIfContactIsMissing()
-	throws Exception {
+			throws Exception {
+		// FIXME: Test new methods
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -527,16 +506,16 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final Batch batch = context.mock(Batch.class);
 		final Offer offer = context.mock(Offer.class);
 		final SubscriptionUpdate subscriptionUpdate =
-			context.mock(SubscriptionUpdate.class);
+				context.mock(SubscriptionUpdate.class);
 		final TransportUpdate transportUpdate =
-			context.mock(TransportUpdate.class);
+				context.mock(TransportUpdate.class);
 		context.checking(new Expectations() {{
 			// Check whether the contact is still in the DB (which it's not)
-			exactly(19).of(database).startTransaction();
+			exactly(15).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(19).of(database).containsContact(txn, contactId);
+			exactly(15).of(database).containsContact(txn, contactId);
 			will(returnValue(false));
-			exactly(19).of(database).commitTransaction(txn);
+			exactly(15).of(database).abortTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
@@ -577,21 +556,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			fail();
 		} catch(NoSuchContactException expected) {}
 
-		try {
-			db.getConnectionContext(contactId, remoteIndex);
-			fail();
-		} catch(NoSuchContactException expected) {}
-
-		try {
-			db.getConnectionWindow(contactId, remoteIndex);
-			fail();
-		} catch(NoSuchContactException expected) {}
-
-		try {
-			db.getRemoteIndex(contactId, transportId);
-			fail();
-		} catch(NoSuchContactException expected) {}
-
 		try {
 			db.hasSendableMessages(contactId);
 			fail();
@@ -627,11 +591,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			fail();
 		} catch(NoSuchContactException expected) {}
 
-		try {
-			db.setConnectionWindow(contactId, remoteIndex, null);
-			fail();
-		} catch(NoSuchContactException expected) {}
-
 		try {
 			db.setSeen(contactId, Collections.singletonList(messageId));
 			fail();
@@ -811,7 +770,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
 		final SubscriptionUpdate subscriptionUpdate =
-			context.mock(SubscriptionUpdate.class);
+				context.mock(SubscriptionUpdate.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -883,7 +842,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
 		final TransportUpdate transportUpdate =
-			context.mock(TransportUpdate.class);
+				context.mock(TransportUpdate.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1015,7 +974,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testReceiveBatchDoesNotStoreGroupMessageUnlessSubscribed()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1050,7 +1009,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testReceiveBatchDoesNotCalculateSendabilityForDuplicates()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1241,7 +1200,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
 		final SubscriptionUpdate subscriptionUpdate =
-			context.mock(SubscriptionUpdate.class);
+				context.mock(SubscriptionUpdate.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1281,7 +1240,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
 		final TransportUpdate transportUpdate =
-			context.mock(TransportUpdate.class);
+				context.mock(TransportUpdate.class);
 		context.checking(new Expectations() {{
 			allowing(database).startTransaction();
 			will(returnValue(txn));
@@ -1375,7 +1334,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testAddingDuplicateGroupMessageDoesNotCallListeners()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1405,7 +1364,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testAddingDuplicatePrivateMessageDoesNotCallListeners()
-	throws Exception {
+			throws Exception {
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1435,16 +1394,15 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testTransportPropertiesChangedCallsListeners()
-	throws Exception {
+			throws Exception {
 		final TransportProperties properties =
-			new TransportProperties(Collections.singletonMap("bar", "baz"));
+				new TransportProperties(Collections.singletonMap("bar", "baz"));
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
 		final DatabaseCleaner cleaner = context.mock(DatabaseCleaner.class);
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final PacketFactory packetFactory = context.mock(PacketFactory.class);
-		final DatabaseListener listener = context.mock(DatabaseListener.class);
 		context.checking(new Expectations() {{
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
@@ -1454,13 +1412,10 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).setTransportsModified(with(txn),
 					with(any(long.class)));
 			oneOf(database).commitTransaction(txn);
-			oneOf(listener).eventOccurred(with(any(
-					TransportAddedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				shutdown, packetFactory);
 
-		db.addListener(listener);
 		db.setLocalProperties(transportId, properties);
 
 		context.assertIsSatisfied();
@@ -1468,9 +1423,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testTransportPropertiesUnchangedDoesNotCallListeners()
-	throws Exception {
+			throws Exception {
 		final TransportProperties properties =
-			new TransportProperties(Collections.singletonMap("bar", "baz"));
+				new TransportProperties(Collections.singletonMap("bar", "baz"));
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1521,7 +1476,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 	@Test
 	public void testVisibilityChangedCallsListeners() throws Exception {
-		final ContactId contactId1 = new ContactId(234);
+		final ContactId contactId1 = new ContactId(123);
 		final Collection<ContactId> both =
 				Arrays.asList(new ContactId[] {contactId, contactId1});
 		Mockery context = new Mockery();
diff --git a/test/net/sf/briar/db/H2DatabaseTest.java b/test/net/sf/briar/db/H2DatabaseTest.java
index 0451fe9f5b688ac0046e4f964632e73160f1a7df..783487e262e3f621897be9fb3e773954ff2f4c78 100644
--- a/test/net/sf/briar/db/H2DatabaseTest.java
+++ b/test/net/sf/briar/db/H2DatabaseTest.java
@@ -3,6 +3,7 @@ package net.sf.briar.db;
 import static org.junit.Assert.assertArrayEquals;
 
 import java.io.File;
+import java.io.IOException;
 import java.sql.Connection;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -18,7 +19,6 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import net.sf.briar.BriarTestCase;
-import net.sf.briar.TestDatabaseModule;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.Rating;
@@ -38,28 +38,12 @@ import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContextFactory;
-import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.api.transport.ConnectionWindowFactory;
-import net.sf.briar.api.transport.TransportConstants;
-import net.sf.briar.clock.ClockModule;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.lifecycle.LifecycleModule;
-import net.sf.briar.protocol.ProtocolModule;
-import net.sf.briar.protocol.duplex.DuplexProtocolModule;
-import net.sf.briar.protocol.simplex.SimplexProtocolModule;
-import net.sf.briar.serial.SerialModule;
-import net.sf.briar.transport.TransportModule;
 
 import org.apache.commons.io.FileSystemUtils;
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
 public class H2DatabaseTest extends BriarTestCase {
 
 	private static final int ONE_MEGABYTE = 1024 * 1024;
@@ -70,10 +54,8 @@ public class H2DatabaseTest extends BriarTestCase {
 	private final String passwordString = "foo bar";
 	private final Password password = new TestPassword();
 	private final Random random = new Random();
-	private final ConnectionContextFactory connectionContextFactory;
-	private final ConnectionWindowFactory connectionWindowFactory;
 	private final GroupFactory groupFactory;
-
+	private final Group group;
 	private final AuthorId authorId;
 	private final BatchId batchId;
 	private final ContactId contactId;
@@ -84,33 +66,21 @@ public class H2DatabaseTest extends BriarTestCase {
 	private final int size;
 	private final byte[] raw;
 	private final Message message, privateMessage;
-	private final Group group;
 	private final TransportId transportId;
-	private final TransportIndex localIndex, remoteIndex;
 	private final TransportProperties properties;
 	private final Map<ContactId, TransportProperties> remoteProperties;
 	private final Collection<Transport> remoteTransports;
-	private final byte[] inSecret, outSecret;
-	private final Collection<byte[]> erase;
 
 	public H2DatabaseTest() throws Exception {
 		super();
-		// FIXME: Use mocks for the factories rather than building the whole app
-		Injector i = Guice.createInjector(new ClockModule(), new CryptoModule(),
-				new DatabaseModule(), new LifecycleModule(),
-				new ProtocolModule(), new SerialModule(),
-				new SimplexProtocolModule(), new TransportModule(),
-				new DuplexProtocolModule(), new TestDatabaseModule(testDir));
-		connectionContextFactory =
-			i.getInstance(ConnectionContextFactory.class);
-		connectionWindowFactory = i.getInstance(ConnectionWindowFactory.class);
-		groupFactory = i.getInstance(GroupFactory.class);
+		groupFactory = new TestGroupFactory();
 		authorId = new AuthorId(TestUtils.getRandomId());
 		batchId = new BatchId(TestUtils.getRandomId());
 		contactId = new ContactId(1);
 		groupId = new GroupId(TestUtils.getRandomId());
 		messageId = new MessageId(TestUtils.getRandomId());
 		privateMessageId = new MessageId(TestUtils.getRandomId());
+		group = new TestGroup(groupId, "Foo", null);
 		subject = "Foo";
 		timestamp = System.currentTimeMillis();
 		size = 1234;
@@ -120,22 +90,12 @@ public class H2DatabaseTest extends BriarTestCase {
 				timestamp, raw);
 		privateMessage = new TestMessage(privateMessageId, null, null, null,
 				subject, timestamp, raw);
-		group = groupFactory.createGroup(groupId, "Group name", null);
 		transportId = new TransportId(TestUtils.getRandomId());
-		localIndex = new TransportIndex(1);
-		remoteIndex = new TransportIndex(13);
 		properties = new TransportProperties(
 				Collections.singletonMap("foo", "bar"));
 		remoteProperties = Collections.singletonMap(contactId, properties);
-		Transport remoteTransport = new Transport(transportId, remoteIndex,
-				properties);
+		Transport remoteTransport = new Transport(transportId, properties);
 		remoteTransports = Collections.singletonList(remoteTransport);
-		Random r = new Random();
-		inSecret = new byte[32];
-		r.nextBytes(inSecret);
-		outSecret = new byte[32];
-		r.nextBytes(outSecret);
-		erase = new ArrayList<byte[]>();
 	}
 
 	@Before
@@ -149,7 +109,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 		assertFalse(db.containsContact(txn, contactId));
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		assertTrue(db.containsContact(txn, contactId));
 		assertFalse(db.containsSubscription(txn, groupId));
 		db.addSubscription(txn, group);
@@ -205,23 +165,20 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Create three contacts
 		assertFalse(db.containsContact(txn, contactId));
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		assertTrue(db.containsContact(txn, contactId));
 		assertFalse(db.containsContact(txn, contactId1));
-		assertEquals(contactId1,
-				db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId1, db.addContact(txn));
 		assertTrue(db.containsContact(txn, contactId1));
 		assertFalse(db.containsContact(txn, contactId2));
-		assertEquals(contactId2,
-				db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId2, db.addContact(txn));
 		assertTrue(db.containsContact(txn, contactId2));
 		// Delete the contact with the highest ID
 		db.removeContact(txn, contactId2);
 		assertFalse(db.containsContact(txn, contactId2));
 		// Add another contact - a new ID should be created
 		assertFalse(db.containsContact(txn, contactId3));
-		assertEquals(contactId3,
-				db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId3, db.addContact(txn));
 		assertTrue(db.containsContact(txn, contactId3));
 
 		db.commitTransaction(txn);
@@ -268,7 +225,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and store a private message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addPrivateMessage(txn, privateMessage, contactId);
 
 		// Removing the contact should remove the message
@@ -282,18 +239,18 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSendablePrivateMessagesMustHaveStatusNew()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact and store a private message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addPrivateMessage(txn, privateMessage, contactId);
 
 		// The message has no status yet, so it should not be sendable
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// Changing the status to NEW should make the message sendable
@@ -321,19 +278,19 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSendablePrivateMessagesMustFitCapacity()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact and store a private message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addPrivateMessage(txn, privateMessage, contactId);
 		db.setStatus(txn, contactId, privateMessageId, Status.NEW);
 
 		// The message is sendable, but too large to send
 		assertTrue(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, size - 1).iterator();
+				db.getSendableMessages(txn, contactId, size - 1).iterator();
 		assertFalse(it.hasNext());
 
 		// The message is just the right size to send
@@ -349,12 +306,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSendableGroupMessagesMustHavePositiveSendability()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -364,7 +321,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// The message should not be sendable
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// Changing the sendability to > 0 should make the message sendable
@@ -387,12 +344,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSendableGroupMessagesMustHaveStatusNew()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -402,7 +359,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// The message has no status yet, so it should not be sendable
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// Changing the status to Status.NEW should make the message sendable
@@ -434,7 +391,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addGroupMessage(txn, message);
@@ -444,7 +401,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// The contact is not subscribed, so the message should not be sendable
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// The contact subscribing should make the message sendable
@@ -467,12 +424,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSendableGroupMessagesMustBeNewerThanSubscriptions()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addGroupMessage(txn, message);
@@ -484,7 +441,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addSubscription(txn, contactId, group, timestamp + 1);
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// Changing the contact's subscription should make the message sendable
@@ -506,7 +463,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -517,7 +474,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// The message is sendable, but too large to send
 		assertTrue(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, size - 1).iterator();
+				db.getSendableMessages(txn, contactId, size - 1).iterator();
 		assertFalse(it.hasNext());
 
 		// The message is just the right size to send
@@ -537,7 +494,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addSubscription(txn, contactId, group, 0L);
 		db.addGroupMessage(txn, message);
@@ -548,7 +505,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// should not be sendable
 		assertFalse(db.hasSendableMessages(txn, contactId));
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertFalse(it.hasNext());
 
 		// Making the subscription visible should make the message sendable
@@ -570,7 +527,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and some batches to ack
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addBatchToAck(txn, contactId, batchId);
 		db.addBatchToAck(txn, contactId, batchId1);
 
@@ -597,7 +554,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and receive the same batch twice
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addBatchToAck(txn, contactId, batchId);
 		db.addBatchToAck(txn, contactId, batchId);
 
@@ -623,7 +580,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 
@@ -648,8 +605,8 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add two contacts, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
-		ContactId contactId1 = db.addContact(txn, inSecret, outSecret, erase);
+		assertEquals(contactId, db.addContact(txn));
+		ContactId contactId1 = db.addContact(txn);
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 
@@ -671,7 +628,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -681,7 +638,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Retrieve the message from the database and mark it as sent
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertTrue(it.hasNext());
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
@@ -710,7 +667,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -720,7 +677,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Get the message and mark it as sent
 		Iterator<MessageId> it =
-			db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
+				db.getSendableMessages(txn, contactId, ONE_MEGABYTE).iterator();
 		assertTrue(it.hasNext());
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
@@ -755,7 +712,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 
 		// Add some outstanding batches, a few ms apart
 		for(int i = 0; i < ids.length; i++) {
@@ -795,7 +752,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 
 		// Add some outstanding batches, a few ms apart
 		for(int i = 0; i < ids.length; i++) {
@@ -832,7 +789,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Check that each message is retrievable via its author
 		Iterator<MessageId> it =
-			db.getMessagesByAuthor(txn, authorId).iterator();
+				db.getMessagesByAuthor(txn, authorId).iterator();
 		assertTrue(it.hasNext());
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
@@ -1021,35 +978,29 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact with a transport
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.setTransports(txn, contactId, remoteTransports, 1);
 		assertEquals(remoteProperties,
 				db.getRemoteProperties(txn, transportId));
 
 		// Replace the transport properties
 		TransportProperties properties1 =
-			new TransportProperties(Collections.singletonMap("baz", "bam"));
-		Transport remoteTransport1 =
-			new Transport(transportId, remoteIndex, properties1);
+				new TransportProperties(Collections.singletonMap("baz", "bam"));
+		Transport remoteTransport1 = new Transport(transportId, properties1);
 		Collection<Transport> remoteTransports1 =
-			Collections.singletonList(remoteTransport1);
+				Collections.singletonList(remoteTransport1);
 		Map<ContactId, TransportProperties> remoteProperties1 =
-			Collections.singletonMap(contactId, properties1);
+				Collections.singletonMap(contactId, properties1);
 		db.setTransports(txn, contactId, remoteTransports1, 2);
 		assertEquals(remoteProperties1,
 				db.getRemoteProperties(txn, transportId));
 
-		// Remove the transport properties but leave the transport
+		// Remove the transport properties
 		properties1 = new TransportProperties();
-		remoteTransport1 = new Transport(transportId, remoteIndex, properties1);
+		remoteTransport1 = new Transport(transportId, properties1);
 		remoteTransports1 = Collections.singletonList(remoteTransport1);
 		remoteProperties1 = Collections.singletonMap(contactId, properties1);
 		db.setTransports(txn, contactId, remoteTransports1, 3);
-		assertEquals(remoteProperties1,
-				db.getRemoteProperties(txn, transportId));
-
-		// Remove the transport
-		db.setTransports(txn, contactId, Collections.<Transport>emptyList(), 4);
 		assertEquals(Collections.emptyMap(),
 				db.getRemoteProperties(txn, transportId));
 
@@ -1062,9 +1013,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Allocate a transport index
-		assertEquals(localIndex, db.addTransport(txn, transportId));
-
 		// Set the transport properties
 		db.setLocalProperties(txn, transportId, properties);
 		assertEquals(Collections.singletonList(properties),
@@ -1072,8 +1020,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Remove the transport properties but leave the transport
 		db.setLocalProperties(txn, transportId, new TransportProperties());
-		assertEquals(Collections.singletonList(Collections.emptyMap()),
-				db.getLocalTransports(txn));
+		assertEquals(Collections.emptyList(), db.getLocalTransports(txn));
 
 		db.commitTransaction(txn);
 		db.close();
@@ -1082,16 +1029,13 @@ public class H2DatabaseTest extends BriarTestCase {
 	@Test
 	public void testUpdateTransportConfig() throws Exception {
 		TransportConfig config =
-			new TransportConfig(Collections.singletonMap("foo", "bar"));
+				new TransportConfig(Collections.singletonMap("foo", "bar"));
 		TransportConfig config1 =
-			new TransportConfig(Collections.singletonMap("baz", "bam"));
+				new TransportConfig(Collections.singletonMap("baz", "bam"));
 
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Allocate a transport index
-		assertEquals(localIndex, db.addTransport(txn, transportId));
-
 		// Set the transport config
 		db.setConfig(txn, transportId, config);
 		assertEquals(config, db.getConfig(txn, transportId));
@@ -1114,31 +1058,29 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact with a transport
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.setTransports(txn, contactId, remoteTransports, 1);
 		assertEquals(remoteProperties,
 				db.getRemoteProperties(txn, transportId));
 
 		// Replace the transport properties using a timestamp of 2
 		TransportProperties properties1 =
-			new TransportProperties(Collections.singletonMap("baz", "bam"));
-		Transport remoteTransport1 =
-			new Transport(transportId, remoteIndex, properties1);
+				new TransportProperties(Collections.singletonMap("baz", "bam"));
+		Transport remoteTransport1 = new Transport(transportId, properties1);
 		Collection<Transport> remoteTransports1 =
-			Collections.singletonList(remoteTransport1);
+				Collections.singletonList(remoteTransport1);
 		Map<ContactId, TransportProperties> remoteProperties1 =
-			Collections.singletonMap(contactId, properties1);
+				Collections.singletonMap(contactId, properties1);
 		db.setTransports(txn, contactId, remoteTransports1, 2);
 		assertEquals(remoteProperties1,
 				db.getRemoteProperties(txn, transportId));
 
 		// Try to replace the transport properties using a timestamp of 1
 		TransportProperties properties2 =
-			new TransportProperties(Collections.singletonMap("quux", "etc"));
-		Transport remoteTransport2 =
-			new Transport(transportId, remoteIndex, properties2);
+				new TransportProperties(Collections.singletonMap("quux", "etc"));
+		Transport remoteTransport2 = new Transport(transportId, properties2);
 		Collection<Transport> remoteTransports2 =
-			Collections.singletonList(remoteTransport2);
+				Collections.singletonList(remoteTransport2);
 		db.setTransports(txn, contactId, remoteTransports2, 1);
 
 		// The old properties should still be there
@@ -1151,12 +1093,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetMessageIfSendableReturnsNullIfNotInDatabase()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addSubscription(txn, contactId, group, 0L);
 
@@ -1169,12 +1111,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetMessageIfSendableReturnsNullIfSeen()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addSubscription(txn, contactId, group, 0L);
 		db.addGroupMessage(txn, message);
@@ -1192,12 +1134,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetMessageIfSendableReturnsNullIfNotSendable()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addSubscription(txn, contactId, group, 0L);
 		db.addGroupMessage(txn, message);
@@ -1220,7 +1162,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Add a contact, subscribe to a group and store a message -
 		// the message is older than the contact's subscription
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, timestamp + 1);
@@ -1243,7 +1185,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -1263,12 +1205,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSetStatusSeenIfVisibleRequiresMessageInDatabase()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -1282,12 +1224,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSetStatusSeenIfVisibleRequiresLocalSubscription()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact with a subscription
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, contactId, group, 0L);
 
 		// There's no local subscription for the group
@@ -1299,12 +1241,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSetStatusSeenIfVisibleRequiresContactSubscription()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 		db.setStatus(txn, contactId, messageId, Status.NEW);
@@ -1318,12 +1260,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSetStatusSeenIfVisibleRequiresVisibility()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addGroupMessage(txn, message);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -1338,12 +1280,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSetStatusSeenIfVisibleReturnsTrueIfAlreadySeen()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -1360,12 +1302,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testSetStatusSeenIfVisibleReturnsTrueIfNew()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact, subscribe to a group and store a message
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		db.addVisibility(txn, contactId, groupId);
 		db.addSubscription(txn, contactId, group, 0L);
@@ -1386,7 +1328,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 		// The group should not be visible to the contact
 		assertEquals(Collections.emptyList(), db.getVisibility(txn, groupId));
@@ -1402,58 +1344,6 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
-	@Test
-	public void testGettingUnknownConnectionWindowReturnsDefault()
-	throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
-		// Get the connection window for a new index
-		ConnectionWindow w = db.getConnectionWindow(txn, contactId,
-				remoteIndex);
-		// The connection window should exist and be in the initial state
-		assertNotNull(w);
-		long top = TransportConstants.CONNECTION_WINDOW_SIZE / 2 - 1;
-		for(long l = 0; l <= top; l++) assertFalse(w.isSeen(l));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
-	@Test
-	public void testConnectionWindow() throws Exception {
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add a contact
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
-		// Get the connection window for a new index
-		ConnectionWindow w = db.getConnectionWindow(txn, contactId,
-				remoteIndex);
-		// The connection window should exist and be in the initial state
-		assertNotNull(w);
-		Map<Long, byte[]> unseen = w.getUnseen();
-		long top = TransportConstants.CONNECTION_WINDOW_SIZE / 2 - 1;
-		assertEquals(top + 1, unseen.size());
-		for(long l = 0; l <= top; l++) {
-			assertFalse(w.isSeen(l));
-			assertTrue(unseen.containsKey(l));
-		}
-		// Update the connection window and store it
-		w.setSeen(5);
-		db.setConnectionWindow(txn, contactId, remoteIndex, w);
-		// Check that the connection window was stored
-		w = db.getConnectionWindow(txn, contactId, remoteIndex);
-		assertNotNull(w);
-		top += 5;
-		for(long l = 0; l <= top; l++) assertEquals(l == 5, w.isSeen(l));
-
-		db.commitTransaction(txn);
-		db.close();
-	}
-
 	@Test
 	public void testGetGroupMessageParentWithNoParent() throws Exception {
 		Database<Connection> db = open(false);
@@ -1498,7 +1388,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetGroupMessageParentWithParentInAnotherGroup()
-	throws Exception {
+			throws Exception {
 		GroupId groupId1 = new GroupId(TestUtils.getRandomId());
 		Group group1 = groupFactory.createGroup(groupId1, "Group name", null);
 		Database<Connection> db = open(false);
@@ -1527,12 +1417,12 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetGroupMessageParentWithPrivateParent()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 
 		// A message with a private parent should return null
@@ -1551,7 +1441,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@Test
 	public void testGetGroupMessageParentWithParentInSameGroup()
-	throws Exception {
+			throws Exception {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
@@ -1581,7 +1471,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Connection txn = db.startTransaction();
 
 		// Add a contact and subscribe to a group
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 		db.addSubscription(txn, group);
 
 		// Store a couple of messages
@@ -1813,7 +1703,7 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Subscribe to the groups and add a contact
 		for(Group g : groups) db.addSubscription(txn, g);
-		assertEquals(contactId, db.addContact(txn, inSecret, outSecret, erase));
+		assertEquals(contactId, db.addContact(txn));
 
 		// Make the groups visible to the contact
 		Collections.shuffle(groups);
@@ -1849,7 +1739,6 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	private Database<Connection> open(boolean resume) throws Exception {
 		Database<Connection> db = new H2Database(testDir, password, MAX_SIZE,
-				connectionContextFactory, connectionWindowFactory,
 				groupFactory, new SystemClock());
 		db.open(resume);
 		return db;
@@ -1857,7 +1746,6 @@ public class H2DatabaseTest extends BriarTestCase {
 
 	@After
 	public void tearDown() {
-		erase.clear();
 		TestUtils.deleteTestDirectory(testDir);
 	}
 
@@ -1867,4 +1755,17 @@ public class H2DatabaseTest extends BriarTestCase {
 			return passwordString.toCharArray();
 		}
 	}
+
+	private class TestGroupFactory implements GroupFactory {
+
+		public Group createGroup(String name, byte[] publicKey)
+				throws IOException {
+			GroupId id = new GroupId(TestUtils.getRandomId());
+			return new TestGroup(id, name, publicKey);
+		}
+
+		public Group createGroup(GroupId id, String name, byte[] publicKey) {
+			return new TestGroup(id, name, publicKey);
+		}
+	}
 }
diff --git a/test/net/sf/briar/plugins/DuplexTest.java b/test/net/sf/briar/plugins/DuplexTest.java
index c247415e46079e5c59f91d570d6d08f0c8a8f1cd..cd87fca94b049c2e427f465bf6b551c1192b940c 100644
--- a/test/net/sf/briar/plugins/DuplexTest.java
+++ b/test/net/sf/briar/plugins/DuplexTest.java
@@ -16,7 +16,7 @@ abstract class DuplexTest {
 	protected static final String RESPONSE = "Potatoes!";
 	protected static final long INVITATION_TIMEOUT = 30 * 1000;
 
-	protected final ContactId contactId = new ContactId(0);
+	protected final ContactId contactId = new ContactId(234);
 
 	protected DuplexPlugin plugin = null;
 
diff --git a/test/net/sf/briar/plugins/InvitationStarterImplTest.java b/test/net/sf/briar/plugins/InvitationStarterImplTest.java
index 8dfe5dace6d5dbae98f9798538d0283302c98068..b64c42b5d6d1fa82a22dcc9c791ef537c124c57e 100644
--- a/test/net/sf/briar/plugins/InvitationStarterImplTest.java
+++ b/test/net/sf/briar/plugins/InvitationStarterImplTest.java
@@ -16,6 +16,8 @@ import com.google.inject.Injector;
 
 public class InvitationStarterImplTest extends BriarTestCase {
 
+	// FIXME: This is actually a test of CryptoComponent
+
 	private final CryptoComponent crypto;
 
 	public InvitationStarterImplTest() {
@@ -32,13 +34,8 @@ public class InvitationStarterImplTest extends BriarTestCase {
 		KeyPair b = crypto.generateAgreementKeyPair();
 		byte[] bPub = b.getPublic().getEncoded();
 		PrivateKey bPriv = b.getPrivate();
-		byte[][] aSecrets = crypto.deriveInitialSecrets(aPub, bPub, aPriv, 123,
-				true);
-		byte[][] bSecrets = crypto.deriveInitialSecrets(bPub, aPub, bPriv, 123,
-				false);
-		assertEquals(2, aSecrets.length);
-		assertEquals(2, bSecrets.length);
-		assertArrayEquals(aSecrets[0], bSecrets[0]);
-		assertArrayEquals(aSecrets[1], bSecrets[1]);
+		byte[] aSecret = crypto.deriveInitialSecret(aPub, bPub, aPriv, true);
+		byte[] bSecret = crypto.deriveInitialSecret(bPub, aPub, bPriv, false);
+		assertArrayEquals(aSecret, bSecret);
 	}
 }
diff --git a/test/net/sf/briar/plugins/PluginManagerImplTest.java b/test/net/sf/briar/plugins/PluginManagerImplTest.java
index 8c734e1ae391e56d7967f007816f31a87ce071c5..79fcee1b1a161465ebb03c09739d1d701b69dca3 100644
--- a/test/net/sf/briar/plugins/PluginManagerImplTest.java
+++ b/test/net/sf/briar/plugins/PluginManagerImplTest.java
@@ -3,14 +3,12 @@ package net.sf.briar.plugins;
 import java.util.Collection;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
-import java.util.concurrent.atomic.AtomicInteger;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionDispatcher;
 import net.sf.briar.api.ui.UiCallback;
 
@@ -29,13 +27,8 @@ public class PluginManagerImplTest extends BriarTestCase {
 		final ConnectionDispatcher dispatcher =
 				context.mock(ConnectionDispatcher.class);
 		final UiCallback uiCallback = context.mock(UiCallback.class);
-		final AtomicInteger index = new AtomicInteger(0);
 		context.checking(new Expectations() {{
 			oneOf(poller).start(with(any(Collection.class)));
-			allowing(db).getLocalIndex(with(any(TransportId.class)));
-			will(returnValue(null));
-			allowing(db).addTransport(with(any(TransportId.class)));
-			will(returnValue(new TransportIndex(index.getAndIncrement())));
 			allowing(db).getConfig(with(any(TransportId.class)));
 			will(returnValue(new TransportConfig()));
 			allowing(db).getLocalProperties(with(any(TransportId.class)));
diff --git a/test/net/sf/briar/plugins/email/GmailPluginTest.java b/test/net/sf/briar/plugins/email/GmailPluginTest.java
index cf5ddc8d5529b01a376388704369cae8b7b3b2f1..8d20fc3dbce1c0132c79e397ffbb990a24e41e4b 100644
--- a/test/net/sf/briar/plugins/email/GmailPluginTest.java
+++ b/test/net/sf/briar/plugins/email/GmailPluginTest.java
@@ -1,17 +1,14 @@
 package net.sf.briar.plugins.email;
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
 
 import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
-import java.util.Set;
-import java.util.Map.Entry;
 import java.util.concurrent.Executors;
 
-import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
@@ -19,7 +16,6 @@ import net.sf.briar.api.plugins.simplex.SimplexPluginCallback;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 
-import org.jmock.Mockery;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -47,7 +43,7 @@ public class GmailPluginTest {
 
 		props1 = new TransportProperties();
 		props1.put("email", System.getenv("CONTACT1_EMAIL"));
-		test1 = new ContactId(12);
+		test1 = new ContactId(234);
 		map.put(test1, props1);
 		assertEquals(1, map.size());
 
@@ -120,7 +116,7 @@ public class GmailPluginTest {
 		GmailPlugin pluginTest = new GmailPlugin(
 				Executors.newSingleThreadExecutor(), callback);
 		assertEquals(true, pluginTest.connectSMTP(test1));
-		assertEquals(false, pluginTest.connectSMTP(new ContactId(7)));
+		assertEquals(false, pluginTest.connectSMTP(new ContactId(123)));
 		pluginTest.stop();
 	}
 
diff --git a/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java b/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java
index 8de3042bc620c8545358bc7aa146a6f083ce07b2..58ac2f5cf871039c8b6e6bc74e77b86428cf1ed9 100644
--- a/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java
+++ b/test/net/sf/briar/plugins/file/RemovableDrivePluginTest.java
@@ -28,7 +28,7 @@ import org.junit.Test;
 public class RemovableDrivePluginTest extends BriarTestCase {
 
 	private final File testDir = TestUtils.getTestDirectory();
-	private final ContactId contactId = new ContactId(0);
+	private final ContactId contactId = new ContactId(234);
 
 	@Before
 	public void setUp() {
diff --git a/test/net/sf/briar/plugins/socket/SimpleSocketPluginTest.java b/test/net/sf/briar/plugins/socket/SimpleSocketPluginTest.java
index 3b160df85c3f85c9bc7451d70740bb65518fe8c2..af06bcbeadab203cb75d3c6e6fd0da90faf026be 100644
--- a/test/net/sf/briar/plugins/socket/SimpleSocketPluginTest.java
+++ b/test/net/sf/briar/plugins/socket/SimpleSocketPluginTest.java
@@ -23,7 +23,7 @@ import org.junit.Test;
 
 public class SimpleSocketPluginTest extends BriarTestCase {
 
-	private final ContactId contactId = new ContactId(0);
+	private final ContactId contactId = new ContactId(234);
 
 	@Test
 	public void testIncomingConnection() throws Exception {
diff --git a/test/net/sf/briar/plugins/tor/TorPluginTest.java b/test/net/sf/briar/plugins/tor/TorPluginTest.java
index cfc2884fe5e255ce8d52061bdfe3f7ab8cfa0e36..76d691fb67ad1a2fd5fba7ab4d402fd78d937af1 100644
--- a/test/net/sf/briar/plugins/tor/TorPluginTest.java
+++ b/test/net/sf/briar/plugins/tor/TorPluginTest.java
@@ -22,7 +22,7 @@ import org.junit.Test;
 
 public class TorPluginTest extends BriarTestCase {
 
-	private final ContactId contactId = new ContactId(1);
+	private final ContactId contactId = new ContactId(234);
 
 	@Test
 	public void testHiddenService() throws Exception {
diff --git a/test/net/sf/briar/protocol/ConstantsTest.java b/test/net/sf/briar/protocol/ConstantsTest.java
index 784ecb610c0c5d477725059791fdc96665900bb5..e4c6af3a1a9a52e32c31819b974701b0521addd4 100644
--- a/test/net/sf/briar/protocol/ConstantsTest.java
+++ b/test/net/sf/briar/protocol/ConstantsTest.java
@@ -35,7 +35,6 @@ import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.crypto.CryptoModule;
@@ -158,8 +157,7 @@ public class ConstantsTest extends BriarTestCase {
 		Collection<Transport> transports = new ArrayList<Transport>();
 		for(int i = 0; i < MAX_TRANSPORTS; i++) {
 			TransportId id = new TransportId(TestUtils.getRandomId());
-			TransportIndex index = new TransportIndex(i);
-			Transport t = new Transport(id, index);
+			Transport t = new Transport(id);
 			for(int j = 0; j < MAX_PROPERTIES_PER_TRANSPORT; j++) {
 				String key = createRandomString(MAX_PROPERTY_LENGTH);
 				String value = createRandomString(MAX_PROPERTY_LENGTH);
diff --git a/test/net/sf/briar/protocol/ProtocolReadWriteTest.java b/test/net/sf/briar/protocol/ProtocolReadWriteTest.java
index 54434c6b5c1d24bd2a5a221d579abb6032dec702..51c15ea2055018960e3935a8b2d83df6e063e9c5 100644
--- a/test/net/sf/briar/protocol/ProtocolReadWriteTest.java
+++ b/test/net/sf/briar/protocol/ProtocolReadWriteTest.java
@@ -28,7 +28,6 @@ import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.SubscriptionUpdate;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.serial.SerialModule;
@@ -71,8 +70,7 @@ public class ProtocolReadWriteTest extends BriarTestCase {
 		bitSet.set(7);
 		subscriptions = Collections.singletonMap(group, 123L);
 		TransportId transportId = new TransportId(TestUtils.getRandomId());
-		TransportIndex transportIndex = new TransportIndex(13);
-		Transport transport = new Transport(transportId, transportIndex,
+		Transport transport = new Transport(transportId,
 				Collections.singletonMap("bar", "baz"));
 		transports = Collections.singletonList(transport);
 	}
diff --git a/test/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java b/test/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java
index 8706ad15c53ac9e5bba577f928a142b2a53cc408..09109ee844531caa2bdbfd7f939cb3fa122f5373 100644
--- a/test/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java
+++ b/test/net/sf/briar/protocol/simplex/OutgoingSimplexConnectionTest.java
@@ -1,7 +1,9 @@
 package net.sf.briar.protocol.simplex;
 
+import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
+import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayOutputStream;
@@ -12,20 +14,19 @@ import java.util.concurrent.Executors;
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.TestUtils;
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.KeyManager;
 import net.sf.briar.api.db.DatabaseComponent;
 import net.sf.briar.api.db.DatabaseExecutor;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.BatchId;
-import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.RawBatch;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.UniqueId;
 import net.sf.briar.api.transport.ConnectionContext;
+import net.sf.briar.api.transport.ConnectionRecogniser;
 import net.sf.briar.api.transport.ConnectionRegistry;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
-import net.sf.briar.api.transport.TransportConstants;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.protocol.ProtocolModule;
 import net.sf.briar.protocol.duplex.DuplexProtocolModule;
@@ -43,24 +44,31 @@ import com.google.inject.Module;
 
 public class OutgoingSimplexConnectionTest extends BriarTestCase {
 
+	// FIXME: This is an integration test, not a unit test
+
 	private final Mockery context;
 	private final DatabaseComponent db;
+	private final KeyManager keyManager;
+	private final ConnectionRecogniser connRecogniser;
 	private final ConnectionRegistry connRegistry;
 	private final ConnectionWriterFactory connFactory;
 	private final ProtocolWriterFactory protoFactory;
 	private final ContactId contactId;
 	private final TransportId transportId;
-	private final TransportIndex transportIndex;
 	private final byte[] secret;
 
 	public OutgoingSimplexConnectionTest() {
 		super();
 		context = new Mockery();
 		db = context.mock(DatabaseComponent.class);
+		keyManager = context.mock(KeyManager.class);
+		connRecogniser = context.mock(ConnectionRecogniser.class);
 		Module testModule = new AbstractModule() {
 			@Override
 			public void configure() {
 				bind(DatabaseComponent.class).toInstance(db);
+				bind(KeyManager.class).toInstance(keyManager);
+				bind(ConnectionRecogniser.class).toInstance(connRecogniser);
 				bind(Executor.class).annotatedWith(
 						DatabaseExecutor.class).toInstance(
 								Executors.newCachedThreadPool());
@@ -73,9 +81,8 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 		connRegistry = i.getInstance(ConnectionRegistry.class);
 		connFactory = i.getInstance(ConnectionWriterFactory.class);
 		protoFactory = i.getInstance(ProtocolWriterFactory.class);
-		contactId = new ContactId(1);
+		contactId = new ContactId(234);
 		transportId = new TransportId(TestUtils.getRandomId());
-		transportIndex = new TransportIndex(13);
 		secret = new byte[32];
 	}
 
@@ -83,40 +90,31 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 	public void testConnectionTooShort() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, ProtocolConstants.MAX_PACKET_LENGTH, true);
+				out, MAX_PACKET_LENGTH, true);
+		byte[] tag = new byte[TAG_LENGTH];
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, secret, 0L, true);
 		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
-				connRegistry, connFactory, protoFactory, contactId, transportId,
-				transportIndex, transport);
-		final ConnectionContext ctx = context.mock(ConnectionContext.class);
-		context.checking(new Expectations() {{
-			oneOf(db).getConnectionContext(contactId, transportIndex);
-			will(returnValue(ctx));
-			oneOf(ctx).getSecret();
-			will(returnValue(secret));
-		}});
+				connRegistry, connFactory, protoFactory, ctx, transport);
 		connection.write();
 		// Nothing should have been written
 		assertEquals(0, out.size());
 		// The transport should have been disposed with exception == true
 		assertTrue(transport.getDisposed());
 		assertTrue(transport.getException());
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testNothingToSend() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, TransportConstants.MIN_CONNECTION_LENGTH, true);
+				out, MIN_CONNECTION_LENGTH, true);
+		byte[] tag = new byte[TAG_LENGTH];
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, secret, 0L, true);
 		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
-				connRegistry, connFactory, protoFactory, contactId, transportId,
-				transportIndex, transport);
-		final ConnectionContext ctx = context.mock(ConnectionContext.class);
+				connRegistry, connFactory, protoFactory, ctx, transport);
 		context.checking(new Expectations() {{
-			oneOf(db).getConnectionContext(contactId, transportIndex);
-			will(returnValue(ctx));
-			oneOf(ctx).getSecret();
-			will(returnValue(secret));
 			// No transports to send
 			oneOf(db).generateTransportUpdate(contactId);
 			will(returnValue(null));
@@ -143,20 +141,17 @@ public class OutgoingSimplexConnectionTest extends BriarTestCase {
 	public void testSomethingToSend() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
-				out, TransportConstants.MIN_CONNECTION_LENGTH, true);
+				out, MIN_CONNECTION_LENGTH, true);
+		byte[] tag = new byte[TAG_LENGTH];
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, secret, 0L, true);
 		OutgoingSimplexConnection connection = new OutgoingSimplexConnection(db,
-				connRegistry, connFactory, protoFactory, contactId, transportId,
-				transportIndex, transport);
-		final ConnectionContext ctx = context.mock(ConnectionContext.class);
+				connRegistry, connFactory, protoFactory, ctx, transport);
 		final Ack ack = context.mock(Ack.class);
 		final BatchId batchId = new BatchId(TestUtils.getRandomId());
 		final RawBatch batch = context.mock(RawBatch.class);
 		final byte[] message = new byte[1234];
 		context.checking(new Expectations() {{
-			oneOf(db).getConnectionContext(contactId, transportIndex);
-			will(returnValue(ctx));
-			oneOf(ctx).getSecret();
-			will(returnValue(secret));
 			// No transports to send
 			oneOf(db).generateTransportUpdate(contactId);
 			will(returnValue(null));
diff --git a/test/net/sf/briar/protocol/simplex/SimplexConnectionReadWriteTest.java b/test/net/sf/briar/protocol/simplex/SimplexConnectionReadWriteTest.java
index 036392a974491c7e1d48735a15f05e94948d38b4..7628ec6322931d2d516ea8cd7343bd5e8ff52112 100644
--- a/test/net/sf/briar/protocol/simplex/SimplexConnectionReadWriteTest.java
+++ b/test/net/sf/briar/protocol/simplex/SimplexConnectionReadWriteTest.java
@@ -23,7 +23,6 @@ import net.sf.briar.api.protocol.ProtocolReaderFactory;
 import net.sf.briar.api.protocol.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.Transport;
 import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.protocol.TransportUpdate;
 import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
@@ -49,11 +48,12 @@ import com.google.inject.Injector;
 
 public class SimplexConnectionReadWriteTest extends BriarTestCase {
 
+	// FIXME: This is an integration test, not a unit test
+
 	private final File testDir = TestUtils.getTestDirectory();
 	private final File aliceDir = new File(testDir, "alice");
 	private final File bobDir = new File(testDir, "bob");
 	private final TransportId transportId;
-	private final TransportIndex transportIndex;
 	private final byte[] aliceToBobSecret, bobToAliceSecret;
 
 	private Injector alice, bob;
@@ -61,7 +61,6 @@ public class SimplexConnectionReadWriteTest extends BriarTestCase {
 	public SimplexConnectionReadWriteTest() throws Exception {
 		super();
 		transportId = new TransportId(TestUtils.getRandomId());
-		transportIndex = new TransportIndex(1);
 		// Create matching secrets for Alice and Bob
 		Random r = new Random();
 		aliceToBobSecret = new byte[32];
@@ -102,7 +101,7 @@ public class SimplexConnectionReadWriteTest extends BriarTestCase {
 		DatabaseComponent db = alice.getInstance(DatabaseComponent.class);
 		db.open(false);
 		// Add Bob as a contact and send him a message
-		ContactId contactId = db.addContact(bobToAliceSecret, aliceToBobSecret);
+		ContactId contactId = db.addContact();
 		String subject = "Hello";
 		byte[] body = "Hi Bob!".getBytes("UTF-8");
 		MessageFactory messageFactory = alice.getInstance(MessageFactory.class);
@@ -111,16 +110,19 @@ public class SimplexConnectionReadWriteTest extends BriarTestCase {
 		// Create an outgoing batch connection
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		ConnectionRegistry connRegistry =
-			alice.getInstance(ConnectionRegistry.class);
+				alice.getInstance(ConnectionRegistry.class);
 		ConnectionWriterFactory connFactory =
-			alice.getInstance(ConnectionWriterFactory.class);
+				alice.getInstance(ConnectionWriterFactory.class);
 		ProtocolWriterFactory protoFactory =
-			alice.getInstance(ProtocolWriterFactory.class);
+				alice.getInstance(ProtocolWriterFactory.class);
 		TestSimplexTransportWriter transport = new TestSimplexTransportWriter(
 				out, Long.MAX_VALUE, false);
+		// FIXME: Encode the tag
+		byte[] tag = new byte[TAG_LENGTH];
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, aliceToBobSecret, 0L, true);
 		OutgoingSimplexConnection simplex = new OutgoingSimplexConnection(db,
-				connRegistry, connFactory, protoFactory, contactId, transportId,
-				transportIndex, transport);
+				connRegistry, connFactory, protoFactory, ctx, transport);
 		// Write whatever needs to be written
 		simplex.write();
 		assertTrue(transport.getDisposed());
@@ -139,14 +141,12 @@ public class SimplexConnectionReadWriteTest extends BriarTestCase {
 		MessageListener listener = new MessageListener();
 		db.addListener(listener);
 		// Add Alice as a contact
-		ContactId contactId = db.addContact(aliceToBobSecret, bobToAliceSecret);
-		// Add the transport
-		assertEquals(transportIndex, db.addTransport(transportId));
+		ContactId contactId = db.addContact();
 		// Fake a transport update from Alice
 		TransportUpdate transportUpdate = new TransportUpdate() {
 
 			public Collection<Transport> getTransports() {
-				Transport t = new Transport(transportId, transportIndex);
+				Transport t = new Transport(transportId);
 				return Collections.singletonList(t);
 			}
 
@@ -164,19 +164,17 @@ public class SimplexConnectionReadWriteTest extends BriarTestCase {
 		ConnectionContext ctx = rec.acceptConnection(transportId, tag);
 		assertNotNull(ctx);
 		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportIndex, ctx.getTransportIndex());
 		// Create an incoming batch connection
 		ConnectionRegistry connRegistry =
-			bob.getInstance(ConnectionRegistry.class);
+				bob.getInstance(ConnectionRegistry.class);
 		ConnectionReaderFactory connFactory =
-			bob.getInstance(ConnectionReaderFactory.class);
+				bob.getInstance(ConnectionReaderFactory.class);
 		ProtocolReaderFactory protoFactory =
-			bob.getInstance(ProtocolReaderFactory.class);
+				bob.getInstance(ProtocolReaderFactory.class);
 		TestSimplexTransportReader transport = new TestSimplexTransportReader(in);
 		IncomingSimplexConnection simplex = new IncomingSimplexConnection(
 				new ImmediateExecutor(), new ImmediateExecutor(), db,
-				connRegistry, connFactory, protoFactory, ctx, transportId,
-				transport);
+				connRegistry, connFactory, protoFactory, ctx, transport);
 		// No messages should have been added yet
 		assertFalse(listener.messagesAdded);
 		// Read whatever needs to be read
diff --git a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
deleted file mode 100644
index e4cd00d3cab2249b2ef3aec8c25e094c352969c6..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
+++ /dev/null
@@ -1,624 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Random;
-import java.util.concurrent.Executor;
-
-import javax.crypto.Cipher;
-
-import net.sf.briar.BriarTestCase;
-import net.sf.briar.TestUtils;
-import net.sf.briar.api.ContactId;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.crypto.ErasableKey;
-import net.sf.briar.api.db.DatabaseComponent;
-import net.sf.briar.api.db.event.ContactRemovedEvent;
-import net.sf.briar.api.db.event.RemoteTransportsUpdatedEvent;
-import net.sf.briar.api.db.event.TransportAddedEvent;
-import net.sf.briar.api.protocol.Transport;
-import net.sf.briar.api.protocol.TransportId;
-import net.sf.briar.api.protocol.TransportIndex;
-import net.sf.briar.api.transport.ConnectionContext;
-import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.plugins.ImmediateExecutor;
-
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class ConnectionRecogniserImplTest extends BriarTestCase {
-
-	private final CryptoComponent crypto;
-	private final ContactId contactId;
-	private final byte[] inSecret;
-	private final TransportId transportId;
-	private final TransportIndex localIndex, remoteIndex;
-	private final Collection<Transport> localTransports, remoteTransports;
-
-	public ConnectionRecogniserImplTest() {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		crypto = i.getInstance(CryptoComponent.class);
-		contactId = new ContactId(1);
-		inSecret = new byte[32];
-		new Random().nextBytes(inSecret);
-		transportId = new TransportId(TestUtils.getRandomId());
-		localIndex = new TransportIndex(13);
-		remoteIndex = new TransportIndex(7);
-		Map<String, String> properties = Collections.singletonMap("foo", "bar");
-		Transport localTransport = new Transport(transportId, localIndex,
-				properties);
-		localTransports = Collections.singletonList(localTransport);
-		Transport remoteTransport = new Transport(transportId, remoteIndex,
-				properties);
-		remoteTransports = Collections.singletonList(remoteTransport);
-	}
-
-	@Test
-	public void testUnexpectedIv() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		assertNull(c.acceptConnection(transportId, new byte[TAG_LENGTH]));
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testExpectedIv() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// The tag should not be expected by the wrong transport
-		TransportId wrong = new TransportId(TestUtils.getRandomId());
-		assertNull(c.acceptConnection(wrong, tag));
-		// The tag should be expected by the right transport
-		ConnectionContext ctx = c.acceptConnection(transportId, tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testContactRemovedAfterInit() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise before removing contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Ensure the recogniser is initialised
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, new byte[TAG_LENGTH]));
-		assertTrue(c.isInitialised());
-		// Remove the contact
-		c.eventOccurred(new ContactRemovedEvent(contactId));
-		// The tag should not be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testContactRemovedBeforeInit() throws Exception {
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise after removing contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.emptyList()));
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Remove the contact
-		c.eventOccurred(new ContactRemovedEvent(contactId));
-		// The tag should not be expected
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, tag));
-		assertTrue(c.isInitialised());
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLocalTransportAddedAfterInit() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise before adding transport
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			// Add the transport
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// The tag should not be expected
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, tag));
-		assertTrue(c.isInitialised());
-		// Add the transport
-		c.eventOccurred(new TransportAddedEvent(transportId));
-		// The tag should be expected
-		ConnectionContext ctx = c.acceptConnection(transportId, tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLocalTransportAddedBeforeInit() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise after adding transport
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Add the transport
-		c.eventOccurred(new TransportAddedEvent(transportId));
-		// The tag should be expected
-		assertFalse(c.isInitialised());
-		ConnectionContext ctx = c.acceptConnection(transportId, tag);
-		assertTrue(c.isInitialised());
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRemoteTransportAddedAfterInit() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise before updating the contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(null));
-			// Update the contact
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// The tag should not be expected
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, tag));
-		assertTrue(c.isInitialised());
-		// Update the contact
-		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
-				remoteTransports));
-		// The tag should be expected
-		ConnectionContext ctx = c.acceptConnection(transportId, tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRemoteTransportAddedBeforeInit() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise after updating the contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Update the contact
-		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
-				remoteTransports));
-		// The tag should be expected
-		assertFalse(c.isInitialised());
-		ConnectionContext ctx = c.acceptConnection(transportId, tag);
-		assertTrue(c.isInitialised());
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRemoteTransportRemovedAfterInit() throws Exception {
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise before updating the contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Ensure the recogniser is initialised
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, new byte[TAG_LENGTH]));
-		assertTrue(c.isInitialised());
-		// Update the contact
-		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
-				Collections.<Transport>emptyList()));
-		// The tag should not be expected
-		assertNull(c.acceptConnection(transportId, tag));
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRemoteTransportRemovedBeforeInit() throws Exception {
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise after updating the contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(null));
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Update the contact
-		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
-				Collections.<Transport>emptyList()));
-		// The tag should not be expected
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, tag));
-		assertTrue(c.isInitialised());
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRemoteTransportIndexChangedAfterInit() throws Exception {
-		// The contact changes the transport ID <-> index relationships
-		final TransportId transportId1 =
-			new TransportId(TestUtils.getRandomId());
-		final TransportIndex remoteIndex1 = new TransportIndex(11);
-		Map<String, String> properties = Collections.singletonMap("foo", "bar");
-		Transport remoteTransport = new Transport(transportId, remoteIndex1,
-				properties);
-		Transport remoteTransport1 = new Transport(transportId1, remoteIndex,
-				properties);
-		Collection<Transport> remoteTransports1 = Arrays.asList(
-				new Transport[] {remoteTransport, remoteTransport1});
-		// Use two local transports for this test
-		TransportIndex localIndex1 = new TransportIndex(17);
-		Transport localTransport = new Transport(transportId, localIndex,
-				properties);
-		Transport localTransport1 = new Transport(transportId1, localIndex1,
-				properties);
-		final Collection<Transport> localTransports1 = Arrays.asList(
-				new Transport[] {localTransport, localTransport1});
-
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		final ConnectionWindow window1 = createConnectionWindow(remoteIndex1);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise before updating the contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports1));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			// First, transportId <-> remoteIndex, transportId1 <-> remoteIndex
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).getRemoteIndex(contactId, transportId1);
-			will(returnValue(remoteIndex1));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex1);
-			will(returnValue(window1));
-			// Later, transportId <-> remoteIndex1, transportId1 <-> remoteIndex
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex1);
-			will(returnValue(window1));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Ensure the recogniser is initialised
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, new byte[TAG_LENGTH]));
-		assertTrue(c.isInitialised());
-		// Update the contact
-		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
-				remoteTransports1));
-		// The tag should not be expected by the old transport
-		assertNull(c.acceptConnection(transportId, tag));
-		// The tag should be expected by the new transport
-		ConnectionContext ctx = c.acceptConnection(transportId1, tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId1, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRemoteTransportIndexChangedBeforeInit() throws Exception {
-		// The contact changes the transport ID <-> index relationships
-		final TransportId transportId1 =
-			new TransportId(TestUtils.getRandomId());
-		final TransportIndex remoteIndex1 = new TransportIndex(11);
-		Map<String, String> properties = Collections.singletonMap("foo", "bar");
-		Transport remoteTransport = new Transport(transportId, remoteIndex1,
-				properties);
-		Transport remoteTransport1 = new Transport(transportId1, remoteIndex,
-				properties);
-		Collection<Transport> remoteTransports1 = Arrays.asList(
-				new Transport[] {remoteTransport, remoteTransport1});
-		// Use two local transports for this test
-		TransportIndex localIndex1 = new TransportIndex(17);
-		Transport localTransport = new Transport(transportId, localIndex,
-				properties);
-		Transport localTransport1 = new Transport(transportId1, localIndex1,
-				properties);
-		final Collection<Transport> localTransports1 = Arrays.asList(
-				new Transport[] {localTransport, localTransport1});
-
-		final ConnectionWindow window = createConnectionWindow(remoteIndex);
-		final ConnectionWindow window1 = createConnectionWindow(remoteIndex1);
-		Mockery context = new Mockery();
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Initialise after updating the contact
-			oneOf(db).addListener(with(any(ConnectionRecogniserImpl.class)));
-			oneOf(db).getLocalTransports();
-			will(returnValue(localTransports1));
-			oneOf(db).getContacts();
-			will(returnValue(Collections.singletonList(contactId)));
-			// First, transportId <-> remoteIndex1, transportId1 <-> remoteIndex
-			oneOf(db).getRemoteIndex(contactId, transportId);
-			will(returnValue(remoteIndex1));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex1);
-			will(returnValue(window1));
-			oneOf(db).getRemoteIndex(contactId, transportId1);
-			will(returnValue(remoteIndex));
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			// Update the window
-			oneOf(db).getConnectionWindow(contactId, remoteIndex);
-			will(returnValue(window));
-			oneOf(db).setConnectionWindow(contactId, remoteIndex, window);
-		}});
-		Executor executor = new ImmediateExecutor();
-		ConnectionRecogniserImpl c = new ConnectionRecogniserImpl(executor, db,
-				crypto);
-		byte[] tag = calculateTag();
-		// Update the contact
-		c.eventOccurred(new RemoteTransportsUpdatedEvent(contactId,
-				remoteTransports1));
-		// The tag should not be expected by the old transport
-		assertFalse(c.isInitialised());
-		assertNull(c.acceptConnection(transportId, tag));
-		assertTrue(c.isInitialised());
-		// The tag should be expected by the new transport
-		ConnectionContext ctx = c.acceptConnection(transportId1, tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(remoteIndex, ctx.getTransportIndex());
-		assertEquals(3, ctx.getConnectionNumber());
-		// The tag should no longer be expected
-		assertNull(c.acceptConnection(transportId1, tag));
-		// The window should have advanced
-		Map<Long, byte[]> unseen = window.getUnseen();
-		assertEquals(19, unseen.size());
-		for(int i = 0; i < 19; i++) {
-			assertEquals(i != 3, unseen.containsKey(Long.valueOf(i)));
-		}
-		context.assertIsSatisfied();
-	}
-
-	private ConnectionWindow createConnectionWindow(TransportIndex index) {
-		return new ConnectionWindowImpl(crypto, index, inSecret) {
-			@Override
-			public void erase() {}
-		};
-	}
-
-	private byte[] calculateTag() throws Exception {
-		// Calculate the shared secret for connection number 3
-		byte[] secret = inSecret;
-		for(int i = 0; i < 4; i++) {
-			secret = crypto.deriveNextSecret(secret, remoteIndex.getInt(), i);
-		}
-		// Calculate the expected tag for connection number 3
-		ErasableKey tagKey = crypto.deriveTagKey(secret, true);
-		Cipher tagCipher = crypto.getTagCipher();
-		byte[] tag = new byte[TAG_LENGTH];
-		TagEncoder.encodeTag(tag, tagCipher, tagKey);
-		return tag;
-	}
-}
diff --git a/test/net/sf/briar/transport/ConnectionWindowImplTest.java b/test/net/sf/briar/transport/ConnectionWindowImplTest.java
index 61e253ca912143e5dd9004ef15208d8d649373c1..359945e468403a47eb99ce7a01199c3709ff42b1 100644
--- a/test/net/sf/briar/transport/ConnectionWindowImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionWindowImplTest.java
@@ -1,39 +1,19 @@
 package net.sf.briar.transport;
 
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Random;
+import java.util.HashSet;
+import java.util.Set;
 
 import net.sf.briar.BriarTestCase;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.protocol.TransportIndex;
 import net.sf.briar.api.transport.ConnectionWindow;
-import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.util.ByteUtils;
 
 import org.junit.Test;
 
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
 public class ConnectionWindowImplTest extends BriarTestCase {
 
-	private final CryptoComponent crypto;
-	private final byte[] secret;
-	private final TransportIndex transportIndex = new TransportIndex(13);
-
-	public ConnectionWindowImplTest() {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		crypto = i.getInstance(CryptoComponent.class);
-		secret = new byte[32];
-		new Random().nextBytes(secret);
-	}
-
 	@Test
 	public void testWindowSliding() {
-		ConnectionWindow w = new ConnectionWindowImpl(crypto,
-				transportIndex, secret);
+		ConnectionWindow w = new ConnectionWindowImpl();
 		for(int i = 0; i < 100; i++) {
 			assertFalse(w.isSeen(i));
 			w.setSeen(i);
@@ -43,8 +23,7 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 
 	@Test
 	public void testWindowJumping() {
-		ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
-				secret);
+		ConnectionWindow w = new ConnectionWindowImpl();
 		for(int i = 0; i < 100; i += 13) {
 			assertFalse(w.isSeen(i));
 			w.setSeen(i);
@@ -54,8 +33,7 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 
 	@Test
 	public void testWindowUpperLimit() {
-		ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
-				secret);
+		ConnectionWindow w = new ConnectionWindowImpl();
 		// Centre is 0, highest value in window is 15
 		w.setSeen(15);
 		// Centre is 16, highest value in window is 31
@@ -66,11 +44,11 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 			fail();
 		} catch(IllegalArgumentException expected) {}
 		// Values greater than 2^32 - 1 should never be allowed
-		Map<Long, byte[]> unseen = new HashMap<Long, byte[]>();
+		Set<Long> unseen = new HashSet<Long>();
 		for(int i = 0; i < 32; i++) {
-			unseen.put(ByteUtils.MAX_32_BIT_UNSIGNED - i, secret);
+			unseen.add(ByteUtils.MAX_32_BIT_UNSIGNED - i);
 		}
-		w = new ConnectionWindowImpl(crypto, transportIndex, unseen);
+		w = new ConnectionWindowImpl(unseen);
 		w.setSeen(ByteUtils.MAX_32_BIT_UNSIGNED);
 		try {
 			w.setSeen(ByteUtils.MAX_32_BIT_UNSIGNED + 1);
@@ -80,8 +58,7 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 
 	@Test
 	public void testWindowLowerLimit() {
-		ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
-				secret);
+		ConnectionWindow w = new ConnectionWindowImpl();
 		// Centre is 0, negative values should never be allowed
 		try {
 			w.setSeen(-1);
@@ -111,8 +88,7 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 
 	@Test
 	public void testCannotSetSeenTwice() {
-		ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
-				secret);
+		ConnectionWindow w = new ConnectionWindowImpl();
 		w.setSeen(15);
 		try {
 			w.setSeen(15);
@@ -122,13 +98,12 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 
 	@Test
 	public void testGetUnseenConnectionNumbers() {
-		ConnectionWindow w = new ConnectionWindowImpl(crypto, transportIndex,
-				secret);
+		ConnectionWindow w = new ConnectionWindowImpl();
 		// Centre is 0; window should cover 0 to 15, inclusive, with none seen
-		Map<Long, byte[]> unseen = w.getUnseen();
+		Set<Long> unseen = w.getUnseen();
 		assertEquals(16, unseen.size());
 		for(int i = 0; i < 16; i++) {
-			assertTrue(unseen.containsKey(Long.valueOf(i)));
+			assertTrue(unseen.contains(Long.valueOf(i)));
 			assertFalse(w.isSeen(i));
 		}
 		w.setSeen(3);
@@ -138,10 +113,10 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 		assertEquals(19, unseen.size());
 		for(int i = 0; i < 21; i++) {
 			if(i == 3 || i == 4) {
-				assertFalse(unseen.containsKey(Long.valueOf(i)));
+				assertFalse(unseen.contains(Long.valueOf(i)));
 				assertTrue(w.isSeen(i));
 			} else {
-				assertTrue(unseen.containsKey(Long.valueOf(i)));
+				assertTrue(unseen.contains(Long.valueOf(i)));
 				assertFalse(w.isSeen(i));
 			}
 		}
@@ -151,10 +126,10 @@ public class ConnectionWindowImplTest extends BriarTestCase {
 		assertEquals(30, unseen.size());
 		for(int i = 4; i < 36; i++) {
 			if(i == 4 || i == 19) {
-				assertFalse(unseen.containsKey(Long.valueOf(i)));
+				assertFalse(unseen.contains(Long.valueOf(i)));
 				assertTrue(w.isSeen(i));
 			} else {
-				assertTrue(unseen.containsKey(Long.valueOf(i)));
+				assertTrue(unseen.contains(Long.valueOf(i)));
 				assertFalse(w.isSeen(i));
 			}
 		}
diff --git a/test/net/sf/briar/transport/ConnectionWriterTest.java b/test/net/sf/briar/transport/ConnectionWriterTest.java
index 615944366c870e6a8975975bd0a5051e34785eb6..cbfc46b44a2dcc026c2b2b1f8bd7c4e8abd6d875 100644
--- a/test/net/sf/briar/transport/ConnectionWriterTest.java
+++ b/test/net/sf/briar/transport/ConnectionWriterTest.java
@@ -2,12 +2,17 @@ package net.sf.briar.transport;
 
 import static net.sf.briar.api.protocol.ProtocolConstants.MAX_PACKET_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MIN_CONNECTION_LENGTH;
+import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayOutputStream;
 import java.util.Random;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.TestDatabaseModule;
+import net.sf.briar.TestUtils;
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.protocol.TransportId;
+import net.sf.briar.api.transport.ConnectionContext;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.clock.ClockModule;
@@ -27,6 +32,8 @@ import com.google.inject.Injector;
 public class ConnectionWriterTest extends BriarTestCase {
 
 	private final ConnectionWriterFactory connectionWriterFactory;
+	private final ContactId contactId;
+	private final TransportId transportId;
 	private final byte[] secret;
 
 	public ConnectionWriterTest() throws Exception {
@@ -37,6 +44,8 @@ public class ConnectionWriterTest extends BriarTestCase {
 				new TestDatabaseModule(), new SimplexProtocolModule(),
 				new TransportModule(), new DuplexProtocolModule());
 		connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
+		contactId = new ContactId(234);
+		transportId = new TransportId(TestUtils.getRandomId());
 		secret = new byte[32];
 		new Random().nextBytes(secret);
 	}
@@ -44,9 +53,12 @@ public class ConnectionWriterTest extends BriarTestCase {
 	@Test
 	public void testOverheadWithTag() throws Exception {
 		ByteArrayOutputStream out =
-			new ByteArrayOutputStream(MIN_CONNECTION_LENGTH);
+				new ByteArrayOutputStream(MIN_CONNECTION_LENGTH);
+		byte[] tag = new byte[TAG_LENGTH];
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				tag, secret, 0L, true);
 		ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out,
-				MIN_CONNECTION_LENGTH, secret, true);
+				MIN_CONNECTION_LENGTH, ctx, true);
 		// Check that the connection writer thinks there's room for a packet
 		long capacity = w.getRemainingCapacity();
 		assertTrue(capacity > MAX_PACKET_LENGTH);
@@ -63,9 +75,11 @@ public class ConnectionWriterTest extends BriarTestCase {
 	@Test
 	public void testOverheadWithoutTag() throws Exception {
 		ByteArrayOutputStream out =
-			new ByteArrayOutputStream(MIN_CONNECTION_LENGTH);
+				new ByteArrayOutputStream(MIN_CONNECTION_LENGTH);
+		ConnectionContext ctx = new ConnectionContext(contactId, transportId,
+				null, secret, 0L, true);
 		ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out,
-				MIN_CONNECTION_LENGTH, secret, false);
+				MIN_CONNECTION_LENGTH, ctx, false);
 		// Check that the connection writer thinks there's room for a packet
 		long capacity = w.getRemainingCapacity();
 		assertTrue(capacity > MAX_PACKET_LENGTH);
diff --git a/test/net/sf/briar/transport/FrameReadWriteTest.java b/test/net/sf/briar/transport/FrameReadWriteTest.java
index a9b7361e75604551ac9257e2cc65de8c7c94bf26..6443b973f1bbbd8946198bd6a4675062f2983553 100644
--- a/test/net/sf/briar/transport/FrameReadWriteTest.java
+++ b/test/net/sf/briar/transport/FrameReadWriteTest.java
@@ -1,6 +1,5 @@
 package net.sf.briar.transport;
 
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
 
 import java.io.ByteArrayInputStream;
@@ -9,8 +8,6 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Random;
 
-import javax.crypto.Cipher;
-
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.AuthenticatedCipher;
 import net.sf.briar.api.crypto.CryptoComponent;
@@ -29,24 +26,21 @@ public class FrameReadWriteTest extends BriarTestCase {
 	private final int FRAME_LENGTH = 2048;
 
 	private final CryptoComponent crypto;
-	private final Cipher tagCipher;
 	private final AuthenticatedCipher frameCipher;
 	private final Random random;
 	private final byte[] outSecret;
-	private final ErasableKey tagKey, frameKey;
+	private final ErasableKey frameKey;
 
 	public FrameReadWriteTest() {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule());
 		crypto = i.getInstance(CryptoComponent.class);
-		tagCipher = crypto.getTagCipher();
 		frameCipher = crypto.getFrameCipher();
 		random = new Random();
 		// Since we're sending frames to ourselves, we only need outgoing keys
 		outSecret = new byte[32];
 		random.nextBytes(outSecret);
-		tagKey = crypto.deriveTagKey(outSecret, true);
-		frameKey = crypto.deriveFrameKey(outSecret, true);
+		frameKey = crypto.deriveFrameKey(outSecret, 0L, true, true);
 	}
 
 	@Test
@@ -60,22 +54,17 @@ public class FrameReadWriteTest extends BriarTestCase {
 	}
 
 	private void testWriteAndRead(boolean initiator) throws Exception {
-		// Encode the tag
-		byte[] tag = new byte[TAG_LENGTH];
-		TagEncoder.encodeTag(tag, tagCipher, tagKey);
 		// Generate two random frames
 		byte[] frame = new byte[1234];
 		random.nextBytes(frame);
 		byte[] frame1 = new byte[321];
 		random.nextBytes(frame1);
-		// Copy the keys - the copies will be erased
-		ErasableKey tagCopy = tagKey.copy();
+		// Copy the frame key - the copy will be erased
 		ErasableKey frameCopy = frameKey.copy();
 		// Write the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		FrameWriter encryptionOut = new OutgoingEncryptionLayer(out,
-				Long.MAX_VALUE, tagCipher, frameCipher, tagCopy, frameCopy,
-				FRAME_LENGTH);
+				Long.MAX_VALUE, frameCipher, frameCopy, FRAME_LENGTH);
 		ConnectionWriter writer = new ConnectionWriterImpl(encryptionOut,
 				FRAME_LENGTH);
 		OutputStream out1 = writer.getOutputStream();
@@ -84,11 +73,11 @@ public class FrameReadWriteTest extends BriarTestCase {
 		out1.write(frame1);
 		out1.flush();
 		byte[] output = out.toByteArray();
-		assertEquals(TAG_LENGTH + FRAME_LENGTH * 2, output.length);
+		assertEquals(FRAME_LENGTH * 2, output.length);
 		// Read the tag and the frames back
 		ByteArrayInputStream in = new ByteArrayInputStream(output);
-		FrameReader encryptionIn = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		FrameReader encryptionIn = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		ConnectionReader reader = new ConnectionReaderImpl(encryptionIn,
 				FRAME_LENGTH);
 		InputStream in1 = reader.getInputStream();
diff --git a/test/net/sf/briar/transport/IncomingEncryptionLayerTest.java b/test/net/sf/briar/transport/IncomingEncryptionLayerTest.java
index 34f94a3617f03922d17c2edcba070b5338188986..4eb9dc02aeb6c5cc217bf312cd69e5e010ed8337 100644
--- a/test/net/sf/briar/transport/IncomingEncryptionLayerTest.java
+++ b/test/net/sf/briar/transport/IncomingEncryptionLayerTest.java
@@ -5,12 +5,8 @@ import static net.sf.briar.api.transport.TransportConstants.AAD_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.HEADER_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.IV_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayInputStream;
-import java.io.EOFException;
-
-import javax.crypto.Cipher;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.FormatException;
@@ -19,7 +15,6 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
 import net.sf.briar.crypto.CryptoModule;
 
-import org.junit.Before;
 import org.junit.Test;
 
 import com.google.inject.Guice;
@@ -32,99 +27,46 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
 
 	private final CryptoComponent crypto;
-	private final Cipher tagCipher;
 	private final AuthenticatedCipher frameCipher;
-
-	private ErasableKey tagKey = null, frameKey = null;
+	private final ErasableKey frameKey;
 
 	public IncomingEncryptionLayerTest() {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule());
 		crypto = i.getInstance(CryptoComponent.class);
-		tagCipher = crypto.getTagCipher();
 		frameCipher = crypto.getFrameCipher();
-	}
-
-	@Before
-	public void setUp() {
-		tagKey = crypto.generateTestKey();
 		frameKey = crypto.generateTestKey();
 	}
 
 	@Test
-	public void testReadValidTagAndFrames() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
+	public void testReadValidFrames() throws Exception {
 		// Generate two valid frames
 		byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false);
 		byte[] frame1 = generateFrame(1L, FRAME_LENGTH, 123, false, false);
-		// Concatenate the tag and the frames
-		byte[] valid = new byte[TAG_LENGTH + FRAME_LENGTH * 2];
-		System.arraycopy(tag, 0, valid, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, valid, TAG_LENGTH, FRAME_LENGTH);
-		System.arraycopy(frame1, 0, valid, TAG_LENGTH + FRAME_LENGTH,
-				FRAME_LENGTH);
-		// Read the frames, which should first read the tag
+		// Concatenate the frames
+		byte[] valid = new byte[FRAME_LENGTH * 2];
+		System.arraycopy(frame, 0, valid, 0, FRAME_LENGTH);
+		System.arraycopy(frame1, 0, valid, FRAME_LENGTH, FRAME_LENGTH);
+		// Read the frames
 		ByteArrayInputStream in = new ByteArrayInputStream(valid);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH];
 		assertEquals(123, i.readFrame(buf));
 		assertEquals(123, i.readFrame(buf));
 	}
 
-	@Test
-	public void testTruncatedTagThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
-		// Chop off the last byte
-		byte[] truncated = new byte[TAG_LENGTH - 1];
-		System.arraycopy(tag, 0, truncated, 0, TAG_LENGTH - 1);
-		// Try to read the frame, which should first try to read the tag
-		ByteArrayInputStream in = new ByteArrayInputStream(truncated);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, crypto.generateTestKey(), FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(EOFException expected) {}
-	}
-
 	@Test
 	public void testTruncatedFrameThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
 		// Generate a valid frame
 		byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false);
 		// Chop off the last byte
-		byte[] truncated = new byte[TAG_LENGTH + FRAME_LENGTH - 1];
-		System.arraycopy(tag, 0, truncated, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, truncated, TAG_LENGTH, FRAME_LENGTH - 1);
+		byte[] truncated = new byte[FRAME_LENGTH - 1];
+		System.arraycopy(frame, 0, truncated, 0, FRAME_LENGTH - 1);
 		// Try to read the frame, which should fail due to truncation
 		ByteArrayInputStream in = new ByteArrayInputStream(truncated);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testModifiedTagThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
-		// Generate a valid frame
-		byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false);
-		// Modify a randomly chosen byte of the tag
-		byte[] modified = new byte[TAG_LENGTH + FRAME_LENGTH];
-		System.arraycopy(tag, 0, modified, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, modified, TAG_LENGTH, FRAME_LENGTH);
-		modified[(int) (Math.random() * TAG_LENGTH)] ^= 1;
-		// Try to read the frame, which should fail due to modification
-		ByteArrayInputStream in = new ByteArrayInputStream(modified);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		try {
 			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
 			fail();
@@ -133,19 +75,14 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 
 	@Test
 	public void testModifiedFrameThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
 		// Generate a valid frame
 		byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, false);
 		// Modify a randomly chosen byte of the frame
-		byte[] modified = new byte[TAG_LENGTH + FRAME_LENGTH];
-		System.arraycopy(tag, 0, modified, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, modified, TAG_LENGTH, FRAME_LENGTH);
-		modified[TAG_LENGTH + (int) (Math.random() * FRAME_LENGTH)] ^= 1;
+		frame[(int) (Math.random() * FRAME_LENGTH)] ^= 1;
 		// Try to read the frame, which should fail due to modification
-		ByteArrayInputStream in = new ByteArrayInputStream(modified);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		ByteArrayInputStream in = new ByteArrayInputStream(frame);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		try {
 			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
 			fail();
@@ -154,18 +91,12 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 
 	@Test
 	public void testShortNonFinalFrameThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
 		// Generate a short non-final frame
 		byte[] frame = generateFrame(0L, FRAME_LENGTH - 1, 123, false, false);
-		// Concatenate the tag and the frame
-		byte[] tooShort = new byte[TAG_LENGTH + FRAME_LENGTH - 1];
-		System.arraycopy(tag, 0, tooShort, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, tooShort, TAG_LENGTH, FRAME_LENGTH - 1);
 		// Try to read the frame, which should fail due to invalid length
-		ByteArrayInputStream in = new ByteArrayInputStream(tooShort);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		ByteArrayInputStream in = new ByteArrayInputStream(frame);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		try {
 			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
 			fail();
@@ -174,37 +105,25 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 
 	@Test
 	public void testShortFinalFrameDoesNotThrowException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
 		// Generate a short final frame
 		byte[] frame = generateFrame(0L, FRAME_LENGTH - 1, 123, true, false);
-		// Concatenate the tag and the frame
-		byte[] valid = new byte[TAG_LENGTH + FRAME_LENGTH - 1];
-		System.arraycopy(tag, 0, valid, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, valid, TAG_LENGTH, FRAME_LENGTH - 1);
-		// Read the frame, which should first read the tag
-		ByteArrayInputStream in = new ByteArrayInputStream(valid);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		// Read the frame
+		ByteArrayInputStream in = new ByteArrayInputStream(frame);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		int length = i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
 		assertEquals(123, length);
 	}
 
 	@Test
 	public void testInvalidPayloadLengthThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
 		// Generate a frame with an invalid payload length
 		byte[] frame = generateFrame(0L, FRAME_LENGTH, MAX_PAYLOAD_LENGTH + 1,
 				false, false);
-		// Concatenate the tag and the frame
-		byte[] tooLong = new byte[TAG_LENGTH + FRAME_LENGTH];
-		System.arraycopy(tag, 0, tooLong, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, tooLong, TAG_LENGTH, FRAME_LENGTH);
 		// Try to read the frame, which should fail due to invalid length
-		ByteArrayInputStream in = new ByteArrayInputStream(tooLong);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		ByteArrayInputStream in = new ByteArrayInputStream(frame);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		try {
 			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
 			fail();
@@ -213,18 +132,12 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 
 	@Test
 	public void testNonZeroPaddingThrowsException() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
-		// Generate a frame with pad padding
+		// Generate a frame with bad padding
 		byte[] frame = generateFrame(0L, FRAME_LENGTH, 123, false, true);
-		// Concatenate the tag and the frame
-		byte[] badPadding = new byte[TAG_LENGTH + FRAME_LENGTH];
-		System.arraycopy(tag, 0, badPadding, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, badPadding, TAG_LENGTH, FRAME_LENGTH);
 		// Try to read the frame, which should fail due to bad padding
-		ByteArrayInputStream in = new ByteArrayInputStream(badPadding);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		ByteArrayInputStream in = new ByteArrayInputStream(frame);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		try {
 			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
 			fail();
@@ -233,34 +146,24 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 
 	@Test
 	public void testCannotReadBeyondFinalFrame() throws Exception {
-		// Generate a valid tag
-		byte[] tag = generateTag(tagKey);
 		// Generate a valid final frame and another valid final frame after it
 		byte[] frame = generateFrame(0L, FRAME_LENGTH, MAX_PAYLOAD_LENGTH, true,
 				false);
 		byte[] frame1 = generateFrame(1L, FRAME_LENGTH, 123, true, false);
-		// Concatenate the tag and the frames
-		byte[] extraFrame = new byte[TAG_LENGTH + FRAME_LENGTH * 2];
-		System.arraycopy(tag, 0, extraFrame, 0, TAG_LENGTH);
-		System.arraycopy(frame, 0, extraFrame, TAG_LENGTH, FRAME_LENGTH);
-		System.arraycopy(frame1, 0, extraFrame, TAG_LENGTH + FRAME_LENGTH,
-				FRAME_LENGTH);
+		// Concatenate the frames
+		byte[] extraFrame = new byte[FRAME_LENGTH * 2];
+		System.arraycopy(frame, 0, extraFrame, 0, FRAME_LENGTH);
+		System.arraycopy(frame1, 0, extraFrame, FRAME_LENGTH, FRAME_LENGTH);
 		// Read the final frame, which should first read the tag
 		ByteArrayInputStream in = new ByteArrayInputStream(extraFrame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, tagCipher,
-				frameCipher, tagKey, frameKey, FRAME_LENGTH);
+		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+				frameKey, FRAME_LENGTH);
 		byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH];
 		assertEquals(MAX_PAYLOAD_LENGTH, i.readFrame(buf));
 		// The frame after the final frame should not be read
 		assertEquals(-1, i.readFrame(buf));
 	}
 
-	private byte[] generateTag(ErasableKey tagKey) {
-		byte[] tag = new byte[TAG_LENGTH];
-		TagEncoder.encodeTag(tag, tagCipher, tagKey);
-		return tag;
-	}
-
 	private byte[] generateFrame(long frameNumber, int frameLength,
 			int payloadLength, boolean finalFrame, boolean badPadding)
 					throws Exception {
diff --git a/test/net/sf/briar/transport/OutgoingEncryptionLayerTest.java b/test/net/sf/briar/transport/OutgoingEncryptionLayerTest.java
index 68c3f1f9343acceca7911e3b094a828d6db3c71e..f4eba234864abc61d0e2af08f645e76b8345204c 100644
--- a/test/net/sf/briar/transport/OutgoingEncryptionLayerTest.java
+++ b/test/net/sf/briar/transport/OutgoingEncryptionLayerTest.java
@@ -9,8 +9,6 @@ import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayOutputStream;
 
-import javax.crypto.Cipher;
-
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.AuthenticatedCipher;
 import net.sf.briar.api.crypto.CryptoComponent;
@@ -29,28 +27,24 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
 
 	private final CryptoComponent crypto;
-	private final Cipher tagCipher;
 	private final AuthenticatedCipher frameCipher;
+	private final byte[] tag;
 
 	public OutgoingEncryptionLayerTest() {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule());
 		crypto = i.getInstance(CryptoComponent.class);
-		tagCipher = crypto.getTagCipher();
 		frameCipher = crypto.getFrameCipher();
+		tag = new byte[TAG_LENGTH];
 	}
 
 	@Test
 	public void testEncryption() throws Exception {
 		int payloadLength = 123;
-		byte[] tag = new byte[TAG_LENGTH];
 		byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH];
 		byte[] plaintext = new byte[FRAME_LENGTH - MAC_LENGTH];
 		byte[] ciphertext = new byte[FRAME_LENGTH];
-		ErasableKey tagKey = crypto.generateTestKey();
 		ErasableKey frameKey = crypto.generateTestKey();
-		// Calculate the expected tag
-		TagEncoder.encodeTag(tag, tagCipher, tagKey);
 		// Calculate the expected ciphertext
 		FrameEncoder.encodeIv(iv, 0);
 		FrameEncoder.encodeAad(aad, 0, plaintext.length);
@@ -60,14 +54,11 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		// Check that the actual tag and ciphertext match what's expected
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, tagCipher, frameCipher, tagKey, frameKey,
-				FRAME_LENGTH);
+				10 * FRAME_LENGTH, frameCipher, frameKey, FRAME_LENGTH, tag);
 		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false);
 		byte[] actual = out.toByteArray();
 		assertEquals(TAG_LENGTH + FRAME_LENGTH, actual.length);
-		for(int i = 0; i < TAG_LENGTH; i++) {
-			assertEquals(tag[i], actual[i]);
-		}
+		for(int i = 0; i < TAG_LENGTH; i++) assertEquals(tag[i], actual[i]);
 		for(int i = 0; i < FRAME_LENGTH; i++) {
 			assertEquals("" + i, ciphertext[i], actual[TAG_LENGTH + i]);
 		}
@@ -78,9 +69,8 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Initiator's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, tagCipher, frameCipher,
-				crypto.generateTestKey(), crypto.generateTestKey(),
-				FRAME_LENGTH);
+				10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(),
+				FRAME_LENGTH, tag);
 		// Write an empty final frame without having written any other frames
 		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true);
 		// Nothing should be written to the output stream
@@ -106,9 +96,8 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Initiator's constructor
 		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				10 * FRAME_LENGTH, tagCipher, frameCipher,
-				crypto.generateTestKey(), crypto.generateTestKey(),
-				FRAME_LENGTH);
+				10 * FRAME_LENGTH, frameCipher, crypto.generateTestKey(),
+				FRAME_LENGTH, tag);
 		// There should be space for nine full frames and one partial frame
 		byte[] frame = new byte[FRAME_LENGTH - MAC_LENGTH];
 		assertEquals(10 * MAX_PAYLOAD_LENGTH - TAG_LENGTH,