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..46948f2a26625dc9b41fe434bd1e80cdebd8b78f
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/StreamDecrypterFactory.java
@@ -0,0 +1,18 @@
+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, int maxFrameLength,
+			StreamContext ctx);
+
+	/**
+	 * Creates a {@link StreamDecrypter} for decrypting an invitation stream.
+	 */
+	StreamDecrypter createInvitationStreamDecrypter(InputStream in,
+			int maxFrameLength, 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..d48deff3a9c2a8af6cea674542a43a4a1d2d5d99
--- /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, 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..95912937e4e57a57ad7f39d64ec42bad0a7ba5ca
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/crypto/StreamEncrypterFactory.java
@@ -0,0 +1,18 @@
+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,
+			int maxFrameLength, StreamContext ctx);
+
+	/**
+	 * Creates a {@link StreamEncrypter} for encrypting an invitation stream.
+	 */
+	StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
+			int maxFrameLength, byte[] secret, boolean alice);
+}
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..3bf4cf049041e2fc8d1cb25298c6377f40c14477 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamReaderFactory.java
@@ -4,11 +4,17 @@ import java.io.InputStream;
 
 public interface StreamReaderFactory {
 
-	/** Creates a {@link StreamReader} for a transport connection. */
-	StreamReader createStreamReader(InputStream in, int maxFrameLength,
+	/**
+	 * Creates an {@link java.io.InputStream InputStream} for reading from a
+	 * transport stream.
+	 */
+	InputStream createStreamReader(InputStream in, int maxFrameLength,
 			StreamContext ctx);
 
-	/** Creates a {@link StreamReader} for an invitation connection. */
-	StreamReader createInvitationStreamReader(InputStream in,
+	/**
+	 * Creates an {@link java.io.InputStream InputStream} for reading from an
+	 * invitation stream.
+	 */
+	InputStream createInvitationStreamReader(InputStream in,
 			int maxFrameLength, 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..04a924cc674e6f9ddc1ae7b48289d117608f670a 100644
--- a/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
+++ b/briar-api/src/org/briarproject/api/transport/StreamWriterFactory.java
@@ -4,11 +4,17 @@ import java.io.OutputStream;
 
 public interface StreamWriterFactory {
 
-	/** Creates a {@link StreamWriter} for a transport connection. */
-	StreamWriter createStreamWriter(OutputStream out, int maxFrameLength,
+	/**
+	 * Creates an {@link java.io.OutputStream OutputStream} for writing to a
+	 * transport stream
+	 */
+	OutputStream createStreamWriter(OutputStream out, int maxFrameLength,
 			StreamContext ctx);
 
-	/** Creates a {@link StreamWriter} for an invitation connection. */
-	StreamWriter createInvitationStreamWriter(OutputStream out,
+	/**
+	 * Creates an {@link java.io.OutputStream OutputStream} for writing to an
+	 * invitation stream.
+	 */
+	OutputStream createInvitationStreamWriter(OutputStream out,
 			int maxFrameLength, byte[] secret, boolean alice);
 }
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 98%
rename from briar-core/src/org/briarproject/transport/FrameEncoder.java
rename to briar-core/src/org/briarproject/crypto/FrameEncoder.java
index 684b8754248391d6d8c01dca3b89b12ca55cf252..5a30593db4bddac8d8fb3863b087ed00621f63d3 100644
--- a/briar-core/src/org/briarproject/transport/FrameEncoder.java
+++ b/briar-core/src/org/briarproject/crypto/FrameEncoder.java
@@ -1,4 +1,4 @@
-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;
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..8f04e686ae86d21b492bfc77b7df94d52eeab1db
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterFactoryImpl.java
@@ -0,0 +1,42 @@
+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,
+			int maxFrameLength, StreamContext ctx) {
+		byte[] secret = ctx.getSecret();
+		long streamNumber = ctx.getStreamNumber();
+		boolean alice = !ctx.getAlice();
+		// Derive the frame key
+		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
+		// Create the decrypter
+		return new StreamDecrypterImpl(in, crypto.getFrameCipher(), frameKey,
+				maxFrameLength);
+	}
+
+	public StreamDecrypter createInvitationStreamDecrypter(InputStream in,
+			int maxFrameLength, 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,
+				maxFrameLength);
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/IncomingEncryptionLayer.java b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
similarity index 78%
rename from briar-core/src/org/briarproject/transport/IncomingEncryptionLayer.java
rename to briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
index e5e4381d8cfcccf4af9444bc798026687995edf5..ff29697dfd469ee0e829ef0b24f33f294368454b 100644
--- a/briar-core/src/org/briarproject/transport/IncomingEncryptionLayer.java
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
@@ -1,4 +1,4 @@
-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;
@@ -13,19 +13,20 @@ 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 IncomingEncryptionLayer implements FrameReader {
+class StreamDecrypterImpl implements StreamDecrypter {
 
 	private final InputStream in;
 	private final AuthenticatedCipher frameCipher;
 	private final SecretKey frameKey;
-	private final byte[] iv, aad, ciphertext;
+	private final byte[] iv, aad, plaintext, ciphertext;
 	private final int frameLength;
 
 	private long frameNumber;
 	private boolean finalFrame;
 
-	IncomingEncryptionLayer(InputStream in, AuthenticatedCipher frameCipher,
+	StreamDecrypterImpl(InputStream in, AuthenticatedCipher frameCipher,
 			SecretKey frameKey, int frameLength) {
 		this.in = in;
 		this.frameCipher = frameCipher;
@@ -33,12 +34,13 @@ class IncomingEncryptionLayer implements FrameReader {
 		this.frameLength = frameLength;
 		iv = new byte[IV_LENGTH];
 		aad = new byte[AAD_LENGTH];
+		plaintext = new byte[frameLength - MAC_LENGTH];
 		ciphertext = new byte[frameLength];
 		frameNumber = 0;
 		finalFrame = false;
 	}
 
-	public int readFrame(byte[] frame) throws IOException {
+	public int readFrame(byte[] payload) throws IOException {
 		if(finalFrame) return -1;
 		// Read the frame
 		int ciphertextLength = 0;
@@ -61,23 +63,25 @@ class IncomingEncryptionLayer implements FrameReader {
 		try {
 			frameCipher.init(false, frameKey, iv, aad);
 			int decrypted = frameCipher.doFinal(ciphertext, 0, ciphertextLength,
-					frame, 0);
+					plaintext, 0);
 			if(decrypted != plaintextLength) throw new RuntimeException();
 		} catch(GeneralSecurityException e) {
 			throw new FormatException();
 		}
 		// Decode and validate the header
-		finalFrame = FrameEncoder.isFinalFrame(frame);
+		finalFrame = FrameEncoder.isFinalFrame(plaintext);
 		if(!finalFrame && ciphertextLength < frameLength)
 			throw new FormatException();
-		int payloadLength = FrameEncoder.getPayloadLength(frame);
+		int payloadLength = FrameEncoder.getPayloadLength(plaintext);
 		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();
+			if(plaintext[i] != 0) throw new FormatException();
 		}
 		frameNumber++;
+		// Copy the payload
+		System.arraycopy(plaintext, HEADER_LENGTH, payload, 0, payloadLength);
 		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..979ebb17c859e2251330c3672ab7b3e459385c33
--- /dev/null
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
@@ -0,0 +1,49 @@
+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,
+			int maxFrameLength, 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);
+		tagKey.erase();
+		// Derive the frame key
+		SecretKey frameKey = crypto.deriveFrameKey(secret, streamNumber, alice);
+		// Create the encrypter
+		return new StreamEncrypterImpl(out, crypto.getFrameCipher(), frameKey,
+				maxFrameLength, tag);
+	}
+
+	public StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
+			int maxFrameLength, 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,
+				maxFrameLength, null);
+	}
+}
diff --git a/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
similarity index 79%
rename from briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
rename to briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
index 1bb90c1e8cf51d321fe8318a557052eb7d045d3a..d3e2e43ce19af8d9742270d0d6cd52a6abb33988 100644
--- a/briar-core/src/org/briarproject/transport/OutgoingEncryptionLayer.java
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
@@ -1,4 +1,4 @@
-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;
@@ -12,19 +12,20 @@ import java.security.GeneralSecurityException;
 
 import org.briarproject.api.crypto.AuthenticatedCipher;
 import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.api.crypto.StreamEncrypter;
 
-class OutgoingEncryptionLayer implements FrameWriter {
+class StreamEncrypterImpl implements StreamEncrypter {
 
 	private final OutputStream out;
 	private final AuthenticatedCipher frameCipher;
 	private final SecretKey frameKey;
-	private final byte[] tag, iv, aad, ciphertext;
+	private final byte[] tag, iv, aad, plaintext, ciphertext;
 	private final int frameLength;
 
 	private long frameNumber;
 	private boolean writeTag;
 
-	OutgoingEncryptionLayer(OutputStream out, AuthenticatedCipher frameCipher,
+	StreamEncrypterImpl(OutputStream out, AuthenticatedCipher frameCipher,
 			SecretKey frameKey, int frameLength, byte[] tag) {
 		this.out = out;
 		this.frameCipher = frameCipher;
@@ -33,13 +34,14 @@ class OutgoingEncryptionLayer implements FrameWriter {
 		this.tag = tag;
 		iv = new byte[IV_LENGTH];
 		aad = new byte[AAD_LENGTH];
+		plaintext = new byte[frameLength - MAC_LENGTH];
 		ciphertext = new byte[frameLength];
 		frameNumber = 0;
 		writeTag = (tag != null);
 	}
 
-	public void writeFrame(byte[] frame, int payloadLength, boolean finalFrame)
-			throws IOException {
+	public void writeFrame(byte[] payload, int payloadLength,
+			boolean finalFrame) throws IOException {
 		if(frameNumber > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
 		// Write the tag if required
 		if(writeTag) {
@@ -51,8 +53,6 @@ class OutgoingEncryptionLayer implements FrameWriter {
 			}
 			writeTag = false;
 		}
-		// Encode the header
-		FrameEncoder.encodeHeader(frame, finalFrame, payloadLength);
 		// Don't pad the final frame
 		int plaintextLength, ciphertextLength;
 		if(finalFrame) {
@@ -62,16 +62,19 @@ class OutgoingEncryptionLayer implements FrameWriter {
 			plaintextLength = frameLength - MAC_LENGTH;
 			ciphertextLength = frameLength;
 		}
+		// Encode the header
+		FrameEncoder.encodeHeader(plaintext, finalFrame, payloadLength);
+		// Copy the payload
+		System.arraycopy(payload, 0, plaintext, HEADER_LENGTH, payloadLength);
 		// If there's any padding it must all be zeroes
-		for(int i = HEADER_LENGTH + payloadLength; i < plaintextLength; i++) {
-			frame[i] = 0;
-		}
+		for(int i = HEADER_LENGTH + payloadLength; i < plaintextLength; i++)
+			plaintext[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,
+			int encrypted = frameCipher.doFinal(plaintext, 0, plaintextLength,
 					ciphertext, 0);
 			if(encrypted != ciphertextLength) throw new RuntimeException();
 		} catch(GeneralSecurityException badCipher) {
diff --git a/briar-core/src/org/briarproject/invitation/AliceConnector.java b/briar-core/src/org/briarproject/invitation/AliceConnector.java
index 9c7fd31f3aa0d0adf90501dbb6b2170954998d20..8c0bd78130b535ac9c0729d99dd84084addbfa97 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
@@ -131,14 +129,16 @@ class AliceConnector extends Connector {
 		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 =
+		r = readerFactory.createReader(streamReader);
+		// Create the writers
+		OutputStream streamWriter =
 				streamWriterFactory.createInvitationStreamWriter(out,
 						maxFrameLength, secret, true); // Alice's stream
-		w = writerFactory.createWriter(streamWriter.getOutputStream());
+		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..6b435fd2a6c12e92d3d1bf3d54ec4307f871ab3b 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
@@ -131,14 +129,16 @@ class BobConnector extends Connector {
 		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 =
+		r = readerFactory.createReader(streamReader);
+		// Create the writers
+		OutputStream streamWriter =
 				streamWriterFactory.createInvitationStreamWriter(out,
 						maxFrameLength, secret, false); // Bob's stream
-		w = writerFactory.createWriter(streamWriter.getOutputStream());
+		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/plugins/ConnectionManagerImpl.java b/briar-core/src/org/briarproject/plugins/ConnectionManagerImpl.java
index 9dad32f2e28a8f6f2b443d768147098528af9844..b72efb688db1d08dff6ea213022ccfa01ef1d7be 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,9 +25,7 @@ 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;
@@ -97,11 +96,10 @@ class ConnectionManagerImpl implements ConnectionManager {
 	private MessagingSession createIncomingSession(StreamContext ctx,
 			TransportConnectionReader r) throws IOException {
 		try {
-			StreamReader streamReader = streamReaderFactory.createStreamReader(
+			InputStream streamReader = streamReaderFactory.createStreamReader(
 					r.getInputStream(), r.getMaxFrameLength(), ctx);
 			return messagingSessionFactory.createIncomingSession(
-					ctx.getContactId(), ctx.getTransportId(),
-					streamReader.getInputStream());
+					ctx.getContactId(), ctx.getTransportId(), streamReader);
 		} finally {
 			ByteUtils.erase(ctx.getSecret());
 		}
@@ -110,11 +108,11 @@ class ConnectionManagerImpl implements ConnectionManager {
 	private MessagingSession createSimplexOutgoingSession(StreamContext ctx,
 			TransportConnectionWriter w) throws IOException {
 		try {
-			StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
+			OutputStream streamWriter = streamWriterFactory.createStreamWriter(
 					w.getOutputStream(), w.getMaxFrameLength(), ctx);
 			return messagingSessionFactory.createSimplexOutgoingSession(
 					ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
-					streamWriter.getOutputStream());
+					streamWriter);
 		} finally {
 			ByteUtils.erase(ctx.getSecret());
 		}
@@ -123,11 +121,11 @@ class ConnectionManagerImpl implements ConnectionManager {
 	private MessagingSession createDuplexOutgoingSession(StreamContext ctx,
 			TransportConnectionWriter w) throws IOException {
 		try {
-			StreamWriter streamWriter = streamWriterFactory.createStreamWriter(
+			OutputStream streamWriter = streamWriterFactory.createStreamWriter(
 					w.getOutputStream(), w.getMaxFrameLength(), ctx);
 			return messagingSessionFactory.createDuplexOutgoingSession(
 					ctx.getContactId(), ctx.getTransportId(), w.getMaxLatency(),
-					w.getMaxIdleTime(), streamWriter.getOutputStream());
+					w.getMaxIdleTime(), streamWriter);
 		} finally {
 			ByteUtils.erase(ctx.getSecret());
 		}
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/StreamReaderFactoryImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
index d71bfc50ac24c8cc72009a9df6689abee11ddfaa..daa4f2bb769f93d9da0664cb7b67e8b718ebc5e0 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderFactoryImpl.java
@@ -4,37 +4,32 @@ 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;
-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, int maxFrameLength,
+			StreamContext ctx) {
+		StreamDecrypter s = streamDecrypterFactory.createStreamDecrypter(in,
+				maxFrameLength, ctx);
+		return new StreamReaderImpl(s, maxFrameLength);
 	}
 
-	public StreamReader createInvitationStreamReader(InputStream in,
+	public InputStream 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);
+		StreamDecrypter s =
+				streamDecrypterFactory.createInvitationStreamDecrypter(in,
+						maxFrameLength, secret, alice);
+		return new StreamReaderImpl(s, maxFrameLength);
 	}
 }
diff --git a/briar-core/src/org/briarproject/transport/StreamReaderImpl.java b/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
index 0de048a6fa67f805425a645c01affdc8fbd46cf0..e9a30b24a96d5bd871087c49f4bcf5aef78b9ffc 100644
--- a/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamReaderImpl.java
@@ -6,22 +6,18 @@ import static org.briarproject.api.transport.TransportConstants.MAC_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, int frameLength) {
+		this.decrypter = decrypter;
+		payload = new byte[frameLength - HEADER_LENGTH - MAC_LENGTH];
 	}
 
 	@Override
@@ -30,7 +26,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 +44,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 +52,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..f57a8f69bf96c413bbae7f88bbbf9ae5523b905f 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterFactoryImpl.java
@@ -1,46 +1,35 @@
 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.StreamEncrypter;
+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, int maxFrameLength,
+			StreamContext ctx) {
+		StreamEncrypter s = streamEncrypterFactory.createStreamEncrypter(out,
+				maxFrameLength, ctx);
+		return new StreamWriterImpl(s, maxFrameLength);
 	}
 
-	public StreamWriter createInvitationStreamWriter(OutputStream out,
+	public OutputStream 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);
+		StreamEncrypter s =
+				streamEncrypterFactory.createInvitationStreamEncrypter(out,
+						maxFrameLength, secret, alice);
+		return new StreamWriterImpl(s, maxFrameLength);
 	}
 }
\ 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 7e38809a8baa6b8c440a9e801153c844e581d54a..05653fd42a4754b005d6b49e9e3b8617876a64ec 100644
--- a/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
+++ b/briar-core/src/org/briarproject/transport/StreamWriterImpl.java
@@ -6,7 +6,7 @@ import static org.briarproject.api.transport.TransportConstants.MAC_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 +15,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, int maxFrameLength) {
+		this.encrypter = encrypter;
+		payload = new byte[maxFrameLength - HEADER_LENGTH - MAC_LENGTH];
 	}
 
 	@Override
 	public void close() throws IOException {
 		writeFrame(true);
-		out.flush();
+		encrypter.flush();
 		super.close();
 	}
 
 	@Override
 	public void flush() throws IOException {
 		writeFrame(false);
-		out.flush();
+		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 +54,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, finalFrame);
 		length = 0;
 	}
 }
diff --git a/briar-tests/build.xml b/briar-tests/build.xml
index 2ab788bba3b4ecc03415ed0f4aabe0343e183b1f..561a3fbe55f269efd55d54370c17649efb808a7d 100644
--- a/briar-tests/build.xml
+++ b/briar-tests/build.xml
@@ -102,6 +102,8 @@
 			<test name="org.briarproject.crypto.PasswordBasedKdfTest"/>
 			<test name="org.briarproject.crypto.PasswordStrengthEstimatorTest"/>
 			<test name='org.briarproject.crypto.SecretKeyImplTest'/>
+			<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 +128,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..a3983b5a2eaf490f3030f72db9e9371a80fb9117 100644
--- a/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/ProtocolIntegrationTest.java
@@ -7,6 +7,7 @@ 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 +37,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;
@@ -119,10 +118,10 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		StreamContext ctx = new StreamContext(contactId, transportId,
 				secret.clone(), 0, true);
-		StreamWriter streamWriter = streamWriterFactory.createStreamWriter(out,
+		OutputStream streamWriter = streamWriterFactory.createStreamWriter(out,
 				MAX_FRAME_LENGTH, ctx);
 		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(
-				streamWriter.getOutputStream());
+				streamWriter);
 
 		packetWriter.writeAck(new Ack(messageIds));
 
@@ -140,7 +139,7 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 				transportProperties, 1);
 		packetWriter.writeTransportUpdate(tu);
 
-		streamWriter.getOutputStream().flush();
+		streamWriter.flush();
 		return out.toByteArray();
 	}
 
@@ -151,10 +150,10 @@ public class ProtocolIntegrationTest extends BriarTestCase {
 		// FIXME: Check that the expected tag was received
 		StreamContext ctx = new StreamContext(contactId, transportId,
 				secret.clone(), 0, false);
-		StreamReader streamReader = streamReaderFactory.createStreamReader(in,
+		InputStream streamReader = streamReaderFactory.createStreamReader(in,
 				MAX_FRAME_LENGTH, ctx);
 		PacketReader packetReader = packetReaderFactory.createPacketReader(
-				streamReader.getInputStream());
+				streamReader);
 
 		// Read the ack
 		assertTrue(packetReader.hasAck());
diff --git a/briar-tests/src/org/briarproject/transport/IncomingEncryptionLayerTest.java b/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
similarity index 80%
rename from briar-tests/src/org/briarproject/transport/IncomingEncryptionLayerTest.java
rename to briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
index 8e1791785632439f24962584d36c03e6d41f06de..1ee05436a6af717656b71924aa968d8697ea4307 100644
--- a/briar-tests/src/org/briarproject/transport/IncomingEncryptionLayerTest.java
+++ b/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
@@ -1,4 +1,4 @@
-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;
@@ -14,13 +14,12 @@ 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 {
+public class StreamDecrypterImplTest extends BriarTestCase {
 
 	// FIXME: This is an integration test, not a unit test
 
@@ -32,7 +31,7 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 	private final AuthenticatedCipher frameCipher;
 	private final SecretKey frameKey;
 
-	public IncomingEncryptionLayerTest() {
+	public StreamDecrypterImplTest() {
 		Injector i = Guice.createInjector(new CryptoModule(),
 				new TestLifecycleModule(), new TestSystemModule());
 		crypto = i.getInstance(CryptoComponent.class);
@@ -51,11 +50,11 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		System.arraycopy(frame1, 0, valid, FRAME_LENGTH, FRAME_LENGTH);
 		// Read the frames
 		ByteArrayInputStream in = new ByteArrayInputStream(valid);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
-		byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH];
-		assertEquals(123, i.readFrame(buf));
-		assertEquals(123, i.readFrame(buf));
+		byte[] payload = new byte[MAX_PAYLOAD_LENGTH];
+		assertEquals(123, i.readFrame(payload));
+		assertEquals(123, i.readFrame(payload));
 	}
 
 	@Test
@@ -67,10 +66,10 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
 		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
+			i.readFrame(new byte[MAX_PAYLOAD_LENGTH]);
 			fail();
 		} catch(FormatException expected) {}
 	}
@@ -83,10 +82,10 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
 		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
+			i.readFrame(new byte[MAX_PAYLOAD_LENGTH]);
 			fail();
 		} catch(FormatException expected) {}
 	}
@@ -97,10 +96,10 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
 		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
+			i.readFrame(new byte[MAX_PAYLOAD_LENGTH]);
 			fail();
 		} catch(FormatException expected) {}
 	}
@@ -111,9 +110,9 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		byte[] frame = generateFrame(0, FRAME_LENGTH - 1, 123, true, false);
 		// Read the frame
 		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		IncomingEncryptionLayer i = new IncomingEncryptionLayer(in, frameCipher,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
-		int length = i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
+		int length = i.readFrame(new byte[MAX_PAYLOAD_LENGTH]);
 		assertEquals(123, length);
 	}
 
@@ -124,10 +123,10 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 				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,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
 		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
+			i.readFrame(new byte[MAX_PAYLOAD_LENGTH]);
 			fail();
 		} catch(FormatException expected) {}
 	}
@@ -138,10 +137,10 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
 		try {
-			i.readFrame(new byte[FRAME_LENGTH - MAC_LENGTH]);
+			i.readFrame(new byte[MAX_PAYLOAD_LENGTH]);
 			fail();
 		} catch(FormatException expected) {}
 	}
@@ -158,12 +157,12 @@ public class IncomingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamDecrypterImpl i = new StreamDecrypterImpl(in, frameCipher,
 				frameKey, FRAME_LENGTH);
-		byte[] buf = new byte[FRAME_LENGTH - MAC_LENGTH];
-		assertEquals(MAX_PAYLOAD_LENGTH, i.readFrame(buf));
+		byte[] payload = new byte[MAX_PAYLOAD_LENGTH];
+		assertEquals(MAX_PAYLOAD_LENGTH, i.readFrame(payload));
 		// The frame after the final frame should not be read
-		assertEquals(-1, i.readFrame(buf));
+		assertEquals(-1, i.readFrame(payload));
 	}
 
 	private byte[] generateFrame(long frameNumber, int frameLength,
diff --git a/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java b/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
similarity index 88%
rename from briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
rename to briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
index d9b7340335a1afb4fe960525e593d5104f44ff6e..d4f8b33a5b767f1fb128594fc551884ef2808145 100644
--- a/briar-tests/src/org/briarproject/transport/OutgoingEncryptionLayerTest.java
+++ b/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
@@ -1,4 +1,4 @@
-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;
@@ -15,13 +15,12 @@ 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 {
+public class StreamEncrypterImplTest extends BriarTestCase {
 
 	// FIXME: This is an integration test, not a unit test
 
@@ -30,7 +29,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 	private final CryptoComponent crypto;
 	private final AuthenticatedCipher frameCipher;
 
-	public OutgoingEncryptionLayerTest() {
+	public StreamEncrypterImplTest() {
 		Injector i = Guice.createInjector(new CryptoModule(),
 				new TestLifecycleModule(), new TestSystemModule());
 		crypto = i.getInstance(CryptoComponent.class);
@@ -52,9 +51,9 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamEncrypterImpl o = new StreamEncrypterImpl(out,
 				frameCipher, frameKey, FRAME_LENGTH, null);
-		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false);
+		o.writeFrame(new byte[payloadLength], payloadLength, false);
 		byte[] actual = out.toByteArray();
 		assertEquals(FRAME_LENGTH, actual.length);
 		for(int i = 0; i < FRAME_LENGTH; i++)
@@ -78,9 +77,9 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		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,
+		StreamEncrypterImpl o = new StreamEncrypterImpl(out,
 				frameCipher, frameKey, FRAME_LENGTH, tag);
-		o.writeFrame(new byte[FRAME_LENGTH - MAC_LENGTH], payloadLength, false);
+		o.writeFrame(new byte[payloadLength], 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]);
@@ -94,7 +93,7 @@ public class OutgoingEncryptionLayerTest extends BriarTestCase {
 		new Random().nextBytes(tag);
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		// Initiator's constructor
-		OutgoingEncryptionLayer o = new OutgoingEncryptionLayer(out,
+		StreamEncrypterImpl o = new StreamEncrypterImpl(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);
diff --git a/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
index 7a24f0c94141d812b1f80eef88d52744bd79feb2..3bce2a6e39d6b004dfa4daaa3feae094bf1b7ad0 100644
--- a/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/messaging/SimplexMessagingIntegrationTest.java
@@ -9,6 +9,8 @@ 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;
@@ -39,9 +41,7 @@ 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;
@@ -136,26 +136,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,
+		OutputStream streamWriter = streamWriterFactory.createStreamWriter(out,
 				MAX_FRAME_LENGTH, ctx);
 		// Create an outgoing messaging session
 		EventBus eventBus = alice.getInstance(EventBus.class);
 		PacketWriterFactory packetWriterFactory =
 				alice.getInstance(PacketWriterFactory.class);
 		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(
-				streamWriter.getOutputStream());
+				streamWriter);
 		MessagingSession session = new SimplexOutgoingSession(db,
 				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();
@@ -204,7 +205,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		// Create a stream reader
 		StreamReaderFactory streamReaderFactory =
 				bob.getInstance(StreamReaderFactory.class);
-		StreamReader streamReader = streamReaderFactory.createStreamReader(in,
+		InputStream streamReader = streamReaderFactory.createStreamReader(in,
 				MAX_FRAME_LENGTH, ctx);
 		// Create an incoming messaging session
 		EventBus eventBus = bob.getInstance(EventBus.class);
@@ -213,7 +214,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		PacketReaderFactory packetReaderFactory =
 				bob.getInstance(PacketReaderFactory.class);
 		PacketReader packetReader = packetReaderFactory.createPacketReader(
-				streamReader.getInputStream());
+				streamReader);
 		MessagingSession session = new IncomingSession(db,
 				new ImmediateExecutor(), new ImmediateExecutor(), eventBus,
 				messageVerifier, contactId, transportId, packetReader);
@@ -221,7 +222,7 @@ public class SimplexMessagingIntegrationTest extends BriarTestCase {
 		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/transport/StreamReaderImplTest.java b/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java
index 67337a50ca4fca64e76f3ffa6b82fd54450e84ce..988299852d0506172aebf4280feedeb6e8148789 100644
--- a/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java
+++ b/briar-tests/src/org/briarproject/transport/StreamReaderImplTest.java
@@ -4,6 +4,7 @@ import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.api.crypto.StreamDecrypter;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
@@ -17,18 +18,18 @@ public class StreamReaderImplTest extends BriarTestCase {
 	@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, FRAME_LENGTH);
 		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 +41,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, FRAME_LENGTH);
 		byte[] buf = new byte[MAX_PAYLOAD_LENGTH];
 		// Skip the first empty frame, read the two payload bytes
 		assertEquals(2, r.read(buf));
@@ -66,14 +67,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, FRAME_LENGTH);
 		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 +89,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, FRAME_LENGTH);
 		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 708769b2cded12d94da47794678553df3e52003f..688621bd1511766e0bc8f7bf6f1ff1a170e4c385 100644
--- a/briar-tests/src/org/briarproject/transport/StreamWriterImplTest.java
+++ b/briar-tests/src/org/briarproject/transport/StreamWriterImplTest.java
@@ -4,6 +4,7 @@ import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.api.crypto.StreamEncrypter;
 import org.jmock.Expectations;
 import org.jmock.Mockery;
 import org.junit.Test;
@@ -17,15 +18,15 @@ public class StreamWriterImplTest extends BriarTestCase {
 	@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),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), 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, FRAME_LENGTH);
 		w.close();
 		context.assertIsSatisfied();
 	}
@@ -34,14 +35,14 @@ public class StreamWriterImplTest extends BriarTestCase {
 	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, FRAME_LENGTH);
 		context.checking(new Expectations() {{
 			// Write a non-final frame with an empty payload
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(0),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
 					with(false));
 			// Flush the stream
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.flush();
 		context.assertIsSatisfied();
@@ -49,9 +50,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),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
 					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -61,14 +62,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, FRAME_LENGTH);
 		context.checking(new Expectations() {{
 			// Write a non-final frame with one payload byte
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(1),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(1),
 					with(false));
 			// Flush the stream
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.write(0);
 		w.flush();
@@ -77,9 +78,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),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
 					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -88,11 +89,11 @@ 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, FRAME_LENGTH);
 		context.checking(new Expectations() {{
 			// Write a full non-final frame
-			oneOf(writer).writeFrame(with(any(byte[].class)),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)),
 					with(MAX_PAYLOAD_LENGTH), with(false));
 		}});
 		for(int i = 0; i < MAX_PAYLOAD_LENGTH; i++) {
@@ -103,9 +104,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),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
 					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -114,11 +115,11 @@ 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, FRAME_LENGTH);
 		context.checking(new Expectations() {{
 			// Write two full non-final frames
-			exactly(2).of(writer).writeFrame(with(any(byte[].class)),
+			exactly(2).of(encrypter).writeFrame(with(any(byte[].class)),
 					with(MAX_PAYLOAD_LENGTH), with(false));
 		}});
 		// Sanity check
@@ -134,9 +135,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),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(0),
 					with(true));
