diff --git a/briar-api/src/org/briarproject/api/transport/TransportConstants.java b/briar-api/src/org/briarproject/api/transport/TransportConstants.java
index 797a47667a5e61aea45817186a343d924139c86c..219f5d481461cb499635be23d59e8baba074a7d4 100644
--- a/briar-api/src/org/briarproject/api/transport/TransportConstants.java
+++ b/briar-api/src/org/briarproject/api/transport/TransportConstants.java
@@ -1,25 +1,35 @@
 package org.briarproject.api.transport;
 
 
+import org.briarproject.api.crypto.SecretKey;
+
 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 = 1024;
+	/** The length of the stream header IV in bytes. */
+	int STREAM_HEADER_IV_LENGTH = 24;
 
 	/** The length of the message authentication code (MAC) in bytes. */
 	int MAC_LENGTH = 16;
 
+	/** The length of the stream header in bytes. */
+	int STREAM_HEADER_LENGTH = STREAM_HEADER_IV_LENGTH + SecretKey.LENGTH
+			+ MAC_LENGTH;
+
+	/** The length of the frame initalisation vector (IV) in bytes. */
+	int FRAME_IV_LENGTH = 12;
+
 	/** The length of the frame header in bytes. */
-	int HEADER_LENGTH = 4 + MAC_LENGTH;
+	int FRAME_HEADER_LENGTH = 4 + MAC_LENGTH;
 
-	/** The maximum total length of the frame payload and padding in bytes. */
-	int MAX_PAYLOAD_LENGTH = MAX_FRAME_LENGTH - HEADER_LENGTH - MAC_LENGTH;
+	/** The maximum length of a frame in bytes, including the header and MAC. */
+	int MAX_FRAME_LENGTH = 1024;
 
