diff --git a/briar-android/res/values/strings.xml b/briar-android/res/values/strings.xml
index a1ed38a6a66506f77cef757781990df5a2fb9784..3096f600ecc0e7063df19e31fa4a49d41a9223f9 100644
--- a/briar-android/res/values/strings.xml
+++ b/briar-android/res/values/strings.xml
@@ -93,8 +93,8 @@
     </plurals>
     <string name="settings_title">Settings</string>
     <string name="bluetooth_setting_title">BLUETOOTH</string>
-    <string name="bluetooth_setting">Turn on Bluetooth</string>
-    <string name="bluetooth_setting_enabled">While signed in</string>
+    <string name="bluetooth_setting">Connect via Bluetooth</string>
+    <string name="bluetooth_setting_enabled">Whenever contacts are nearby</string>
     <string name="bluetooth_setting_disabled">Only when adding contacts</string>
     <string name="notification_settings_title">NOTIFICATIONS</string>
     <string name="notify_private_messages_setting">Show alerts for private messages</string>
diff --git a/briar-android/src/org/briarproject/android/PasswordActivity.java b/briar-android/src/org/briarproject/android/PasswordActivity.java
index 26ac3dee0938710792bb9389d3081c3cabc0c4c0..727985f1b1a989aed8eaa1fa00857b962334dca2 100644
--- a/briar-android/src/org/briarproject/android/PasswordActivity.java
+++ b/briar-android/src/org/briarproject/android/PasswordActivity.java
@@ -133,10 +133,7 @@ public class PasswordActivity extends RoboActivity {
 		continueButton.setVisibility(GONE);
 		progress.setVisibility(VISIBLE);
 		// Decrypt the database key in a background thread
-		int length = e.length();
-		final char[] password = new char[length];
-		e.getChars(0, length, password, 0);
-		e.delete(0, length);
+		final String password = e.toString();
 		cryptoExecutor.execute(new Runnable() {
 			public void run() {
 				byte[] key = crypto.decryptWithPassword(encrypted, password);
diff --git a/briar-android/src/org/briarproject/android/SetupActivity.java b/briar-android/src/org/briarproject/android/SetupActivity.java
index a71442eeb0cbe4fc841329863542656bd0222625..109ce0cee8077441187b78a7a22da2f9934fe8dd 100644
--- a/briar-android/src/org/briarproject/android/SetupActivity.java
+++ b/briar-android/src/org/briarproject/android/SetupActivity.java
@@ -19,7 +19,6 @@ import static org.briarproject.android.util.CommonLayoutParams.WRAP_WRAP;
 import static org.briarproject.api.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
 import static org.briarproject.api.crypto.PasswordStrengthEstimator.WEAK;
 
-import java.util.Arrays;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -43,7 +42,6 @@ import android.content.Intent;
 import android.content.SharedPreferences;
 import android.content.SharedPreferences.Editor;
 import android.os.Bundle;
-import android.text.Editable;
 import android.view.KeyEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -187,18 +185,16 @@ OnEditorActionListener {
 		else strengthMeter.setVisibility(INVISIBLE);
 		String nickname = nicknameEntry.getText().toString();
 		int nicknameLength = StringUtils.toUtf8(nickname).length;
-		char[] firstPassword = getChars(passwordEntry.getText());
-		char[] secondPassword = getChars(passwordConfirmation.getText());
-		boolean passwordsMatch = Arrays.equals(firstPassword, secondPassword);
+		String firstPassword = passwordEntry.getText().toString();
+		String secondPassword = passwordConfirmation.getText().toString();
+		boolean passwordsMatch = firstPassword.equals(secondPassword);
 		float strength = strengthEstimator.estimateStrength(firstPassword);
-		for(int i = 0; i < firstPassword.length; i++) firstPassword[i] = 0;
-		for(int i = 0; i < secondPassword.length; i++) secondPassword[i] = 0;
 		strengthMeter.setStrength(strength);
 		if(nicknameLength > MAX_AUTHOR_NAME_LENGTH) {
 			feedback.setText(R.string.name_too_long);
-		} else if(firstPassword.length == 0) {
+		} else if(firstPassword.length() == 0) {
 			feedback.setText("");
-		} else if(secondPassword.length == 0 || passwordsMatch) {
+		} else if(secondPassword.length() == 0 || passwordsMatch) {
 			if(strength < PasswordStrengthEstimator.WEAK)
 				feedback.setText(R.string.password_too_weak);
 			else feedback.setText("");
@@ -212,13 +208,6 @@ OnEditorActionListener {
 				&& passwordsMatch && strength >= WEAK);
 	}
 
-	private char[] getChars(Editable e) {
-		int length = e.length();
-		char[] c = new char[length];
-		e.getChars(0, length, c, 0);
-		return c;
-	}
-
 	public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
 		// Hide the soft keyboard
 		Object o = getSystemService(INPUT_METHOD_SERVICE);
@@ -231,18 +220,14 @@ OnEditorActionListener {
 		feedback.setVisibility(GONE);
 		continueButton.setVisibility(GONE);
 		progress.setVisibility(VISIBLE);
-		// Copy the passwords and erase the originals
 		final String nickname = nicknameEntry.getText().toString();
-		final char[] password = getChars(passwordEntry.getText());
-		delete(passwordEntry.getText());
-		delete(passwordConfirmation.getText());
+		final String password = passwordEntry.getText().toString();
 		// Store the DB key and create the identity in a background thread
 		cryptoExecutor.execute(new Runnable() {
 			public void run() {
-				byte[] key = crypto.generateSecretKey().getEncoded();
+				byte[] key = crypto.generateSecretKey().getBytes();
 				databaseConfig.setEncryptionKey(key);
 				byte[] encrypted = encryptDatabaseKey(key, password);
-				for(int i = 0; i < password.length; i++) password[i] = 0;
 				storeEncryptedDatabaseKey(encrypted);
 				LocalAuthor localAuthor = createLocalAuthor(nickname);
 				showDashboard(referenceManager.putReference(localAuthor,
@@ -251,10 +236,6 @@ OnEditorActionListener {
 		});
 	}
 
-	private void delete(Editable e) {
-		e.delete(0, e.length());
-	}
-
 	private void storeEncryptedDatabaseKey(final byte[] encrypted) {
 		long now = System.currentTimeMillis();
 		SharedPreferences prefs = getSharedPreferences("db", MODE_PRIVATE);
@@ -266,7 +247,7 @@ OnEditorActionListener {
 			LOG.info("Key storage took " + duration + " ms");
 	}
 
-	private byte[] encryptDatabaseKey(byte[] key, char[] password) {
+	private byte[] encryptDatabaseKey(byte[] key, String password) {
 		long now = System.currentTimeMillis();
 		byte[] encrypted = crypto.encryptWithPassword(key, password);
 		long duration = System.currentTimeMillis() - now;
diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
index 1b65e45c8717b24e39f9f41baffe93a2614cd6d0..a63bbc7ebbc189a19c7d9a47094709caf5e78b40 100644
--- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
@@ -69,8 +69,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 	private final SecureRandom secureRandom;
 	private final Clock clock;
 	private final DuplexPluginCallback callback;
-	private final int maxFrameLength;
-	private final long maxLatency, pollingInterval;
+	private final int maxLatency, pollingInterval;
 
 	private volatile boolean running = false;
 	private volatile boolean wasDisabled = false;
@@ -82,15 +81,14 @@ class DroidtoothPlugin implements DuplexPlugin {
 
 	DroidtoothPlugin(Executor ioExecutor, AndroidExecutor androidExecutor,
 			Context appContext, SecureRandom secureRandom, Clock clock,
-			DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
-			long pollingInterval) {
+			DuplexPluginCallback callback, int maxLatency,
+			int pollingInterval) {
 		this.ioExecutor = ioExecutor;
 		this.androidExecutor = androidExecutor;
 		this.appContext = appContext;
 		this.secureRandom = secureRandom;
 		this.clock = clock;
 		this.callback = callback;
-		this.maxFrameLength = maxFrameLength;
 		this.maxLatency = maxLatency;
 		this.pollingInterval = pollingInterval;
 	}
@@ -99,12 +97,13 @@ class DroidtoothPlugin implements DuplexPlugin {
 		return ID;
 	}
 
-	public int getMaxFrameLength() {
-		return maxFrameLength;
+	public int getMaxLatency() {
+		return maxLatency;
 	}
 
-	public long getMaxLatency() {
-		return maxLatency;
+	public int getMaxIdleTime() {
+		// Bluetooth detects dead connections so we don't need keepalives
+		return Integer.MAX_VALUE;
 	}
 
 	public boolean start() throws IOException {
@@ -240,7 +239,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 		return true;
 	}
 
-	public long getPollingInterval() {
+	public int getPollingInterval() {
 		return pollingInterval;
 	}
 
@@ -361,6 +360,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 
 	private class BluetoothStateReceiver extends BroadcastReceiver {
 
+		@Override
 		public void onReceive(Context ctx, Intent intent) {
 			int state = intent.getIntExtra(EXTRA_STATE, 0);
 			if(state == STATE_ON) {
diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPluginFactory.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPluginFactory.java
index b7bb2028546aaa3dc508cffdbde4da0ab9bb3835..2b04481c4b6e1f2d0641a734011bf90634d00201 100644
--- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPluginFactory.java
+++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPluginFactory.java
@@ -15,9 +15,8 @@ import android.content.Context;
 
 public class DroidtoothPluginFactory implements DuplexPluginFactory {
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+	private static final int POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
 
 	private final Executor ioExecutor;
 	private final AndroidExecutor androidExecutor;
@@ -41,7 +40,6 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
 
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
 		return new DroidtoothPlugin(ioExecutor, androidExecutor, appContext,
-				secureRandom, clock, callback, MAX_FRAME_LENGTH, MAX_LATENCY,
-				POLLING_INTERVAL);
+				secureRandom, clock, callback, MAX_LATENCY, POLLING_INTERVAL);
 	}
 }
diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java
index d349650735cbd6540378c97777b9e28e013eb8b2..e9b645e1c81d8c4efa31f34ac3467c00518c7968 100644
--- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java
+++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothTransportConnection.java
@@ -39,10 +39,6 @@ class DroidtoothTransportConnection implements DuplexTransportConnection {
 
 	private class Reader implements TransportConnectionReader {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
-		}
-
 		public long getMaxLatency() {
 			return plugin.getMaxLatency();
 		}
@@ -60,12 +56,12 @@ class DroidtoothTransportConnection implements DuplexTransportConnection {
 
 	private class Writer implements TransportConnectionWriter {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
+		public int getMaxLatency() {
+			return plugin.getMaxLatency();
 		}
 
-		public long getMaxLatency() {
-			return plugin.getMaxLatency();
+		public int getMaxIdleTime() {
+			return plugin.getMaxIdleTime();
 		}
 
 		public long getCapacity() {
diff --git a/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java b/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java
index eaec50c41c0873622e5ae4da61fac973c90132e3..a7689bbda817040374156027b5616220025d2f22 100644
--- a/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java
@@ -26,10 +26,9 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 	private volatile BroadcastReceiver networkStateReceiver = null;
 
 	AndroidLanTcpPlugin(Executor ioExecutor, Context appContext,
-			DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
-			long pollingInterval) {
-		super(ioExecutor, callback, maxFrameLength, maxLatency,
-				pollingInterval);
+			DuplexPluginCallback callback, int maxLatency,
+			int maxIdleTime, int pollingInterval) {
+		super(ioExecutor, callback, maxLatency, maxIdleTime, pollingInterval);
 		this.appContext = appContext;
 	}
 
diff --git a/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPluginFactory.java b/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPluginFactory.java
index a51326e55bb441263c69f2191935408d5b1b621f..debad159c47e8d84eb96c11182186b67f2526dbd 100644
--- a/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPluginFactory.java
+++ b/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPluginFactory.java
@@ -11,9 +11,9 @@ import android.content.Context;
 
 public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 60 * 1000; // 1 minute
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+	private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
+	private static final int POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
 
 	private final Executor ioExecutor;
 	private final Context appContext;
@@ -29,6 +29,6 @@ public class AndroidLanTcpPluginFactory implements DuplexPluginFactory {
 
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
 		return new AndroidLanTcpPlugin(ioExecutor, appContext, callback,
-				MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
+				MAX_LATENCY, MAX_IDLE_TIME, POLLING_INTERVAL);
 	}
 }
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
index 905040b5c696f2c20e15f758a084092655785788..0d89832611d713258abc8848756cbe70d2d88285 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
@@ -75,8 +75,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 	private final Context appContext;
 	private final LocationUtils locationUtils;
 	private final DuplexPluginCallback callback;
-	private final int maxFrameLength;
-	private final long maxLatency, pollingInterval;
+	private final int maxLatency, maxIdleTime, pollingInterval, socketTimeout;
 	private final File torDirectory, torFile, geoIpFile, configFile, doneFile;
 	private final File cookieFile, hostnameFile;
 	private final AtomicBoolean circuitBuilt;
@@ -90,14 +89,17 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 
 	TorPlugin(Executor ioExecutor, Context appContext,
 			LocationUtils locationUtils, DuplexPluginCallback callback,
-			int maxFrameLength, long maxLatency, long pollingInterval) {
+			int maxLatency, int maxIdleTime, int pollingInterval) {
 		this.ioExecutor = ioExecutor;
 		this.appContext = appContext;
 		this.locationUtils = locationUtils;
 		this.callback = callback;
-		this.maxFrameLength = maxFrameLength;
 		this.maxLatency = maxLatency;
+		this.maxIdleTime = maxIdleTime;
 		this.pollingInterval = pollingInterval;
+		if(maxIdleTime > Integer.MAX_VALUE / 2)
+			socketTimeout = Integer.MAX_VALUE;
+		else socketTimeout = maxIdleTime * 2;
 		torDirectory = appContext.getDir("tor", MODE_PRIVATE);
 		torFile = new File(torDirectory, "tor");
 		geoIpFile = new File(torDirectory, "geoip");
@@ -112,12 +114,12 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 		return ID;
 	}
 
-	public int getMaxFrameLength() {
-		return maxFrameLength;
+	public int getMaxLatency() {
+		return maxLatency;
 	}
 
-	public long getMaxLatency() {
-		return maxLatency;
+	public int getMaxIdleTime() {
+		return maxIdleTime;
 	}
 
 	public boolean start() throws IOException {
@@ -446,6 +448,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 			Socket s;
 			try {
 				s = ss.accept();
+				s.setSoTimeout(socketTimeout);
 			} catch(IOException e) {
 				// This is expected when the socket is closed
 				if(LOG.isLoggable(INFO)) LOG.info(e.toString());
@@ -494,7 +497,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 		return true;
 	}
 
-	public long getPollingInterval() {
+	public int getPollingInterval() {
 		return pollingInterval;
 	}
 
@@ -529,6 +532,7 @@ class TorPlugin implements DuplexPlugin, EventHandler {
 			Socks5Proxy proxy = new Socks5Proxy("127.0.0.1", SOCKS_PORT);
 			proxy.resolveAddrLocally(false);
 			Socket s = new SocksSocket(proxy, onion, 80);
+			s.setSoTimeout(socketTimeout);
 			if(LOG.isLoggable(INFO)) LOG.info("Connected to " + onion);
 			return new TorTransportConnection(this, s);
 		} catch(IOException e) {
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java b/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java
index b7c08b225a72c1d0dea4c73a09e9f9118216a68b..4861ba6b797188982c9433a4922f8fea107127f5 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPluginFactory.java
@@ -17,9 +17,9 @@ public class TorPluginFactory implements DuplexPluginFactory {
 	private static final Logger LOG =
 			Logger.getLogger(TorPluginFactory.class.getName());
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+	private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
+	private static final int POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
 
 	private final Executor ioExecutor;
 	private final Context appContext;
@@ -43,6 +43,6 @@ public class TorPluginFactory implements DuplexPluginFactory {
 			return null;
 		}
 		return new TorPlugin(ioExecutor,appContext, locationUtils, callback,
-				MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
+				MAX_LATENCY, MAX_IDLE_TIME, POLLING_INTERVAL);
 	}
 }
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java b/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java
index ab0969eae633fd8270cc35bcb33bc8e361884ebf..4640eb1f6be81d3ae0275ed2274141aec2c385b2 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorTransportConnection.java
@@ -38,10 +38,6 @@ class TorTransportConnection implements DuplexTransportConnection {
 
 	private class Reader implements TransportConnectionReader {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
-		}
-
 		public long getMaxLatency() {
 			return plugin.getMaxLatency();
 		}
@@ -59,12 +55,12 @@ class TorTransportConnection implements DuplexTransportConnection {
 
 	private class Writer implements TransportConnectionWriter {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
+		public int getMaxLatency() {
+			return plugin.getMaxLatency();
 		}
 
-		public long getMaxLatency() {
-			return plugin.getMaxLatency();
+		public int getMaxIdleTime() {
+			return plugin.getMaxIdleTime();
 		}
 
 		public long getCapacity() {
diff --git a/briar-api/src/org/briarproject/api/crypto/AuthenticatedCipher.java b/briar-api/src/org/briarproject/api/crypto/AuthenticatedCipher.java
index 1a6d955229509fc9888ff2e11f94eaf5500b8bc3..6f60ed0526c2fe714cb80c7418ee0d3db95b2b69 100644
--- a/briar-api/src/org/briarproject/api/crypto/AuthenticatedCipher.java
+++ b/briar-api/src/org/briarproject/api/crypto/AuthenticatedCipher.java
@@ -2,7 +2,7 @@ package org.briarproject.api.crypto;
 
 import java.security.GeneralSecurityException;
 
-/** An authenticated cipher that support additional authenticated data. */
+/** An authenticated cipher that supports additional authenticated data. */
 public interface AuthenticatedCipher {
 
 	/**
@@ -13,10 +13,10 @@ public interface AuthenticatedCipher {
 			throws GeneralSecurityException;
 
 	/** Encrypts or decrypts data in a single-part operation. */
-	int doFinal(byte[] input, int inputOff, int len, byte[] output,
+	int process(byte[] input, int inputOff, int len, byte[] output,
 			int outputOff) throws GeneralSecurityException;
 
-	/** Returns the length of the message authenticated code (MAC) in bytes. */
+	/** Returns the length of the message authentication code (MAC) in bytes. */
 	int getMacLength();
 
 	/** Returns the block size of the cipher in bytes. */
diff --git a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
index ba9c86224b04d83b6ae67d1da2ed50b7f9f90b83..0f448daef8b84cfcc5a458404dba73e651a76168 100644
--- a/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
+++ b/briar-api/src/org/briarproject/api/crypto/CryptoComponent.java
@@ -65,7 +65,7 @@ public interface CryptoComponent {
 
 	/**
 	 * Derives a tag key from the given temporary secret.
-	 * @param alice indicates whether the key is for connections initiated by
+	 * @param alice indicates whether the key is for streams initiated by
 	 * Alice or Bob.
 	 */
 	SecretKey deriveTagKey(byte[] secret, boolean alice);
@@ -89,7 +89,7 @@ public interface CryptoComponent {
 	 * given password. The ciphertext will be decryptable using the same
 	 * password after the app restarts.
 	 */
-	byte[] encryptWithPassword(byte[] plaintext, char[] password);
+	byte[] encryptWithPassword(byte[] plaintext, String password);
 
 	/**
 	 * Decrypts and authenticates the given ciphertext that has been read from
@@ -97,5 +97,5 @@ public interface CryptoComponent {
 	 * given password. Returns null if the ciphertext cannot be decrypted and
 	 * authenticated (for example, if the password is wrong).
 	 */
-	byte[] decryptWithPassword(byte[] ciphertext, char[] password);
+	byte[] decryptWithPassword(byte[] ciphertext, String password);
 }
diff --git a/briar-api/src/org/briarproject/api/crypto/KeyManager.java b/briar-api/src/org/briarproject/api/crypto/KeyManager.java
index 59e4d7af3083c5d034de0ef0040806a5162944a3..02a43e9706656cd06dace79eb64730f1e2561ed6 100644
--- a/briar-api/src/org/briarproject/api/crypto/KeyManager.java
+++ b/briar-api/src/org/briarproject/api/crypto/KeyManager.java
@@ -16,9 +16,6 @@ public interface KeyManager extends Service {
 	 */
 	StreamContext getStreamContext(ContactId c, TransportId t);
 
-	/**
-	 * Called whenever an endpoint has been added. The initial secret is erased
-	 * before returning.
-	 */
-	void endpointAdded(Endpoint ep, long maxLatency, byte[] initialSecret);
+	/** 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/PasswordStrengthEstimator.java b/briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java
index 9ffa2dc244da42bfe0d4a675721510a640b53754..93d27a6b32aaa142a86cfd4c53f054900cf7fc4d 100644
--- a/briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java
+++ b/briar-api/src/org/briarproject/api/crypto/PasswordStrengthEstimator.java
@@ -12,5 +12,5 @@ public interface PasswordStrengthEstimator {
 	 * Returns an estimate between 0 (weakest) and 1 (strongest), inclusive,
 	 * of the strength of the given password.
 	 */
-	float estimateStrength(char[] password);
+	float estimateStrength(String password);
 }
diff --git a/briar-api/src/org/briarproject/api/crypto/SecretKey.java b/briar-api/src/org/briarproject/api/crypto/SecretKey.java
index 63d1f7661705087cc95ed9050072bba010bd0370..62b75a9452bfbc736179d0abe83050574228d8a8 100644
--- a/briar-api/src/org/briarproject/api/crypto/SecretKey.java
+++ b/briar-api/src/org/briarproject/api/crypto/SecretKey.java
@@ -1,21 +1,15 @@
 package org.briarproject.api.crypto;
 
 /** A secret key used for encryption and/or authentication. */
-public interface SecretKey {
+public class SecretKey {
 
-	/** Returns the encoded representation of this key. */
-	byte[] getEncoded();
+	private final byte[] key;
 
-	/**
-	 * Returns a copy of this key - erasing this key will erase the copy and
-	 * vice versa.
-	 */
-	SecretKey copy();
+	public SecretKey(byte[] key) {
+		this.key = key;
+	}
 
-	/**
-	 * Erases this key from memory. Any copies derived from this key via the
-	 * {@link #copy()} method, and any keys from which this key was derived via
-	 * the {@link #copy()} method, are also erased.
-	 */
-	void erase();
+	public byte[] getBytes() {
+		return key;
+	}
 }
diff --git a/briar-api/src/org/briarproject/api/crypto/StreamDecrypter.java b/briar-api/src/org/briarproject/api/crypto/StreamDecrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..9f217c13861ba4cc6525c7c69ddcc4abcfae3d0f
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/StreamDecrypter.java
@@ -0,0 +1,14 @@
+package org.briarproject.api.crypto;
+
+import java.io.IOException;
+
+public interface StreamDecrypter {
+
+	/**
+	 * Reads a frame, decrypts its payload into the given buffer and returns
+	 * the payload length, or -1 if no more frames can be read from the stream.
+	 * @throws java.io.IOException if an error occurs while reading the frame,
+	 * or if authenticated decryption fails.
+	 */
+	int readFrame(byte[] payload) throws IOException;
+}
diff --git a/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java b/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..8b758ec11feef6c53fe7ef5cbb8ad65ca4a6f6aa
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java
@@ -0,0 +1,17 @@
+package org.briarproject.api.crypto;
+
+import java.io.InputStream;
+
+import org.briarproject.api.transport.StreamContext;
+
+public interface StreamDecrypterFactory {
+
+	/** Creates a {@link StreamDecrypter} for decrypting a transport stream. */
+	StreamDecrypter createStreamDecrypter(InputStream in, StreamContext ctx);
+
+	/**
+	 * Creates a {@link StreamDecrypter} for decrypting an invitation stream.
+	 */
+	StreamDecrypter createInvitationStreamDecrypter(InputStream in,
+			byte[] secret, boolean alice);
+}
diff --git a/briar-api/src/org/briarproject/api/crypto/StreamEncrypter.java b/briar-api/src/org/briarproject/api/crypto/StreamEncrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..a332743bb1924fbca5dbb90b22ec6bbbd2b8eeee
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/StreamEncrypter.java
@@ -0,0 +1,13 @@
+package org.briarproject.api.crypto;
+
+import java.io.IOException;
+
+public interface StreamEncrypter {
+
+	/** Encrypts the given frame and writes it to the stream. */
+	void writeFrame(byte[] payload, int payloadLength, int paddingLength,
+			boolean finalFrame) throws IOException;
+
+	/** Flushes the stream. */
+	void flush() throws IOException;
+}
diff --git a/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java b/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..86df7bbe2439f1f0ea9c6417cd60c8b1911614f3
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java
@@ -0,0 +1,17 @@
+package org.briarproject.api.crypto;
+
+import java.io.OutputStream;
+
+import org.briarproject.api.transport.StreamContext;
+
+public interface StreamEncrypterFactory {
+
+	/** Creates a {@link StreamEncrypter} for encrypting a transport stream. */
+	StreamEncrypter createStreamEncrypter(OutputStream out, StreamContext ctx);
+
+	/**
+	 * Creates a {@link StreamEncrypter} for encrypting an invitation stream.
+	 */
+	StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
+			byte[] secret, boolean alice);
+}
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index e7b55dd6d2bf857e89825fcdc12d7bdff18521e1..5159ac0220bc19863092d67a1fe6b0427c15143c 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -73,7 +73,7 @@ public interface DatabaseComponent {
 	 * Stores a transport and returns true if the transport was not previously
 	 * in the database.
 	 */
-	boolean addTransport(TransportId t, long maxLatency) throws DbException;
+	boolean addTransport(TransportId t, int maxLatency) throws DbException;
 
 	/**
 	 * Returns an acknowledgement for the given contact, or null if there are
@@ -88,14 +88,14 @@ public interface DatabaseComponent {
 	 * sendable messages that fit in the given length.
 	 */
 	Collection<byte[]> generateBatch(ContactId c, int maxLength,
-			long maxLatency) throws DbException;
+			int maxLatency) throws DbException;
 
 	/**
 	 * Returns an offer for the given contact for transmission over a
 	 * transport with the given maximum latency, or null if there are no
 	 * messages to offer.
 	 */
-	Offer generateOffer(ContactId c, int maxMessages, long maxLatency)
+	Offer generateOffer(ContactId c, int maxMessages, int maxLatency)
 			throws DbException;
 
 	/**
@@ -112,7 +112,7 @@ public interface DatabaseComponent {
 	 * sendable messages that fit in the given length.
 	 */
 	Collection<byte[]> generateRequestedBatch(ContactId c, int maxLength,
-			long maxLatency) throws DbException;
+			int maxLatency) throws DbException;
 
 	/**
 	 * Returns a retention ack for the given contact, or null if no retention
@@ -125,7 +125,7 @@ public interface DatabaseComponent {
 	 * over a transport with the given latency. Returns null if no update is
 	 * due.
 	 */
-	RetentionUpdate generateRetentionUpdate(ContactId c, long maxLatency)
+	RetentionUpdate generateRetentionUpdate(ContactId c, int maxLatency)
 			throws DbException;
 
 	/**
@@ -139,7 +139,7 @@ public interface DatabaseComponent {
 	 * over a transport with the given latency. Returns null if no update is
 	 * due.
 	 */
-	SubscriptionUpdate generateSubscriptionUpdate(ContactId c, long maxLatency)
+	SubscriptionUpdate generateSubscriptionUpdate(ContactId c, int maxLatency)
 			throws DbException;
 
 	/**
@@ -155,7 +155,7 @@ public interface DatabaseComponent {
 	 * updates are due.
 	 */
 	Collection<TransportUpdate> generateTransportUpdates(ContactId c,
-			long maxLatency) throws DbException;
+			int maxLatency) throws DbException;
 
 	/**
 	 * Returns the status of all groups to which the user subscribes or can
@@ -227,8 +227,8 @@ public interface DatabaseComponent {
 	/** Returns all contacts who subscribe to the given group. */
 	Collection<Contact> getSubscribers(GroupId g) throws DbException;
 
-	/** Returns the maximum latencies of all local transports. */
-	Map<TransportId, Long> getTransportLatencies() throws DbException;
+	/** Returns the maximum latencies of all supported transports. */
+	Map<TransportId, Integer> getTransportLatencies() throws DbException;
 
 	/** Returns the number of unread messages in each subscribed group. */
 	Map<GroupId, Integer> getUnreadMessageCounts() throws DbException;
diff --git a/briar-api/src/org/briarproject/api/event/TransportAddedEvent.java b/briar-api/src/org/briarproject/api/event/TransportAddedEvent.java
index 7a1f599a6e59d452f782d61144678e5454c0a1ab..b775ce6c9e5dd88c112f6821fe157b4a0e68d853 100644
--- a/briar-api/src/org/briarproject/api/event/TransportAddedEvent.java
+++ b/briar-api/src/org/briarproject/api/event/TransportAddedEvent.java
@@ -6,9 +6,9 @@ import org.briarproject.api.TransportId;
 public class TransportAddedEvent extends Event {
 
 	private final TransportId transportId;
-	private final long maxLatency;
+	private final int maxLatency;
 
-	public TransportAddedEvent(TransportId transportId, long maxLatency) {
+	public TransportAddedEvent(TransportId transportId, int maxLatency) {
 		this.transportId = transportId;
 		this.maxLatency = maxLatency;
 	}
@@ -17,7 +17,7 @@ public class TransportAddedEvent extends Event {
 		return transportId;
 	}
 
-	public long getMaxLatency() {
+	public int getMaxLatency() {
 		return maxLatency;
 	}
 }
diff --git a/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java b/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java
index c5ceca842c7afbc01945b5080afc7f5ed01ab63f..8764f3e255bb0ed10e8a676b9887dfd63172b22d 100644
--- a/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java
+++ b/briar-api/src/org/briarproject/api/messaging/MessagingSessionFactory.java
@@ -11,6 +11,9 @@ public interface MessagingSessionFactory {
 	MessagingSession createIncomingSession(ContactId c, TransportId t,
 			InputStream in);
 
-	MessagingSession createOutgoingSession(ContactId c, TransportId t,
-			long maxLatency, boolean duplex, OutputStream out);
+	MessagingSession createSimplexOutgoingSession(ContactId c, TransportId t,
+			int maxLatency, OutputStream out);
+
+	MessagingSession createDuplexOutgoingSession(ContactId c, TransportId t,
+			int maxLatency, int maxIdleTime, OutputStream out);
 }
diff --git a/briar-api/src/org/briarproject/api/messaging/PacketWriter.java b/briar-api/src/org/briarproject/api/messaging/PacketWriter.java
index bc7b4e1de6d4086956003b46872a508e7a548800..1300e84c5b7730eb3f2088bf308e6dfcb561b0cd 100644
--- a/briar-api/src/org/briarproject/api/messaging/PacketWriter.java
+++ b/briar-api/src/org/briarproject/api/messaging/PacketWriter.java
@@ -29,4 +29,6 @@ public interface PacketWriter {
 	void writeTransportAck(TransportAck a) throws IOException;
 
 	void writeTransportUpdate(TransportUpdate u) throws IOException;
+
+	void flush() throws IOException;
 }
diff --git a/briar-api/src/org/briarproject/api/plugins/Plugin.java b/briar-api/src/org/briarproject/api/plugins/Plugin.java
index 3389a73577d9da1ce789a25d3387d2ae87fe9b16..cc0338a4a3b9d1d9386db6885368e6f34adbef6c 100644
--- a/briar-api/src/org/briarproject/api/plugins/Plugin.java
+++ b/briar-api/src/org/briarproject/api/plugins/Plugin.java
@@ -11,11 +11,11 @@ public interface Plugin {
 	/** Returns the plugin's transport identifier. */
 	TransportId getId();
 
-	/** Returns the transport's maximum frame length in bytes. */
-	int getMaxFrameLength();
-
 	/** Returns the transport's maximum latency in milliseconds. */
-	long getMaxLatency();
+	int getMaxLatency();
+
+	/** Returns the transport's maximum idle time in milliseconds. */
+	int getMaxIdleTime();
 
 	/** Starts the plugin and returns true if it started successfully. */
 	boolean start() throws IOException;
@@ -36,7 +36,7 @@ public interface Plugin {
 	 * Returns the desired interval in milliseconds between calls to the
 	 * plugin's {@link #poll(Collection)} method.
 	 */
-	long getPollingInterval();
+	int getPollingInterval();
 
 	/**
 	 * Attempts to establish connections to contacts, passing any created
diff --git a/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java b/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java
index c4c99dfa37c3e17c94a43a438922fe5a52d173c2..e071c4432b0b8d5eeccd0f014294b25281ed262a 100644
--- a/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java
+++ b/briar-api/src/org/briarproject/api/plugins/TransportConnectionReader.java
@@ -9,9 +9,6 @@ import java.io.InputStream;
  */
 public interface TransportConnectionReader {
 
-	/** Returns the maximum frame length of the transport in bytes. */
-	int getMaxFrameLength();
-
 	/** Returns the maximum latency of the transport in milliseconds. */
 	long getMaxLatency();
 
diff --git a/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java b/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java
index 554e5de8a110e3d24c249cf896287bdd1e5336d4..d75522f1bb2c48f6541909864008e4bbf876ddb9 100644
--- a/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java
+++ b/briar-api/src/org/briarproject/api/plugins/TransportConnectionWriter.java
@@ -9,11 +9,11 @@ import java.io.OutputStream;
  */
 public interface TransportConnectionWriter {
 
-	/** Returns the maximum frame length of the transport in bytes. */
-	int getMaxFrameLength();
-
 	/** Returns the maximum latency of the transport in milliseconds. */
-	long getMaxLatency();
+	int getMaxLatency();
+
+	/** Returns the maximum idle time of the transport in milliseconds. */
+	int getMaxIdleTime();
 
 	/** Returns the capacity of the transport connection in bytes. */
 	long getCapacity();
diff --git a/briar-api/src/org/briarproject/api/transport/StreamReader.java b/briar-api/src/org/briarproject/api/transport/StreamReader.java
deleted file mode 100644
index 2fef7b4df0a913f119c7436090d85046f8fbb164..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/transport/StreamReader.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.briarproject.api.transport;
-
-import java.io.InputStream;
-
-/** Decrypts and authenticates data received over an underlying transport. */
-public interface StreamReader {
-
-	/**
-	 * Returns an input stream from which the decrypted, authenticated data can
-	 * be read.
-	 */
-	InputStream getInputStream();
-}
diff --git a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
index 14459ec60a0d7e1c2e1f3e286fdaea0dd8554ea9..5cf4fd6394ac7f9004d5ad1d48f5d1cca25f7fe2 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
@@ -4,11 +4,16 @@ import java.io.InputStream;
 
 public interface StreamReaderFactory {
 
-	/** Creates a {@link StreamReader} for a transport connection. */
-	StreamReader createStreamReader(InputStream in, int maxFrameLength,
-			StreamContext ctx);
+	/**
+	 * Creates an {@link java.io.InputStream InputStream} for reading from a
+	 * transport stream.
+	 */
+	InputStream createStreamReader(InputStream in, StreamContext ctx);
 
-	/** Creates a {@link StreamReader} for an invitation connection. */
-	StreamReader createInvitationStreamReader(InputStream in,
-			int maxFrameLength, byte[] secret, boolean alice);
+	/**
+	 * Creates an {@link java.io.InputStream InputStream} for reading from an
+	 * invitation stream.
+	 */
+	InputStream createInvitationStreamReader(InputStream in,
+			byte[] secret, boolean alice);
 }
diff --git a/briar-api/src/org/briarproject/api/transport/StreamWriter.java b/briar-api/src/org/briarproject/api/transport/StreamWriter.java
deleted file mode 100644
index 2684742dbdf5889fcb382c8cadbaade65b2eb7f0..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/transport/StreamWriter.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.briarproject.api.transport;
-
-import java.io.OutputStream;
-
-/** Encrypts and authenticates data to be sent over an underlying transport. */
-public interface StreamWriter {
-
-	/**
-	 * Returns an output stream to which unencrypted, unauthenticated data can
-	 * be written.
-	 */
-	OutputStream getOutputStream();
-}
diff --git a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
index f038a75221d52f0798ab9176ed353868d281d624..38e373ccc244a1439d332176ee5380a89343aa9f 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
@@ -4,11 +4,16 @@ import java.io.OutputStream;
 
 public interface StreamWriterFactory {
 
-	/** Creates a {@link StreamWriter} for a transport connection. */
-	StreamWriter createStreamWriter(OutputStream out, int maxFrameLength,
-			StreamContext ctx);
+	/**
+	 * Creates an {@link java.io.OutputStream OutputStream} for writing to a
+	 * transport stream
+	 */
+	OutputStream createStreamWriter(OutputStream out, StreamContext ctx);
 
-	/** Creates a {@link StreamWriter} for an invitation connection. */
-	StreamWriter createInvitationStreamWriter(OutputStream out,
-			int maxFrameLength, byte[] secret, boolean alice);
+	/**
+	 * Creates an {@link java.io.OutputStream OutputStream} for writing to an
+	 * invitation stream.
+	 */
+	OutputStream createInvitationStreamWriter(OutputStream out,
+			byte[] secret, boolean alice);
 }
diff --git a/briar-api/src/org/briarproject/api/transport/TransportConstants.java b/briar-api/src/org/briarproject/api/transport/TransportConstants.java
index 666d74e964b00202b89b907c1011caa5e98616d3..8f05eb23ab9c9ecf44608671176c67ae1b861657 100644
--- a/briar-api/src/org/briarproject/api/transport/TransportConstants.java
+++ b/briar-api/src/org/briarproject/api/transport/TransportConstants.java
@@ -1,24 +1,25 @@
 package org.briarproject.api.transport;
 
+
 public interface TransportConstants {
 
 	/** The length of the pseudo-random tag in bytes. */
 	int TAG_LENGTH = 16;
 
 	/** The maximum length of a frame in bytes, including the header and MAC. */
-	int MAX_FRAME_LENGTH = 32768; // 2^15, 32 KiB
-
-	/** The length of the initalisation vector (IV) in bytes. */
-	int IV_LENGTH = 12;
+	int MAX_FRAME_LENGTH = 1024;
 
-	/** The length of the additional authenticated data (AAD) in bytes. */
-	int AAD_LENGTH = 6;
+	/** The length of the message authentication code (MAC) in bytes. */
+	int MAC_LENGTH = 16;
 
 	/** The length of the frame header in bytes. */
-	int HEADER_LENGTH = 2;
+	int HEADER_LENGTH = 4 + MAC_LENGTH;
 
-	/** The length of the message authentication code (MAC) in bytes. */
-	int MAC_LENGTH = 16;
+	/** The maximum total length of the frame payload and padding in bytes. */
+	int MAX_PAYLOAD_LENGTH = MAX_FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
+
+	/** The length of the initalisation vector (IV) in bytes. */
+	int IV_LENGTH = 12;
 
 	/**
 	 * The minimum stream length in bytes that all transport plugins must
diff --git a/briar-core/src/org/briarproject/crypto/AuthenticatedCipherImpl.java b/briar-core/src/org/briarproject/crypto/AuthenticatedCipherImpl.java
index 93807087d263c1d5f3ad43b100ea304a907b0441..d001b60c5441d4f90fbeb074874485c8a4f984e0 100644
--- a/briar-core/src/org/briarproject/crypto/AuthenticatedCipherImpl.java
+++ b/briar-core/src/org/briarproject/crypto/AuthenticatedCipherImpl.java
@@ -20,7 +20,7 @@ class AuthenticatedCipherImpl implements AuthenticatedCipher {
 		this.macLength = macLength;
 	}
 
-	public int doFinal(byte[] input, int inputOff, int len, byte[] output,
+	public int process(byte[] input, int inputOff, int len, byte[] output,
 			int outputOff) throws GeneralSecurityException {
 		int processed = 0;
 		if(len != 0) {
@@ -38,7 +38,7 @@ class AuthenticatedCipherImpl implements AuthenticatedCipher {
 
 	public void init(boolean encrypt, SecretKey key, byte[] iv, byte[] aad)
 			throws GeneralSecurityException {
-		KeyParameter k = new KeyParameter(key.getEncoded());
+		KeyParameter k = new KeyParameter(key.getBytes());
 		AEADParameters params = new AEADParameters(k, macLength * 8, iv, aad);
 		try {
 			cipher.init(encrypt, params);
diff --git a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
index 4edd26a4fe9835f83f54054660f0442ed66e0a4e..5612c407769787fd131893587e45798395047aa7 100644
--- a/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
+++ b/briar-core/src/org/briarproject/crypto/CryptoComponentImpl.java
@@ -7,8 +7,6 @@ import static org.briarproject.crypto.EllipticCurveConstants.P;
 import static org.briarproject.crypto.EllipticCurveConstants.PARAMETERS;
 import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
@@ -31,6 +29,7 @@ import org.briarproject.api.crypto.SecretKey;
 import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.system.SeedProvider;
 import org.briarproject.util.ByteUtils;
+import org.briarproject.util.StringUtils;
 import org.spongycastle.crypto.AsymmetricCipherKeyPair;
 import org.spongycastle.crypto.BlockCipher;
 import org.spongycastle.crypto.CipherParameters;
@@ -44,11 +43,11 @@ import org.spongycastle.crypto.generators.PKCS5S2ParametersGenerator;
 import org.spongycastle.crypto.macs.HMac;
 import org.spongycastle.crypto.modes.AEADBlockCipher;
 import org.spongycastle.crypto.modes.GCMBlockCipher;
+import org.spongycastle.crypto.modes.gcm.BasicGCMMultiplier;
 import org.spongycastle.crypto.params.ECKeyGenerationParameters;
 import org.spongycastle.crypto.params.ECPrivateKeyParameters;
 import org.spongycastle.crypto.params.ECPublicKeyParameters;
 import org.spongycastle.crypto.params.KeyParameter;
-import org.spongycastle.util.Strings;
 
 class CryptoComponentImpl implements CryptoComponent {
 
@@ -114,7 +113,7 @@ class CryptoComponentImpl implements CryptoComponent {
 	public SecretKey generateSecretKey() {
 		byte[] b = new byte[CIPHER_KEY_BYTES];
 		secureRandom.nextBytes(b);
-		return new SecretKeyImpl(b);
+		return new SecretKey(b);
 	}
 
 	public MessageDigest getMessageDigest() {
@@ -188,8 +187,6 @@ class CryptoComponentImpl implements CryptoComponent {
 		int[] codes = new int[2];
 		codes[0] = ByteUtils.readUint(alice, CODE_BITS);
 		codes[1] = ByteUtils.readUint(bob, CODE_BITS);
-		ByteUtils.erase(alice);
-		ByteUtils.erase(bob);
 		return codes;
 	}
 
@@ -223,9 +220,7 @@ class CryptoComponentImpl implements CryptoComponent {
 		byte[] raw = deriveSharedSecret(ourPriv, theirPub);
 		// Derive the cooked secret from the raw secret using the
 		// concatenation KDF
-		byte[] cooked = concatenationKdf(raw, MASTER, aliceInfo, bobInfo);
-		ByteUtils.erase(raw);
-		return cooked;
+		return concatenationKdf(raw, MASTER, aliceInfo, bobInfo);
 	}
 
 	// Package access for testing
@@ -296,12 +291,16 @@ class CryptoComponentImpl implements CryptoComponent {
 	}
 
 	private SecretKey deriveKey(byte[] secret, byte[] label, long context) {
-		byte[] key = counterModeKdf(secret, label, context);
-		return new SecretKeyImpl(key);
+		return new SecretKey(counterModeKdf(secret, label, context));
 	}
 
 	public AuthenticatedCipher getFrameCipher() {
-		AEADBlockCipher a = new GCMBlockCipher(new AESLightEngine());
+		return getAuthenticatedCipher();
+	}
+
+	private AuthenticatedCipher getAuthenticatedCipher() {
+		AEADBlockCipher a = new GCMBlockCipher(new AESLightEngine(),
+				new BasicGCMMultiplier());
 		return new AuthenticatedCipherImpl(a, MAC_BYTES);
 	}
 
@@ -313,21 +312,19 @@ class CryptoComponentImpl implements CryptoComponent {
 		ByteUtils.writeUint32(streamNumber, tag, 0);
 		BlockCipher cipher = new AESLightEngine();
 		assert cipher.getBlockSize() == TAG_LENGTH;
-		KeyParameter k = new KeyParameter(tagKey.getEncoded());
+		KeyParameter k = new KeyParameter(tagKey.getBytes());
 		cipher.init(true, k);
 		cipher.processBlock(tag, 0, tag, 0);
-		ByteUtils.erase(k.getKey());
 	}
 
-	public byte[] encryptWithPassword(byte[] input, char[] password) {
+	public byte[] encryptWithPassword(byte[] input, String password) {
 		// Generate a random salt
 		byte[] salt = new byte[PBKDF_SALT_BYTES];
 		secureRandom.nextBytes(salt);
 		// Calibrate the KDF
 		int iterations = chooseIterationCount(PBKDF_TARGET_MILLIS);
 		// Derive the key from the password
-		byte[] keyBytes = pbkdf2(password, salt, iterations);
-		SecretKey key = new SecretKeyImpl(keyBytes);
+		SecretKey key = new SecretKey(pbkdf2(password, salt, iterations));
 		// Generate a random IV
 		byte[] iv = new byte[STORAGE_IV_BYTES];
 		secureRandom.nextBytes(iv);
@@ -338,22 +335,18 @@ class CryptoComponentImpl implements CryptoComponent {
 		ByteUtils.writeUint32(iterations, output, salt.length);
 		System.arraycopy(iv, 0, output, salt.length + 4, iv.length);
 		// Initialise the cipher and encrypt the plaintext
+		AuthenticatedCipher cipher = getAuthenticatedCipher();
 		try {
-			AEADBlockCipher a = new GCMBlockCipher(new AESLightEngine());
-			AuthenticatedCipher cipher = new AuthenticatedCipherImpl(a,
-					MAC_BYTES);
 			cipher.init(true, key, iv, null);
 			int outputOff = salt.length + 4 + iv.length;
-			cipher.doFinal(input, 0, input.length, output, outputOff);
+			cipher.process(input, 0, input.length, output, outputOff);
 			return output;
 		} catch(GeneralSecurityException e) {
 			throw new RuntimeException(e);
-		} finally {
-			key.erase();
 		}
 	}
 
-	public byte[] decryptWithPassword(byte[] input, char[] password) {
+	public byte[] decryptWithPassword(byte[] input, String password) {
 		// The input contains the salt, iterations, IV, ciphertext and MAC
 		if(input.length < PBKDF_SALT_BYTES + 4 + STORAGE_IV_BYTES + MAC_BYTES)
 			return null; // Invalid
@@ -365,16 +358,12 @@ class CryptoComponentImpl implements CryptoComponent {
 		byte[] iv = new byte[STORAGE_IV_BYTES];
 		System.arraycopy(input, salt.length + 4, iv, 0, iv.length);
 		// Derive the key from the password
-		byte[] keyBytes = pbkdf2(password, salt, (int) iterations);
-		SecretKey key = new SecretKeyImpl(keyBytes);
+		SecretKey key = new SecretKey(pbkdf2(password, salt, (int) iterations));
 		// Initialise the cipher
-		AuthenticatedCipher cipher;
+		AuthenticatedCipher cipher = getAuthenticatedCipher();
 		try {
-			AEADBlockCipher a = new GCMBlockCipher(new AESLightEngine());
-			cipher = new AuthenticatedCipherImpl(a, MAC_BYTES);
 			cipher.init(false, key, iv, null);
 		} catch(GeneralSecurityException e) {
-			key.erase();
 			throw new RuntimeException(e);
 		}
 		// Try to decrypt the ciphertext (may be invalid)
@@ -382,12 +371,10 @@ class CryptoComponentImpl implements CryptoComponent {
 			int inputOff = salt.length + 4 + iv.length;
 			int inputLen = input.length - inputOff;
 			byte[] output = new byte[inputLen - MAC_BYTES];
-			cipher.doFinal(input, inputOff, inputLen, output, 0);
+			cipher.process(input, inputOff, inputLen, output, 0);
 			return output;
 		} catch(GeneralSecurityException e) {
-			return null; // Invalid
-		} finally {
-			key.erase();
+			return null; // Invalid ciphertext
 		}
 	}
 
@@ -417,7 +404,6 @@ class CryptoComponentImpl implements CryptoComponent {
 		// The secret is the first CIPHER_KEY_BYTES bytes of the hash
 		byte[] output = new byte[CIPHER_KEY_BYTES];
 		System.arraycopy(hash, 0, output, 0, output.length);
-		ByteUtils.erase(hash);
 		return output;
 	}
 
@@ -447,20 +433,17 @@ class CryptoComponentImpl implements CryptoComponent {
 		prf.update((byte) CIPHER_KEY_BYTES); // Output length
 		prf.doFinal(mac, 0);
 		System.arraycopy(mac, 0, output, 0, output.length);
-		ByteUtils.erase(mac);
-		ByteUtils.erase(k.getKey());
 		return output;
 	}
 
 	// Password-based key derivation function - see PKCS#5 v2.1, section 5.2
-	private byte[] pbkdf2(char[] password, byte[] salt, int iterations) {
-		byte[] utf8 = toUtf8ByteArray(password);
+	private byte[] pbkdf2(String password, byte[] salt, int iterations) {
+		byte[] utf8 = StringUtils.toUtf8(password);
 		Digest digest = new SHA384Digest();
 		PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(digest);
 		gen.init(utf8, salt, iterations);
 		int keyLengthInBits = CIPHER_KEY_BYTES * 8;
 		CipherParameters p = gen.generateDerivedParameters(keyLengthInBits);
-		ByteUtils.erase(utf8);
 		return ((KeyParameter) p).getKey();
 	}
 
@@ -512,18 +495,4 @@ class CryptoComponentImpl implements CryptoComponent {
 		if(size % 2 == 1) return list.get(size / 2);
 		return list.get(size / 2 - 1) + list.get(size / 2) / 2;
 	}
-
-	private byte[] toUtf8ByteArray(char[] c) {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		try {
-			Strings.toUTF8ByteArray(c, out);
-			byte[] utf8 = out.toByteArray();
-			// Erase the output stream's buffer
-			out.reset();
-			out.write(new byte[utf8.length]);
-			return utf8;
-		} catch(IOException e) {
-			throw new RuntimeException(e);
-		}
-	}
 }
diff --git a/briar-core/src/org/briarproject/crypto/CryptoModule.java b/briar-core/src/org/briarproject/crypto/CryptoModule.java
index d7dd2fdb49a6798281ea8132c20dc46efe4a63e1..d4ac20c6495d50fd7c21f3c8a339a0579cb8ec9b 100644
--- a/briar-core/src/org/briarproject/crypto/CryptoModule.java
+++ b/briar-core/src/org/briarproject/crypto/CryptoModule.java
@@ -14,6 +14,8 @@ import javax.inject.Singleton;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.crypto.PasswordStrengthEstimator;
+import org.briarproject.api.crypto.StreamDecrypterFactory;
+import org.briarproject.api.crypto.StreamEncrypterFactory;
 import org.briarproject.api.lifecycle.LifecycleManager;
 
 import com.google.inject.AbstractModule;
@@ -44,6 +46,8 @@ public class CryptoModule extends AbstractModule {
 				CryptoComponentImpl.class).in(Singleton.class);
 		bind(PasswordStrengthEstimator.class).to(
 				PasswordStrengthEstimatorImpl.class);
+		bind(StreamDecrypterFactory.class).to(StreamDecrypterFactoryImpl.class);
+		bind(StreamEncrypterFactory.class).to(StreamEncrypterFactoryImpl.class);
 	}
 
 	@Provides @Singleton @CryptoExecutor
diff --git a/briar-core/src/org/briarproject/transport/FrameEncoder.java b/briar-core/src/org/briarproject/crypto/FrameEncoder.java
similarity index 54%
rename from briar-core/src/org/briarproject/transport/FrameEncoder.java
rename to briar-core/src/org/briarproject/crypto/FrameEncoder.java
index 684b8754248391d6d8c01dca3b89b12ca55cf252..2cc6ffd58adca65a4cf6ba632ad86f63bb7df58f 100644
--- a/briar-core/src/org/briarproject/transport/FrameEncoder.java
+++ b/briar-core/src/org/briarproject/crypto/FrameEncoder.java
@@ -1,44 +1,33 @@
-package org.briarproject.transport;
+package org.briarproject.crypto;
 
-import static org.briarproject.api.transport.TransportConstants.AAD_LENGTH;
 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 static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
 import org.briarproject.util.ByteUtils;
 
 class FrameEncoder {
 
-	static void encodeIv(byte[] iv, long frameNumber) {
+	static void encodeIv(byte[] iv, long frameNumber, boolean header) {
 		if(iv.length < IV_LENGTH) throw new IllegalArgumentException();
 		if(frameNumber < 0 || frameNumber > MAX_32_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
 		ByteUtils.writeUint32(frameNumber, iv, 0);
-		for(int i = 4; i < IV_LENGTH; i++) iv[i] = 0;
-	}
-
-	static void encodeAad(byte[] aad, long frameNumber, int plaintextLength) {
-		if(aad.length < AAD_LENGTH) throw new IllegalArgumentException();
-		if(frameNumber < 0 || frameNumber > MAX_32_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		if(plaintextLength < HEADER_LENGTH)
-			throw new IllegalArgumentException();
-		if(plaintextLength > MAX_FRAME_LENGTH - MAC_LENGTH)
-			throw new IllegalArgumentException();
-		ByteUtils.writeUint32(frameNumber, aad, 0);
-		ByteUtils.writeUint16(plaintextLength, aad, 4);
+		if(header) iv[4] = 1;
+		else iv[4] = 0;
+		for(int i = 5; i < IV_LENGTH; i++) iv[i] = 0;
 	}
 
 	static void encodeHeader(byte[] header, boolean finalFrame,
-			int payloadLength) {
+			int payloadLength, int paddingLength) {
 		if(header.length < HEADER_LENGTH) throw new IllegalArgumentException();
-		if(payloadLength < 0)
-			throw new IllegalArgumentException();
-		if(payloadLength > MAX_FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH)
+		if(payloadLength < 0) throw new IllegalArgumentException();
+		if(paddingLength < 0) throw new IllegalArgumentException();
+		if(payloadLength + paddingLength > MAX_PAYLOAD_LENGTH)
 			throw new IllegalArgumentException();
 		ByteUtils.writeUint16(payloadLength, header, 0);
+		ByteUtils.writeUint16(paddingLength, header, 2);
 		if(finalFrame) header[0] |= 0x80;
 	}
 
@@ -51,4 +40,9 @@ class FrameEncoder {
 		if(header.length < HEADER_LENGTH) throw new IllegalArgumentException();
 		return ByteUtils.readUint16(header, 0) & 0x7FFF;
 	}
+
+	static int getPaddingLength(byte[] header) {
+		if(header.length < HEADER_LENGTH) throw new IllegalArgumentException();
+		return ByteUtils.readUint16(header, 2);
+	}
 }
diff --git a/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java b/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java
index aeec2ddc3bcc3127224796c101edd96c46efcc10..1fd99f52bfe2c8b79660a3be5df2e038c2daf6ad 100644
--- a/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java
+++ b/briar-core/src/org/briarproject/crypto/PasswordStrengthEstimatorImpl.java
@@ -13,9 +13,10 @@ class PasswordStrengthEstimatorImpl implements PasswordStrengthEstimator {
 	private static final double STRONG = Math.log(Math.pow(LOWER + UPPER +
 			DIGIT + OTHER, 10));
 
-	public float estimateStrength(char[] password) {
+	public float estimateStrength(String password) {
 		HashSet<Character> unique = new HashSet<Character>();
-		for(char c : password) unique.add(c);
+		int length = password.length();
+		for(int i = 0; i < length; i++) unique.add(password.charAt(i));
 		boolean lower = false, upper = false, digit = false, other = false;
 		for(char c : unique) {
 			if(Character.isLowerCase(c)) lower = true;
diff --git a/briar-core/src/org/briarproject/crypto/SecretKeyImpl.java b/briar-core/src/org/briarproject/crypto/SecretKeyImpl.java
deleted file mode 100644
index f37685cd17860279f57d6c2bf7af27c35ba203c6..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/crypto/SecretKeyImpl.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package org.briarproject.crypto;
-
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.util.ByteUtils;
-
-class SecretKeyImpl implements SecretKey {
-
-	private final byte[] key;
-
-	private boolean erased = false;
-	
-	private final Lock synchLock = new ReentrantLock();
-
-	SecretKeyImpl(byte[] key) {
-		this.key = key;
-	}
-
-	public byte[] getEncoded() {
-		synchLock.lock();
-		try{
-			if(erased) throw new IllegalStateException();
-			return key;
-		}
-		finally{
-			synchLock.unlock();
-		}
-
-	}
-
-	public SecretKey copy() {
-		return new SecretKeyImpl(key.clone());
-	}
-
-	public void erase() {
-		synchLock.lock();
-		try{
-			if(erased) throw new IllegalStateException();
-			ByteUtils.erase(key);
-			erased = true;
-		}
-		finally{
-			synchLock.unlock();
-		}
-	}
-}
diff --git a/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java b/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..bed0503c244ace043a359662c0a3155225920a99
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java
@@ -0,0 +1,40 @@
+package org.briarproject.crypto;
+
+import java.io.InputStream;
+
+import javax.inject.Inject;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamDecrypter;
+import org.briarproject.api.crypto.StreamDecrypterFactory;
+import org.briarproject.api.transport.StreamContext;
+
+class StreamDecrypterFactoryImpl implements StreamDecrypterFactory {
+
+	private final CryptoComponent crypto;
+
+	@Inject
+	StreamDecrypterFactoryImpl(CryptoComponent crypto) {
+		this.crypto = crypto;
+	}
+
+	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
+		return new StreamDecrypterImpl(in, crypto.getFrameCipher(), frameKey);
+	}
+
+	public StreamDecrypter createInvitationStreamDecrypter(InputStream in,
+			byte[] secret, boolean alice) {
+		// Derive the frame key
+		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
+		// Create the decrypter
+		return new StreamDecrypterImpl(in, crypto.getFrameCipher(), frameKey);
+	}
+}
diff --git a/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b27b0cf9e44c33b0933f775696690b0aab1e58b6
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
@@ -0,0 +1,97 @@
+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 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.AuthenticatedCipher;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamDecrypter;
+
+class StreamDecrypterImpl implements StreamDecrypter {
+
+	private final InputStream in;
+	private final AuthenticatedCipher frameCipher;
+	private final SecretKey frameKey;
+	private final byte[] iv, aad, header, ciphertext;
+
+	private long frameNumber;
+	private boolean finalFrame;
+
+	StreamDecrypterImpl(InputStream in, AuthenticatedCipher frameCipher,
+			SecretKey frameKey) {
+		this.in = in;
+		this.frameCipher = frameCipher;
+		this.frameKey = frameKey;
+		iv = new byte[IV_LENGTH];
+		aad = new byte[IV_LENGTH];
+		header = new byte[HEADER_LENGTH];
+		ciphertext = new byte[MAX_FRAME_LENGTH];
+		frameNumber = 0;
+		finalFrame = false;
+	}
+
+	public int readFrame(byte[] payload) throws IOException {
+		if(payload.length < MAX_PAYLOAD_LENGTH)
+			throw new IllegalArgumentException();
+		if(finalFrame) return -1;
+		// Read the header
+		int offset = 0;
+		while(offset < HEADER_LENGTH) {
+			int read = in.read(ciphertext, offset, HEADER_LENGTH - offset);
+			if(read == -1) throw new EOFException();
+			offset += read;
+		}
+		// Decrypt and authenticate the header
+		FrameEncoder.encodeIv(iv, frameNumber, true);
+		FrameEncoder.encodeIv(aad, frameNumber, true);
+		try {
+			frameCipher.init(false, frameKey, iv, aad);
+			int decrypted = frameCipher.process(ciphertext, 0, HEADER_LENGTH,
+					header, 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);
+		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);
+			if(read == -1) throw new EOFException();
+			offset += read;
+		}
+		// Decrypt and authenticate the payload and padding
+		FrameEncoder.encodeIv(iv, frameNumber, false);
+		FrameEncoder.encodeIv(aad, frameNumber, false);
+		try {
+			frameCipher.init(false, frameKey, iv, aad);
+			int decrypted = frameCipher.process(ciphertext, HEADER_LENGTH,
+					payloadLength + paddingLength + MAC_LENGTH, payload, 0);
+			if(decrypted != payloadLength + paddingLength)
+				throw new RuntimeException();
+		} catch(GeneralSecurityException e) {
+			throw new FormatException();
+		}
+		// If there's any padding it must be all zeroes
+		for(int i = 0; i < paddingLength; i++)
+			if(payload[payloadLength + i] != 0) throw new FormatException();
+		frameNumber++;
+		return payloadLength;
+	}
+}
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java b/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ec8286e06100b00598b726925e6a4eb0eb096cc7
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
@@ -0,0 +1,48 @@
+package org.briarproject.crypto;
+
+import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
+
+import java.io.OutputStream;
+
+import javax.inject.Inject;
+
+import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamEncrypter;
+import org.briarproject.api.crypto.StreamEncrypterFactory;
+import org.briarproject.api.transport.StreamContext;
+
+class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
+
+	private final CryptoComponent crypto;
+
+	@Inject
+	StreamEncrypterFactoryImpl(CryptoComponent crypto) {
+		this.crypto = crypto;
+	}
+
+	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
+		return new StreamEncrypterImpl(out, crypto.getFrameCipher(), frameKey,
+				tag);
+	}
+
+	public StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
+			byte[] secret, boolean alice) {
+		// Derive the frame key
+		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
+		// Create the encrypter
+		return new StreamEncrypterImpl(out, crypto.getFrameCipher(), frameKey,
+				null);
+	}
+}
diff --git a/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..475c62f59fe8cec9994f8115ca8369b9c2465cbf
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
@@ -0,0 +1,97 @@
+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 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.AuthenticatedCipher;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamEncrypter;
+
+class StreamEncrypterImpl implements StreamEncrypter {
+
+	private final OutputStream out;
+	private final AuthenticatedCipher frameCipher;
+	private final SecretKey frameKey;
+	private final byte[] tag, iv, aad, plaintext, ciphertext;
+
+	private long frameNumber;
+	private boolean writeTag;
+
+	StreamEncrypterImpl(OutputStream out, AuthenticatedCipher frameCipher,
+			SecretKey frameKey, byte[] tag) {
+		this.out = out;
+		this.frameCipher = frameCipher;
+		this.frameKey = frameKey;
+		this.tag = tag;
+		iv = new byte[IV_LENGTH];
+		aad = new byte[IV_LENGTH];
+		plaintext = new byte[HEADER_LENGTH + MAX_PAYLOAD_LENGTH];
+		ciphertext = new byte[MAX_FRAME_LENGTH];
+		frameNumber = 0;
+		writeTag = (tag != null);
+	}
+
+	public void writeFrame(byte[] payload, int payloadLength,
+			int paddingLength, boolean finalFrame) throws IOException {
+		if(payloadLength + paddingLength > MAX_PAYLOAD_LENGTH)
+			throw new IllegalArgumentException();
+		// Don't allow the frame counter to wrap
+		if(frameNumber > MAX_32_BIT_UNSIGNED) throw new IOException();
+		// Write the tag if required
+		if(writeTag) {
+			out.write(tag, 0, tag.length);
+			writeTag = false;
+		}
+		// Encode the header
+		FrameEncoder.encodeHeader(plaintext, finalFrame, payloadLength,
+				paddingLength);
+		// Encrypt and authenticate the header
+		FrameEncoder.encodeIv(iv, frameNumber, true);
+		FrameEncoder.encodeIv(aad, frameNumber, true);
+		try {
+			frameCipher.init(true, frameKey, iv, aad);
+			int encrypted = frameCipher.process(plaintext, 0,
+					HEADER_LENGTH - MAC_LENGTH, ciphertext, 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);
+		for(int i = 0; i < paddingLength; i++)
+			plaintext[HEADER_LENGTH + payloadLength + i] = 0;
+		// Encrypt and authenticate the payload and padding
+		FrameEncoder.encodeIv(iv, frameNumber, false);
+		FrameEncoder.encodeIv(aad, frameNumber, false);
+		try {
+			frameCipher.init(true, frameKey, iv, aad);
+			int encrypted = frameCipher.process(plaintext, HEADER_LENGTH,
+					payloadLength + paddingLength, ciphertext, 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);
+		frameNumber++;
+	}
+
+	public void flush() throws IOException {
+		// Write the tag if required
+		if(writeTag) {
+			out.write(tag, 0, tag.length);
+			writeTag = false;
+		}
+		out.flush();
+	}
+}
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index 5bce51798e1ce545ec8e51b5656a85006cf7239c..ba3c4ffafc2031fc19e8051a50db502013b7ebf3 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -152,7 +152,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: write.
 	 */
-	boolean addTransport(T txn, TransportId t, long maxLatency)
+	boolean addTransport(T txn, TransportId t, int maxLatency)
 			throws DbException;
 
 	/**
@@ -460,7 +460,7 @@ interface Database<T> {
 	 * <p>
 	 * Locking: write.
 	 */
-	RetentionUpdate getRetentionUpdate(T txn, ContactId c, long maxLatency)
+	RetentionUpdate getRetentionUpdate(T txn, ContactId c, int maxLatency)
 			throws DbException;
 
 	/**
@@ -499,7 +499,7 @@ interface Database<T> {
 	 * Locking: write.
 	 */
 	SubscriptionUpdate getSubscriptionUpdate(T txn, ContactId c,
-			long maxLatency) throws DbException;
+			int maxLatency) throws DbException;
 
 	/**
 	 * Returns a collection of transport acks for the given contact, or null if
@@ -511,11 +511,11 @@ interface Database<T> {
 			throws DbException;
 
 	/**
-	 * Returns the maximum latencies of all local transports.
+	 * Returns the maximum latencies of all supported transports.
 	 * <p>
 	 * Locking: read.
 	 */
-	Map<TransportId, Long> getTransportLatencies(T txn) throws DbException;
+	Map<TransportId, Integer> getTransportLatencies(T txn) throws DbException;
 
 	/**
 	 * Returns a collection of transport updates for the given contact and
@@ -525,7 +525,7 @@ interface Database<T> {
 	 * Locking: write.
 	 */
 	Collection<TransportUpdate> getTransportUpdates(T txn, ContactId c,
-			long maxLatency) throws DbException;
+			int maxLatency) throws DbException;
 
 	/**
 	 * Returns the number of unread messages in each subscribed group.
@@ -798,6 +798,6 @@ interface Database<T> {
 	 * <p>
 	 * Locking: write.
 	 */
-	void updateExpiryTime(T txn, ContactId c, MessageId m, long maxLatency)
+	void updateExpiryTime(T txn, ContactId c, MessageId m, int maxLatency)
 			throws DbException;
 }
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index c22643f0be075806ecc43d52659faddb86a11bf5..1c8bc564d7cc6cbdedfef4a15bbcae1605d07d91 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -314,7 +314,7 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public boolean addTransport(TransportId t, long maxLatency)
+	public boolean addTransport(TransportId t, int maxLatency)
 			throws DbException {
 		boolean added;
 		lock.writeLock().lock();
@@ -357,7 +357,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public Collection<byte[]> generateBatch(ContactId c, int maxLength,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		Collection<MessageId> ids;
 		List<byte[]> messages = new ArrayList<byte[]>();
 		lock.writeLock().lock();
@@ -384,7 +384,7 @@ DatabaseCleaner.Callback {
 		return Collections.unmodifiableList(messages);
 	}
 
-	public Offer generateOffer(ContactId c, int maxMessages, long maxLatency)
+	public Offer generateOffer(ContactId c, int maxMessages, int maxLatency)
 			throws DbException {
 		Collection<MessageId> ids;
 		lock.writeLock().lock();
@@ -432,7 +432,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public Collection<byte[]> generateRequestedBatch(ContactId c, int maxLength,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		Collection<MessageId> ids;
 		List<byte[]> messages = new ArrayList<byte[]>();
 		lock.writeLock().lock();
@@ -478,7 +478,7 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public RetentionUpdate generateRetentionUpdate(ContactId c, long maxLatency)
+	public RetentionUpdate generateRetentionUpdate(ContactId c, int maxLatency)
 			throws DbException {
 		lock.writeLock().lock();
 		try {
@@ -519,7 +519,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public SubscriptionUpdate generateSubscriptionUpdate(ContactId c,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
@@ -560,7 +560,7 @@ DatabaseCleaner.Callback {
 	}
 
 	public Collection<TransportUpdate> generateTransportUpdates(ContactId c,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
@@ -932,12 +932,13 @@ DatabaseCleaner.Callback {
 		}
 	}
 
-	public Map<TransportId, Long> getTransportLatencies() throws DbException {
+	public Map<TransportId, Integer> getTransportLatencies()
+			throws DbException {
 		lock.readLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
-				Map<TransportId, Long> latencies =
+				Map<TransportId, Integer> latencies =
 						db.getTransportLatencies(txn);
 				db.commitTransaction(txn);
 				return latencies;
diff --git a/briar-core/src/org/briarproject/db/ExponentialBackoff.java b/briar-core/src/org/briarproject/db/ExponentialBackoff.java
index 15444d11c15b52103e0222ca8d3fe20e4f0ea007..5248e8e50c888005bf0b275ad256462512b09199 100644
--- a/briar-core/src/org/briarproject/db/ExponentialBackoff.java
+++ b/briar-core/src/org/briarproject/db/ExponentialBackoff.java
@@ -11,13 +11,12 @@ class ExponentialBackoff {
 	 * transmissions increases exponentially. If the expiry time would
 	 * be greater than Long.MAX_VALUE, Long.MAX_VALUE is returned.
 	 */
-	static long calculateExpiry(long now, long maxLatency, int txCount) {
+	static long calculateExpiry(long now, int maxLatency, int txCount) {
 		if(now < 0) throw new IllegalArgumentException();
 		if(maxLatency <= 0) throw new IllegalArgumentException();
 		if(txCount < 0) throw new IllegalArgumentException();
 		// The maximum round-trip time is twice the maximum latency
-		long roundTrip = maxLatency * 2;
-		if(roundTrip < 0) return Long.MAX_VALUE;
+		long roundTrip = maxLatency * 2L;
 		// The interval between transmissions is roundTrip * 2 ^ txCount
 		for(int i = 0; i < txCount; i++) {
 			roundTrip <<= 1;
diff --git a/briar-core/src/org/briarproject/db/H2Database.java b/briar-core/src/org/briarproject/db/H2Database.java
index d7f064ddb26b7f1c18a372504e1c640728b128e1..fc91e20dca23aab698dfa8ad5435d34f7898e28d 100644
--- a/briar-core/src/org/briarproject/db/H2Database.java
+++ b/briar-core/src/org/briarproject/db/H2Database.java
@@ -81,34 +81,18 @@ class H2Database extends JdbcDatabase {
 		}
 	}
 
+	@Override
 	protected Connection createConnection() throws SQLException {
 		byte[] key = config.getEncryptionKey();
 		if(key == null) throw new IllegalStateException();
-		char[] password = encodePassword(key);
 		Properties props = new Properties();
 		props.setProperty("user", "user");
-		props.put("password", password);
-		try {
-			return DriverManager.getConnection(url, props);
-		} finally {
-			for(int i = 0; i < password.length; i++) password[i] = 0;
-		}
-	}
-
-	private char[] encodePassword(byte[] key) {
-		// The database password is the hex-encoded key
-		char[] hex = StringUtils.toHexChars(key);
-		// Separate the database password from the user password with a space
-		char[] user = "password".toCharArray();
-		char[] combined = new char[hex.length + 1 + user.length];
-		System.arraycopy(hex, 0, combined, 0, hex.length);
-		combined[hex.length] = ' ';
-		System.arraycopy(user, 0, combined, hex.length + 1, user.length);
-		// Erase the hex-encoded key
-		for(int i = 0; i < hex.length; i++) hex[i] = 0;
-		return combined;
+		// Separate the file password from the user password with a space
+		props.put("password", StringUtils.toHexString(key) + " password");
+		return DriverManager.getConnection(url, props);
 	}
 
+	@Override
 	protected void flushBuffersToDisk(Statement s) throws SQLException {
 		// FIXME: Remove this after implementing BTPv2?
 		s.execute("CHECKPOINT SYNC");
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index 57b596f7efdde420370cd9a60471b19534ad02a9..189add0cac1e8af5dc159eb969f2b8a977b43469 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -65,8 +65,8 @@ import org.briarproject.api.transport.TemporarySecret;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 6;
-	private static final int MIN_SCHEMA_VERSION = 5;
+	private static final int SCHEMA_VERSION = 7;
+	private static final int MIN_SCHEMA_VERSION = 7;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -216,7 +216,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	private static final String CREATE_TRANSPORTS =
 			"CREATE TABLE transports"
 					+ " (transportId VARCHAR NOT NULL,"
-					+ " maxLatency BIGINT NOT NULL,"
+					+ " maxLatency INT NOT NULL,"
 					+ " PRIMARY KEY (transportId))";
 
 	private static final String CREATE_TRANSPORT_CONFIGS =
@@ -897,7 +897,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public boolean addTransport(Connection txn, TransportId t, long maxLatency)
+	public boolean addTransport(Connection txn, TransportId t, int maxLatency)
 			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2055,7 +2055,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public RetentionUpdate getRetentionUpdate(Connection txn, ContactId c,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		long now = clock.currentTimeMillis();
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2233,7 +2233,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public SubscriptionUpdate getSubscriptionUpdate(Connection txn, ContactId c,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		long now = clock.currentTimeMillis();
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2327,7 +2327,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public Map<TransportId, Long> getTransportLatencies(Connection txn)
+	public Map<TransportId, Integer> getTransportLatencies(Connection txn)
 			throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -2335,10 +2335,11 @@ abstract class JdbcDatabase implements Database<Connection> {
 			String sql = "SELECT transportId, maxLatency FROM transports";
 			ps = txn.prepareStatement(sql);
 			rs = ps.executeQuery();
-			Map<TransportId, Long> latencies = new HashMap<TransportId, Long>();
+			Map<TransportId, Integer> latencies =
+					new HashMap<TransportId, Integer>();
 			while(rs.next()){
 				TransportId id = new TransportId(rs.getString(1));
-				latencies.put(id, rs.getLong(2));
+				latencies.put(id, rs.getInt(2));
 			}
 			rs.close();
 			ps.close();
@@ -2351,7 +2352,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public Collection<TransportUpdate> getTransportUpdates(Connection txn,
-			ContactId c, long maxLatency) throws DbException {
+			ContactId c, int maxLatency) throws DbException {
 		long now = clock.currentTimeMillis();
 		PreparedStatement ps = null;
 		ResultSet rs = null;
@@ -3332,7 +3333,7 @@ abstract class JdbcDatabase implements Database<Connection> {
 	}
 
 	public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
-			long maxLatency) throws DbException {
+			int maxLatency) throws DbException {
 		PreparedStatement ps = null;
 		ResultSet rs = null;
 		try {
diff --git a/briar-core/src/org/briarproject/invitation/AliceConnector.java b/briar-core/src/org/briarproject/invitation/AliceConnector.java
index 9c7fd31f3aa0d0adf90501dbb6b2170954998d20..f641af40a65124ff9df9ae4584910aa95fd1f52e 100644
--- a/briar-core/src/org/briarproject/invitation/AliceConnector.java
+++ b/briar-core/src/org/briarproject/invitation/AliceConnector.java
@@ -29,9 +29,7 @@ import org.briarproject.api.serial.ReaderFactory;
 import org.briarproject.api.serial.Writer;
 import org.briarproject.api.serial.WriterFactory;
 import org.briarproject.api.system.Clock;
-import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 
 /** A connection thread for the peer being Alice in the invitation protocol. */
@@ -51,9 +49,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
@@ -130,15 +128,16 @@ class AliceConnector extends Connector {
 		// Confirmation succeeded - upgrade to a secure connection
 		if(LOG.isLoggable(INFO))
 			LOG.info(pluginName + " confirmation succeeded");
-		int maxFrameLength = conn.getReader().getMaxFrameLength();
-		StreamReader streamReader =
+		// Create the readers
+		InputStream streamReader =
 				streamReaderFactory.createInvitationStreamReader(in,
-						maxFrameLength, secret, false); // Bob's stream
-		r = readerFactory.createReader(streamReader.getInputStream());
-		StreamWriter streamWriter =
+						secret, false); // Bob's stream
+		r = readerFactory.createReader(streamReader);
+		// Create the writers
+		OutputStream streamWriter =
 				streamWriterFactory.createInvitationStreamWriter(out,
-						maxFrameLength, secret, true); // Alice's stream
-		w = writerFactory.createWriter(streamWriter.getOutputStream());
+						secret, true); // Alice's stream
+		w = writerFactory.createWriter(streamWriter);
 		// Derive the invitation nonces
 		byte[][] nonces = crypto.deriveInvitationNonces(secret);
 		byte[] aliceNonce = nonces[0], bobNonce = nonces[1];
diff --git a/briar-core/src/org/briarproject/invitation/BobConnector.java b/briar-core/src/org/briarproject/invitation/BobConnector.java
index 205b3446ddbbb54d0dcb0080531318cc0d679ab1..dae8691920e1acf62cfc81c4af54feae020ba704 100644
--- a/briar-core/src/org/briarproject/invitation/BobConnector.java
+++ b/briar-core/src/org/briarproject/invitation/BobConnector.java
@@ -29,9 +29,7 @@ import org.briarproject.api.serial.ReaderFactory;
 import org.briarproject.api.serial.Writer;
 import org.briarproject.api.serial.WriterFactory;
 import org.briarproject.api.system.Clock;
-import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 
 /** A connection thread for the peer being Bob in the invitation protocol. */
@@ -51,9 +49,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
@@ -130,15 +128,16 @@ class BobConnector extends Connector {
 		// Confirmation succeeded - upgrade to a secure connection
 		if(LOG.isLoggable(INFO))
 			LOG.info(pluginName + " confirmation succeeded");
-		int maxFrameLength = conn.getReader().getMaxFrameLength();
-		StreamReader streamReader =
+		// Create the readers
+		InputStream streamReader =
 				streamReaderFactory.createInvitationStreamReader(in,
-						maxFrameLength, secret, true); // Alice's stream
-		r = readerFactory.createReader(streamReader.getInputStream());
-		StreamWriter streamWriter =
+						secret, true); // Alice's stream
+		r = readerFactory.createReader(streamReader);
+		// Create the writers
+		OutputStream streamWriter =
 				streamWriterFactory.createInvitationStreamWriter(out,
-						maxFrameLength, secret, false); // Bob's stream
-		w = writerFactory.createWriter(streamWriter.getOutputStream());
+						secret, false); // Bob's stream
+		w = writerFactory.createWriter(streamWriter);
 		// Derive the nonces
 		byte[][] nonces = crypto.deriveInvitationNonces(secret);
 		byte[] aliceNonce = nonces[0], bobNonce = nonces[1];
diff --git a/briar-core/src/org/briarproject/invitation/Connector.java b/briar-core/src/org/briarproject/invitation/Connector.java
index 9c0144620286c09c6bc48f28de6faf93b322434f..1fa00401217ad5717afd9f7f086adc47bb916d6f 100644
--- a/briar-core/src/org/briarproject/invitation/Connector.java
+++ b/briar-core/src/org/briarproject/invitation/Connector.java
@@ -285,7 +285,7 @@ abstract class Connector extends Thread {
 		db.setRemoteProperties(contactId, remoteProps);
 		// Create an endpoint for each transport shared with the contact
 		List<TransportId> ids = new ArrayList<TransportId>();
-		Map<TransportId, Long> latencies = db.getTransportLatencies();
+		Map<TransportId, Integer> latencies = db.getTransportLatencies();
 		for(TransportId id : localProps.keySet()) {
 			if(latencies.containsKey(id) && remoteProps.containsKey(id))
 				ids.add(id);
@@ -296,7 +296,7 @@ abstract class Connector extends Thread {
 		for(int i = 0; i < size; i++) {
 			TransportId id = ids.get(i);
 			Endpoint ep = new Endpoint(contactId, id, epoch, alice);
-			long maxLatency = latencies.get(id);
+			int maxLatency = latencies.get(id);
 			try {
 				db.addEndpoint(ep);
 			} catch(NoSuchTransportException e) {
diff --git a/briar-core/src/org/briarproject/messaging/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/messaging/DuplexOutgoingSession.java
index a656c94587b84f0296cbde6487e52c04cae0ba70..23177097f912900e73d18b20c320528d6aa5bda6 100644
--- a/briar-core/src/org/briarproject/messaging/DuplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/messaging/DuplexOutgoingSession.java
@@ -1,11 +1,11 @@
 package org.briarproject.messaging;
 
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
 
 import java.io.IOException;
-import java.io.OutputStream;
 import java.util.Collection;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executor;
@@ -36,7 +36,6 @@ import org.briarproject.api.messaging.Ack;
 import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.Offer;
 import org.briarproject.api.messaging.PacketWriter;
-import org.briarproject.api.messaging.PacketWriterFactory;
 import org.briarproject.api.messaging.Request;
 import org.briarproject.api.messaging.RetentionAck;
 import org.briarproject.api.messaging.RetentionUpdate;
@@ -44,16 +43,18 @@ 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.system.Clock;
 
 /**
  * An outgoing {@link org.briarproject.api.messaging.MessagingSession
  * MessagingSession} suitable for duplex transports. The session offers
  * messages before sending them, keeps its output stream open when there are no
- * more packets to send, and reacts to events that make packets available to
- * send.
+ * packets to send, and reacts to events that make packets available to send.
  */
 class DuplexOutgoingSession implements MessagingSession, EventListener {
 
+	// Check for retransmittable packets once every 60 seconds
+	private static final int RETX_QUERY_INTERVAL = 60 * 1000;
 	private static final Logger LOG =
 			Logger.getLogger(DuplexOutgoingSession.class.getName());
 
@@ -65,27 +66,32 @@ class DuplexOutgoingSession implements MessagingSession, EventListener {
 	private final DatabaseComponent db;
 	private final Executor dbExecutor;
 	private final EventBus eventBus;
+	private final Clock clock;
 	private final ContactId contactId;
 	private final TransportId transportId;
-	private final long maxLatency;
-	private final OutputStream out;
+	private final int maxLatency, maxIdleTime;
 	private final PacketWriter packetWriter;
 	private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
 
+	// The following must only be accessed on the writer thread
+	private long nextKeepalive = 0, nextRetxQuery = 0;
+	private boolean dataToFlush = true;
+
 	private volatile boolean interrupted = false;
 
 	DuplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
-			EventBus eventBus, PacketWriterFactory packetWriterFactory,
-			ContactId contactId, TransportId transportId, long maxLatency,
-			OutputStream out) {
+			EventBus eventBus, Clock clock, ContactId contactId,
+			TransportId transportId, int maxLatency, int maxIdleTime,
+			PacketWriter packetWriter) {
 		this.db = db;
 		this.dbExecutor = dbExecutor;
 		this.eventBus = eventBus;
+		this.clock = clock;
 		this.contactId = contactId;
 		this.transportId = transportId;
 		this.maxLatency = maxLatency;
-		this.out = out;
-		packetWriter = packetWriterFactory.createPacketWriter(out);
+		this.maxIdleTime = maxIdleTime;
+		this.packetWriter = packetWriter;
 		writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
 	}
 
@@ -103,16 +109,50 @@ class DuplexOutgoingSession implements MessagingSession, EventListener {
 			dbExecutor.execute(new GenerateBatch());
 			dbExecutor.execute(new GenerateOffer());
 			dbExecutor.execute(new GenerateRequest());
+			long now = clock.currentTimeMillis();
+			nextKeepalive = now + maxIdleTime;
+			nextRetxQuery = now + RETX_QUERY_INTERVAL;
 			// Write packets until interrupted
 			try {
 				while(!interrupted) {
-					// Flush the stream if it's going to be idle
-					if(writerTasks.isEmpty()) out.flush();
-					ThrowingRunnable<IOException> task = writerTasks.take();
-					if(task == CLOSE) break;
-					task.run();
+					// Work out how long we should wait for a packet
+					now = clock.currentTimeMillis();
+					long wait = Math.min(nextKeepalive, nextRetxQuery) - now;
+					if(wait < 0) wait = 0;
+					// Flush any unflushed data if we're going to wait
+					if(wait > 0 && dataToFlush && writerTasks.isEmpty()) {
+						packetWriter.flush();
+						dataToFlush = false;
+						nextKeepalive = now + maxIdleTime;
+					}
+					// Wait for a packet
+					ThrowingRunnable<IOException> task = writerTasks.poll(wait,
+							MILLISECONDS);
+					if(task == null) {
+						now = clock.currentTimeMillis();
+						if(now >= nextRetxQuery) {
+							// Check for retransmittable packets
+							dbExecutor.execute(new GenerateTransportUpdates());
+							dbExecutor.execute(new GenerateSubscriptionUpdate());
+							dbExecutor.execute(new GenerateRetentionUpdate());
+							dbExecutor.execute(new GenerateBatch());
+							dbExecutor.execute(new GenerateOffer());
+							nextRetxQuery = now + RETX_QUERY_INTERVAL;
+						}
+						if(now >= nextKeepalive) {
+							// Flush the stream to keep it alive
+							packetWriter.flush();
+							dataToFlush = false;
+							nextKeepalive = now + maxIdleTime;
+						}
+					} else if(task == CLOSE) {
+						break;
+					} else {
+						task.run();
+						dataToFlush = true;
+					}
 				}
-				out.flush();
+				if(dataToFlush) packetWriter.flush();
 			} catch(InterruptedException e) {
 				LOG.info("Interrupted while waiting for a packet to write");
 				Thread.currentThread().interrupt();
diff --git a/briar-core/src/org/briarproject/messaging/IncomingSession.java b/briar-core/src/org/briarproject/messaging/IncomingSession.java
index b85ccda0db618ae8aef73072f6d1602913f8f91a..1bd8acd66cbdeee28ade9ef0537cbf1a715e174e 100644
--- a/briar-core/src/org/briarproject/messaging/IncomingSession.java
+++ b/briar-core/src/org/briarproject/messaging/IncomingSession.java
@@ -3,7 +3,6 @@ package org.briarproject.messaging;
 import static java.util.logging.Level.WARNING;
 
 import java.io.IOException;
-import java.io.InputStream;
 import java.security.GeneralSecurityException;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
@@ -25,7 +24,6 @@ import org.briarproject.api.messaging.MessageVerifier;
 import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.Offer;
 import org.briarproject.api.messaging.PacketReader;
-import org.briarproject.api.messaging.PacketReaderFactory;
 import org.briarproject.api.messaging.Request;
 import org.briarproject.api.messaging.RetentionAck;
 import org.briarproject.api.messaging.RetentionUpdate;
@@ -56,9 +54,8 @@ class IncomingSession implements MessagingSession, EventListener {
 
 	IncomingSession(DatabaseComponent db, Executor dbExecutor,
 			Executor cryptoExecutor, EventBus eventBus,
-			MessageVerifier messageVerifier,
-			PacketReaderFactory packetReaderFactory, ContactId contactId,
-			TransportId transportId, InputStream in) {
+			MessageVerifier messageVerifier, ContactId contactId,
+			TransportId transportId, PacketReader packetReader) {
 		this.db = db;
 		this.dbExecutor = dbExecutor;
 		this.cryptoExecutor = cryptoExecutor;
@@ -66,7 +63,7 @@ class IncomingSession implements MessagingSession, EventListener {
 		this.messageVerifier = messageVerifier;
 		this.contactId = contactId;
 		this.transportId = transportId;
-		packetReader = packetReaderFactory.createPacketReader(in);
+		this.packetReader = packetReader;
 	}
 
 	public void run() throws IOException {
diff --git a/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java b/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java
index 04895580ccdd61b606970c60e30c672c5ffbfdb5..c7a31f6094642f983c2549ff460b989c3579f6c5 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingSessionFactoryImpl.java
@@ -15,8 +15,11 @@ import org.briarproject.api.event.EventBus;
 import org.briarproject.api.messaging.MessageVerifier;
 import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.MessagingSessionFactory;
+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.system.Clock;
 
 class MessagingSessionFactoryImpl implements MessagingSessionFactory {
 
@@ -24,6 +27,7 @@ class MessagingSessionFactoryImpl implements MessagingSessionFactory {
 	private final Executor dbExecutor, cryptoExecutor;
 	private final MessageVerifier messageVerifier;
 	private final EventBus eventBus;
+	private final Clock clock;
 	private final PacketReaderFactory packetReaderFactory;
 	private final PacketWriterFactory packetWriterFactory;
 
@@ -31,7 +35,7 @@ class MessagingSessionFactoryImpl implements MessagingSessionFactory {
 	MessagingSessionFactoryImpl(DatabaseComponent db,
 			@DatabaseExecutor Executor dbExecutor,
 			@CryptoExecutor Executor cryptoExecutor,
-			MessageVerifier messageVerifier, EventBus eventBus,
+			MessageVerifier messageVerifier, EventBus eventBus, Clock clock,
 			PacketReaderFactory packetReaderFactory,
 			PacketWriterFactory packetWriterFactory) {
 		this.db = db;
@@ -39,21 +43,29 @@ class MessagingSessionFactoryImpl implements MessagingSessionFactory {
 		this.cryptoExecutor = cryptoExecutor;
 		this.messageVerifier = messageVerifier;
 		this.eventBus = eventBus;
+		this.clock = clock;
 		this.packetReaderFactory = packetReaderFactory;
 		this.packetWriterFactory = packetWriterFactory;
 	}
 
 	public MessagingSession createIncomingSession(ContactId c, TransportId t,
 			InputStream in) {
+		PacketReader packetReader = packetReaderFactory.createPacketReader(in);
 		return new IncomingSession(db, dbExecutor, cryptoExecutor, eventBus,
-				messageVerifier, packetReaderFactory, c, t, in);
+				messageVerifier, c, t, packetReader);
 	}
 
-	public MessagingSession createOutgoingSession(ContactId c, TransportId t,
-			long maxLatency, boolean duplex, OutputStream out) {
-		if(duplex) return new DuplexOutgoingSession(db, dbExecutor, eventBus,
-				packetWriterFactory, c, t, maxLatency, out);
-		else return new SimplexOutgoingSession(db, dbExecutor, eventBus,
-				packetWriterFactory, c, t, maxLatency, out);
+	public MessagingSession createSimplexOutgoingSession(ContactId c,
+			TransportId t, int maxLatency, OutputStream out) {
+		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out);
+		return new SimplexOutgoingSession(db, dbExecutor, eventBus, c, t,
+				maxLatency, packetWriter);
+	}
+
+	public MessagingSession createDuplexOutgoingSession(ContactId c,
+			TransportId t, int maxLatency, int maxIdleTime, OutputStream out) {
+		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out);
+		return new DuplexOutgoingSession(db, dbExecutor, eventBus, clock, c, t,
+				maxLatency, maxIdleTime, packetWriter);
 	}
 }
diff --git a/briar-core/src/org/briarproject/messaging/PacketWriterImpl.java b/briar-core/src/org/briarproject/messaging/PacketWriterImpl.java
index 4b0efd4a7ea5fcec06f1be25205502458717b7bf..1ef8e348c92c4efc555f94c909c757fb674d079f 100644
--- a/briar-core/src/org/briarproject/messaging/PacketWriterImpl.java
+++ b/briar-core/src/org/briarproject/messaging/PacketWriterImpl.java
@@ -143,4 +143,8 @@ class PacketWriterImpl implements PacketWriter {
 		w.writeInteger(u.getVersion());
 		w.writeStructEnd();
 	}
+
+	public void flush() throws IOException {
+		out.flush();
+	}
 }
diff --git a/briar-core/src/org/briarproject/messaging/SimplexOutgoingSession.java b/briar-core/src/org/briarproject/messaging/SimplexOutgoingSession.java
index 51ec4d81a49d124a5d2db805b9c47f12c1070870..6d8dfabf1ae815bdebaba6fef642a40717c7368f 100644
--- a/briar-core/src/org/briarproject/messaging/SimplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/messaging/SimplexOutgoingSession.java
@@ -5,7 +5,6 @@ import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PACKET_LENGTH;
 
 import java.io.IOException;
-import java.io.OutputStream;
 import java.util.Collection;
 import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.Executor;
@@ -26,7 +25,6 @@ import org.briarproject.api.event.TransportRemovedEvent;
 import org.briarproject.api.messaging.Ack;
 import org.briarproject.api.messaging.MessagingSession;
 import org.briarproject.api.messaging.PacketWriter;
-import org.briarproject.api.messaging.PacketWriterFactory;
 import org.briarproject.api.messaging.RetentionAck;
 import org.briarproject.api.messaging.RetentionUpdate;
 import org.briarproject.api.messaging.SubscriptionAck;
@@ -55,8 +53,7 @@ class SimplexOutgoingSession implements MessagingSession, EventListener {
 	private final EventBus eventBus;
 	private final ContactId contactId;
 	private final TransportId transportId;
-	private final long maxLatency;
-	private final OutputStream out;
+	private final int maxLatency;
 	private final PacketWriter packetWriter;
 	private final AtomicInteger outstandingQueries;
 	private final BlockingQueue<ThrowingRunnable<IOException>> writerTasks;
@@ -64,17 +61,15 @@ class SimplexOutgoingSession implements MessagingSession, EventListener {
 	private volatile boolean interrupted = false;
 
 	SimplexOutgoingSession(DatabaseComponent db, Executor dbExecutor,
-			EventBus eventBus, PacketWriterFactory packetWriterFactory,
-			ContactId contactId, TransportId transportId, long maxLatency,
-			OutputStream out) {
+			EventBus eventBus, ContactId contactId, TransportId transportId,
+			int maxLatency, PacketWriter packetWriter) {
 		this.db = db;
 		this.dbExecutor = dbExecutor;
 		this.eventBus = eventBus;
 		this.contactId = contactId;
 		this.transportId = transportId;
 		this.maxLatency = maxLatency;
-		this.out = out;
-		packetWriter = packetWriterFactory.createPacketWriter(out);
+		this.packetWriter = packetWriter;
 		outstandingQueries = new AtomicInteger(8); // One per type of packet
 		writerTasks = new LinkedBlockingQueue<ThrowingRunnable<IOException>>();
 	}
@@ -98,7 +93,7 @@ class SimplexOutgoingSession implements MessagingSession, EventListener {
 					if(task == CLOSE) break;
 					task.run();
 				}
-				out.flush();
+				packetWriter.flush();
 			} catch(InterruptedException e) {
 				LOG.info("Interrupted while waiting for a packet to write");
 				Thread.currentThread().interrupt();
diff --git a/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
index d94e1f80ec0a562b5b6eed312ef4120267efd4ad..b7cb70c1bd8fc9b90560145603d1d49722c8992c 100644
--- a/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
@@ -6,6 +6,7 @@ import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
@@ -24,12 +25,9 @@ import org.briarproject.api.plugins.TransportConnectionReader;
 import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 import org.briarproject.api.transport.TagRecogniser;
-import org.briarproject.util.ByteUtils;
 
 class ConnectionManagerImpl implements ConnectionManager {
 
@@ -96,28 +94,28 @@ class ConnectionManagerImpl implements ConnectionManager {
 
 	private MessagingSession createIncomingSession(StreamContext ctx,
 			TransportConnectionReader r) throws IOException {
-		try {
-			StreamReader streamReader = streamReaderFactory.createStreamReader(
-					r.getInputStream(), r.getMaxFrameLength(), ctx);
-			return messagingSessionFactory.createIncomingSession(
-					ctx.getContactId(), ctx.getTransportId(),
-					streamReader.getInputStream());
-		} finally {
-			ByteUtils.erase(ctx.getSecret());
-		}
+		InputStream streamReader = streamReaderFactory.createStreamReader(
+				r.getInputStream(), ctx);
+		return messagingSessionFactory.createIncomingSession(
+				ctx.getContactId(), ctx.getTransportId(), streamReader);
 	}
 
-	private MessagingSession createOutgoingSession(StreamContext ctx,
-			TransportConnectionWriter w, boolean duplex) throws IOException {
-		try {
-			StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
-					w.getOutputStream(), w.getMaxFrameLength(), ctx);
-			return messagingSessionFactory.createOutgoingSession(
-					ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
-					duplex, streamWriter.getOutputStream());
-		} finally {
-			ByteUtils.erase(ctx.getSecret());
-		}
+	private MessagingSession createSimplexOutgoingSession(StreamContext ctx,
+			TransportConnectionWriter w) throws IOException {
+		OutputStream streamWriter = streamWriterFactory.createStreamWriter(
+				w.getOutputStream(), ctx);
+		return messagingSessionFactory.createSimplexOutgoingSession(
+				ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
+				streamWriter);
+	}
+
+	private MessagingSession createDuplexOutgoingSession(StreamContext ctx,
+			TransportConnectionWriter w) throws IOException {
+		OutputStream streamWriter = streamWriterFactory.createStreamWriter(
+				w.getOutputStream(), ctx);
+		return messagingSessionFactory.createDuplexOutgoingSession(
+				ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
+				w.getMaxIdleTime(), streamWriter);
 	}
 
 	private class ManageIncomingSimplexConnection implements Runnable {
@@ -199,7 +197,7 @@ class ConnectionManagerImpl implements ConnectionManager {
 			connectionRegistry.registerConnection(contactId, transportId);
 			try {
 				// Create and run the outgoing session
-				createOutgoingSession(ctx, writer, false).run();
+				createSimplexOutgoingSession(ctx, writer).run();
 				disposeWriter(false);
 			} catch(IOException e) {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
@@ -287,7 +285,7 @@ class ConnectionManagerImpl implements ConnectionManager {
 			}
 			try {
 				// Create and run the outgoing session
-				outgoingSession = createOutgoingSession(ctx, writer, true);
+				outgoingSession = createDuplexOutgoingSession(ctx, writer);
 				outgoingSession.run();
 				disposeWriter(false);
 			} catch(IOException e) {
@@ -353,7 +351,7 @@ class ConnectionManagerImpl implements ConnectionManager {
 			});
 			try {
 				// Create and run the outgoing session
-				outgoingSession = createOutgoingSession(ctx, writer, true);
+				outgoingSession = createDuplexOutgoingSession(ctx, writer);
 				outgoingSession.run();
 				disposeWriter(false);
 			} catch(IOException e) {
diff --git a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
index 1af4215ba6afb3fdf547cebc033cb07dde57e157..abaa6349256d34c3d8e663922f78e2d28e5b74dd 100644
--- a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
+++ b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
@@ -28,8 +28,7 @@ public abstract class FilePlugin implements SimplexPlugin {
 	protected final Executor ioExecutor;
 	protected final FileUtils fileUtils;
 	protected final SimplexPluginCallback callback;
-	protected final int maxFrameLength;
-	protected final long maxLatency;
+	protected final int maxLatency;
 
 	protected volatile boolean running = false;
 
@@ -39,21 +38,19 @@ public abstract class FilePlugin implements SimplexPlugin {
 	protected abstract void readerFinished(File f);
 
 	protected FilePlugin(Executor ioExecutor, FileUtils fileUtils,
-			SimplexPluginCallback callback, int maxFrameLength,
-			long maxLatency) {
+			SimplexPluginCallback callback, int maxLatency) {
 		this.ioExecutor = ioExecutor;
 		this.fileUtils = fileUtils;
 		this.callback = callback;
-		this.maxFrameLength = maxFrameLength;
 		this.maxLatency = maxLatency;
 	}
 
-	public int getMaxFrameLength() {
-		return maxFrameLength;
+	public int getMaxLatency() {
+		return maxLatency;
 	}
 
-	public long getMaxLatency() {
-		return maxLatency;
+	public int getMaxIdleTime() {
+		return Integer.MAX_VALUE; // We don't need keepalives
 	}
 
 	public boolean isRunning() {
diff --git a/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java b/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java
index 316773ec01270fc0b45282edd62a6b40785985e5..4c434faa621a94788ad9b0f3649fb61518081de3 100644
--- a/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java
+++ b/briar-core/src/org/briarproject/plugins/file/FileTransportReader.java
@@ -24,10 +24,6 @@ class FileTransportReader implements TransportConnectionReader {
 		this.plugin = plugin;
 	}
 
-	public int getMaxFrameLength() {
-		return plugin.getMaxFrameLength();
-	}
-
 	public long getMaxLatency() {
 		return plugin.getMaxLatency();
 	}
diff --git a/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java b/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java
index 2ca55593fc6cdb63b0789a4e9dac14a233b335d4..f9b14651e1e4a3ed8b4a3913faf3511f7795bd3d 100644
--- a/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java
+++ b/briar-core/src/org/briarproject/plugins/file/FileTransportWriter.java
@@ -27,12 +27,12 @@ class FileTransportWriter implements TransportConnectionWriter {
 		this.plugin = plugin;
 	}
 
-	public int getMaxFrameLength() {
-		return plugin.getMaxFrameLength();
+	public int getMaxLatency() {
+		return plugin.getMaxLatency();
 	}
 
-	public long getMaxLatency() {
-		return plugin.getMaxLatency();
+	public int getMaxIdleTime() {
+		return plugin.getMaxIdleTime();
 	}
 
 	public long getCapacity() {
diff --git a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
index 39e4b94184f48dce3b6c073903713b7a59f34aba..ea030c40ee629ed527a9393dc51fd04bb5a9a713 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
@@ -17,9 +17,8 @@ class LanTcpPlugin extends TcpPlugin {
 	static final TransportId ID = new TransportId("lan");
 
 	LanTcpPlugin(Executor ioExecutor, DuplexPluginCallback callback,
-			int maxFrameLength, long maxLatency, long pollingInterval) {
-		super(ioExecutor, callback, maxFrameLength, maxLatency,
-				pollingInterval);
+			int maxLatency, int maxIdleTime, int pollingInterval) {
+		super(ioExecutor, callback, maxLatency, maxIdleTime, pollingInterval);
 	}
 
 	public TransportId getId() {
diff --git a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPluginFactory.java b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPluginFactory.java
index 02b53eab951ec8326377073e2a01bdc5d5b2559d..68f2edc0e77aab769e242a3e63f4334376ac5083 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPluginFactory.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPluginFactory.java
@@ -9,9 +9,9 @@ import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
 
 public class LanTcpPluginFactory implements DuplexPluginFactory {
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 60 * 1000; // 1 minute
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+	private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
+	private static final int POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
 
 	private final Executor ioExecutor;
 
@@ -24,7 +24,7 @@ public class LanTcpPluginFactory implements DuplexPluginFactory {
 	}
 
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
-		return new LanTcpPlugin(ioExecutor, callback, MAX_FRAME_LENGTH,
-				MAX_LATENCY, POLLING_INTERVAL);
+		return new LanTcpPlugin(ioExecutor, callback,  MAX_LATENCY,
+				MAX_IDLE_TIME, POLLING_INTERVAL);
 	}
 }
diff --git a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
index a8d3e8dde839007fbe89ff25e6a22bed292627c4..083f46598d04095aa9e3a094901462f3c421afef 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
@@ -37,8 +37,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 
 	protected final Executor ioExecutor;
 	protected final DuplexPluginCallback callback;
-	protected final int maxFrameLength;
-	protected final long maxLatency, pollingInterval;
+	protected final int maxLatency, maxIdleTime, pollingInterval, socketTimeout;
 
 	protected volatile boolean running = false;
 	protected volatile ServerSocket socket = null;
@@ -53,20 +52,23 @@ abstract class TcpPlugin implements DuplexPlugin {
 	protected abstract boolean isConnectable(InetSocketAddress remote);
 
 	protected TcpPlugin(Executor ioExecutor, DuplexPluginCallback callback,
-			int maxFrameLength, long maxLatency, long pollingInterval) {
+			int maxLatency, int maxIdleTime, int pollingInterval) {
 		this.ioExecutor = ioExecutor;
 		this.callback = callback;
-		this.maxFrameLength = maxFrameLength;
 		this.maxLatency = maxLatency;
+		this.maxIdleTime = maxIdleTime;
 		this.pollingInterval = pollingInterval;
+		if(maxIdleTime > Integer.MAX_VALUE / 2)
+			socketTimeout = Integer.MAX_VALUE;
+		else socketTimeout = maxIdleTime * 2;
 	}
 
-	public int getMaxFrameLength() {
-		return maxFrameLength;
+	public int getMaxLatency() {
+		return maxLatency;
 	}
 
-	public long getMaxLatency() {
-		return maxLatency;
+	public int getMaxIdleTime() {
+		return maxIdleTime;
 	}
 
 	public boolean start() {
@@ -136,6 +138,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 			Socket s;
 			try {
 				s = socket.accept();
+				s.setSoTimeout(socketTimeout);
 			} catch(IOException e) {
 				// This is expected when the socket is closed
 				if(LOG.isLoggable(INFO)) LOG.info(e.toString());
@@ -161,7 +164,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 		return true;
 	}
 
-	public long getPollingInterval() {
+	public int getPollingInterval() {
 		return pollingInterval;
 	}
 
@@ -195,6 +198,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 		try {
 			if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + remote);
 			s.connect(remote);
+			s.setSoTimeout(socketTimeout);
 			if(LOG.isLoggable(INFO)) LOG.info("Connected to " + remote);
 			return new TcpTransportConnection(this, s);
 		} catch(IOException e) {
diff --git a/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java b/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java
index 83506a211682a611ddf4a6918079336e4530eaa4..aae3c53ba86ba2232d0f8313f0ce27983d058ea4 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/TcpTransportConnection.java
@@ -38,10 +38,6 @@ class TcpTransportConnection implements DuplexTransportConnection {
 
 	private class Reader implements TransportConnectionReader {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
-		}
-
 		public long getMaxLatency() {
 			return plugin.getMaxLatency();
 		}
@@ -59,12 +55,12 @@ class TcpTransportConnection implements DuplexTransportConnection {
 
 	private class Writer implements TransportConnectionWriter {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
+		public int getMaxLatency() {
+			return plugin.getMaxLatency();
 		}
 
-		public long getMaxLatency() {
-			return plugin.getMaxLatency();
+		public int getMaxIdleTime() {
+			return plugin.getMaxIdleTime();
 		}
 
 		public long getCapacity() {
diff --git a/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
index 6e2e3e754cf814278a0929ad5363db17d50bb466..15a4dfde82ebfc8e4094decafecb92ad257915b5 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
@@ -20,11 +20,10 @@ class WanTcpPlugin extends TcpPlugin {
 
 	private volatile MappingResult mappingResult;
 
-	WanTcpPlugin(Executor ioExecutor, DuplexPluginCallback callback,
-			int maxFrameLength, long maxLatency, long pollingInterval,
-			PortMapper portMapper) {
-		super(ioExecutor, callback, maxFrameLength, maxLatency,
-				pollingInterval);
+	WanTcpPlugin(Executor ioExecutor, PortMapper portMapper,
+			DuplexPluginCallback callback, int maxLatency, int maxIdleTime,
+			int pollingInterval) {
+		super(ioExecutor, callback, maxLatency, maxIdleTime, pollingInterval);
 		this.portMapper = portMapper;
 	}
 
diff --git a/briar-core/src/org/briarproject/plugins/tcp/WanTcpPluginFactory.java b/briar-core/src/org/briarproject/plugins/tcp/WanTcpPluginFactory.java
index f478bbc295fe13b3825536309fadf3be9f590dcc..bd326ebbc0e8281f47511ceae223cc536caaa25d 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/WanTcpPluginFactory.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/WanTcpPluginFactory.java
@@ -10,9 +10,9 @@ import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
 
 public class WanTcpPluginFactory implements DuplexPluginFactory {
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 5 * 60 * 1000; // 5 minutes
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+	private static final int MAX_IDLE_TIME = 30 * 1000; // 30 seconds
+	private static final int POLLING_INTERVAL = 5 * 60 * 1000; // 5 minutes
 
 	private final Executor ioExecutor;
 	private final ShutdownManager shutdownManager;
@@ -28,8 +28,7 @@ public class WanTcpPluginFactory implements DuplexPluginFactory {
 	}
 
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
-		return new WanTcpPlugin(ioExecutor, callback, MAX_FRAME_LENGTH,
-				MAX_LATENCY, POLLING_INTERVAL,
-				new PortMapperImpl(shutdownManager));
+		return new WanTcpPlugin(ioExecutor, new PortMapperImpl(shutdownManager),
+				callback, MAX_LATENCY, MAX_IDLE_TIME, POLLING_INTERVAL);
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/FrameReader.java b/briar-core/src/org/briarproject/transport/FrameReader.java
deleted file mode 100644
index 8284c47b8d4e322a8f18cc57ae071d8f2bef97c5..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/transport/FrameReader.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.briarproject.transport;
-
-import java.io.IOException;
-
-interface FrameReader {
-
-	/**
-	 * Reads a frame into the given buffer and returns its payload length, or
-	 * -1 if no more frames can be read from the connection.
-	 */
-	int readFrame(byte[] frame) throws IOException;
-}
diff --git a/briar-core/src/org/briarproject/transport/FrameWriter.java b/briar-core/src/org/briarproject/transport/FrameWriter.java
deleted file mode 100644
index 4f29b7999a08fa1c7964fc65140124232f0b9028..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/transport/FrameWriter.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package org.briarproject.transport;
-
-import java.io.IOException;
-
-interface FrameWriter {
-
-	/** Writes the given frame. */
-	void writeFrame(byte[] frame, int payloadLength, boolean finalFrame)
-			throws IOException;
-
-	/** Flushes the stream. */
-	void flush() throws IOException;
-}
diff --git a/briar-core/src/org/briarproject/transport/IncomingEncryptionLayer.java b/briar-core/src/org/briarproject/transport/IncomingEncryptionLayer.java
deleted file mode 100644
index e5e4381d8cfcccf4af9444bc798026687995edf5..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/transport/IncomingEncryptionLayer.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package org.briarproject.transport;
-
-import static org.briarproject.api.transport.TransportConstants.AAD_LENGTH;
-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 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.AuthenticatedCipher;
-import org.briarproject.api.crypto.SecretKey;
-
-class IncomingEncryptionLayer implements FrameReader {
-
-	private final InputStream in;
-	private final AuthenticatedCipher frameCipher;
-	private final SecretKey frameKey;
-	private final byte[] iv, aad, ciphertext;
-	private final int frameLength;
-
-	private long frameNumber;
-	private boolean finalFrame;
-
-	IncomingEncryptionLayer(InputStream in, AuthenticatedCipher frameCipher,
-			SecretKey frameKey, int frameLength) {
-		this.in = in;
-		this.frameCipher = frameCipher;
-		this.frameKey = frameKey;
-		this.frameLength = frameLength;
-		iv = new byte[IV_LENGTH];
-		aad = new byte[AAD_LENGTH];
-		ciphertext = new byte[frameLength];
-		frameNumber = 0;
-		finalFrame = false;
-	}
-
-	public int readFrame(byte[] frame) throws IOException {
-		if(finalFrame) return -1;
-		// Read the frame
-		int ciphertextLength = 0;
-		try {
-			while(ciphertextLength < frameLength) {
-				int read = in.read(ciphertext, ciphertextLength,
-						frameLength - ciphertextLength);
-				if(read == -1) break; // We'll check the length later
-				ciphertextLength += read;
-			}
-		} catch(IOException e) {
-			frameKey.erase();
-			throw e;
-		}
-		int plaintextLength = ciphertextLength - MAC_LENGTH;
-		if(plaintextLength < HEADER_LENGTH) throw new EOFException();
-		// Decrypt and authenticate the frame
-		FrameEncoder.encodeIv(iv, frameNumber);
-		FrameEncoder.encodeAad(aad, frameNumber, plaintextLength);
-		try {
-			frameCipher.init(false, frameKey, iv, aad);
-			int decrypted = frameCipher.doFinal(ciphertext, 0, ciphertextLength,
-					frame, 0);
-			if(decrypted != plaintextLength) throw new RuntimeException();
-		} catch(GeneralSecurityException e) {
-			throw new FormatException();
-		}
-		// Decode and validate the header
-		finalFrame = FrameEncoder.isFinalFrame(frame);
-		if(!finalFrame && ciphertextLength < frameLength)
-			throw new FormatException();
-		int payloadLength = FrameEncoder.getPayloadLength(frame);
-		if(payloadLength > plaintextLength - HEADER_LENGTH)
-			throw new FormatException();
-		// If there's any padding it must be all zeroes
-		for(int i = HEADER_LENGTH + payloadLength; i < plaintextLength; i++) {
-			if(frame[i] != 0) throw new FormatException();
-		}
-		frameNumber++;
-		return payloadLength;
-	}
-}
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
index 6ab1688506300266c960a55e95ab3b5534e8ea1b..d6ec3aa675244c7e491ec0ea8ea59bb0a4564ca2 100644
--- a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
+++ b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java
@@ -35,7 +35,6 @@ 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;
 
 // FIXME: Don't make alien calls with a lock held
 class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
@@ -52,7 +51,7 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 	private final Clock clock;
 	private final Timer timer;
 
-	private final Map<TransportId, Long> maxLatencies;
+	private final Map<TransportId, Integer> maxLatencies;
 	private final Map<EndpointKey, TemporarySecret> oldSecrets;
 	private final Map<EndpointKey, TemporarySecret> currentSecrets;
 	private final Map<EndpointKey, TemporarySecret> newSecrets;
@@ -69,7 +68,7 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 		this.tagRecogniser = tagRecogniser;
 		this.clock = clock;
 		this.timer = timer;
-		maxLatencies = new HashMap<TransportId, Long>();
+		maxLatencies = new HashMap<TransportId, Integer>();
 		oldSecrets = new HashMap<EndpointKey, TemporarySecret>();
 		currentSecrets = new HashMap<EndpointKey, TemporarySecret>();
 		newSecrets = new HashMap<EndpointKey, TemporarySecret>();
@@ -124,10 +123,9 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 		Collection<TemporarySecret> dead = new ArrayList<TemporarySecret>();
 		for(TemporarySecret s : secrets) {
 			// Discard the secret if the transport has been removed
-			Long maxLatency = maxLatencies.get(s.getTransportId());
+			Integer maxLatency = maxLatencies.get(s.getTransportId());
 			if(maxLatency == null) {
 				LOG.info("Discarding obsolete secret");
-				ByteUtils.erase(s.getSecret());
 				continue;
 			}
 			long rotation = maxLatency + MAX_CLOCK_DIFFERENCE;
@@ -151,7 +149,7 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 		return dead;
 	}
 
-	// Replaces and erases the given secrets and returns any secrets created
+	// Replaces the given secrets and returns any secrets created
 	private Collection<TemporarySecret> replaceDeadSecrets(long now,
 			Collection<TemporarySecret> dead) {
 		// If there are several dead secrets for an endpoint, use the newest
@@ -164,18 +162,16 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 				// There's no other secret for this endpoint
 				newest.put(k, s);
 			} else if(exists.getPeriod() < s.getPeriod()) {
-				// There's an older secret - erase it and use this one instead
-				ByteUtils.erase(exists.getSecret());
+				// There's an older secret - use this one instead
 				newest.put(k, s);
 			} else {
-				// There's a newer secret - erase this one
-				ByteUtils.erase(s.getSecret());
+				// 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();
-			Long maxLatency = maxLatencies.get(s.getTransportId());
+			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();
@@ -186,34 +182,23 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 				throw new IllegalStateException();
 			// Derive the old, current and new secrets
 			byte[] b1 = s.getSecret();
-			for(long p = s.getPeriod() + 1; p < period; p++) {
-				byte[] temp = crypto.deriveNextSecret(b1, p);
-				ByteUtils.erase(b1);
-				b1 = temp;
-			}
+			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 - copies may already
-			// exist, in which case erase the new copies (the old copies are
-			// referenced by the connection recogniser)
+			// Add the secrets to their respective maps if not already present
 			EndpointKey k = e.getKey();
-			if(oldSecrets.containsKey(k)) {
-				ByteUtils.erase(b1);
-			} else {
+			if(!oldSecrets.containsKey(k)) {
 				TemporarySecret s1 = new TemporarySecret(s, period - 1, b1);
 				oldSecrets.put(k, s1);
 				created.add(s1);
 			}
-			if(currentSecrets.containsKey(k)) {
-				ByteUtils.erase(b2);
-			} else {
+			if(!currentSecrets.containsKey(k)) {
 				TemporarySecret s2 = new TemporarySecret(s, period, b2);
 				currentSecrets.put(k, s2);
 				created.add(s2);
 			}
-			if(newSecrets.containsKey(k)) {
-				ByteUtils.erase(b3);
-			} else {
+			if(!newSecrets.containsKey(k)) {
 				TemporarySecret s3 = new TemporarySecret(s, period + 1, b3);
 				newSecrets.put(k, s3);
 				created.add(s3);
@@ -229,9 +214,9 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 			timer.cancel();
 			tagRecogniser.removeSecrets();
 			maxLatencies.clear();
-			removeAndEraseSecrets(oldSecrets);
-			removeAndEraseSecrets(currentSecrets);
-			removeAndEraseSecrets(newSecrets);
+			oldSecrets.clear();
+			currentSecrets.clear();
+			newSecrets.clear();
 			return true;
 		}
 		finally{
@@ -239,11 +224,6 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 		}
 	}
 
-	private void removeAndEraseSecrets(Map<?, TemporarySecret> m) {
-		for(TemporarySecret s : m.values()) ByteUtils.erase(s.getSecret());
-		m.clear();
-	}
-
 	public StreamContext getStreamContext(ContactId c,
 			TransportId t) {
 		synchLock.lock();
@@ -264,8 +244,7 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 				if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 				return null;
 			}
-			// Clone the secret - the original will be erased
-			byte[] secret = s.getSecret().clone();
+		byte[] secret = s.getSecret();
 			return new StreamContext(c, t, secret, streamNumber, s.getAlice());
 		}
 		finally{
@@ -273,7 +252,7 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 		}
 	}
 
-	public void endpointAdded(Endpoint ep, long maxLatency,
+	public synchronized void endpointAdded(Endpoint ep, int maxLatency,
 			byte[] initialSecret) {
 		synchLock.lock();
 		try{
@@ -285,11 +264,8 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 			if(period < 1) throw new IllegalStateException();
 			// Derive the old, current and new secrets
 			byte[] b1 = initialSecret;
-			for(long p = 0; p < period; p++) {
-				byte[] temp = crypto.deriveNextSecret(b1, p);
-				ByteUtils.erase(b1);
-				b1 = temp;
-			}
+			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);
@@ -370,27 +346,16 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 		}
 	}
 
-	private void removeAndEraseSecrets(ContactId c, Map<?, TemporarySecret> m) {
+	private void removeSecrets(ContactId c, Map<?, TemporarySecret> m) {
 		Iterator<TemporarySecret> it = m.values().iterator();
-		while(it.hasNext()) {
-			TemporarySecret s = it.next();
-			if(s.getContactId().equals(c)) {
-				ByteUtils.erase(s.getSecret());
-				it.remove();
-			}
-		}
+		while(it.hasNext())
+			if(it.next().getContactId().equals(c)) it.remove();
 	}
 
-	private void removeAndEraseSecrets(TransportId t,
-			Map<?, TemporarySecret> m) {
+	private void removeSecrets(TransportId t, Map<?, TemporarySecret> m) {
 		Iterator<TemporarySecret> it = m.values().iterator();
-		while(it.hasNext()) {
-			TemporarySecret s = it.next();
-			if(s.getTransportId().equals(t)) {
-				ByteUtils.erase(s.getSecret());
-				it.remove();
-			}
-		}
+		while(it.hasNext())
+			if(it.next().getTransportId().equals(t)) it.remove();
 	}
 
 	private static class EndpointKey {
@@ -436,10 +401,10 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 			ContactId c = event.getContactId();
 			tagRecogniser.removeSecrets(c);
 			synchLock.lock();
-			try {
-				removeAndEraseSecrets(c, oldSecrets);
-				removeAndEraseSecrets(c, currentSecrets);
-				removeAndEraseSecrets(c, newSecrets);
+			try{
+				removeSecrets(c, oldSecrets);
+				removeSecrets(c, currentSecrets);
+				removeSecrets(c, newSecrets);
 			}
 			finally{
 				synchLock.unlock();
@@ -482,9 +447,9 @@ class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
 			synchLock.lock();
 			try {
 				maxLatencies.remove(t);
-				removeAndEraseSecrets(t, oldSecrets);
-				removeAndEraseSecrets(t, currentSecrets);
-				removeAndEraseSecrets(t, newSecrets);
+				removeSecrets(t, oldSecrets);
+				removeSecrets(t, currentSecrets);
+				removeSecrets(t, newSecrets);
 			}
 			finally{
 				synchLock.unlock();
diff --git a/briar-core/src/org/briarproject/transport/KeyManagerImpl.java.orig b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java.orig
new file mode 100644
index 0000000000000000000000000000000000000000..88863dadaa19b1fdbec84aed9fa57809942fb228
--- /dev/null
+++ b/briar-core/src/org/briarproject/transport/KeyManagerImpl.java.orig
@@ -0,0 +1,546 @@
+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.DbException;
+import org.briarproject.api.event.ContactRemovedEvent;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventBus;
+import org.briarproject.api.event.EventListener;
+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.StreamContext;
+import org.briarproject.api.transport.TagRecogniser;
+import org.briarproject.api.transport.TemporarySecret;
+
+// FIXME: Don't make alien calls with a lock held
+class KeyManagerImpl extends TimerTask implements KeyManager, EventListener {
+
+	private static final int MS_BETWEEN_CHECKS = 60 * 1000;
+
+	private static final Logger LOG =
+			Logger.getLogger(KeyManagerImpl.class.getName());
+
+	private final CryptoComponent crypto;
+	private final DatabaseComponent db;
+	private final EventBus eventBus;
+	private final TagRecogniser tagRecogniser;
+	private final Clock clock;
+	private final Timer timer;
+
+<<<<<<< HEAD
+	private final Map<TransportId, Long> maxLatencies;
+=======
+	// All of the following are locking: this
+	private final Map<TransportId, Integer> maxLatencies;
+>>>>>>> theSource
+	private final Map<EndpointKey, TemporarySecret> oldSecrets;
+	private final Map<EndpointKey, TemporarySecret> currentSecrets;
+	private final Map<EndpointKey, TemporarySecret> newSecrets;
+
+	private final Lock synchLock = new ReentrantLock();
+
+	@Inject
+	KeyManagerImpl(CryptoComponent crypto, DatabaseComponent db,
+			EventBus eventBus, TagRecogniser tagRecogniser, Clock clock,
+			Timer timer) {
+		this.crypto = crypto;
+		this.db = db;
+		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>();
+	}
+
+	public boolean start() {
+		synchLock.lock();
+		try {
+			eventBus.addListener(this);
+			// Load the temporary secrets and transport latencies from the database
+			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{
+			synchLock.unlock();
+		}
+	}
+
+	// Assigns secrets to the appropriate maps and returns any dead secrets
+	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;
+	}
+
+<<<<<<< HEAD
+	// Replaces and erases the given secrets and returns any secrets created
+=======
+	// Replaces the given secrets and returns any secrets created
+	// Locking: this
+>>>>>>> theSource
+	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);
+			}
+		}
+		return created;
+	}
+
+<<<<<<< HEAD
+	public boolean stop() {
+		synchLock.lock();
+		try{
+			eventBus.removeListener(this);
+			timer.cancel();
+			tagRecogniser.removeSecrets();
+			maxLatencies.clear();
+			removeAndEraseSecrets(oldSecrets);
+			removeAndEraseSecrets(currentSecrets);
+			removeAndEraseSecrets(newSecrets);
+			return true;
+		}
+		finally{
+			synchLock.unlock();
+		}
+	}
+
+	private void removeAndEraseSecrets(Map<?, TemporarySecret> m) {
+		for(TemporarySecret s : m.values()) ByteUtils.erase(s.getSecret());
+		m.clear();
+	}
+
+	public StreamContext getStreamContext(ContactId c,
+=======
+	public synchronized boolean stop() {
+		eventBus.removeListener(this);
+		timer.cancel();
+		tagRecogniser.removeSecrets();
+		maxLatencies.clear();
+		oldSecrets.clear();
+		currentSecrets.clear();
+		newSecrets.clear();
+		return true;
+	}
+
+	public synchronized StreamContext getStreamContext(ContactId c,
+>>>>>>> theSource
+			TransportId t) {
+		synchLock.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;
+			}
+			// Clone the secret - the original will be erased
+			byte[] secret = s.getSecret().clone();
+			return new StreamContext(c, t, secret, streamNumber, s.getAlice());
+		}
+		finally{
+			synchLock.unlock();
+		}
+<<<<<<< HEAD
+	}
+
+	public void endpointAdded(Endpoint ep, long maxLatency,
+			byte[] initialSecret) {
+		synchLock.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++) {
+				byte[] temp = crypto.deriveNextSecret(b1, p);
+				ByteUtils.erase(b1);
+				b1 = temp;
+			}
+			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{
+			synchLock.unlock();
+=======
+		byte[] secret = s.getSecret();
+		return new StreamContext(c, t, secret, streamNumber, s.getAlice());
+	}
+
+	public synchronized void endpointAdded(Endpoint ep, int maxLatency,
+			byte[] initialSecret) {
+		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;
+>>>>>>> theSource
+		}
+	}
+
+	@Override
+	public void run() {
+		synchLock.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{
+			synchLock.unlock();
+		}
+	}
+
+	public void eventOccurred(Event e) {
+		if(e instanceof ContactRemovedEvent) {
+			ContactRemovedEvent c = (ContactRemovedEvent) e;
+			timer.schedule(new ContactRemovedTask(c), 0);
+		} else if(e instanceof TransportAddedEvent) {
+			TransportAddedEvent t = (TransportAddedEvent) e;
+			timer.schedule(new TransportAddedTask(t), 0);
+		} else if(e instanceof TransportRemovedEvent) {
+			TransportRemovedEvent t = (TransportRemovedEvent) e;
+			timer.schedule(new TransportRemovedTask(t), 0);
+		}
+	}
+
+<<<<<<< HEAD
+	private void removeAndEraseSecrets(ContactId c, Map<?, TemporarySecret> m) {
+=======
+	// Locking: this
+	private void removeSecrets(ContactId c, Map<?, TemporarySecret> m) {
+>>>>>>> theSource
+		Iterator<TemporarySecret> it = m.values().iterator();
+		while(it.hasNext())
+			if(it.next().getContactId().equals(c)) it.remove();
+	}
+
+<<<<<<< HEAD
+	private void removeAndEraseSecrets(TransportId t,
+			Map<?, TemporarySecret> m) {
+=======
+	// Locking: this
+	private void removeSecrets(TransportId t, Map<?, TemporarySecret> m) {
+>>>>>>> theSource
+		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);
+			}
+			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);
+<<<<<<< HEAD
+			synchLock.lock();
+			try {
+				removeAndEraseSecrets(c, oldSecrets);
+				removeAndEraseSecrets(c, currentSecrets);
+				removeAndEraseSecrets(c, newSecrets);
+=======
+			synchronized(KeyManagerImpl.this) {
+				removeSecrets(c, oldSecrets);
+				removeSecrets(c, currentSecrets);
+				removeSecrets(c, newSecrets);
+>>>>>>> theSource
+			}
+			finally{
+				synchLock.unlock();
+			}
+		}
+	}
+
+	private class TransportAddedTask extends TimerTask {
+
+		private final TransportAddedEvent event;
+
+		private TransportAddedTask(TransportAddedEvent event) {
+			this.event = event;
+		}
+
+		@Override
+		public void run() {
+			synchLock.lock();
+			try {
+				maxLatencies.put(event.getTransportId(), event.getMaxLatency());
+			}
+			finally{
+				synchLock.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);
+			synchLock.lock();
+			try {
+				maxLatencies.remove(t);
+				removeSecrets(t, oldSecrets);
+				removeSecrets(t, currentSecrets);
+				removeSecrets(t, newSecrets);
+			}
+			finally{
+				synchLock.unlock();
+			}
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java b/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
deleted file mode 100644
index 1bb90c1e8cf51d321fe8318a557052eb7d045d3a..0000000000000000000000000000000000000000
--- a/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
+++ /dev/null
@@ -1,103 +0,0 @@
-package org.briarproject.transport;
-
-import static org.briarproject.api.transport.TransportConstants.AAD_LENGTH;
-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.util.ByteUtils.MAX_32_BIT_UNSIGNED;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.security.GeneralSecurityException;
-
-import org.briarproject.api.crypto.AuthenticatedCipher;
-import org.briarproject.api.crypto.SecretKey;
-
-class OutgoingEncryptionLayer implements FrameWriter {
-
-	private final OutputStream out;
-	private final AuthenticatedCipher frameCipher;
-	private final SecretKey frameKey;
-	private final byte[] tag, iv, aad, ciphertext;
-	private final int frameLength;
-
-	private long frameNumber;
-	private boolean writeTag;
-
-	OutgoingEncryptionLayer(OutputStream out, AuthenticatedCipher frameCipher,
-			SecretKey frameKey, int frameLength, byte[] tag) {
-		this.out = out;
-		this.frameCipher = frameCipher;
-		this.frameKey = frameKey;
-		this.frameLength = frameLength;
-		this.tag = tag;
-		iv = new byte[IV_LENGTH];
-		aad = new byte[AAD_LENGTH];
-		ciphertext = new byte[frameLength];
-		frameNumber = 0;
-		writeTag = (tag != null);
-	}
-
-	public void writeFrame(byte[] frame, int payloadLength, boolean finalFrame)
-			throws IOException {
-		if(frameNumber > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
-		// Write the tag if required
-		if(writeTag) {
-			try {
-				out.write(tag, 0, tag.length);
-			} catch(IOException e) {
-				frameKey.erase();
-				throw e;
-			}
-			writeTag = false;
-		}
-		// Encode the header
-		FrameEncoder.encodeHeader(frame, finalFrame, payloadLength);
-		// Don't pad the final frame
-		int plaintextLength, ciphertextLength;
-		if(finalFrame) {
-			plaintextLength = HEADER_LENGTH + payloadLength;
-			ciphertextLength = plaintextLength + MAC_LENGTH;
-		} else {
-			plaintextLength = frameLength - MAC_LENGTH;
-			ciphertextLength = frameLength;
-		}
-		// If there's any padding it must all be zeroes
-		for(int i = HEADER_LENGTH + payloadLength; i < plaintextLength; i++) {
-			frame[i] = 0;
-		}
-		// Encrypt and authenticate the frame
-		FrameEncoder.encodeIv(iv, frameNumber);
-		FrameEncoder.encodeAad(aad, frameNumber, plaintextLength);
-		try {
-			frameCipher.init(true, frameKey, iv, aad);
-			int encrypted = frameCipher.doFinal(frame, 0, plaintextLength,
-					ciphertext, 0);
-			if(encrypted != ciphertextLength) throw new RuntimeException();
-		} catch(GeneralSecurityException badCipher) {
-			throw new RuntimeException(badCipher);
-		}
-		// Write the frame
-		try {
-			out.write(ciphertext, 0, ciphertextLength);
-		} catch(IOException e) {
-			frameKey.erase();
-			throw e;
-		}
-		frameNumber++;
-	}
-
-	public void flush() throws IOException {
-		// Write the tag if required
-		if(writeTag) {
-			try {
-				out.write(tag, 0, tag.length);
-			} catch(IOException e) {
-				frameKey.erase();
-				throw e;
-			}
-			writeTag = false;
-		}
-		out.flush();
-	}
-}
\ No newline at end of file
diff --git a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
index d71bfc50ac24c8cc72009a9df6689abee11ddfaa..d066cacd4f6dcefe45ea1a5ad230f6c3fa03b87e 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
@@ -4,37 +4,28 @@ import java.io.InputStream;
 
 import javax.inject.Inject;
 
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamDecrypterFactory;
 import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
 
 class StreamReaderFactoryImpl implements StreamReaderFactory {
 
-	private final CryptoComponent crypto;
+	private final StreamDecrypterFactory streamDecrypterFactory;
 
 	@Inject
-	StreamReaderFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
+	StreamReaderFactoryImpl(StreamDecrypterFactory streamDecrypterFactory) {
+		this.streamDecrypterFactory = streamDecrypterFactory;
 	}
 
-	public StreamReader createStreamReader(InputStream in,
-			int maxFrameLength, StreamContext ctx) {
-		byte[] secret = ctx.getSecret();
-		long streamNumber = ctx.getStreamNumber();
-		boolean alice = !ctx.getAlice();
-		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
-		FrameReader frameReader = new IncomingEncryptionLayer(in,
-				crypto.getFrameCipher(), frameKey, maxFrameLength);
-		return new StreamReaderImpl(frameReader, maxFrameLength);
+	public InputStream createStreamReader(InputStream in, StreamContext ctx) {
+		return new StreamReaderImpl(
+				streamDecrypterFactory.createStreamDecrypter(in, ctx));
 	}
 
-	public StreamReader createInvitationStreamReader(InputStream in,
-			int maxFrameLength, byte[] secret, boolean alice) {
-		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
-		FrameReader frameReader = new IncomingEncryptionLayer(in,
-				crypto.getFrameCipher(), frameKey, maxFrameLength);
-		return new StreamReaderImpl(frameReader, maxFrameLength);
+	public InputStream createInvitationStreamReader(InputStream in,
+			byte[] secret, boolean alice) {
+		return new StreamReaderImpl(
+				streamDecrypterFactory.createInvitationStreamDecrypter(in,
+						secret, alice));
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/StreamReaderImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
index 0de048a6fa67f805425a645c01affdc8fbd46cf0..cc94ff505bdd8fd203037b6a4cd955e852b4d07b 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
@@ -1,27 +1,22 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
 
 import java.io.IOException;
 import java.io.InputStream;
 
-import org.briarproject.api.transport.StreamReader;
+import org.briarproject.api.crypto.StreamDecrypter;
 
-class StreamReaderImpl extends InputStream implements StreamReader {
+class StreamReaderImpl extends InputStream {
 
-	private final FrameReader in;
-	private final byte[] frame;
+	private final StreamDecrypter decrypter;
+	private final byte[] payload;
 
 	private int offset = 0, length = 0;
 
-	StreamReaderImpl(FrameReader in, int frameLength) {
-		this.in = in;
-		frame = new byte[frameLength - MAC_LENGTH];
-	}
-
-	public InputStream getInputStream() {
-		return this;
+	StreamReaderImpl(StreamDecrypter decrypter) {
+		this.decrypter = decrypter;
+		payload = new byte[MAX_PAYLOAD_LENGTH];
 	}
 
 	@Override
@@ -30,7 +25,7 @@ class StreamReaderImpl extends InputStream implements StreamReader {
 			if(length == -1) return -1;
 			readFrame();
 		}
-		int b = frame[offset] & 0xff;
+		int b = payload[offset] & 0xff;
 		offset++;
 		length--;
 		return b;
@@ -48,7 +43,7 @@ class StreamReaderImpl extends InputStream implements StreamReader {
 			readFrame();
 		}
 		len = Math.min(len, length);
-		System.arraycopy(frame, offset, b, off, len);
+		System.arraycopy(payload, offset, b, off, len);
 		offset += len;
 		length -= len;
 		return len;
@@ -56,7 +51,7 @@ class StreamReaderImpl extends InputStream implements StreamReader {
 
 	private void readFrame() throws IOException {
 		assert length == 0;
-		offset = HEADER_LENGTH;
-		length = in.readFrame(frame);
+		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 638ecdff03d2c162eec5ff323876f24db65cb5a0..89da3a46445d4dbed63895332179229562c38417 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
@@ -1,46 +1,32 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
-
 import java.io.OutputStream;
 
 import javax.inject.Inject;
 
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamEncrypterFactory;
 import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 
 class StreamWriterFactoryImpl implements StreamWriterFactory {
 
-	private final CryptoComponent crypto;
+	private final StreamEncrypterFactory streamEncrypterFactory;
 
 	@Inject
-	StreamWriterFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
+	StreamWriterFactoryImpl(StreamEncrypterFactory streamEncrypterFactory) {
+		this.streamEncrypterFactory = streamEncrypterFactory;
 	}
 
-	public StreamWriter createStreamWriter(OutputStream out,
-			int maxFrameLength, StreamContext ctx) {
-		byte[] secret = ctx.getSecret();
-		long streamNumber = ctx.getStreamNumber();
-		boolean alice = ctx.getAlice();
-		byte[] tag = new byte[TAG_LENGTH];
-		SecretKey tagKey = crypto.deriveTagKey(secret, alice);
-		crypto.encodeTag(tag, tagKey, streamNumber);
-		tagKey.erase();
-		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
-		FrameWriter frameWriter = new OutgoingEncryptionLayer(out,
-				crypto.getFrameCipher(), frameKey, maxFrameLength, tag);
-		return new StreamWriterImpl(frameWriter, maxFrameLength);
+	public OutputStream createStreamWriter(OutputStream out,
+			StreamContext ctx) {
+		return new StreamWriterImpl(
+				streamEncrypterFactory.createStreamEncrypter(out, ctx));
 	}
 
-	public StreamWriter createInvitationStreamWriter(OutputStream out,
-			int maxFrameLength, byte[] secret, boolean alice) {
-		SecretKey frameKey = crypto.deriveFrameKey(secret, 0, alice);
-		FrameWriter frameWriter = new OutgoingEncryptionLayer(out,
-				crypto.getFrameCipher(), frameKey, maxFrameLength, null);
-		return new StreamWriterImpl(frameWriter, maxFrameLength);
+	public OutputStream createInvitationStreamWriter(OutputStream out,
+			byte[] secret, boolean alice) {
+		return new StreamWriterImpl(
+				streamEncrypterFactory.createInvitationStreamEncrypter(out,
+						secret, alice));
 	}
 }
\ 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 7a65a12199a7133a79928c768d38f0d3d7be0e68..6d7b90ff714e3add2a7c1e3a8941056ee652616d 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
@@ -1,12 +1,11 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
 
 import java.io.IOException;
 import java.io.OutputStream;
 
-import org.briarproject.api.transport.StreamWriter;
+import org.briarproject.api.crypto.StreamEncrypter;
 
 /**
  * A {@link org.briarproject.api.transport.StreamWriter StreamWriter} that
@@ -15,43 +14,36 @@ import org.briarproject.api.transport.StreamWriter;
  * <p>
  * This class is not thread-safe.
  */
-class StreamWriterImpl extends OutputStream implements StreamWriter {
+class StreamWriterImpl extends OutputStream {
 
-	private final FrameWriter out;
-	private final byte[] frame;
-	private final int frameLength;
+	private final StreamEncrypter encrypter;
+	private final byte[] payload;
 
 	private int length = 0;
 
-	StreamWriterImpl(FrameWriter out, int frameLength) {
-		this.out = out;
-		this.frameLength = frameLength;
-		frame = new byte[frameLength - MAC_LENGTH];
-	}
-
-	public OutputStream getOutputStream() {
-		return this;
+	StreamWriterImpl(StreamEncrypter encrypter) {
+		this.encrypter = encrypter;
+		payload = new byte[MAX_PAYLOAD_LENGTH];
 	}
 
 	@Override
 	public void close() throws IOException {
 		writeFrame(true);
-		out.flush();
+		encrypter.flush();
 		super.close();
 	}
 
 	@Override
 	public void flush() throws IOException {
-		if(length > 0) writeFrame(false);
-		out.flush();
+		writeFrame(false);
+		encrypter.flush();
 	}
 
 	@Override
 	public void write(int b) throws IOException {
-		frame[HEADER_LENGTH + length] = (byte) b;
+		payload[length] = (byte) b;
 		length++;
-		if(HEADER_LENGTH + length + MAC_LENGTH == frameLength)
-			writeFrame(false);
+		if(length == payload.length) writeFrame(false);
 	}
 
 	@Override
@@ -61,21 +53,21 @@ class StreamWriterImpl extends OutputStream implements StreamWriter {
 
 	@Override
 	public void write(byte[] b, int off, int len) throws IOException {
-		int available = frameLength - HEADER_LENGTH - length - MAC_LENGTH;
+		int available = payload.length - length;
 		while(available <= len) {
-			System.arraycopy(b, off, frame, HEADER_LENGTH + length, available);
+			System.arraycopy(b, off, payload, length, available);
 			length += available;
 			writeFrame(false);
 			off += available;
 			len -= available;
-			available = frameLength - HEADER_LENGTH - length - MAC_LENGTH;
+			available = payload.length - length;
 		}
-		System.arraycopy(b, off, frame, HEADER_LENGTH + length, len);
+		System.arraycopy(b, off, payload, length, len);
 		length += len;
 	}
 
 	private void writeFrame(boolean finalFrame) throws IOException {
-		out.writeFrame(frame, length, finalFrame);
+		encrypter.writeFrame(payload, length, 0, finalFrame);
 		length = 0;
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java b/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java
index 353681d498280ac4260fd40ac670439407974a3a..b7042429ab1764bafdd6cc3761cf4e44fface658 100644
--- a/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java
+++ b/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java
@@ -62,13 +62,10 @@ class TransportTagRecogniser {
 					assert duplicate == null;
 				}
 			}
-			key.erase();
 			// Store the updated reordering window in the DB
 			db.setReorderingWindow(t.contactId, transportId, t.period,
 					t.window.getCentre(), t.window.getBitmap());
-			// Clone the secret - the key manager will erase the original
-			byte[] secret = t.secret.clone();
-			return new StreamContext(t.contactId, transportId, secret,
+		return new StreamContext(t.contactId, transportId, t.secret,
 					t.streamNumber, t.alice);
 		}
 		finally{
@@ -96,7 +93,6 @@ class TransportTagRecogniser {
 				TagContext duplicate = tagMap.put(new Bytes(tag), added);
 				assert duplicate == null;
 			}
-			key.erase();
 			// 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);
@@ -128,7 +124,6 @@ class TransportTagRecogniser {
 			TagContext removed = tagMap.remove(new Bytes(tag));
 			assert removed != null;
 		}
-		key.erase();
 	}
 
 	void removeSecrets(ContactId c) {
diff --git a/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java.orig b/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java.orig
new file mode 100644
index 0000000000000000000000000000000000000000..0e46da82ef5c701c70825abf4ac4512d27bce212
--- /dev/null
+++ b/briar-core/src/org/briarproject/transport/TransportTagRecogniser.java.orig
@@ -0,0 +1,235 @@
+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 Map<Bytes, TagContext> tagMap;
+	private final Map<RemovalKey, RemovalContext> removalMap;
+
+	private final Lock synchLock = new ReentrantLock();
+
+	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 {
+		synchLock.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;
+				}
+			}
+			key.erase();
+			// Store the updated reordering window in the DB
+			db.setReorderingWindow(t.contactId, transportId, t.period,
+					t.window.getCentre(), t.window.getBitmap());
+			// Clone the secret - the key manager will erase the original
+			byte[] secret = t.secret.clone();
+			return new StreamContext(t.contactId, transportId, secret,
+					t.streamNumber, t.alice);
+		}
+		finally{
+			synchLock.unlock();
+		}
+<<<<<<< HEAD
+=======
+		// 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);
+>>>>>>> theSource
+	}
+
+	void addSecret(TemporarySecret s) {
+		synchLock.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;
+			}
+			key.erase();
+			// 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{
+			synchLock.unlock();
+		}
+<<<<<<< HEAD
+=======
+		// 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);
+>>>>>>> theSource
+	}
+
+	void removeSecret(ContactId contactId, long period) {
+		synchLock.lock();
+		try{
+			RemovalKey k = new RemovalKey(contactId, period);
+			RemovalContext removed = removalMap.remove(k);
+			if(removed == null) throw new IllegalArgumentException();
+			removeSecret(removed);
+		}
+		finally{
+			synchLock.unlock();
+		}
+	}
+
+	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) {
+		synchLock.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{
+			synchLock.unlock();
+		}
+	}
+
+	void removeSecrets() {
+		synchLock.lock();
+		try{
+			for(RemovalContext r : removalMap.values()) removeSecret(r);
+			assert tagMap.isEmpty();
+			removalMap.clear();
+		}
+		finally{
+			synchLock.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-core/src/org/briarproject/util/ByteUtils.java b/briar-core/src/org/briarproject/util/ByteUtils.java
index ed40306e5931afcfed36dca9def9e540f2a7259f..9cc9e6bc8826f36847c1d14b639b8d2a897c7900 100644
--- a/briar-core/src/org/briarproject/util/ByteUtils.java
+++ b/briar-core/src/org/briarproject/util/ByteUtils.java
@@ -48,10 +48,6 @@ public class ByteUtils {
 				| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
 	}
 
-	public static void erase(byte[] b) {
-		for(int i = 0; i < b.length; i++) b[i] = 0;
-	}
-
 	public static int readUint(byte[] b, int bits) {
 		if(b.length << 3 < bits) throw new IllegalArgumentException();
 		int result = 0;
diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
index 82f69bee237d01ba3ccd0de5a4537507eda9b198..45dca315f2a2b6ad64e7f21cc20e1a6b721c0b16 100644
--- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
@@ -46,8 +46,7 @@ class BluetoothPlugin implements DuplexPlugin {
 	private final Clock clock;
 	private final SecureRandom secureRandom;
 	private final DuplexPluginCallback callback;
-	private final int maxFrameLength;
-	private final long maxLatency, pollingInterval;
+	private final int maxLatency, pollingInterval;
 	private final Semaphore discoverySemaphore = new Semaphore(1);
 
 	private volatile boolean running = false;
@@ -55,13 +54,12 @@ class BluetoothPlugin implements DuplexPlugin {
 	private volatile LocalDevice localDevice = null;
 
 	BluetoothPlugin(Executor ioExecutor, Clock clock, SecureRandom secureRandom,
-			DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
-			long pollingInterval) {
+			DuplexPluginCallback callback, int maxLatency,
+			int pollingInterval) {
 		this.ioExecutor = ioExecutor;
 		this.clock = clock;
 		this.secureRandom = secureRandom;
 		this.callback = callback;
-		this.maxFrameLength = maxFrameLength;
 		this.maxLatency = maxLatency;
 		this.pollingInterval = pollingInterval;
 	}
@@ -70,12 +68,13 @@ class BluetoothPlugin implements DuplexPlugin {
 		return ID;
 	}
 
-	public int getMaxFrameLength() {
-		return maxFrameLength;
+	public int getMaxLatency() {
+		return maxLatency;
 	}
 
-	public long getMaxLatency() {
-		return maxLatency;
+	public int getMaxIdleTime() {
+		// Bluetooth detects dead connections so we don't need keepalives
+		return Integer.MAX_VALUE;
 	}
 
 	public boolean start() throws IOException {
@@ -181,7 +180,7 @@ class BluetoothPlugin implements DuplexPlugin {
 		return true;
 	}
 
-	public long getPollingInterval() {
+	public int getPollingInterval() {
 		return pollingInterval;
 	}
 
diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPluginFactory.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPluginFactory.java
index 446846305b840605d64c9504fc2a2a3077689920..28859c4d7eae9e79ff42baf2081b70f71dafc809 100644
--- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPluginFactory.java
+++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPluginFactory.java
@@ -12,9 +12,8 @@ import org.briarproject.system.SystemClock;
 
 public class BluetoothPluginFactory implements DuplexPluginFactory {
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
+	private static final int POLLING_INTERVAL = 3 * 60 * 1000; // 3 minutes
 
 	private final Executor ioExecutor;
 	private final SecureRandom secureRandom;
@@ -33,6 +32,6 @@ public class BluetoothPluginFactory implements DuplexPluginFactory {
 
 	public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
 		return new BluetoothPlugin(ioExecutor, clock, secureRandom, callback,
-				MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
+				MAX_LATENCY, POLLING_INTERVAL);
 	}
 }
diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java
index 8a2c7967257c223d5e34c08444eab33663ab3afd..8e70db480b0cfa70cdb256c110ffb5c275740a02 100644
--- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java
+++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothTransportConnection.java
@@ -39,10 +39,6 @@ class BluetoothTransportConnection implements DuplexTransportConnection {
 
 	private class Reader implements TransportConnectionReader {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
-		}
-
 		public long getMaxLatency() {
 			return plugin.getMaxLatency();
 		}
@@ -60,12 +56,12 @@ class BluetoothTransportConnection implements DuplexTransportConnection {
 
 	private class Writer implements TransportConnectionWriter {
 
-		public int getMaxFrameLength() {
-			return plugin.getMaxFrameLength();
+		public int getMaxLatency() {
+			return plugin.getMaxLatency();
 		}
 
-		public long getMaxLatency() {
-			return plugin.getMaxLatency();
+		public int getMaxIdleTime() {
+			return plugin.getMaxIdleTime();
 		}
 
 		public long getCapacity() {
diff --git a/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java b/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java
index 4ca9e3ce66fcd30bbe931774f0849aa25784b822..8938e0776e7be907616adbd4994b59a7e41406fa 100644
--- a/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java
+++ b/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java
@@ -17,7 +17,7 @@ class PollingRemovableDriveMonitor implements RemovableDriveMonitor, Runnable {
 
 	private final Executor ioExecutor;
 	private final RemovableDriveFinder finder;
-	private final long pollingInterval;
+	private final int pollingInterval;
 
 	private volatile boolean running = false;
 	private volatile Callback callback = null;
@@ -27,7 +27,7 @@ class PollingRemovableDriveMonitor implements RemovableDriveMonitor, Runnable {
 
 
 	public PollingRemovableDriveMonitor(Executor ioExecutor,
-			RemovableDriveFinder finder, long pollingInterval) {
+			RemovableDriveFinder finder, int pollingInterval) {
 		this.ioExecutor = ioExecutor;
 		this.finder = finder;
 		this.pollingInterval = pollingInterval;
diff --git a/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java.orig b/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java.orig
new file mode 100644
index 0000000000000000000000000000000000000000..d27f65a0a1226dac3b5507636a6994ea76a88802
--- /dev/null
+++ b/briar-desktop/src/org/briarproject/plugins/file/PollingRemovableDriveMonitor.java.orig
@@ -0,0 +1,83 @@
+package org.briarproject.plugins.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Collection;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.logging.Logger;
+
+class PollingRemovableDriveMonitor implements RemovableDriveMonitor, Runnable {
+
+	private static final Logger LOG =
+			Logger.getLogger(PollingRemovableDriveMonitor.class.getName());
+
+	private final Executor ioExecutor;
+	private final RemovableDriveFinder finder;
+<<<<<<< HEAD
+	private final long pollingInterval;
+=======
+	private final int pollingInterval;
+	private final Object pollingLock = new Object();
+>>>>>>> theSource
+
+	private volatile boolean running = false;
+	private volatile Callback callback = null;
+
+	private final Lock pollingLock = new ReentrantLock();
+	private final Condition stopPolling = pollingLock.newCondition();
+
+
+	public PollingRemovableDriveMonitor(Executor ioExecutor,
+			RemovableDriveFinder finder, int pollingInterval) {
+		this.ioExecutor = ioExecutor;
+		this.finder = finder;
+		this.pollingInterval = pollingInterval;
+	}
+
+	public void start(Callback callback) throws IOException {
+		this.callback = callback;
+		running = true;
+		ioExecutor.execute(this);
+	}
+
+	public void stop() throws IOException {
+		running = false;
+		pollingLock.lock();
+		try {
+			stopPolling.signalAll();
+		} 
+		finally {
+			pollingLock.unlock();
+		}
+	}
+
+	public void run() {
+		try {
+			Collection<File> drives = finder.findRemovableDrives();
+			while(running) {
+				pollingLock.lock();
+				try {
+					stopPolling.await(pollingInterval, TimeUnit.MILLISECONDS);
+				} 
+				finally{
+					pollingLock.unlock();
+				}
+				if(!running) return;
+				Collection<File> newDrives = finder.findRemovableDrives();
+				for(File f : newDrives) {
+					if(!drives.contains(f)) callback.driveInserted(f);
+				}
+				drives = newDrives;
+			}
+		} catch(InterruptedException e) {
+			LOG.warning("Interrupted while waiting to poll");
+			Thread.currentThread().interrupt();
+		} catch(IOException e) {
+			callback.exceptionThrown(e);
+		}
+	}
+}
diff --git a/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java b/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java
index 09ffd6f663592668cc148c7ae74af1b1cc065005..f648ff092c402c0df55d767838a64d92b22ef996 100644
--- a/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java
@@ -29,9 +29,8 @@ implements RemovableDriveMonitor.Callback {
 
 	RemovableDrivePlugin(Executor ioExecutor, FileUtils fileUtils,
 			SimplexPluginCallback callback, RemovableDriveFinder finder,
-			RemovableDriveMonitor monitor, int maxFrameLength,
-			long maxLatency) {
-		super(ioExecutor, fileUtils, callback, maxFrameLength, maxLatency);
+			RemovableDriveMonitor monitor, int maxLatency) {
+		super(ioExecutor, fileUtils, callback, maxLatency);
 		this.finder = finder;
 		this.monitor = monitor;
 	}
@@ -55,7 +54,7 @@ implements RemovableDriveMonitor.Callback {
 		return false;
 	}
 
-	public long getPollingInterval() {
+	public int getPollingInterval() {
 		throw new UnsupportedOperationException();
 	}
 
diff --git a/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePluginFactory.java b/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePluginFactory.java
index 01688d40344aee7171076077cdd4ca3b09ecfda5..68b1eaf31c8bb2a4e856f8f21b19b0705db99e66 100644
--- a/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePluginFactory.java
+++ b/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePluginFactory.java
@@ -1,7 +1,5 @@
 package org.briarproject.plugins.file;
 
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-
 import java.util.concurrent.Executor;
 
 import org.briarproject.api.TransportId;
@@ -14,8 +12,8 @@ import org.briarproject.util.OsUtils;
 public class RemovableDrivePluginFactory implements SimplexPluginFactory {
 
 	// Maximum latency 14 days (Royal Mail or lackadaisical carrier pigeon)
-	private static final long MAX_LATENCY = 14 * 24 * 60 * 60 * 1000;
-	private static final long POLLING_INTERVAL = 10 * 1000; // 10 seconds
+	private static final int MAX_LATENCY = 14 * 24 * 60 * 60 * 1000;
+	private static final int POLLING_INTERVAL = 10 * 1000; // 10 seconds
 
 	private final Executor ioExecutor;
 	private final FileUtils fileUtils;
@@ -52,6 +50,6 @@ public class RemovableDrivePluginFactory implements SimplexPluginFactory {
 			return null;
 		}
 		return new RemovableDrivePlugin(ioExecutor, fileUtils, callback,
-				finder, monitor, MAX_FRAME_LENGTH, MAX_LATENCY);
+				finder, monitor, MAX_LATENCY);
 	}
 }
diff --git a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
index 686f1c540473701a543ec962df43063f09739f45..b0a58969cb6ccb1a533994b7640c5e00f8a81ad8 100644
--- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
@@ -32,33 +32,30 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 	private final ModemFactory modemFactory;
 	private final SerialPortList serialPortList;
 	private final DuplexPluginCallback callback;
-	private final int maxFrameLength;
-	private final long maxLatency, pollingInterval;
+	private final int maxLatency;
 
 	private volatile boolean running = false;
 	private volatile Modem modem = null;
 
 	ModemPlugin(ModemFactory modemFactory, SerialPortList serialPortList,
-			DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
-			long pollingInterval) {
+			DuplexPluginCallback callback, int maxLatency) {
 		this.modemFactory = modemFactory;
 		this.serialPortList = serialPortList;
 		this.callback = callback;
-		this.maxFrameLength = maxFrameLength;
 		this.maxLatency = maxLatency;
-		this.pollingInterval = pollingInterval;
 	}
 
 	public TransportId getId() {
 		return ID;
 	}
 
-	public int getMaxFrameLength() {
-		return maxFrameLength;
+	public int getMaxLatency() {
+		return maxLatency;
 	}
 
-	public long getMaxLatency() {
-		return maxLatency;
+	public int getMaxIdleTime() {
+		// FIXME: Do we need keepalives for this transport?
+		return Integer.MAX_VALUE;
 	}
 
 	public boolean start() {
@@ -98,8 +95,8 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 		return false;
 	}
 
-	public long getPollingInterval() {
-		return pollingInterval;
+	public int getPollingInterval() {
+		throw new UnsupportedOperationException();
 	}
 
 	public void poll(Collection<ContactId> connected) {
@@ -197,10 +194,6 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 
 		private class Reader implements TransportConnectionReader {
 
-			public int getMaxFrameLength() {
-				return maxFrameLength;
-			}
-
 			public long getMaxLatency() {
 				return maxLatency;
 			}
@@ -217,12 +210,12 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 
 		private class Writer implements TransportConnectionWriter {
 
-			public int getMaxFrameLength() {
-				return maxFrameLength;
+			public int getMaxLatency() {
+				return getMaxLatency();
 			}
 
-			public long getMaxLatency() {
-				return maxLatency;
+			public int getMaxIdleTime() {
+				return getMaxIdleTime();
 			}
 
 			public long getCapacity() {
diff --git a/briar-desktop/src/org/briarproject/plugins/modem/ModemPluginFactory.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPluginFactory.java
index 2732bbbc7cd569922cb2dc00ff942961f9fcee26..f70fc3be712fc4a5ca32cd415b0aadadd8177a05 100644
--- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPluginFactory.java
+++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPluginFactory.java
@@ -11,9 +11,7 @@ import org.briarproject.util.StringUtils;
 
 public class ModemPluginFactory implements DuplexPluginFactory {
 
-	private static final int MAX_FRAME_LENGTH = 1024;
-	private static final long MAX_LATENCY = 60 * 1000; // 1 minute
-	private static final long POLLING_INTERVAL = 60 * 60 * 1000; // 1 hour
+	private static final int MAX_LATENCY = 30 * 1000; // 30 seconds
 
 	private final ModemFactory modemFactory;
 	private final SerialPortList serialPortList;
@@ -33,6 +31,6 @@ public class ModemPluginFactory implements DuplexPluginFactory {
 		String enabled = callback.getConfig().get("enabled");
 		if(StringUtils.isNullOrEmpty(enabled)) return null;
 		return new ModemPlugin(modemFactory, serialPortList, callback,
-				MAX_FRAME_LENGTH, MAX_LATENCY, POLLING_INTERVAL);
+				MAX_LATENCY);
 	}
 }
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index 2ab788bba3b4ecc03415ed0f4aabe0343e183b1f..eb4766a090b7d734a50b0f2c83bc06af304eccc6 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -100,8 +100,9 @@
 			<test name='org.briarproject.crypto.KeyDerivationTest'/>
 			<test name='org.briarproject.crypto.KeyEncodingAndParsingTest'/>
 			<test name="org.briarproject.crypto.PasswordBasedKdfTest"/>
-			<test name="org.briarproject.crypto.PasswordStrengthEstimatorTest"/>
-			<test name='org.briarproject.crypto.SecretKeyImplTest'/>
+			<test name="org.briarproject.crypto.PasswordStrengthEstimatorImplTest"/>
+			<test name='org.briarproject.crypto.StreamDecrypterImplTest'/>
+			<test name='org.briarproject.crypto.StreamEncrypterImplTest'/>
 			<test name='org.briarproject.db.BasicH2Test'/>
 			<test name='org.briarproject.db.DatabaseCleanerImplTest'/>
 			<test name='org.briarproject.db.DatabaseComponentImplTest'/>
@@ -126,10 +127,8 @@
 			<test name='org.briarproject.serial.ReaderImplTest'/>
 			<test name='org.briarproject.serial.WriterImplTest'/>
 			<test name='org.briarproject.system.LinuxSeedProviderTest'/>
-			<test name='org.briarproject.transport.IncomingEncryptionLayerTest'/>
 			<test name='org.briarproject.transport.KeyManagerImplTest'/>
 			<test name='org.briarproject.transport.KeyRotationIntegrationTest'/>
-			<test name='org.briarproject.transport.OutgoingEncryptionLayerTest'/>
 			<test name='org.briarproject.transport.ReorderingWindowTest'/>
 			<test name='org.briarproject.transport.StreamReaderImplTest'/>
 			<test name='org.briarproject.transport.StreamWriterImplTest'/>
diff --git a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
index cebd634d4d63631403419599edaf4a15af47edd3..633a4929ad23c580685f96f2a63cbc05d20682ca 100644
--- a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
@@ -1,12 +1,12 @@
 package org.briarproject;
 
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -36,9 +36,7 @@ import org.briarproject.api.messaging.SubscriptionUpdate;
 import org.briarproject.api.messaging.TransportUpdate;
 import org.briarproject.api.messaging.UnverifiedMessage;
 import org.briarproject.api.transport.StreamContext;
-import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 import org.briarproject.crypto.CryptoModule;
 import org.briarproject.db.DatabaseModule;
@@ -117,12 +115,12 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 
 	private byte[] write() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamContext ctx = new StreamContext(contactId, transportId,
-				secret.clone(), 0, true);
-		StreamWriter streamWriter = streamWriterFactory.createStreamWriter(out,
-				MAX_FRAME_LENGTH, ctx);
+		StreamContext ctx = new StreamContext(contactId, transportId, secret,
+				0, true);
+		OutputStream streamWriter =
+				streamWriterFactory.createStreamWriter(out, ctx);
 		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(
-				streamWriter.getOutputStream());
+				streamWriter);
 
 		packetWriter.writeAck(new Ack(messageIds));
 
@@ -140,7 +138,7 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 				transportProperties, 1);
 		packetWriter.writeTransportUpdate(tu);
 
-		streamWriter.getOutputStream().flush();
+		streamWriter.flush();
 		return out.toByteArray();
 	}
 
@@ -149,12 +147,12 @@ 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.clone(), 0, false);
-		StreamReader streamReader = streamReaderFactory.createStreamReader(in,
-				MAX_FRAME_LENGTH, ctx);
+		StreamContext ctx = new StreamContext(contactId, transportId, secret,
+				0, false);
+		InputStream streamReader =
+				streamReaderFactory.createStreamReader(in, ctx);
 		PacketReader packetReader = packetReaderFactory.createPacketReader(
-				streamReader.getInputStream());
+				streamReader);
 
 		// Read the ack
 		assertTrue(packetReader.hasAck());
diff --git a/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java b/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
index 54c4a3c1560d6363bbe277a740a358ae811aff30..d0d71a7eb77dadec8b18cdad72d6f1095780d492 100644
--- a/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
+++ b/briar-tests/src/org/briarproject/crypto/KeyDerivationTest.java
@@ -30,9 +30,9 @@ public class KeyDerivationTest extends BriarTestCase {
 		keys.add(crypto.deriveTagKey(secret, true));
 		keys.add(crypto.deriveTagKey(secret, false));
 		for(int i = 0; i < 4; i++) {
-			byte[] keyI = keys.get(i).getEncoded();
+			byte[] keyI = keys.get(i).getBytes();
 			for(int j = 0; j < 4; j++) {
-				byte[] keyJ = keys.get(j).getEncoded();
+				byte[] keyJ = keys.get(j).getBytes();
 				assertEquals(i == j, Arrays.equals(keyI, keyJ));
 			}
 		}
@@ -59,9 +59,8 @@ public class KeyDerivationTest extends BriarTestCase {
 	@Test
 	public void testStreamNumberAffectsDerivation() {
 		List<byte[]> secrets = new ArrayList<byte[]>();
-		for(int i = 0; i < 20; i++) {
-			secrets.add(crypto.deriveNextSecret(secret.clone(), i));
-		}
+		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++) {
diff --git a/briar-tests/src/org/briarproject/crypto/PasswordBasedKdfTest.java b/briar-tests/src/org/briarproject/crypto/PasswordBasedKdfTest.java
index 151f78fb57777f1ecffa4df16620bce3e9f3c64c..cdc59b3323a7e88b37b48ccc9bf2df006ac63744 100644
--- a/briar-tests/src/org/briarproject/crypto/PasswordBasedKdfTest.java
+++ b/briar-tests/src/org/briarproject/crypto/PasswordBasedKdfTest.java
@@ -18,7 +18,7 @@ public class PasswordBasedKdfTest extends BriarTestCase {
 		Random random = new Random();
 		byte[] input = new byte[1234];
 		random.nextBytes(input);
-		char[] password = "password".toCharArray();
+		String password = "password";
 		byte[] ciphertext = crypto.encryptWithPassword(input, password);
 		byte[] output = crypto.decryptWithPassword(ciphertext, password);
 		assertArrayEquals(input, output);
@@ -29,7 +29,7 @@ public class PasswordBasedKdfTest extends BriarTestCase {
 		Random random = new Random();
 		byte[] input = new byte[1234];
 		random.nextBytes(input);
-		char[] password = "password".toCharArray();
+		String password = "password";
 		byte[] ciphertext = crypto.encryptWithPassword(input, password);
 		// Modify the ciphertext
 		int position = random.nextInt(ciphertext.length);
diff --git a/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java b/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorImplTest.java
similarity index 50%
rename from briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java
rename to briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorImplTest.java
index 9f703b378f7acaa13670691c8101a156279f132e..5407daf9ae72feb4331f2b71d87134d05b10db46 100644
--- a/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorTest.java
+++ b/briar-tests/src/org/briarproject/crypto/PasswordStrengthEstimatorImplTest.java
@@ -6,24 +6,23 @@ import org.briarproject.BriarTestCase;
 import org.briarproject.api.crypto.PasswordStrengthEstimator;
 import org.junit.Test;
 
-public class PasswordStrengthEstimatorTest extends BriarTestCase {
+public class PasswordStrengthEstimatorImplTest extends BriarTestCase {
 
 	@Test
 	public void testWeakPasswords() {
 		PasswordStrengthEstimator e = new PasswordStrengthEstimatorImpl();
-		assertTrue(e.estimateStrength("".toCharArray()) < QUITE_STRONG);
-		assertTrue(e.estimateStrength("password".toCharArray()) < QUITE_STRONG);
-		assertTrue(e.estimateStrength("letmein".toCharArray()) < QUITE_STRONG);
-		assertTrue(e.estimateStrength("123456".toCharArray()) < QUITE_STRONG);
+		assertTrue(e.estimateStrength("") < QUITE_STRONG);
+		assertTrue(e.estimateStrength("password") < QUITE_STRONG);
+		assertTrue(e.estimateStrength("letmein") < QUITE_STRONG);
+		assertTrue(e.estimateStrength("123456") < QUITE_STRONG);
 	}
 
 	@Test
 	public void testStrongPasswords() {
 		PasswordStrengthEstimator e = new PasswordStrengthEstimatorImpl();
 		// Industry standard
-		assertTrue(e.estimateStrength("Tr0ub4dor&3".toCharArray())
-				> QUITE_STRONG);
-		assertTrue(e.estimateStrength("correcthorsebatterystaple".toCharArray())
+		assertTrue(e.estimateStrength("Tr0ub4dor&3") > QUITE_STRONG);
+		assertTrue(e.estimateStrength("correcthorsebatterystaple")
 				> QUITE_STRONG);
 	}
 }
diff --git a/briar-tests/src/org/briarproject/crypto/SecretKeyImplTest.java b/briar-tests/src/org/briarproject/crypto/SecretKeyImplTest.java
deleted file mode 100644
index 20ee3605bf954d68df560c0d11bfb47d4bbeb599..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/crypto/SecretKeyImplTest.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package org.briarproject.crypto;
-
-import static org.junit.Assert.assertArrayEquals;
-
-import java.util.Random;
-
-import org.briarproject.BriarTestCase;
-import org.briarproject.api.crypto.SecretKey;
-import org.junit.Test;
-
-public class SecretKeyImplTest extends BriarTestCase {
-
-	private static final int KEY_BYTES = 32; // 256 bits
-
-	@Test
-	public void testCopiesAreErased() {
-		byte[] master = new byte[KEY_BYTES];
-		new Random().nextBytes(master);
-		SecretKey k = new SecretKeyImpl(master);
-		byte[] copy = k.getEncoded();
-		assertArrayEquals(master, copy);
-		k.erase();
-		byte[] blank = new byte[KEY_BYTES];
-		assertArrayEquals(blank, master);
-		assertArrayEquals(blank, copy);
-	}
-}
diff --git a/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java b/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..c907f37e246939b7db1c13491317772fb0aa44b5
--- /dev/null
+++ b/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
@@ -0,0 +1,37 @@
+package org.briarproject.crypto;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+public class StreamDecrypterImplTest extends BriarTestCase {
+
+	@Test
+	public void testReadValidFrames() throws Exception {
+		// FIXME
+	}
+
+	@Test
+	public void testTruncatedFrameThrowsException() throws Exception {
+		// FIXME
+	}
+
+	@Test
+	public void testModifiedFrameThrowsException() throws Exception {
+		// FIXME
+	}
+
+	@Test
+	public void testInvalidPayloadLengthThrowsException() throws Exception {
+		// FIXME
+	}
+
+	@Test
+	public void testNonZeroPaddingThrowsException() throws Exception {
+		// FIXME
+	}
+
+	@Test
+	public void testCannotReadBeyondFinalFrame() throws Exception {
+		// FIXME
+	}
+}
diff --git a/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java b/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..a874314165b0e3b6de8b38c93c443136e0d928a0
--- /dev/null
+++ b/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
@@ -0,0 +1,255 @@
+package org.briarproject.crypto;
+
+import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
+import static org.junit.Assert.assertArrayEquals;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Random;
+
+import org.briarproject.BriarTestCase;
+import org.briarproject.api.crypto.AuthenticatedCipher;
+import org.briarproject.api.crypto.SecretKey;
+import org.junit.Test;
+
+public class StreamEncrypterImplTest extends BriarTestCase {
+
+	private final AuthenticatedCipher frameCipher;
+	private final SecretKey frameKey;
+	private final byte[] tag;
+
+	public StreamEncrypterImplTest() {
+		frameCipher = new TestAuthenticatedCipher();
+		frameKey = new SecretKey(new byte[32]);
+		tag = new byte[TAG_LENGTH];
+		new Random().nextBytes(tag);
+	}
+
+	@Test
+	public void testWriteUnpaddedNonFinalFrameWithTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, tag);
+		int payloadLength = 123;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, 0, false);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, false, payloadLength, 0);
+		byte[] expected = new byte[TAG_LENGTH + HEADER_LENGTH + payloadLength
+		                           + MAC_LENGTH];
+		System.arraycopy(tag, 0, expected, 0, TAG_LENGTH);
+		System.arraycopy(header, 0, expected, TAG_LENGTH, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, TAG_LENGTH + HEADER_LENGTH,
+				payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWriteUnpaddedFinalFrameWithTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, tag);
+		int payloadLength = 123;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, 0, true);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, true, payloadLength, 0);
+		byte[] expected = new byte[TAG_LENGTH + HEADER_LENGTH + payloadLength
+		                           + MAC_LENGTH];
+		System.arraycopy(tag, 0, expected, 0, TAG_LENGTH);
+		System.arraycopy(header, 0, expected, TAG_LENGTH, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, TAG_LENGTH + HEADER_LENGTH,
+				payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWriteUnpaddedNonFinalFrameWithoutTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, null);
+		int payloadLength = 123;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, 0, false);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, false, payloadLength, 0);
+		byte[] expected = new byte[HEADER_LENGTH + payloadLength + MAC_LENGTH];
+		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWriteUnpaddedFinalFrameWithoutTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, null);
+		int payloadLength = 123;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, 0, true);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, true, payloadLength, 0);
+		byte[] expected = new byte[HEADER_LENGTH + payloadLength + MAC_LENGTH];
+		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWritePaddedNonFinalFrameWithTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, tag);
+		int payloadLength = 123, paddingLength = 234;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, paddingLength, false);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, false, payloadLength, paddingLength);
+		byte[] expected = new byte[TAG_LENGTH + HEADER_LENGTH + payloadLength
+		                           + paddingLength + MAC_LENGTH];
+		System.arraycopy(tag, 0, expected, 0, TAG_LENGTH);
+		System.arraycopy(header, 0, expected, TAG_LENGTH, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, TAG_LENGTH + HEADER_LENGTH,
+				payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWritePaddedFinalFrameWithTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, tag);
+		int payloadLength = 123, paddingLength = 234;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, paddingLength, true);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, true, payloadLength, paddingLength);
+		byte[] expected = new byte[TAG_LENGTH + HEADER_LENGTH + payloadLength
+		                           + paddingLength + MAC_LENGTH];
+		System.arraycopy(tag, 0, expected, 0, TAG_LENGTH);
+		System.arraycopy(header, 0, expected, TAG_LENGTH, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, TAG_LENGTH + HEADER_LENGTH,
+				payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWritePaddedNonFinalFrameWithoutTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, null);
+		int payloadLength = 123, paddingLength = 234;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, paddingLength, false);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, false, payloadLength, paddingLength);
+		byte[] expected = new byte[HEADER_LENGTH + payloadLength
+		                           + paddingLength + MAC_LENGTH];
+		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWritePaddedFinalFrameWithoutTag() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, null);
+		int payloadLength = 123, paddingLength = 234;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+
+		s.writeFrame(payload, payloadLength, paddingLength, true);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, true, payloadLength, paddingLength);
+		byte[] expected = new byte[HEADER_LENGTH + payloadLength
+		                           + paddingLength + MAC_LENGTH];
+		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testWriteTwoFrames() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, null);
+		int payloadLength = 123, paddingLength = 234;
+		byte[] payload = new byte[payloadLength];
+		new Random().nextBytes(payload);
+		int payloadLength1 = 345, paddingLength1 = 456;
+		byte[] payload1 = new byte[payloadLength1];
+		new Random().nextBytes(payload1);
+
+		s.writeFrame(payload, payloadLength, paddingLength, false);
+		s.writeFrame(payload1, payloadLength1, paddingLength1, true);
+
+		byte[] header = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header, false, payloadLength, paddingLength);
+		byte[] header1 = new byte[HEADER_LENGTH];
+		FrameEncoder.encodeHeader(header1, true, payloadLength1,
+				paddingLength1);
+		byte[] expected = new byte[HEADER_LENGTH + payloadLength
+		                           + paddingLength + MAC_LENGTH
+		                           + HEADER_LENGTH + payloadLength1
+		                           + paddingLength1 + MAC_LENGTH];
+		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
+		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
+		System.arraycopy(header1, 0, expected, HEADER_LENGTH + payloadLength
+				+ paddingLength + MAC_LENGTH, HEADER_LENGTH);
+		System.arraycopy(payload1, 0, expected, HEADER_LENGTH + payloadLength
+				+ paddingLength + MAC_LENGTH + HEADER_LENGTH, payloadLength1);
+		assertArrayEquals(expected, out.toByteArray());
+	}
+
+	@Test
+	public void testFlushWritesTagIfNotAlreadyWritten() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, tag);
+		s.flush();
+		assertArrayEquals(tag, out.toByteArray());
+	}
+
+	@Test
+	public void testFlushDoesNotWriteTagIfAlreadyWritten() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, tag);
+		s.flush();
+		s.flush();
+		assertArrayEquals(tag, out.toByteArray());
+	}
+
+	@Test
+	public void testFlushDoesNotWriteTagIfNull() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
+				frameKey, null);
+		s.flush();
+		assertEquals(0, out.size());
+	}
+}
diff --git a/briar-tests/src/org/briarproject/crypto/TestAuthenticatedCipher.java b/briar-tests/src/org/briarproject/crypto/TestAuthenticatedCipher.java
new file mode 100644
index 0000000000000000000000000000000000000000..0271975bd43f5b64368486dbfdb2c5f0d9f0f64e
--- /dev/null
+++ b/briar-tests/src/org/briarproject/crypto/TestAuthenticatedCipher.java
@@ -0,0 +1,45 @@
+package org.briarproject.crypto;
+
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+
+import java.security.GeneralSecurityException;
+
+import org.briarproject.api.crypto.AuthenticatedCipher;
+import org.briarproject.api.crypto.SecretKey;
+
+class TestAuthenticatedCipher implements AuthenticatedCipher {
+
+	private static final int BLOCK_BYTES = 16;
+
+	private boolean encrypt = false;
+
+	public void init(boolean encrypt, SecretKey key, byte[] iv, byte[] aad)
+			throws GeneralSecurityException {
+		this.encrypt = encrypt;
+	}
+
+	public int process(byte[] input, int inputOff, int len, byte[] output,
+			int outputOff) throws GeneralSecurityException {
+		if(encrypt) {
+			System.arraycopy(input, inputOff, output, outputOff, len);
+			for(int i = 0; i < MAC_LENGTH; i++)
+				output[outputOff + len + i] = 0;
+			return len + MAC_LENGTH;
+		} else {
+			for(int i = 0; i < MAC_LENGTH; i++)
+				if(input[inputOff + len - MAC_LENGTH + i] != 0)
+					throw new GeneralSecurityException();
+			System.arraycopy(input, inputOff, output, outputOff,
+					len - MAC_LENGTH);
+			return len - MAC_LENGTH;
+		}
+	}
+
+	public int getMacLength() {
+		return MAC_LENGTH;
+	}
+
+	public int getBlockSize() {
+		return BLOCK_BYTES;
+	}
+}
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java
index 57855cfb6f8e8c6277ec501ca7acd720a99343e4..338b8c9577655a2f4c2dec5542c682a4fae77e7f 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentTest.java
@@ -75,6 +75,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 	protected final Message message, message1;
 	protected final TransportId transportId;
 	protected final TransportProperties transportProperties;
+	protected final int maxLatency;
 	protected final ContactId contactId;
 	protected final Contact contact;
 	protected final Endpoint endpoint;
@@ -102,6 +103,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 		transportId = new TransportId("id");
 		transportProperties = new TransportProperties(Collections.singletonMap(
 				"bar", "baz"));
+		maxLatency = Integer.MAX_VALUE;
 		contactId = new ContactId(234);
 		contact = new Contact(contactId, author, localAuthorId);
 		endpoint = new Endpoint(contactId, transportId, 123, true);
@@ -691,11 +693,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).getRawMessage(txn, messageId);
 			will(returnValue(raw));
 			oneOf(database).updateExpiryTime(txn, contactId, messageId,
-					Long.MAX_VALUE);
+					maxLatency);
 			oneOf(database).getRawMessage(txn, messageId1);
 			will(returnValue(raw1));
 			oneOf(database).updateExpiryTime(txn, contactId, messageId1,
-					Long.MAX_VALUE);
+					maxLatency);
 			oneOf(database).lowerRequestedFlag(txn, contactId, ids);
 			oneOf(database).commitTransaction(txn);
 		}});
@@ -703,7 +705,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				eventBus, shutdown);
 
 		assertEquals(messages, db.generateBatch(contactId, size * 2,
-				Long.MAX_VALUE));
+				maxLatency));
 
 		context.assertIsSatisfied();
 	}
@@ -726,15 +728,15 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).getMessagesToOffer(txn, contactId, 123);
 			will(returnValue(ids));
 			oneOf(database).updateExpiryTime(txn, contactId, messageId,
-					Long.MAX_VALUE);
+					maxLatency);
 			oneOf(database).updateExpiryTime(txn, contactId, messageId1,
-					Long.MAX_VALUE);
+					maxLatency);
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		Offer o = db.generateOffer(contactId, 123, Long.MAX_VALUE);
+		Offer o = db.generateOffer(contactId, 123, maxLatency);
 		assertEquals(ids, o.getMessageIds());
 
 		context.assertIsSatisfied();
@@ -791,11 +793,11 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			oneOf(database).getRawMessage(txn, messageId);
 			will(returnValue(raw));
 			oneOf(database).updateExpiryTime(txn, contactId, messageId,
-					Long.MAX_VALUE);
+					maxLatency);
 			oneOf(database).getRawMessage(txn, messageId1);
 			will(returnValue(raw1));
 			oneOf(database).updateExpiryTime(txn, contactId, messageId1,
-					Long.MAX_VALUE);
+					maxLatency);
 			oneOf(database).lowerRequestedFlag(txn, contactId, ids);
 			oneOf(database).commitTransaction(txn);
 		}});
@@ -803,7 +805,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				eventBus, shutdown);
 
 		assertEquals(messages, db.generateRequestedBatch(contactId, size * 2,
-				Long.MAX_VALUE));
+				maxLatency));
 
 		context.assertIsSatisfied();
 	}
@@ -821,14 +823,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getRetentionUpdate(txn, contactId, Long.MAX_VALUE);
+			oneOf(database).getRetentionUpdate(txn, contactId, maxLatency);
 			will(returnValue(null));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		assertNull(db.generateRetentionUpdate(contactId, Long.MAX_VALUE));
+		assertNull(db.generateRetentionUpdate(contactId, maxLatency));
 
 		context.assertIsSatisfied();
 	}
@@ -846,15 +848,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getRetentionUpdate(txn, contactId, Long.MAX_VALUE);
+			oneOf(database).getRetentionUpdate(txn, contactId, maxLatency);
 			will(returnValue(new RetentionUpdate(0, 1)));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		RetentionUpdate u = db.generateRetentionUpdate(contactId,
-				Long.MAX_VALUE);
+		RetentionUpdate u = db.generateRetentionUpdate(contactId, maxLatency);
 		assertEquals(0, u.getRetentionTime());
 		assertEquals(1, u.getVersion());
 
@@ -874,15 +875,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getSubscriptionUpdate(txn, contactId,
-					Long.MAX_VALUE);
+			oneOf(database).getSubscriptionUpdate(txn, contactId, maxLatency);
 			will(returnValue(null));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		assertNull(db.generateSubscriptionUpdate(contactId, Long.MAX_VALUE));
+		assertNull(db.generateSubscriptionUpdate(contactId, maxLatency));
 
 		context.assertIsSatisfied();
 	}
@@ -900,8 +900,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getSubscriptionUpdate(txn, contactId,
-					Long.MAX_VALUE);
+			oneOf(database).getSubscriptionUpdate(txn, contactId, maxLatency);
 			will(returnValue(new SubscriptionUpdate(Arrays.asList(group), 1)));
 			oneOf(database).commitTransaction(txn);
 		}});
@@ -909,7 +908,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				eventBus, shutdown);
 
 		SubscriptionUpdate u = db.generateSubscriptionUpdate(contactId,
-				Long.MAX_VALUE);
+				maxLatency);
 		assertEquals(Arrays.asList(group), u.getGroups());
 		assertEquals(1, u.getVersion());
 
@@ -929,14 +928,14 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getTransportUpdates(txn, contactId, Long.MAX_VALUE);
+			oneOf(database).getTransportUpdates(txn, contactId, maxLatency);
 			will(returnValue(null));
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, cleaner,
 				eventBus, shutdown);
 
-		assertNull(db.generateTransportUpdates(contactId, Long.MAX_VALUE));
+		assertNull(db.generateTransportUpdates(contactId, maxLatency));
 
 		context.assertIsSatisfied();
 	}
@@ -954,7 +953,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 			will(returnValue(txn));
 			oneOf(database).containsContact(txn, contactId);
 			will(returnValue(true));
-			oneOf(database).getTransportUpdates(txn, contactId, Long.MAX_VALUE);
+			oneOf(database).getTransportUpdates(txn, contactId, maxLatency);
 			will(returnValue(Arrays.asList(new TransportUpdate(transportId,
 					transportProperties, 1))));
 			oneOf(database).commitTransaction(txn);
@@ -963,7 +962,7 @@ public abstract class DatabaseComponentTest extends BriarTestCase {
 				eventBus, shutdown);
 
 		Collection<TransportUpdate> updates =
-				db.generateTransportUpdates(contactId, Long.MAX_VALUE);
+				db.generateTransportUpdates(contactId, maxLatency);
 		assertNotNull(updates);
 		assertEquals(1, updates.size());
 		TransportUpdate u = updates.iterator().next();
diff --git a/briar-tests/src/org/briarproject/db/ExponentialBackoffTest.java b/briar-tests/src/org/briarproject/db/ExponentialBackoffTest.java
index 923939e304409933a61520588f800dfdc94aa35a..f758e8c9e0c0be5ab1150d9f8cf58d44be5a2cd7 100644
--- a/briar-tests/src/org/briarproject/db/ExponentialBackoffTest.java
+++ b/briar-tests/src/org/briarproject/db/ExponentialBackoffTest.java
@@ -32,32 +32,30 @@ public class ExponentialBackoffTest extends BriarTestCase {
 		assertEquals(now, fromNow - fromZero);
 	}
 
-	@Test
-	public void testRoundTripTimeOverflow() {
-		long maxLatency = Long.MAX_VALUE / 2 + 1; // RTT will overflow
-		long expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 0);
-		assertEquals(Long.MAX_VALUE, expiry); // Overflow caught
-	}
-
 	@Test
 	public void testTransmissionCountOverflow() {
-		long maxLatency = (Long.MAX_VALUE - 1) / 2; // RTT will not overflow
+		int maxLatency = Integer.MAX_VALUE; // RTT will not overflow
 		long expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 0);
-		assertEquals(Long.MAX_VALUE - 1, expiry); // No overflow
-		expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 1);
+		assertEquals(Integer.MAX_VALUE * 2L, expiry); // No overflow
+		expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 31);
+		assertEquals(Integer.MAX_VALUE * (2L << 31), expiry); // No overflow
+		expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 32);
 		assertEquals(Long.MAX_VALUE, expiry); // Overflow caught
-		expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 2);
+		expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 33);
 		assertEquals(Long.MAX_VALUE, expiry); // Overflow caught
 	}
 
 	@Test
 	public void testCurrentTimeOverflow() {
-		long maxLatency = (Long.MAX_VALUE - 1) / 2; // RTT will not overflow
-		long expiry = ExponentialBackoff.calculateExpiry(0, maxLatency, 0);
+		int maxLatency = Integer.MAX_VALUE; // RTT will not overflow
+		long now = Long.MAX_VALUE - (Integer.MAX_VALUE * (2L << 31));
+		long expiry = ExponentialBackoff.calculateExpiry(now, maxLatency, 0);
+		assertEquals(now + Integer.MAX_VALUE * 2L, expiry); // No overflow
+		expiry = ExponentialBackoff.calculateExpiry(now - 1, maxLatency, 31);
 		assertEquals(Long.MAX_VALUE - 1, expiry); // No overflow
-		expiry = ExponentialBackoff.calculateExpiry(1, maxLatency, 0);
+		expiry = ExponentialBackoff.calculateExpiry(now, maxLatency, 31);
 		assertEquals(Long.MAX_VALUE, expiry); // No overflow
-		expiry = ExponentialBackoff.calculateExpiry(2, maxLatency, 0);
+		expiry = ExponentialBackoff.calculateExpiry(now + 1, maxLatency, 32);
 		assertEquals(Long.MAX_VALUE, expiry); // Overflow caught
 	}
 }
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index 3ecaf692a1501f0bc085b8d2c25c463bfce7828c..b567c2778ca97c8473bed2d2a2ba5b5d486f59f2 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -381,7 +381,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		assertTrue(it.hasNext());
 		assertEquals(messageId, it.next());
 		assertFalse(it.hasNext());
-		db.updateExpiryTime(txn, contactId, messageId, Long.MAX_VALUE);
+		db.updateExpiryTime(txn, contactId, messageId, Integer.MAX_VALUE);
 
 		// The message should no longer be sendable
 		it = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE).iterator();
@@ -1109,7 +1109,8 @@ public class H2DatabaseTest extends BriarTestCase {
 	@Test
 	public void testTemporarySecrets() throws Exception {
 		// Create an endpoint and four consecutive temporary secrets
-		long epoch = 123, latency = 234;
+		long epoch = 123;
+		int latency = 234;
 		boolean alice = false;
 		long outgoing1 = 345, centre1 = 456;
 		long outgoing2 = 567, centre2 = 678;
@@ -1235,7 +1236,8 @@ public class H2DatabaseTest extends BriarTestCase {
 	@Test
 	public void testIncrementStreamCounter() throws Exception {
 		// Create an endpoint and a temporary secret
-		long epoch = 123, latency = 234;
+		long epoch = 123;
+		int latency = 234;
 		boolean alice = false;
 		long period = 345, outgoing = 456, centre = 567;
 		Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
@@ -1290,7 +1292,8 @@ public class H2DatabaseTest extends BriarTestCase {
 	@Test
 	public void testSetReorderingWindow() throws Exception {
 		// Create an endpoint and a temporary secret
-		long epoch = 123, latency = 234;
+		long epoch = 123;
+		int latency = 234;
 		boolean alice = false;
 		long period = 345, outgoing = 456, centre = 567;
 		Endpoint ep = new Endpoint(contactId, transportId, epoch, alice);
@@ -1359,8 +1362,8 @@ public class H2DatabaseTest extends BriarTestCase {
 	@Test
 	public void testEndpoints() throws Exception {
 		// Create some endpoints
-		long epoch1 = 123, latency1 = 234;
-		long epoch2 = 345, latency2 = 456;
+		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");
diff --git a/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
index 4d5b25767efae6b90f9a055a6d6f99eac920aba9..75583607001c8e619b3dd5771deb991acec71b8a 100644
--- a/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
@@ -2,12 +2,14 @@ package org.briarproject.messaging;
 
 import static org.briarproject.api.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.messaging.MessagingConstants.GROUP_SALT_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
 import java.util.Random;
 
 import org.briarproject.BriarTestCase;
@@ -32,13 +34,13 @@ import org.briarproject.api.messaging.Message;
 import org.briarproject.api.messaging.MessageFactory;
 import org.briarproject.api.messaging.MessageVerifier;
 import org.briarproject.api.messaging.MessagingSession;
+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.StreamContext;
-import org.briarproject.api.transport.StreamReader;
 import org.briarproject.api.transport.StreamReaderFactory;
-import org.briarproject.api.transport.StreamWriter;
 import org.briarproject.api.transport.StreamWriterFactory;
 import org.briarproject.api.transport.TagRecogniser;
 import org.briarproject.crypto.CryptoModule;
@@ -56,8 +58,9 @@ import com.google.inject.Injector;
 
 public class SimplexMessagingIntegrationTest extends BriarTestCase {
 
-	private static final long CLOCK_DIFFERENCE = 60 * 1000;
-	private static final long LATENCY = 60 * 1000;
+	private static final int MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
+	private static final int ROTATION_PERIOD =
+			MAX_CLOCK_DIFFERENCE + MAX_LATENCY;
 
 	private final File testDir = TestUtils.getTestDirectory();
 	private final File aliceDir = new File(testDir, "alice");
@@ -73,8 +76,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		// Create matching secrets for Alice and Bob
 		initialSecret = new byte[32];
 		new Random().nextBytes(initialSecret);
-		long rotationPeriod = 2 * CLOCK_DIFFERENCE + LATENCY;
-		epoch = System.currentTimeMillis() - 2 * rotationPeriod;
+		epoch = System.currentTimeMillis() - 2 * ROTATION_PERIOD;
 	}
 
 	@Override
@@ -121,10 +123,10 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		db.addGroup(group);
 		db.setInboxGroup(contactId, group);
 		// Add the transport and the endpoint
-		db.addTransport(transportId, LATENCY);
+		db.addTransport(transportId, MAX_LATENCY);
 		Endpoint ep = new Endpoint(contactId, transportId, epoch, true);
 		db.addEndpoint(ep);
-		keyManager.endpointAdded(ep, LATENCY, initialSecret.clone());
+		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
 		// Send Bob a message
 		String contentType = "text/plain";
 		long timestamp = System.currentTimeMillis();
@@ -133,25 +135,27 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		Message message = messageFactory.createAnonymousMessage(null, group,
 				contentType, timestamp, body);
 		db.addLocalMessage(message);
+		// Get a stream context
+		StreamContext ctx = keyManager.getStreamContext(contactId, transportId);
+		assertNotNull(ctx);
 		// Create a stream writer
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		StreamWriterFactory streamWriterFactory =
 				alice.getInstance(StreamWriterFactory.class);
-		StreamContext ctx = keyManager.getStreamContext(contactId, transportId);
-		assertNotNull(ctx);
-		StreamWriter streamWriter = streamWriterFactory.createStreamWriter(out,
-				MAX_FRAME_LENGTH, ctx);
+		OutputStream streamWriter =
+				streamWriterFactory.createStreamWriter(out, ctx);
 		// Create an outgoing messaging session
 		EventBus eventBus = alice.getInstance(EventBus.class);
 		PacketWriterFactory packetWriterFactory =
 				alice.getInstance(PacketWriterFactory.class);
+		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(
+				streamWriter);
 		MessagingSession session = new SimplexOutgoingSession(db,
-				new ImmediateExecutor(), eventBus, packetWriterFactory,
-				contactId, transportId, Long.MAX_VALUE,
-				streamWriter.getOutputStream());
+				new ImmediateExecutor(), eventBus, contactId, transportId,
+				MAX_LATENCY, packetWriter);
 		// Write whatever needs to be written
 		session.run();
-		streamWriter.getOutputStream().close();
+		streamWriter.close();
 		// Clean up
 		keyManager.stop();
 		db.close();
@@ -182,10 +186,10 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		db.addGroup(group);
 		db.setInboxGroup(contactId, group);
 		// Add the transport and the endpoint
-		db.addTransport(transportId, LATENCY);
+		db.addTransport(transportId, MAX_LATENCY);
 		Endpoint ep = new Endpoint(contactId, transportId, epoch, false);
 		db.addEndpoint(ep);
-		keyManager.endpointAdded(ep, LATENCY, initialSecret.clone());
+		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
 		// Set up an event listener
 		MessageListener listener = new MessageListener();
 		bob.getInstance(EventBus.class).addListener(listener);
@@ -200,23 +204,24 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		// Create a stream reader
 		StreamReaderFactory streamReaderFactory =
 				bob.getInstance(StreamReaderFactory.class);
-		StreamReader streamReader = streamReaderFactory.createStreamReader(in,
-				MAX_FRAME_LENGTH, ctx);
+		InputStream streamReader =
+				streamReaderFactory.createStreamReader(in, ctx);
 		// Create an incoming messaging session
 		EventBus eventBus = bob.getInstance(EventBus.class);
 		MessageVerifier messageVerifier =
 				bob.getInstance(MessageVerifier.class);
 		PacketReaderFactory packetReaderFactory =
 				bob.getInstance(PacketReaderFactory.class);
+		PacketReader packetReader = packetReaderFactory.createPacketReader(
+				streamReader);
 		MessagingSession session = new IncomingSession(db,
 				new ImmediateExecutor(), new ImmediateExecutor(), eventBus,
-				messageVerifier, packetReaderFactory, contactId, transportId,
-				streamReader.getInputStream());
+				messageVerifier, contactId, transportId, packetReader);
 		// No messages should have been added yet
 		assertFalse(listener.messageAdded);
 		// Read whatever needs to be read
 		session.run();
-		streamReader.getInputStream().close();
+		streamReader.close();
 		// The private message from Alice should have been added
 		assertTrue(listener.messageAdded);
 		// Clean up
diff --git a/briar-tests/src/org/briarproject/messaging/SimplexOutgoingSessionTest.java b/briar-tests/src/org/briarproject/messaging/SimplexOutgoingSessionTest.java
index f9fa68c6fdd0efef239201b32a131848093983de..fa13af51024f6cdc9938635c41ff3def51032182 100644
--- a/briar-tests/src/org/briarproject/messaging/SimplexOutgoingSessionTest.java
+++ b/briar-tests/src/org/briarproject/messaging/SimplexOutgoingSessionTest.java
@@ -1,72 +1,53 @@
 package org.briarproject.messaging;
 
-import java.io.ByteArrayOutputStream;
 import java.util.Arrays;
-import java.util.Random;
 import java.util.concurrent.Executor;
 
 import org.briarproject.BriarTestCase;
 import org.briarproject.TestUtils;
 import org.briarproject.api.ContactId;
 import org.briarproject.api.TransportId;
-import org.briarproject.api.UniqueId;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.messaging.Ack;
 import org.briarproject.api.messaging.MessageId;
-import org.briarproject.api.messaging.PacketWriterFactory;
+import org.briarproject.api.messaging.PacketWriter;
 import org.briarproject.plugins.ImmediateExecutor;
-import org.briarproject.serial.SerialModule;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Module;
-
 public class SimplexOutgoingSessionTest extends BriarTestCase {
 
-	// FIXME: This is an integration test, not a unit test
+	private static final int MAX_MESSAGES_PER_ACK = 10;
 
 	private final Mockery context;
 	private final DatabaseComponent db;
 	private final Executor dbExecutor;
 	private final EventBus eventBus;
-	private final PacketWriterFactory packetWriterFactory;
 	private final ContactId contactId;
 	private final TransportId transportId;
 	private final MessageId messageId;
-	private final byte[] secret;
+	private final int maxLatency;
+	private final PacketWriter packetWriter;
 
 	public SimplexOutgoingSessionTest() {
 		context = new Mockery();
 		db = context.mock(DatabaseComponent.class);
 		dbExecutor = new ImmediateExecutor();
-		Module testModule = new AbstractModule() {
-			@Override
-			public void configure() {
-				bind(PacketWriterFactory.class).to(
-						PacketWriterFactoryImpl.class);
-			}
-		};
-		Injector i = Guice.createInjector(testModule, new SerialModule());
 		eventBus = context.mock(EventBus.class);
-		packetWriterFactory = i.getInstance(PacketWriterFactory.class);
+		packetWriter = context.mock(PacketWriter.class);
 		contactId = new ContactId(234);
 		transportId = new TransportId("id");
 		messageId = new MessageId(TestUtils.getRandomId());
-		secret = new byte[32];
-		new Random().nextBytes(secret);
+		maxLatency = Integer.MAX_VALUE;
 	}
 
 	@Test
 	public void testNothingToSend() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
-				dbExecutor, eventBus, packetWriterFactory, contactId,
-				transportId, Long.MAX_VALUE, out);
+				dbExecutor, eventBus, contactId, transportId, maxLatency,
+				packetWriter);
 		context.checking(new Expectations() {{
 			// Add listener
 			oneOf(eventBus).addListener(session);
@@ -74,46 +55,45 @@ public class SimplexOutgoingSessionTest extends BriarTestCase {
 			oneOf(db).generateTransportAcks(contactId);
 			will(returnValue(null));
 			// No transport updates to send
-			oneOf(db).generateTransportUpdates(with(contactId),
-					with(any(long.class)));
+			oneOf(db).generateTransportUpdates(contactId, maxLatency);
 			will(returnValue(null));
 			// No subscription ack to send
 			oneOf(db).generateSubscriptionAck(contactId);
 			will(returnValue(null));
 			// No subscription update to send
-			oneOf(db).generateSubscriptionUpdate(with(contactId),
-					with(any(long.class)));
+			oneOf(db).generateSubscriptionUpdate(contactId, maxLatency);
 			will(returnValue(null));
 			// No retention ack to send
 			oneOf(db).generateRetentionAck(contactId);
 			will(returnValue(null));
 			// No retention update to send
-			oneOf(db).generateRetentionUpdate(with(contactId),
-					with(any(long.class)));
+			oneOf(db).generateRetentionUpdate(contactId, maxLatency);
 			will(returnValue(null));
 			// No acks to send
-			oneOf(db).generateAck(with(contactId), with(any(int.class)));
+			oneOf(packetWriter).getMaxMessagesForAck(with(any(long.class)));
+			will(returnValue(MAX_MESSAGES_PER_ACK));
+			oneOf(db).generateAck(contactId, MAX_MESSAGES_PER_ACK);
 			will(returnValue(null));
 			// No messages to send
 			oneOf(db).generateBatch(with(contactId), with(any(int.class)),
-					with(any(long.class)));
+					with(maxLatency));
 			will(returnValue(null));
+			// Flush the output stream
+			oneOf(packetWriter).flush();
 			// Remove listener
 			oneOf(eventBus).removeListener(session);
 		}});
 		session.run();
-		// Nothing should have been written
-		assertEquals(0, out.size());
 		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testSomethingToSend() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
-				dbExecutor, eventBus, packetWriterFactory, contactId,
-				transportId, Long.MAX_VALUE, out);
+		final Ack ack = new Ack(Arrays.asList(messageId));
 		final byte[] raw = new byte[1234];
+		final SimplexOutgoingSession session = new SimplexOutgoingSession(db,
+				dbExecutor, eventBus, contactId, transportId, maxLatency,
+				packetWriter);
 		context.checking(new Expectations() {{
 			// Add listener
 			oneOf(eventBus).addListener(session);
@@ -121,43 +101,46 @@ public class SimplexOutgoingSessionTest extends BriarTestCase {
 			oneOf(db).generateTransportAcks(contactId);
 			will(returnValue(null));
 			// No transport updates to send
-			oneOf(db).generateTransportUpdates(with(contactId),
-					with(any(long.class)));
+			oneOf(db).generateTransportUpdates(contactId, maxLatency);
 			will(returnValue(null));
 			// No subscription ack to send
 			oneOf(db).generateSubscriptionAck(contactId);
 			will(returnValue(null));
 			// No subscription update to send
-			oneOf(db).generateSubscriptionUpdate(with(contactId),
-					with(any(long.class)));
+			oneOf(db).generateSubscriptionUpdate(contactId, maxLatency);
 			will(returnValue(null));
 			// No retention ack to send
 			oneOf(db).generateRetentionAck(contactId);
 			will(returnValue(null));
 			// No retention update to send
-			oneOf(db).generateRetentionUpdate(with(contactId),
-					with(any(long.class)));
+			oneOf(db).generateRetentionUpdate(contactId, maxLatency);
 			will(returnValue(null));
 			// One ack to send
-			oneOf(db).generateAck(with(contactId), with(any(int.class)));
-			will(returnValue(new Ack(Arrays.asList(messageId))));
+			oneOf(packetWriter).getMaxMessagesForAck(with(any(long.class)));
+			will(returnValue(MAX_MESSAGES_PER_ACK));
+			oneOf(db).generateAck(contactId, MAX_MESSAGES_PER_ACK);
+			will(returnValue(ack));
+			oneOf(packetWriter).writeAck(ack);
 			// No more acks
-			oneOf(db).generateAck(with(contactId), with(any(int.class)));
+			oneOf(packetWriter).getMaxMessagesForAck(with(any(long.class)));
+			will(returnValue(MAX_MESSAGES_PER_ACK));
+			oneOf(db).generateAck(contactId, MAX_MESSAGES_PER_ACK);
 			will(returnValue(null));
 			// One message to send
 			oneOf(db).generateBatch(with(contactId), with(any(int.class)),
-					with(any(long.class)));
+					with(maxLatency));
 			will(returnValue(Arrays.asList(raw)));
+			oneOf(packetWriter).writeMessage(raw);
 			// No more messages
 			oneOf(db).generateBatch(with(contactId), with(any(int.class)),
-					with(any(long.class)));
+					with(maxLatency));
 			will(returnValue(null));
+			// Flush the output stream
+			oneOf(packetWriter).flush();
 			// Remove listener
 			oneOf(eventBus).removeListener(session);
 		}});
 		session.run();
-		// Something should have been written
-		assertTrue(out.size() > UniqueId.LENGTH + raw.length);
 		context.assertIsSatisfied();
 	}
 }
diff --git a/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java b/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
index 2e088fa3066b09da872c8c1adb5fb9db60ed8efa..8dab883cb74cade22f78f7e63d188c9156a2d5e8 100644
--- a/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/plugins/PluginManagerImplTest.java
@@ -44,19 +44,19 @@ public class PluginManagerImplTest extends BriarTestCase {
 				context.mock(SimplexPluginFactory.class);
 		final SimplexPlugin simplexPlugin = context.mock(SimplexPlugin.class);
 		final TransportId simplexId = new TransportId("simplex");
-		final long simplexLatency = 12345;
+		final int simplexLatency = 12345;
 		final SimplexPluginFactory simplexFailFactory =
 				context.mock(SimplexPluginFactory.class, "simplexFailFactory");
 		final SimplexPlugin simplexFailPlugin =
 				context.mock(SimplexPlugin.class, "simplexFailPlugin");
 		final TransportId simplexFailId = new TransportId("simplex1");
-		final long simplexFailLatency = 23456;
+		final int simplexFailLatency = 23456;
 		// Two duplex plugin factories: one creates a plugin, the other fails
 		final DuplexPluginFactory duplexFactory =
 				context.mock(DuplexPluginFactory.class);
 		final DuplexPlugin duplexPlugin = context.mock(DuplexPlugin.class);
 		final TransportId duplexId = new TransportId("duplex");
-		final long duplexLatency = 34567;
+		final int duplexLatency = 34567;
 		final DuplexPluginFactory duplexFailFactory =
 				context.mock(DuplexPluginFactory.class, "duplexFailFactory");
 		final TransportId duplexFailId = new TransportId("duplex1");
diff --git a/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothClientTest.java b/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothClientTest.java
index 3b86da8a1bd3a8f80c79445f80d14082261921c9..b046f3a6bc615862785b3c9c564c45342fd25bdd 100644
--- a/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothClientTest.java
+++ b/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothClientTest.java
@@ -23,12 +23,12 @@ public class BluetoothClientTest extends DuplexClientTest {
 		p.put("address", serverAddress);
 		p.put("uuid", BluetoothTest.EMPTY_UUID);
 		Map<ContactId, TransportProperties> remote =
-			Collections.singletonMap(contactId, p);
+				Collections.singletonMap(contactId, p);
 		// Create the plugin
 		callback = new ClientCallback(new TransportConfig(),
 				new TransportProperties(), remote);
 		plugin = new BluetoothPlugin(executor, new SystemClock(),
-				new SecureRandom(), callback, 0, 0, 0);
+				new SecureRandom(), callback, 0, 0);
 	}
 
 	public static void main(String[] args) throws Exception {
diff --git a/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothServerTest.java b/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothServerTest.java
index 1dfbf841dcc52f64bf50f2e94e2d5a5422186c8a..121f71b4031db95c9a114cc1540e7397e565f405 100644
--- a/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothServerTest.java
+++ b/briar-tests/src/org/briarproject/plugins/bluetooth/BluetoothServerTest.java
@@ -23,7 +23,7 @@ public class BluetoothServerTest extends DuplexServerTest {
 		callback = new ServerCallback(new TransportConfig(), local,
 				Collections.singletonMap(contactId, new TransportProperties()));
 		plugin = new BluetoothPlugin(executor, new SystemClock(),
-				new SecureRandom(), callback, 0, 0, 0);
+				new SecureRandom(), callback, 0, 0);
 	}
 
 	public static void main(String[] args) throws Exception {
diff --git a/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java b/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
index b8e4ee88b24ac4fe00e2a7babdb3c973d32f465e..5ae0bbb5030235a60ebb45aa0e05fb146155a34a 100644
--- a/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/file/RemovableDrivePluginTest.java
@@ -1,6 +1,5 @@
 package org.briarproject.plugins.file;
 
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MIN_STREAM_LENGTH;
 
 import java.io.File;
@@ -58,7 +57,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		assertNull(plugin.createWriter(contactId));
@@ -93,7 +92,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		assertNull(plugin.createWriter(contactId));
@@ -130,7 +129,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		assertNull(plugin.createWriter(contactId));
@@ -169,7 +168,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		assertNull(plugin.createWriter(contactId));
@@ -208,7 +207,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		assertNotNull(plugin.createWriter(contactId));
@@ -251,7 +250,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		TransportConnectionWriter writer = plugin.createWriter(contactId);
@@ -290,7 +289,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 		}});
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 		plugin.start();
 
 		plugin.driveInserted(testDir);
@@ -310,7 +309,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 				context.mock(RemovableDriveMonitor.class);
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(executor,
-				fileUtils, callback, finder, monitor, MAX_FRAME_LENGTH, 0);
+				fileUtils, callback, finder, monitor, 0);
 
 		assertFalse(plugin.isPossibleConnectionFilename("abcdefg.dat"));
 		assertFalse(plugin.isPossibleConnectionFilename("abcdefghi.dat"));
@@ -339,7 +338,7 @@ public class RemovableDrivePluginTest extends BriarTestCase {
 
 		RemovableDrivePlugin plugin = new RemovableDrivePlugin(
 				new ImmediateExecutor(), fileUtils, callback, finder, monitor,
-				MAX_FRAME_LENGTH, 0);
+				0);
 		plugin.start();
 
 		File f = new File(testDir, "abcdefgh.dat");
diff --git a/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java b/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java
index 3371bd7e0dfe715f69cbaf55c3bfef7e4ff2afbb..7966ef8c489d4de3d23f7a11786a1f6ad4f6e702 100644
--- a/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/modem/ModemPluginTest.java
@@ -24,7 +24,7 @@ public class ModemPluginTest extends BriarTestCase {
 		final SerialPortList serialPortList =
 				context.mock(SerialPortList.class);
 		final ModemPlugin plugin = new ModemPlugin(modemFactory,
-				serialPortList, null, 0, 0, 0);
+				serialPortList, null, 0);
 		final Modem modem = context.mock(Modem.class);
 		context.checking(new Expectations() {{
 			oneOf(serialPortList).getPortNames();
@@ -58,7 +58,7 @@ public class ModemPluginTest extends BriarTestCase {
 		final DuplexPluginCallback callback =
 				context.mock(DuplexPluginCallback.class);
 		final ModemPlugin plugin = new ModemPlugin(modemFactory,
-				serialPortList, callback, 0, 0, 0);
+				serialPortList, callback, 0);
 		final Modem modem = context.mock(Modem.class);
 		final TransportProperties local = new TransportProperties();
 		local.put("iso3166", ISO_1336);
@@ -99,7 +99,7 @@ public class ModemPluginTest extends BriarTestCase {
 		final DuplexPluginCallback callback =
 				context.mock(DuplexPluginCallback.class);
 		final ModemPlugin plugin = new ModemPlugin(modemFactory,
-				serialPortList, callback, 0, 0, 0);
+				serialPortList, callback, 0);
 		final Modem modem = context.mock(Modem.class);
 		final TransportProperties local = new TransportProperties();
 		local.put("iso3166", ISO_1336);
@@ -140,7 +140,7 @@ public class ModemPluginTest extends BriarTestCase {
 		final DuplexPluginCallback callback =
 				context.mock(DuplexPluginCallback.class);
 		final ModemPlugin plugin = new ModemPlugin(modemFactory,
-				serialPortList, callback, 0, 0, 0);
+				serialPortList, callback, 0);
 		final Modem modem = context.mock(Modem.class);
 		final TransportProperties local = new TransportProperties();
 		local.put("iso3166", ISO_1336);
diff --git a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpClientTest.java b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpClientTest.java
index 8991467a9188b5034326c938f358bd57ae514f64..19168a5675aec21ec229602734a213081ac41285 100644
--- a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpClientTest.java
+++ b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpClientTest.java
@@ -15,6 +15,10 @@ import org.briarproject.plugins.DuplexClientTest;
 // is running on another machine
 public class LanTcpClientTest extends DuplexClientTest {
 
+	private static final int MAX_LATENCY = 60 * 1000;
+	private static final int MAX_IDLE_TIME = 30 * 1000;
+	private static final int POLLING_INTERVAL = 60 * 1000;
+
 	private LanTcpClientTest(Executor executor, String serverAddress,
 			String serverPort) {
 		// Store the server's internal address and port
@@ -22,11 +26,12 @@ public class LanTcpClientTest extends DuplexClientTest {
 		p.put("address", serverAddress);
 		p.put("port", serverPort);
 		Map<ContactId, TransportProperties> remote =
-			Collections.singletonMap(contactId, p);
+				Collections.singletonMap(contactId, p);
 		// Create the plugin
 		callback = new ClientCallback(new TransportConfig(),
 				new TransportProperties(), remote);
-		plugin = new LanTcpPlugin(executor, callback, 0, 0, 0);
+		plugin = new LanTcpPlugin(executor, callback,  MAX_LATENCY,
+				MAX_IDLE_TIME, POLLING_INTERVAL);
 	}
 
 	public static void main(String[] args) throws Exception {
diff --git a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpServerTest.java b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpServerTest.java
index ff96ae16d2a15848ee48fe3fda1b123b61b1c513..7531978148639e5b2b1eb232ecacbb8caf505582 100644
--- a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpServerTest.java
+++ b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpServerTest.java
@@ -13,11 +13,16 @@ import org.briarproject.plugins.DuplexServerTest;
 // is running on another machine
 public class LanTcpServerTest extends DuplexServerTest {
 
+	private static final int MAX_LATENCY = 60 * 1000;
+	private static final int MAX_IDLE_TIME = 30 * 1000;
+	private static final int POLLING_INTERVAL = 60 * 1000;
+
 	private LanTcpServerTest(Executor executor) {
 		callback = new ServerCallback(new TransportConfig(),
 				new TransportProperties(),
 				Collections.singletonMap(contactId, new TransportProperties()));
-		plugin = new LanTcpPlugin(executor, callback, 0, 0, 0);
+		plugin = new LanTcpPlugin(executor, callback, MAX_LATENCY,
+				MAX_IDLE_TIME, POLLING_INTERVAL);
 	}
 
 	public static void main(String[] args) throws Exception {
diff --git a/briar-tests/src/org/briarproject/transport/IncomingEncryptionLayerTest.java b/briar-tests/src/org/briarproject/transport/IncomingEncryptionLayerTest.java
deleted file mode 100644
index 8e1791785632439f24962584d36c03e6d41f06de..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/transport/IncomingEncryptionLayerTest.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package org.briarproject.transport;
-
-import static org.briarproject.api.transport.TransportConstants.AAD_LENGTH;
-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 java.io.ByteArrayInputStream;
-
-import org.briarproject.BriarTestCase;
-import org.briarproject.TestLifecycleModule;
-import org.briarproject.TestSystemModule;
-import org.briarproject.api.FormatException;
-import org.briarproject.api.crypto.AuthenticatedCipher;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.crypto.CryptoModule;
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class IncomingEncryptionLayerTest extends BriarTestCase {
-
-	// FIXME: This is an integration test, not a unit test
-
-	private static final int FRAME_LENGTH = 1024;
-	private static final int MAX_PAYLOAD_LENGTH =
-			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
-
-	private final CryptoComponent crypto;
-	private final AuthenticatedCipher frameCipher;
-	private final SecretKey frameKey;
-
-	public IncomingEncryptionLayerTest() {
-		Injector i = Guice.createInjector(new CryptoModule(),
-				new TestLifecycleModule(), new TestSystemModule());
-		crypto = i.getInstance(CryptoComponent.class);
-		frameCipher = crypto.getFrameCipher();
-		frameKey = crypto.generateSecretKey();
-	}
-
-	@Test
-	public void testReadValidFrames() throws Exception {
-		// Generate two valid frames
-		byte[] frame = generateFrame(0, FRAME_LENGTH, 123, false, false);
-		byte[] frame1 = generateFrame(1, FRAME_LENGTH, 123, false, false);
-		// Concatenate the frames
-		byte[] valid = new byte[FRAME_LENGTH * 2];
-		System.arraycopy(frame, 0, valid, 0, FRAME_LENGTH);
-		System.arraycopy(frame1, 0, valid, FRAME_LENGTH, FRAME_LENGTH);
-		// Read the frames
-		ByteArrayInputStream in = new ByteArrayInputStream(valid);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH];
-		assertEquals(123, i.readFrame(buf));
-		assertEquals(123, i.readFrame(buf));
-	}
-
-	@Test
-	public void testTruncatedFrameThrowsException() throws Exception {
-		// Generate a valid frame
-		byte[] frame = generateFrame(0, FRAME_LENGTH, 123, false, false);
-		// Chop off the last byte
-		byte[] truncated = new byte[FRAME_LENGTH - 1];
-		System.arraycopy(frame, 0, truncated, 0, FRAME_LENGTH - 1);
-		// Try to read the frame, which should fail due to truncation
-		ByteArrayInputStream in = new ByteArrayInputStream(truncated);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testModifiedFrameThrowsException() throws Exception {
-		// Generate a valid frame
-		byte[] frame = generateFrame(0, FRAME_LENGTH, 123, false, false);
-		// Modify a randomly chosen byte of the frame
-		frame[(int) (Math.random() * FRAME_LENGTH)] ^= 1;
-		// Try to read the frame, which should fail due to modification
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testShortNonFinalFrameThrowsException() throws Exception {
-		// Generate a short non-final frame
-		byte[] frame = generateFrame(0, FRAME_LENGTH - 1, 123, false, false);
-		// Try to read the frame, which should fail due to invalid length
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testShortFinalFrameDoesNotThrowException() throws Exception {
-		// Generate a short final frame
-		byte[] frame = generateFrame(0, FRAME_LENGTH - 1, 123, true, false);
-		// Read the frame
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		int length = i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-		assertEquals(123, length);
-	}
-
-	@Test
-	public void testInvalidPayloadLengthThrowsException() throws Exception {
-		// Generate a frame with an invalid payload length
-		byte[] frame = generateFrame(0, FRAME_LENGTH, MAX_PAYLOAD_LENGTH + 1,
-				false, false);
-		// Try to read the frame, which should fail due to invalid length
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testNonZeroPaddingThrowsException() throws Exception {
-		// Generate a frame with bad padding
-		byte[] frame = generateFrame(0, FRAME_LENGTH, 123, false, true);
-		// Try to read the frame, which should fail due to bad padding
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testCannotReadBeyondFinalFrame() throws Exception {
-		// Generate a valid final frame and another valid final frame after it
-		byte[] frame = generateFrame(0, FRAME_LENGTH, MAX_PAYLOAD_LENGTH, true,
-				false);
-		byte[] frame1 = generateFrame(1, FRAME_LENGTH, 123, true, false);
-		// Concatenate the frames
-		byte[] extraFrame = new byte[FRAME_LENGTH * 2];
-		System.arraycopy(frame, 0, extraFrame, 0, FRAME_LENGTH);
-		System.arraycopy(frame1, 0, extraFrame, FRAME_LENGTH, FRAME_LENGTH);
-		// Read the final frame, which should first read the tag
-		ByteArrayInputStream in = new ByteArrayInputStream(extraFrame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH];
-		assertEquals(MAX_PAYLOAD_LENGTH, i.readFrame(buf));
-		// The frame after the final frame should not be read
-		assertEquals(-1, i.readFrame(buf));
-	}
-
-	private byte[] generateFrame(long frameNumber, int frameLength,
-			int payloadLength, boolean finalFrame, boolean badPadding)
-					throws Exception {
-		byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH];
-		byte[] plaintext = new byte[frameLength - MAC_LENGTH];
-		byte[] ciphertext = new byte[frameLength];
-		FrameEncoder.encodeIv(iv, frameNumber);
-		FrameEncoder.encodeAad(aad, frameNumber, plaintext.length);
-		frameCipher.init(true, frameKey, iv, aad);
-		FrameEncoder.encodeHeader(plaintext, finalFrame, payloadLength);
-		if(badPadding) plaintext[HEADER_LENGTH + payloadLength] = 1;
-		frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0);
-		return ciphertext;
-	}
-}
diff --git a/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java b/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java
index f11f11f154260870fa75d7fa5afe93224497394f..12880b1b8ea8789ce2c690f30eac6c2ed430e2d7 100644
--- a/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java
+++ b/briar-tests/src/org/briarproject/transport/KeyManagerImplTest.java
@@ -26,9 +26,9 @@ import org.junit.Test;
 public class KeyManagerImplTest extends BriarTestCase {
 
 	private static final long EPOCH = 1000L * 1000L * 1000L * 1000L;
-	private static final long MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
-	private static final long ROTATION_PERIOD_LENGTH =
-			MAX_LATENCY + MAX_CLOCK_DIFFERENCE;
+	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;
@@ -103,9 +103,9 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -123,11 +123,11 @@ public class KeyManagerImplTest extends BriarTestCase {
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(EPOCH));
 			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0.clone()));
+			will(returnValue(secret0));
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			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);
@@ -140,7 +140,7 @@ public class KeyManagerImplTest extends BriarTestCase {
 		}});
 
 		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret.clone());
+		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
 		keyManager.stop();
 
 		context.assertIsSatisfied();
@@ -161,9 +161,9 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -181,11 +181,11 @@ public class KeyManagerImplTest extends BriarTestCase {
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(EPOCH));
 			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0.clone()));
+			will(returnValue(secret0));
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			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);
@@ -201,7 +201,7 @@ public class KeyManagerImplTest extends BriarTestCase {
 		}});
 
 		assertTrue(keyManager.start());
-		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret.clone());
+		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
 		StreamContext ctx =
 				keyManager.getStreamContext(contactId, transportId);
 		assertNotNull(ctx);
@@ -230,9 +230,9 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -278,11 +278,11 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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.clone());
+		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
 
 		context.checking(new Expectations() {{
 			// start()
@@ -294,14 +294,14 @@ public class KeyManagerImplTest extends BriarTestCase {
 					MAX_LATENCY)));
 			// The current time is the start of period 2
 			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + ROTATION_PERIOD_LENGTH));
+			will(returnValue(EPOCH + ROTATION_PERIOD));
 			// The secret for period 3 should be derived and stored
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			will(returnValue(secret2));
 			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3.clone()));
+			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);
@@ -336,12 +336,12 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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.clone());
-		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4.clone());
+		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
+		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4);
 
 		context.checking(new Expectations() {{
 			// start()
@@ -353,14 +353,14 @@ public class KeyManagerImplTest extends BriarTestCase {
 					MAX_LATENCY)));
 			// The current time is the end of period 3
 			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + 3 * ROTATION_PERIOD_LENGTH - 1));
+			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.clone()));
+			will(returnValue(secret2));
 			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3.clone()));
+			will(returnValue(secret3));
 			oneOf(crypto).deriveNextSecret(secret3, 4);
-			will(returnValue(secret4.clone()));
+			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
@@ -396,9 +396,9 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -459,11 +459,11 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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.clone());
+		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
 
 		context.checking(new Expectations() {{
 			// start()
@@ -484,13 +484,13 @@ public class KeyManagerImplTest extends BriarTestCase {
 					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_LENGTH + 1));
+			will(returnValue(EPOCH + ROTATION_PERIOD + 1));
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			will(returnValue(secret2));
 			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3.clone()));
+			will(returnValue(secret3));
 			oneOf(tagRecogniser).removeSecret(contactId, transportId, 0);
 			oneOf(db).addSecrets(Arrays.asList(s3));
 			oneOf(tagRecogniser).addSecret(s3);
@@ -533,12 +533,12 @@ public class KeyManagerImplTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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.clone());
-		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4.clone());
+		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
+		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4);
 
 		context.checking(new Expectations() {{
 			// start()
@@ -559,13 +559,13 @@ public class KeyManagerImplTest extends BriarTestCase {
 					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_LENGTH + 1));
+			will(returnValue(EPOCH + 2 * ROTATION_PERIOD + 1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			will(returnValue(secret2));
 			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3.clone()));
+			will(returnValue(secret3));
 			oneOf(crypto).deriveNextSecret(secret3, 4);
-			will(returnValue(secret4.clone()));
+			will(returnValue(secret4));
 			oneOf(tagRecogniser).removeSecret(contactId, transportId, 0);
 			oneOf(tagRecogniser).removeSecret(contactId, transportId, 1);
 			oneOf(db).addSecrets(Arrays.asList(s3, s4));
diff --git a/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java b/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java
index 1207f9363e85b7042050a0cc2f8779148c391cb1..84b6f89289d70d4b868a349d3765cdd266d68167 100644
--- a/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/transport/KeyRotationIntegrationTest.java
@@ -32,14 +32,15 @@ import org.junit.Test;
 public class KeyRotationIntegrationTest extends BriarTestCase {
 
 	private static final long EPOCH = 1000L * 1000L * 1000L * 1000L;
-	private static final long MAX_LATENCY = 2 * 60 * 1000; // 2 minutes
-	private static final long ROTATION_PERIOD_LENGTH =
-			MAX_LATENCY + MAX_CLOCK_DIFFERENCE;
+	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() {
@@ -60,6 +61,11 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		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;
@@ -112,9 +118,6 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		final Clock clock = context.mock(Clock.class);
 		final Timer timer = context.mock(Timer.class);
-		final SecretKey k0 = context.mock(SecretKey.class, "k0");
-		final SecretKey k1 = context.mock(SecretKey.class, "k1");
-		final SecretKey k2 = context.mock(SecretKey.class, "k2");
 
 		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
 		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
@@ -122,9 +125,9 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -142,11 +145,11 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(EPOCH));
 			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0.clone()));
+			will(returnValue(secret0));
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			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);
@@ -155,10 +158,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -166,10 +166,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -177,10 +174,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// stop()
 			// The recogniser should derive the tags for period 0
 			oneOf(crypto).deriveTagKey(secret0, false);
@@ -189,10 +183,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -200,10 +191,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -211,17 +199,14 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// 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.clone());
+		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
 		keyManager.stop();
 
 		context.assertIsSatisfied();
@@ -235,9 +220,6 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		final Clock clock = context.mock(Clock.class);
 		final Timer timer = context.mock(Timer.class);
-		final SecretKey k0 = context.mock(SecretKey.class, "k0");
-		final SecretKey k1 = context.mock(SecretKey.class, "k1");
-		final SecretKey k2 = context.mock(SecretKey.class, "k2");
 
 		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
 		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
@@ -245,9 +227,9 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -265,11 +247,11 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(EPOCH));
 			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0.clone()));
+			will(returnValue(secret0));
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			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);
@@ -278,10 +260,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -289,10 +268,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -300,10 +276,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// getConnectionContext()
 			oneOf(db).incrementStreamCounter(contactId, transportId, 1);
 			will(returnValue(0L));
@@ -315,10 +288,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -326,10 +296,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -337,17 +304,14 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// 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.clone());
+		keyManager.endpointAdded(ep, MAX_LATENCY, initialSecret);
 		StreamContext ctx =
 				keyManager.getStreamContext(contactId, transportId);
 		assertNotNull(ctx);
@@ -369,9 +333,6 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		final Clock clock = context.mock(Clock.class);
 		final Timer timer = context.mock(Timer.class);
-		final SecretKey k0 = context.mock(SecretKey.class, "k0");
-		final SecretKey k1 = context.mock(SecretKey.class, "k1");
-		final SecretKey k2 = context.mock(SecretKey.class, "k2");
 
 		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
 		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
@@ -379,9 +340,9 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -399,11 +360,11 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(EPOCH));
 			oneOf(crypto).deriveNextSecret(initialSecret, 0);
-			will(returnValue(secret0.clone()));
+			will(returnValue(secret0));
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			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);
@@ -412,10 +373,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -423,10 +381,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -434,21 +389,15 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// acceptConnection()
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
-			oneOf(crypto).encodeTag(with(any(byte[].class)),
-					with(k2), with(16L));
+			oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
+					with(16L));
 			will(new EncodeTagAction());
-			oneOf(k2).getEncoded();
-			will(returnValue(key2));
 			oneOf(db).setReorderingWindow(contactId, transportId, 2, 1,
 					new byte[] {0, 1, 0, 0});
-			oneOf(k2).erase();
 			// stop()
 			// The recogniser should derive the tags for period 0
 			oneOf(crypto).deriveTagKey(secret0, false);
@@ -457,10 +406,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -468,10 +414,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the updated tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -479,17 +422,14 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// 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.clone());
+		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);
@@ -513,9 +453,6 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		final Clock clock = context.mock(Clock.class);
 		final Timer timer = context.mock(Timer.class);
-		final SecretKey k0 = context.mock(SecretKey.class, "k0");
-		final SecretKey k1 = context.mock(SecretKey.class, "k1");
-		final SecretKey k2 = context.mock(SecretKey.class, "k2");
 
 		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
 		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
@@ -523,9 +460,9 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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()
@@ -545,10 +482,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -556,10 +490,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -567,10 +498,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// Start the timer
 			oneOf(timer).scheduleAtFixedRate(with(keyManager),
 					with(any(long.class)), with(any(long.class)));
@@ -582,10 +510,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k0),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k0).getEncoded();
-				will(returnValue(key0));
 			}
-			oneOf(k0).erase();
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
 			will(returnValue(k1));
@@ -593,10 +518,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -604,10 +526,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// Remove the listener and stop the timer
 			oneOf(eventBus).removeListener(with(any(EventListener.class)));
 			oneOf(timer).cancel();
@@ -627,9 +546,6 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		final Clock clock = context.mock(Clock.class);
 		final Timer timer = context.mock(Timer.class);
-		final SecretKey k1 = context.mock(SecretKey.class, "k1");
-		final SecretKey k2 = context.mock(SecretKey.class, "k2");
-		final SecretKey k3 = context.mock(SecretKey.class, "k3");
 
 		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
 		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
@@ -637,11 +553,11 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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.clone());
+		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
 
 		context.checking(new Expectations() {{
 			// start()
@@ -653,14 +569,14 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 					MAX_LATENCY)));
 			// The current time is the start of period 2
 			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + ROTATION_PERIOD_LENGTH));
+			will(returnValue(EPOCH + ROTATION_PERIOD));
 			// The secret for period 3 should be derived and stored
 			oneOf(crypto).deriveNextSecret(secret0, 1);
-			will(returnValue(secret1.clone()));
+			will(returnValue(secret1));
 			oneOf(crypto).deriveNextSecret(secret1, 2);
-			will(returnValue(secret2.clone()));
+			will(returnValue(secret2));
 			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3.clone()));
+			will(returnValue(secret3));
 			oneOf(db).addSecrets(Arrays.asList(s3));
 			// The recogniser should derive the tags for period 1
 			oneOf(crypto).deriveTagKey(secret1, false);
@@ -669,10 +585,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -680,10 +593,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// The recogniser should derive the tags for period 3
 			oneOf(crypto).deriveTagKey(secret3, false);
 			will(returnValue(k3));
@@ -691,10 +601,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k3).getEncoded();
-				will(returnValue(key3));
 			}
-			oneOf(k3).erase();
 			// Start the timer
 			oneOf(timer).scheduleAtFixedRate(with(keyManager),
 					with(any(long.class)), with(any(long.class)));
@@ -706,10 +613,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k1),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k1).getEncoded();
-				will(returnValue(key1));
 			}
-			oneOf(k1).erase();
 			// The recogniser should derive the tags for period 2
 			oneOf(crypto).deriveTagKey(secret2, false);
 			will(returnValue(k2));
@@ -717,10 +621,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// The recogniser should remove the tags for period 3
 			oneOf(crypto).deriveTagKey(secret3, false);
 			will(returnValue(k3));
@@ -728,10 +629,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k3).getEncoded();
-				will(returnValue(key3));
 			}
-			oneOf(k3).erase();
 			// Remove the listener and stop the timer
 			oneOf(eventBus).removeListener(with(any(EventListener.class)));
 			oneOf(timer).cancel();
@@ -751,9 +649,6 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		final Clock clock = context.mock(Clock.class);
 		final Timer timer = context.mock(Timer.class);
-		final SecretKey k2 = context.mock(SecretKey.class, "k2");
-		final SecretKey k3 = context.mock(SecretKey.class, "k3");
-		final SecretKey k4 = context.mock(SecretKey.class, "k4");
 
 		final TagRecogniser tagRecogniser = new TagRecogniserImpl(crypto, db);
 		final KeyManagerImpl keyManager = new KeyManagerImpl(crypto, db,
@@ -761,12 +656,12 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 
 		// 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.clone());
-		final TemporarySecret s1 = new TemporarySecret(ep, 1, secret1.clone());
-		final TemporarySecret s2 = new TemporarySecret(ep, 2, secret2.clone());
+		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.clone());
-		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4.clone());
+		final TemporarySecret s3 = new TemporarySecret(ep, 3, secret3);
+		final TemporarySecret s4 = new TemporarySecret(ep, 4, secret4);
 
 		context.checking(new Expectations() {{
 			// start()
@@ -778,14 +673,14 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 					MAX_LATENCY)));
 			// The current time is the end of period 3
 			oneOf(clock).currentTimeMillis();
-			will(returnValue(EPOCH + 3 * ROTATION_PERIOD_LENGTH - 1));
+			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.clone()));
+			will(returnValue(secret2));
 			oneOf(crypto).deriveNextSecret(secret2, 3);
-			will(returnValue(secret3.clone()));
+			will(returnValue(secret3));
 			oneOf(crypto).deriveNextSecret(secret3, 4);
-			will(returnValue(secret4.clone()));
+			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
@@ -795,10 +690,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// The recogniser should derive the tags for period 3
 			oneOf(crypto).deriveTagKey(secret3, false);
 			will(returnValue(k3));
@@ -806,10 +698,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k3).getEncoded();
-				will(returnValue(key3));
 			}
-			oneOf(k3).erase();
 			// The recogniser should derive the tags for period 4
 			oneOf(crypto).deriveTagKey(secret4, false);
 			will(returnValue(k4));
@@ -817,10 +706,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k4),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k4).getEncoded();
-				will(returnValue(key4));
 			}
-			oneOf(k4).erase();
 			// Start the timer
 			oneOf(timer).scheduleAtFixedRate(with(keyManager),
 					with(any(long.class)), with(any(long.class)));
@@ -832,10 +718,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k2),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k2).getEncoded();
-				will(returnValue(key2));
 			}
-			oneOf(k2).erase();
 			// The recogniser should remove the tags for period 3
 			oneOf(crypto).deriveTagKey(secret3, false);
 			will(returnValue(k3));
@@ -843,10 +726,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k3),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k3).getEncoded();
-				will(returnValue(key3));
 			}
-			oneOf(k3).erase();
 			// The recogniser should derive the tags for period 4
 			oneOf(crypto).deriveTagKey(secret4, false);
 			will(returnValue(k4));
@@ -854,10 +734,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 				oneOf(crypto).encodeTag(with(any(byte[].class)), with(k4),
 						with((long) i));
 				will(new EncodeTagAction());
-				oneOf(k4).getEncoded();
-				will(returnValue(key4));
 			}
-			oneOf(k4).erase();
 			// Remove the listener and stop the timer
 			oneOf(eventBus).removeListener(with(any(EventListener.class)));
 			oneOf(timer).cancel();
@@ -885,7 +762,7 @@ public class KeyRotationIntegrationTest extends BriarTestCase {
 			byte[] tag = (byte[]) invocation.getParameter(0);
 			SecretKey key = (SecretKey) invocation.getParameter(1);
 			long streamNumber = (Long) invocation.getParameter(2);
-			encodeTag(tag, key.getEncoded(), streamNumber);
+			encodeTag(tag, key.getBytes(), streamNumber);
 			return null;
 		}
 	}
diff --git a/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java b/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
deleted file mode 100644
index d9b7340335a1afb4fe960525e593d5104f44ff6e..0000000000000000000000000000000000000000
--- a/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
+++ /dev/null
@@ -1,104 +0,0 @@
-package org.briarproject.transport;
-
-import static org.briarproject.api.transport.TransportConstants.AAD_LENGTH;
-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.TAG_LENGTH;
-
-import java.io.ByteArrayOutputStream;
-import java.util.Random;
-
-import org.briarproject.BriarTestCase;
-import org.briarproject.TestLifecycleModule;
-import org.briarproject.TestSystemModule;
-import org.briarproject.api.crypto.AuthenticatedCipher;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.crypto.CryptoModule;
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class OutgoingEncryptionLayerTest extends BriarTestCase {
-
-	// FIXME: This is an integration test, not a unit test
-
-	private static final int FRAME_LENGTH = 1024;
-
-	private final CryptoComponent crypto;
-	private final AuthenticatedCipher frameCipher;
-
-	public OutgoingEncryptionLayerTest() {
-		Injector i = Guice.createInjector(new CryptoModule(),
-				new TestLifecycleModule(), new TestSystemModule());
-		crypto = i.getInstance(CryptoComponent.class);
-		frameCipher = crypto.getFrameCipher();
-	}
-
-	@Test
-	public void testEncryptionWithoutTag() throws Exception {
-		int payloadLength = 123;
-		byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH];
-		byte[] plaintext = new byte[FRAME_LENGTH - MAC_LENGTH];
-		byte[] ciphertext = new byte[FRAME_LENGTH];
-		SecretKey frameKey = crypto.generateSecretKey();
-		// Calculate the expected ciphertext
-		FrameEncoder.encodeIv(iv, 0);
-		FrameEncoder.encodeAad(aad, 0, plaintext.length);
-		frameCipher.init(true, frameKey, iv, aad);
-		FrameEncoder.encodeHeader(plaintext, false, payloadLength);
-		frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0);
-		// Check that the actual ciphertext matches what's expected
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				frameCipher, frameKey, FRAME_LENGTH, null);
-		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false);
-		byte[] actual = out.toByteArray();
-		assertEquals(FRAME_LENGTH, actual.length);
-		for(int i = 0; i < FRAME_LENGTH; i++)
-			assertEquals(ciphertext[i], actual[i]);
-	}
-
-	@Test
-	public void testEncryptionWithTag() throws Exception {
-		byte[] tag = new byte[TAG_LENGTH];
-		new Random().nextBytes(tag);
-		int payloadLength = 123;
-		byte[] iv = new byte[IV_LENGTH], aad = new byte[AAD_LENGTH];
-		byte[] plaintext = new byte[FRAME_LENGTH - MAC_LENGTH];
-		byte[] ciphertext = new byte[FRAME_LENGTH];
-		SecretKey frameKey = crypto.generateSecretKey();
-		// Calculate the expected ciphertext
-		FrameEncoder.encodeIv(iv, 0);
-		FrameEncoder.encodeAad(aad, 0, plaintext.length);
-		frameCipher.init(true, frameKey, iv, aad);
-		FrameEncoder.encodeHeader(plaintext, false, payloadLength);
-		frameCipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0);
-		// Check that the actual tag and ciphertext match what's expected
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				frameCipher, frameKey, FRAME_LENGTH, tag);
-		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false);
-		byte[] actual = out.toByteArray();
-		assertEquals(TAG_LENGTH + FRAME_LENGTH, actual.length);
-		for(int i = 0; i < TAG_LENGTH; i++) assertEquals(tag[i], actual[i]);
-		for(int i = 0; i < FRAME_LENGTH; i++)
-			assertEquals(ciphertext[i], actual[TAG_LENGTH + i]);
-	}
-
-	@Test
-	public void testCloseConnectionWithoutWriting() throws Exception {
-		byte[] tag = new byte[TAG_LENGTH];
-		new Random().nextBytes(tag);
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		// Initiator's constructor
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
-				frameCipher, crypto.generateSecretKey(), FRAME_LENGTH, tag);
-		// Write an empty final frame without having written any other frames
-		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], 0, true);
-		// The tag and the empty frame should be written to the output stream
-		assertEquals(TAG_LENGTH + HEADER_LENGTH + MAC_LENGTH, out.size());
-	}
-}
diff --git a/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java b/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java
index 67337a50ca4fca64e76f3ffa6b82fd54450e84ce..7e9e97bb2f92c773e0e2c7b36e56e058d32396cc 100644
--- a/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java
+++ b/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java
@@ -1,34 +1,30 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.api.crypto.StreamDecrypter;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
 public class StreamReaderImplTest extends BriarTestCase {
 
-	private static final int FRAME_LENGTH = 1024;
-	private static final int MAX_PAYLOAD_LENGTH =
-			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
-
 	@Test
 	public void testEmptyFramesAreSkipped() throws Exception {
 		Mockery context = new Mockery();
-		final FrameReader reader = context.mock(FrameReader.class);
+		final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
 		context.checking(new Expectations() {{
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(0)); // Empty frame
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(2)); // Non-empty frame with two payload bytes
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(0)); // Empty frame
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(-1)); // No more frames
 		}});