-			oneOf(writer).flush();
+			oneOf(encrypter).flush();
 		}});
 		w.close();
 		context.assertIsSatisfied();
@@ -145,17 +146,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, FRAME_LENGTH);
 		context.checking(new Expectations() {{
 			// Write two full non-final frames
-			exactly(2).of(writer).writeFrame(with(any(byte[].class)),
+			exactly(2).of(encrypter).writeFrame(with(any(byte[].class)),
 					with(MAX_PAYLOAD_LENGTH), with(false));
 			// Write a final frame with a one-byte payload
-			oneOf(writer).writeFrame(with(any(byte[].class)), with(1),
+			oneOf(encrypter).writeFrame(with(any(byte[].class)), with(1),
 					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..b0438801e6beb25915c8566418921e6d04d6e9a7
--- /dev/null
+++ b/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java
@@ -0,0 +1,44 @@
+package org.briarproject.transport;
+
+import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_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, int frameLength) {
+		this.in = in;
+		frame = new byte[frameLength];
+	}
+
+	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..4ad4c667c6203ef7d0a7d01f9c56c5238f5d986f
--- /dev/null
+++ b/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java
@@ -0,0 +1,44 @@
+package org.briarproject.transport;
+
+import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_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, int frameLength, byte[] tag) {
+		this.out = out;
+		this.tag = tag;
+		frame = new byte[frameLength];
+	}
+
+	public void writeFrame(byte[] payload, int payloadLength,
+			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..f55e5b6e1a8ef83fc5d504ca5a5ef79fe6fb2a1b 100644
--- a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
@@ -11,48 +11,18 @@ 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,27 +36,24 @@ 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[1234];
+		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,
+		StreamEncrypter encrypter = new TestStreamEncrypter(out, FRAME_LENGTH,
+				tag);
+		OutputStream streamWriter = new StreamWriterImpl(encrypter,
 				FRAME_LENGTH);
-		OutputStream out1 = streamWriter.getOutputStream();
-		out1.write(frame);
-		out1.flush();
-		out1.write(frame1);
-		out1.flush();
+		streamWriter.write(payload1);
+		streamWriter.flush();
+		streamWriter.write(payload2);
+		streamWriter.flush();
 		byte[] output = out.toByteArray();
 		assertEquals(TAG_LENGTH + FRAME_LENGTH * 2, output.length);
 		// Read the tag back
@@ -95,17 +62,15 @@ public class TransportIntegrationTest extends BriarTestCase {
 		read(in, recoveredTag);
 		assertArrayEquals(tag, recoveredTag);
 		// Read the frames back
-		FrameReader frameReader = new IncomingEncryptionLayer(in, frameCipher,
-				frameKey, FRAME_LENGTH);
-		StreamReaderImpl streamReader = new StreamReaderImpl(frameReader,
+		StreamDecrypter decrypter = new TestStreamDecrypter(in, FRAME_LENGTH);
+		InputStream streamReader = new StreamReaderImpl(decrypter,
 				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);
+		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();
 	}