-	/** The length of the initalisation vector (IV) in bytes. */
-	int IV_LENGTH = 12;
+	/** The maximum total length of the frame payload and padding in bytes. */
+	int MAX_PAYLOAD_LENGTH = MAX_FRAME_LENGTH - FRAME_HEADER_LENGTH
+			- MAC_LENGTH;
 
 	/**
 	 * The minimum stream length in bytes that all transport plugins must
diff --git a/briar-core/src/org/briarproject/crypto/FrameEncoder.java b/briar-core/src/org/briarproject/crypto/FrameEncoder.java
index 641e9a4664337826b3f26631774d31ca480fd374..efd03ed49bad9c530eb7851571f78fae0f5509a0 100644
--- a/briar-core/src/org/briarproject/crypto/FrameEncoder.java
+++ b/briar-core/src/org/briarproject/crypto/FrameEncoder.java
@@ -1,27 +1,28 @@
 package org.briarproject.crypto;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.IV_LENGTH;
+import org.briarproject.util.ByteUtils;
+
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_IV_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, boolean header) {
-		if (iv.length < IV_LENGTH) throw new IllegalArgumentException();
+		if (iv.length < FRAME_IV_LENGTH) throw new IllegalArgumentException();
 		if (frameNumber < 0 || frameNumber > MAX_32_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
 		ByteUtils.writeUint32(frameNumber, iv, 0);
 		if (header) iv[4] = 1;
 		else iv[4] = 0;
-		for (int i = 5; i < IV_LENGTH; i++) iv[i] = 0;
+		for (int i = 5; i < FRAME_IV_LENGTH; i++) iv[i] = 0;
 	}
 
 	static void encodeHeader(byte[] header, boolean finalFrame,
 			int payloadLength, int paddingLength) {
-		if (header.length < HEADER_LENGTH) throw new IllegalArgumentException();
+		if (header.length < FRAME_HEADER_LENGTH)
+			throw new IllegalArgumentException();
 		if (payloadLength < 0) throw new IllegalArgumentException();
 		if (paddingLength < 0) throw new IllegalArgumentException();
 		if (payloadLength + paddingLength > MAX_PAYLOAD_LENGTH)
@@ -32,17 +33,20 @@ class FrameEncoder {
 	}
 
 	static boolean isFinalFrame(byte[] header) {
-		if (header.length < HEADER_LENGTH) throw new IllegalArgumentException();
+		if (header.length < FRAME_HEADER_LENGTH)
+			throw new IllegalArgumentException();
 		return (header[0] & 0x80) == 0x80;
 	}
 
 	static int getPayloadLength(byte[] header) {
-		if (header.length < HEADER_LENGTH) throw new IllegalArgumentException();
+		if (header.length < FRAME_HEADER_LENGTH)
+			throw new IllegalArgumentException();
 		return ByteUtils.readUint16(header, 0) & 0x7FFF;
 	}
 
 	static int getPaddingLength(byte[] header) {
-		if (header.length < HEADER_LENGTH) throw new IllegalArgumentException();
+		if (header.length < FRAME_HEADER_LENGTH)
+			throw new IllegalArgumentException();
 		return ByteUtils.readUint16(header, 2);
 	}
 }
diff --git a/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
index 19d3c69c456084e511d0c59a8c6b5d8c66f0d341..89e18bee6119fea1b02809d6f582a9f2696d93e5 100644
--- a/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamDecrypterImpl.java
@@ -9,31 +9,34 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.security.GeneralSecurityException;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.IV_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_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.api.transport.TransportConstants.STREAM_HEADER_IV_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
 
-// FIXME: Implementation is incomplete, doesn't read the stream header
 class StreamDecrypterImpl implements StreamDecrypter {
 
 	private final InputStream in;
-	private final AuthenticatedCipher frameCipher;
-	private final SecretKey frameKey;
-	private final byte[] iv, frameHeader, frameCiphertext;
+	private final AuthenticatedCipher cipher;
+	private final SecretKey streamHeaderKey;
+	private final byte[] frameIv, frameHeader, frameCiphertext;
 
+	private SecretKey frameKey;
 	private long frameNumber;
 	private boolean finalFrame;
 
-	StreamDecrypterImpl(InputStream in, AuthenticatedCipher frameCipher,
-			SecretKey headerKey) {
+	StreamDecrypterImpl(InputStream in, AuthenticatedCipher cipher,
+			SecretKey streamHeaderKey) {
 		this.in = in;
-		this.frameCipher = frameCipher;
-		this.frameKey = headerKey; // FIXME
-		iv = new byte[IV_LENGTH];
-		frameHeader = new byte[HEADER_LENGTH];
+		this.cipher = cipher;
+		this.streamHeaderKey = streamHeaderKey;
+		frameIv = new byte[FRAME_IV_LENGTH];
+		frameHeader = new byte[FRAME_HEADER_LENGTH];
 		frameCiphertext = new byte[MAX_FRAME_LENGTH];
+		frameKey = null;
 		frameNumber = 0;
 		finalFrame = false;
 	}
@@ -43,20 +46,23 @@ class StreamDecrypterImpl implements StreamDecrypter {
 		if (payload.length < MAX_PAYLOAD_LENGTH)
 			throw new IllegalArgumentException();
 		if (finalFrame) return -1;
+		// Read the stream header if required
+		if (frameKey == null) readStreamHeader();
 		// Read the frame header
 		int offset = 0;
-		while (offset < HEADER_LENGTH) {
-			int read = in.read(frameCiphertext, offset, HEADER_LENGTH - offset);
+		while (offset < FRAME_HEADER_LENGTH) {
+			int read = in.read(frameCiphertext, offset,
+					FRAME_HEADER_LENGTH - offset);
 			if (read == -1) throw new EOFException();
 			offset += read;
 		}
 		// Decrypt and authenticate the frame header
-		FrameEncoder.encodeIv(iv, frameNumber, true);
+		FrameEncoder.encodeIv(frameIv, frameNumber, true);
 		try {
-			frameCipher.init(false, frameKey, iv);
-			int decrypted = frameCipher.process(frameCiphertext, 0,
-					HEADER_LENGTH, frameHeader, 0);
-			if (decrypted != HEADER_LENGTH - MAC_LENGTH)
+			cipher.init(false, frameKey, frameIv);
+			int decrypted = cipher.process(frameCiphertext, 0,
+					FRAME_HEADER_LENGTH, frameHeader, 0);
+			if (decrypted != FRAME_HEADER_LENGTH - MAC_LENGTH)
 				throw new RuntimeException();
 		} catch (GeneralSecurityException e) {
 			throw new FormatException();
@@ -68,7 +74,7 @@ class StreamDecrypterImpl implements StreamDecrypter {
 		if (payloadLength + paddingLength > MAX_PAYLOAD_LENGTH)
 			throw new FormatException();
 		// Read the payload and padding
-		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
+		int frameLength = FRAME_HEADER_LENGTH + payloadLength + paddingLength
 				+ MAC_LENGTH;
 		while (offset < frameLength) {
 			int read = in.read(frameCiphertext, offset, frameLength - offset);
@@ -76,10 +82,10 @@ class StreamDecrypterImpl implements StreamDecrypter {
 			offset += read;
 		}
 		// Decrypt and authenticate the payload and padding
-		FrameEncoder.encodeIv(iv, frameNumber, false);
+		FrameEncoder.encodeIv(frameIv, frameNumber, false);
 		try {
-			frameCipher.init(false, frameKey, iv);
-			int decrypted = frameCipher.process(frameCiphertext, HEADER_LENGTH,
+			cipher.init(false, frameKey, frameIv);
+			int decrypted = cipher.process(frameCiphertext, FRAME_HEADER_LENGTH,
 					payloadLength + paddingLength + MAC_LENGTH, payload, 0);
 			if (decrypted != payloadLength + paddingLength)
 				throw new RuntimeException();
@@ -92,4 +98,31 @@ class StreamDecrypterImpl implements StreamDecrypter {
 		frameNumber++;
 		return payloadLength;
 	}
+
+	private void readStreamHeader() throws IOException {
+		byte[] streamHeaderIv = new byte[STREAM_HEADER_IV_LENGTH];
+		byte[] streamHeaderCiphertext = new byte[STREAM_HEADER_LENGTH];
+		byte[] streamHeaderPlaintext = new byte[SecretKey.LENGTH];
+		// Read the stream header
+		int offset = 0;
+		while (offset < STREAM_HEADER_LENGTH) {
+			int read = in.read(streamHeaderCiphertext, offset,
+					STREAM_HEADER_LENGTH - offset);
+			if (read == -1) throw new EOFException();
+			offset += read;
+		}
+		// Decrypt and authenticate the stream header
+		System.arraycopy(streamHeaderCiphertext, 0, streamHeaderIv, 0,
+				STREAM_HEADER_IV_LENGTH);
+		try {
+			cipher.init(false, streamHeaderKey, streamHeaderIv);
+			int decrypted = cipher.process(streamHeaderCiphertext,
+					STREAM_HEADER_IV_LENGTH, SecretKey.LENGTH + MAC_LENGTH,
+					streamHeaderPlaintext, 0);
+			if (decrypted != SecretKey.LENGTH) throw new RuntimeException();
+		} catch (GeneralSecurityException e) {
+			throw new FormatException();
+		}
+		frameKey = new SecretKey(streamHeaderPlaintext);
+	}
 }
\ 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
index e8e6fada7f34ec49d2e5a1bf8e81bd266e59354a..989292064752851962f90b8eab1e358ec93f7575 100644
--- a/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterFactoryImpl.java
@@ -1,17 +1,18 @@
 package org.briarproject.crypto;
 
-import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
+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 java.io.OutputStream;
 
 import javax.inject.Inject;
 import javax.inject.Provider;
 
-import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.crypto.SecretKey;
-import org.briarproject.api.crypto.StreamEncrypter;
-import org.briarproject.api.crypto.StreamEncrypterFactory;
-import org.briarproject.api.transport.StreamContext;
+import static org.briarproject.api.transport.TransportConstants.STREAM_HEADER_IV_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 
 class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
 
@@ -27,15 +28,23 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
 
 	public StreamEncrypter createStreamEncrypter(OutputStream out,
 			StreamContext ctx) {
+		AuthenticatedCipher cipher = cipherProvider.get();
 		byte[] tag = new byte[TAG_LENGTH];
 		crypto.encodeTag(tag, ctx.getTagKey(), ctx.getStreamNumber());
-		AuthenticatedCipher cipher = cipherProvider.get();
-		return new StreamEncrypterImpl(out, cipher, ctx.getHeaderKey(), tag);
+		byte[] streamHeaderIv = new byte[STREAM_HEADER_IV_LENGTH];
+		crypto.getSecureRandom().nextBytes(streamHeaderIv);
+		SecretKey frameKey = crypto.generateSecretKey();
+		return new StreamEncrypterImpl(out, cipher, tag, streamHeaderIv,
+				ctx.getHeaderKey(), frameKey);
 	}
 
 	public StreamEncrypter createInvitationStreamEncrypter(OutputStream out,
 			SecretKey headerKey) {
 		AuthenticatedCipher cipher = cipherProvider.get();
-		return new StreamEncrypterImpl(out, cipher, headerKey, null);
+		byte[] streamHeaderIv = new byte[STREAM_HEADER_IV_LENGTH];
+		crypto.getSecureRandom().nextBytes(streamHeaderIv);
+		SecretKey frameKey = crypto.generateSecretKey();
+		return new StreamEncrypterImpl(out, cipher, null, streamHeaderIv,
+				headerKey, frameKey);
 	}
 }
diff --git a/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
index 89fde2a91da9e7bfc506b04a77dfc63a0c119dcd..988be7e0293c9b91e0833af20fdff5543bbebdd8 100644
--- a/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
+++ b/briar-core/src/org/briarproject/crypto/StreamEncrypterImpl.java
@@ -7,35 +7,41 @@ import java.io.IOException;
 import java.io.OutputStream;
 import java.security.GeneralSecurityException;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.IV_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_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.api.transport.TransportConstants.STREAM_HEADER_IV_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
 import static org.briarproject.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
-// FIXME: Implementation is incomplete, doesn't write the stream header
 class StreamEncrypterImpl implements StreamEncrypter {
 
 	private final OutputStream out;
-	private final AuthenticatedCipher frameCipher;
-	private final SecretKey frameKey;
-	private final byte[] tag, iv, framePlaintext, frameCiphertext;
+	private final AuthenticatedCipher cipher;
+	private final SecretKey streamHeaderKey, frameKey;
+	private final byte[] tag, streamHeaderIv;
+	private final byte[] frameIv, framePlaintext, frameCiphertext;
 
 	private long frameNumber;
-	private boolean writeTag;
+	private boolean writeTag, writeStreamHeader;
 
-	StreamEncrypterImpl(OutputStream out, AuthenticatedCipher frameCipher,
-			SecretKey headerKey, byte[] tag) {
+	StreamEncrypterImpl(OutputStream out, AuthenticatedCipher cipher,
+			byte[] tag, byte[] streamHeaderIv, SecretKey streamHeaderKey,
+			SecretKey frameKey) {
 		this.out = out;
-		this.frameCipher = frameCipher;
-		this.frameKey = headerKey; // FIXME
+		this.cipher = cipher;
 		this.tag = tag;
-		iv = new byte[IV_LENGTH];
-		framePlaintext = new byte[HEADER_LENGTH + MAX_PAYLOAD_LENGTH];
+		this.streamHeaderIv = streamHeaderIv;
+		this.streamHeaderKey = streamHeaderKey;
+		this.frameKey = frameKey;
+		frameIv = new byte[FRAME_IV_LENGTH];
+		framePlaintext = new byte[FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH];
 		frameCiphertext = new byte[MAX_FRAME_LENGTH];
 		frameNumber = 0;
 		writeTag = (tag != null);
+		writeStreamHeader = true;
 	}
 
 	public void writeFrame(byte[] payload, int payloadLength,
@@ -45,52 +51,75 @@ class StreamEncrypterImpl implements StreamEncrypter {
 		// 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;
-		}
+		if (writeTag) writeTag();
+		// Write the stream header if required
+		if (writeStreamHeader) writeStreamHeader();
 		// Encode the frame header
 		FrameEncoder.encodeHeader(framePlaintext, finalFrame, payloadLength,
 				paddingLength);
 		// Encrypt and authenticate the frame header
-		FrameEncoder.encodeIv(iv, frameNumber, true);
+		FrameEncoder.encodeIv(frameIv, frameNumber, true);
 		try {
-			frameCipher.init(true, frameKey, iv);
-			int encrypted = frameCipher.process(framePlaintext, 0,
-					HEADER_LENGTH - MAC_LENGTH, frameCiphertext, 0);
-			if (encrypted != HEADER_LENGTH) throw new RuntimeException();
+			cipher.init(true, frameKey, frameIv);
+			int encrypted = cipher.process(framePlaintext, 0,
+					FRAME_HEADER_LENGTH - MAC_LENGTH, frameCiphertext, 0);
+			if (encrypted != FRAME_HEADER_LENGTH) throw new RuntimeException();
 		} catch (GeneralSecurityException badCipher) {
 			throw new RuntimeException(badCipher);
 		}
 		// Combine the payload and padding
-		System.arraycopy(payload, 0, framePlaintext, HEADER_LENGTH,
+		System.arraycopy(payload, 0, framePlaintext, FRAME_HEADER_LENGTH,
 				payloadLength);
 		for (int i = 0; i < paddingLength; i++)
-			framePlaintext[HEADER_LENGTH + payloadLength + i] = 0;
+			framePlaintext[FRAME_HEADER_LENGTH + payloadLength + i] = 0;
 		// Encrypt and authenticate the payload and padding
-		FrameEncoder.encodeIv(iv, frameNumber, false);
+		FrameEncoder.encodeIv(frameIv, frameNumber, false);
 		try {
-			frameCipher.init(true, frameKey, iv);
-			int encrypted = frameCipher.process(framePlaintext, HEADER_LENGTH,
+			cipher.init(true, frameKey, frameIv);
+			int encrypted = cipher.process(framePlaintext, FRAME_HEADER_LENGTH,
 					payloadLength + paddingLength, frameCiphertext,
-					HEADER_LENGTH);
+					FRAME_HEADER_LENGTH);
 			if (encrypted != payloadLength + paddingLength + MAC_LENGTH)
 				throw new RuntimeException();
 		} catch (GeneralSecurityException badCipher) {
 			throw new RuntimeException(badCipher);
 		}
 		// Write the frame
-		out.write(frameCiphertext, 0, HEADER_LENGTH + payloadLength
+		out.write(frameCiphertext, 0, FRAME_HEADER_LENGTH + payloadLength
 				+ paddingLength + MAC_LENGTH);
 		frameNumber++;
 	}
 
+	private void writeTag() throws IOException {
+		out.write(tag, 0, tag.length);
+		writeTag = false;
+	}
+
+	private void writeStreamHeader() throws IOException {
+		byte[] streamHeaderPlaintext = frameKey.getBytes();
+		byte[] streamHeaderCiphertext = new byte[STREAM_HEADER_LENGTH];
+		System.arraycopy(streamHeaderIv, 0, streamHeaderCiphertext, 0,
+				STREAM_HEADER_IV_LENGTH);
+		// Encrypt and authenticate the frame key
+		try {
+			cipher.init(true, streamHeaderKey, streamHeaderIv);
+			int encrypted = cipher.process(streamHeaderPlaintext, 0,
+					SecretKey.LENGTH, streamHeaderCiphertext,
+					STREAM_HEADER_IV_LENGTH);
+			if (encrypted != SecretKey.LENGTH + MAC_LENGTH)
+				throw new RuntimeException();
+		} catch (GeneralSecurityException badCipher) {
+			throw new RuntimeException(badCipher);
+		}
+		out.write(streamHeaderCiphertext);
+		writeStreamHeader = false;
+	}
+
 	public void flush() throws IOException {
 		// Write the tag if required
-		if (writeTag) {
-			out.write(tag, 0, tag.length);
-			writeTag = false;
-		}
+		if (writeTag) writeTag();
+		// Write the stream header if required
+		if (writeStreamHeader) writeStreamHeader();
 		out.flush();
 	}
 }
\ No newline at end of file
diff --git a/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java b/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
index c907f37e246939b7db1c13491317772fb0aa44b5..4ba24e7ea48f9036dd73982eedd9b332d3dabfbf 100644
--- a/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
+++ b/briar-tests/src/org/briarproject/crypto/StreamDecrypterImplTest.java
@@ -1,37 +1,226 @@
 package org.briarproject.crypto;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.TestUtils;
+import org.briarproject.api.crypto.SecretKey;
+import org.briarproject.util.ByteUtils;
 import org.junit.Test;
 
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Random;
+
+import static junit.framework.Assert.assertEquals;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_PAYLOAD_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.STREAM_HEADER_IV_LENGTH;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.fail;
+
 public class StreamDecrypterImplTest extends BriarTestCase {
 
+	private final AuthenticatedCipher cipher;
+	private final SecretKey streamHeaderKey, frameKey;
+	private final byte[] streamHeaderIv;
+	private final Random random;
+
+	public StreamDecrypterImplTest() {
+		cipher = new TestAuthenticatedCipher(); // Null cipher
+		streamHeaderKey = TestUtils.createSecretKey();
+		frameKey = TestUtils.createSecretKey();
+		streamHeaderIv = new byte[STREAM_HEADER_IV_LENGTH];
+		random = new Random();
+		random.nextBytes(streamHeaderIv);
+	}
+
 	@Test
 	public void testReadValidFrames() throws Exception {
-		// FIXME
+		byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
+		int payloadLength = 123, paddingLength = 234;
+		FrameEncoder.encodeHeader(frameHeader, false, payloadLength,
+				paddingLength);
+		byte[] payload = new byte[payloadLength];
+		random.nextBytes(payload);
+
+		byte[] frameHeader1 = new byte[FRAME_HEADER_LENGTH];
+		int payloadLength1 = 345, paddingLength1 = 456;
+		FrameEncoder.encodeHeader(frameHeader1, true, payloadLength1,
+				paddingLength1);
+		byte[] payload1 = new byte[payloadLength1];
+		random.nextBytes(payload1);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(streamHeaderIv);
+		out.write(frameKey.getBytes());
+		out.write(new byte[MAC_LENGTH]);
+		out.write(frameHeader);
+		out.write(payload);
+		out.write(new byte[paddingLength]);
+		out.write(new byte[MAC_LENGTH]);
+		out.write(frameHeader1);
+		out.write(payload1);
+		out.write(new byte[paddingLength1]);
+		out.write(new byte[MAC_LENGTH]);
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		StreamDecrypterImpl s = new StreamDecrypterImpl(in, cipher,
+				streamHeaderKey);
+
+		// Read the first frame
+		byte[] buffer = new byte[MAX_PAYLOAD_LENGTH];
+		assertEquals(payloadLength, s.readFrame(buffer));
+		assertArrayStartsWith(payload, buffer, payloadLength);
+
+		// Read the second frame
+		assertEquals(payloadLength1, s.readFrame(buffer));
+		assertArrayStartsWith(payload1, buffer, payloadLength1);
+
+		// End of stream
+		assertEquals(-1, s.readFrame(buffer));
 	}
 
 	@Test
 	public void testTruncatedFrameThrowsException() throws Exception {
-		// FIXME
-	}
+		byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
+		int payloadLength = 123, paddingLength = 234;
+		FrameEncoder.encodeHeader(frameHeader, false, payloadLength,
+				paddingLength);
+		byte[] payload = new byte[payloadLength];
+		random.nextBytes(payload);
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(streamHeaderIv);
+		out.write(frameKey.getBytes());
+		out.write(new byte[MAC_LENGTH]);
+		out.write(frameHeader);
+		out.write(payload);
+		out.write(new byte[paddingLength]);
+		out.write(new byte[MAC_LENGTH - 1]); // Chop off the last byte
 
-	@Test
-	public void testModifiedFrameThrowsException() throws Exception {
-		// FIXME
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		StreamDecrypterImpl s = new StreamDecrypterImpl(in, cipher,
+				streamHeaderKey);
+
+		// Try to read the truncated frame
+		byte[] buffer = new byte[MAX_PAYLOAD_LENGTH];
+		try {
+			s.readFrame(buffer);
+			fail();
+		} catch (IOException expected) {
+			// Expected
+		}
 	}
 
 	@Test
-	public void testInvalidPayloadLengthThrowsException() throws Exception {
-		// FIXME
+	public void testInvalidPayloadAndPaddingLengthThrowsException()
+			throws Exception {
+		byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
+		// The payload length plus padding length is invalid
+		int payloadLength = MAX_PAYLOAD_LENGTH - 1, paddingLength = 2;
+		ByteUtils.writeUint16(payloadLength, frameHeader, 0);
+		ByteUtils.writeUint16(paddingLength, frameHeader, 2);
+		byte[] payload = new byte[payloadLength];
+		random.nextBytes(payload);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(streamHeaderIv);
+		out.write(frameKey.getBytes());
+		out.write(new byte[MAC_LENGTH]);
+		out.write(frameHeader);
+		out.write(payload);
+		out.write(new byte[paddingLength]);
+		out.write(new byte[MAC_LENGTH]);
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		StreamDecrypterImpl s = new StreamDecrypterImpl(in, cipher,
+				streamHeaderKey);
+
+		// Try to read the invalid frame
+		byte[] buffer = new byte[MAX_PAYLOAD_LENGTH];
+		try {
+			s.readFrame(buffer);
+			fail();
+		} catch (IOException expected) {
+			// Expected
+		}
 	}
 
 	@Test
 	public void testNonZeroPaddingThrowsException() throws Exception {
-		// FIXME
+		byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
+		int payloadLength = 123, paddingLength = 234;
+		FrameEncoder.encodeHeader(frameHeader, false, payloadLength,
+				paddingLength);
+		byte[] payload = new byte[payloadLength];
+		random.nextBytes(payload);
+		// Set one of the padding bytes non-zero
+		byte[] padding = new byte[paddingLength];
+		padding[paddingLength - 1] = 1;
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(streamHeaderIv);
+		out.write(frameKey.getBytes());
+		out.write(new byte[MAC_LENGTH]);
+		out.write(frameHeader);
+		out.write(payload);
+		out.write(padding);
+		out.write(new byte[MAC_LENGTH]);
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		StreamDecrypterImpl s = new StreamDecrypterImpl(in, cipher,
+				streamHeaderKey);
+
+		// Try to read the invalid frame
+		byte[] buffer = new byte[MAX_PAYLOAD_LENGTH];
+		try {
+			s.readFrame(buffer);
+			fail();
+		} catch (IOException expected) {
+			// Expected
+		}
 	}
 
 	@Test
 	public void testCannotReadBeyondFinalFrame() throws Exception {
-		// FIXME
+		byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
+		int payloadLength = 123, paddingLength = 234;
+		FrameEncoder.encodeHeader(frameHeader, true, payloadLength,
+				paddingLength);
+		byte[] payload = new byte[payloadLength];
+		random.nextBytes(payload);
+
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(streamHeaderIv);
+		out.write(frameKey.getBytes());
+		out.write(new byte[MAC_LENGTH]);
+		out.write(frameHeader);
+		out.write(payload);
+		out.write(new byte[paddingLength]);
+		out.write(new byte[MAC_LENGTH]);
+		// Add some data beyond the final frame
+		out.write(new byte[1024]);
+
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		StreamDecrypterImpl s = new StreamDecrypterImpl(in, cipher,
+				streamHeaderKey);
+
+		// Read the first frame
+		byte[] buffer = new byte[MAX_PAYLOAD_LENGTH];
+		assertEquals(payloadLength, s.readFrame(buffer));
+		assertArrayStartsWith(payload, buffer, payloadLength);
+
+		// End of stream
+		assertEquals(-1, s.readFrame(buffer));
+
+		// Yup, definitely end of stream
+		assertEquals(-1, s.readFrame(buffer));
+	}
+
+	private static void assertArrayStartsWith(byte[] expected, byte[] actual,
+			int len) {
+		byte[] prefix = new byte[len];
+		System.arraycopy(actual, 0, prefix, 0, len);
+		assertArrayEquals(expected, prefix);
 	}
 }
diff --git a/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java b/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
index 947c4bee1287599b6f321e2c64d7eb9d8ec4b4ce..d67980a1cdb84ec8322988b346cd96a86944c10b 100644
--- a/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
+++ b/briar-tests/src/org/briarproject/crypto/StreamEncrypterImplTest.java
@@ -1,261 +1,345 @@
 package org.briarproject.crypto;
 
 import org.briarproject.BriarTestCase;
+import org.briarproject.TestUtils;
 import org.briarproject.api.crypto.SecretKey;
 import org.junit.Test;
 
 import java.io.ByteArrayOutputStream;
 import java.util.Random;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.STREAM_HEADER_IV_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
-import static org.junit.Assert.assertEquals;
 
 public class StreamEncrypterImplTest extends BriarTestCase {
 
-	private final AuthenticatedCipher frameCipher;
-	private final SecretKey frameKey;
-	private final byte[] tag;
+	private final AuthenticatedCipher cipher;
+	private final SecretKey streamHeaderKey, frameKey;
+	private final byte[] tag, streamHeaderIv;
+	private final Random random;
 
 	public StreamEncrypterImplTest() {
-		frameCipher = new TestAuthenticatedCipher();
-		frameKey = new SecretKey(new byte[32]);
+		cipher = new TestAuthenticatedCipher(); // Null cipher
+		streamHeaderKey = TestUtils.createSecretKey();
+		frameKey = TestUtils.createSecretKey();
 		tag = new byte[TAG_LENGTH];
-		new Random().nextBytes(tag);
+		streamHeaderIv = new byte[STREAM_HEADER_IV_LENGTH];
+		random = new Random();
+		random.nextBytes(tag);
+		random.nextBytes(streamHeaderIv);
 	}
 
 	@Test
 	public void testWriteUnpaddedNonFinalFrameWithTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, tag);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		random.nextBytes(payload);
 
 		s.writeFrame(payload, payloadLength, 0, false);
 
-		byte[] header = new byte[HEADER_LENGTH];
-		FrameEncoder.encodeHeader(header, false, payloadLength, 0);
-		int frameLength = HEADER_LENGTH + payloadLength + MAC_LENGTH;
-		byte[] expected = new byte[TAG_LENGTH + frameLength];
-		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());
+		// Expect the tag, stream header, frame header, payload and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, false, payloadLength, 0);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWriteUnpaddedFinalFrameWithTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, tag);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123;
-		int frameLength = HEADER_LENGTH + payloadLength + MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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 + frameLength];
-		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());
+		// Expect the tag, stream header, frame header, payload and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, true, payloadLength, 0);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWriteUnpaddedNonFinalFrameWithoutTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, null);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, null,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123;
-		int frameLength = HEADER_LENGTH + payloadLength + MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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[frameLength];
-		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
-		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
-		assertArrayEquals(expected, out.toByteArray());
+		// Expect the stream header, frame header, payload and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, false, payloadLength, 0);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWriteUnpaddedFinalFrameWithoutTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, null);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, null,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123;
-		int frameLength = HEADER_LENGTH + payloadLength + MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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[frameLength];
-		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
-		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
-		assertArrayEquals(expected, out.toByteArray());
+		// Expect the stream header, frame header, payload and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, true, payloadLength, 0);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWritePaddedNonFinalFrameWithTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, tag);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123, paddingLength = 234;
-		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
-				+ MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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 + frameLength];
-		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());
+		// Expect the tag, stream header, frame header, payload, padding and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, false, payloadLength,
+				paddingLength);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[paddingLength]);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWritePaddedFinalFrameWithTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, tag);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123, paddingLength = 234;
-		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
-				+ MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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 + frameLength];
-		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());
+		// Expect the tag, stream header, frame header, payload, padding and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, true, payloadLength,
+				paddingLength);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[paddingLength]);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWritePaddedNonFinalFrameWithoutTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, null);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, null,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123, paddingLength = 234;
-		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
-				+ MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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[frameLength];
-		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
-		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
-		assertArrayEquals(expected, out.toByteArray());
+		// Expect the stream header, frame header, payload, padding and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, false, payloadLength,
+				paddingLength);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[paddingLength]);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testWritePaddedFinalFrameWithoutTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, null);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, null,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123, paddingLength = 234;
-		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
-				+ MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		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[frameLength];
-		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
-		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
-		assertArrayEquals(expected, out.toByteArray());
+		// Expect the stream header, frame header, payload, padding and MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, true, payloadLength,
+				paddingLength);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[paddingLength]);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
-	public void testWriteTwoFrames() throws Exception {
+	public void testWriteTwoFramesWithTag() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, null);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
 		int payloadLength = 123, paddingLength = 234;
-		int frameLength = HEADER_LENGTH + payloadLength + paddingLength
-				+ MAC_LENGTH;
 		byte[] payload = new byte[payloadLength];
-		new Random().nextBytes(payload);
+		random.nextBytes(payload);
 		int payloadLength1 = 345, paddingLength1 = 456;
-		int frameLength1 = HEADER_LENGTH + payloadLength1 + paddingLength1
-				+ MAC_LENGTH;
 		byte[] payload1 = new byte[payloadLength1];
-		new Random().nextBytes(payload1);
+		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,
+		// Expect the tag, stream header, first frame header, payload, padding,
+		// MAC, second frame header, payload, padding, MAC
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader, false, payloadLength,
+				paddingLength);
+		expected.write(expectedFrameHeader);
+		expected.write(payload);
+		expected.write(new byte[paddingLength]);
+		expected.write(new byte[MAC_LENGTH]);
+		byte[] expectedFrameHeader1 = new byte[FRAME_HEADER_LENGTH];
+		FrameEncoder.encodeHeader(expectedFrameHeader1, true, payloadLength1,
 				paddingLength1);
-		byte[] expected = new byte[frameLength + frameLength1];
-		System.arraycopy(header, 0, expected, 0, HEADER_LENGTH);
-		System.arraycopy(payload, 0, expected, HEADER_LENGTH, payloadLength);
-		System.arraycopy(header1, 0, expected, frameLength, HEADER_LENGTH);
-		System.arraycopy(payload1, 0, expected, frameLength + HEADER_LENGTH,
-				payloadLength1);
-		assertArrayEquals(expected, out.toByteArray());
+		expected.write(expectedFrameHeader1);
+		expected.write(payload1);
+		expected.write(new byte[paddingLength1]);
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
-	public void testFlushWritesTagIfNotAlreadyWritten() throws Exception {
+	public void testFlushWritesTagAndStreamHeaderIfNotAlreadyWritten()
+			throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, tag);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
+
+		// Flush the stream once
 		s.flush();
-		assertArrayEquals(tag, out.toByteArray());
+
+		// Expect the tag and stream header
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
-	public void testFlushDoesNotWriteTagIfAlreadyWritten() throws Exception {
+	public void testFlushDoesNotWriteTagOrStreamHeaderIfAlreadyWritten()
+			throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, tag);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, tag,
+				streamHeaderIv, streamHeaderKey, frameKey);
+
+		// Flush the stream twice
 		s.flush();
 		s.flush();
-		assertArrayEquals(tag, out.toByteArray());
+
+		// Expect the tag and stream header
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(tag);
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 
 	@Test
 	public void testFlushDoesNotWriteTagIfNull() throws Exception {
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		StreamEncrypterImpl s = new StreamEncrypterImpl(out, frameCipher,
-				frameKey, null);
+		StreamEncrypterImpl s = new StreamEncrypterImpl(out, cipher, null,
+				streamHeaderIv, streamHeaderKey, frameKey);
+
+		// Flush the stream once
 		s.flush();
-		assertEquals(0, out.size());
+
+		// Expect the stream header
+		ByteArrayOutputStream expected = new ByteArrayOutputStream();
+		expected.write(streamHeaderIv);
+		expected.write(frameKey.getBytes());
+		expected.write(new byte[MAC_LENGTH]);
+
+		assertArrayEquals(expected.toByteArray(), out.toByteArray());
 	}
 }
diff --git a/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java b/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java
index 9d06e52a4f1389abbc556dd6c1c08e297dd384a5..f95e46740914903240e89f439acaa78869880a5b 100644
--- a/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java
+++ b/briar-tests/src/org/briarproject/transport/TestStreamDecrypter.java
@@ -1,6 +1,5 @@
 package org.briarproject.transport;
 
-import org.briarproject.api.FormatException;
 import org.briarproject.api.crypto.StreamDecrypter;
 import org.briarproject.util.ByteUtils;
 
@@ -8,38 +7,55 @@ import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_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.STREAM_HEADER_LENGTH;
 
 class TestStreamDecrypter implements StreamDecrypter {
 
 	private final InputStream in;
 	private final byte[] frame;
 
+	private boolean readStreamHeader = true, finalFrame = false;
+
 	TestStreamDecrypter(InputStream in) {
 		this.in = in;
 		frame = new byte[MAX_FRAME_LENGTH];
 	}
 
 	public int readFrame(byte[] payload) throws IOException {
+		if (finalFrame) return -1;
+		if (readStreamHeader) readStreamHeader();
 		int offset = 0;
-		while (offset < HEADER_LENGTH) {
-			int read = in.read(frame, offset, HEADER_LENGTH - offset);
+		while (offset < FRAME_HEADER_LENGTH) {
+			int read = in.read(frame, offset, FRAME_HEADER_LENGTH - offset);
 			if (read == -1) throw new EOFException();
 			offset += read;
 		}
-		boolean finalFrame = (frame[0] & 0x80) == 0x80;
+		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;
+		int paddingLength = ByteUtils.readUint16(frame, 2);
+		int frameLength = FRAME_HEADER_LENGTH + payloadLength + paddingLength
+				+ MAC_LENGTH;
+		while (offset < frameLength) {
+			int read = in.read(frame, offset, frameLength - offset);
+			if (read == -1) throw new EOFException();
 			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);
+		System.arraycopy(frame, FRAME_HEADER_LENGTH, payload, 0, payloadLength);
 		return payloadLength;
 	}
+
+	private void readStreamHeader() throws IOException {
+		byte[] streamHeader = new byte[STREAM_HEADER_LENGTH];
+		int offset = 0;
+		while (offset < STREAM_HEADER_LENGTH) {
+			int read = in.read(streamHeader, offset,
+					STREAM_HEADER_LENGTH - offset);
+			if (read == -1) throw new EOFException();
+			offset += read;
+		}
+		readStreamHeader = false;
+	}
 }
diff --git a/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java b/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java
index 6aa4c806843ff51067beef93e0b7aadfcdb5dfb6..2315acfa94ab20f5ba17caa6950eaf47fcf18ac2 100644
--- a/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java
+++ b/briar-tests/src/org/briarproject/transport/TestStreamEncrypter.java
@@ -6,40 +6,43 @@ import org.briarproject.util.ByteUtils;
 import java.io.IOException;
 import java.io.OutputStream;
 
-import static org.briarproject.api.transport.TransportConstants.HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_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.STREAM_HEADER_LENGTH;
 
 class TestStreamEncrypter implements StreamEncrypter {
 
 	private final OutputStream out;
-	private final byte[] tag, frame;
+	private final byte[] tag;
 
-	private boolean writeTag = true;
+	private boolean writeTagAndHeader = 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);
+		if (writeTagAndHeader) writeTagAndHeader();
+		byte[] frameHeader = new byte[FRAME_HEADER_LENGTH];
+		ByteUtils.writeUint16(payloadLength, frameHeader, 0);
+		ByteUtils.writeUint16(paddingLength, frameHeader, 2);
+		if (finalFrame) frameHeader[0] |= 0x80;
+		out.write(frameHeader);
+		out.write(payload, 0, payloadLength);
+		out.write(new byte[paddingLength]);
+		out.write(new byte[MAC_LENGTH]);
 	}
 
 	public void flush() throws IOException {
+		if (writeTagAndHeader) writeTagAndHeader();
 		out.flush();
 	}
+
+	private void writeTagAndHeader() throws IOException {
+		out.write(tag);
+		out.write(new byte[STREAM_HEADER_LENGTH]);
+		writeTagAndHeader = false;
+	}
 }
diff --git a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
index b3739706114da8ebe97ae0946b0ff48cfebf60d1..24e401a537815ac4856992b33d0d9e6b4cfa4802 100644
--- a/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
+++ b/briar-tests/src/org/briarproject/transport/TransportIntegrationTest.java
@@ -12,30 +12,19 @@ import java.io.InputStream;
 import java.io.OutputStream;
 import java.util.Random;
 
-import static org.briarproject.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAC_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.STREAM_HEADER_LENGTH;
 import static org.briarproject.api.transport.TransportConstants.TAG_LENGTH;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 
 public class TransportIntegrationTest extends BriarTestCase {
 
-	private final Random random;
-
-	public TransportIntegrationTest() {
-		random = new Random();
-	}
+	private final Random random = new Random();
 
 	@Test
-	public void testInitiatorWriteAndRead() throws Exception {
-		testWriteAndRead(true);
-	}
-
-	@Test
-	public void testResponderWriteAndRead() throws Exception {
-		testWriteAndRead(false);
-	}
-
-	private void testWriteAndRead(boolean initiator) throws Exception {
+	public void testWriteAndRead() throws Exception {
 		// Generate a random tag
 		byte[] tag = new byte[TAG_LENGTH];
 		random.nextBytes(tag);
@@ -53,7 +42,10 @@ public class TransportIntegrationTest extends BriarTestCase {
 		streamWriter.write(payload2);
 		streamWriter.flush();
 		byte[] output = out.toByteArray();
-		assertEquals(TAG_LENGTH + MAX_FRAME_LENGTH * 2, output.length);
+		assertEquals(TAG_LENGTH + STREAM_HEADER_LENGTH
+				+ FRAME_HEADER_LENGTH  + payload1.length + MAC_LENGTH
+				+ FRAME_HEADER_LENGTH  + payload2.length + MAC_LENGTH,
+				output.length);
 		// Read the tag back
 		ByteArrayInputStream in = new ByteArrayInputStream(output);
 		byte[] recoveredTag = new byte[tag.length];