-		StreamReaderImpl r = new StreamReaderImpl(reader, FRAME_LENGTH);
+		StreamReaderImpl r = new StreamReaderImpl(decrypter);
 		assertEquals(0, r.read()); // Skip the first empty frame, read a byte
 		assertEquals(0, r.read()); // Read another byte
 		assertEquals(-1, r.read()); // Skip the second empty frame, reach EOF
@@ -40,18 +36,18 @@ public class StreamReaderImplTest extends BriarTestCase {
 	@Test
 	public void testEmptyFramesAreSkippedWithBuffer() throws Exception {
 		Mockery context = new Mockery();
-		final FrameReader reader = context.mock(FrameReader.class);
+		final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
 		context.checking(new Expectations() {{
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(0)); // Empty frame
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(2)); // Non-empty frame with two payload bytes
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(0)); // Empty frame
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(-1)); // No more frames
 		}});
-		StreamReaderImpl r = new StreamReaderImpl(reader, FRAME_LENGTH);
+		StreamReaderImpl r = new StreamReaderImpl(decrypter);
 		byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
 		// Skip the first empty frame, read the two payload bytes
 		assertEquals(2, r.read(buf));
@@ -66,14 +62,14 @@ public class StreamReaderImplTest extends BriarTestCase {
 	@Test
 	public void testMultipleReadsPerFrame() throws Exception {
 		Mockery context = new Mockery();
-		final FrameReader reader = context.mock(FrameReader.class);
+		final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
 		context.checking(new Expectations() {{
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(-1)); // No more frames
 		}});
-		StreamReaderImpl r = new StreamReaderImpl(reader, FRAME_LENGTH);
+		StreamReaderImpl r = new StreamReaderImpl(decrypter);
 		byte[] buf = new byte[MAX_PAYLOAD_LENGTH / 2];
 		// Read the first half of the payload
 		assertEquals(MAX_PAYLOAD_LENGTH / 2, r.read(buf));
@@ -88,14 +84,14 @@ public class StreamReaderImplTest extends BriarTestCase {
 	@Test
 	public void testMultipleReadsPerFrameWithOffsets() throws Exception {
 		Mockery context = new Mockery();
-		final FrameReader reader = context.mock(FrameReader.class);
+		final StreamDecrypter decrypter = context.mock(StreamDecrypter.class);
 		context.checking(new Expectations() {{
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(MAX_PAYLOAD_LENGTH)); // Nice long frame
-			oneOf(reader).readFrame(with(any(byte[].class)));
+			oneOf(decrypter).readFrame(with(any(byte[].class)));
 			will(returnValue(-1)); // No more frames
 		}});
-		StreamReaderImpl r = new StreamReaderImpl(reader, FRAME_LENGTH);
+		StreamReaderImpl r = new StreamReaderImpl(decrypter);
 		byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
 		// Read the first half of the payload
 		assertEquals(MAX_PAYLOAD_LENGTH / 2, r.read(buf, MAX_PAYLOAD_LENGTH / 2,
diff --git a/briar-tests/src/org/briarproject/transport/StreamWriterImplTest.java b/briar-tests/src/org/briarproject/transport/StreamWriterImplTest.java
index e55e9092d031b6ad13e1b8f8dde0e98bba7218d9..37c979f2d1ed62f9da92212e9b43ce06f4976102 100644
--- a/briar-tests/src/org/briarproject/transport/StreamWriterImplTest.java
+++ b/briar-tests/src/org/briarproject/transport/StreamWriterImplTest.java
@@ -1,43 +1,43 @@
 package org.briarproject.transport;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.api.crypto.StreamEncrypter;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
 
 public class StreamWriterImplTest extends BriarTestCase {
 
-	private static final int FRAME_LENGTH = 1024;
-	private static final int MAX_PAYLOAD_LENGTH =
-			FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
-
 	@Test
 	public void testCloseWithoutWritingWritesFinalFrame() throws Exception {
 		Mockery context = new Mockery();
-		final FrameWriter writer = context.mock(FrameWriter.class);
+		final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
 		context.checking(new Expectations() {{
 			// Write an empty final frame
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(0),
-					with(true));
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
+					with(0), with(true));
 			// Flush the stream
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
-		StreamWriterImpl w = new StreamWriterImpl(writer, FRAME_LENGTH);
+		StreamWriterImpl w = new StreamWriterImpl(encrypter);
 		w.close();
 		context.assertIsSatisfied();
 	}
 
 	@Test
-	public void testFlushWithoutBufferedDataOnlyFlushes() throws Exception {
+	public void testFlushWithoutBufferedDataWritesFrameAndFlushes()
+			throws Exception {
 		Mockery context = new Mockery();
-		final FrameWriter writer = context.mock(FrameWriter.class);
-		StreamWriterImpl w = new StreamWriterImpl(writer, FRAME_LENGTH);
+		final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
+		StreamWriterImpl w = new StreamWriterImpl(encrypter);
 		context.checking(new Expectations() {{
+			// Write a non-final frame with an empty payload
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
+					with(0), with(false));
 			// Flush the stream
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.flush();
 		context.assertIsSatisfied();
@@ -45,9 +45,9 @@ public class StreamWriterImplTest extends BriarTestCase {
 		// Clean up
 		context.checking(new Expectations() {{
 			// Closing the writer writes a final frame and flushes again
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(0),
-					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
+					with(0), with(true));
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -57,14 +57,14 @@ public class StreamWriterImplTest extends BriarTestCase {
 	public void testFlushWithBufferedDataWritesFrameAndFlushes()
 			throws Exception {
 		Mockery context = new Mockery();
-		final FrameWriter writer = context.mock(FrameWriter.class);
-		StreamWriterImpl w = new StreamWriterImpl(writer, FRAME_LENGTH);
+		final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
+		StreamWriterImpl w = new StreamWriterImpl(encrypter);
 		context.checking(new Expectations() {{
 			// Write a non-final frame with one payload byte
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(1),
-					with(false));
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(1),
+					with(0), with(false));
 			// Flush the stream
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.write(0);
 		w.flush();
@@ -73,9 +73,9 @@ public class StreamWriterImplTest extends BriarTestCase {
 		// Clean up
 		context.checking(new Expectations() {{
 			// Closing the writer writes a final frame and flushes again
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(0),
-					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
+					with(0), with(true));
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -84,24 +84,22 @@ public class StreamWriterImplTest extends BriarTestCase {
 	@Test
 	public void testSingleByteWritesWriteFullFrame() throws Exception {
 		Mockery context = new Mockery();
-		final FrameWriter writer = context.mock(FrameWriter.class);
-		StreamWriterImpl w = new StreamWriterImpl(writer, FRAME_LENGTH);
+		final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
+		StreamWriterImpl w = new StreamWriterImpl(encrypter);
 		context.checking(new Expectations() {{
 			// Write a full non-final frame
-			oneOf(writer).writeFrame(with(any(byte[].class)),
-					with(MAX_PAYLOAD_LENGTH), with(false));
+			oneOf(encrypter).writeFrame(with(any(byte[].class)),
+					with(MAX_PAYLOAD_LENGTH), with(0), with(false));
 		}});
-		for(int i = 0; i < MAX_PAYLOAD_LENGTH; i++) {
-			w.write(0);
-		}
+		for(int i = 0; i < MAX_PAYLOAD_LENGTH; i++) w.write(0);
 		context.assertIsSatisfied();
 
 		// Clean up
 		context.checking(new Expectations() {{
 			// Closing the writer writes a final frame and flushes again
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(0),
-					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
+					with(0), with(true));
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -110,12 +108,12 @@ public class StreamWriterImplTest extends BriarTestCase {
 	@Test
 	public void testMultiByteWritesWriteFullFrames() throws Exception {
 		Mockery context = new Mockery();
-		final FrameWriter writer = context.mock(FrameWriter.class);
-		StreamWriterImpl w = new StreamWriterImpl(writer, FRAME_LENGTH);
+		final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
+		StreamWriterImpl w = new StreamWriterImpl(encrypter);
 		context.checking(new Expectations() {{
 			// Write two full non-final frames
-			exactly(2).of(writer).writeFrame(with(any(byte[].class)),
-					with(MAX_PAYLOAD_LENGTH), with(false));
+			exactly(2).of(encrypter).writeFrame(with(any(byte[].class)),
+					with(MAX_PAYLOAD_LENGTH), with(0), with(false));
 		}});
 		// Sanity check
 		assertEquals(0, MAX_PAYLOAD_LENGTH % 2);
@@ -130,9 +128,9 @@ public class StreamWriterImplTest extends BriarTestCase {
 		// Clean up
 		context.checking(new Expectations() {{
 			// Closing the writer writes a final frame and flushes again
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(0),
-					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
+					with(0), with(true));
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -141,17 +139,17 @@ public class StreamWriterImplTest extends BriarTestCase {
 	@Test
 	public void testLargeMultiByteWriteWritesFullFrames() throws Exception {
 		Mockery context = new Mockery();
-		final FrameWriter writer = context.mock(FrameWriter.class);
-		StreamWriterImpl w = new StreamWriterImpl(writer, FRAME_LENGTH);
+		final StreamEncrypter encrypter = context.mock(StreamEncrypter.class);
+		StreamWriterImpl w = new StreamWriterImpl(encrypter);
 		context.checking(new Expectations() {{
 			// Write two full non-final frames
-			exactly(2).of(writer).writeFrame(with(any(byte[].class)),
-					with(MAX_PAYLOAD_LENGTH), with(false));
+			exactly(2).of(encrypter).writeFrame(with(any(byte[].class)),
+					with(MAX_PAYLOAD_LENGTH), with(0), with(false));
 			// Write a final frame with a one-byte payload
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(1),
-					with(true));
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(1),
+					with(0), with(true));
 			// Flush the stream
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		// Write two full payloads using one large multi-byte write
 		byte[] b = new byte[MAX_PAYLOAD_LENGTH * 2 + 1];
diff --git a/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java b/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..b901678a6beca92fbad25d2065abda5ac8710fec
--- /dev/null
+++ b/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java
@@ -0,0 +1,45 @@
+package org.briarproject.transport;
+
+import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.crypto.StreamDecrypter;
+import org.briarproject.util.ByteUtils;
+
+class TestStreamDecrypter implements StreamDecrypter {
+
+	private final InputStream in;
+	private final byte[] frame;
+
+	TestStreamDecrypter(InputStream in) {
+		this.in = in;
+		frame = new byte[MAX_FRAME_LENGTH];
+	}
+
+	public int readFrame(byte[] payload) throws IOException {
+		int offset = 0;
+		while(offset < HEADER_LENGTH) {
+			int read = in.read(frame, offset, HEADER_LENGTH - offset);
+			if(read == -1) throw new EOFException();
+			offset += read;
+		}
+		boolean finalFrame = (frame[0] & 0x80) == 0x80;
+		int payloadLength = ByteUtils.readUint16(frame, 0) & 0x7FFF;
+		while(offset < frame.length) {
+			int read = in.read(frame, offset, frame.length - offset);
+			if(read == -1) break;
+			offset += read;
+		}
+		if(!finalFrame && offset < frame.length) throw new EOFException();
+		if(offset < HEADER_LENGTH + payloadLength + MAC_LENGTH)
+			throw new FormatException();
+		System.arraycopy(frame, HEADER_LENGTH, payload, 0, payloadLength);
+		return payloadLength;
+	}
+}
diff --git a/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java b/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..d78b4aa3c7570c4ce4643bc7933cac84eb0ce817
--- /dev/null
+++ b/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java
@@ -0,0 +1,45 @@
+package org.briarproject.transport;
+
+import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import org.briarproject.api.crypto.StreamEncrypter;
+import org.briarproject.util.ByteUtils;
+
+class TestStreamEncrypter implements StreamEncrypter {
+
+	private final OutputStream out;
+	private final byte[] tag, frame;
+
+	private boolean writeTag = true;
+
+	TestStreamEncrypter(OutputStream out, byte[] tag) {
+		this.out = out;
+		this.tag = tag;
+		frame = new byte[MAX_FRAME_LENGTH];
+	}
+
+	public void writeFrame(byte[] payload, int payloadLength,
+			int paddingLength, boolean finalFrame) throws IOException {
+		if(writeTag) {
+			out.write(tag);
+			writeTag = false;
+		}
+		ByteUtils.writeUint16(payloadLength, frame, 0);
+		if(finalFrame) frame[0] |= 0x80;
+		System.arraycopy(payload, 0, frame, HEADER_LENGTH, payloadLength);
+		for(int i = HEADER_LENGTH + payloadLength; i < frame.length; i++)
+			frame[i] = 0;
+		if(finalFrame)
+			out.write(frame, 0, HEADER_LENGTH + payloadLength + MAC_LENGTH);
+		else out.write(frame, 0, frame.length);
+	}
+
+	public void flush() throws IOException {
+		out.flush();
+	}
+}
diff --git a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
index 19153db75a48546db0f4f12a6c827568ffd7511c..af006350ae623ecc622a2c582f5b896389727be8 100644
--- a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
@@ -1,5 +1,6 @@
 package org.briarproject.transport;
 
+import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
 
@@ -11,48 +12,16 @@ import java.io.OutputStream;
 import java.util.Random;
 
 import org.briarproject.BriarTestCase;
-import org.briarproject.TestLifecycleModule;
-import org.briarproject.TestSystemModule;
-import org.briarproject.api.crypto.AuthenticatedCipher;
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.transport.StreamWriterFactory;
-import org.briarproject.crypto.CryptoModule;
+import org.briarproject.api.crypto.StreamDecrypter;
+import org.briarproject.api.crypto.StreamEncrypter;
 import org.junit.Test;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-import com.google.inject.Module;
-
 public class TransportIntegrationTest extends BriarTestCase {
 
-	private final int FRAME_LENGTH = 2048;
-
-	private final CryptoComponent crypto;
-	private final AuthenticatedCipher frameCipher;
 	private final Random random;
-	private final byte[] secret;
-	private final SecretKey tagKey, frameKey;
 
 	public TransportIntegrationTest() {
-		Module testModule = new AbstractModule() {
-			@Override
-			public void configure() {
-				bind(StreamWriterFactory.class).to(
-						StreamWriterFactoryImpl.class);
-			}
-		};
-		Injector i = Guice.createInjector(testModule, new CryptoModule(),
-				new TestLifecycleModule(), new TestSystemModule());
-		crypto = i.getInstance(CryptoComponent.class);
-		frameCipher = crypto.getFrameCipher();
 		random = new Random();
-		// Since we're sending frames to ourselves, we only need outgoing keys
-		secret = new byte[32];
-		random.nextBytes(secret);
-		tagKey = crypto.deriveTagKey(secret, true);
-		frameKey = crypto.deriveFrameKey(secret, 0, true);
 	}
 
 	@Test
@@ -66,46 +35,38 @@ public class TransportIntegrationTest extends BriarTestCase {
 	}
 
 	private void testWriteAndRead(boolean initiator) throws Exception {
-		// Encode the tag
+		// Generate a random tag
 		byte[] tag = new byte[TAG_LENGTH];
-		crypto.encodeTag(tag, tagKey, 0);
-		// Generate two random frames
-		byte[] frame = new byte[1234];
-		random.nextBytes(frame);
-		byte[] frame1 = new byte[321];
-		random.nextBytes(frame1);
-		// Copy the frame key - the copy will be erased
-		SecretKey frameCopy = frameKey.copy();
+		random.nextBytes(tag);
+		// Generate two frames with random payloads
+		byte[] payload1 = new byte[123];
+		random.nextBytes(payload1);
+		byte[] payload2 = new byte[321];
+		random.nextBytes(payload2);
 		// Write the tag and the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		FrameWriter frameWriter = new OutgoingEncryptionLayer(out,
-				frameCipher, frameCopy, FRAME_LENGTH, tag);
-		StreamWriterImpl streamWriter = new StreamWriterImpl(frameWriter,
-				FRAME_LENGTH);
-		OutputStream out1 = streamWriter.getOutputStream();
-		out1.write(frame);
-		out1.flush();
-		out1.write(frame1);
-		out1.flush();
+		StreamEncrypter encrypter = new TestStreamEncrypter(out, tag);
+		OutputStream streamWriter = new StreamWriterImpl(encrypter);
+		streamWriter.write(payload1);
+		streamWriter.flush();
+		streamWriter.write(payload2);
+		streamWriter.flush();
 		byte[] output = out.toByteArray();
-		assertEquals(TAG_LENGTH + FRAME_LENGTH * 2, output.length);
+		assertEquals(TAG_LENGTH + MAX_FRAME_LENGTH * 2, output.length);
 		// Read the tag back
 		ByteArrayInputStream in = new ByteArrayInputStream(output);
 		byte[] recoveredTag = new byte[tag.length];
 		read(in, recoveredTag);
 		assertArrayEquals(tag, recoveredTag);
 		// Read the frames back
-		FrameReader frameReader = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		StreamReaderImpl streamReader = new StreamReaderImpl(frameReader,
-				FRAME_LENGTH);
-		InputStream in1 = streamReader.getInputStream();
-		byte[] recoveredFrame = new byte[frame.length];
-		read(in1, recoveredFrame);
-		assertArrayEquals(frame, recoveredFrame);
-		byte[] recoveredFrame1 = new byte[frame1.length];
-		read(in1, recoveredFrame1);
-		assertArrayEquals(frame1, recoveredFrame1);
+		StreamDecrypter decrypter = new TestStreamDecrypter(in);
+		InputStream streamReader = new StreamReaderImpl(decrypter);
+		byte[] recoveredPayload1 = new byte[payload1.length];
+		read(streamReader, recoveredPayload1);
+		assertArrayEquals(payload1, recoveredPayload1);
+		byte[] recoveredPayload2 = new byte[payload2.length];
+		read(streamReader, recoveredPayload2);
+		assertArrayEquals(payload2, recoveredPayload2);
 		streamWriter.close();
 		streamReader.close();
 	}
diff --git a/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java b/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java
index ba1e84d48a13f583801e135f55255faf8df808a1..340745489de7a8c5a4d2babb0b52da75cc933bfe 100644
--- a/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java
+++ b/briar-tests/src/org/briarproject/transport/TransportTagRecogniserTest.java
@@ -25,6 +25,7 @@ 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() {
@@ -33,7 +34,6 @@ public class TransportTagRecogniserTest extends BriarTestCase {
 		final byte[] secret = new byte[32];
 		new Random().nextBytes(secret);
 		final boolean alice = false;
-		final SecretKey tagKey = context.mock(SecretKey.class);
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		context.checking(new Expectations() {{
 			// Add secret
@@ -44,7 +44,6 @@ public class TransportTagRecogniserTest extends BriarTestCase {
 						with((long) i));
 				will(new EncodeTagAction());
 			}
-			oneOf(tagKey).erase();
 			// Remove secret
 			oneOf(crypto).deriveTagKey(secret, !alice);
 			will(returnValue(tagKey));
@@ -53,7 +52,6 @@ public class TransportTagRecogniserTest extends BriarTestCase {
 						with((long) i));
 				will(new EncodeTagAction());
 			}
-			oneOf(tagKey).erase();
 		}});
 		TemporarySecret s = new TemporarySecret(contactId, transportId, 123,
 				alice, 0, secret, 0, 0, new byte[4]);
@@ -71,7 +69,6 @@ public class TransportTagRecogniserTest extends BriarTestCase {
 		final byte[] secret = new byte[32];
 		new Random().nextBytes(secret);
 		final boolean alice = false;
-		final SecretKey tagKey = context.mock(SecretKey.class);
 		final DatabaseComponent db = context.mock(DatabaseComponent.class);
 		context.checking(new Expectations() {{
 			// Add secret
@@ -82,7 +79,6 @@ public class TransportTagRecogniserTest extends BriarTestCase {
 						with((long) i));
 				will(new EncodeTagAction());
 			}
-			oneOf(tagKey).erase();
 			// Recognise tag 0
 			oneOf(crypto).deriveTagKey(secret, !alice);
 			will(returnValue(tagKey));
@@ -93,7 +89,6 @@ public class TransportTagRecogniserTest extends BriarTestCase {
 			// The updated window should be stored
 			oneOf(db).setReorderingWindow(contactId, transportId, 0, 1,
 					new byte[] {0, 1, 0, 0});
-			oneOf(tagKey).erase();
 			// Recognise tag again - no expectations
 		}});
 		TemporarySecret s = new TemporarySecret(contactId, transportId, 123,