diff --git a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
index cff01cd66abf43ca2780c7c084d23f4cc62b52dd..97b4dcc81396e45326a29cb9f1b5c67dab07ada0 100644
--- a/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
+++ b/briar-android/src/org/briarproject/android/AndroidNotificationManagerImpl.java
@@ -32,7 +32,6 @@ import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.SettingsUpdatedEvent;
-import org.briarproject.api.lifecycle.Service;
 import org.briarproject.api.messaging.GroupId;
 import org.briarproject.util.StringUtils;
 
@@ -45,7 +44,7 @@ import android.support.v4.app.NotificationCompat;
 import android.support.v4.app.TaskStackBuilder;
 
 class AndroidNotificationManagerImpl implements AndroidNotificationManager,
-Service, EventListener {
+EventListener {
 
 	private static final int PRIVATE_MESSAGE_NOTIFICATION_ID = 3;
 	private static final int GROUP_POST_NOTIFICATION_ID = 4;
diff --git a/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java b/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java
index c0be8a754f920352d32d50d7184002a2a9f7b6f1..992116647047df605723da8471fb78e6ea7258f8 100644
--- a/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java
+++ b/briar-api/src/org/briarproject/api/android/AndroidNotificationManager.java
@@ -1,13 +1,14 @@
 package org.briarproject.api.android;
 
 import org.briarproject.api.ContactId;
+import org.briarproject.api.lifecycle.Service;
 import org.briarproject.api.messaging.GroupId;
 
 /**
  * Manages notifications for private messages and group posts. All methods must
  * be called from the Android UI thread.
  */
-public interface AndroidNotificationManager {
+public interface AndroidNotificationManager extends Service {
 
 	public void showPrivateMessageNotification(ContactId c);
 
diff --git a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
index 6add0417b71c42bedff400b6c9cf4a84a30f1990..3a69a4e79144cddbe9158b67bc8ded4b2e866cc6 100644
--- a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
+++ b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
@@ -1,5 +1,8 @@
 package org.briarproject.api.crypto;
 
+import org.briarproject.api.TransportId;
+import org.briarproject.api.transport.TransportKeys;
+
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 
@@ -26,56 +29,50 @@ public interface CryptoComponent {
 	/** Generates a random invitation code. */
 	int generateInvitationCode();
 
-	/**
-	 * Derives two confirmation codes from the given master 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 two nonces from the given master secret. The first nonce is for
-	 * Alice to sign; the second is for Bob to sign.
-	 */
-	byte[][] deriveInvitationNonces(byte[] secret);
-
 	/**
 	 * Derives a shared master secret from two public keys and one of the
 	 * corresponding private keys.
-	 * @param alice indicates whether the private key belongs to Alice or Bob.
+	 * @param alice whether the private key belongs to Alice or Bob.
 	 */
-	byte[] deriveMasterSecret(byte[] theirPublicKey, KeyPair ourKeyPair,
+	SecretKey deriveMasterSecret(byte[] theirPublicKey, KeyPair ourKeyPair,
 			boolean alice) throws GeneralSecurityException;
 
-	/** Derives a group salt from the given master secret. */
-	byte[] deriveGroupSalt(byte[] secret);
+	/**
+	 * Derives a confirmation code from the given master secret.
+	 * @param alice whether the code is for use by Alice or Bob.
+	 */
+	int deriveConfirmationCode(SecretKey master, boolean alice);
 
 	/**
-	 * Derives an initial secret for the given transport from the given master
+	 * Derives a header key for an invitation stream from the given master
 	 * secret.
+	 * @param alice whether the key is for use by Alice or Bob.
 	 */
-	byte[] deriveInitialSecret(byte[] secret, int transportIndex);
+	SecretKey deriveInvitationKey(SecretKey master, boolean alice);
 
 	/**
-	 * Derives a temporary secret for the given period from the given secret,
-	 * which is either the initial shared secret or the previous period's
-	 * temporary secret.
+	 * Derives a nonce from the given master secret for one of the parties to
+	 * sign.
+	 * @param alice whether the nonce is for use by Alice or Bob.
 	 */
-	byte[] deriveNextSecret(byte[] secret, long period);
+	byte[] deriveSignatureNonce(SecretKey master, boolean alice);
+
+	/** Derives a group salt from the given master secret. */
+	byte[] deriveGroupSalt(SecretKey master);
 
 	/**
-	 * Derives a tag key from the given temporary secret.
-	 * @param alice indicates whether the key is for streams initiated by
-	 * Alice or Bob.
+	 * Derives initial transport keys for the given transport in the given
+	 * rotation period from the given master secret.
+	 * @param alice whether the keys are for use by Alice or Bob.
 	 */
-	SecretKey deriveTagKey(byte[] secret, boolean alice);
+	TransportKeys deriveTransportKeys(TransportId t, SecretKey master,
+			long rotationPeriod, boolean alice);
 
 	/**
-	 * Derives a frame key from the given temporary secret and stream number.
-	 * @param alice indicates whether the key is for a stream initiated by
-	 * Alice or Bob.
+	 * Rotates the given transport keys to the given rotation period. If the
+	 * keys are for a future rotation period they are not rotated.
 	 */
-	SecretKey deriveFrameKey(byte[] secret, long streamNumber, boolean alice);
+	TransportKeys rotateTransportKeys(TransportKeys k, long rotationPeriod);
 
 	/** Encodes the pseudo-random tag that is used to recognise a stream. */
 	void encodeTag(byte[] tag, SecretKey tagKey, long streamNumber);
diff --git a/briar-api/src/org/briarproject/api/crypto/KeyManager.java b/briar-api/src/org/briarproject/api/crypto/KeyManager.java
deleted file mode 100644
index 02a43e9706656cd06dace79eb64730f1e2561ed6..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/crypto/KeyManager.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.briarproject.api.crypto;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.lifecycle.Service;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.StreamContext;
-
-public interface KeyManager extends Service {
-
-	/**
-	 * Returns a {@link org.briarproject.api.transport.StreamContext
-	 * StreamContext} for sending data to the given contact over the given
-	 * transport, or null if an error occurs or the contact does not support
-	 * the transport.
-	 */
-	StreamContext getStreamContext(ContactId c, TransportId t);
-
-	/** Called whenever an endpoint has been added. */
-	void endpointAdded(Endpoint ep, int maxLatency, byte[] initialSecret);
-}
diff --git a/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java b/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java
index 8b758ec11feef6c53fe7ef5cbb8ad65ca4a6f6aa..0d75a94287cbd13ed3dbab8b395c9f2207b74bfd 100644
--- a/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java
+++ b/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java
@@ -13,5 +13,5 @@ public interface StreamDecrypterFactory {
 	 * Creates a {@link StreamDecrypter} for decrypting an invitation stream.
 	 */
 	StreamDecrypter createInvitationStreamDecrypter(InputStream in,
-			byte[] secret, boolean alice);
+			SecretKey headerKey);
 }
diff --git a/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java b/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java
index 86df7bbe2439f1f0ea9c6417cd60c8b1911614f3..6c94ee7f0c2ef2161bb3a94edaca252353b7b1e8 100644
--- a/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java
+++ b/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java
@@ -13,5 +13,5 @@ public interface StreamEncrypterFactory {
 	 * Creates a {@link StreamEncrypter} for encrypting an invitation stream.
 	 */
 	StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
-			byte[] secret, boolean alice);
+			SecretKey headerKey);
 }
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index 852b84a9fc20113bded8bf149219e1411ea54c9d..530cffbb20eab558bda1112c4ae587d6507383f3 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -1,9 +1,5 @@
 package org.briarproject.api.db;
 
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Map;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorId;
 import org.briarproject.api.Contact;
@@ -26,8 +22,11 @@ import org.briarproject.api.messaging.SubscriptionAck;
 import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportAck;
 import org.briarproject.api.messaging.TransportUpdate;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.TransportKeys;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
 
 /**
  * Encapsulates the database implementation and exposes high-level operations
@@ -47,9 +46,6 @@ public interface DatabaseComponent {
 	 */
 	ContactId addContact(Author remote, AuthorId local) throws DbException;
 
-	/** Stores an endpoint. */
-	void addEndpoint(Endpoint ep) throws DbException;
-
 	/**
 	 * Subscribes to a group, or returns false if the user already has the
 	 * maximum number of public subscriptions.
@@ -62,18 +58,17 @@ public interface DatabaseComponent {
 	/** Stores a local message. */
 	void addLocalMessage(Message m) throws DbException;
 
-	/**
-	 * Stores the given temporary secrets and deletes any secrets that have
-	 * been made obsolete.
-	 */
-	void addSecrets(Collection<TemporarySecret> secrets) throws DbException;
-
 	/**
 	 * Stores a transport and returns true if the transport was not previously
 	 * in the database.
 	 */
 	boolean addTransport(TransportId t, int maxLatency) throws DbException;
 
+	/**
+	 * Stores the given transport keys for a newly added contact.
+	 */
+	void addTransportKeys(ContactId c, TransportKeys k) throws DbException;
+
 	/**
 	 * Returns an acknowledgement for the given contact, or null if there are
 	 * no messages to acknowledge.
@@ -214,16 +209,17 @@ public interface DatabaseComponent {
 	Map<ContactId, TransportProperties> getRemoteProperties(TransportId t)
 			throws DbException;
 
-	/** Returns all temporary secrets. */
-	Collection<TemporarySecret> getSecrets() throws DbException;
-
 	/** Returns all settings. */
 	Settings getSettings() throws DbException;
 
 	/** Returns all contacts who subscribe to the given group. */
 	Collection<Contact> getSubscribers(GroupId g) throws DbException;
 
-	/** Returns the maximum latencies of all supported transports. */
+	/** Returns all transport keys for the given transport. */
+	Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
+			throws DbException;
+
+	/** Returns the maximum latencies in milliseconds of all transports. */
 	Map<TransportId, Integer> getTransportLatencies() throws DbException;
 
 	/** Returns the number of unread messages in each subscribed group. */
@@ -233,11 +229,10 @@ public interface DatabaseComponent {
 	Collection<ContactId> getVisibility(GroupId g) throws DbException;
 
 	/**
-	 * Increments the outgoing stream counter for the given endpoint in the
-	 * given rotation period and returns the old value, or -1 if the counter
-	 * does not exist.
+	 * Increments the outgoing stream counter for the given contact and
+	 * transport in the given rotation period .
 	 */
-	long incrementStreamCounter(ContactId c, TransportId t, long period)
+	void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod)
 			throws DbException;
 
 	/**
@@ -310,18 +305,11 @@ public interface DatabaseComponent {
 	 */
 	void removeTransport(TransportId t) throws DbException;
 
-	/**
-	 * Sets the reordering window for the given endpoint in the given rotation
-	 * period.
-	 */
-	void setReorderingWindow(ContactId c, TransportId t, long period,
-			long centre, byte[] bitmap) throws DbException;
-
 	/**
 	 * Makes a group visible to the given contact, adds it to the contact's
 	 * subscriptions, and sets it as the inbox group for the contact.
 	 */
-	public void setInboxGroup(ContactId c, Group g) throws DbException;
+	void setInboxGroup(ContactId c, Group g) throws DbException;
 
 	/**
 	 * Marks a message as read or unread.
@@ -335,6 +323,13 @@ public interface DatabaseComponent {
 	void setRemoteProperties(ContactId c,
 			Map<TransportId, TransportProperties> p) throws DbException;
 
+	/**
+	 * Sets the reordering window for the given contact and transport in the
+	 * given rotation period.
+	 */
+	void setReorderingWindow(ContactId c, TransportId t, long rotationPeriod,
+			long base, byte[] bitmap) throws DbException;
+
 	/**
 	 * Makes a group visible to the given set of contacts and invisible to any
 	 * other current or future contacts.
@@ -347,4 +342,10 @@ public interface DatabaseComponent {
 	 * to future contacts.
 	 */
 	void setVisibleToAll(GroupId g, boolean all) throws DbException;
+
+	/**
+	 * Stores the given transport keys, deleting any keys they have replaced.
+	 */
+	void updateTransportKeys(Map<ContactId, TransportKeys> keys)
+			throws DbException;
 }
diff --git a/briar-api/src/org/briarproject/api/messaging/Message.java b/briar-api/src/org/briarproject/api/messaging/Message.java
index 608be41239ac492f4b8d2bbacbcfc04621e90389..dd6291e55e82565e739422d15ec8dc1bae44cd87 100644
--- a/briar-api/src/org/briarproject/api/messaging/Message.java
+++ b/briar-api/src/org/briarproject/api/messaging/Message.java
@@ -28,7 +28,7 @@ public interface Message {
 	/** Returns the message's content type. */
 	String getContentType();
 
-	/** Returns the message's timestamp. */
+	/** Returns the message's timestamp in milliseconds since the Unix epoch. */
 	long getTimestamp();
 
 	/** Returns the serialised message. */
diff --git a/briar-api/src/org/briarproject/api/transport/Endpoint.java b/briar-api/src/org/briarproject/api/transport/Endpoint.java
deleted file mode 100644
index 6ec3d2d6d2d7afe169700973d1071d993dfd1964..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/transport/Endpoint.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package org.briarproject.api.transport;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-
-public class Endpoint {
-
-	protected final ContactId contactId;
-	protected final TransportId transportId;
-	private final long epoch;
-	private final boolean alice;
-
-	public Endpoint(ContactId contactId, TransportId transportId, long epoch,
-			boolean alice) {
-		this.contactId = contactId;
-		this.transportId = transportId;
-		this.epoch = epoch;
-		this.alice = alice;
-	}
-
-	public ContactId getContactId() {
-		return contactId;
-	}
-
-	public TransportId getTransportId() {
-		return transportId;
-	}
-
-	public long getEpoch() {
-		return epoch;
-	}
-
-	public boolean getAlice() {
-		return alice;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/transport/IncomingKeys.java b/briar-api/src/org/briarproject/api/transport/IncomingKeys.java
new file mode 100644
index 0000000000000000000000000000000000000000..6ee30759e55227abdc53e17850d2412b0d0d6103
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/transport/IncomingKeys.java
@@ -0,0 +1,51 @@
+package org.briarproject.api.transport;
+
+import org.briarproject.api.crypto.SecretKey;
+
+import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
+
+/**
+ * Contains transport keys for receiving streams from a given contact over a
+ * given transport in a given rotation period.
+ */
+public class IncomingKeys {
+
+	private final SecretKey tagKey, headerKey;
+	private final long rotationPeriod, windowBase;
+	private final byte[] windowBitmap;
+
+	public IncomingKeys(SecretKey tagKey, SecretKey headerKey,
+			long rotationPeriod) {
+		this(tagKey, headerKey, rotationPeriod, 0,
+				new byte[REORDERING_WINDOW_SIZE / 8]);
+	}
+
+	public IncomingKeys(SecretKey tagKey, SecretKey headerKey,
+			long rotationPeriod, long windowBase, byte[] windowBitmap) {
+		this.tagKey = tagKey;
+		this.headerKey = headerKey;
+		this.rotationPeriod = rotationPeriod;
+		this.windowBase = windowBase;
+		this.windowBitmap = windowBitmap;
+	}
+
+	public SecretKey getTagKey() {
+		return tagKey;
+	}
+
+	public SecretKey getHeaderKey() {
+		return headerKey;
+	}
+
+	public long getRotationPeriod() {
+		return rotationPeriod;
+	}
+
+	public long getWindowBase() {
+		return windowBase;
+	}
+
+	public byte[] getWindowBitmap() {
+		return windowBitmap;
+	}
+}
\ No newline at end of file
diff --git a/briar-api/src/org/briarproject/api/transport/KeyManager.java b/briar-api/src/org/briarproject/api/transport/KeyManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..7b08b1c0e21325f83577b24b489967f5f4903efe
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/transport/KeyManager.java
@@ -0,0 +1,36 @@
+package org.briarproject.api.transport;
+
+import org.briarproject.api.ContactId;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.lifecycle.Service;
+
+import java.util.Collection;
+
+/**
+ * Responsible for managing transport keys and recognising the pseudo-random
+ * tags of incoming streams.
+ */
+public interface KeyManager extends Service {
+
+	/**
+	 * Informs the key manager that a new contact has been added.
+	 * {@link StreamContext StreamContexts} for the contact can be created
+	 * after this method has returned.
+	 */
+	void contactAdded(ContactId c, Collection<TransportKeys> keys);
+
+	/**
+	 * Returns a {@link StreamContext} for sending a stream to the given
+	 * contact over the given transport, or null if an error occurs or the
+	 * contact does not support the transport.
+	 */
+	StreamContext getStreamContext(ContactId c, TransportId t);
+
+	/**
+	 * Looks up the given tag and returns a {@link StreamContext} for reading
+	 * from the corresponding stream if the tag was expected, or null if the
+	 * tag was unexpected.
+	 */
+	StreamContext recogniseTag(TransportId t, byte[] tag) throws DbException;
+}
diff --git a/briar-api/src/org/briarproject/api/transport/OutgoingKeys.java b/briar-api/src/org/briarproject/api/transport/OutgoingKeys.java
new file mode 100644
index 0000000000000000000000000000000000000000..08a06d2d9c513bce262001ea8ba99b573fa76755
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/transport/OutgoingKeys.java
@@ -0,0 +1,42 @@
+package org.briarproject.api.transport;
+
+import org.briarproject.api.crypto.SecretKey;
+
+/**
+ * Contains transport keys for sending streams to a given contact over a given
+ * transport in a given rotation period.
+ */
+public class OutgoingKeys {
+
+	private final SecretKey tagKey, headerKey;
+	private final long rotationPeriod, streamCounter;
+
+	public OutgoingKeys(SecretKey tagKey, SecretKey headerKey,
+			long rotationPeriod) {
+		this(tagKey, headerKey, rotationPeriod, 0);
+	}
+
+	public OutgoingKeys(SecretKey tagKey, SecretKey headerKey,
+			long rotationPeriod, long streamCounter) {
+		this.tagKey = tagKey;
+		this.headerKey = headerKey;
+		this.rotationPeriod = rotationPeriod;
+		this.streamCounter = streamCounter;
+	}
+
+	public SecretKey getTagKey() {
+		return tagKey;
+	}
+
+	public SecretKey getHeaderKey() {
+		return headerKey;
+	}
+
+	public long getRotationPeriod() {
+		return rotationPeriod;
+	}
+
+	public long getStreamCounter() {
+		return streamCounter;
+	}
+}
\ No newline at end of file
diff --git a/briar-api/src/org/briarproject/api/transport/StreamContext.java b/briar-api/src/org/briarproject/api/transport/StreamContext.java
index 61554c0aceb5043482ef499b653340ea5d5b00d6..5c46b9f8c1664f4e9196429246df78c4af65c4ff 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamContext.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamContext.java
@@ -2,22 +2,22 @@ package org.briarproject.api.transport;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
+import org.briarproject.api.crypto.SecretKey;
 
 public class StreamContext {
 
 	private final ContactId contactId;
 	private final TransportId transportId;
-	private final byte[] secret;
+	private final SecretKey tagKey, headerKey;
 	private final long streamNumber;
-	private final boolean alice;
 
 	public StreamContext(ContactId contactId, TransportId transportId,
-			byte[] secret, long streamNumber, boolean alice) {
+			SecretKey tagKey, SecretKey headerKey, long streamNumber) {
 		this.contactId = contactId;
 		this.transportId = transportId;
-		this.secret = secret;
+		this.tagKey = tagKey;
+		this.headerKey = headerKey;
 		this.streamNumber = streamNumber;
-		this.alice = alice;
 	}
 
 	public ContactId getContactId() {
@@ -28,15 +28,15 @@ public class StreamContext {
 		return transportId;
 	}
 
-	public byte[] getSecret() {
-		return secret;
+	public SecretKey getTagKey() {
+		return tagKey;
 	}
 
-	public long getStreamNumber() {
-		return streamNumber;
+	public SecretKey getHeaderKey() {
+		return headerKey;
 	}
 
-	public boolean getAlice() {
-		return alice;
+	public long getStreamNumber() {
+		return streamNumber;
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
index 5cf4fd6394ac7f9004d5ad1d48f5d1cca25f7fe2..8e8544b525dac332e0a9ec7b69c30fad7830ad99 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
@@ -2,6 +2,8 @@ package org.briarproject.api.transport;
 
 import java.io.InputStream;
 
+import org.briarproject.api.crypto.SecretKey;
+
 public interface StreamReaderFactory {
 
 	/**
@@ -15,5 +17,5 @@ public interface StreamReaderFactory {
 	 * invitation stream.
 	 */
 	InputStream createInvitationStreamReader(InputStream in,
-			byte[] secret, boolean alice);
+			SecretKey headerKey);
 }
diff --git a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
index 38e373ccc244a1439d332176ee5380a89343aa9f..7a23fa6e9a9809744ea3f9c5f4b5f061509d6c0f 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
@@ -2,6 +2,8 @@ package org.briarproject.api.transport;
 
 import java.io.OutputStream;
 
+import org.briarproject.api.crypto.SecretKey;
+
 public interface StreamWriterFactory {
 
 	/**
@@ -15,5 +17,5 @@ public interface StreamWriterFactory {
 	 * invitation stream.
 	 */
 	OutputStream createInvitationStreamWriter(OutputStream out,
-			byte[] secret, boolean alice);
+			SecretKey headerKey);
 }
diff --git a/briar-api/src/org/briarproject/api/transport/TagRecogniser.java b/briar-api/src/org/briarproject/api/transport/TagRecogniser.java
deleted file mode 100644
index b73d096979d6f5a153e66c6a205c0355cdfec81f..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/transport/TagRecogniser.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.briarproject.api.transport;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.db.DbException;
-
-/** Keeps track of expected tags and uses them to recognise incoming streams. */
-public interface TagRecogniser {
-
-	/**
-	 * Looks up the given tag and returns a {@link StreamContext} for reading
-	 * from the stream if the tag was expected, or null if the tag was
-	 * unexpected.
-	 */
-	StreamContext recogniseTag(TransportId t, byte[] tag) throws DbException;
-
-	void addSecret(TemporarySecret s);
-
-	void removeSecret(ContactId c, TransportId t, long period);
-
-	void removeSecrets(ContactId c);
-
-	void removeSecrets(TransportId t);
-
-	void removeSecrets();
-}
diff --git a/briar-api/src/org/briarproject/api/transport/TemporarySecret.java b/briar-api/src/org/briarproject/api/transport/TemporarySecret.java
deleted file mode 100644
index c2a1108208a07776a68725c7b3e10b958d48de0d..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/transport/TemporarySecret.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package org.briarproject.api.transport;
-
-import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-
-public class TemporarySecret extends Endpoint {
-
-	private final long period, outgoing, centre;
-	private final byte[] secret, bitmap;
-
-	/** Creates a temporary secret with the given reordering window. */
-	public TemporarySecret(ContactId contactId, TransportId transportId,
-			long epoch, boolean alice, long period, byte[] secret,
-			long outgoing, long centre, byte[] bitmap) {
-		super(contactId, transportId, epoch, alice);
-		this.period = period;
-		this.secret = secret;
-		this.outgoing = outgoing;
-		this.centre = centre;
-		this.bitmap = bitmap;
-	}
-
-	/** Creates a temporary secret with a new reordering window. */
-	public TemporarySecret(ContactId contactId, TransportId transportId,
-			long epoch, boolean alice, long period, byte[] secret) {
-		this(contactId, transportId, epoch, alice, period, secret, 0, 0,
-				new byte[REORDERING_WINDOW_SIZE / 8]);
-	}
-
-	/** Creates a temporary secret derived from the given endpoint. */
-	public TemporarySecret(Endpoint ep, long period, byte[] secret) {
-		this(ep.getContactId(), ep.getTransportId(), ep.getEpoch(),
-				ep.getAlice(), period, secret);
-	}
-
-	public long getPeriod() {
-		return period;
-	}
-
-	public byte[] getSecret() {
-		return secret;
-	}
-
-	public long getOutgoingStreamCounter() {
-		return outgoing;
-	}
-
-	public long getWindowCentre() {
-		return centre;
-	}
-
-	public byte[] getWindowBitmap() {
-		return bitmap;
-	}
-
-	@Override
-	public int hashCode() {
-		int periodHashCode = (int) (period ^ (period >>> 32));
-		return contactId.hashCode() ^ transportId.hashCode() ^ periodHashCode;
-	}
-
-	@Override
-	public boolean equals(Object o) {
-		if (o instanceof TemporarySecret) {
-			TemporarySecret s = (TemporarySecret) o;
-			return contactId.equals(s.contactId) &&
-					transportId.equals(s.transportId) && period == s.period;
-		}
-		return false;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/transport/TransportConstants.java b/briar-api/src/org/briarproject/api/transport/TransportConstants.java
index 51444c4c791ec34cb2f7b50131264a0063889ea9..797a47667a5e61aea45817186a343d924139c86c 100644
--- a/briar-api/src/org/briarproject/api/transport/TransportConstants.java
+++ b/briar-api/src/org/briarproject/api/transport/TransportConstants.java
@@ -28,8 +28,8 @@ public interface TransportConstants {
 	 */
 	int MIN_STREAM_LENGTH = 64 * 1024; // 64 KiB
 
-	/** The maximum difference between two communicating devices' clocks. */
-	int MAX_CLOCK_DIFFERENCE = 60 * 60 * 1000; // 1 hour
+	/** The maximum difference in milliseconds between two peers' clocks. */
+	int MAX_CLOCK_DIFFERENCE = 24 * 60 * 60 * 1000; // 24 hours
 
 	/** The size of the reordering window. */
 	int REORDERING_WINDOW_SIZE = 32;
diff --git a/briar-api/src/org/briarproject/api/transport/TransportKeys.java b/briar-api/src/org/briarproject/api/transport/TransportKeys.java
new file mode 100644
index 0000000000000000000000000000000000000000..72b7e62c459b70cfc889fcdd69111c8eda9c7880
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/transport/TransportKeys.java
@@ -0,0 +1,44 @@
+package org.briarproject.api.transport;
+
+import org.briarproject.api.TransportId;
+
+/** Keys for communicating with a given contact over a given transport. */
+public class TransportKeys {
+
+	private final TransportId transportId;
+	private final IncomingKeys inPrev, inCurr, inNext;
+	private final OutgoingKeys outCurr;
+
+	public TransportKeys(TransportId transportId, IncomingKeys inPrev,
+			IncomingKeys inCurr, IncomingKeys inNext, OutgoingKeys outCurr) {
+		this.transportId = transportId;
+		this.inPrev = inPrev;
+		this.inCurr = inCurr;
+		this.inNext = inNext;
+		this.outCurr = outCurr;
+	}
+
+	public TransportId getTransportId() {
+		return transportId;
+	}
+
+	public IncomingKeys getPreviousIncomingKeys() {
+		return inPrev;
+	}
+
+	public IncomingKeys getCurrentIncomingKeys() {
+		return inCurr;
+	}
+
+	public IncomingKeys getNextIncomingKeys() {
+		return inNext;
+	}
+
+	public OutgoingKeys getCurrentOutgoingKeys() {
+		return outCurr;
+	}
+
+	public long getRotationPeriod() {
+		return outCurr.getRotationPeriod();
+	}
+}
diff --git a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
index 7fbcc7c2cc0bd9c47f0d95de51f1b9244363c9f3..d86f02a94d8f63f98110ca6a6e30c11daca409c5 100644
--- a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
+++ b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
@@ -1,20 +1,6 @@
 package org.briarproject.crypto;
 
-import static java.util.logging.Level.INFO;
-import static org.briarproject.api.invitation.InvitationConstants.CODE_BITS;
-import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
-import static org.briarproject.crypto.EllipticCurveConstants.PARAMETERS;
-import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
-
-import java.security.GeneralSecurityException;
-import java.security.SecureRandom;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
+import org.briarproject.api.TransportId;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyPair;
 import org.briarproject.api.crypto.KeyParser;
@@ -25,6 +11,9 @@ import org.briarproject.api.crypto.PublicKey;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.system.SeedProvider;
+import org.briarproject.api.transport.IncomingKeys;
+import org.briarproject.api.transport.OutgoingKeys;
+import org.briarproject.api.transport.TransportKeys;
 import org.briarproject.util.ByteUtils;
 import org.briarproject.util.StringUtils;
 import org.spongycastle.crypto.AsymmetricCipherKeyPair;
@@ -43,6 +32,21 @@ import org.spongycastle.crypto.params.ECPrivateKeyParameters;
 import org.spongycastle.crypto.params.ECPublicKeyParameters;
 import org.spongycastle.crypto.params.KeyParameter;
 
+import java.security.GeneralSecurityException;
+import java.security.SecureRandom;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static org.briarproject.api.invitation.InvitationConstants.CODE_BITS;
+import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
+import static org.briarproject.crypto.EllipticCurveConstants.PARAMETERS;
+import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
 class CryptoComponentImpl implements CryptoComponent {
 
 	private static final Logger LOG =
@@ -55,22 +59,33 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final int PBKDF_TARGET_MILLIS = 500;
 	private static final int PBKDF_SAMPLES = 30;
 
-	// Labels for secret derivation
-	private static final byte[] MASTER = { 'M', 'A', 'S', 'T', 'E', 'R', '\0' };
-	private static final byte[] SALT = { 'S', 'A', 'L', 'T', '\0' };
-	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', '\0' };
-	// Label for invitation nonce derivation
-	private static final byte[] NONCE = { 'N', 'O', 'N', 'C', 'E', '\0' };
-	// Labels for key derivation
-	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', '_', 'F', 'R', 'A', 'M', 'E', '\0' };
-	private static final byte[] B_FRAME =
-		{ 'B', '_', 'F', 'R', 'A', 'M', 'E', '\0' };
+	// KDF label for master key derivation
+	private static final byte[] MASTER = { 'M', 'A', 'S', 'T', 'E', 'R' };
+	// KDF labels for confirmation code derivation
+	private static final byte[] A_CONFIRM =
+			{ 'A', '_', 'C', 'O', 'N', 'F', 'I', 'R', 'M' };
+	private static final byte[] B_CONFIRM =
+			{ 'B', '_', 'C', 'O', 'N', 'F', 'I', 'R', 'M' };
+	// KDF labels for invitation stream header key derivation
+	private static final byte[] A_INVITE =
+			{ 'A', '_', 'I', 'N', 'V', 'I', 'T', 'E' };
+	private static final byte[] B_INVITE =
+			{ 'B', '_', 'I', 'N', 'V', 'I', 'T', 'E' };
+	// KDF labels for signature nonce derivation
+	private static final byte[] A_NONCE = { 'A', '_', 'N', 'O', 'N', 'C', 'E' };
+	private static final byte[] B_NONCE = { 'B', '_', 'N', 'O', 'N', 'C', 'E' };
+	// KDF label for group salt derivation
+	private static final byte[] SALT = { 'S', 'A', 'L', 'T' };
+	// KDF labels for tag key derivation
+	private static final byte[] A_TAG = { 'A', '_', 'T', 'A', 'G' };
+	private static final byte[] B_TAG = { 'B', '_', 'T', 'A', 'G' };
+	// KDF labels for header key derivation
+	private static final byte[] A_HEADER =
+			{ 'A', '_', 'H', 'E', 'A', 'D', 'E', 'R' };
+	private static final byte[] B_HEADER =
+			{ 'B', '_', 'H', 'E', 'A', 'D', 'E', 'R' };
+	// KDF label for key rotation
+	private static final byte[] ROTATE = { 'R', 'O', 'T', 'A', 'T', 'E' };
 
 	private final SecureRandom secureRandom;
 	private final ECKeyPairGenerator agreementKeyPairGenerator;
@@ -167,26 +182,7 @@ class CryptoComponentImpl implements CryptoComponent {
 		return ByteUtils.readUint(random, CODE_BITS);
 	}
 
-	public int[] deriveConfirmationCodes(byte[] secret) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		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);
-		return codes;
-	}
-
-	public byte[][] deriveInvitationNonces(byte[] secret) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		byte[] alice = counterModeKdf(secret, NONCE, 0);
-		byte[] bob = counterModeKdf(secret, NONCE, 1);
-		return new byte[][] { alice, bob };
-	}
-
-	public byte[] deriveMasterSecret(byte[] theirPublicKey,
+	public SecretKey deriveMasterSecret(byte[] theirPublicKey,
 			KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException {
 		MessageDigest messageDigest = getMessageDigest();
 		byte[] ourPublicKey = ourKeyPair.getPublic().getEncoded();
@@ -204,9 +200,8 @@ class CryptoComponentImpl implements CryptoComponent {
 		PublicKey theirPub = agreementKeyParser.parsePublicKey(theirPublicKey);
 		// The raw secret comes from the key agreement algorithm
 		byte[] raw = deriveSharedSecret(ourPriv, theirPub);
-		// Derive the cooked secret from the raw secret using the
-		// concatenation KDF
-		return concatenationKdf(raw, MASTER, aliceInfo, bobInfo);
+		// Derive the master secret from the raw secret using the hash KDF
+		return new SecretKey(hashKdf(raw, MASTER, aliceInfo, bobInfo));
 	}
 
 	// Package access for testing
@@ -228,46 +223,90 @@ class CryptoComponentImpl implements CryptoComponent {
 		return secret;
 	}
 
-	public byte[] deriveGroupSalt(byte[] secret) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		return counterModeKdf(secret, SALT, 0);
+	public int deriveConfirmationCode(SecretKey master, boolean alice) {
+		byte[] b = macKdf(master, alice ? A_CONFIRM : B_CONFIRM);
+		return ByteUtils.readUint(b, CODE_BITS);
 	}
 
-	public byte[] deriveInitialSecret(byte[] secret, int transportIndex) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		if (transportIndex < 0) throw new IllegalArgumentException();
-		return counterModeKdf(secret, FIRST, transportIndex);
+	public SecretKey deriveInvitationKey(SecretKey master, boolean alice) {
+		return new SecretKey(macKdf(master, alice ? A_INVITE : B_INVITE));
 	}
 
-	public byte[] deriveNextSecret(byte[] secret, long period) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		if (period < 0 || period > MAX_32_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		return counterModeKdf(secret, ROTATE, period);
+	public byte[] deriveSignatureNonce(SecretKey master, boolean alice) {
+		return macKdf(master, alice ? A_NONCE : B_NONCE);
 	}
 
-	public SecretKey deriveTagKey(byte[] secret, boolean alice) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		if (alice) return deriveKey(secret, A_TAG, 0);
-		else return deriveKey(secret, B_TAG, 0);
+	public byte[] deriveGroupSalt(SecretKey master) {
+		return macKdf(master, SALT);
 	}
 
-	public SecretKey deriveFrameKey(byte[] secret, long streamNumber,
+	public TransportKeys deriveTransportKeys(TransportId t,
+			SecretKey master, long rotationPeriod, boolean alice) {
+		// Keys for the previous period are derived from the master secret
+		SecretKey inTagPrev = deriveTagKey(master, t, !alice);
+		SecretKey inHeaderPrev = deriveHeaderKey(master, t, !alice);
+		SecretKey outTagPrev = deriveTagKey(master, t, alice);
+		SecretKey outHeaderPrev = deriveHeaderKey(master, t, alice);
+		// Derive the keys for the current and next periods
+		SecretKey inTagCurr = rotateKey(inTagPrev, rotationPeriod);
+		SecretKey inHeaderCurr = rotateKey(inHeaderPrev, rotationPeriod);
+		SecretKey inTagNext = rotateKey(inTagCurr, rotationPeriod + 1);
+		SecretKey inHeaderNext = rotateKey(inHeaderCurr, rotationPeriod + 1);
+		SecretKey outTagCurr = rotateKey(outTagPrev, rotationPeriod);
+		SecretKey outHeaderCurr = rotateKey(outHeaderPrev, rotationPeriod);
+		// Initialise the reordering windows and stream counters
+		IncomingKeys inPrev = new IncomingKeys(inTagPrev, inHeaderPrev,
+				rotationPeriod - 1);
+		IncomingKeys inCurr = new IncomingKeys(inTagCurr, inHeaderCurr,
+				rotationPeriod);
+		IncomingKeys inNext = new IncomingKeys(inTagNext, inHeaderNext,
+				rotationPeriod + 1);
+		OutgoingKeys outCurr = new OutgoingKeys(outTagCurr, outHeaderCurr,
+				rotationPeriod);
+		// Collect and return the keys
+		return new TransportKeys(t, inPrev, inCurr, inNext, outCurr);
+	}
+
+	public TransportKeys rotateTransportKeys(TransportKeys k,
+			long rotationPeriod) {
+		if (k.getRotationPeriod() >= rotationPeriod) return k;
+		IncomingKeys inPrev = k.getPreviousIncomingKeys();
+		IncomingKeys inCurr = k.getCurrentIncomingKeys();
+		IncomingKeys inNext = k.getNextIncomingKeys();
+		OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
+		long startPeriod = outCurr.getRotationPeriod();
+		// Rotate the keys
+		for (long p = startPeriod + 1; p <= rotationPeriod; p++) {
+			inPrev = inCurr;
+			inCurr = inNext;
+			SecretKey inNextTag = rotateKey(inNext.getTagKey(), p + 1);
+			SecretKey inNextHeader = rotateKey(inNext.getHeaderKey(), p + 1);
+			inNext = new IncomingKeys(inNextTag, inNextHeader, p);
+			SecretKey outCurrTag = rotateKey(outCurr.getTagKey(), p);
+			SecretKey outCurrHeader = rotateKey(outCurr.getHeaderKey(), p);
+			outCurr = new OutgoingKeys(outCurrTag, outCurrHeader, p);
+		}
+		// Collect and return the keys
+		return new TransportKeys(k.getTransportId(), inPrev, inCurr, inNext,
+				outCurr);
+	}
+
+	private SecretKey rotateKey(SecretKey k, long rotationPeriod) {
+		byte[] period = new byte[4];
+		ByteUtils.writeUint32(rotationPeriod, period, 0);
+		return new SecretKey(macKdf(k, ROTATE, period));
+	}
+
+	private SecretKey deriveTagKey(SecretKey master, TransportId t,
 			boolean alice) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		if (streamNumber < 0 || streamNumber > MAX_32_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		if (alice) return deriveKey(secret, A_FRAME, streamNumber);
-		else return deriveKey(secret, B_FRAME, streamNumber);
+		byte[] id = StringUtils.toUtf8(t.getString());
+		return new SecretKey(macKdf(master, alice ? A_TAG : B_TAG, id));
 	}
 
-	private SecretKey deriveKey(byte[] secret, byte[] label, long context) {
-		return new SecretKey(counterModeKdf(secret, label, context));
+	private SecretKey deriveHeaderKey(SecretKey master, TransportId t,
+			boolean alice) {
+		byte[] id = StringUtils.toUtf8(t.getString());
+		return new SecretKey(macKdf(master, alice ? A_HEADER : B_HEADER, id));
 	}
 
 	public void encodeTag(byte[] tag, SecretKey tagKey, long streamNumber) {
@@ -277,7 +316,8 @@ class CryptoComponentImpl implements CryptoComponent {
 		for (int i = 0; i < TAG_LENGTH; i++) tag[i] = 0;
 		ByteUtils.writeUint32(streamNumber, tag, 0);
 		BlockCipher cipher = new AESLightEngine();
-		assert cipher.getBlockSize() == TAG_LENGTH;
+		if (cipher.getBlockSize() != TAG_LENGTH)
+			throw new IllegalStateException();
 		KeyParameter k = new KeyParameter(tagKey.getBytes());
 		cipher.init(true, k);
 		cipher.processBlock(tag, 0, tag, 0);
@@ -348,16 +388,16 @@ class CryptoComponentImpl implements CryptoComponent {
 
 	// Key derivation function based on a hash function - see NIST SP 800-56A,
 	// section 5.8
-	private byte[] concatenationKdf(byte[]... inputs) {
+	private byte[] hashKdf(byte[]... inputs) {
 		// The output of the hash function must be long enough to use as a key
 		MessageDigest messageDigest = getMessageDigest();
 		if (messageDigest.getDigestLength() < SecretKey.LENGTH)
-			throw new RuntimeException();
-		// Each input is length-prefixed - the length must fit in an
-		// unsigned 8-bit integer
+			throw new IllegalStateException();
+		// Calculate the hash over the concatenated length-prefixed inputs
+		byte[] length = new byte[4];
 		for (byte[] input : inputs) {
-			if (input.length > 255) throw new IllegalArgumentException();
-			messageDigest.update((byte) input.length);
+			ByteUtils.writeUint32(input.length, length, 0);
+			messageDigest.update(length);
 			messageDigest.update(input);
 		}
 		byte[] hash = messageDigest.digest();
@@ -368,28 +408,24 @@ class CryptoComponentImpl implements CryptoComponent {
 		return truncated;
 	}
 
-	// Key derivation function based on a PRF in counter mode - see
+	// Key derivation function based on a pseudo-random function - see
 	// NIST SP 800-108, section 5.1
-	private byte[] counterModeKdf(byte[] secret, byte[] label, long context) {
-		if (secret.length != SecretKey.LENGTH)
-			throw new IllegalArgumentException();
-		// The label must be null-terminated
-		if (label[label.length - 1] != '\0')
-			throw new IllegalArgumentException();
+	private byte[] macKdf(SecretKey key, byte[]... inputs) {
 		// Initialise the PRF
 		Mac prf = new HMac(new SHA256Digest());
-		KeyParameter k = new KeyParameter(secret);
-		prf.init(k);
-		int macLength = prf.getMacSize();
+		prf.init(new KeyParameter(key.getBytes()));
 		// The output of the PRF must be long enough to use as a key
-		if (macLength < SecretKey.LENGTH) throw new RuntimeException();
+		int macLength = prf.getMacSize();
+		if (macLength < SecretKey.LENGTH)
+			throw new IllegalStateException();
+		// Calculate the PRF over the concatenated length-prefixed inputs
+		byte[] length = new byte[4];
+		for (byte[] input : inputs) {
+			ByteUtils.writeUint32(input.length, length, 0);
+			prf.update(length, 0, length.length);
+			prf.update(input, 0, input.length);
+		}
 		byte[] mac = new byte[macLength];
-		prf.update((byte) 0); // Counter
-		prf.update(label, 0, label.length); // Null-terminated
-		byte[] contextBytes = new byte[4];
-		ByteUtils.writeUint32(context, contextBytes, 0);
-		prf.update(contextBytes, 0, contextBytes.length);
-		prf.update((byte) SecretKey.LENGTH); // Output length
 		prf.doFinal(mac, 0);
 		// The output is the first SecretKey.LENGTH bytes of the MAC
 		if (mac.length == SecretKey.LENGTH) return mac;
diff --git a/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java b/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java
index 205fc2e5e2de488d71a40278173b9176d13d0da8..571f80013bda5a4197bf1cafa6ed4156c3eeeb36 100644
--- a/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java
@@ -5,7 +5,6 @@ import java.io.InputStream;
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.crypto.StreamDecrypter;
 import org.briarproject.api.crypto.StreamDecrypterFactory;
@@ -13,34 +12,21 @@ import org.briarproject.api.transport.StreamContext;
 
 class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
 
-	private final CryptoComponent crypto;
 	private final Provider<AuthenticatedCipher> cipherProvider;
 
 	@Inject
-	StreamDecrypterFactoryImpl(CryptoComponent crypto,
-			Provider<AuthenticatedCipher> cipherProvider) {
-		this.crypto = crypto;
+	StreamDecrypterFactoryImpl(Provider<AuthenticatedCipher> cipherProvider) {
 		this.cipherProvider = cipherProvider;
 	}
 
 	public StreamDecrypter createStreamDecrypter(InputStream in,
 			StreamContext ctx) {
-		// Derive the frame key
-		byte[] secret = ctx.getSecret();
-		long streamNumber = ctx.getStreamNumber();
-		boolean alice = !ctx.getAlice();
-		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
-		// Create the decrypter
 		AuthenticatedCipher cipher = cipherProvider.get();
-		return new StreamDecrypterImpl(in, cipher, frameKey);
+		return new StreamDecrypterImpl(in, cipher, ctx.getHeaderKey());
 	}
 
 	public StreamDecrypter createInvitationStreamDecrypter(InputStream in,
-			byte[] secret, boolean alice) {
-		// Derive the frame key
-		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
-		// Create the decrypter
-		AuthenticatedCipher cipher = cipherProvider.get();
-		return new StreamDecrypterImpl(in, cipher, frameKey);
+			SecretKey headerKey) {
+		return new StreamDecrypterImpl(in, cipherProvider.get(), headerKey);
 	}
 }
diff --git a/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
index 1cad6d2184f2bea30b5646637b479211c3b3c52f..19d3c69c456084e511d0c59a8c6b5d8c66f0d341 100644
--- a/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
@@ -1,75 +1,77 @@
 package org.briarproject.crypto;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.IV_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamDecrypter;
 
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 
-import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.crypto.StreamDecrypter;
+import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.IV_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
 
+// FIXME: Implementation is incomplete, doesn't read the stream header
 class StreamDecrypterImpl implements StreamDecrypter {
 
 	private final InputStream in;
 	private final AuthenticatedCipher frameCipher;
 	private final SecretKey frameKey;
-	private final byte[] iv, header, ciphertext;
+	private final byte[] iv, frameHeader, frameCiphertext;
 
 	private long frameNumber;
 	private boolean finalFrame;
 
 	StreamDecrypterImpl(InputStream in, AuthenticatedCipher frameCipher,
-			SecretKey frameKey) {
+			SecretKey headerKey) {
 		this.in = in;
 		this.frameCipher = frameCipher;
-		this.frameKey = frameKey;
+		this.frameKey = headerKey; // FIXME
 		iv = new byte[IV_LENGTH];
-		header = new byte[HEADER_LENGTH];
-		ciphertext = new byte[MAX_FRAME_LENGTH];
+		frameHeader = new byte[HEADER_LENGTH];
+		frameCiphertext = new byte[MAX_FRAME_LENGTH];
 		frameNumber = 0;
 		finalFrame = false;
 	}
 
 	public int readFrame(byte[] payload) throws IOException {
+		// The buffer must be big enough for a full-size frame
 		if (payload.length < MAX_PAYLOAD_LENGTH)
 			throw new IllegalArgumentException();
 		if (finalFrame) return -1;
-		// Read the header
+		// Read the frame header
 		int offset = 0;
 		while (offset < HEADER_LENGTH) {
-			int read = in.read(ciphertext, offset, HEADER_LENGTH - offset);
+			int read = in.read(frameCiphertext, offset, HEADER_LENGTH - offset);
 			if (read == -1) throw new EOFException();
 			offset += read;
 		}
-		// Decrypt and authenticate the header
+		// Decrypt and authenticate the frame header
 		FrameEncoder.encodeIv(iv, frameNumber, true);
 		try {
 			frameCipher.init(false, frameKey, iv);
-			int decrypted = frameCipher.process(ciphertext, 0, HEADER_LENGTH,
-					header, 0);
+			int decrypted = frameCipher.process(frameCiphertext, 0,
+					HEADER_LENGTH, frameHeader, 0);
 			if (decrypted != HEADER_LENGTH - MAC_LENGTH)
 				throw new RuntimeException();
 		} catch (GeneralSecurityException e) {
 			throw new FormatException();
 		}
-		// Decode and validate the header
-		finalFrame = FrameEncoder.isFinalFrame(header);
-		int payloadLength = FrameEncoder.getPayloadLength(header);
-		int paddingLength = FrameEncoder.getPaddingLength(header);
+		// Decode and validate the frame header
+		finalFrame = FrameEncoder.isFinalFrame(frameHeader);
+		int payloadLength = FrameEncoder.getPayloadLength(frameHeader);
+		int paddingLength = FrameEncoder.getPaddingLength(frameHeader);
 		if (payloadLength + paddingLength > MAX_PAYLOAD_LENGTH)
 			throw new FormatException();
 		// Read the payload and padding
 		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
 				+ MAC_LENGTH;
 		while (offset < frameLength) {
-			int read = in.read(ciphertext, offset, frameLength - offset);
+			int read = in.read(frameCiphertext, offset, frameLength - offset);
 			if (read == -1) throw new EOFException();
 			offset += read;
 		}
@@ -77,7 +79,7 @@ class StreamDecrypterImpl implements StreamDecrypter {
 		FrameEncoder.encodeIv(iv, frameNumber, false);
 		try {
 			frameCipher.init(false, frameKey, iv);
-			int decrypted = frameCipher.process(ciphertext, HEADER_LENGTH,
+			int decrypted = frameCipher.process(frameCiphertext, HEADER_LENGTH,
 					payloadLength + paddingLength + MAC_LENGTH, payload, 0);
 			if (decrypted != payloadLength + paddingLength)
 				throw new RuntimeException();
diff --git a/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java b/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
index 90fd2af20e97deb33a7668f07e91c49be02ffcb7..e8e6fada7f34ec49d2e5a1bf8e81bd266e59354a 100644
--- a/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
@@ -27,26 +27,15 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
 
 	public StreamEncrypter createStreamEncrypter(OutputStream out,
 			StreamContext ctx) {
-		byte[] secret = ctx.getSecret();
-		long streamNumber = ctx.getStreamNumber();
-		boolean alice = ctx.getAlice();
-		// Encode the tag
 		byte[] tag = new byte[TAG_LENGTH];
-		SecretKey tagKey = crypto.deriveTagKey(secret, alice);
-		crypto.encodeTag(tag, tagKey, streamNumber);
-		// Derive the frame key
-		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
-		// Create the encrypter
+		crypto.encodeTag(tag, ctx.getTagKey(), ctx.getStreamNumber());
 		AuthenticatedCipher cipher = cipherProvider.get();
-		return new StreamEncrypterImpl(out, cipher, frameKey, tag);
+		return new StreamEncrypterImpl(out, cipher, ctx.getHeaderKey(), tag);
 	}
 
 	public StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
-			byte[] secret, boolean alice) {
-		// Derive the frame key
-		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
-		// Create the encrypter
+			SecretKey headerKey) {
 		AuthenticatedCipher cipher = cipherProvider.get();
-		return new StreamEncrypterImpl(out, cipher, frameKey, null);
+		return new StreamEncrypterImpl(out, cipher, headerKey, null);
 	}
 }
diff --git a/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
index 7d9f71b7cf5060180cf4f0fa4b25aad7d31095fa..89fde2a91da9e7bfc506b04a77dfc63a0c119dcd 100644
--- a/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
@@ -1,5 +1,12 @@
 package org.briarproject.crypto;
 
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamEncrypter;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+
 import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.IV_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
@@ -7,32 +14,26 @@ import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH
 import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
 import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
-import java.io.IOException;
-import java.io.OutputStream;
-import java.security.GeneralSecurityException;
-
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.crypto.StreamEncrypter;
-
+// FIXME: Implementation is incomplete, doesn't write the stream header
 class StreamEncrypterImpl implements StreamEncrypter {
 
 	private final OutputStream out;
 	private final AuthenticatedCipher frameCipher;
 	private final SecretKey frameKey;
-	private final byte[] tag, iv, plaintext, ciphertext;
+	private final byte[] tag, iv, framePlaintext, frameCiphertext;
 
 	private long frameNumber;
 	private boolean writeTag;
 
 	StreamEncrypterImpl(OutputStream out, AuthenticatedCipher frameCipher,
-			SecretKey frameKey, byte[] tag) {
+			SecretKey headerKey, byte[] tag) {
 		this.out = out;
 		this.frameCipher = frameCipher;
-		this.frameKey = frameKey;
+		this.frameKey = headerKey; // FIXME
 		this.tag = tag;
 		iv = new byte[IV_LENGTH];
-		plaintext = new byte[HEADER_LENGTH + MAX_PAYLOAD_LENGTH];
-		ciphertext = new byte[MAX_FRAME_LENGTH];
+		framePlaintext = new byte[HEADER_LENGTH + MAX_PAYLOAD_LENGTH];
+		frameCiphertext = new byte[MAX_FRAME_LENGTH];
 		frameNumber = 0;
 		writeTag = (tag != null);
 	}
@@ -48,37 +49,39 @@ class StreamEncrypterImpl implements StreamEncrypter {
 			out.write(tag, 0, tag.length);
 			writeTag = false;
 		}
-		// Encode the header
-		FrameEncoder.encodeHeader(plaintext, finalFrame, payloadLength,
+		// Encode the frame header
+		FrameEncoder.encodeHeader(framePlaintext, finalFrame, payloadLength,
 				paddingLength);
-		// Encrypt and authenticate the header
+		// Encrypt and authenticate the frame header
 		FrameEncoder.encodeIv(iv, frameNumber, true);
 		try {
 			frameCipher.init(true, frameKey, iv);
-			int encrypted = frameCipher.process(plaintext, 0,
-					HEADER_LENGTH - MAC_LENGTH, ciphertext, 0);
+			int encrypted = frameCipher.process(framePlaintext, 0,
+					HEADER_LENGTH - MAC_LENGTH, frameCiphertext, 0);
 			if (encrypted != HEADER_LENGTH) throw new RuntimeException();
 		} catch (GeneralSecurityException badCipher) {
 			throw new RuntimeException(badCipher);
 		}
 		// Combine the payload and padding
-		System.arraycopy(payload, 0, plaintext, HEADER_LENGTH, payloadLength);
+		System.arraycopy(payload, 0, framePlaintext, HEADER_LENGTH,
+				payloadLength);
 		for (int i = 0; i < paddingLength; i++)
-			plaintext[HEADER_LENGTH + payloadLength + i] = 0;
+			framePlaintext[HEADER_LENGTH + payloadLength + i] = 0;
 		// Encrypt and authenticate the payload and padding
 		FrameEncoder.encodeIv(iv, frameNumber, false);
 		try {
 			frameCipher.init(true, frameKey, iv);
-			int encrypted = frameCipher.process(plaintext, HEADER_LENGTH,
-					payloadLength + paddingLength, ciphertext, HEADER_LENGTH);
+			int encrypted = frameCipher.process(framePlaintext, HEADER_LENGTH,
+					payloadLength + paddingLength, frameCiphertext,
+					HEADER_LENGTH);
 			if (encrypted != payloadLength + paddingLength + MAC_LENGTH)
 				throw new RuntimeException();
 		} catch (GeneralSecurityException badCipher) {
 			throw new RuntimeException(badCipher);
 		}
 		// Write the frame
-		out.write(ciphertext, 0, HEADER_LENGTH + payloadLength + paddingLength
-				+ MAC_LENGTH);
+		out.write(frameCiphertext, 0, HEADER_LENGTH + payloadLength
+				+ paddingLength + MAC_LENGTH);
 		frameNumber++;
 	}
 
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index da83486e42fb51e4c8abca90f6dc986f12f411f5..9d3eda9bf891a361c08eb6ee2733b3b562765f4e 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -1,9 +1,5 @@
 package org.briarproject.db;
 
-import java.io.IOException;
-import java.util.Collection;
-import java.util.Map;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorId;
 import org.briarproject.api.Contact;
@@ -25,8 +21,11 @@ import org.briarproject.api.messaging.SubscriptionAck;
 import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportAck;
 import org.briarproject.api.messaging.TransportUpdate;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.TransportKeys;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.Map;
 
 // FIXME: Document the preconditions for calling each method
 
@@ -89,13 +88,6 @@ interface Database<T> {
 	ContactId addContact(T txn, Author remote, AuthorId local)
 			throws DbException;
 
-	/**
-	 * Stores an endpoint.
-	 * <p>
-	 * Locking: write.
-	 */
-	void addEndpoint(T txn, Endpoint ep) throws DbException;
-
 	/**
 	 * Subscribes to a group, or returns false if the user already has the
 	 * maximum number of subscriptions.
@@ -125,15 +117,6 @@ interface Database<T> {
 	 */
 	void addOfferedMessage(T txn, ContactId c, MessageId m) throws DbException;
 
-	/**
-	 * Stores the given temporary secrets and deletes any secrets that have
-	 * been made obsolete.
-	 * <p>
-	 * Locking: write.
-	 */
-	void addSecrets(T txn, Collection<TemporarySecret> secrets)
-			throws DbException;
-
 	/**
 	 * Initialises the status of the given message with respect to the given
 	 * contact.
@@ -154,6 +137,13 @@ interface Database<T> {
 	boolean addTransport(T txn, TransportId t, int maxLatency)
 			throws DbException;
 
+	/**
+	 * Stores the given transport keys for a newly added contact.
+	 * <p>
+	 * Locking: write.
+	 */
+	void addTransportKeys(T txn, ContactId c, TransportKeys k) throws DbException;
+
 	/**
 	 * Makes a group visible to the given contact.
 	 * <p>
@@ -270,13 +260,6 @@ interface Database<T> {
 	 */
 	Collection<ContactId> getContacts(T txn, AuthorId a) throws DbException;
 
-	/**
-	 * Returns all endpoints.
-	 * <p>
-	 * Locking: read.
-	 */
-	Collection<Endpoint> getEndpoints(T txn) throws DbException;
-
 	/**
 	 * Returns the amount of free storage space available to the database, in
 	 * bytes. This is based on the minimum of the space available on the device
@@ -461,13 +444,6 @@ interface Database<T> {
 	RetentionUpdate getRetentionUpdate(T txn, ContactId c, int maxLatency)
 			throws DbException;
 
-	/**
-	 * Returns all temporary secrets.
-	 * <p>
-	 * Locking: read.
-	 */
-	Collection<TemporarySecret> getSecrets(T txn) throws DbException;
-
 	/**
 	 * Returns all settings.
 	 * <p>
@@ -509,7 +485,15 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the maximum latencies of all supported transports.
+	 * Returns all transport keys for the given transport.
+	 * <p>
+	 * Locking: read.
+	 */
+	Map<ContactId, TransportKeys> getTransportKeys(T txn, TransportId t)
+			throws DbException;
+
+	/**
+	 * Returns the maximum latencies in milliseconds of all transports.
 	 * <p>
 	 * Locking: read.
 	 */
@@ -540,14 +524,13 @@ interface Database<T> {
 	Collection<ContactId> getVisibility(T txn, GroupId g) throws DbException;
 
 	/**
-	 * Increments the outgoing stream counter for the given endpoint in the
-	 * given rotation period and returns the old value, or -1 if the counter
-	 * does not exist.
+	 * Increments the outgoing stream counter for the given contact and
+	 * transport in the given rotation period.
 	 * <p>
 	 * Locking: write.
 	 */
-	long incrementStreamCounter(T txn, ContactId c, TransportId t, long period)
-			throws DbException;
+	void incrementStreamCounter(T txn, ContactId c, TransportId t,
+			long rotationPeriod) throws DbException;
 
 	/**
 	 * Increments the retention time versions for all contacts to indicate that
@@ -692,13 +675,13 @@ interface Database<T> {
 	void resetExpiryTime(T txn, ContactId c, MessageId m) throws DbException;
 
 	/**
-	 * Sets the reordering window for the given endpoint in the given rotation
-	 * period.
+	 * Sets the reordering window for the given contact and transport in the
+	 * given rotation period.
 	 * <p>
 	 * Locking: write.
 	 */
-	void setReorderingWindow(T txn, ContactId c, TransportId t, long period,
-			long centre, byte[] bitmap) throws DbException;
+	void setReorderingWindow(T txn, ContactId c, TransportId t,
+			long rotationPeriod, long base, byte[] bitmap) throws DbException;
 
 	/**
 	 * Updates the groups to which the given contact subscribes and returns
@@ -716,7 +699,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: write.
 	 */
-	public void setInboxGroup(T txn, ContactId c, Group g) throws DbException;
+	void setInboxGroup(T txn, ContactId c, Group g) throws DbException;
 
 	/**
 	 * Marks a message as read or unread.
@@ -798,4 +781,12 @@ interface Database<T> {
 	 */
 	void updateExpiryTime(T txn, ContactId c, MessageId m, int maxLatency)
 			throws DbException;
+
+	/**
+	 * Stores the given transport keys, deleting any keys they have replaced.
+	 * <p>
+	 * Locking: write.
+	 */
+	void updateTransportKeys(T txn, Map<ContactId, TransportKeys> keys)
+			throws DbException;
 }
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index fbf81fe6b18324027519f63c87f45b2e4635444f..4eafdfa5139ee1009b71e88ba3140b24c8cc5617 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -1,25 +1,5 @@
 package org.briarproject.db;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.db.DatabaseConstants.BYTES_PER_SWEEP;
-import static org.briarproject.db.DatabaseConstants.CRITICAL_FREE_SPACE;
-import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
-import static org.briarproject.db.DatabaseConstants.MAX_TRANSACTIONS_BETWEEN_SPACE_CHECKS;
-import static org.briarproject.db.DatabaseConstants.MIN_FREE_SPACE;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorId;
 import org.briarproject.api.Contact;
@@ -75,8 +55,29 @@ import org.briarproject.api.messaging.SubscriptionAck;
 import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportAck;
 import org.briarproject.api.messaging.TransportUpdate;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.TransportKeys;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.db.DatabaseConstants.BYTES_PER_SWEEP;
+import static org.briarproject.db.DatabaseConstants.CRITICAL_FREE_SPACE;
+import static org.briarproject.db.DatabaseConstants.MAX_OFFERED_MESSAGES;
+import static org.briarproject.db.DatabaseConstants.MAX_TRANSACTIONS_BETWEEN_SPACE_CHECKS;
+import static org.briarproject.db.DatabaseConstants.MIN_FREE_SPACE;
 
 /**
  * An implementation of DatabaseComponent using reentrant read-write locks.
@@ -85,7 +86,7 @@ import org.briarproject.api.transport.TemporarySecret;
  * implementation is safe on a given JVM.
  */
 class DatabaseComponentImpl<T> implements DatabaseComponent,
-DatabaseCleaner.Callback {
+		DatabaseCleaner.Callback {
 
 	private static final Logger LOG =
 			Logger.getLogger(DatabaseComponentImpl.class.getName());
@@ -180,26 +181,6 @@ DatabaseCleaner.Callback {
 		return c;
 	}
 
-	public void addEndpoint(Endpoint ep) throws DbException {
-		lock.writeLock().lock();
-		try {
-			T txn = db.startTransaction();
-			try {
-				if (!db.containsContact(txn, ep.getContactId()))
-					throw new NoSuchContactException();
-				if (!db.containsTransport(txn, ep.getTransportId()))
-					throw new NoSuchTransportException();
-				db.addEndpoint(txn, ep);
-				db.commitTransaction(txn);
-			} catch (DbException e) {
-				db.abortTransaction(txn);
-				throw e;
-			}
-		} finally {
-			lock.writeLock().unlock();
-		}
-	}
-
 	public boolean addGroup(Group g) throws DbException {
 		boolean added = false;
 		lock.writeLock().lock();
@@ -290,20 +271,14 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void addSecrets(Collection<TemporarySecret> secrets)
+	public boolean addTransport(TransportId t, int maxLatency)
 			throws DbException {
+		boolean added;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Collection<TemporarySecret> relevant =
-						new ArrayList<TemporarySecret>();
-				for (TemporarySecret s : secrets) {
-					if (db.containsContact(txn, s.getContactId()))
-						if (db.containsTransport(txn, s.getTransportId()))
-							relevant.add(s);
-				}
-				if (!secrets.isEmpty()) db.addSecrets(txn, relevant);
+				added = db.addTransport(txn, t, maxLatency);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -312,16 +287,21 @@ DatabaseCleaner.Callback {
 		} finally {
 			lock.writeLock().unlock();
 		}
+		if (added) eventBus.broadcast(new TransportAddedEvent(t, maxLatency));
+		return added;
 	}
 
-	public boolean addTransport(TransportId t, int maxLatency)
+	public void addTransportKeys(ContactId c, TransportKeys k)
 			throws DbException {
-		boolean added;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				added = db.addTransport(txn, t, maxLatency);
+				if (!db.containsContact(txn, c))
+					throw new NoSuchContactException();
+				if (!db.containsTransport(txn, k.getTransportId()))
+					throw new NoSuchTransportException();
+				db.addTransportKeys(txn, c, k);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -330,8 +310,6 @@ DatabaseCleaner.Callback {
 		} finally {
 			lock.writeLock().unlock();
 		}
-		if (added) eventBus.broadcast(new TransportAddedEvent(t, maxLatency));
-		return added;
 	}
 
 	public Ack generateAck(ContactId c, int maxMessages) throws DbException {
@@ -883,14 +861,14 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Collection<TemporarySecret> getSecrets() throws DbException {
+	public Settings getSettings() throws DbException {
 		lock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Collection<TemporarySecret> secrets = db.getSecrets(txn);
+				Settings s = db.getSettings(txn);
 				db.commitTransaction(txn);
-				return secrets;
+				return s;
 			} catch (DbException e) {
 				db.abortTransaction(txn);
 				throw e;
@@ -900,14 +878,14 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Settings getSettings() throws DbException {
+	public Collection<Contact> getSubscribers(GroupId g) throws DbException {
 		lock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Settings s = db.getSettings(txn);
+				Collection<Contact> contacts = db.getSubscribers(txn, g);
 				db.commitTransaction(txn);
-				return s;
+				return contacts;
 			} catch (DbException e) {
 				db.abortTransaction(txn);
 				throw e;
@@ -917,14 +895,18 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Collection<Contact> getSubscribers(GroupId g) throws DbException {
+	public Map<ContactId, TransportKeys> getTransportKeys(TransportId t)
+			throws DbException {
 		lock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Collection<Contact> contacts = db.getSubscribers(txn, g);
+				if (!db.containsTransport(txn, t))
+					throw new NoSuchTransportException();
+				Map<ContactId, TransportKeys> keys =
+						db.getTransportKeys(txn, t);
 				db.commitTransaction(txn);
-				return contacts;
+				return keys;
 			} catch (DbException e) {
 				db.abortTransaction(txn);
 				throw e;
@@ -989,8 +971,8 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public long incrementStreamCounter(ContactId c, TransportId t,
-			long period) throws DbException {
+	public void incrementStreamCounter(ContactId c, TransportId t,
+			long rotationPeriod) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
@@ -999,9 +981,8 @@ DatabaseCleaner.Callback {
 					throw new NoSuchContactException();
 				if (!db.containsTransport(txn, t))
 					throw new NoSuchTransportException();
-				long counter = db.incrementStreamCounter(txn, c, t, period);
+				db.incrementStreamCounter(txn, c, t, rotationPeriod);
 				db.commitTransaction(txn);
-				return counter;
 			} catch (DbException e) {
 				db.abortTransaction(txn);
 				throw e;
@@ -1404,17 +1385,14 @@ DatabaseCleaner.Callback {
 		eventBus.broadcast(new TransportRemovedEvent(t));
 	}
 
-	public void setReorderingWindow(ContactId c, TransportId t, long period,
-			long centre, byte[] bitmap) throws DbException {
+	public void setInboxGroup(ContactId c, Group g) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsContact(txn, c))
 					throw new NoSuchContactException();
-				if (!db.containsTransport(txn, t))
-					throw new NoSuchTransportException();
-				db.setReorderingWindow(txn, c, t, period, centre, bitmap);
+				db.setInboxGroup(txn, c, g);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1425,14 +1403,14 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void setInboxGroup(ContactId c, Group g) throws DbException {
+	public void setReadFlag(MessageId m, boolean read) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				if (!db.containsContact(txn, c))
-					throw new NoSuchContactException();
-				db.setInboxGroup(txn, c, g);
+				if (!db.containsMessage(txn, m))
+					throw new NoSuchMessageException();
+				db.setReadFlag(txn, m, read);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1443,14 +1421,15 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void setReadFlag(MessageId m, boolean read) throws DbException {
+	public void setRemoteProperties(ContactId c,
+			Map<TransportId, TransportProperties> p) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				if (!db.containsMessage(txn, m))
-					throw new NoSuchMessageException();
-				db.setReadFlag(txn, m, read);
+				if (!db.containsContact(txn, c))
+					throw new NoSuchContactException();
+				db.setRemoteProperties(txn, c, p);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1461,15 +1440,17 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public void setRemoteProperties(ContactId c,
-			Map<TransportId, TransportProperties> p) throws DbException {
+	public void setReorderingWindow(ContactId c, TransportId t,
+			long rotationPeriod, long base, byte[] bitmap) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
 				if (!db.containsContact(txn, c))
 					throw new NoSuchContactException();
-				db.setRemoteProperties(txn, c, p);
+				if (!db.containsTransport(txn, t))
+					throw new NoSuchTransportException();
+				db.setReorderingWindow(txn, c, t, rotationPeriod, base, bitmap);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1552,6 +1533,33 @@ DatabaseCleaner.Callback {
 			eventBus.broadcast(new LocalSubscriptionsUpdatedEvent(affected));
 	}
 
+	public void updateTransportKeys(Map<ContactId, TransportKeys> keys)
+			throws DbException {
+		lock.writeLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				Map<ContactId, TransportKeys> filtered =
+						new HashMap<ContactId, TransportKeys>();
+				for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
+					ContactId c = e.getKey();
+					TransportKeys k = e.getValue();
+					if (db.containsContact(txn, c)
+							&& db.containsTransport(txn, k.getTransportId())) {
+						filtered.put(c, k);
+					}
+				}
+				db.updateTransportKeys(txn, filtered);
+				db.commitTransaction(txn);
+			} catch (DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
 	public void checkFreeSpaceAndClean() throws DbException {
 		long freeSpace = db.getFreeSpace();
 		if (LOG.isLoggable(INFO)) LOG.info(freeSpace + " bytes free space");
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index 4ac27fd6ac7c2203ccc3113408191762c1d35fcb..acecbf4fb79cee3234bb388e7af9d00db05c98a9 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -1,37 +1,5 @@
 package org.briarproject.db;
 
-import static java.sql.Types.BINARY;
-import static java.sql.Types.VARCHAR;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.Author.Status.ANONYMOUS;
-import static org.briarproject.api.Author.Status.UNKNOWN;
-import static org.briarproject.api.Author.Status.VERIFIED;
-import static org.briarproject.api.messaging.MessagingConstants.MAX_SUBSCRIPTIONS;
-import static org.briarproject.api.messaging.MessagingConstants.RETENTION_GRANULARITY;
-import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
-
-import java.io.IOException;
-import java.sql.Connection;
-import java.sql.PreparedStatement;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.concurrent.locks.Condition;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.logging.Logger;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorId;
 import org.briarproject.api.Contact;
@@ -41,6 +9,7 @@ import org.briarproject.api.Settings;
 import org.briarproject.api.TransportConfig;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DbClosedException;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.MessageHeader;
@@ -56,8 +25,41 @@ import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportAck;
 import org.briarproject.api.messaging.TransportUpdate;
 import org.briarproject.api.system.Clock;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.IncomingKeys;
+import org.briarproject.api.transport.OutgoingKeys;
+import org.briarproject.api.transport.TransportKeys;
+
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Logger;
+
+import static java.sql.Types.BINARY;
+import static java.sql.Types.VARCHAR;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.Author.Status.ANONYMOUS;
+import static org.briarproject.api.Author.Status.UNKNOWN;
+import static org.briarproject.api.Author.Status.VERIFIED;
+import static org.briarproject.api.messaging.MessagingConstants.MAX_SUBSCRIPTIONS;
+import static org.briarproject.api.messaging.MessagingConstants.RETENTION_GRANULARITY;
+import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
 
 /**
  * A generic database implementation that can be used with any JDBC-compatible
@@ -65,8 +67,8 @@ import org.briarproject.api.transport.TemporarySecret;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 9;
-	private static final int MIN_SCHEMA_VERSION = 9;
+	private static final int SCHEMA_VERSION = 10;
+	private static final int MIN_SCHEMA_VERSION = 10;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -277,13 +279,16 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE)";
 
-	private static final String CREATE_ENDPOINTS =
-			"CREATE TABLE endpoints"
+	private static final String CREATE_INCOMING_KEYS =
+			"CREATE TABLE incomingKeys"
 					+ " (contactId INT NOT NULL,"
 					+ " transportId VARCHAR NOT NULL,"
-					+ " epoch BIGINT NOT NULL,"
-					+ " alice BOOLEAN NOT NULL,"
-					+ " PRIMARY KEY (contactId, transportId),"
+					+ " period BIGINT NOT NULL,"
+					+ " tagKey SECRET NOT NULL,"
+					+ " headerKey SECRET NOT NULL,"
+					+ " base BIGINT NOT NULL,"
+					+ " bitmap BINARY NOT NULL,"
+					+ " PRIMARY KEY (contactId, transportId, period),"
 					+ " FOREIGN KEY (contactId)"
 					+ " REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE,"
@@ -291,16 +296,15 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " REFERENCES transports (transportId)"
 					+ " ON DELETE CASCADE)";
 
-	private static final String CREATE_SECRETS =
-			"CREATE TABLE secrets"
+	private static final String CREATE_OUTGOING_KEYS =
+			"CREATE TABLE outgoingKeys"
 					+ " (contactId INT NOT NULL,"
 					+ " transportId VARCHAR 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),"
+					+ " tagKey SECRET NOT NULL,"
+					+ " headerKey SECRET NOT NULL,"
+					+ " stream BIGINT NOT NULL,"
+					+ " PRIMARY KEY (contactId, transportId),"
 					+ " FOREIGN KEY (contactId)"
 					+ " REFERENCES contacts (contactId)"
 					+ " ON DELETE CASCADE,"
@@ -324,6 +328,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private boolean closed = false; // Locking: connectionsLock
 
 	protected abstract Connection createConnection() throws SQLException;
+
 	protected abstract void flushBuffersToDisk(Statement s) throws SQLException;
 
 	private final Lock connectionsLock = new ReentrantLock();
@@ -339,7 +344,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	protected void open(String driverClass, boolean reopen) throws DbException,
-	IOException {
+			IOException {
 		// Load the JDBC driver
 		try {
 			Class.forName(driverClass);
@@ -382,7 +387,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			if (rs != null) rs.close();
 		} catch (SQLException e) {
-			if (LOG.isLoggable(WARNING))LOG.log(WARNING, e.toString(), e);
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
 	}
 
@@ -390,7 +395,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		try {
 			if (s != null) s.close();
 		} catch (SQLException e) {
-			if (LOG.isLoggable(WARNING))LOG.log(WARNING, e.toString(), e);
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
 	}
 
@@ -418,8 +423,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			s.executeUpdate(insertTypeNames(CREATE_TRANSPORT_VERSIONS));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORT_PROPS));
 			s.executeUpdate(insertTypeNames(CREATE_CONTACT_TRANSPORT_VERSIONS));
-			s.executeUpdate(insertTypeNames(CREATE_ENDPOINTS));
-			s.executeUpdate(insertTypeNames(CREATE_SECRETS));
+			s.executeUpdate(insertTypeNames(CREATE_INCOMING_KEYS));
+			s.executeUpdate(insertTypeNames(CREATE_OUTGOING_KEYS));
 			s.close();
 		} catch (SQLException e) {
 			tryToClose(s);
@@ -480,7 +485,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 			try {
 				txn.close();
 			} catch (SQLException e1) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e1.toString(), e1);
+				if (LOG.isLoggable(WARNING))
+					LOG.log(WARNING, e1.toString(), e1);
 			}
 			// Whatever happens, allow the database to close
 			connectionsLock.lock();
@@ -679,26 +685,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void addEndpoint(Connection txn, Endpoint ep) throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "INSERT INTO endpoints"
-					+ " (contactId, transportId, epoch, alice)"
-					+ " VALUES (?, ?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, ep.getContactId().getInt());
-			ps.setString(2, ep.getTransportId().getString());
-			ps.setLong(3, ep.getEpoch());
-			ps.setBoolean(4, ep.getAlice());
-			int affected = ps.executeUpdate();
-			if (affected != 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public boolean addGroup(Connection txn, Group g) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -824,52 +810,6 @@ 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)"
-					+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
-			ps = txn.prepareStatement(sql);
-			for (TemporarySecret s : secrets) {
-				ps.setInt(1, s.getContactId().getInt());
-				ps.setString(2, s.getTransportId().getString());
-				ps.setLong(3, s.getPeriod());
-				ps.setBytes(4, s.getSecret());
-				ps.setLong(5, s.getOutgoingStreamCounter());
-				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.setString(2, s.getTransportId().getString());
-				ps.setLong(3, s.getPeriod() - 2);
-				ps.addBatch();
-			}
-			batchAffected = ps.executeBatch();
-			if (batchAffected.length != secrets.size())
-				throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public void addStatus(Connection txn, ContactId c, MessageId m, boolean ack,
 			boolean seen) throws DbException {
 		PreparedStatement ps = null;
@@ -947,6 +887,68 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public void addTransportKeys(Connection txn, ContactId c, TransportKeys k)
+			throws DbException {
+		PreparedStatement ps = null;
+		try {
+			// Store the incoming keys
+			String sql = "INSERT INTO incomingKeys (contactId, transportId,"
+					+ " period, tagKey, headerKey, base, bitmap)"
+					+ " VALUES (?, ?, ?, ?, ?, ?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setString(2, k.getTransportId().getString());
+			// Previous rotation period
+			IncomingKeys inPrev = k.getPreviousIncomingKeys();
+			ps.setLong(3, inPrev.getRotationPeriod());
+			ps.setBytes(4, inPrev.getTagKey().getBytes());
+			ps.setBytes(5, inPrev.getHeaderKey().getBytes());
+			ps.setLong(6, inPrev.getWindowBase());
+			ps.setBytes(7, inPrev.getWindowBitmap());
+			ps.addBatch();
+			// Current rotation period
+			IncomingKeys inCurr = k.getCurrentIncomingKeys();
+			ps.setLong(3, inCurr.getRotationPeriod());
+			ps.setBytes(4, inCurr.getTagKey().getBytes());
+			ps.setBytes(5, inCurr.getHeaderKey().getBytes());
+			ps.setLong(6, inCurr.getWindowBase());
+			ps.setBytes(7, inCurr.getWindowBitmap());
+			ps.addBatch();
+			// Next rotation period
+			IncomingKeys inNext = k.getNextIncomingKeys();
+			ps.setLong(3, inNext.getRotationPeriod());
+			ps.setBytes(4, inNext.getTagKey().getBytes());
+			ps.setBytes(5, inNext.getHeaderKey().getBytes());
+			ps.setLong(6, inNext.getWindowBase());
+			ps.setBytes(7, inNext.getWindowBitmap());
+			ps.addBatch();
+			int[] batchAffected = ps.executeBatch();
+			if (batchAffected.length != 3) throw new DbStateException();
+			for (int i = 0; i < batchAffected.length; i++) {
+				if (batchAffected[i] != 1) throw new DbStateException();
+			}
+			ps.close();
+			// Store the outgoing keys
+			sql = "INSERT INTO outgoingKeys (contactId, transportId, period,"
+					+ " tagKey, headerKey, stream)"
+					+ " VALUES (?, ?, ?, ?, ?, ?)";
+			ps = txn.prepareStatement(sql);
+			ps.setInt(1, c.getInt());
+			ps.setString(2, k.getTransportId().getString());
+			OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
+			ps.setLong(3, outCurr.getRotationPeriod());
+			ps.setBytes(4, outCurr.getTagKey().getBytes());
+			ps.setBytes(5, outCurr.getHeaderKey().getBytes());
+			ps.setLong(6, outCurr.getStreamCounter());
+			int affected = ps.executeUpdate();
+			if (affected != 1) throw new DbStateException();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public void addVisibility(Connection txn, ContactId c, GroupId g)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -1326,32 +1328,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<Endpoint> getEndpoints(Connection txn)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT contactId, transportId, epoch, alice"
-					+ " FROM endpoints";
-			ps = txn.prepareStatement(sql);
-			rs = ps.executeQuery();
-			List<Endpoint> endpoints = new ArrayList<Endpoint>();
-			while (rs.next()) {
-				ContactId contactId = new ContactId(rs.getInt(1));
-				TransportId transportId = new TransportId(rs.getString(2));
-				long epoch = rs.getLong(3);
-				boolean alice = rs.getBoolean(4);
-				endpoints.add(new Endpoint(contactId, transportId, epoch,
-						alice));
-			}
-			return Collections.unmodifiableList(endpoints);
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Group getGroup(Connection txn, GroupId g) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2098,43 +2074,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Collection<TemporarySecret> getSecrets(Connection txn)
-			throws DbException {
-		PreparedStatement ps = null;
-		ResultSet rs = null;
-		try {
-			String sql = "SELECT e.contactId, e.transportId, epoch, alice,"
-					+ " period, secret, outgoing, centre, bitmap"
-					+ " FROM endpoints AS e"
-					+ " JOIN secrets AS s"
-					+ " ON e.contactId = s.contactId"
-					+ " AND e.transportId = s.transportId";
-			ps = txn.prepareStatement(sql);
-			rs = ps.executeQuery();
-			List<TemporarySecret> secrets = new ArrayList<TemporarySecret>();
-			while (rs.next()) {
-				ContactId contactId = new ContactId(rs.getInt(1));
-				TransportId transportId = new TransportId(rs.getString(2));
-				long epoch = rs.getLong(3);
-				boolean alice = rs.getBoolean(4);
-				long period = rs.getLong(5);
-				byte[] secret = rs.getBytes(6);
-				long outgoing = rs.getLong(7);
-				long centre = rs.getLong(8);
-				byte[] bitmap = rs.getBytes(9);
-				secrets.add(new TemporarySecret(contactId, transportId, epoch,
-						alice, period, secret, outgoing, centre, bitmap));
-			}
-			rs.close();
-			ps.close();
-			return Collections.unmodifiableList(secrets);
-		} catch (SQLException e) {
-			tryToClose(rs);
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public Settings getSettings(Connection txn) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2317,6 +2256,67 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
+	public Map<ContactId, TransportKeys> getTransportKeys(Connection txn,
+			TransportId t) throws DbException {
+		PreparedStatement ps = null;
+		ResultSet rs = null;
+		try {
+			// Retrieve the incoming keys
+			String sql = "SELECT period, tagKey, headerKey, base, bitmap"
+					+ " FROM incomingKeys"
+					+ " WHERE transportId = ?"
+					+ " ORDER BY contactId, period";
+			ps = txn.prepareStatement(sql);
+			ps.setString(1, t.getString());
+			rs = ps.executeQuery();
+			List<IncomingKeys> inKeys = new ArrayList<IncomingKeys>();
+			while (rs.next()) {
+				long rotationPeriod = rs.getLong(1);
+				SecretKey tagKey = new SecretKey(rs.getBytes(2));
+				SecretKey headerKey = new SecretKey(rs.getBytes(3));
+				long windowBase = rs.getLong(4);
+				byte[] windowBitmap = rs.getBytes(5);
+				inKeys.add(new IncomingKeys(tagKey, headerKey, rotationPeriod,
+						windowBase, windowBitmap));
+			}
+			rs.close();
+			ps.close();
+			// Retrieve the outgoing keys in the same order
+			sql = "SELECT contactId, period, tagKey, headerKey, stream"
+					+ " FROM outgoingKeys"
+					+ " WHERE transportId = ?"
+					+ " ORDER BY contactId, period";
+			ps = txn.prepareStatement(sql);
+			ps.setString(1, t.getString());
+			rs = ps.executeQuery();
+			Map<ContactId, TransportKeys> keys =
+					new HashMap<ContactId, TransportKeys>();
+			for (int i = 0; rs.next(); i++) {
+				// There should be three times as many incoming keys
+				if (inKeys.size() < (i + 1) * 3) throw new DbStateException();
+				ContactId contactId = new ContactId(rs.getInt(1));
+				long rotationPeriod = rs.getLong(2);
+				SecretKey tagKey = new SecretKey(rs.getBytes(3));
+				SecretKey headerKey = new SecretKey(rs.getBytes(4));
+				long streamCounter = rs.getLong(5);
+				OutgoingKeys outCurr = new OutgoingKeys(tagKey, headerKey,
+						rotationPeriod, streamCounter);
+				IncomingKeys inPrev = inKeys.get(i * 3);
+				IncomingKeys inCurr = inKeys.get(i * 3 + 1);
+				IncomingKeys inNext = inKeys.get(i * 3 + 2);
+				keys.put(contactId, new TransportKeys(t, inPrev, inCurr,
+						inNext, outCurr));
+			}
+			rs.close();
+			ps.close();
+			return Collections.unmodifiableMap(keys);
+		} catch (SQLException e) {
+			tryToClose(rs);
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+	}
+
 	public Map<TransportId, Integer> getTransportLatencies(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
@@ -2327,7 +2327,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 			rs = ps.executeQuery();
 			Map<TransportId, Integer> latencies =
 					new HashMap<TransportId, Integer>();
-			while (rs.next()){
+			while (rs.next()) {
 				TransportId id = new TransportId(rs.getString(1));
 				latencies.put(id, rs.getInt(2));
 			}
@@ -2392,7 +2392,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 				ps.setString(3, u.getId().getString());
 				ps.addBatch();
 			}
-			int [] batchAffected = ps.executeBatch();
+			int[] batchAffected = ps.executeBatch();
 			if (batchAffected.length != updates.size())
 				throw new DbStateException();
 			for (i = 0; i < batchAffected.length; i++) {
@@ -2455,42 +2455,21 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public long incrementStreamCounter(Connection txn, ContactId c,
-			TransportId t, long period) throws DbException {
+	public void incrementStreamCounter(Connection txn, ContactId c,
+			TransportId t, long rotationPeriod) throws DbException {
 		PreparedStatement ps = null;
-		ResultSet rs = null;
 		try {
-			// Get the current stream counter
-			String sql = "SELECT outgoing FROM secrets"
-					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setInt(1, c.getInt());
-			ps.setString(2, t.getString());
-			ps.setLong(3, period);
-			rs = ps.executeQuery();
-			if (!rs.next()) {
-				rs.close();
-				ps.close();
-				return -1;
-			}
-			long streamNumber = rs.getLong(1);
-			if (rs.next()) throw new DbStateException();
-			rs.close();
-			ps.close();
-			// Increment the stream counter
-			sql = "UPDATE secrets SET outgoing = outgoing + 1"
+			String sql = "UPDATE outgoingKeys SET stream = stream + 1"
 					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
 			ps = txn.prepareStatement(sql);
 			ps.setInt(1, c.getInt());
 			ps.setString(2, t.getString());
-			ps.setLong(3, period);
+			ps.setLong(3, rotationPeriod);
 			int affected = ps.executeUpdate();
 			if (affected != 1) throw new DbStateException();
 			ps.close();
-			return streamNumber;
 		} catch (SQLException e) {
 			tryToClose(ps);
-			tryToClose(rs);
 			throw new DbException(e);
 		}
 	}
@@ -2929,18 +2908,19 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throw new DbException(e);
 		}
 	}
+
 	public void setReorderingWindow(Connection txn, ContactId c, TransportId t,
-			long period, long centre, byte[] bitmap) throws DbException {
+			long rotationPeriod, long base, byte[] bitmap) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "UPDATE secrets SET centre = ?, bitmap = ?"
+			String sql = "UPDATE incomingKeys SET base = ?, bitmap = ?"
 					+ " WHERE contactId = ? AND transportId = ? AND period = ?";
 			ps = txn.prepareStatement(sql);
-			ps.setLong(1, centre);
+			ps.setLong(1, base);
 			ps.setBytes(2, bitmap);
 			ps.setInt(3, c.getInt());
 			ps.setString(4, t.getString());
-			ps.setLong(5, period);
+			ps.setLong(5, rotationPeriod);
 			int affected = ps.executeUpdate();
 			if (affected < 0 || affected > 1) throw new DbStateException();
 			ps.close();
@@ -3139,7 +3119,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 
 	public boolean setRemoteProperties(Connection txn, ContactId c,
 			TransportId t, TransportProperties p, long version)
-					throws DbException {
+			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
@@ -3354,4 +3334,46 @@ abstract class JdbcDatabase implements Database<Connection> {
 			throw new DbException(e);
 		}
 	}
+
+	public void updateTransportKeys(Connection txn,
+			Map<ContactId, TransportKeys> keys) throws DbException {
+		PreparedStatement ps = null;
+		try {
+			// Delete any existing incoming keys
+			String sql = "DELETE FROM incomingKeys"
+					+ " WHERE contactId = ?"
+					+ " AND transportId = ?";
+			ps = txn.prepareStatement(sql);
+			for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
+				ps.setInt(1, e.getKey().getInt());
+				ps.setString(2, e.getValue().getTransportId().getString());
+				ps.addBatch();
+			}
+			int[] batchAffected = ps.executeBatch();
+			if (batchAffected.length != keys.size())
+				throw new DbStateException();
+			ps.close();
+			// Delete any existing outgoing keys
+			sql = "DELETE FROM outgoingKeys"
+					+ " WHERE contactId = ?"
+					+ " AND transportId = ?";
+			ps = txn.prepareStatement(sql);
+			for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
+				ps.setInt(1, e.getKey().getInt());
+				ps.setString(2, e.getValue().getTransportId().getString());
+				ps.addBatch();
+			}
+			batchAffected = ps.executeBatch();
+			if (batchAffected.length != keys.size())
+				throw new DbStateException();
+			ps.close();
+		} catch (SQLException e) {
+			tryToClose(ps);
+			throw new DbException(e);
+		}
+		// Store the new keys
+		for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
+			addTransportKeys(txn, e.getKey(), e.getValue());
+		}
+	}
 }
diff --git a/briar-core/src/org/briarproject/invitation/AliceConnector.java b/briar-core/src/org/briarproject/invitation/AliceConnector.java
index 1e4e533b7ba0153bcb986b4d9194c03d259ae570..f37357385c74f2c90967c0ef34bd50f28bb828ea 100644
--- a/briar-core/src/org/briarproject/invitation/AliceConnector.java
+++ b/briar-core/src/org/briarproject/invitation/AliceConnector.java
@@ -1,23 +1,13 @@
 package org.briarproject.invitation;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.GeneralSecurityException;
-import java.util.Map;
-import java.util.logging.Logger;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.data.Reader;
 import org.briarproject.api.data.ReaderFactory;
 import org.briarproject.api.data.Writer;
@@ -29,9 +19,20 @@ import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 import org.briarproject.api.system.Clock;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
 /** A connection thread for the peer being Alice in the invitation protocol. */
 class AliceConnector extends Connector {
 
@@ -49,9 +50,9 @@ class AliceConnector extends Connector {
 			Map<TransportId, TransportProperties> localProps,
 			PseudoRandom random) {
 		super(crypto, db, readerFactory, writerFactory, streamReaderFactory,
-				streamWriterFactory, authorFactory, groupFactory, keyManager,
-				connectionManager, clock, reuseConnection, group, plugin,
-				localAuthor, localProps, random);
+				streamWriterFactory, authorFactory, groupFactory,
+				keyManager, connectionManager, clock, reuseConnection, group,
+				plugin, localAuthor, localProps, random);
 	}
 
 	@Override
@@ -71,7 +72,7 @@ class AliceConnector extends Connector {
 		OutputStream out;
 		Reader r;
 		Writer w;
-		byte[] secret;
+		SecretKey master;
 		try {
 			in = conn.getReader().getInputStream();
 			out = conn.getWriter().getOutputStream();
@@ -82,7 +83,7 @@ class AliceConnector extends Connector {
 			byte[] hash = receivePublicKeyHash(r);
 			sendPublicKey(w);
 			byte[] key = receivePublicKey(r);
-			secret = deriveMasterSecret(hash, key, true);
+			master = deriveMasterSecret(hash, key, true);
 		} catch (IOException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			group.keyAgreementFailed();
@@ -96,8 +97,8 @@ class AliceConnector extends Connector {
 		}
 		// The key agreement succeeded - derive the confirmation codes
 		if (LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
-		int[] codes = crypto.deriveConfirmationCodes(secret);
-		int aliceCode = codes[0], bobCode = codes[1];
+		int aliceCode = crypto.deriveConfirmationCode(master, true);
+		int bobCode = crypto.deriveConfirmationCode(master, false);
 		group.keyAgreementSucceeded(aliceCode, bobCode);
 		// Exchange confirmation results
 		boolean localMatched, remoteMatched;
@@ -130,19 +131,22 @@ class AliceConnector extends Connector {
 		// Confirmation succeeded - upgrade to a secure connection
 		if (LOG.isLoggable(INFO))
 			LOG.info(pluginName + " confirmation succeeded");
+		// Derive the header keys
+		SecretKey aliceHeaderKey = crypto.deriveInvitationKey(master, true);
+		SecretKey bobHeaderKey = crypto.deriveInvitationKey(master, false);
 		// Create the readers
 		InputStream streamReader =
 				streamReaderFactory.createInvitationStreamReader(in,
-						secret, false); // Bob's stream
+						bobHeaderKey);
 		r = readerFactory.createReader(streamReader);
 		// Create the writers
 		OutputStream streamWriter =
 				streamWriterFactory.createInvitationStreamWriter(out,
-						secret, true); // Alice's stream
+						aliceHeaderKey);
 		w = writerFactory.createWriter(streamWriter);
 		// Derive the invitation nonces
-		byte[][] nonces = crypto.deriveInvitationNonces(secret);
-		byte[] aliceNonce = nonces[0], bobNonce = nonces[1];
+		byte[] aliceNonce = crypto.deriveSignatureNonce(master, true);
+		byte[] bobNonce = crypto.deriveSignatureNonce(master, false);
 		// Exchange pseudonyms, signed nonces, timestamps and transports
 		Author remoteAuthor;
 		long remoteTimestamp;
@@ -171,11 +175,11 @@ class AliceConnector extends Connector {
 			tryToClose(conn, true);
 			return;
 		}
-		// The epoch is the minimum of the peers' timestamps
-		long epoch = Math.min(localTimestamp, remoteTimestamp);
+		// The agreed timestamp is the minimum of the peers' timestamps
+		long timestamp = Math.min(localTimestamp, remoteTimestamp);
 		// Add the contact and store the transports
 		try {
-			addContact(remoteAuthor, remoteProps, secret, epoch, true);
+			addContact(remoteAuthor, remoteProps, master, timestamp, true);
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			tryToClose(conn, true);
@@ -190,4 +194,4 @@ class AliceConnector extends Connector {
 			LOG.info(pluginName + " pseudonym exchange succeeded");
 		group.pseudonymExchangeSucceeded(remoteAuthor);
 	}
-}
\ No newline at end of file
+}
diff --git a/briar-core/src/org/briarproject/invitation/BobConnector.java b/briar-core/src/org/briarproject/invitation/BobConnector.java
index 278e3338d7d1c7ff63fddfa7da0a89d8fcd7e016..845ee0df7ba724d8fa81191ee06baf2f0d232936 100644
--- a/briar-core/src/org/briarproject/invitation/BobConnector.java
+++ b/briar-core/src/org/briarproject/invitation/BobConnector.java
@@ -1,23 +1,13 @@
 package org.briarproject.invitation;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.security.GeneralSecurityException;
-import java.util.Map;
-import java.util.logging.Logger;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.data.Reader;
 import org.briarproject.api.data.ReaderFactory;
 import org.briarproject.api.data.Writer;
@@ -29,9 +19,20 @@ import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 import org.briarproject.api.system.Clock;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.GeneralSecurityException;
+import java.util.Map;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
 /** A connection thread for the peer being Bob in the invitation protocol. */
 class BobConnector extends Connector {
 
@@ -49,9 +50,9 @@ class BobConnector extends Connector {
 			Map<TransportId, TransportProperties> localProps,
 			PseudoRandom random) {
 		super(crypto, db, readerFactory, writerFactory, streamReaderFactory,
-				streamWriterFactory, authorFactory, groupFactory, keyManager,
-				connectionManager, clock, reuseConnection, group, plugin,
-				localAuthor, localProps, random);
+				streamWriterFactory, authorFactory, groupFactory,
+				keyManager, connectionManager, clock, reuseConnection, group,
+				plugin, localAuthor, localProps, random);
 	}
 
 	@Override
@@ -65,7 +66,7 @@ class BobConnector extends Connector {
 		OutputStream out;
 		Reader r;
 		Writer w;
-		byte[] secret;
+		SecretKey master;
 		try {
 			in = conn.getReader().getInputStream();
 			out = conn.getWriter().getOutputStream();
@@ -82,7 +83,7 @@ class BobConnector extends Connector {
 			sendPublicKeyHash(w);
 			byte[] key = receivePublicKey(r);
 			sendPublicKey(w);
-			secret = deriveMasterSecret(hash, key, false);
+			master = deriveMasterSecret(hash, key, false);
 		} catch (IOException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			group.keyAgreementFailed();
@@ -96,8 +97,8 @@ class BobConnector extends Connector {
 		}
 		// The key agreement succeeded - derive the confirmation codes
 		if (LOG.isLoggable(INFO)) LOG.info(pluginName + " agreement succeeded");
-		int[] codes = crypto.deriveConfirmationCodes(secret);
-		int aliceCode = codes[0], bobCode = codes[1];
+		int aliceCode = crypto.deriveConfirmationCode(master, true);
+		int bobCode = crypto.deriveConfirmationCode(master, false);
 		group.keyAgreementSucceeded(bobCode, aliceCode);
 		// Exchange confirmation results
 		boolean localMatched, remoteMatched;
@@ -130,19 +131,22 @@ class BobConnector extends Connector {
 		// Confirmation succeeded - upgrade to a secure connection
 		if (LOG.isLoggable(INFO))
 			LOG.info(pluginName + " confirmation succeeded");
+		// Derive the header keys
+		SecretKey aliceHeaderKey = crypto.deriveInvitationKey(master, true);
+		SecretKey bobHeaderKey = crypto.deriveInvitationKey(master, false);
 		// Create the readers
 		InputStream streamReader =
 				streamReaderFactory.createInvitationStreamReader(in,
-						secret, true); // Alice's stream
+						aliceHeaderKey);
 		r = readerFactory.createReader(streamReader);
 		// Create the writers
 		OutputStream streamWriter =
 				streamWriterFactory.createInvitationStreamWriter(out,
-						secret, false); // Bob's stream
+						bobHeaderKey);
 		w = writerFactory.createWriter(streamWriter);
 		// Derive the nonces
-		byte[][] nonces = crypto.deriveInvitationNonces(secret);
-		byte[] aliceNonce = nonces[0], bobNonce = nonces[1];
+		byte[] aliceNonce = crypto.deriveSignatureNonce(master, true);
+		byte[] bobNonce = crypto.deriveSignatureNonce(master, false);
 		// Exchange pseudonyms, signed nonces, timestamps and transports
 		Author remoteAuthor;
 		long remoteTimestamp;
@@ -171,11 +175,11 @@ class BobConnector extends Connector {
 			tryToClose(conn, true);
 			return;
 		}
-		// The epoch is the minimum of the peers' timestamps
-		long epoch = Math.min(localTimestamp, remoteTimestamp);
+		// The agreed timestamp is the minimum of the peers' timestamps
+		long timestamp = Math.min(localTimestamp, remoteTimestamp);
 		// Add the contact and store the transports
 		try {
-			addContact(remoteAuthor, remoteProps, secret, epoch, false);
+			addContact(remoteAuthor, remoteProps, master, timestamp, false);
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			tryToClose(conn, true);
diff --git a/briar-core/src/org/briarproject/invitation/Connector.java b/briar-core/src/org/briarproject/invitation/Connector.java
index 40517c3a42377eb61e094429c26e3da0f7a213cc..d8c4e674fcc8df39a1eda267923f729161bb3491 100644
--- a/briar-core/src/org/briarproject/invitation/Connector.java
+++ b/briar-core/src/org/briarproject/invitation/Connector.java
@@ -1,27 +1,5 @@
 package org.briarproject.invitation;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
-import static org.briarproject.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
-import static org.briarproject.api.AuthorConstants.MAX_SIGNATURE_LENGTH;
-import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
-import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
-import static org.briarproject.api.TransportPropertyConstants.MAX_TRANSPORT_ID_LENGTH;
-import static org.briarproject.api.invitation.InvitationConstants.CONNECTION_TIMEOUT;
-
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.logging.Logger;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.ContactId;
@@ -30,11 +8,11 @@ import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.crypto.KeyPair;
 import org.briarproject.api.crypto.KeyParser;
 import org.briarproject.api.crypto.MessageDigest;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.data.Reader;
 import org.briarproject.api.data.ReaderFactory;
@@ -42,17 +20,39 @@ import org.briarproject.api.data.Writer;
 import org.briarproject.api.data.WriterFactory;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.NoSuchTransportException;
 import org.briarproject.api.messaging.Group;
 import org.briarproject.api.messaging.GroupFactory;
 import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 import org.briarproject.api.system.Clock;
-import org.briarproject.api.transport.Endpoint;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
+import org.briarproject.api.transport.TransportKeys;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.logging.Logger;
 
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+import static org.briarproject.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
+import static org.briarproject.api.AuthorConstants.MAX_SIGNATURE_LENGTH;
+import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
+import static org.briarproject.api.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
+import static org.briarproject.api.TransportPropertyConstants.MAX_TRANSPORT_ID_LENGTH;
+import static org.briarproject.api.invitation.InvitationConstants.CONNECTION_TIMEOUT;
+import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
+
+// FIXME: This class has way too many dependencies
 abstract class Connector extends Thread {
 
 	private static final Logger LOG =
@@ -152,8 +152,8 @@ abstract class Connector extends Thread {
 		return b;
 	}
 
-	protected byte[] deriveMasterSecret(byte[] hash, byte[] key, boolean alice)
-			throws GeneralSecurityException {
+	protected SecretKey deriveMasterSecret(byte[] hash, byte[] key,
+			boolean alice) throws GeneralSecurityException {
 		// Check that the hash matches the key
 		if (!Arrays.equals(hash, messageDigest.digest(key))) {
 			if (LOG.isLoggable(INFO))
@@ -271,39 +271,34 @@ abstract class Connector extends Thread {
 	}
 
 	protected void addContact(Author remoteAuthor,
-			Map<TransportId, TransportProperties> remoteProps,  byte[] secret,
-			long epoch, boolean alice) throws DbException {
+			Map<TransportId, TransportProperties> remoteProps, SecretKey master,
+			long timestamp, boolean alice) throws DbException {
 		// Add the contact to the database
 		contactId = db.addContact(remoteAuthor, localAuthor.getId());
 		// Create and store the inbox group
-		byte[] salt = crypto.deriveGroupSalt(secret);
+		byte[] salt = crypto.deriveGroupSalt(master);
 		Group inbox = groupFactory.createGroup("Inbox", salt);
 		db.addGroup(inbox);
 		db.setInboxGroup(contactId, inbox);
 		// Store the remote transport properties
 		db.setRemoteProperties(contactId, remoteProps);
-		// Create an endpoint for each transport shared with the contact
-		List<TransportId> ids = new ArrayList<TransportId>();
+		// Derive transport keys for each transport shared with the contact
 		Map<TransportId, Integer> latencies = db.getTransportLatencies();
-		for (TransportId id : localProps.keySet()) {
-			if (latencies.containsKey(id) && remoteProps.containsKey(id))
-				ids.add(id);
-		}
-		// Assign indices to the transports deterministically and derive keys
-		Collections.sort(ids, TransportIdComparator.INSTANCE);
-		int size = ids.size();
-		for (int i = 0; i < size; i++) {
-			TransportId id = ids.get(i);
-			Endpoint ep = new Endpoint(contactId, id, epoch, alice);
-			int maxLatency = latencies.get(id);
-			try {
-				db.addEndpoint(ep);
-			} catch (NoSuchTransportException e) {
-				continue;
+		List<TransportKeys> keys = new ArrayList<TransportKeys>();
+		for (TransportId t : localProps.keySet()) {
+			if (remoteProps.containsKey(t) && latencies.containsKey(t)) {
+				// Work out what rotation period the timestamp belongs to
+				long latency = latencies.get(t);
+				long rotationPeriodLength = latency + MAX_CLOCK_DIFFERENCE;
+				long rotationPeriod = timestamp / rotationPeriodLength;
+				// Derive the transport keys
+				TransportKeys k = crypto.deriveTransportKeys(t, master,
+						rotationPeriod, alice);
+				db.addTransportKeys(contactId, k);
+				keys.add(k);
 			}
-			byte[] initialSecret = crypto.deriveInitialSecret(secret, i);
-			keyManager.endpointAdded(ep, maxLatency, initialSecret);
 		}
+		keyManager.contactAdded(contactId, keys);
 	}
 
 	protected void tryToClose(DuplexTransportConnection conn,
@@ -322,16 +317,4 @@ abstract class Connector extends Thread {
 		TransportId t = plugin.getId();
 		connectionManager.manageOutgoingConnection(contactId, t, conn);
 	}
-
-	private static class TransportIdComparator
-	implements Comparator<TransportId> {
-
-		private static final TransportIdComparator INSTANCE =
-				new TransportIdComparator();
-
-		public int compare(TransportId t1, TransportId t2) {
-			String s1 = t1.getString(), s2 = t2.getString();
-			return String.CASE_INSENSITIVE_ORDER.compare(s1, s2);
-		}
-	}
 }
diff --git a/briar-core/src/org/briarproject/invitation/ConnectorGroup.java b/briar-core/src/org/briarproject/invitation/ConnectorGroup.java
index 24439b181cbf4f2b8ebb3fff9a4163a9fc928fab..5861c07b005ced62980f51ec048125c95d534418 100644
--- a/briar-core/src/org/briarproject/invitation/ConnectorGroup.java
+++ b/briar-core/src/org/briarproject/invitation/ConnectorGroup.java
@@ -1,19 +1,5 @@
 package org.briarproject.invitation;
 
-import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.invitation.InvitationConstants.CONFIRMATION_TIMEOUT;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Map;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.logging.Logger;
-
 import org.briarproject.api.Author;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.AuthorId;
@@ -21,7 +7,6 @@ import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.crypto.PseudoRandom;
 import org.briarproject.api.data.ReaderFactory;
 import org.briarproject.api.data.WriterFactory;
@@ -35,9 +20,24 @@ import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.PluginManager;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.system.Clock;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Logger;
+
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.invitation.InvitationConstants.CONFIRMATION_TIMEOUT;
+
 /** A task consisting of one or more parallel connection attempts. */
 class ConnectorGroup extends Thread implements InvitationTask {
 
diff --git a/briar-core/src/org/briarproject/invitation/InvitationTaskFactoryImpl.java b/briar-core/src/org/briarproject/invitation/InvitationTaskFactoryImpl.java
index 6f460e8f7291409eac077b6dc7a9e4bf10698d3c..97c65c0985a67b96a28a8753dab46717b87555dc 100644
--- a/briar-core/src/org/briarproject/invitation/InvitationTaskFactoryImpl.java
+++ b/briar-core/src/org/briarproject/invitation/InvitationTaskFactoryImpl.java
@@ -5,7 +5,6 @@ import javax.inject.Inject;
 import org.briarproject.api.AuthorFactory;
 import org.briarproject.api.AuthorId;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.data.ReaderFactory;
 import org.briarproject.api.data.WriterFactory;
 import org.briarproject.api.db.DatabaseComponent;
@@ -15,6 +14,7 @@ import org.briarproject.api.messaging.GroupFactory;
 import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.PluginManager;
 import org.briarproject.api.system.Clock;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
 
diff --git a/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
index 72314e45bbd68754c63cc0b9f72b862ece86f947..5b1e32be1031afb730aa4be533898df0e816063f 100644
--- a/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
@@ -14,7 +14,6 @@ import javax.inject.Inject;
 
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.messaging.MessagingSession;
@@ -24,10 +23,10 @@ import org.briarproject.api.plugins.ConnectionRegistry;
 import org.briarproject.api.plugins.TransportConnectionReader;
 import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
-import org.briarproject.api.transport.TagRecogniser;
 
 class ConnectionManagerImpl implements ConnectionManager {
 
@@ -36,7 +35,6 @@ class ConnectionManagerImpl implements ConnectionManager {
 
 	private final Executor ioExecutor;
 	private final KeyManager keyManager;
-	private final TagRecogniser tagRecogniser;
 	private final StreamReaderFactory streamReaderFactory;
 	private final StreamWriterFactory streamWriterFactory;
 	private final MessagingSessionFactory messagingSessionFactory;
@@ -44,14 +42,12 @@ class ConnectionManagerImpl implements ConnectionManager {
 
 	@Inject
 	ConnectionManagerImpl(@IoExecutor Executor ioExecutor,
-			KeyManager keyManager, TagRecogniser tagRecogniser,
-			StreamReaderFactory streamReaderFactory,
+			KeyManager keyManager, StreamReaderFactory streamReaderFactory,
 			StreamWriterFactory streamWriterFactory,
 			MessagingSessionFactory messagingSessionFactory,
 			ConnectionRegistry connectionRegistry) {
 		this.ioExecutor = ioExecutor;
 		this.keyManager = keyManager;
-		this.tagRecogniser = tagRecogniser;
 		this.streamReaderFactory = streamReaderFactory;
 		this.streamWriterFactory = streamWriterFactory;
 		this.messagingSessionFactory = messagingSessionFactory;
@@ -134,7 +130,7 @@ class ConnectionManagerImpl implements ConnectionManager {
 			StreamContext ctx;
 			try {
 				byte[] tag = readTag(transportId, reader);
-				ctx = tagRecogniser.recogniseTag(transportId, tag);
+				ctx = keyManager.recogniseTag(transportId, tag);
 			} catch (IOException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				disposeReader(true, false);
@@ -238,7 +234,7 @@ class ConnectionManagerImpl implements ConnectionManager {
 			StreamContext ctx;
 			try {
 				byte[] tag = readTag(transportId, reader);
-				ctx = tagRecogniser.recogniseTag(transportId, tag);
+				ctx = keyManager.recogniseTag(transportId, tag);
 			} catch (IOException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				disposeReader(true, false);
@@ -367,7 +363,7 @@ class ConnectionManagerImpl implements ConnectionManager {
 			StreamContext ctx;
 			try {
 				byte[] tag = readTag(transportId, reader);
-				ctx = tagRecogniser.recogniseTag(transportId, tag);
+				ctx = keyManager.recogniseTag(transportId, tag);
 			} catch (IOException e) {
 				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				disposeReader(true, true);
@@ -420,4 +416,4 @@ class ConnectionManagerImpl implements ConnectionManager {
 			}
 		}
 	}
-}
\ No newline at end of file
+}
diff --git a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
index 05fe90842f181d7e27a3ad09f42e69cb662a0840..53448084821c3516bc3116bc377940754a87947f 100644
--- a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
@@ -1,22 +1,5 @@
 package org.briarproject.plugins;
 
-import static java.util.logging.Level.INFO;
-import static java.util.logging.Level.WARNING;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executor;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportConfig;
 import org.briarproject.api.TransportId;
@@ -42,6 +25,23 @@ import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.ui.UiCallback;
 
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Level.WARNING;
+
 class PluginManagerImpl implements PluginManager {
 
 	private static final Logger LOG =
@@ -367,7 +367,7 @@ class PluginManagerImpl implements PluginManager {
 	}
 
 	private class SimplexCallback extends PluginCallbackImpl
-	implements SimplexPluginCallback {
+			implements SimplexPluginCallback {
 
 		private SimplexCallback(TransportId id) {
 			super(id);
@@ -383,7 +383,7 @@ class PluginManagerImpl implements PluginManager {
 	}
 
 	private class DuplexCallback extends PluginCallbackImpl
-	implements DuplexPluginCallback {
+			implements DuplexPluginCallback {
 
 		private DuplexCallback(TransportId id) {
 			super(id);
diff --git a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
index 7b926cc4ffa225d23fa19a22bd0687a3dd0a868c..64dbe14e3fab37926c42753655b1f6ed09c8acbd 100644
--- a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
+++ b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
@@ -1,27 +1,10 @@
 package org.briarproject.transport;
 
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.TimerTask;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-import java.util.logging.Logger;
-
-import javax.inject.Inject;
-
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DatabaseExecutor;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
@@ -31,429 +14,115 @@ import org.briarproject.api.event.TransportAddedEvent;
 import org.briarproject.api.event.TransportRemovedEvent;
 import org.briarproject.api.system.Clock;
 import org.briarproject.api.system.Timer;
-import org.briarproject.api.transport.Endpoint;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.TagRecogniser;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.TransportKeys;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
 
-// FIXME: Don't make alien calls with a lock held
-class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
+import javax.inject.Inject;
+
+import static java.util.logging.Level.WARNING;
 
-	private static final int MS_BETWEEN_CHECKS = 60 * 1000;
+class KeyManagerImpl implements KeyManager, EventListener {
 
 	private static final Logger LOG =
 			Logger.getLogger(KeyManagerImpl.class.getName());
 
-	private final CryptoComponent crypto;
 	private final DatabaseComponent db;
+	private final CryptoComponent crypto;
+	private final Executor dbExecutor;
 	private final EventBus eventBus;
-	private final TagRecogniser tagRecogniser;
-	private final Clock clock;
 	private final Timer timer;
-	private final Lock lock = new ReentrantLock();
-
-	// The following are locking: lock
-	private final Map<TransportId, Integer> maxLatencies;
-	private final Map<EndpointKey, TemporarySecret> oldSecrets;
-	private final Map<EndpointKey, TemporarySecret> currentSecrets;
-	private final Map<EndpointKey, TemporarySecret> newSecrets;
+	private final Clock clock;
+	private final ConcurrentHashMap<TransportId, TransportKeyManager> managers;
 
 	@Inject
-	KeyManagerImpl(CryptoComponent crypto, DatabaseComponent db,
-			EventBus eventBus, TagRecogniser tagRecogniser, Clock clock,
-			Timer timer) {
-		this.crypto = crypto;
+	KeyManagerImpl(DatabaseComponent db, CryptoComponent crypto,
+			@DatabaseExecutor Executor dbExecutor, EventBus eventBus,
+			Timer timer, Clock clock) {
 		this.db = db;
+		this.crypto = crypto;
+		this.dbExecutor = dbExecutor;
 		this.eventBus = eventBus;
-		this.tagRecogniser = tagRecogniser;
-		this.clock = clock;
 		this.timer = timer;
-		maxLatencies = new HashMap<TransportId, Integer>();
-		oldSecrets = new HashMap<EndpointKey, TemporarySecret>();
-		currentSecrets = new HashMap<EndpointKey, TemporarySecret>();
-		newSecrets = new HashMap<EndpointKey, TemporarySecret>();
+		this.clock = clock;
+		managers = new ConcurrentHashMap<TransportId, TransportKeyManager>();
 	}
 
 	public boolean start() {
-		lock.lock();
+		eventBus.addListener(this);
 		try {
-			eventBus.addListener(this);
-			// Load the temporary secrets and transport latencies from the DB
-			Collection<TemporarySecret> secrets;
-			try {
-				secrets = db.getSecrets();
-				maxLatencies.putAll(db.getTransportLatencies());
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				return false;
-			}
-			// Work out what phase of its lifecycle each secret is in
-			long now = clock.currentTimeMillis();
-			Collection<TemporarySecret> dead =
-					assignSecretsToMaps(now, secrets);
-			// Replace any dead secrets
-			Collection<TemporarySecret> created = replaceDeadSecrets(now, dead);
-			if (!created.isEmpty()) {
-				// Store any secrets that have been created,
-				// removing any dead ones
-				try {
-					db.addSecrets(created);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING))
-						LOG.log(WARNING, e.toString(), e);
-					return false;
-				}
-			}
-			// Pass the old, current and new secrets to the recogniser
-			for (TemporarySecret s : oldSecrets.values())
-				tagRecogniser.addSecret(s);
-			for (TemporarySecret s : currentSecrets.values())
-				tagRecogniser.addSecret(s);
-			for (TemporarySecret s : newSecrets.values())
-				tagRecogniser.addSecret(s);
-			// Schedule periodic key rotation
-			timer.scheduleAtFixedRate(this, MS_BETWEEN_CHECKS,
-					MS_BETWEEN_CHECKS);
-			return true;
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	// Assigns secrets to the appropriate maps and returns any dead secrets
-	// Locking: lock
-	private Collection<TemporarySecret> assignSecretsToMaps(long now,
-			Collection<TemporarySecret> secrets) {
-		Collection<TemporarySecret> dead = new ArrayList<TemporarySecret>();
-		for (TemporarySecret s : secrets) {
-			// Discard the secret if the transport has been removed
-			Integer maxLatency = maxLatencies.get(s.getTransportId());
-			if (maxLatency == null) {
-				LOG.info("Discarding obsolete secret");
-				continue;
-			}
-			long rotation = maxLatency + MAX_CLOCK_DIFFERENCE;
-			long creationTime = s.getEpoch() + rotation * (s.getPeriod() - 2);
-			long activationTime = creationTime + rotation;
-			long deactivationTime = activationTime + rotation;
-			long destructionTime = deactivationTime + rotation;
-			if (now >= destructionTime) {
-				dead.add(s);
-			} else if (now >= deactivationTime) {
-				oldSecrets.put(new EndpointKey(s), s);
-			} else if (now >= activationTime) {
-				currentSecrets.put(new EndpointKey(s), s);
-			} else if (now >= creationTime) {
-				newSecrets.put(new EndpointKey(s), s);
-			} else {
-				// FIXME: Work out what to do here
-				throw new Error("Clock has moved backwards");
-			}
-		}
-		return dead;
-	}
-
-	// Replaces the given secrets and returns any secrets created
-	// Locking: lock
-	private Collection<TemporarySecret> replaceDeadSecrets(long now,
-			Collection<TemporarySecret> dead) {
-		// If there are several dead secrets for an endpoint, use the newest
-		Map<EndpointKey, TemporarySecret> newest =
-				new HashMap<EndpointKey, TemporarySecret>();
-		for (TemporarySecret s : dead) {
-			EndpointKey k = new EndpointKey(s);
-			TemporarySecret exists = newest.get(k);
-			if (exists == null) {
-				// There's no other secret for this endpoint
-				newest.put(k, s);
-			} else if (exists.getPeriod() < s.getPeriod()) {
-				// There's an older secret - use this one instead
-				newest.put(k, s);
-			} else {
-				// There's a newer secret - keep using it
-			}
-		}
-		Collection<TemporarySecret> created = new ArrayList<TemporarySecret>();
-		for (Entry<EndpointKey, TemporarySecret> e : newest.entrySet()) {
-			TemporarySecret s = e.getValue();
-			Integer maxLatency = maxLatencies.get(s.getTransportId());
-			if (maxLatency == null) throw new IllegalStateException();
-			// Work out which rotation period we're in
-			long elapsed = now - s.getEpoch();
-			long rotation = maxLatency + MAX_CLOCK_DIFFERENCE;
-			long period = (elapsed / rotation) + 1;
-			if (period < 1) throw new IllegalStateException();
-			if (period - s.getPeriod() < 2)
-				throw new IllegalStateException();
-			// Derive the old, current and new secrets
-			byte[] b1 = s.getSecret();
-			for (long p = s.getPeriod() + 1; p < period; p++)
-				b1 = crypto.deriveNextSecret(b1, p);
-			byte[] b2 = crypto.deriveNextSecret(b1, period);
-			byte[] b3 = crypto.deriveNextSecret(b2, period + 1);
-			// Add the secrets to their respective maps if not already present
-			EndpointKey k = e.getKey();
-			if (!oldSecrets.containsKey(k)) {
-				TemporarySecret s1 = new TemporarySecret(s, period - 1, b1);
-				oldSecrets.put(k, s1);
-				created.add(s1);
-			}
-			if (!currentSecrets.containsKey(k)) {
-				TemporarySecret s2 = new TemporarySecret(s, period, b2);
-				currentSecrets.put(k, s2);
-				created.add(s2);
-			}
-			if (!newSecrets.containsKey(k)) {
-				TemporarySecret s3 = new TemporarySecret(s, period + 1, b3);
-				newSecrets.put(k, s3);
-				created.add(s3);
-			}
+			Map<TransportId, Integer> latencies = db.getTransportLatencies();
+			for (Entry<TransportId, Integer> e : latencies.entrySet())
+				addTransport(e.getKey(), e.getValue());
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return false;
 		}
-		return created;
+		return true;
 	}
 
 	public boolean stop() {
-		lock.lock();
-		try {
-			eventBus.removeListener(this);
-			timer.cancel();
-			tagRecogniser.removeSecrets();
-			maxLatencies.clear();
-			oldSecrets.clear();
-			currentSecrets.clear();
-			newSecrets.clear();
-			return true;
-		} finally {
-			lock.unlock();
-		}
+		eventBus.removeListener(this);
+		return true;
 	}
 
-	public StreamContext getStreamContext(ContactId c,
-			TransportId t) {
-		lock.lock();
-		try {
-			TemporarySecret s = currentSecrets.get(new EndpointKey(c, t));
-			if (s == null) {
-				LOG.info("No secret for endpoint");
-				return null;
-			}
-			long streamNumber;
-			try {
-				streamNumber = db.incrementStreamCounter(c, t, s.getPeriod());
-				if (streamNumber == -1) {
-					LOG.info("No counter for period");
-					return null;
-				}
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				return null;
-			}
-			byte[] secret = s.getSecret();
-			return new StreamContext(c, t, secret, streamNumber, s.getAlice());
-		} finally {
-			lock.unlock();
+	public void contactAdded(ContactId c, Collection<TransportKeys> keys) {
+		for (TransportKeys k : keys) {
+			TransportKeyManager m = managers.get(k.getTransportId());
+			if (m != null) m.addContact(c, k);
 		}
 	}
 
-	public void endpointAdded(Endpoint ep, int maxLatency,
-			byte[] initialSecret) {
-		lock.lock();
-		try {
-			maxLatencies.put(ep.getTransportId(), maxLatency);
-			// Work out which rotation period we're in
-			long elapsed = clock.currentTimeMillis() - ep.getEpoch();
-			long rotation = maxLatency + MAX_CLOCK_DIFFERENCE;
-			long period = (elapsed / rotation) + 1;
-			if (period < 1) throw new IllegalStateException();
-			// Derive the old, current and new secrets
-			byte[] b1 = initialSecret;
-			for (long p = 0; p < period; p++)
-				b1 = crypto.deriveNextSecret(b1, p);
-			byte[] b2 = crypto.deriveNextSecret(b1, period);
-			byte[] b3 = crypto.deriveNextSecret(b2, period + 1);
-			TemporarySecret s1 = new TemporarySecret(ep, period - 1, b1);
-			TemporarySecret s2 = new TemporarySecret(ep, period, b2);
-			TemporarySecret s3 = new TemporarySecret(ep, period + 1, b3);
-			// Add the incoming secrets to their respective maps
-			EndpointKey k = new EndpointKey(ep);
-			oldSecrets.put(k, s1);
-			currentSecrets.put(k, s2);
-			newSecrets.put(k, s3);
-			// Store the new secrets
-			try {
-				db.addSecrets(Arrays.asList(s1, s2, s3));
-			} catch (DbException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				return;
-			}
-			// Pass the new secrets to the recogniser
-			tagRecogniser.addSecret(s1);
-			tagRecogniser.addSecret(s2);
-			tagRecogniser.addSecret(s3);
-		} finally {
-			lock.unlock();
-		}
+	public StreamContext getStreamContext(ContactId c, TransportId t) {
+		TransportKeyManager m = managers.get(t);
+		return m == null ? null : m.getStreamContext(c);
 	}
 
-	@Override
-	public void run() {
-		lock.lock();
-		try {
-			// Rebuild the maps because we may be running a whole period late
-			Collection<TemporarySecret> secrets = new ArrayList<TemporarySecret>();
-			secrets.addAll(oldSecrets.values());
-			secrets.addAll(currentSecrets.values());
-			secrets.addAll(newSecrets.values());
-			oldSecrets.clear();
-			currentSecrets.clear();
-			newSecrets.clear();
-			// Work out what phase of its lifecycle each secret is in
-			long now = clock.currentTimeMillis();
-			Collection<TemporarySecret> dead = assignSecretsToMaps(now, secrets);
-			// Remove any dead secrets from the recogniser
-			for (TemporarySecret s : dead) {
-				ContactId c = s.getContactId();
-				TransportId t = s.getTransportId();
-				long period = s.getPeriod();
-				tagRecogniser.removeSecret(c, t, period);
-			}
-			// Replace any dead secrets
-			Collection<TemporarySecret> created = replaceDeadSecrets(now, dead);
-			if (!created.isEmpty()) {
-				// Store any secrets that have been created
-				try {
-					db.addSecrets(created);
-				} catch (DbException e) {
-					if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-				}
-				// Pass any secrets that have been created to the recogniser
-				for (TemporarySecret s : created) tagRecogniser.addSecret(s);
-			}
-		} finally {
-			lock.unlock();
-		}
+	public StreamContext recogniseTag(TransportId t, byte[] tag)
+			throws DbException {
+		TransportKeyManager m = managers.get(t);
+		return m == null ? null : m.recogniseTag(tag);
 	}
 
 	public void eventOccurred(Event e) {
-		if (e instanceof ContactRemovedEvent) {
-			ContactRemovedEvent c = (ContactRemovedEvent) e;
-			timer.schedule(new ContactRemovedTask(c), 0);
-		} else if (e instanceof TransportAddedEvent) {
+		if (e instanceof TransportAddedEvent) {
 			TransportAddedEvent t = (TransportAddedEvent) e;
-			timer.schedule(new TransportAddedTask(t), 0);
+			addTransport(t.getTransportId(), t.getMaxLatency());
 		} else if (e instanceof TransportRemovedEvent) {
-			TransportRemovedEvent t = (TransportRemovedEvent) e;
-			timer.schedule(new TransportRemovedTask(t), 0);
+			removeTransport(((TransportRemovedEvent) e).getTransportId());
+		} else if (e instanceof ContactRemovedEvent) {
+			removeContact(((ContactRemovedEvent) e).getContactId());
 		}
 	}
 
-	// Locking: lock
-	private void removeSecrets(ContactId c, Map<?, TemporarySecret> m) {
-		Iterator<TemporarySecret> it = m.values().iterator();
-		while (it.hasNext())
-			if (it.next().getContactId().equals(c)) it.remove();
-	}
-
-	// Locking: lock
-	private void removeSecrets(TransportId t, Map<?, TemporarySecret> m) {
-		Iterator<TemporarySecret> it = m.values().iterator();
-		while (it.hasNext())
-			if (it.next().getTransportId().equals(t)) it.remove();
-	}
-
-	private static class EndpointKey {
-
-		private final ContactId contactId;
-		private final TransportId transportId;
-
-		private EndpointKey(ContactId contactId, TransportId transportId) {
-			this.contactId = contactId;
-			this.transportId = transportId;
-		}
-
-		private EndpointKey(Endpoint ep) {
-			this(ep.getContactId(), ep.getTransportId());
-		}
-
-		@Override
-		public int hashCode() {
-			return contactId.hashCode() ^ transportId.hashCode();
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			if (o instanceof EndpointKey) {
-				EndpointKey k = (EndpointKey) o;
-				return contactId.equals(k.contactId) &&
-						transportId.equals(k.transportId);
+	private void addTransport(final TransportId t, final int maxLatency) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				TransportKeyManager m = new TransportKeyManager(db, crypto,
+						dbExecutor, timer, clock, t, maxLatency);
+				// Don't add transport twice if event is received during startup
+				if (managers.putIfAbsent(t, m) == null) m.start();
 			}
-			return false;
-		}
+		});
 	}
 
-	private class ContactRemovedTask extends TimerTask {
-
-		private final ContactRemovedEvent event;
-
-		private ContactRemovedTask(ContactRemovedEvent event) {
-			this.event = event;
-		}
-
-		@Override
-		public void run() {
-			ContactId c = event.getContactId();
-			tagRecogniser.removeSecrets(c);
-			lock.lock();
-			try {
-				removeSecrets(c, oldSecrets);
-				removeSecrets(c, currentSecrets);
-				removeSecrets(c, newSecrets);
-			} finally {
-				lock.unlock();
-			}
-		}
+	private void removeTransport(TransportId t) {
+		managers.remove(t);
 	}
 
-	private class TransportAddedTask extends TimerTask {
-
-		private final TransportAddedEvent event;
-
-		private TransportAddedTask(TransportAddedEvent event) {
-			this.event = event;
-		}
-
-		@Override
-		public void run() {
-			lock.lock();
-			try {
-				maxLatencies.put(event.getTransportId(), event.getMaxLatency());
-			} finally {
-				lock.unlock();
-			}
-		}
-	}
-
-	private class TransportRemovedTask extends TimerTask {
-
-		private TransportRemovedEvent event;
-
-		private TransportRemovedTask(TransportRemovedEvent event) {
-			this.event = event;
-		}
-
-		@Override
-		public void run() {
-			TransportId t = event.getTransportId();
-			tagRecogniser.removeSecrets(t);
-			lock.lock();
-			try {
-				maxLatencies.remove(t);
-				removeSecrets(t, oldSecrets);
-				removeSecrets(t, currentSecrets);
-				removeSecrets(t, newSecrets);
-			} finally {
-				lock.unlock();
+	private void removeContact(final ContactId c) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				for (TransportKeyManager m : managers.values())
+					m.removeContact(c);
 			}
-		}
+		});
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/MutableIncomingKeys.java b/briar-core/src/org/briarproject/transport/MutableIncomingKeys.java
new file mode 100644
index 0000000000000000000000000000000000000000..6be3d1823d210416539696516922a19e9ba651c5
--- /dev/null
+++ b/briar-core/src/org/briarproject/transport/MutableIncomingKeys.java
@@ -0,0 +1,40 @@
+package org.briarproject.transport;
+
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.transport.IncomingKeys;
+
+// This class is not thread-safe
+class MutableIncomingKeys {
+
+	private final SecretKey tagKey, headerKey;
+	private final long rotationPeriod;
+	private final ReorderingWindow window;
+
+	MutableIncomingKeys(IncomingKeys in) {
+		tagKey = in.getTagKey();
+		headerKey = in.getHeaderKey();
+		rotationPeriod = in.getRotationPeriod();
+		window = new ReorderingWindow(in.getWindowBase(), in.getWindowBitmap());
+	}
+
+	IncomingKeys snapshot() {
+		return new IncomingKeys(tagKey, headerKey, rotationPeriod,
+				window.getBase(), window.getBitmap());
+	}
+
+	SecretKey getTagKey() {
+		return tagKey;
+	}
+
+	SecretKey getHeaderKey() {
+		return headerKey;
+	}
+
+	long getRotationPeriod() {
+		return rotationPeriod;
+	}
+
+	ReorderingWindow getWindow() {
+		return window;
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/MutableOutgoingKeys.java b/briar-core/src/org/briarproject/transport/MutableOutgoingKeys.java
new file mode 100644
index 0000000000000000000000000000000000000000..d8163e4f2b045ee8436110ede340075efffcdea6
--- /dev/null
+++ b/briar-core/src/org/briarproject/transport/MutableOutgoingKeys.java
@@ -0,0 +1,44 @@
+package org.briarproject.transport;
+
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.transport.OutgoingKeys;
+
+// This class is not thread-safe
+class MutableOutgoingKeys {
+
+	private final SecretKey tagKey, headerKey;
+	private final long rotationPeriod;
+	private long streamCounter;
+
+	MutableOutgoingKeys(OutgoingKeys out) {
+		tagKey = out.getTagKey();
+		headerKey = out.getHeaderKey();
+		rotationPeriod = out.getRotationPeriod();
+		streamCounter = out.getStreamCounter();
+	}
+
+	OutgoingKeys snapshot() {
+		return new OutgoingKeys(tagKey, headerKey, rotationPeriod,
+				streamCounter);
+	}
+
+	SecretKey getTagKey() {
+		return tagKey;
+	}
+
+	SecretKey getHeaderKey() {
+		return headerKey;
+	}
+
+	long getRotationPeriod() {
+		return rotationPeriod;
+	}
+
+	long getStreamCounter() {
+		return streamCounter;
+	}
+
+	void incrementStreamCounter() {
+		streamCounter++;
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/MutableTransportKeys.java b/briar-core/src/org/briarproject/transport/MutableTransportKeys.java
new file mode 100644
index 0000000000000000000000000000000000000000..65861974e75e97c61fcff529e40b706b12da22a9
--- /dev/null
+++ b/briar-core/src/org/briarproject/transport/MutableTransportKeys.java
@@ -0,0 +1,44 @@
+package org.briarproject.transport;
+
+import org.briarproject.api.TransportId;
+import org.briarproject.api.transport.TransportKeys;
+
+class MutableTransportKeys {
+
+	private final TransportId transportId;
+	private final MutableIncomingKeys inPrev, inCurr, inNext;
+	private final MutableOutgoingKeys outCurr;
+
+	MutableTransportKeys(TransportKeys k) {
+		transportId = k.getTransportId();
+		inPrev = new MutableIncomingKeys(k.getPreviousIncomingKeys());
+		inCurr = new MutableIncomingKeys(k.getCurrentIncomingKeys());
+		inNext = new MutableIncomingKeys(k.getNextIncomingKeys());
+		outCurr = new MutableOutgoingKeys(k.getCurrentOutgoingKeys());
+	}
+
+	TransportKeys snapshot() {
+		return new TransportKeys(transportId, inPrev.snapshot(),
+				inCurr.snapshot(), inNext.snapshot(), outCurr.snapshot());
+	}
+
+	TransportId getTransportId() {
+		return transportId;
+	}
+
+	MutableIncomingKeys getPreviousIncomingKeys() {
+		return inPrev;
+	}
+
+	MutableIncomingKeys getCurrentIncomingKeys() {
+		return inCurr;
+	}
+
+	MutableIncomingKeys getNextIncomingKeys() {
+		return inNext;
+	}
+
+	MutableOutgoingKeys getCurrentOutgoingKeys() {
+		return outCurr;
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/ReorderingWindow.java b/briar-core/src/org/briarproject/transport/ReorderingWindow.java
index 67f82fe24fefc482a97c9f2b3bed1eac28cc9d51..c1baca01b709293ce7b7a076d4efaff664c9954a 100644
--- a/briar-core/src/org/briarproject/transport/ReorderingWindow.java
+++ b/briar-core/src/org/briarproject/transport/ReorderingWindow.java
@@ -1,102 +1,98 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
-import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
-
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
+import java.util.Collections;
+import java.util.List;
+
+import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
 // This class is not thread-safe
 class ReorderingWindow {
 
-	private final Set<Long> unseen;
-
-	private long centre;
-
-	ReorderingWindow() {
-		unseen = new HashSet<Long>();
-		for (long l = 0; l < REORDERING_WINDOW_SIZE / 2; l++) unseen.add(l);
-		centre = 0;
-	}
+	private long base;
+	private boolean[] seen;
 
-	ReorderingWindow(long centre, byte[] bitmap) {
-		if (centre < 0 || centre > MAX_32_BIT_UNSIGNED + 1)
+	ReorderingWindow(long base, byte[] bitmap) {
+		if (base < 0) throw new IllegalArgumentException();
+		if (base > MAX_32_BIT_UNSIGNED + 1)
 			throw new IllegalArgumentException();
-		if (bitmap.length != REORDERING_WINDOW_SIZE / 8)
-			throw new IllegalArgumentException();
-		this.centre = centre;
-		unseen = new HashSet<Long>();
-		long bitmapBottom = centre - REORDERING_WINDOW_SIZE / 2;
-		for (int bytes = 0; bytes < bitmap.length; bytes++) {
-			for (int bits = 0; bits < 8; bits++) {
-				long streamNumber = bitmapBottom + bytes * 8 + bits;
-				if (streamNumber >= 0 && streamNumber <= MAX_32_BIT_UNSIGNED) {
-					if ((bitmap[bytes] & (128 >> bits)) == 0)
-						unseen.add(streamNumber);
-				}
+		this.base = base;
+		seen = new boolean[bitmap.length * 8];
+		for (int i = 0; i < bitmap.length; i++) {
+			for (int j = 0; j < 8; j++) {
+				if ((bitmap[i] & (128 >> j)) != 0) seen[i * 8 + j] = true;
 			}
 		}
 	}
 
-	boolean isSeen(long streamNumber) {
-		return !unseen.contains(streamNumber);
+	long getBase() {
+		return base;
 	}
 
-	Collection<Long> setSeen(long streamNumber) {
-		long bottom = getBottom(centre);
-		long top = getTop(centre);
-		if (streamNumber < bottom || streamNumber > top)
-			throw new IllegalArgumentException();
-		if (!unseen.remove(streamNumber))
-			throw new IllegalArgumentException();
-		Collection<Long> changed = new ArrayList<Long>();
-		if (streamNumber >= centre) {
-			centre = streamNumber + 1;
-			long newBottom = getBottom(centre);
-			long newTop = getTop(centre);
-			for (long l = bottom; l < newBottom; l++) {
-				if (unseen.remove(l)) changed.add(l);
-			}
-			for (long l = top + 1; l <= newTop; l++) {
-				if (unseen.add(l)) changed.add(l);
+	byte[] getBitmap() {
+		byte[] bitmap = new byte[seen.length / 8];
+		for (int i = 0; i < bitmap.length; i++) {
+			for (int j = 0; j < 8; j++) {
+				if (seen[i * 8 + j]) bitmap[i] |= 128 >> j;
 			}
 		}
-		return changed;
+		return bitmap;
 	}
 
-	long getCentre() {
-		return centre;
+	List<Long> getUnseen() {
+		List<Long> unseen = new ArrayList<Long>(seen.length);
+		for (int i = 0; i < seen.length; i++)
+			if (!seen[i]) unseen.add(base + i);
+		return unseen;
 	}
 
-	byte[] getBitmap() {
-		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
-		long bitmapBottom = centre - REORDERING_WINDOW_SIZE / 2;
-		for (int bytes = 0; bytes < bitmap.length; bytes++) {
-			for (int bits = 0; bits < 8; bits++) {
-				long streamNumber = bitmapBottom + bytes * 8 + bits;
-				if (streamNumber >= 0 && streamNumber <= MAX_32_BIT_UNSIGNED) {
-					if (!unseen.contains(streamNumber))
-						bitmap[bytes] |= 128 >> bits;
-				}
-			}
+	Change setSeen(long index) {
+		if (index < base) throw new IllegalArgumentException();
+		if (index >= base + seen.length) throw new IllegalArgumentException();
+		if (index > MAX_32_BIT_UNSIGNED) throw new IllegalArgumentException();
+		int offset = (int) (index - base);
+		if (seen[offset]) throw new IllegalArgumentException();
+		seen[offset] = true;
+		// Rule 1: Slide until all elements above the midpoint are unseen
+		int slide = Math.max(0, offset + 1 - seen.length / 2);
+		// Rule 2: Slide until the lowest element is unseen
+		while (seen[slide]) slide++;
+		// If the window doesn't need to slide, return
+		if (slide == 0) {
+			List<Long> added = Collections.emptyList();
+			List<Long> removed = Collections.singletonList(index);
+			return new Change(added, removed);
 		}
-		return bitmap;
+		// Record the elements that will be added and removed
+		List<Long> added = new ArrayList<Long>(slide);
+		List<Long> removed = new ArrayList<Long>(slide);
+		for (int i = 0; i < slide; i++) {
+			if (!seen[i]) removed.add(base + i);
+			added.add(base + seen.length + i);
+		}
+		removed.add(index);
+		// Update the window
+		base += slide;
+		for (int i = 0; i + slide < seen.length; i++) seen[i] = seen[i + slide];
+		for (int i = seen.length - slide; i < seen.length; i++) seen[i] = false;
+		return new Change(added, removed);
 	}
 
-	// Returns the lowest value contained in a window with the given centre
-	private static long getBottom(long centre) {
-		return Math.max(0, centre - REORDERING_WINDOW_SIZE / 2);
-	}
+	static class Change {
 
-	// Returns the highest value contained in a window with the given centre
-	private static long getTop(long centre) {
-		return Math.min(MAX_32_BIT_UNSIGNED,
-				centre + REORDERING_WINDOW_SIZE / 2 - 1);
-	}
+		private final List<Long> added, removed;
 
-	public Collection<Long> getUnseen() {
-		return unseen;
+		Change(List<Long> added, List<Long> removed) {
+			this.added = added;
+			this.removed = removed;
+		}
+
+		List<Long> getAdded() {
+			return added;
+		}
+
+		List<Long> getRemoved() {
+			return removed;
+		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
index d066cacd4f6dcefe45ea1a5ad230f6c3fa03b87e..48179153f57898276f03b52a5eceb5acbbfe3380 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
@@ -4,6 +4,7 @@ import java.io.InputStream;
 
 import javax.inject.Inject;
 
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.crypto.StreamDecrypterFactory;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamReaderFactory;
@@ -23,9 +24,9 @@ class StreamReaderFactoryImpl implements StreamReaderFactory {
 	}
 
 	public InputStream createInvitationStreamReader(InputStream in,
-			byte[] secret, boolean alice) {
+			SecretKey headerKey) {
 		return new StreamReaderImpl(
 				streamDecrypterFactory.createInvitationStreamDecrypter(in,
-						secret, alice));
+						headerKey));
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/StreamReaderImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
index 002d177237d80c4ae5ebd551c539561c88d02f4b..87a90eb796163c5448c3665f3fbe29030d6c333b 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
@@ -7,6 +7,12 @@ import java.io.InputStream;
 
 import org.briarproject.api.crypto.StreamDecrypter;
 
+/**
+ * An {@link java.io.InputStream InputStream} that unpacks payload data from
+ * transport frames.
+ * <p>
+ * This class is not thread-safe.
+ */
 class StreamReaderImpl extends InputStream {
 
 	private final StreamDecrypter decrypter;
@@ -50,7 +56,7 @@ class StreamReaderImpl extends InputStream {
 	}
 
 	private void readFrame() throws IOException {
-		assert length == 0;
+		if (length != 0) throw new IllegalStateException();
 		offset = 0;
 		length = decrypter.readFrame(payload);
 	}
diff --git a/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java b/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
index 89da3a46445d4dbed63895332179229562c38417..4335273db3d067df4385e7b402939ba566c2735c 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
@@ -4,6 +4,7 @@ import java.io.OutputStream;
 
 import javax.inject.Inject;
 
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.crypto.StreamEncrypterFactory;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamWriterFactory;
@@ -24,9 +25,9 @@ class StreamWriterFactoryImpl implements StreamWriterFactory {
 	}
 
 	public OutputStream createInvitationStreamWriter(OutputStream out,
-			byte[] secret, boolean alice) {
+			SecretKey headerKey) {
 		return new StreamWriterImpl(
 				streamEncrypterFactory.createInvitationStreamEncrypter(out,
-						secret, alice));
+						headerKey));
 	}
 }
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/transport/StreamWriterImpl.java b/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
index c76e933b59a191b1310a40e864c7835fbd1a73ee..2fad041439270224c6d0fdd3163173537b8c5b68 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
@@ -8,9 +8,9 @@ import java.io.OutputStream;
 import org.briarproject.api.crypto.StreamEncrypter;
 
 /**
- * A {@link org.briarproject.api.transport.StreamWriter StreamWriter} that
- * buffers its input and writes a frame whenever there is a full frame to write
- * or the {@link #flush()} method is called.
+ * An {@link java.io.OutputStream OutputStream} that packs data into transport
+ * frames, writing a frame whenever there is a full frame to write or the
+ * {@link #flush()} method is called.
  * <p>
  * This class is not thread-safe.
  */
diff --git a/briar-core/src/org/briarproject/transport/TagRecogniserImpl.java b/briar-core/src/org/briarproject/transport/TagRecogniserImpl.java
deleted file mode 100644
index b6e53b9556a140be2d2098efb814719badef440b..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/transport/TagRecogniserImpl.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package org.briarproject.transport;
-
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import javax.inject.Inject;
-
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.TagRecogniser;
-import org.briarproject.api.transport.TemporarySecret;
-
-class TagRecogniserImpl implements TagRecogniser {
-
-	private final CryptoComponent crypto;
-	private final DatabaseComponent db;
-	private final Lock lock = new ReentrantLock();
-
-	// Locking: lock
-	private final Map<TransportId, TransportTagRecogniser> recognisers;
-
-	@Inject
-	TagRecogniserImpl(CryptoComponent crypto, DatabaseComponent db) {
-		this.crypto = crypto;
-		this.db = db;
-		recognisers = new HashMap<TransportId, TransportTagRecogniser>();
-	}
-
-	public StreamContext recogniseTag(TransportId t, byte[] tag)
-			throws DbException {
-		TransportTagRecogniser r;
-		lock.lock();
-		try {
-			r = recognisers.get(t);
-		} finally {
-			lock.unlock();
-		}
-		if (r == null) return null;
-		return r.recogniseTag(tag);
-	}
-
-	public void addSecret(TemporarySecret s) {
-		TransportId t = s.getTransportId();
-		TransportTagRecogniser r;
-		lock.lock();
-		try {
-			r = recognisers.get(t);
-			if (r == null) {
-				r = new TransportTagRecogniser(crypto, db, t);
-				recognisers.put(t, r);
-			}
-		} finally {
-			lock.unlock();
-		}
-		r.addSecret(s);
-	}
-
-	public void removeSecret(ContactId c, TransportId t, long period) {
-		TransportTagRecogniser r;
-		lock.lock();
-		try {
-			r = recognisers.get(t);
-		} finally {
-			lock.unlock();
-		}
-		if (r != null) r.removeSecret(c, period);
-	}
-
-	public void removeSecrets(ContactId c) {
-		lock.lock();
-		try {
-			for (TransportTagRecogniser r : recognisers.values())
-				r.removeSecrets(c);
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	public void removeSecrets(TransportId t) {
-		lock.lock();
-		try {
-			recognisers.remove(t);
-		} finally {
-			lock.unlock();
-		}
-
-	}
-
-	public void removeSecrets() {
-		lock.lock();
-		try {
-			for (TransportTagRecogniser r : recognisers.values())
-				r.removeSecrets();
-		} finally {
-			lock.unlock();
-		}
-
-	}
-}
diff --git a/briar-core/src/org/briarproject/transport/TransportKeyManager.java b/briar-core/src/org/briarproject/transport/TransportKeyManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..7818c356efa3d66cac3dc119b6d66045aad943c2
--- /dev/null
+++ b/briar-core/src/org/briarproject/transport/TransportKeyManager.java
@@ -0,0 +1,294 @@
+package org.briarproject.transport;
+
+import org.briarproject.api.Bytes;
+import org.briarproject.api.ContactId;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.db.DatabaseComponent;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.system.Clock;
+import org.briarproject.api.system.Timer;
+import org.briarproject.api.transport.StreamContext;
+import org.briarproject.api.transport.TransportKeys;
+import org.briarproject.transport.ReorderingWindow.Change;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.TimerTask;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Logger;
+
+import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
+import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
+import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
+class TransportKeyManager extends TimerTask {
+
+	private static final Logger LOG =
+			Logger.getLogger(TransportKeyManager.class.getName());
+
+	private final DatabaseComponent db;
+	private final CryptoComponent crypto;
+	private final Executor dbExecutor;
+	private final Timer timer;
+	private final Clock clock;
+	private final TransportId transportId;
+	private final long rotationPeriodLength;
+	private final ReentrantLock lock;
+
+	// The following are locking: lock
+	private final Map<Bytes, TagContext> inContexts;
+	private final Map<ContactId, MutableOutgoingKeys> outContexts;
+	private final Map<ContactId, MutableTransportKeys> keys;
+
+	TransportKeyManager(DatabaseComponent db, CryptoComponent crypto,
+			Executor dbExecutor, Timer timer, Clock clock,
+			TransportId transportId, long maxLatency) {
+		this.db = db;
+		this.crypto = crypto;
+		this.dbExecutor = dbExecutor;
+		this.timer = timer;
+		this.clock = clock;
+		this.transportId = transportId;
+		rotationPeriodLength = maxLatency + MAX_CLOCK_DIFFERENCE;
+		lock = new ReentrantLock();
+		inContexts = new HashMap<Bytes, TagContext>();
+		outContexts = new HashMap<ContactId, MutableOutgoingKeys>();
+		keys = new HashMap<ContactId, MutableTransportKeys>();
+	}
+
+	void start() {
+		// Load the transport keys from the DB
+		Map<ContactId, TransportKeys> loaded;
+		try {
+			loaded = db.getTransportKeys(transportId);
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+			return;
+		}
+		// Rotate the keys to the current rotation period
+		Map<ContactId, TransportKeys> rotated =
+				new HashMap<ContactId, TransportKeys>();
+		Map<ContactId, TransportKeys> current =
+				new HashMap<ContactId, TransportKeys>();
+		long now = clock.currentTimeMillis();
+		long rotationPeriod = now / rotationPeriodLength;
+		for (Entry<ContactId, TransportKeys> e : loaded.entrySet()) {
+			ContactId c = e.getKey();
+			TransportKeys k = e.getValue();
+			TransportKeys k1 = crypto.rotateTransportKeys(k, rotationPeriod);
+			if (k1.getRotationPeriod() > k.getRotationPeriod())
+				rotated.put(c, k1);
+			current.put(c, k1);
+		}
+		lock.lock();
+		try {
+			// Initialise mutable state for all contacts
+			for (Entry<ContactId, TransportKeys> e : current.entrySet())
+				addKeys(e.getKey(), new MutableTransportKeys(e.getValue()));
+			// Write any rotated keys back to the DB
+			saveTransportKeys(rotated);
+		} finally {
+			lock.unlock();
+		}
+		// Schedule a periodic task to rotate the keys
+		long delay = rotationPeriodLength - now % rotationPeriodLength;
+		timer.scheduleAtFixedRate(this, delay, rotationPeriodLength);
+	}
+
+	// Locking: lock
+	private void addKeys(ContactId c, MutableTransportKeys m) {
+		encodeTags(c, m.getPreviousIncomingKeys());
+		encodeTags(c, m.getCurrentIncomingKeys());
+		encodeTags(c, m.getNextIncomingKeys());
+		outContexts.put(c, m.getCurrentOutgoingKeys());
+		keys.put(c, m);
+	}
+
+	// Locking: lock
+	private void encodeTags(ContactId c, MutableIncomingKeys inKeys) {
+		for (long streamNumber : inKeys.getWindow().getUnseen()) {
+			TagContext tagCtx = new TagContext(c, inKeys, streamNumber);
+			byte[] tag = new byte[TAG_LENGTH];
+			crypto.encodeTag(tag, inKeys.getTagKey(), streamNumber);
+			inContexts.put(new Bytes(tag), tagCtx);
+		}
+	}
+
+	private void saveTransportKeys(final Map<ContactId, TransportKeys> rotated) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					db.updateTransportKeys(rotated);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	void addContact(ContactId c, TransportKeys k) {
+		lock.lock();
+		try {
+			// Initialise mutable state for the contact
+			addKeys(c, new MutableTransportKeys(k));
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	void removeContact(ContactId c) {
+		lock.lock();
+		try {
+			// Remove mutable state for the contact
+			Iterator<Entry<Bytes, TagContext>> it =
+					inContexts.entrySet().iterator();
+			while (it.hasNext())
+				if (it.next().getValue().contactId.equals(c)) it.remove();
+			outContexts.remove(c);
+			keys.remove(c);
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	StreamContext getStreamContext(ContactId c) {
+		StreamContext ctx;
+		lock.lock();
+		try {
+			// Look up the outgoing keys for the contact
+			MutableOutgoingKeys outKeys = outContexts.get(c);
+			if (outKeys == null) return null;
+			if (outKeys.getStreamCounter() > MAX_32_BIT_UNSIGNED) return null;
+			// Create a stream context
+			ctx = new StreamContext(c, transportId, outKeys.getTagKey(),
+					outKeys.getHeaderKey(), outKeys.getStreamCounter());
+			// Increment the stream counter and write it back to the DB
+			outKeys.incrementStreamCounter();
+			saveIncrementedStreamCounter(c, outKeys.getRotationPeriod());
+		} finally {
+			lock.unlock();
+		}
+		// TODO: Wait for save to complete, return null if it fails
+		return ctx;
+	}
+
+	private void saveIncrementedStreamCounter(final ContactId c,
+			final long rotationPeriod) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					db.incrementStreamCounter(c, transportId, rotationPeriod);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	StreamContext recogniseTag(byte[] tag) {
+		StreamContext ctx;
+		lock.lock();
+		try {
+			// Look up the incoming keys for the tag
+			TagContext tagCtx = inContexts.remove(new Bytes(tag));
+			if (tagCtx == null) return null;
+			MutableIncomingKeys inKeys = tagCtx.inKeys;
+			// Create a stream context
+			ctx = new StreamContext(tagCtx.contactId, transportId,
+					inKeys.getTagKey(), inKeys.getHeaderKey(),
+					tagCtx.streamNumber);
+			// Update the reordering window
+			ReorderingWindow window = inKeys.getWindow();
+			Change change = window.setSeen(tagCtx.streamNumber);
+			// Add tags for any stream numbers added to the window
+			for (long streamNumber : change.getAdded()) {
+				byte[] addTag = new byte[TAG_LENGTH];
+				crypto.encodeTag(addTag, inKeys.getTagKey(), streamNumber);
+				inContexts.put(new Bytes(addTag), new TagContext(
+						tagCtx.contactId, inKeys, streamNumber));
+			}
+			// Remove tags for any stream numbers removed from the window
+			for (long streamNumber : change.getRemoved()) {
+				byte[] removeTag = new byte[TAG_LENGTH];
+				crypto.encodeTag(removeTag, inKeys.getTagKey(), streamNumber);
+				inContexts.remove(new Bytes(removeTag));
+			}
+			// Write the window back to the DB
+			saveReorderingWindow(tagCtx.contactId, inKeys.getRotationPeriod(),
+					window.getBase(), window.getBitmap());
+		} finally {
+			lock.unlock();
+		}
+		// TODO: Wait for save to complete, return null if it fails
+		return ctx;
+	}
+
+	private void saveReorderingWindow(final ContactId c,
+			final long rotationPeriod, final long base, final byte[] bitmap) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				try {
+					db.setReorderingWindow(c, transportId, rotationPeriod,
+							base, bitmap);
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				}
+			}
+		});
+	}
+
+	@Override
+	public void run() {
+		lock.lock();
+		try {
+			// Rotate the keys to the current rotation period
+			Map<ContactId, TransportKeys> rotated =
+					new HashMap<ContactId, TransportKeys>();
+			Map<ContactId, TransportKeys> current =
+					new HashMap<ContactId, TransportKeys>();
+			long now = clock.currentTimeMillis();
+			long rotationPeriod = now / rotationPeriodLength;
+			for (Entry<ContactId, MutableTransportKeys> e : keys.entrySet()) {
+				ContactId c = e.getKey();
+				TransportKeys k = e.getValue().snapshot();
+				TransportKeys k1 = crypto.rotateTransportKeys(k,
+						rotationPeriod);
+				if (k1.getRotationPeriod() > k.getRotationPeriod())
+					rotated.put(c, k1);
+				current.put(c, k1);
+			}
+			// Rebuild the mutable state for all contacts
+			inContexts.clear();
+			outContexts.clear();
+			keys.clear();
+			for (Entry<ContactId, TransportKeys> e : current.entrySet())
+				addKeys(e.getKey(), new MutableTransportKeys(e.getValue()));
+			// Write any rotated keys back to the DB
+			saveTransportKeys(rotated);
+		} finally {
+			lock.unlock();
+		}
+	}
+
+	private static class TagContext {
+
+		private final ContactId contactId;
+		private final MutableIncomingKeys inKeys;
+		private final long streamNumber;
+
+		private TagContext(ContactId contactId, MutableIncomingKeys inKeys,
+				long streamNumber) {
+			this.contactId = contactId;
+			this.inKeys = inKeys;
+			this.streamNumber = streamNumber;
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/TransportModule.java b/briar-core/src/org/briarproject/transport/TransportModule.java
index 42e2417e8c20de7e50040c3e8517b4bc18ed78b1..36a7b3e3e7cb17dd5689006ea2fd1e4013ec18ae 100644
--- a/briar-core/src/org/briarproject/transport/TransportModule.java
+++ b/briar-core/src/org/briarproject/transport/TransportModule.java
@@ -1,23 +1,20 @@
 package org.briarproject.transport;
 
-import javax.inject.Singleton;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
 
-import org.briarproject.api.crypto.KeyManager;
 import org.briarproject.api.lifecycle.LifecycleManager;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
-import org.briarproject.api.transport.TagRecogniser;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
+import javax.inject.Singleton;
 
 public class TransportModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
 		bind(StreamReaderFactory.class).to(StreamReaderFactoryImpl.class);
-		bind(TagRecogniser.class).to(
-				TagRecogniserImpl.class).in(Singleton.class);
 		bind(StreamWriterFactory.class).to(StreamWriterFactoryImpl.class);
 	}
 
diff --git a/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java b/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java
deleted file mode 100644
index eaa1029631ca13ae2ef90c8508d7813817dbc3e3..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java
+++ /dev/null
@@ -1,215 +0,0 @@
-package org.briarproject.transport;
-
-import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.briarproject.api.Bytes;
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.db.DbException;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.TemporarySecret;
-
-// FIXME: Don't make alien calls with a lock held
-/**
- * A {@link org.briarproject.api.transport.TagRecogniser TagRecogniser} for a
- * specific transport.
- */
-class TransportTagRecogniser {
-
-	private final CryptoComponent crypto;
-	private final DatabaseComponent db;
-	private final TransportId transportId;
-	private final Lock lock = new ReentrantLock();
-
-	// The following are locking: lock
-	private final Map<Bytes, TagContext> tagMap;
-	private final Map<RemovalKey, RemovalContext> removalMap;
-
-	TransportTagRecogniser(CryptoComponent crypto, DatabaseComponent db,
-			TransportId transportId) {
-		this.crypto = crypto;
-		this.db = db;
-		this.transportId = transportId;
-		tagMap = new HashMap<Bytes, TagContext>();
-		removalMap = new HashMap<RemovalKey, RemovalContext>();
-	}
-
-	StreamContext recogniseTag(byte[] tag) throws DbException {
-		lock.lock();
-		try {
-			TagContext t = tagMap.remove(new Bytes(tag));
-			if (t == null) return null; // The tag was not expected
-			// Update the reordering window and the expected tags
-			SecretKey key = crypto.deriveTagKey(t.secret, !t.alice);
-			for (long streamNumber : t.window.setSeen(t.streamNumber)) {
-				byte[] tag1 = new byte[TAG_LENGTH];
-				crypto.encodeTag(tag1, key, streamNumber);
-				if (streamNumber < t.streamNumber) {
-					TagContext removed = tagMap.remove(new Bytes(tag1));
-					assert removed != null;
-				} else {
-					TagContext added = new TagContext(t, streamNumber);
-					TagContext duplicate = tagMap.put(new Bytes(tag1), added);
-					assert duplicate == null;
-				}
-			}
-			// Store the updated reordering window in the DB
-			db.setReorderingWindow(t.contactId, transportId, t.period,
-					t.window.getCentre(), t.window.getBitmap());
-			return new StreamContext(t.contactId, transportId, t.secret,
-					t.streamNumber, t.alice);
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	void addSecret(TemporarySecret s) {
-		lock.lock();
-		try {
-			ContactId contactId = s.getContactId();
-			boolean alice = s.getAlice();
-			long period = s.getPeriod();
-			byte[] secret = s.getSecret();
-			long centre = s.getWindowCentre();
-			byte[] bitmap = s.getWindowBitmap();
-			// Create the reordering window and the expected tags
-			SecretKey key = crypto.deriveTagKey(secret, !alice);
-			ReorderingWindow window = new ReorderingWindow(centre, bitmap);
-			for (long streamNumber : window.getUnseen()) {
-				byte[] tag = new byte[TAG_LENGTH];
-				crypto.encodeTag(tag, key, streamNumber);
-				TagContext added = new TagContext(contactId, alice, period,
-						secret, window, streamNumber);
-				TagContext duplicate = tagMap.put(new Bytes(tag), added);
-				assert duplicate == null;
-			}
-			// Create a removal context to remove the window and the tags later
-			RemovalContext r = new RemovalContext(window, secret, alice);
-			removalMap.put(new RemovalKey(contactId, period), r);
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	void removeSecret(ContactId contactId, long period) {
-		lock.lock();
-		try {
-			RemovalKey k = new RemovalKey(contactId, period);
-			RemovalContext removed = removalMap.remove(k);
-			if (removed == null) throw new IllegalArgumentException();
-			removeSecret(removed);
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	// Locking: lock
-	private void removeSecret(RemovalContext r) {
-		// Remove the expected tags
-		SecretKey key = crypto.deriveTagKey(r.secret, !r.alice);
-		byte[] tag = new byte[TAG_LENGTH];
-		for (long streamNumber : r.window.getUnseen()) {
-			crypto.encodeTag(tag, key, streamNumber);
-			TagContext removed = tagMap.remove(new Bytes(tag));
-			assert removed != null;
-		}
-	}
-
-	void removeSecrets(ContactId c) {
-		lock.lock();
-		try {
-			Collection<RemovalKey> keysToRemove = new ArrayList<RemovalKey>();
-			for (RemovalKey k : removalMap.keySet())
-				if (k.contactId.equals(c)) keysToRemove.add(k);
-			for (RemovalKey k : keysToRemove)
-				removeSecret(k.contactId, k.period);
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	void removeSecrets() {
-		lock.lock();
-		try {
-			for (RemovalContext r : removalMap.values()) removeSecret(r);
-			assert tagMap.isEmpty();
-			removalMap.clear();
-		} finally {
-			lock.unlock();
-		}
-	}
-
-	private static class TagContext {
-
-		private final ContactId contactId;
-		private final boolean alice;
-		private final long period;
-		private final byte[] secret;
-		private final ReorderingWindow window;
-		private final long streamNumber;
-
-		private TagContext(ContactId contactId, boolean alice, long period,
-				byte[] secret, ReorderingWindow window, long streamNumber) {
-			this.contactId = contactId;
-			this.alice = alice;
-			this.period = period;
-			this.secret = secret;
-			this.window = window;
-			this.streamNumber = streamNumber;
-		}
-
-		private TagContext(TagContext t, long streamNumber) {
-			this(t.contactId, t.alice, t.period, t.secret, t.window,
-					streamNumber);
-		}
-	}
-
-	private static class RemovalKey {
-
-		private final ContactId contactId;
-		private final long period;
-
-		private RemovalKey(ContactId contactId, long period) {
-			this.contactId = contactId;
-			this.period = period;
-		}
-
-		@Override
-		public int hashCode() {
-			return contactId.hashCode() ^ (int) (period ^ (period >>> 32));
-		}
-
-		@Override
-		public boolean equals(Object o) {
-			if (o instanceof RemovalKey) {
-				RemovalKey k = (RemovalKey) o;
-				return contactId.equals(k.contactId) && period == k.period;
-			}
-			return false;
-		}
-	}
-
-	private static class RemovalContext {
-
-		private final ReorderingWindow window;
-		private final byte[] secret;
-		private final boolean alice;
-
-		private RemovalContext(ReorderingWindow window, byte[] secret,
-				boolean alice) {
-			this.window = window;
-			this.secret = secret;
-			this.alice = alice;
-		}
-	}
-}
diff --git a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
index 92f2d671751809b3f5c68c588a5c9f910208daf4..2575bb268f4a780729b140fd53c62f24e0eaa084 100644
--- a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
+++ b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
@@ -1,6 +1,10 @@
 package org.briarproject.plugins;
 
 import com.google.inject.Provides;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Executor;
 
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.lifecycle.IoExecutor;
diff --git a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
index ce87ecd76880c292879a1ffb33c3aa6828f264a8..07405398bff5990d3dcd7a9123997dd4e111fceb 100644
--- a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
@@ -10,6 +10,7 @@ import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.messaging.Ack;
 import org.briarproject.api.messaging.Group;
 import org.briarproject.api.messaging.GroupFactory;
@@ -44,7 +45,6 @@ import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Random;
 
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
@@ -61,14 +61,9 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 	private final MessageVerifier messageVerifier;
 
 	private final ContactId contactId;
-	private final byte[] secret;
-	private final Author author;
+	private final SecretKey tagKey, headerKey;
 	private final Group group;
 	private final Message message, message1;
-	private final String authorName = "Alice";
-	private final String contentType = "text/plain";
-	private final long timestamp = System.currentTimeMillis();
-	private final String messageBody = "Hello world";
 	private final Collection<MessageId> messageIds;
 	private final TransportId transportId;
 	private final TransportProperties transportProperties;
@@ -85,9 +80,9 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		packetWriterFactory = i.getInstance(PacketWriterFactory.class);
 		messageVerifier = i.getInstance(MessageVerifier.class);
 		contactId = new ContactId(234);
-		// Create a shared secret
-		secret = new byte[32];
-		new Random().nextBytes(secret);
+		// Create the transport keys
+		tagKey = TestUtils.createSecretKey();
+		headerKey = TestUtils.createSecretKey();
 		// Create a group
 		GroupFactory groupFactory = i.getInstance(GroupFactory.class);
 		group = groupFactory.createGroup("Group");
@@ -95,12 +90,15 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		AuthorFactory authorFactory = i.getInstance(AuthorFactory.class);
 		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
 		KeyPair authorKeyPair = crypto.generateSignatureKeyPair();
-		author = authorFactory.createAuthor(authorName,
+		Author author = authorFactory.createAuthor("Alice",
 				authorKeyPair.getPublic().getEncoded());
 		// Create two messages to the group: one anonymous, one pseudonymous
 		MessageFactory messageFactory = i.getInstance(MessageFactory.class);
+		String contentType = "text/plain";
+		long timestamp = System.currentTimeMillis();
+		String messageBody = "Hello world";
 		message = messageFactory.createAnonymousMessage(null, group,
-				contentType, timestamp, messageBody.getBytes("UTF-8"));
+				"text/plain", timestamp, messageBody.getBytes("UTF-8"));
 		message1 = messageFactory.createPseudonymousMessage(null, group,
 				author, authorKeyPair.getPrivate(), contentType, timestamp,
 				messageBody.getBytes("UTF-8"));
@@ -118,8 +116,8 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 
 	private byte[] write() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamContext ctx = new StreamContext(contactId, transportId, secret,
-				0, true);
+		StreamContext ctx = new StreamContext(contactId, transportId, tagKey,
+				headerKey, 0);
 		OutputStream streamWriter =
 				streamWriterFactory.createStreamWriter(out, ctx);
 		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(
@@ -134,7 +132,8 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 
 		packetWriter.writeRequest(new Request(messageIds));
 
-		SubscriptionUpdate su = new SubscriptionUpdate(Arrays.asList(group), 1);
+		SubscriptionUpdate su = new SubscriptionUpdate(
+				Collections.singletonList(group), 1);
 		packetWriter.writeSubscriptionUpdate(su);
 
 		TransportUpdate tu = new TransportUpdate(transportId,
@@ -150,8 +149,8 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		byte[] tag = new byte[TAG_LENGTH];
 		assertEquals(TAG_LENGTH, in.read(tag, 0, TAG_LENGTH));
 		// FIXME: Check that the expected tag was received
-		StreamContext ctx = new StreamContext(contactId, transportId, secret,
-				0, false);
+		StreamContext ctx = new StreamContext(contactId, transportId, tagKey,
+				headerKey, 0);
 		InputStream streamReader =
 				streamReaderFactory.createStreamReader(in, ctx);
 		PacketReader packetReader = packetReaderFactory.createPacketReader(
@@ -184,7 +183,7 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		// Read the subscription update
 		assertTrue(packetReader.hasSubscriptionUpdate());
 		SubscriptionUpdate su = packetReader.readSubscriptionUpdate();
-		assertEquals(Arrays.asList(group), su.getGroups());
+		assertEquals(Collections.singletonList(group), su.getGroups());
 		assertEquals(1, su.getVersion());
 
 		// Read the transport update
diff --git a/briar-tests/src/org/briarproject/TestUtils.java b/briar-tests/src/org/briarproject/TestUtils.java
index f48b39fc2dcc87c4c06fe22aa3a005a582a94ab2..e6d7f02f6a8fee5e4088b4a8e167f6364296eaf7 100644
--- a/briar-tests/src/org/briarproject/TestUtils.java
+++ b/briar-tests/src/org/briarproject/TestUtils.java
@@ -7,6 +7,9 @@ import java.io.File;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import org.briarproject.api.UniqueId;
+import org.briarproject.api.crypto.SecretKey;
+
 public class TestUtils {
 
 	private static final AtomicInteger nextTestDir =
@@ -35,4 +38,10 @@ public class TestUtils {
 			c[i] = (char) ('a' + random.nextInt(26));
 		return new String(c);
 	}
+
+	public static SecretKey createSecretKey() {
+		byte[] b = new byte[SecretKey.LENGTH];
+		random.nextBytes(b);
+		return new SecretKey(b);
+	}
 }
diff --git a/briar-tests/src/org/briarproject/crypto/KeyAgreementTest.java b/briar-tests/src/org/briarproject/crypto/KeyAgreementTest.java
index f4bfa8cd6045f2c067cb3e0c574041483016c191..732e638725723ee9e192de69f5dd525d25bad0a3 100644
--- a/briar-tests/src/org/briarproject/crypto/KeyAgreementTest.java
+++ b/briar-tests/src/org/briarproject/crypto/KeyAgreementTest.java
@@ -4,6 +4,7 @@ import org.briarproject.BriarTestCase;
 import org.briarproject.TestSeedProvider;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.system.SeedProvider;
 import org.junit.Test;
 
@@ -19,8 +20,8 @@ public class KeyAgreementTest extends BriarTestCase {
 		byte[] aPub = aPair.getPublic().getEncoded();
 		KeyPair bPair = crypto.generateAgreementKeyPair();
 		byte[] bPub = bPair.getPublic().getEncoded();
-		byte[] aSecret = crypto.deriveMasterSecret(aPub, bPair, true);
-		byte[] bSecret = crypto.deriveMasterSecret(bPub, aPair, false);
-		assertArrayEquals(aSecret, bSecret);
+		SecretKey aMaster = crypto.deriveMasterSecret(aPub, bPair, true);
+		SecretKey bMaster = crypto.deriveMasterSecret(bPub, aPair, false);
+		assertArrayEquals(aMaster.getBytes(), bMaster.getBytes());
 	}
 }
diff --git a/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java b/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
index 0e3c80a71d56460499c7d43160d3153ec6f72063..2fc33cdb1316d76c133c1227ff31b72ddd0d70a9 100644
--- a/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
+++ b/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
@@ -2,72 +2,164 @@ package org.briarproject.crypto;
 
 import org.briarproject.BriarTestCase;
 import org.briarproject.TestSeedProvider;
+import org.briarproject.TestUtils;
+import org.briarproject.api.TransportId;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.transport.TransportKeys;
 import org.junit.Test;
 
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Random;
 
-import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertFalse;
 
 public class KeyDerivationTest extends BriarTestCase {
 
+	private final TransportId transportId = new TransportId("id");
 	private final CryptoComponent crypto;
-	private final byte[] secret;
+	private final SecretKey master;
 
 	public KeyDerivationTest() {
 		crypto = new CryptoComponentImpl(new TestSeedProvider());
-		secret = new byte[32];
-		new Random().nextBytes(secret);
+		master = TestUtils.createSecretKey();
 	}
 
 	@Test
 	public void testKeysAreDistinct() {
-		List<SecretKey> keys = new ArrayList<SecretKey>();
-		keys.add(crypto.deriveFrameKey(secret, 0, true));
-		keys.add(crypto.deriveFrameKey(secret, 0, false));
-		keys.add(crypto.deriveTagKey(secret, true));
-		keys.add(crypto.deriveTagKey(secret, false));
-		for (int i = 0; i < 4; i++) {
-			byte[] keyI = keys.get(i).getBytes();
-			for (int j = 0; j < 4; j++) {
-				byte[] keyJ = keys.get(j).getBytes();
-				assertEquals(i == j, Arrays.equals(keyI, keyJ));
-			}
-		}
+		TransportKeys k = crypto.deriveTransportKeys(transportId, master,
+				123, true);
+		assertAllDifferent(k);
 	}
 
 	@Test
-	public void testSecretAffectsDerivation() {
-		Random r = new Random();
-		List<byte[]> secrets = new ArrayList<byte[]>();
-		for (int i = 0; i < 20; i++) {
-			byte[] b = new byte[32];
-			r.nextBytes(b);
-			secrets.add(crypto.deriveNextSecret(b, 0));
-		}
-		for (int i = 0; i < 20; i++) {
-			byte[] secretI = secrets.get(i);
-			for (int j = 0; j < 20; j++) {
-				byte[] secretJ = secrets.get(j);
-				assertEquals(i == j, Arrays.equals(secretI, secretJ));
-			}
-		}
+	public void testCurrentKeysMatchCurrentKeysOfContact() {
+		// Start in rotation period 123
+		TransportKeys kA = crypto.deriveTransportKeys(transportId, master,
+				123, true);
+		TransportKeys kB = crypto.deriveTransportKeys(transportId, master,
+				123, false);
+		// Alice's incoming keys should equal Bob's outgoing keys
+		assertArrayEquals(kA.getCurrentIncomingKeys().getTagKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getCurrentIncomingKeys().getHeaderKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getHeaderKey().getBytes());
+		// Alice's outgoing keys should equal Bob's incoming keys
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getTagKey().getBytes(),
+				kB.getCurrentIncomingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getHeaderKey().getBytes(),
+				kB.getCurrentIncomingKeys().getHeaderKey().getBytes());
+		// Rotate into the future
+		kA = crypto.rotateTransportKeys(kA, 456);
+		kB = crypto.rotateTransportKeys(kB, 456);
+		// Alice's incoming keys should equal Bob's outgoing keys
+		assertArrayEquals(kA.getCurrentIncomingKeys().getTagKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getCurrentIncomingKeys().getHeaderKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getHeaderKey().getBytes());
+		// Alice's outgoing keys should equal Bob's incoming keys
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getTagKey().getBytes(),
+				kB.getCurrentIncomingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getHeaderKey().getBytes(),
+				kB.getCurrentIncomingKeys().getHeaderKey().getBytes());
 	}
 
 	@Test
-	public void testStreamNumberAffectsDerivation() {
-		List<byte[]> secrets = new ArrayList<byte[]>();
-		for (int i = 0; i < 20; i++)
-			secrets.add(crypto.deriveNextSecret(secret, i));
-		for (int i = 0; i < 20; i++) {
-			byte[] secretI = secrets.get(i);
-			for (int j = 0; j < 20; j++) {
-				byte[] secretJ = secrets.get(j);
-				assertEquals(i == j, Arrays.equals(secretI, secretJ));
+	public void testPreviousKeysMatchPreviousKeysOfContact() {
+		// Start in rotation period 123
+		TransportKeys kA = crypto.deriveTransportKeys(transportId, master,
+				123, true);
+		TransportKeys kB = crypto.deriveTransportKeys(transportId, master,
+				123, false);
+		// Compare Alice's previous keys in period 456 with Bob's current keys
+		// in period 455
+		kA = crypto.rotateTransportKeys(kA, 456);
+		kB = crypto.rotateTransportKeys(kB, 455);
+		// Alice's previous incoming keys should equal Bob's outgoing keys
+		assertArrayEquals(kA.getPreviousIncomingKeys().getTagKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getPreviousIncomingKeys().getHeaderKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getHeaderKey().getBytes());
+		// Compare Alice's current keys in period 456 with Bob's previous keys
+		// in period 457
+		kB = crypto.rotateTransportKeys(kB, 457);
+		// Alice's outgoing keys should equal Bob's previous incoming keys
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getTagKey().getBytes(),
+				kB.getPreviousIncomingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getHeaderKey().getBytes(),
+				kB.getPreviousIncomingKeys().getHeaderKey().getBytes());
+	}
+
+	@Test
+	public void testNextKeysMatchNextKeysOfContact() {
+		// Start in rotation period 123
+		TransportKeys kA = crypto.deriveTransportKeys(transportId, master,
+				123, true);
+		TransportKeys kB = crypto.deriveTransportKeys(transportId, master,
+				123, false);
+		// Compare Alice's current keys in period 456 with Bob's next keys in
+		// period 455
+		kA = crypto.rotateTransportKeys(kA, 456);
+		kB = crypto.rotateTransportKeys(kB, 455);
+		// Alice's outgoing keys should equal Bob's next incoming keys
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getTagKey().getBytes(),
+				kB.getNextIncomingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getCurrentOutgoingKeys().getHeaderKey().getBytes(),
+				kB.getNextIncomingKeys().getHeaderKey().getBytes());
+		// Compare Alice's next keys in period 456 with Bob's current keys
+		// in period 457
+		kB = crypto.rotateTransportKeys(kB, 457);
+		// Alice's next incoming keys should equal Bob's outgoing keys
+		assertArrayEquals(kA.getNextIncomingKeys().getTagKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
+		assertArrayEquals(kA.getNextIncomingKeys().getHeaderKey().getBytes(),
+				kB.getCurrentOutgoingKeys().getHeaderKey().getBytes());
+	}
+
+	@Test
+	public void testMasterKeyAffectsOutput() {
+		SecretKey master1 = TestUtils.createSecretKey();
+		assertFalse(Arrays.equals(master.getBytes(), master1.getBytes()));
+		TransportKeys k = crypto.deriveTransportKeys(transportId, master,
+				123, true);
+		TransportKeys k1 = crypto.deriveTransportKeys(transportId, master1,
+				123, true);
+		assertAllDifferent(k, k1);
+	}
+
+	@Test
+	public void testTransportIdAffectsOutput() {
+		TransportId transportId1 = new TransportId("id1");
+		assertFalse(transportId.getString().equals(transportId1.getString()));
+		TransportKeys k = crypto.deriveTransportKeys(transportId, master,
+				123, true);
+		TransportKeys k1 = crypto.deriveTransportKeys(transportId1, master,
+				123, true);
+		assertAllDifferent(k, k1);
+	}
+
+	private void assertAllDifferent(TransportKeys... transportKeys) {
+		List<SecretKey> secretKeys = new ArrayList<SecretKey>();
+		for (TransportKeys k : transportKeys) {
+			secretKeys.add(k.getPreviousIncomingKeys().getTagKey());
+			secretKeys.add(k.getPreviousIncomingKeys().getHeaderKey());
+			secretKeys.add(k.getCurrentIncomingKeys().getTagKey());
+			secretKeys.add(k.getCurrentIncomingKeys().getHeaderKey());
+			secretKeys.add(k.getNextIncomingKeys().getTagKey());
+			secretKeys.add(k.getNextIncomingKeys().getHeaderKey());
+			secretKeys.add(k.getCurrentOutgoingKeys().getTagKey());
+			secretKeys.add(k.getCurrentOutgoingKeys().getHeaderKey());
+		}
+		assertAllDifferent(secretKeys);
+	}
+
+	private void assertAllDifferent(List<SecretKey> keys) {
+		for (SecretKey ki : keys) {
+			for (SecretKey kj : keys) {
+				if (ki == kj) assertArrayEquals(ki.getBytes(), kj.getBytes());
+				else assertFalse(Arrays.equals(ki.getBytes(), kj.getBytes()));
 			}
 		}
 	}
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java
index 364d2ef6226d95a03fdf952905dd273af7a27266..0c4c69792d864061e8fb870f4d20120f2a3a0dfe 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java
@@ -11,6 +11,7 @@ import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportConfig;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.NoSuchContactException;
 import org.briarproject.api.db.NoSuchLocalAuthorException;
@@ -45,8 +46,9 @@ import org.briarproject.api.messaging.SubscriptionAck;
 import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportAck;
 import org.briarproject.api.messaging.TransportUpdate;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.IncomingKeys;
+import org.briarproject.api.transport.OutgoingKeys;
+import org.briarproject.api.transport.TransportKeys;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
@@ -84,8 +86,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	protected final int maxLatency;
 	protected final ContactId contactId;
 	protected final Contact contact;
-	protected final Endpoint endpoint;
-	protected final TemporarySecret temporarySecret;
 
 	public DatabaseComponentTest() {
 		groupId = new GroupId(TestUtils.getRandomId());
@@ -112,9 +112,6 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		maxLatency = Integer.MAX_VALUE;
 		contactId = new ContactId(234);
 		contact = new Contact(contactId, author, localAuthorId);
-		endpoint = new Endpoint(contactId, transportId, 123, true);
-		temporarySecret = new TemporarySecret(contactId, transportId, 123,
-				false, 234, new byte[32], 345, 456, new byte[4]);
 	}
 
 	protected abstract <T> DatabaseComponent createDatabaseComponent(
@@ -157,7 +154,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(eventBus).broadcast(with(any(ContactAddedEvent.class)));
 			// getContacts()
 			oneOf(database).getContacts(txn);
-			will(returnValue(Arrays.asList(contact)));
+			will(returnValue(Collections.singletonList(contact)));
 			// getRemoteProperties()
 			oneOf(database).getRemoteProperties(txn, transportId);
 			will(returnValue(Collections.emptyMap()));
@@ -177,7 +174,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(Collections.emptyList()));
 			// getGroups()
 			oneOf(database).getGroups(txn);
-			will(returnValue(Arrays.asList(groupId)));
+			will(returnValue(Collections.singletonList(group)));
 			// removeGroup()
 			oneOf(database).containsGroup(txn, groupId);
 			will(returnValue(true));
@@ -213,13 +210,13 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		assertFalse(db.open());
 		db.addLocalAuthor(localAuthor);
 		assertEquals(contactId, db.addContact(author, localAuthorId));
-		assertEquals(Arrays.asList(contact), db.getContacts());
+		assertEquals(Collections.singletonList(contact), db.getContacts());
 		assertEquals(Collections.emptyMap(),
 				db.getRemoteProperties(transportId));
 		db.addGroup(group); // First time - listeners called
 		db.addGroup(group); // Second time - not called
 		assertEquals(Collections.emptyList(), db.getMessageHeaders(groupId));
-		assertEquals(Arrays.asList(groupId), db.getGroups());
+		assertEquals(Collections.singletonList(group), db.getGroups());
 		db.removeGroup(group);
 		db.removeContact(contactId);
 		db.removeLocalAuthor(localAuthorId);
@@ -297,9 +294,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).addMessage(txn, message, true);
 			oneOf(database).setReadFlag(txn, messageId, true);
 			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Arrays.asList(contactId)));
+			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).getContactIds(txn);
-			will(returnValue(Arrays.asList(contactId)));
+			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
 			will(returnValue(false));
 			oneOf(database).addStatus(txn, contactId, messageId, false, false);
@@ -336,7 +333,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				eventBus, shutdown);
 
 		try {
-			db.addEndpoint(endpoint);
+			db.addTransportKeys(contactId, createTransportKeys());
 			fail();
 		} catch (NoSuchContactException expected) {}
 
@@ -401,7 +398,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		} catch (NoSuchContactException expected) {}
 
 		try {
-			Ack a = new Ack(Arrays.asList(messageId));
+			Ack a = new Ack(Collections.singletonList(messageId));
 			db.receiveAck(contactId, a);
 			fail();
 		} catch (NoSuchContactException expected) {}
@@ -412,7 +409,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		} catch (NoSuchContactException expected) {}
 
 		try {
-			Offer o = new Offer(Arrays.asList(messageId));
+			Offer o = new Offer(Collections.singletonList(messageId));
 			db.receiveOffer(contactId, o);
 			fail();
 		} catch (NoSuchContactException expected) {}
@@ -594,7 +591,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			// Check whether the transport is in the DB (which it's not)
 			exactly(8).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(3).of(database).containsContact(txn, contactId);
+			exactly(2).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			exactly(8).of(database).containsTransport(txn, transportId);
 			will(returnValue(false));
@@ -607,17 +604,17 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(author, localAuthorId));
 
 		try {
-			db.addEndpoint(endpoint);
+			db.getConfig(transportId);
 			fail();
 		} catch (NoSuchTransportException expected) {}
 
 		try {
-			db.getConfig(transportId);
+			db.getLocalProperties(transportId);
 			fail();
 		} catch (NoSuchTransportException expected) {}
 
 		try {
-			db.getLocalProperties(transportId);
+			db.getTransportKeys(transportId);
 			fail();
 		} catch (NoSuchTransportException expected) {}
 
@@ -909,7 +906,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			oneOf(database).getSubscriptionUpdate(txn, contactId, maxLatency);
-			will(returnValue(new SubscriptionUpdate(Arrays.asList(group), 1)));
+			will(returnValue(new SubscriptionUpdate(
+					Collections.singletonList(group), 1)));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
@@ -917,7 +915,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 
 		SubscriptionUpdate u = db.generateSubscriptionUpdate(contactId,
 				maxLatency);
-		assertEquals(Arrays.asList(group), u.getGroups());
+		assertEquals(Collections.singletonList(group), u.getGroups());
 		assertEquals(1, u.getVersion());
 
 		context.assertIsSatisfied();
@@ -962,8 +960,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			oneOf(database).getTransportUpdates(txn, contactId, maxLatency);
-			will(returnValue(Arrays.asList(new TransportUpdate(transportId,
-					transportProperties, 1))));
+			will(returnValue(Collections.singletonList(new TransportUpdate(
+					transportId, transportProperties, 1))));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
@@ -1003,7 +1001,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		db.receiveAck(contactId, new Ack(Arrays.asList(messageId)));
+		db.receiveAck(contactId, new Ack(Collections.singletonList(messageId)));
 
 		context.assertIsSatisfied();
 	}
@@ -1027,9 +1025,9 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).addMessage(txn, message, false);
 			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Arrays.asList(contactId)));
+			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).getContactIds(txn);
-			will(returnValue(Arrays.asList(contactId)));
+			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).removeOfferedMessage(txn, contactId, messageId);
 			will(returnValue(false));
 			oneOf(database).addStatus(txn, contactId, messageId, false, true);
@@ -1176,7 +1174,8 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		db.receiveRequest(contactId, new Request(Arrays.asList(messageId)));
+		db.receiveRequest(contactId, new Request(Collections.singletonList(
+				messageId)));
 
 		context.assertIsSatisfied();
 	}
@@ -1244,13 +1243,15 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).setGroups(txn, contactId, Arrays.asList(group), 1);
+			oneOf(database).setGroups(txn, contactId,
+					Collections.singletonList(group), 1);
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		SubscriptionUpdate u = new SubscriptionUpdate(Arrays.asList(group), 1);
+		SubscriptionUpdate u = new SubscriptionUpdate(
+				Collections.singletonList(group), 1);
 		db.receiveSubscriptionUpdate(contactId, u);
 
 		context.assertIsSatisfied();
@@ -1398,7 +1399,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		db.setVisibility(groupId, Arrays.asList(contactId));
+		db.setVisibility(groupId, Collections.singletonList(contactId));
 
 		context.assertIsSatisfied();
 	}
@@ -1467,7 +1468,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(true));
 			oneOf(database).setVisibleToAll(txn, groupId, true);
 			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Arrays.asList(contactId)));
+			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).getContactIds(txn);
 			will(returnValue(both));
 			oneOf(database).addVisibility(txn, contactId1, groupId);
@@ -1478,14 +1479,15 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		db.setVisibility(groupId, Arrays.asList(contactId));
+		db.setVisibility(groupId, Collections.singletonList(contactId));
 		db.setVisibleToAll(groupId, true);
 
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testTemporarySecrets() throws Exception {
+	public void testTransportKeys() throws Exception {
+		final TransportKeys keys = createTransportKeys();
 		Mockery context = new Mockery();
 		@SuppressWarnings("unchecked")
 		final Database<Object> database = context.mock(Database.class);
@@ -1493,28 +1495,52 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
-			// addSecrets()
+			// updateTransportKeys()
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
 			oneOf(database).containsTransport(txn, transportId);
 			will(returnValue(true));
-			oneOf(database).addSecrets(txn, Arrays.asList(temporarySecret));
+			oneOf(database).updateTransportKeys(txn,
+					Collections.singletonMap(contactId, keys));
 			oneOf(database).commitTransaction(txn);
-			// getSecrets()
+			// getTransportKeys()
 			oneOf(database).startTransaction();
 			will(returnValue(txn));
-			oneOf(database).getSecrets(txn);
-			will(returnValue(Arrays.asList(temporarySecret)));
+			oneOf(database).containsTransport(txn, transportId);
+			will(returnValue(true));
+			oneOf(database).getTransportKeys(txn, transportId);
+			will(returnValue(Collections.singletonMap(contactId, keys)));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		db.addSecrets(Arrays.asList(temporarySecret));
-		assertEquals(Arrays.asList(temporarySecret), db.getSecrets());
+		db.updateTransportKeys(Collections.singletonMap(contactId, keys));
+		assertEquals(Collections.singletonMap(contactId, keys),
+				db.getTransportKeys(transportId));
 
 		context.assertIsSatisfied();
 	}
+
+	private TransportKeys createTransportKeys() {
+		SecretKey inPrevTagKey = TestUtils.createSecretKey();
+		SecretKey inPrevHeaderKey = TestUtils.createSecretKey();
+		IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
+				1, 123, new byte[4]);
+		SecretKey inCurrTagKey = TestUtils.createSecretKey();
+		SecretKey inCurrHeaderKey = TestUtils.createSecretKey();
+		IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
+				2, 234, new byte[4]);
+		SecretKey inNextTagKey = TestUtils.createSecretKey();
+		SecretKey inNextHeaderKey = TestUtils.createSecretKey();
+		IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
+				3, 345, new byte[4]);
+		SecretKey outCurrTagKey = TestUtils.createSecretKey();
+		SecretKey outCurrHeaderKey = TestUtils.createSecretKey();
+		OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
+				2, 456);
+		return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
+	}
 }
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index 4da4e362db7dc9e64faa6c1429c60060b357583f..df9c26c5b75809cf764f06d6dfed0049811ce262 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -11,14 +11,17 @@ import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportConfig;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.TransportProperties;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.MessageHeader;
 import org.briarproject.api.messaging.Group;
 import org.briarproject.api.messaging.GroupId;
 import org.briarproject.api.messaging.Message;
 import org.briarproject.api.messaging.MessageId;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.TemporarySecret;
+import org.briarproject.api.transport.IncomingKeys;
+import org.briarproject.api.transport.OutgoingKeys;
+import org.briarproject.api.transport.TransportKeys;
+import org.briarproject.system.FileUtilsImpl;
 import org.briarproject.system.SystemClock;
 import org.junit.After;
 import org.junit.Before;
@@ -34,6 +37,7 @@ import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Random;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -59,7 +63,6 @@ public class H2DatabaseTest extends BriarTestCase {
 	private final Random random = new Random();
 	private final GroupId groupId;
 	private final Group group;
-	private final AuthorId authorId;
 	private final Author author;
 	private final AuthorId localAuthorId;
 	private final LocalAuthor localAuthor;
@@ -75,7 +78,7 @@ public class H2DatabaseTest extends BriarTestCase {
 	public H2DatabaseTest() throws Exception {
 		groupId = new GroupId(TestUtils.getRandomId());
 		group = new Group(groupId, "Group", new byte[GROUP_SALT_LENGTH]);
-		authorId = new AuthorId(TestUtils.getRandomId());
+		AuthorId authorId = new AuthorId(TestUtils.getRandomId());
 		author = new Author(authorId, "Alice", new byte[MAX_PUBLIC_KEY_LENGTH]);
 		localAuthorId = new AuthorId(TestUtils.getRandomId());
 		localAuthor = new LocalAuthor(localAuthorId, "Bob",
@@ -171,7 +174,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, true);
 
 		// The message has no status yet, so it should not be sendable
@@ -216,7 +219,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertTrue(ids.isEmpty());
 
 		// The contact subscribing should make the message sendable
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertFalse(ids.isEmpty());
 		Iterator<MessageId> it = ids.iterator();
@@ -243,7 +246,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -273,7 +276,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -305,7 +308,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
 		// Add some messages to ack
 		MessageId messageId1 = new MessageId(TestUtils.getRandomId());
@@ -342,7 +345,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
 		// Receive the same message twice
 		db.addMessage(txn, message, true);
@@ -352,10 +355,10 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// The message ID should only be returned once
 		Collection<MessageId> ids = db.getMessagesToAck(txn, contactId, 1234);
-		assertEquals(Arrays.asList(messageId), ids);
+		assertEquals(Collections.singletonList(messageId), ids);
 
 		// Remove the message ID
-		db.lowerAckFlag(txn, contactId, Arrays.asList(messageId));
+		db.lowerAckFlag(txn, contactId, Collections.singletonList(messageId));
 
 		// The message ID should have been removed
 		assertEquals(Collections.emptyList(), db.getMessagesToAck(txn,
@@ -375,7 +378,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -674,7 +677,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
 		db.addVisibility(txn, contactId, groupId);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
 		// The message is not in the database
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
@@ -692,7 +695,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact with a subscription
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
 		// There's no local subscription for the group
 		assertFalse(db.containsVisibleMessage(txn, contactId, messageId));
@@ -711,7 +714,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 		db.addMessage(txn, message, true);
 		db.addStatus(txn, contactId, messageId, false, false);
 
@@ -737,7 +740,8 @@ public class H2DatabaseTest extends BriarTestCase {
 
 		// Make the group visible to the contact
 		db.addVisibility(txn, contactId, groupId);
-		assertEquals(Arrays.asList(contactId), db.getVisibility(txn, groupId));
+		assertEquals(Collections.singletonList(contactId),
+				db.getVisibility(txn, groupId));
 
 		// Make the group invisible again
 		db.removeVisibility(txn, contactId, groupId);
@@ -1111,307 +1115,134 @@ public class H2DatabaseTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testTemporarySecrets() throws Exception {
-		// Create an endpoint and four consecutive temporary secrets
-		long epoch = 123;
-		int latency = 234;
-		boolean alice = false;
-		long outgoing1 = 345, centre1 = 456;
-		long outgoing2 = 567, centre2 = 678;
-		long outgoing3 = 789, centre3 = 890;
-		long outgoing4 = 901, centre4 = 123;
-		Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
-		Random random = new Random();
-		byte[] secret1 = new byte[32], bitmap1 = new byte[4];
-		random.nextBytes(secret1);
-		random.nextBytes(bitmap1);
-		TemporarySecret s1 = new TemporarySecret(contactId, transportId, epoch,
-				alice, 0, secret1, outgoing1, centre1, bitmap1);
-		byte[] secret2 = new byte[32], bitmap2 = new byte[4];
-		random.nextBytes(secret2);
-		random.nextBytes(bitmap2);
-		TemporarySecret s2 = new TemporarySecret(contactId, transportId, epoch,
-				alice, 1, secret2, outgoing2, centre2, bitmap2);
-		byte[] secret3 = new byte[32], bitmap3 = new byte[4];
-		random.nextBytes(secret3);
-		random.nextBytes(bitmap3);
-		TemporarySecret s3 = new TemporarySecret(contactId, transportId, epoch,
-				alice, 2, secret3, outgoing3, centre3, bitmap3);
-		byte[] secret4 = new byte[32], bitmap4 = new byte[4];
-		random.nextBytes(secret4);
-		random.nextBytes(bitmap4);
-		TemporarySecret s4 = new TemporarySecret(contactId, transportId, epoch,
-				alice, 3, secret4, outgoing4, centre4, bitmap4);
+	public void testTransportKeys() throws Exception {
+		TransportKeys keys = createTransportKeys();
 
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Initially there should be no secrets in the database
-		assertEquals(Collections.emptyList(), db.getSecrets(txn));
+		// Initially there should be no transport keys in the database
+		assertEquals(Collections.emptyMap(),
+				db.getTransportKeys(txn, transportId));
 
-		// Add the contact, the transport, the endpoint and the first three
-		// secrets (periods 0, 1 and 2)
+		// Add the contact, the transport and the transport keys
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addTransport(txn, transportId, latency);
-		db.addEndpoint(txn, ep);
-		db.addSecrets(txn, Arrays.asList(s1, s2, s3));
-
-		// Retrieve the first three secrets
-		Collection<TemporarySecret> secrets = db.getSecrets(txn);
-		assertEquals(3, secrets.size());
-		boolean foundFirst = false, foundSecond = false, foundThird = false;
-		for (TemporarySecret s : secrets) {
-			assertEquals(contactId, s.getContactId());
-			assertEquals(transportId, s.getTransportId());
-			assertEquals(epoch, s.getEpoch());
-			assertEquals(alice, s.getAlice());
-			if (s.getPeriod() == 0) {
-				assertArrayEquals(secret1, s.getSecret());
-				assertEquals(outgoing1, s.getOutgoingStreamCounter());
-				assertEquals(centre1, s.getWindowCentre());
-				assertArrayEquals(bitmap1, s.getWindowBitmap());
-				foundFirst = true;
-			} else if (s.getPeriod() == 1) {
-				assertArrayEquals(secret2, s.getSecret());
-				assertEquals(outgoing2, s.getOutgoingStreamCounter());
-				assertEquals(centre2, s.getWindowCentre());
-				assertArrayEquals(bitmap2, s.getWindowBitmap());
-				foundSecond = true;
-			} else if (s.getPeriod() == 2) {
-				assertArrayEquals(secret3, s.getSecret());
-				assertEquals(outgoing3, s.getOutgoingStreamCounter());
-				assertEquals(centre3, s.getWindowCentre());
-				assertArrayEquals(bitmap3, s.getWindowBitmap());
-				foundThird = true;
-			} else {
-				fail();
-			}
-		}
-		assertTrue(foundFirst);
-		assertTrue(foundSecond);
-		assertTrue(foundThird);
-
-		// Adding the fourth secret (period 3) should delete the first
-		db.addSecrets(txn, Arrays.asList(s4));
-		secrets = db.getSecrets(txn);
-		assertEquals(3, secrets.size());
-		foundSecond = foundThird = false;
-		boolean foundFourth = false;
-		for (TemporarySecret s : secrets) {
-			assertEquals(contactId, s.getContactId());
-			assertEquals(transportId, s.getTransportId());
-			assertEquals(epoch, s.getEpoch());
-			assertEquals(alice, s.getAlice());
-			if (s.getPeriod() == 1) {
-				assertArrayEquals(secret2, s.getSecret());
-				assertEquals(outgoing2, s.getOutgoingStreamCounter());
-				assertEquals(centre2, s.getWindowCentre());
-				assertArrayEquals(bitmap2, s.getWindowBitmap());
-				foundSecond = true;
-			} else if (s.getPeriod() == 2) {
-				assertArrayEquals(secret3, s.getSecret());
-				assertEquals(outgoing3, s.getOutgoingStreamCounter());
-				assertEquals(centre3, s.getWindowCentre());
-				assertArrayEquals(bitmap3, s.getWindowBitmap());
-				foundThird = true;
-			} else if (s.getPeriod() == 3) {
-				assertArrayEquals(secret4, s.getSecret());
-				assertEquals(outgoing4, s.getOutgoingStreamCounter());
-				assertEquals(centre4, s.getWindowCentre());
-				assertArrayEquals(bitmap4, s.getWindowBitmap());
-				foundFourth = true;
-			} else {
-				fail();
-			}
-		}
-		assertTrue(foundSecond);
-		assertTrue(foundThird);
-		assertTrue(foundFourth);
-
-		// Removing the contact should remove the secrets
+		db.addTransport(txn, transportId, 123);
+		db.addTransportKeys(txn, contactId, keys);
+
+		// Retrieve the transport keys
+		Map<ContactId, TransportKeys> newKeys =
+				db.getTransportKeys(txn, transportId);
+		assertEquals(1, newKeys.size());
+		Entry<ContactId, TransportKeys> e =
+				newKeys.entrySet().iterator().next();
+		assertEquals(contactId, e.getKey());
+		TransportKeys k = e.getValue();
+		assertEquals(transportId, k.getTransportId());
+		assertKeysEquals(keys.getPreviousIncomingKeys(),
+				k.getPreviousIncomingKeys());
+		assertKeysEquals(keys.getCurrentIncomingKeys(),
+				k.getCurrentIncomingKeys());
+		assertKeysEquals(keys.getNextIncomingKeys(),
+				k.getNextIncomingKeys());
+		assertKeysEquals(keys.getCurrentOutgoingKeys(),
+				k.getCurrentOutgoingKeys());
+
+		// Removing the contact should remove the transport keys
 		db.removeContact(txn, contactId);
-		assertEquals(Collections.emptyList(), db.getSecrets(txn));
+		assertEquals(Collections.emptyMap(),
+				db.getTransportKeys(txn, transportId));
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
-	@Test
-	public void testIncrementStreamCounter() throws Exception {
-		// Create an endpoint and a temporary secret
-		long epoch = 123;
-		int latency = 234;
-		boolean alice = false;
-		long period = 345, outgoing = 456, centre = 567;
-		Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
-		Random random = new Random();
-		byte[] secret = new byte[32], bitmap = new byte[4];
-		random.nextBytes(secret);
-		TemporarySecret s = new TemporarySecret(contactId, transportId, epoch,
-				alice, period, secret, outgoing, centre, bitmap);
-
-		Database<Connection> db = open(false);
-		Connection txn = db.startTransaction();
-
-		// Add the contact, transport, endpoint and temporary secret
-		db.addLocalAuthor(txn, localAuthor);
-		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addTransport(txn, transportId, latency);
-		db.addEndpoint(txn, ep);
-		db.addSecrets(txn, Arrays.asList(s));
-
-		// Retrieve the secret
-		Collection<TemporarySecret> secrets = db.getSecrets(txn);
-		assertEquals(1, secrets.size());
-		s = secrets.iterator().next();
-		assertEquals(contactId, s.getContactId());
-		assertEquals(transportId, s.getTransportId());
-		assertEquals(period, s.getPeriod());
-		assertArrayEquals(secret, s.getSecret());
-		assertEquals(outgoing, s.getOutgoingStreamCounter());
-		assertEquals(centre, s.getWindowCentre());
-		assertArrayEquals(bitmap, s.getWindowBitmap());
-
-		// Increment the stream counter twice and retrieve the secret again
-		assertEquals(outgoing, db.incrementStreamCounter(txn,
-				s.getContactId(), s.getTransportId(), s.getPeriod()));
-		assertEquals(outgoing + 1, db.incrementStreamCounter(txn,
-				s.getContactId(), s.getTransportId(), s.getPeriod()));
-		secrets = db.getSecrets(txn);
-		assertEquals(1, secrets.size());
-		s = secrets.iterator().next();
-		assertEquals(contactId, s.getContactId());
-		assertEquals(transportId, s.getTransportId());
-		assertEquals(period, s.getPeriod());
-		assertArrayEquals(secret, s.getSecret());
-		assertEquals(outgoing + 2, s.getOutgoingStreamCounter());
-		assertEquals(centre, s.getWindowCentre());
-		assertArrayEquals(bitmap, s.getWindowBitmap());
+	private void assertKeysEquals(IncomingKeys expected, IncomingKeys actual) {
+		assertArrayEquals(expected.getTagKey().getBytes(),
+				actual.getTagKey().getBytes());
+		assertArrayEquals(expected.getHeaderKey().getBytes(),
+				actual.getHeaderKey().getBytes());
+		assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
+		assertEquals(expected.getWindowBase(), actual.getWindowBase());
+		assertArrayEquals(expected.getWindowBitmap(), actual.getWindowBitmap());
+	}
 
-		db.commitTransaction(txn);
-		db.close();
+	private void assertKeysEquals(OutgoingKeys expected, OutgoingKeys actual) {
+		assertArrayEquals(expected.getTagKey().getBytes(),
+				actual.getTagKey().getBytes());
+		assertArrayEquals(expected.getHeaderKey().getBytes(),
+				actual.getHeaderKey().getBytes());
+		assertEquals(expected.getRotationPeriod(), actual.getRotationPeriod());
+		assertEquals(expected.getStreamCounter(), actual.getStreamCounter());
 	}
 
 	@Test
-	public void testSetReorderingWindow() throws Exception {
-		// Create an endpoint and a temporary secret
-		long epoch = 123;
-		int latency = 234;
-		boolean alice = false;
-		long period = 345, outgoing = 456, centre = 567;
-		Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
-		Random random = new Random();
-		byte[] secret = new byte[32], bitmap = new byte[4];
-		random.nextBytes(secret);
-		TemporarySecret s = new TemporarySecret(contactId, transportId, epoch,
-				alice, period, secret, outgoing, centre, bitmap);
+	public void testIncrementStreamCounter() throws Exception {
+		TransportKeys keys = createTransportKeys();
+		long rotationPeriod = keys.getCurrentOutgoingKeys().getRotationPeriod();
+		long streamCounter = keys.getCurrentOutgoingKeys().getStreamCounter();
 
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add the contact, transport, endpoint and temporary secret
+		// Add the contact, transport and transport keys
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addTransport(txn, transportId, latency);
-		db.addEndpoint(txn, ep);
-		db.addSecrets(txn, Arrays.asList(s));
-
-		// Retrieve the secret
-		Collection<TemporarySecret> secrets = db.getSecrets(txn);
-		assertEquals(1, secrets.size());
-		s = secrets.iterator().next();
-		assertEquals(contactId, s.getContactId());
-		assertEquals(transportId, s.getTransportId());
-		assertEquals(period, s.getPeriod());
-		assertArrayEquals(secret, s.getSecret());
-		assertEquals(outgoing, s.getOutgoingStreamCounter());
-		assertEquals(centre, s.getWindowCentre());
-		assertArrayEquals(bitmap, s.getWindowBitmap());
-
-		// Update the reordering window and retrieve the secret again
-		random.nextBytes(bitmap);
-		db.setReorderingWindow(txn, contactId, transportId, period, centre,
-				bitmap);
-		secrets = db.getSecrets(txn);
-		assertEquals(1, secrets.size());
-		s = secrets.iterator().next();
-		assertEquals(contactId, s.getContactId());
-		assertEquals(transportId, s.getTransportId());
-		assertEquals(period, s.getPeriod());
-		assertArrayEquals(secret, s.getSecret());
-		assertEquals(outgoing, s.getOutgoingStreamCounter());
-		assertEquals(centre, s.getWindowCentre());
-		assertArrayEquals(bitmap, s.getWindowBitmap());
-
-		// Updating a nonexistent window should not throw an exception
-		db.setReorderingWindow(txn, contactId, transportId, period + 1, 1,
-				bitmap);
-		// The nonexistent window should not have been created
-		secrets = db.getSecrets(txn);
-		assertEquals(1, secrets.size());
-		s = secrets.iterator().next();
-		assertEquals(contactId, s.getContactId());
-		assertEquals(transportId, s.getTransportId());
-		assertEquals(period, s.getPeriod());
-		assertArrayEquals(secret, s.getSecret());
-		assertEquals(outgoing, s.getOutgoingStreamCounter());
-		assertEquals(centre, s.getWindowCentre());
-		assertArrayEquals(bitmap, s.getWindowBitmap());
+		db.addTransport(txn, transportId, 123);
+		db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
+
+		// Increment the stream counter twice and retrieve the transport keys
+		db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
+		db.incrementStreamCounter(txn, contactId, transportId, rotationPeriod);
+		Map<ContactId, TransportKeys> newKeys =
+				db.getTransportKeys(txn, transportId);
+		assertEquals(1, newKeys.size());
+		Entry<ContactId, TransportKeys> e =
+				newKeys.entrySet().iterator().next();
+		assertEquals(contactId, e.getKey());
+		TransportKeys k = e.getValue();
+		assertEquals(transportId, k.getTransportId());
+		OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
+		assertEquals(rotationPeriod, outCurr.getRotationPeriod());
+		assertEquals(streamCounter + 2, outCurr.getStreamCounter());
 
 		db.commitTransaction(txn);
 		db.close();
 	}
 
 	@Test
-	public void testEndpoints() throws Exception {
-		// Create some endpoints
-		long epoch1 = 123, epoch2 = 234;
-		int latency1 = 345, latency2 = 456;
-		boolean alice1 = true, alice2 = false;
-		TransportId transportId1 = new TransportId("bar");
-		TransportId transportId2 = new TransportId("baz");
-		Endpoint ep1 = new Endpoint(contactId, transportId1, epoch1, alice1);
-		Endpoint ep2 = new Endpoint(contactId, transportId2, epoch2, alice2);
+	public void testSetReorderingWindow() throws Exception {
+		TransportKeys keys = createTransportKeys();
+		long rotationPeriod = keys.getCurrentIncomingKeys().getRotationPeriod();
+		long base = keys.getCurrentIncomingKeys().getWindowBase();
+		byte[] bitmap = keys.getCurrentIncomingKeys().getWindowBitmap();
 
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Initially there should be no endpoints in the database
-		assertEquals(Collections.emptyList(), db.getEndpoints(txn));
-
-		// Add the contact, the transports and the endpoints
+		// Add the contact, transport and transport keys
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.addTransport(txn, transportId1, latency1);
-		db.addTransport(txn, transportId2, latency2);
-		db.addEndpoint(txn, ep1);
-		db.addEndpoint(txn, ep2);
-
-		// Retrieve the endpoints
-		Collection<Endpoint> endpoints = db.getEndpoints(txn);
-		assertEquals(2, endpoints.size());
-		boolean foundFirst = false, foundSecond = false;
-		for (Endpoint ep : endpoints) {
-			assertEquals(contactId, ep.getContactId());
-			if (ep.getTransportId().equals(transportId1)) {
-				assertEquals(epoch1, ep.getEpoch());
-				assertEquals(alice1, ep.getAlice());
-				foundFirst = true;
-			} else if (ep.getTransportId().equals(transportId2)) {
-				assertEquals(epoch2, ep.getEpoch());
-				assertEquals(alice2, ep.getAlice());
-				foundSecond = true;
-			} else {
-				fail();
-			}
-		}
-		assertTrue(foundFirst);
-		assertTrue(foundSecond);
+		db.addTransport(txn, transportId, 123);
+		db.updateTransportKeys(txn, Collections.singletonMap(contactId, keys));
 
-		// Removing the contact should remove the endpoints
-		db.removeContact(txn, contactId);
-		assertEquals(Collections.emptyList(), db.getEndpoints(txn));
+		// Update the reordering window and retrieve the transport keys
+		random.nextBytes(bitmap);
+		db.setReorderingWindow(txn, contactId, transportId, rotationPeriod,
+				base + 1, bitmap);
+		Map<ContactId, TransportKeys> newKeys =
+				db.getTransportKeys(txn, transportId);
+		assertEquals(1, newKeys.size());
+		Entry<ContactId, TransportKeys> e =
+				newKeys.entrySet().iterator().next();
+		assertEquals(contactId, e.getKey());
+		TransportKeys k = e.getValue();
+		assertEquals(transportId, k.getTransportId());
+		IncomingKeys inCurr = k.getCurrentIncomingKeys();
+		assertEquals(rotationPeriod, inCurr.getRotationPeriod());
+		assertEquals(base + 1, inCurr.getWindowBase());
+		assertArrayEquals(bitmap, inCurr.getWindowBitmap());
 
 		db.commitTransaction(txn);
 		db.close();
@@ -1431,27 +1262,30 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		assertEquals(contactId1, db.addContact(txn, author1, localAuthorId));
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
-		db.setGroups(txn, contactId1, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
+		db.setGroups(txn, contactId1, Collections.singletonList(group), 1);
 
 		// The group should be available
 		assertEquals(Collections.emptyList(), db.getGroups(txn));
-		assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
+		assertEquals(Collections.singletonList(group),
+				db.getAvailableGroups(txn));
 
 		// Subscribe to the group - it should no longer be available
 		db.addGroup(txn, group);
-		assertEquals(Arrays.asList(group), db.getGroups(txn));
+		assertEquals(Collections.singletonList(group), db.getGroups(txn));
 		assertEquals(Collections.emptyList(), db.getAvailableGroups(txn));
 
 		// Unsubscribe from the group - it should be available again
 		db.removeGroup(txn, groupId);
 		assertEquals(Collections.emptyList(), db.getGroups(txn));
-		assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
+		assertEquals(Collections.singletonList(group),
+				db.getAvailableGroups(txn));
 
 		// The first contact unsubscribes - it should still be available
 		db.setGroups(txn, contactId, Collections.<Group>emptyList(), 2);
 		assertEquals(Collections.emptyList(), db.getGroups(txn));
-		assertEquals(Arrays.asList(group), db.getAvailableGroups(txn));
+		assertEquals(Collections.singletonList(group),
+				db.getAvailableGroups(txn));
 
 		// The second contact unsubscribes - it should no longer be available
 		db.setGroups(txn, contactId1, Collections.<Group>emptyList(), 2);
@@ -1501,9 +1335,8 @@ public class H2DatabaseTest extends BriarTestCase {
 				db.getInboxMessageHeaders(txn, contactId));
 
 		// Add a message to the inbox group - the header should be returned
-		boolean local = true, seen = false;
-		db.addMessage(txn, message, local);
-		db.addStatus(txn, contactId, messageId, false, seen);
+		db.addMessage(txn, message, true);
+		db.addStatus(txn, contactId, messageId, false, false);
 		Collection<MessageHeader> headers =
 				db.getInboxMessageHeaders(txn, contactId);
 		assertEquals(1, headers.size());
@@ -1514,7 +1347,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(localAuthor, header.getAuthor());
 		assertEquals(contentType, header.getContentType());
 		assertEquals(timestamp, header.getTimestamp());
-		assertEquals(local, header.isLocal());
+		assertEquals(true, header.isLocal());
 		assertEquals(false, header.isRead());
 		assertEquals(STORED, header.getStatus());
 		assertFalse(header.isRead());
@@ -1560,7 +1393,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		// Add a contact who subscribes to a group
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
-		db.setGroups(txn, contactId, Arrays.asList(group), 1);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 1);
 
 		// Subscribe to the group and make it visible to the contact
 		db.addGroup(txn, group);
@@ -1571,7 +1404,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.addStatus(txn, contactId, messageId, false, false);
 		Collection<MessageId> sendable = db.getMessagesToSend(txn, contactId,
 				ONE_MEGABYTE);
-		assertEquals(Arrays.asList(messageId), sendable);
+		assertEquals(Collections.singletonList(messageId), sendable);
 
 		// Mark the message as seen - it should no longer be sendable
 		db.raiseSeenFlag(txn, contactId, messageId);
@@ -1584,9 +1417,9 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertEquals(Collections.emptyList(), sendable);
 
 		// The contact resubscribes - the message should be sendable again
-		db.setGroups(txn, contactId, Arrays.asList(group), 3);
+		db.setGroups(txn, contactId, Collections.singletonList(group), 3);
 		sendable = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
-		assertEquals(Arrays.asList(messageId), sendable);
+		assertEquals(Collections.singletonList(messageId), sendable);
 
 		db.commitTransaction(txn);
 		db.close();
@@ -1616,6 +1449,26 @@ public class H2DatabaseTest extends BriarTestCase {
 		return db;
 	}
 
+	private TransportKeys createTransportKeys() {
+		SecretKey inPrevTagKey = TestUtils.createSecretKey();
+		SecretKey inPrevHeaderKey = TestUtils.createSecretKey();
+		IncomingKeys inPrev = new IncomingKeys(inPrevTagKey, inPrevHeaderKey,
+				1, 123, new byte[4]);
+		SecretKey inCurrTagKey = TestUtils.createSecretKey();
+		SecretKey inCurrHeaderKey = TestUtils.createSecretKey();
+		IncomingKeys inCurr = new IncomingKeys(inCurrTagKey, inCurrHeaderKey,
+				2, 234, new byte[4]);
+		SecretKey inNextTagKey = TestUtils.createSecretKey();
+		SecretKey inNextHeaderKey = TestUtils.createSecretKey();
+		IncomingKeys inNext = new IncomingKeys(inNextTagKey, inNextHeaderKey,
+				3, 345, new byte[4]);
+		SecretKey outCurrTagKey = TestUtils.createSecretKey();
+		SecretKey outCurrHeaderKey = TestUtils.createSecretKey();
+		OutgoingKeys outCurr = new OutgoingKeys(outCurrTagKey, outCurrHeaderKey,
+				2, 456);
+		return new TransportKeys(transportId, inPrev, inCurr, inNext, outCurr);
+	}
+
 	@After
 	public void tearDown() {
 		TestUtils.deleteTestDirectory(testDir);
diff --git a/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
index 2c43be177e217aa9c6ff8273be1e6b69eeaee020..5aad3b5c0899cb7bb0ceef8aa254f72c438cedc4 100644
--- a/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
@@ -13,7 +13,8 @@ import org.briarproject.api.AuthorId;
 import org.briarproject.api.ContactId;
 import org.briarproject.api.LocalAuthor;
 import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.KeyManager;
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
@@ -29,11 +30,11 @@ import org.briarproject.api.messaging.PacketReader;
 import org.briarproject.api.messaging.PacketReaderFactory;
 import org.briarproject.api.messaging.PacketWriter;
 import org.briarproject.api.messaging.PacketWriterFactory;
-import org.briarproject.api.transport.Endpoint;
+import org.briarproject.api.transport.KeyManager;
 import org.briarproject.api.transport.StreamContext;
 import org.briarproject.api.transport.StreamReaderFactory;
 import org.briarproject.api.transport.StreamWriterFactory;
-import org.briarproject.api.transport.TagRecogniser;
+import org.briarproject.api.transport.TransportKeys;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.data.DataModule;
 import org.briarproject.db.DatabaseModule;
@@ -49,7 +50,7 @@ import java.io.ByteArrayOutputStream;
 import java.io.File;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.util.Random;
+import java.util.Collections;
 
 import static org.briarproject.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.messaging.MessagingConstants.GROUP_SALT_LENGTH;
@@ -63,26 +64,18 @@ import static org.junit.Assert.assertTrue;
 public class SimplexMessagingIntegrationTest extends BriarTestCase {
 
 	private static final int MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
-	private static final int ROTATION_PERIOD =
-			MAX_CLOCK_DIFFERENCE + MAX_LATENCY;
+	private static final long ROTATION_PERIOD_LENGTH =
+			MAX_LATENCY + MAX_CLOCK_DIFFERENCE;
 
 	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 byte[] initialSecret;
-	private final long epoch;
+	private final TransportId transportId = new TransportId("id");
+	private final SecretKey master = TestUtils.createSecretKey();
+	private final long timestamp = System.currentTimeMillis();
 
 	private Injector alice, bob;
 
-	public SimplexMessagingIntegrationTest() throws Exception {
-		transportId = new TransportId("id");
-		// Create matching secrets for Alice and Bob
-		initialSecret = new byte[32];
-		new Random().nextBytes(initialSecret);
-		epoch = System.currentTimeMillis() - 2 * ROTATION_PERIOD;
-	}
-
 	@Before
 	public void setUp() {
 		testDir.mkdirs();
@@ -125,14 +118,17 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		Group group = gf.createGroup("Group", new byte[GROUP_SALT_LENGTH]);
 		db.addGroup(group);
 		db.setInboxGroup(contactId, group);
-		// Add the transport and the endpoint
+		// Add the transport
 		db.addTransport(transportId, MAX_LATENCY);
-		Endpoint ep = new Endpoint(contactId, transportId, epoch, true);
-		db.addEndpoint(ep);
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
+		// Derive and store the transport keys
+		long rotationPeriod = timestamp / ROTATION_PERIOD_LENGTH;
+		CryptoComponent crypto = alice.getInstance(CryptoComponent.class);
+		TransportKeys keys = crypto.deriveTransportKeys(transportId, master,
+				rotationPeriod, true);
+		db.addTransportKeys(contactId, keys);
+		keyManager.contactAdded(contactId, Collections.singletonList(keys));
 		// Send Bob a message
 		String contentType = "text/plain";
-		long timestamp = System.currentTimeMillis();
 		byte[] body = "Hi Bob!".getBytes("UTF-8");
 		MessageFactory messageFactory = alice.getInstance(MessageFactory.class);
 		Message message = messageFactory.createAnonymousMessage(null, group,
@@ -166,7 +162,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		return out.toByteArray();
 	}
 
-	private void read(byte[] b) throws Exception {
+	private void read(byte[] stream) throws Exception {
 		// Open Bob's database
 		DatabaseComponent db = bob.getInstance(DatabaseComponent.class);
 		assertFalse(db.open());
@@ -188,21 +184,24 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		Group group = gf.createGroup("Group", new byte[GROUP_SALT_LENGTH]);
 		db.addGroup(group);
 		db.setInboxGroup(contactId, group);
-		// Add the transport and the endpoint
+		// Add the transport
 		db.addTransport(transportId, MAX_LATENCY);
-		Endpoint ep = new Endpoint(contactId, transportId, epoch, false);
-		db.addEndpoint(ep);
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
+		// Derive and store the transport keys
+		long rotationPeriod = timestamp / ROTATION_PERIOD_LENGTH;
+		CryptoComponent crypto = bob.getInstance(CryptoComponent.class);
+		TransportKeys keys = crypto.deriveTransportKeys(transportId, master,
+				rotationPeriod, false);
+		db.addTransportKeys(contactId, keys);
+		keyManager.contactAdded(contactId, Collections.singletonList(keys));
 		// Set up an event listener
 		MessageListener listener = new MessageListener();
 		bob.getInstance(EventBus.class).addListener(listener);
-		// Create a tag recogniser and recognise the tag
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		TagRecogniser rec = bob.getInstance(TagRecogniser.class);
+		// Read and recognise the tag
+		ByteArrayInputStream in = new ByteArrayInputStream(stream);
 		byte[] tag = new byte[TAG_LENGTH];
 		int read = in.read(tag);
 		assertEquals(tag.length, read);
-		StreamContext ctx = rec.recogniseTag(transportId, tag);
+		StreamContext ctx = keyManager.recogniseTag(transportId, tag);
 		assertNotNull(ctx);
 		// Create a stream reader
 		StreamReaderFactory streamReaderFactory =
diff --git a/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java b/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java
index 210bd4b97ba92f73868731084306465d78f000d1..d55771fcfd00d09f4e5ae479ce022947d80af830 100644
--- a/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java
@@ -1,600 +1,14 @@
 package org.briarproject.transport;
 
 import org.briarproject.BriarTestCase;
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
-import org.briarproject.api.system.Clock;
-import org.briarproject.api.system.Timer;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.TagRecogniser;
-import org.briarproject.api.transport.TemporarySecret;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
 import org.junit.Test;
 
-import java.util.Arrays;
-import java.util.Collections;
-
-import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 public class KeyManagerImplTest extends BriarTestCase {
 
-	private static final long EPOCH = 1000L * 1000L * 1000L * 1000L;
-	private static final int MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
-	private static final int ROTATION_PERIOD =
-			MAX_CLOCK_DIFFERENCE + MAX_LATENCY;
-
-	private final ContactId contactId;
-	private final TransportId transportId;
-	private final byte[] secret0, secret1, secret2, secret3, secret4;
-	private final byte[] initialSecret;
-
-	public KeyManagerImplTest() {
-		contactId = new ContactId(234);
-		transportId = new TransportId("id");
-		secret0 = new byte[32];
-		secret1 = new byte[32];
-		secret2 = new byte[32];
-		secret3 = new byte[32];
-		secret4 = new byte[32];
-		for (int i = 0; i < secret0.length; i++) secret0[i] = 1;
-		for (int i = 0; i < secret1.length; i++) secret1[i] = 2;
-		for (int i = 0; i < secret2.length; i++) secret2[i] = 3;
-		for (int i = 0; i < secret3.length; i++) secret3[i] = 4;
-		for (int i = 0; i < secret4.length; i++) secret4[i] = 5;
-		initialSecret = new byte[32];
-		for (int i = 0; i < initialSecret.length; i++) initialSecret[i] = 123;
-	}
-
 	@Test
-	public void testStartAndStop() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.emptyMap()));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testEndpointAdded() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The secrets for periods 0 - 2 should be derived
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// endpointAdded() during rotation period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0));
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(db).addSecrets(Arrays.asList(s0, s1, s2));
-			// The secrets for periods 0 - 2 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s0);
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testEndpointAddedAndGetConnectionContext() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The secrets for periods 0 - 2 should be derived
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// endpointAdded() during rotation period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0));
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(db).addSecrets(Arrays.asList(s0, s1, s2));
-			// The secrets for periods 0 - 2 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s0);
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			// getConnectionContext()
-			oneOf(db).incrementStreamCounter(contactId, transportId, 1);
-			will(returnValue(0L));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
-		StreamContext ctx =
-				keyManager.getStreamContext(contactId, transportId);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret1, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(true, ctx.getAlice());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAtEpoch() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the epoch, the start of period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			// The secrets for periods 0 - 2 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s0);
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAtStartOfPeriod2() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-		// The secret for period 3 should be derived and stored
-		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the start of period 2
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + ROTATION_PERIOD));
-			// The secret for period 3 should be derived and stored
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3));
-			oneOf(db).addSecrets(Arrays.asList(s3));
-			// The secrets for periods 1 - 3 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			oneOf(tagRecogniser).addSecret(s3);
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAtEndOfPeriod3() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-		// The secrets for periods 3 and 4 should be derived and stored
-		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
-		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the end of period 3
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + 3 * ROTATION_PERIOD - 1));
-			// The secrets for periods 3 and 4 should be derived from secret 1
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3));
-			oneOf(crypto).deriveNextSecret(secret3, 4);
-			will(returnValue(secret4));
-			// The new secrets should be stored
-			oneOf(db).addSecrets(Arrays.asList(s3, s4));
-			// The secrets for periods 2 - 4 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s2);
-			oneOf(tagRecogniser).addSecret(s3);
-			oneOf(tagRecogniser).addSecret(s4);
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAndRotateInSamePeriod() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the epoch, the start of period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			// The secrets for periods 0 - 2 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s0);
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// run() during period 1: the secrets should not be affected
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + 1));
-			// getConnectionContext()
-			oneOf(db).incrementStreamCounter(contactId, transportId, 1);
-			will(returnValue(0L));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.run();
-		StreamContext ctx =
-				keyManager.getStreamContext(contactId, transportId);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret1, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(true, ctx.getAlice());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAndRotateInNextPeriod() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-		// The secret for period 3 should be derived and stored
-		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the epoch, the start of period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			// The secrets for periods 0 - 2 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s0);
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// run() during period 2: the secrets should be rotated
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + ROTATION_PERIOD + 1));
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3));
-			oneOf(tagRecogniser).removeSecret(contactId, transportId, 0);
-			oneOf(db).addSecrets(Arrays.asList(s3));
-			oneOf(tagRecogniser).addSecret(s3);
-			// getConnectionContext()
-			oneOf(db).incrementStreamCounter(contactId, transportId, 2);
-			will(returnValue(0L));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.run();
-		StreamContext ctx =
-				keyManager.getStreamContext(contactId, transportId);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret2, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(true, ctx.getAlice());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAndRotateAWholePeriodLate() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final TagRecogniser tagRecogniser = context.mock(TagRecogniser.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-		// The secrets for periods 3 and 4 should be derived and stored
-		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
-		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the epoch, the start of period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			// The secrets for periods 0 - 2 should be added to the recogniser
-			oneOf(tagRecogniser).addSecret(s0);
-			oneOf(tagRecogniser).addSecret(s1);
-			oneOf(tagRecogniser).addSecret(s2);
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// run() during period 3 (late): the secrets should be rotated
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + 2 * ROTATION_PERIOD + 1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3));
-			oneOf(crypto).deriveNextSecret(secret3, 4);
-			will(returnValue(secret4));
-			oneOf(tagRecogniser).removeSecret(contactId, transportId, 0);
-			oneOf(tagRecogniser).removeSecret(contactId, transportId, 1);
-			oneOf(db).addSecrets(Arrays.asList(s3, s4));
-			oneOf(tagRecogniser).addSecret(s3);
-			oneOf(tagRecogniser).addSecret(s4);
-			// getConnectionContext()
-			oneOf(db).incrementStreamCounter(contactId, transportId, 3);
-			will(returnValue(0L));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-			oneOf(tagRecogniser).removeSecrets();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.run();
-		StreamContext ctx =
-				keyManager.getStreamContext(contactId, transportId);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret3, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(true, ctx.getAlice());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
 	}
 }
diff --git a/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java b/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java
deleted file mode 100644
index 72ec029dd67ab02de7878066f2ef684b9454fdb4..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java
+++ /dev/null
@@ -1,772 +0,0 @@
-package org.briarproject.transport;
-
-import org.briarproject.BriarTestCase;
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.event.EventBus;
-import org.briarproject.api.event.EventListener;
-import org.briarproject.api.system.Clock;
-import org.briarproject.api.system.Timer;
-import org.briarproject.api.transport.Endpoint;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.TagRecogniser;
-import org.briarproject.api.transport.TemporarySecret;
-import org.briarproject.util.ByteUtils;
-import org.hamcrest.Description;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.jmock.api.Action;
-import org.jmock.api.Invocation;
-import org.junit.Test;
-
-import java.util.Arrays;
-import java.util.Collections;
-
-import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
-import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-
-public class KeyRotationIntegrationTest extends BriarTestCase {
-
-	private static final long EPOCH = 1000L * 1000L * 1000L * 1000L;
-	private static final int MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
-	private static final int ROTATION_PERIOD =
-			MAX_CLOCK_DIFFERENCE + MAX_LATENCY;
-
-	private final ContactId contactId;
-	private final TransportId transportId;
-	private final byte[] secret0, secret1, secret2, secret3, secret4;
-	private final byte[] key0, key1, key2, key3, key4;
-	private final SecretKey k0, k1, k2, k3, k4;
-	private final byte[] initialSecret;
-
-	public KeyRotationIntegrationTest() {
-		contactId = new ContactId(234);
-		transportId = new TransportId("id");
-		secret0 = new byte[32];
-		secret1 = new byte[32];
-		secret2 = new byte[32];
-		secret3 = new byte[32];
-		secret4 = new byte[32];
-		for (int i = 0; i < secret0.length; i++) secret0[i] = 1;
-		for (int i = 0; i < secret1.length; i++) secret1[i] = 2;
-		for (int i = 0; i < secret2.length; i++) secret2[i] = 3;
-		for (int i = 0; i < secret3.length; i++) secret3[i] = 4;
-		for (int i = 0; i < secret4.length; i++) secret4[i] = 5;
-		key0 = new byte[32];
-		key1 = new byte[32];
-		key2 = new byte[32];
-		key3 = new byte[32];
-		key4 = new byte[32];
-		k0 = new SecretKey(key0);
-		k1 = new SecretKey(key1);
-		k2 = new SecretKey(key2);
-		k3 = new SecretKey(key3);
-		k4 = new SecretKey(key4);
-		for (int i = 0; i < key0.length; i++) key0[i] = 1;
-		for (int i = 0; i < key1.length; i++) key1[i] = 2;
-		for (int i = 0; i < key2.length; i++) key2[i] = 3;
-		for (int i = 0; i < key3.length; i++) key3[i] = 4;
-		for (int i = 0; i < key4.length; i++) key4[i] = 5;
-		initialSecret = new byte[32];
-		for (int i = 0; i < initialSecret.length; i++) initialSecret[i] = 123;
-	}
-
-	@Test
-	public void testStartAndStop() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.emptyMap()));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testEndpointAdded() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The secrets for periods 0 - 2 should be derived
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// endpointAdded() during rotation period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0));
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(db).addSecrets(Arrays.asList(s0, s1, s2));
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// stop()
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove the listener and stop the timer
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testEndpointAddedAndGetConnectionContext() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The secrets for periods 0 - 2 should be derived
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// endpointAdded() during rotation period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0));
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(db).addSecrets(Arrays.asList(s0, s1, s2));
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// getConnectionContext()
-			oneOf(db).incrementStreamCounter(contactId, transportId, 1);
-			will(returnValue(0L));
-			// stop()
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove the listener and stop the timer
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
-		StreamContext ctx =
-				keyManager.getStreamContext(contactId, transportId);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret1, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(true, ctx.getAlice());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testEndpointAddedAndAcceptConnection() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The secrets for periods 0 - 2 should be derived
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Collections.emptyList()));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// endpointAdded() during rotation period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0));
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(db).addSecrets(Arrays.asList(s0, s1, s2));
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// acceptConnection()
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-					with(16L));
-			will(new EncodeTagAction());
-			oneOf(db).setReorderingWindow(contactId, transportId, 2, 1,
-					new byte[] {0, 1, 0, 0});
-			// stop()
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the updated tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 1; i < 17; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove the listener and stop the timer
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
-		// Recognise the tag for connection 0 in period 2
-		byte[] tag = new byte[TAG_LENGTH];
-		encodeTag(tag, key2, 0);
-		StreamContext ctx = tagRecogniser.recogniseTag(transportId, tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret2, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(true, ctx.getAlice());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAtEpoch() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the epoch, the start of period 1
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH));
-			// The recogniser should derive the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Start the timer
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			// The recogniser should remove the tags for period 0
-			oneOf(crypto).deriveTagKey(secret0, false);
-			will(returnValue(k0));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove the listener and stop the timer
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAtStartOfPeriod2() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-		// The secret for period 3 should be derived and stored
-		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the start of period 2
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + ROTATION_PERIOD));
-			// The secret for period 3 should be derived and stored
-			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1));
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3));
-			oneOf(db).addSecrets(Arrays.asList(s3));
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 3
-			oneOf(crypto).deriveTagKey(secret3, false);
-			will(returnValue(k3));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Start the timer
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			// The recogniser should derive the tags for period 1
-			oneOf(crypto).deriveTagKey(secret1, false);
-			will(returnValue(k1));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should remove the tags for period 3
-			oneOf(crypto).deriveTagKey(secret3, false);
-			will(returnValue(k3));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove the listener and stop the timer
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testLoadSecretsAtEndOfPeriod3() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		final Clock clock = context.mock(Clock.class);
-		final Timer timer = context.mock(Timer.class);
-
-		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
-		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
-				eventBus, tagRecogniser, clock, timer);
-
-		// The DB contains the secrets for periods 0 - 2
-		Endpoint ep = new Endpoint(contactId, transportId, EPOCH, true);
-		final TemporarySecret s0 = new TemporarySecret(ep, 0, secret0);
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1);
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2);
-		// The secrets for periods 3 and 4 should be derived and stored
-		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
-		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4);
-
-		context.checking(new Expectations() {{
-			// start()
-			oneOf(eventBus).addListener(with(any(EventListener.class)));
-			oneOf(db).getSecrets();
-			will(returnValue(Arrays.asList(s0, s1, s2)));
-			oneOf(db).getTransportLatencies();
-			will(returnValue(Collections.singletonMap(transportId,
-					MAX_LATENCY)));
-			// The current time is the end of period 3
-			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + 3 * ROTATION_PERIOD - 1));
-			// The secrets for periods 3 and 4 should be derived from secret 1
-			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2));
-			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3));
-			oneOf(crypto).deriveNextSecret(secret3, 4);
-			will(returnValue(secret4));
-			// The new secrets should be stored
-			oneOf(db).addSecrets(Arrays.asList(s3, s4));
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 3
-			oneOf(crypto).deriveTagKey(secret3, false);
-			will(returnValue(k3));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 4
-			oneOf(crypto).deriveTagKey(secret4, false);
-			will(returnValue(k4));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k4),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Start the timer
-			oneOf(timer).scheduleAtFixedRate(with(keyManager),
-					with(any(long.class)), with(any(long.class)));
-			// stop()
-			// The recogniser should derive the tags for period 2
-			oneOf(crypto).deriveTagKey(secret2, false);
-			will(returnValue(k2));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should remove the tags for period 3
-			oneOf(crypto).deriveTagKey(secret3, false);
-			will(returnValue(k3));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// The recogniser should derive the tags for period 4
-			oneOf(crypto).deriveTagKey(secret4, false);
-			will(returnValue(k4));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k4),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove the listener and stop the timer
-			oneOf(eventBus).removeListener(with(any(EventListener.class)));
-			oneOf(timer).cancel();
-		}});
-
-		assertTrue(keyManager.start());
-		keyManager.stop();
-
-		context.assertIsSatisfied();
-	}
-
-	private void encodeTag(byte[] tag, byte[] rawKey, long streamNumber) {
-		// Encode a fake tag based on the key and stream number
-		System.arraycopy(rawKey, 0, tag, 0, tag.length);
-		ByteUtils.writeUint32(streamNumber, tag, 0);
-	}
-
-	private class EncodeTagAction implements Action {
-
-		public void describeTo(Description description) {
-			description.appendText("Encodes a tag");
-		}
-
-		public Object invoke(Invocation invocation) throws Throwable {
-			byte[] tag = (byte[]) invocation.getParameter(0);
-			SecretKey key = (SecretKey) invocation.getParameter(1);
-			long streamNumber = (Long) invocation.getParameter(2);
-			encodeTag(tag, key.getBytes(), streamNumber);
-			return null;
-		}
-	}
-}
diff --git a/briar-tests/src/org/briarproject/transport/ReorderingWindowTest.java b/briar-tests/src/org/briarproject/transport/ReorderingWindowTest.java
index e0d567069fb269773d25822dc432f6ef63e1b0ab..85ef23a6e86b858ce85cd1059a6d98187d5077cc 100644
--- a/briar-tests/src/org/briarproject/transport/ReorderingWindowTest.java
+++ b/briar-tests/src/org/briarproject/transport/ReorderingWindowTest.java
@@ -1,5 +1,10 @@
 package org.briarproject.transport;
 
+import org.briarproject.BriarTestCase;
+import org.briarproject.api.transport.TransportConstants;
+import org.briarproject.transport.ReorderingWindow.Change;
+import org.junit.Assert;
+import org.junit.Test;
 import org.briarproject.BriarTestCase;
 import org.junit.Test;
 
@@ -13,148 +18,102 @@ import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Random;
+
+import static org.briarproject.api.transport.TransportConstants.REORDERING_WINDOW_SIZE;
+import static org.junit.Assert.assertArrayEquals;
+
 public class ReorderingWindowTest extends BriarTestCase {
 
 	@Test
-	public void testWindowSliding() {
-		ReorderingWindow w = new ReorderingWindow();
-		for (int i = 0; i < 100; i++) {
-			assertFalse(w.isSeen(i));
-			w.setSeen(i);
-			assertTrue(w.isSeen(i));
+	public void testBitmapConversion() {
+		Random random = new Random();
+		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
+		for (int i = 0; i < 1000; i++) {
+			random.nextBytes(bitmap);
+			ReorderingWindow window = new ReorderingWindow(0L, bitmap);
+			assertArrayEquals(bitmap, window.getBitmap());
 		}
 	}
 
 	@Test
-	public void testWindowJumping() {
-		ReorderingWindow w = new ReorderingWindow();
-		for (int i = 0; i < 100; i += 13) {
-			assertFalse(w.isSeen(i));
-			w.setSeen(i);
-			assertTrue(w.isSeen(i));
-		}
+	public void testWindowSlidesWhenFirstElementIsSeen() {
+		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
+		ReorderingWindow window = new ReorderingWindow(0L, bitmap);
+		// Set the first element seen
+		Change change = window.setSeen(0L);
+		// The window should slide by one element
+		assertEquals(1L, window.getBase());
+		assertEquals(Collections.singletonList((long) REORDERING_WINDOW_SIZE), change.getAdded());
+		assertEquals(Collections.singletonList(0L), change.getRemoved());
+		// All elements in the window should be unseen
+		assertArrayEquals(bitmap, window.getBitmap());
 	}
 
 	@Test
-	public void testWindowUpperLimit() {
-		ReorderingWindow w = new ReorderingWindow();
-		// Centre is 0, highest value in window is 15
-		w.setSeen(15);
-		// Centre is 16, highest value in window is 31
-		w.setSeen(31);
-		try {
-			// Centre is 32, highest value in window is 47
-			w.setSeen(48);
-			fail();
-		} catch (IllegalArgumentException expected) {}
-		// Centre is max - 1, highest value in window is max
+	public void testWindowDoesNotSlideWhenElementBelowMidpointIsSeen() {
 		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
-		w = new ReorderingWindow(MAX_32_BIT_UNSIGNED - 1, bitmap);
-		assertFalse(w.isSeen(MAX_32_BIT_UNSIGNED - 1));
-		assertFalse(w.isSeen(MAX_32_BIT_UNSIGNED));
-		// Values greater than max should never be allowed
-		try {
-			w.setSeen(MAX_32_BIT_UNSIGNED + 1);
-			fail();
-		} catch (IllegalArgumentException expected) {}
-		w.setSeen(MAX_32_BIT_UNSIGNED);
-		assertTrue(w.isSeen(MAX_32_BIT_UNSIGNED));
-		// Centre should have moved to max + 1
-		assertEquals(MAX_32_BIT_UNSIGNED + 1, w.getCentre());
-		// The bit corresponding to max should be set
-		byte[] expectedBitmap = new byte[REORDERING_WINDOW_SIZE / 8];
-		expectedBitmap[expectedBitmap.length / 2 - 1] = 1; // 00000001
-		assertArrayEquals(expectedBitmap, w.getBitmap());
-		// Values greater than max should never be allowed even if centre > max
-		try {
-			w.setSeen(MAX_32_BIT_UNSIGNED + 1);
-			fail();
-		} catch (IllegalArgumentException expected) {}
+		ReorderingWindow window = new ReorderingWindow(0L, bitmap);
+		// Set an element below the midpoint seen
+		Change change = window.setSeen(1L);
+		// The window should not slide
+		assertEquals(0L, window.getBase());
+		assertEquals(Collections.emptyList(), change.getAdded());
+		assertEquals(Collections.singletonList(1L), change.getRemoved());
+		// The second element in the window should be seen
+		bitmap[0] = 0x40; // 0100 0000
+		assertArrayEquals(bitmap, window.getBitmap());
 	}
 
 	@Test
-	public void testWindowLowerLimit() {
-		ReorderingWindow w = new ReorderingWindow();
-		// Centre is 0, negative values should never be allowed
-		try {
-			w.setSeen(-1);
-			fail();
-		} catch (IllegalArgumentException expected) {}
-		// Slide the window
-		w.setSeen(15);
-		// Centre is 16, lowest value in window is 0
-		w.setSeen(0);
-		// Slide the window
-		w.setSeen(16);
-		// Centre is 17, lowest value in window is 1
-		w.setSeen(1);
-		try {
-			w.setSeen(0);
-			fail();
-		} catch (IllegalArgumentException expected) {}
-		// Slide the window
-		w.setSeen(25);
-		// Centre is 26, lowest value in window is 10
-		w.setSeen(10);
-		try {
-			w.setSeen(9);
-			fail();
-		} catch (IllegalArgumentException expected) {}
-		// Centre should still be 26
-		assertEquals(26, w.getCentre());
-		// The bits corresponding to 10, 15, 16 and 25 should be set
-		byte[] expectedBitmap = new byte[REORDERING_WINDOW_SIZE / 8];
-		expectedBitmap[0] = (byte) 134; // 10000110
-		expectedBitmap[1] = 1; // 00000001
-		assertArrayEquals(expectedBitmap, w.getBitmap());
+	public void testWindowSlidesWhenElementAboveMidpointIsSeen() {
+		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
+		ReorderingWindow window = new ReorderingWindow(0, bitmap);
+		long aboveMidpoint = REORDERING_WINDOW_SIZE / 2;
+		// Set an element above the midpoint seen
+		Change change = window.setSeen(aboveMidpoint);
+		// The window should slide by one element
+		assertEquals(1L, window.getBase());
+		assertEquals(Collections.singletonList((long) REORDERING_WINDOW_SIZE), change.getAdded());
+		assertEquals(Arrays.asList(0L, aboveMidpoint), change.getRemoved());
+		// The highest element below the midpoint should be seen
+		bitmap[bitmap.length / 2 - 1] = (byte) 0x01; // 0000 0001
+		assertArrayEquals(bitmap, window.getBitmap());
 	}
 
 	@Test
-	public void testCannotSetSeenTwice() {
-		ReorderingWindow w = new ReorderingWindow();
-		w.setSeen(15);
-		try {
-			w.setSeen(15);
-			fail();
-		} catch (IllegalArgumentException expected) {}
+	public void testWindowSlidesUntilLowestElementIsUnseenWhenFirstElementIsSeen() {
+		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
+		ReorderingWindow window = new ReorderingWindow(0L, bitmap);
+		window.setSeen(1L);
+		// Set the first element seen
+		Change change = window.setSeen(0L);
+		// The window should slide by two elements
+		assertEquals(2L, window.getBase());
+		assertEquals(Arrays.asList((long) REORDERING_WINDOW_SIZE,
+				(long) (REORDERING_WINDOW_SIZE + 1)), change.getAdded());
+		assertEquals(Collections.singletonList(0L), change.getRemoved());
+		// All elements in the window should be unseen
+		assertArrayEquals(bitmap, window.getBitmap());
 	}
 
 	@Test
-	public void testGetUnseenStreamNumbers() {
-		ReorderingWindow w = new ReorderingWindow();
-		// Centre is 0; window should cover 0 to 15, inclusive, with none seen
-		Collection<Long> unseen = w.getUnseen();
-		assertEquals(16, unseen.size());
-		for (int i = 0; i < 16; i++) {
-			assertTrue(unseen.contains(Long.valueOf(i)));
-			assertFalse(w.isSeen(i));
-		}
-		w.setSeen(3);
-		w.setSeen(4);
-		// Centre is 5; window should cover 0 to 20, inclusive, with two seen
-		unseen = w.getUnseen();
-		assertEquals(19, unseen.size());
-		for (int i = 0; i < 21; i++) {
-			if (i == 3 || i == 4) {
-				assertFalse(unseen.contains(Long.valueOf(i)));
-				assertTrue(w.isSeen(i));
-			} else {
-				assertTrue(unseen.contains(Long.valueOf(i)));
-				assertFalse(w.isSeen(i));
-			}
-		}
-		w.setSeen(19);
-		// Centre is 20; window should cover 4 to 35, inclusive, with two seen
-		unseen = w.getUnseen();
-		assertEquals(30, unseen.size());
-		for (int i = 4; i < 36; i++) {
-			if (i == 4 || i == 19) {
-				assertFalse(unseen.contains(Long.valueOf(i)));
-				assertTrue(w.isSeen(i));
-			} else {
-				assertTrue(unseen.contains(Long.valueOf(i)));
-				assertFalse(w.isSeen(i));
-			}
-		}
+	public void testWindowSlidesUntilLowestElementIsUnseenWhenElementAboveMidpointIsSeen() {
+		byte[] bitmap = new byte[REORDERING_WINDOW_SIZE / 8];
+		ReorderingWindow window = new ReorderingWindow(0L, bitmap);
+		window.setSeen(1L);
+		long aboveMidpoint = REORDERING_WINDOW_SIZE / 2;
+		// Set an element above the midpoint seen
+		Change change = window.setSeen(aboveMidpoint);
+		// The window should slide by two elements
+		assertEquals(2L, window.getBase());
+		assertEquals(Arrays.asList((long) REORDERING_WINDOW_SIZE,
+				(long) (REORDERING_WINDOW_SIZE + 1)), change.getAdded());
+		assertEquals(Arrays.asList(0L, aboveMidpoint), change.getRemoved());
+		// The second-highest element below the midpoint should be seen
+		bitmap[bitmap.length / 2 - 1] = (byte) 0x02; // 0000 0010
+		assertArrayEquals(bitmap, window.getBitmap());
 	}
 }
diff --git a/briar-tests/src/org/briarproject/transport/TransportKeyManagerTest.java b/briar-tests/src/org/briarproject/transport/TransportKeyManagerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..8183864059dec3645d9c9893dedac0331bfbb8ff
--- /dev/null
+++ b/briar-tests/src/org/briarproject/transport/TransportKeyManagerTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.transport;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class TransportKeyManagerTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}
diff --git a/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java b/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java
deleted file mode 100644
index d6287599f744ea21817236ee60d0092bd33a6a43..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java
+++ /dev/null
@@ -1,130 +0,0 @@
-package org.briarproject.transport;
-
-import org.briarproject.BriarTestCase;
-import org.briarproject.api.ContactId;
-import org.briarproject.api.TransportId;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.db.DatabaseComponent;
-import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.TemporarySecret;
-import org.briarproject.util.ByteUtils;
-import org.hamcrest.Description;
-import org.jmock.Expectations;
-import org.jmock.Mockery;
-import org.jmock.api.Action;
-import org.jmock.api.Invocation;
-import org.junit.Test;
-
-import java.util.Random;
-
-import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
-import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-
-public class TransportTagRecogniserTest extends BriarTestCase {
-
-	private final ContactId contactId = new ContactId(234);
-	private final TransportId transportId = new TransportId("id");
-	private final SecretKey tagKey = new SecretKey(new byte[32]);
-
-	@Test
-	public void testAddAndRemoveSecret() {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final byte[] secret = new byte[32];
-		new Random().nextBytes(secret);
-		final boolean alice = false;
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Add secret
-			oneOf(crypto).deriveTagKey(secret, !alice);
-			will(returnValue(tagKey));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(tagKey),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Remove secret
-			oneOf(crypto).deriveTagKey(secret, !alice);
-			will(returnValue(tagKey));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(tagKey),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-		}});
-		TemporarySecret s = new TemporarySecret(contactId, transportId, 123,
-				alice, 0, secret, 0, 0, new byte[4]);
-		TransportTagRecogniser recogniser =
-				new TransportTagRecogniser(crypto, db, transportId);
-		recogniser.addSecret(s);
-		recogniser.removeSecret(contactId, 0);
-		context.assertIsSatisfied();
-	}
-
-	@Test
-	public void testRecogniseTag() throws Exception {
-		Mockery context = new Mockery();
-		final CryptoComponent crypto = context.mock(CryptoComponent.class);
-		final byte[] secret = new byte[32];
-		new Random().nextBytes(secret);
-		final boolean alice = false;
-		final DatabaseComponent db = context.mock(DatabaseComponent.class);
-		context.checking(new Expectations() {{
-			// Add secret
-			oneOf(crypto).deriveTagKey(secret, !alice);
-			will(returnValue(tagKey));
-			for (int i = 0; i < 16; i++) {
-				oneOf(crypto).encodeTag(with(any(byte[].class)), with(tagKey),
-						with((long) i));
-				will(new EncodeTagAction());
-			}
-			// Recognise tag 0
-			oneOf(crypto).deriveTagKey(secret, !alice);
-			will(returnValue(tagKey));
-			// The window should slide to include tag 16
-			oneOf(crypto).encodeTag(with(any(byte[].class)), with(tagKey),
-					with(16L));
-			will(new EncodeTagAction());
-			// The updated window should be stored
-			oneOf(db).setReorderingWindow(contactId, transportId, 0, 1,
-					new byte[] {0, 1, 0, 0});
-			// Recognise tag again - no expectations
-		}});
-		TemporarySecret s = new TemporarySecret(contactId, transportId, 123,
-				alice, 0, secret, 0, 0, new byte[4]);
-		TransportTagRecogniser recogniser =
-				new TransportTagRecogniser(crypto, db, transportId);
-		recogniser.addSecret(s);
-		// Tag 0 should be expected
-		byte[] tag = new byte[TAG_LENGTH];
-		StreamContext ctx = recogniser.recogniseTag(tag);
-		assertNotNull(ctx);
-		assertEquals(contactId, ctx.getContactId());
-		assertEquals(transportId, ctx.getTransportId());
-		assertArrayEquals(secret, ctx.getSecret());
-		assertEquals(0, ctx.getStreamNumber());
-		assertEquals(alice, ctx.getAlice());
-		// Tag 0 should not be expected again
-		assertNull(recogniser.recogniseTag(tag));
-		context.assertIsSatisfied();
-	}
-
-	private static class EncodeTagAction implements Action {
-
-		public void describeTo(Description description) {
-			description.appendText("Encodes a tag");
-		}
-
-		public Object invoke(Invocation invocation) throws Throwable {
-			byte[] tag = (byte[]) invocation.getParameter(0);
-			long streamNumber = (Long) invocation.getParameter(2);
-			// Encode a fake tag based on the stream number
-			ByteUtils.writeUint32(streamNumber, tag, 0);
-			return null;
-		}
-	}
-}