diff --git a/api/net/sf/briar/api/serial/FormatException.java b/api/net/sf/briar/api/FormatException.java
similarity index 82%
rename from api/net/sf/briar/api/serial/FormatException.java
rename to api/net/sf/briar/api/FormatException.java
index 784f44e30685166f4a7202e17fb4be39419c9fc9..b48f8ba51f08f7b38f0d229dbf01c16e4635db9d 100644
--- a/api/net/sf/briar/api/serial/FormatException.java
+++ b/api/net/sf/briar/api/FormatException.java
@@ -1,4 +1,4 @@
-package net.sf.briar.api.serial;
+package net.sf.briar.api;
 
 import java.io.IOException;
 
diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java
index 373daea801c4430184d185c2c74a47dfd45e4048..504ed97055b130202be2d8a65c781512757276ad 100644
--- a/api/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/api/net/sf/briar/api/crypto/CryptoComponent.java
@@ -12,13 +12,13 @@ public interface CryptoComponent {
 
 	SecretKey deriveIncomingMacKey(byte[] secret);
 
-	SecretKey deriveIncomingPacketKey(byte[] secret);
+	SecretKey deriveIncomingFrameKey(byte[] secret);
 
 	SecretKey deriveIncomingTagKey(byte[] secret);
 
 	SecretKey deriveOutgoingMacKey(byte[] secret);
 
-	SecretKey deriveOutgoingPacketKey(byte[] secret);
+	SecretKey deriveOutgoingFrameKey(byte[] secret);
 
 	SecretKey deriveOutgoingTagKey(byte[] secret);
 
@@ -32,7 +32,7 @@ public interface CryptoComponent {
 
 	MessageDigest getMessageDigest();
 
-	Cipher getPacketCipher();
+	Cipher getFrameCipher();
 
 	Signature getSignature();
 
diff --git a/api/net/sf/briar/api/transport/ConnectionReader.java b/api/net/sf/briar/api/transport/ConnectionReader.java
new file mode 100644
index 0000000000000000000000000000000000000000..cac3fbe40c825bd368c1b2aa1ea7c63fcab029c4
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionReader.java
@@ -0,0 +1,13 @@
+package net.sf.briar.api.transport;
+
+import java.io.InputStream;
+
+/** Decrypts and authenticates data received over a connection. */
+public interface ConnectionReader {
+
+	/**
+	 * Returns an input stream from which the decrypted, authenticated data can
+	 * be read.
+	 */
+	InputStream getInputStream();
+}
diff --git a/api/net/sf/briar/api/transport/ConnectionReaderFactory.java b/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..536123f48e6cf08ab3efdd89b428dc420f85ac00
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionReaderFactory.java
@@ -0,0 +1,9 @@
+package net.sf.briar.api.transport;
+
+import java.io.InputStream;
+
+public interface ConnectionReaderFactory {
+
+	ConnectionReader createConnectionReader(InputStream in, int transportId,
+			long connection, byte[] secret);
+}
diff --git a/api/net/sf/briar/api/transport/ConnectionWriter.java b/api/net/sf/briar/api/transport/ConnectionWriter.java
new file mode 100644
index 0000000000000000000000000000000000000000..c7ed2038acf5e73f3c12356f6e72bcb89aa1e79d
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionWriter.java
@@ -0,0 +1,13 @@
+package net.sf.briar.api.transport;
+
+import java.io.OutputStream;
+
+/** Encrypts and authenticates data to be sent over a connection. */
+public interface ConnectionWriter {
+
+	/**
+	 * Returns an output stream to which unencrypted, unauthenticated data can
+	 * be written.
+	 */
+	OutputStream getOutputStream();
+}
diff --git a/api/net/sf/briar/api/transport/ConnectionWriterFactory.java b/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
new file mode 100644
index 0000000000000000000000000000000000000000..737096bfe88ad4f629d9d9e5e704402f75926f5c
--- /dev/null
+++ b/api/net/sf/briar/api/transport/ConnectionWriterFactory.java
@@ -0,0 +1,9 @@
+package net.sf.briar.api.transport;
+
+import java.io.OutputStream;
+
+public interface ConnectionWriterFactory {
+
+	ConnectionWriter createConnectionWriter(OutputStream out, int transportId,
+			long connection, byte[] secret);
+}
diff --git a/api/net/sf/briar/api/transport/PacketReader.java b/api/net/sf/briar/api/transport/PacketReader.java
deleted file mode 100644
index 683f3cea087276516954fb4782ee2b555c056119..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/PacketReader.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.sf.briar.api.transport;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-
-/**
- * Reads encrypted packets from an underlying input stream, decrypts and
- * authenticates them.
- */
-public interface PacketReader {
-
-	/**
-	 * Returns the input stream from which packets should be read. (Note that
-	 * this is not the underlying input stream.)
-	 */
-	InputStream getInputStream();
-
-	/**
-	 * Finishes reading the current packet (if any), authenticates the packet
-	 * and prepares to read the next packet. If this method is called twice in
-	 * succession without any intervening reads, the underlying input stream
-	 * will be unaffected.
-	 */
-	void finishPacket() throws IOException, GeneralSecurityException;
-}
diff --git a/api/net/sf/briar/api/transport/PacketReaderFactory.java b/api/net/sf/briar/api/transport/PacketReaderFactory.java
deleted file mode 100644
index 8e2ec5b3b651c03ac30de1c1c1ae2cbd3216de91..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/PacketReaderFactory.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.sf.briar.api.transport;
-
-import java.io.InputStream;
-
-public interface PacketReaderFactory {
-
-	PacketReader createPacketReader(byte[] firstTag, InputStream in,
-			int transportId, long connection, byte[] secret);
-}
diff --git a/api/net/sf/briar/api/transport/PacketWriter.java b/api/net/sf/briar/api/transport/PacketWriter.java
deleted file mode 100644
index bc4d92a0a682bdb1a0e1677fd5c9de9916ace839..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/PacketWriter.java
+++ /dev/null
@@ -1,24 +0,0 @@
-package net.sf.briar.api.transport;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-/**
- * A filter that adds tags and MACs to outgoing packets, encrypts them and
- * writes them to the underlying output stream.
- */
-public interface PacketWriter {
-
-	/**
-	 * Returns the output stream to which packets should be written. (Note that
-	 * this is not the underlying output stream.)
-	 */
-	OutputStream getOutputStream();
-
-	/**
-	 * Finishes writing the current packet (if any) and prepares to write the
-	 * next packet. If this method is called twice in succession without any
-	 * intervening writes, the underlying output stream will be unaffected.
-	 */
-	void finishPacket() throws IOException;
-}
diff --git a/api/net/sf/briar/api/transport/PacketWriterFactory.java b/api/net/sf/briar/api/transport/PacketWriterFactory.java
deleted file mode 100644
index c93e41e7567068d258d4a7b7ea0e97d6dafeea1c..0000000000000000000000000000000000000000
--- a/api/net/sf/briar/api/transport/PacketWriterFactory.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package net.sf.briar.api.transport;
-
-import java.io.OutputStream;
-
-public interface PacketWriterFactory {
-
-	PacketWriter createPacketWriter(OutputStream out, int transportId,
-			long connection, byte[] secret);
-}
diff --git a/api/net/sf/briar/api/transport/TransportConstants.java b/api/net/sf/briar/api/transport/TransportConstants.java
index 3b8503f5663d63d6721ce3a381e79451db5b28ae..2746a8c0dfd9e6e1f8cea917928eebf8a8239da5 100644
--- a/api/net/sf/briar/api/transport/TransportConstants.java
+++ b/api/net/sf/briar/api/transport/TransportConstants.java
@@ -9,14 +9,4 @@ public interface TransportConstants {
 
 	/** The length in bytes of the tag that uniquely identifies a connection. */
 	static final int TAG_LENGTH = 16;
-
-	/**
-	 * The maximum value that can be represented as an unsigned 16-bit integer.
-	 */
-	static final int MAX_16_BIT_UNSIGNED = 65535; // 2^16 - 1
-
-	/**
-	 * The maximum value that can be represented as an unsigned 32-bit integer.
-	 */
-	static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1
 }
diff --git a/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java b/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java
index db2ff72c0bd8167790d7be9ac95386273cbafa91..bd810f8e61110414a38bc406a0aba4f1a06f7d24 100644
--- a/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java
+++ b/api/net/sf/briar/api/transport/batch/BatchTransportPlugin.java
@@ -22,8 +22,8 @@ public interface BatchTransportPlugin {
 	 * Starts the plugin. Any connections that are later initiated by contacts
 	 * or established through polling will be passed to the given callback.
 	 */
-	void start(Map<String, String> localTransports,
-			Map<ContactId, Map<String, String>> remoteTransports,
+	void start(Map<String, String> localProperties,
+			Map<ContactId, Map<String, String>> remoteProperties,
 			Map<String, String> config, BatchTransportCallback c)
 	throws InvalidTransportException, InvalidConfigException;
 
@@ -34,11 +34,11 @@ public interface BatchTransportPlugin {
 	void stop();
 
 	/** Updates the plugin's local transport properties. */
-	void setLocalTransports(Map<String, String> transports)
+	void setLocalProperties(Map<String, String> properties)
 	throws InvalidTransportException;
 
 	/** Updates the plugin's transport properties for the given contact. */
-	void setRemoteTransports(ContactId c, Map<String, String> transports)
+	void setRemoteProperties(ContactId c, Map<String, String> properties)
 	throws InvalidTransportException;
 
 	/** Updates the plugin's configuration properties. */
diff --git a/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java b/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java
index 67e04a204dff6d31a7619d3e3d285acac79ac930..30c35047820f4cfc25d66ad2203907838cce967d 100644
--- a/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java
+++ b/api/net/sf/briar/api/transport/stream/StreamTransportPlugin.java
@@ -22,8 +22,8 @@ public interface StreamTransportPlugin {
 	 * Starts the plugin. Any connections that are later initiated by contacts
 	 * or established through polling will be passed to the given callback.
 	 */
-	void start(Map<String, String> localTransports,
-			Map<ContactId, Map<String, String>> remoteTransports,
+	void start(Map<String, String> localProperties,
+			Map<ContactId, Map<String, String>> remoteProperties,
 			Map<String, String> config, StreamTransportCallback c)
 	throws InvalidTransportException, InvalidConfigException;
 
@@ -34,11 +34,11 @@ public interface StreamTransportPlugin {
 	void stop();
 
 	/** Updates the plugin's local transport properties. */
-	void setLocalTransports(Map<String, String> transports)
+	void setLocalProperties(Map<String, String> properties)
 	throws InvalidTransportException;
 
 	/** Updates the plugin's transport properties for the given contact. */
-	void setRemoteTransports(ContactId c, Map<String, String> transports)
+	void setRemoteProperties(ContactId c, Map<String, String> properties)
 	throws InvalidTransportException;
 
 	/** Updates the plugin's configuration properties. */
diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java
index 366c37fd54949a7f8220f5a2545030f9c9c4e682..f8816a1859a95bf1f4ffc977fa44b2ea9992d176 100644
--- a/components/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -114,12 +114,12 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
-	public SecretKey deriveIncomingPacketKey(byte[] secret) {
+	public SecretKey deriveIncomingFrameKey(byte[] secret) {
 		SharedSecret s = new SharedSecret(secret);
-		return derivePacketKey(s, !s.getAlice());
+		return deriveFrameKey(s, !s.getAlice());
 	}
 
-	private SecretKey derivePacketKey(SharedSecret s, boolean alice) {
+	private SecretKey deriveFrameKey(SharedSecret s, boolean alice) {
 		if(alice) return deriveKey("PKTA", s.getIv(), s.getCiphertext());
 		else return deriveKey("PKTB", s.getIv(), s.getCiphertext());
 	}
@@ -139,9 +139,9 @@ class CryptoComponentImpl implements CryptoComponent {
 		return deriveMacKey(s, s.getAlice());
 	}
 
-	public SecretKey deriveOutgoingPacketKey(byte[] secret) {
+	public SecretKey deriveOutgoingFrameKey(byte[] secret) {
 		SharedSecret s = new SharedSecret(secret);
-		return derivePacketKey(s, s.getAlice());
+		return deriveFrameKey(s, s.getAlice());
 	}
 
 	public SecretKey deriveOutgoingTagKey(byte[] secret) {
@@ -181,7 +181,7 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
-	public Cipher getPacketCipher() {
+	public Cipher getFrameCipher() {
 		try {
 			return Cipher.getInstance(PACKET_CIPHER_ALGO, PROVIDER);
 		} catch(NoSuchAlgorithmException e) {
diff --git a/components/net/sf/briar/protocol/BatchIdReader.java b/components/net/sf/briar/protocol/BatchIdReader.java
index 4542a0f5ecf4d7d033f235119e53df9f05d840c9..21807eb475c6a12ce34af6619aa60f7093a39e0d 100644
--- a/components/net/sf/briar/protocol/BatchIdReader.java
+++ b/components/net/sf/briar/protocol/BatchIdReader.java
@@ -2,10 +2,10 @@ package net.sf.briar.protocol;
 
 import java.io.IOException;
 
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 
diff --git a/components/net/sf/briar/protocol/CountingConsumer.java b/components/net/sf/briar/protocol/CountingConsumer.java
index c882ab0b2b87174781eb44e7f74d2f5d97bbd00c..bb032341e06040e2bbb7996f5d060671cf0a41d5 100644
--- a/components/net/sf/briar/protocol/CountingConsumer.java
+++ b/components/net/sf/briar/protocol/CountingConsumer.java
@@ -2,8 +2,8 @@ package net.sf.briar.protocol;
 
 import java.io.IOException;
 
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.FormatException;
 
 /**
  * A consumer that counts the number of bytes consumed and throws a
diff --git a/components/net/sf/briar/protocol/GroupIdReader.java b/components/net/sf/briar/protocol/GroupIdReader.java
index 07bf2233f138ea11768d0016c3a79eede87dfac8..bf9fd81b1eb1f43e0aff54d0b6c6e6ec9c608298 100644
--- a/components/net/sf/briar/protocol/GroupIdReader.java
+++ b/components/net/sf/briar/protocol/GroupIdReader.java
@@ -2,10 +2,10 @@ package net.sf.briar.protocol;
 
 import java.io.IOException;
 
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 
diff --git a/components/net/sf/briar/protocol/MessageEncoderImpl.java b/components/net/sf/briar/protocol/MessageEncoderImpl.java
index 5c2149ad5c349172b33bd171fe38fb2382dcd0b2..8e430f1a57ff71c01eb3c25b219eb53314df9d67 100644
--- a/components/net/sf/briar/protocol/MessageEncoderImpl.java
+++ b/components/net/sf/briar/protocol/MessageEncoderImpl.java
@@ -58,6 +58,8 @@ class MessageEncoderImpl implements MessageEncoder {
 			throw new IllegalArgumentException();
 		if((group.getPublicKey() == null) != (groupKey == null))
 			throw new IllegalArgumentException();
+		if(body.length > Message.MAX_BODY_LENGTH)
+			throw new IllegalArgumentException();
 
 		long timestamp = System.currentTimeMillis();
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
diff --git a/components/net/sf/briar/protocol/MessageIdReader.java b/components/net/sf/briar/protocol/MessageIdReader.java
index eecf32649fa0f5469768bc34e814c4bce4e4a5c2..32e4e905ace77b21041917ea35d1db9a37985731 100644
--- a/components/net/sf/briar/protocol/MessageIdReader.java
+++ b/components/net/sf/briar/protocol/MessageIdReader.java
@@ -2,10 +2,10 @@ package net.sf.briar.protocol;
 
 import java.io.IOException;
 
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 
diff --git a/components/net/sf/briar/protocol/MessageReader.java b/components/net/sf/briar/protocol/MessageReader.java
index e4c24078a67392612966d0d7d0530a55cf20a370..1ee31dd152282f26dad75d10b1c81f770125a22a 100644
--- a/components/net/sf/briar/protocol/MessageReader.java
+++ b/components/net/sf/briar/protocol/MessageReader.java
@@ -6,6 +6,7 @@ import java.security.MessageDigest;
 import java.security.PublicKey;
 import java.security.Signature;
 
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.KeyParser;
 import net.sf.briar.api.protocol.Author;
@@ -14,7 +15,6 @@ import net.sf.briar.api.protocol.Group;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.Tags;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 
diff --git a/components/net/sf/briar/protocol/OfferIdReader.java b/components/net/sf/briar/protocol/OfferIdReader.java
index 52485760cfbb327948b0a6057ed824b15f6f1054..3c1fe80aad2d418ab8aedc4ba406f19e3f6f51e4 100644
--- a/components/net/sf/briar/protocol/OfferIdReader.java
+++ b/components/net/sf/briar/protocol/OfferIdReader.java
@@ -2,10 +2,10 @@ package net.sf.briar.protocol;
 
 import java.io.IOException;
 
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.OfferId;
 import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 
diff --git a/components/net/sf/briar/serial/ReaderImpl.java b/components/net/sf/briar/serial/ReaderImpl.java
index 63b9e4ef0c9796ae8567edd859a596e03d5dd081..ffc7af901e4a706f1ef0d593d0ea2186db7cce7e 100644
--- a/components/net/sf/briar/serial/ReaderImpl.java
+++ b/components/net/sf/briar/serial/ReaderImpl.java
@@ -9,8 +9,8 @@ import java.util.List;
 import java.util.Map;
 
 import net.sf.briar.api.Bytes;
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.Tag;
diff --git a/components/net/sf/briar/transport/ConnectionDecrypter.java b/components/net/sf/briar/transport/ConnectionDecrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..80bd798eeebe57fa3d0084634758b3ff2e277d8c
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionDecrypter.java
@@ -0,0 +1,14 @@
+package net.sf.briar.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/** Decrypts unauthenticated data received over a connection. */
+interface ConnectionDecrypter {
+
+	/** Returns an input stream from which decrypted data can be read. */
+	InputStream getInputStream();
+
+	/** Reads and decrypts the MAC for the current frame. */
+	void readMac(byte[] mac) throws IOException;
+}
diff --git a/components/net/sf/briar/transport/ConnectionDecrypterImpl.java b/components/net/sf/briar/transport/ConnectionDecrypterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..4ab3b618d76caf95b496475cad15fc084d119025
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionDecrypterImpl.java
@@ -0,0 +1,148 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+import javax.crypto.ShortBufferException;
+import javax.crypto.spec.IvParameterSpec;
+
+class ConnectionDecrypterImpl extends FilterInputStream
+implements ConnectionDecrypter {
+
+	private final int transportId;
+	private final long connection;
+	private final Cipher frameCipher;
+	private final SecretKey frameKey;
+	private final byte[] buf, tag;
+
+	private int bufOff = 0, bufLen = 0;
+	private long frame = 0L;
+	private boolean betweenFrames = true;
+
+	ConnectionDecrypterImpl(InputStream in, int transportId, long connection,
+			Cipher frameCipher, SecretKey frameKey) {
+		super(in);
+		this.transportId = transportId;
+		this.connection = connection;
+		this.frameCipher = frameCipher;
+		this.frameKey = frameKey;
+		buf = new byte[TAG_LENGTH];
+		tag = new byte[TAG_LENGTH];
+	}
+
+	public InputStream getInputStream() {
+		return this;
+	}
+
+	public void readMac(byte[] mac) throws IOException {
+		if(betweenFrames) throw new IllegalStateException();
+		// If we have any plaintext in the buffer, copy it into the MAC
+		System.arraycopy(buf, bufOff, mac, 0, bufLen);
+		// Read the remainder of the MAC
+		int offset = bufLen;
+		while(offset < mac.length) {
+			int read = in.read(mac, offset, mac.length - offset);
+			if(read == -1) break;
+			offset += read;
+		}
+		if(offset < mac.length) throw new EOFException(); // Unexpected EOF
+		// Decrypt the remainder of the MAC
+		try {
+			int length = mac.length - bufLen;
+			int i = frameCipher.doFinal(mac, bufLen, length, mac, bufLen);
+			if(i < length) throw new RuntimeException();
+		} catch(BadPaddingException badCipher) {
+			throw new RuntimeException(badCipher);
+		} catch(IllegalBlockSizeException badCipher) {
+			throw new RuntimeException(badCipher);
+		} catch(ShortBufferException badCipher) {
+			throw new RuntimeException(badCipher);
+		}
+		bufOff = bufLen = 0;
+		betweenFrames = true;
+	}
+
+	@Override
+	public int read() throws IOException {
+		if(betweenFrames) initialiseCipher();
+		if(bufLen == 0) {
+			if(!readBlock()) return -1;
+			bufOff = 0;
+			bufLen = buf.length;
+		}
+		int i = buf[bufOff];
+		bufOff++;
+		bufLen--;
+		return i < 0 ? i + 256 : i;
+	}
+
+	@Override
+	public int read(byte[] b) throws IOException {
+		return read(b, 0, b.length);
+	}
+
+	@Override
+	public int read(byte[] b, int off, int len) throws IOException {
+		if(betweenFrames) initialiseCipher();
+		if(bufLen == 0) {
+			if(!readBlock()) return -1;
+			bufOff = 0;
+			bufLen = buf.length;
+		}
+		int length = Math.min(len, bufLen);
+		System.arraycopy(buf, bufOff, b, off, length);
+		bufOff += length;
+		bufLen -= length;
+		return length;
+	}
+
+	// Although we're using CTR mode, which doesn't require full blocks of
+	// ciphertext, the cipher still tries to operate a block at a time
+	private boolean readBlock() throws IOException {
+		// Try to read a block of ciphertext
+		int offset = 0;
+		while(offset < buf.length) {
+			int read = in.read(buf, offset, buf.length - offset);
+			if(read == -1) break;
+			offset += read;
+		}
+		if(offset == 0) return false;
+		if(offset < buf.length) throw new EOFException(); // Unexpected EOF
+		// Decrypt the block
+		try {
+			int i = frameCipher.update(buf, 0, offset, buf);
+			if(i < offset) throw new RuntimeException();
+		} catch(ShortBufferException badCipher) {
+			throw new RuntimeException(badCipher);
+		}
+		return true;
+	}
+
+	private void initialiseCipher() {
+		assert betweenFrames;
+		if(frame > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
+		TagEncoder.encodeTag(tag, transportId, connection, frame);
+		// Use the plaintext tag to initialise the packet cipher
+		IvParameterSpec iv = new IvParameterSpec(tag);
+		try {
+			frameCipher.init(Cipher.DECRYPT_MODE, frameKey, iv);
+		} catch(InvalidAlgorithmParameterException badIv) {
+			throw new RuntimeException(badIv);
+		} catch(InvalidKeyException badKey) {
+			throw new RuntimeException(badKey);
+		}
+		frame++;
+		betweenFrames = false;
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionEncrypter.java b/components/net/sf/briar/transport/ConnectionEncrypter.java
new file mode 100644
index 0000000000000000000000000000000000000000..595307547b9a4c8be74e3a81d8b508829fba5dd2
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionEncrypter.java
@@ -0,0 +1,14 @@
+package net.sf.briar.transport;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+/** Encrypts authenticated data to be sent over a connection. */
+interface ConnectionEncrypter {
+
+	/** Returns an output stream to which unencrypted data can be written. */
+	OutputStream getOutputStream();
+
+	/** Encrypts and writes the MAC for the current frame. */
+	void writeMac(byte[] mac) throws IOException;
+}
diff --git a/components/net/sf/briar/transport/ConnectionEncrypterImpl.java b/components/net/sf/briar/transport/ConnectionEncrypterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..0fccc905f9cf23abd14f8c980078a31cd4d5eef8
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionEncrypterImpl.java
@@ -0,0 +1,117 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+class ConnectionEncrypterImpl extends FilterOutputStream
+implements ConnectionEncrypter {
+
+	private final int transportId;
+	private final long connection;
+	private final Cipher tagCipher, frameCipher;
+	private final SecretKey frameKey;
+	private final byte[] tag;
+
+	private long frame = 0L;
+	private boolean started = false, betweenFrames = false;
+
+	ConnectionEncrypterImpl(OutputStream out, int transportId,
+			long connection, Cipher tagCipher, Cipher frameCipher,
+			SecretKey tagKey, SecretKey frameKey) {
+		super(out);
+		this.transportId = transportId;
+		this.connection = connection;
+		this.tagCipher = tagCipher;
+		this.frameCipher = frameCipher;
+		this.frameKey = frameKey;
+		tag = new byte[TAG_LENGTH];
+		try {
+			tagCipher.init(Cipher.ENCRYPT_MODE, tagKey);
+		} catch(InvalidKeyException badKey) {
+			throw new IllegalArgumentException(badKey);
+		}
+		if(tagCipher.getOutputSize(TAG_LENGTH) != TAG_LENGTH)
+			throw new IllegalArgumentException();
+	}
+
+	public OutputStream getOutputStream() {
+		return this;
+	}
+
+	public void writeMac(byte[] mac) throws IOException {
+		if(!started || betweenFrames) throw new IllegalStateException();
+		try {
+			out.write(frameCipher.doFinal(mac));
+		} catch(BadPaddingException badCipher) {
+			throw new IOException(badCipher);
+		} catch(IllegalBlockSizeException badCipher) {
+			throw new RuntimeException(badCipher);
+		}
+		betweenFrames = true;
+	}
+
+	@Override
+	public void write(int b) throws IOException {
+		if(!started) writeTag();
+		if(betweenFrames) initialiseCipher();
+		byte[] ciphertext = frameCipher.update(new byte[] {(byte) b});
+		if(ciphertext != null) out.write(ciphertext);
+	}
+
+	@Override
+	public void write(byte[] b) throws IOException {
+		write(b, 0, b.length);
+	}
+
+	@Override
+	public void write(byte[] b, int off, int len) throws IOException {
+		if(!started) writeTag();
+		if(betweenFrames) initialiseCipher();
+		byte[] ciphertext = frameCipher.update(b, off, len);
+		if(ciphertext != null) out.write(ciphertext);
+	}
+
+	private void writeTag() throws IOException {
+		assert !started;
+		assert !betweenFrames;
+		TagEncoder.encodeTag(tag, transportId, connection, 0L);
+		try {
+			out.write(tagCipher.doFinal(tag));
+		} catch(BadPaddingException badCipher) {
+			throw new IOException(badCipher);
+		} catch(IllegalBlockSizeException badCipher) {
+			throw new RuntimeException(badCipher);
+		}
+		started = true;
+		betweenFrames = true;
+	}
+
+	private void initialiseCipher() {
+		assert started;
+		assert betweenFrames;
+		if(frame > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
+		TagEncoder.encodeTag(tag, transportId, connection, frame);
+		IvParameterSpec iv = new IvParameterSpec(tag);
+		try {
+			frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, iv);
+		} catch(InvalidAlgorithmParameterException badIv) {
+			throw new RuntimeException(badIv);
+		} catch(InvalidKeyException badKey) {
+			throw new RuntimeException(badKey);
+		}
+		frame++;
+		betweenFrames = false;
+	}
+}
\ No newline at end of file
diff --git a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..016e4fb43b15091f2333664610d66cf67ca736d0
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
@@ -0,0 +1,40 @@
+package net.sf.briar.transport;
+
+import java.io.InputStream;
+import java.security.InvalidKeyException;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.transport.ConnectionReader;
+import net.sf.briar.api.transport.ConnectionReaderFactory;
+
+import com.google.inject.Inject;
+
+class ConnectionReaderFactoryImpl implements ConnectionReaderFactory {
+
+	private final CryptoComponent crypto;
+
+	@Inject
+	ConnectionReaderFactoryImpl(CryptoComponent crypto) {
+		this.crypto = crypto;
+	}
+
+	public ConnectionReader createConnectionReader(InputStream in,
+			int transportId, long connection, byte[] secret) {
+		SecretKey macKey = crypto.deriveIncomingMacKey(secret);
+		SecretKey frameKey = crypto.deriveIncomingFrameKey(secret);
+		Cipher frameCipher = crypto.getFrameCipher();
+		Mac mac = crypto.getMac();
+		try {
+			mac.init(macKey);
+		} catch(InvalidKeyException e) {
+			throw new IllegalArgumentException(e);
+		}
+		ConnectionDecrypter decrypter = new ConnectionDecrypterImpl(in,
+				transportId, connection, frameCipher, frameKey);
+		return new ConnectionReaderImpl(decrypter, mac);
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionReaderImpl.java b/components/net/sf/briar/transport/ConnectionReaderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f3d54a9a15a680d85e46bf3fbef4ab2b316a3027
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionReaderImpl.java
@@ -0,0 +1,107 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
+import java.io.EOFException;
+import java.io.FilterInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+
+import net.sf.briar.api.FormatException;
+import net.sf.briar.api.transport.ConnectionReader;
+import net.sf.briar.util.ByteUtils;
+
+class ConnectionReaderImpl extends FilterInputStream
+implements ConnectionReader {
+
+	private final ConnectionDecrypter decrypter;
+	private final Mac mac;
+	private final int maxPayloadLength;
+	private final byte[] header, payload, footer;
+
+	private long frame = 0L;
+	private int payloadOff = 0, payloadLen = 0;
+	private boolean betweenFrames = true;
+
+	ConnectionReaderImpl(ConnectionDecrypter decrypter, Mac mac) {
+		super(decrypter.getInputStream());
+		this.decrypter = decrypter;
+		this.mac = mac;
+		maxPayloadLength = MAX_FRAME_LENGTH - 6 - mac.getMacLength();
+		header = new byte[6];
+		payload = new byte[maxPayloadLength];
+		footer = new byte[mac.getMacLength()];
+	}
+
+	public InputStream getInputStream() {
+		return this;
+	}
+
+	@Override
+	public int read() throws IOException {
+		if(betweenFrames && !readFrame()) return -1;
+		int i = payload[payloadOff];
+		payloadOff++;
+		payloadLen--;
+		if(payloadLen == 0) betweenFrames = true;
+		return i;
+	}
+
+	@Override
+	public int read(byte[] b) throws IOException {
+		return read(b, 0, b.length);
+	}
+
+	@Override
+	public int read(byte[] b, int off, int len) throws IOException {
+		if(betweenFrames && !readFrame()) return -1;
+		len = Math.min(len, payloadLen);
+		System.arraycopy(payload, payloadOff, b, off, len);
+		payloadOff += len;
+		payloadLen -= len;
+		if(payloadLen == 0) betweenFrames = true;
+		return len;
+	}
+
+	private boolean readFrame() throws IOException {
+		assert betweenFrames;
+		// Read the header
+		if(frame > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
+		int offset = 0;
+		while(offset < header.length) {
+			int read = in.read(header, offset, header.length - offset);
+			if(read == -1) break;
+			offset += read;
+		}
+		if(offset == 0) return false; // EOF between frames
+		if(offset < header.length) throw new EOFException(); // Unexpected EOF
+		mac.update(header);
+		// Check that the frame has the expected frame number
+		if(ByteUtils.readUint32(header, 0) != frame)
+			throw new FormatException();
+		// Check that the payload length is legal
+		payloadLen = ByteUtils.readUint16(header, 4);
+		if(payloadLen == 0 || payloadLen > maxPayloadLength)
+			throw new FormatException();
+		frame++;
+		// Read the payload
+		offset = 0;
+		while(offset < payloadLen) {
+			int read = in.read(payload, offset, payloadLen - offset);
+			if(read == -1) throw new EOFException(); // Unexpected EOF
+			mac.update(payload, offset, read);
+			offset += read;
+		}
+		payloadOff = 0;
+		// Read the MAC
+		byte[] expectedMac = mac.doFinal();
+		decrypter.readMac(footer);
+		if(!Arrays.equals(expectedMac, footer)) throw new FormatException();
+		betweenFrames = false;
+		return true;
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
index f9f0f9a63654a101254d8ec7c78d9ec1aeeb99e0..5f662503c0b5c4f55360cc1828c56b24b075f35d 100644
--- a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
+++ b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
@@ -73,6 +73,7 @@ DatabaseListener {
 				contactToTags.put(c, tags);
 				contactToWindow.put(c, w);
 			} catch(NoSuchContactException e) {
+				// The contact was removed after the call to getContacts()
 				continue;
 			}
 		}
@@ -80,15 +81,15 @@ DatabaseListener {
 	}
 
 	private synchronized byte[] calculateTag(ContactId c, long connection) {
-		byte[] tag = TagEncoder.encodeTag(transportId, connection, 0L);
+		byte[] tag = TagEncoder.encodeTag(transportId, connection);
 		Cipher cipher = contactToCipher.get(c);
 		assert cipher != null;
 		try {
 			return cipher.doFinal(tag);
-		} catch(BadPaddingException e) {
-			throw new RuntimeException(e);
-		} catch(IllegalBlockSizeException e) {
-			throw new RuntimeException(e);
+		} catch(BadPaddingException badCipher) {
+			throw new RuntimeException(badCipher);
+		} catch(IllegalBlockSizeException badCipher) {
+			throw new RuntimeException(badCipher);
 		}
 	}
 
diff --git a/components/net/sf/briar/transport/ConnectionWindowImpl.java b/components/net/sf/briar/transport/ConnectionWindowImpl.java
index 5ce228e7d3faaac267e8b64c4681b7d78222b258..f0485cb1acafd2cdc0581c315f6f276d509fff00 100644
--- a/components/net/sf/briar/transport/ConnectionWindowImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWindowImpl.java
@@ -1,6 +1,6 @@
 package net.sf.briar.transport;
 
-import static net.sf.briar.api.transport.TransportConstants.MAX_32_BIT_UNSIGNED;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
 import java.util.ArrayList;
 import java.util.Collection;
diff --git a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ae12ef4b1c4384ce4fb3fd6440f8a4e2dc3cc835
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
@@ -0,0 +1,43 @@
+package net.sf.briar.transport;
+
+import java.io.OutputStream;
+import java.security.InvalidKeyException;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.transport.ConnectionWriter;
+import net.sf.briar.api.transport.ConnectionWriterFactory;
+
+import com.google.inject.Inject;
+
+class ConnectionWriterFactoryImpl implements ConnectionWriterFactory {
+
+	private final CryptoComponent crypto;
+
+	@Inject
+	public ConnectionWriterFactoryImpl(CryptoComponent crypto) {
+		this.crypto = crypto;
+	}
+
+	public ConnectionWriter createConnectionWriter(OutputStream out,
+			int transportId, long connection, byte[] secret) {
+		SecretKey macKey = crypto.deriveOutgoingMacKey(secret);
+		SecretKey tagKey = crypto.deriveOutgoingTagKey(secret);
+		SecretKey frameKey = crypto.deriveOutgoingFrameKey(secret);
+		Cipher tagCipher = crypto.getTagCipher();
+		Cipher frameCipher = crypto.getFrameCipher();
+		Mac mac = crypto.getMac();
+		try {
+			mac.init(macKey);
+		} catch(InvalidKeyException badKey) {
+			throw new IllegalArgumentException(badKey);
+		}
+		ConnectionEncrypter encrypter = new ConnectionEncrypterImpl(out,
+				transportId, connection, tagCipher, frameCipher, tagKey,
+				frameKey);
+		return new ConnectionWriterImpl(encrypter, mac);
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionWriterImpl.java b/components/net/sf/briar/transport/ConnectionWriterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..882176a11d7f488e9caef83283eaeffb487f9463
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionWriterImpl.java
@@ -0,0 +1,84 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
+import static net.sf.briar.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
+import java.io.ByteArrayOutputStream;
+import java.io.FilterOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import javax.crypto.Mac;
+
+import net.sf.briar.api.transport.ConnectionWriter;
+import net.sf.briar.util.ByteUtils;
+
+class ConnectionWriterImpl extends FilterOutputStream
+implements ConnectionWriter {
+
+	private final ConnectionEncrypter encrypter;
+	private final Mac mac;
+	private final int maxPayloadLength;
+	private final ByteArrayOutputStream buf;
+	private final byte[] header;
+
+	private long frame = 0L;
+
+	ConnectionWriterImpl(ConnectionEncrypter encrypter, Mac mac) {
+		super(encrypter.getOutputStream());
+		this.encrypter = encrypter;
+		this.mac = mac;
+		maxPayloadLength = MAX_FRAME_LENGTH - 6 - mac.getMacLength();
+		buf = new ByteArrayOutputStream(maxPayloadLength);
+		header = new byte[6];
+	}
+
+	public OutputStream getOutputStream() {
+		return this;
+	}
+
+	@Override
+	public void flush() throws IOException {
+		if(buf.size() > 0) writeFrame();
+		out.flush();
+	}
+
+	@Override
+	public void write(int b) throws IOException {
+		if(buf.size() == maxPayloadLength) writeFrame();
+		buf.write(b);
+	}
+
+	@Override
+	public void write(byte[] b) throws IOException {
+		write(b, 0, b.length);
+	}
+
+	@Override
+	public void write(byte[] b, int off, int len) throws IOException {
+		int available = maxPayloadLength - buf.size();
+		while(available < len) {
+			buf.write(b, off, available);
+			writeFrame();
+			off += available;
+			len -= available;
+			available = maxPayloadLength;
+		}
+		buf.write(b, off, len);
+	}
+
+	private void writeFrame() throws IOException {
+		if(frame > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
+		byte[] payload = buf.toByteArray();
+		if(payload.length > maxPayloadLength) throw new IllegalStateException();
+		ByteUtils.writeUint32(frame, header, 0);
+		ByteUtils.writeUint16(payload.length, header, 4);
+		out.write(header);
+		mac.update(header);
+		out.write(payload);
+		mac.update(payload);
+		encrypter.writeMac(mac.doFinal());
+		frame++;
+		buf.reset();
+	}
+}
diff --git a/components/net/sf/briar/transport/PacketDecrypter.java b/components/net/sf/briar/transport/PacketDecrypter.java
deleted file mode 100644
index 239426021613a7aa842bf46876642d05f9236f8b..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketDecrypter.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package net.sf.briar.transport;
-
-import java.io.IOException;
-import java.io.InputStream;
-
-interface PacketDecrypter {
-
-	/** Returns the input stream from which packets should be read. */
-	InputStream getInputStream();
-
-	/**
-	 * Reads, decrypts and returns a tag from the underlying input stream.
-	 * Returns null if the end of the input stream is reached before any bytes
-	 * are read.
-	 */
-	byte[] readTag() throws IOException;
-}
diff --git a/components/net/sf/briar/transport/PacketDecrypterImpl.java b/components/net/sf/briar/transport/PacketDecrypterImpl.java
deleted file mode 100644
index b1ed14790443856e867258a073334082dc6daec8..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketDecrypterImpl.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.io.EOFException;
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-import java.util.Arrays;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.SecretKey;
-import javax.crypto.ShortBufferException;
-import javax.crypto.spec.IvParameterSpec;
-
-class PacketDecrypterImpl extends FilterInputStream implements PacketDecrypter {
-
-	private final Cipher tagCipher, packetCipher;
-	private final SecretKey packetKey;
-
-	private byte[] cipherBuf, plainBuf;
-	private int bufOff = 0, bufLen = TAG_LENGTH;
-	private boolean betweenPackets = true;
-
-	PacketDecrypterImpl(byte[] firstTag, InputStream in, Cipher tagCipher,
-			Cipher packetCipher, SecretKey tagKey, SecretKey packetKey) {
-		super(in);
-		if(firstTag.length != TAG_LENGTH)
-			throw new IllegalArgumentException();
-		cipherBuf = Arrays.copyOf(firstTag, firstTag.length);
-		plainBuf = new byte[TAG_LENGTH];
-		this.tagCipher = tagCipher;
-		this.packetCipher = packetCipher;
-		this.packetKey = packetKey;
-		try {
-			tagCipher.init(Cipher.DECRYPT_MODE, tagKey);
-		} catch(InvalidKeyException e) {
-			throw new IllegalArgumentException(e);
-		}
-		if(tagCipher.getOutputSize(TAG_LENGTH) != TAG_LENGTH)
-			throw new IllegalArgumentException();
-	}
-
-	public InputStream getInputStream() {
-		return this;
-	}
-
-	public byte[] readTag() throws IOException {
-		byte[] tag = new byte[TAG_LENGTH];
-		System.arraycopy(cipherBuf, bufOff, tag, 0, bufLen);
-		int offset = bufLen;
-		bufOff = bufLen = 0;
-		while(offset < tag.length) {
-			int read = in.read(tag, offset, tag.length - offset);
-			if(read == -1) break;
-			offset += read;
-		}
-		if(offset == 0) return null; // EOF between packets is acceptable
-		if(offset < tag.length) throw new EOFException();
-		betweenPackets = false;
-		try {
-			byte[] decryptedTag = tagCipher.doFinal(tag);
-			IvParameterSpec iv = new IvParameterSpec(decryptedTag);
-			packetCipher.init(Cipher.DECRYPT_MODE, packetKey, iv);
-			return decryptedTag;
-		} catch(BadPaddingException badCipher) {
-			throw new RuntimeException(badCipher);
-		} catch(IllegalBlockSizeException badCipher) {
-			throw new RuntimeException(badCipher);
-		} catch(InvalidAlgorithmParameterException badIv) {
-			throw new RuntimeException(badIv);
-		} catch(InvalidKeyException badKey) {
-			throw new RuntimeException(badKey);
-		}
-	}
-
-	@Override
-	public int read() throws IOException {
-		if(betweenPackets) throw new IllegalStateException();
-		if(bufLen == 0) {
-			int read = readBlock();
-			if(read == 0) return -1;
-			bufOff = 0;
-			bufLen = read;
-		}
-		int i = plainBuf[bufOff];
-		bufOff++;
-		bufLen--;
-		return i < 0 ? i + 256 : i;
-	}
-
-	// Although we're using CTR mode, which doesn't require full blocks of
-	// ciphertext, the cipher still tries to operate a block at a time. We must
-	// either call update() with a full block or doFinal() with the last
-	// (possibly partial) block.
-	private int readBlock() throws IOException {
-		// Try to read a block of ciphertext
-		int off = 0;
-		while(off < cipherBuf.length) {
-			int read = in.read(cipherBuf, off, cipherBuf.length - off);
-			if(read == -1) break;
-			off += read;
-		}
-		if(off == 0) return 0;
-		// Did we get a whole block? If not we must be at EOF
-		if(off < cipherBuf.length) {
-			// We're at EOF so we can call doFinal() to force decryption
-			try {
-				int i = packetCipher.doFinal(cipherBuf, 0, off, plainBuf);
-				if(i < off) throw new RuntimeException();
-				betweenPackets = true;
-			} catch(BadPaddingException badCipher) {
-				throw new RuntimeException(badCipher);
-			} catch(IllegalBlockSizeException badCipher) {
-				throw new RuntimeException(badCipher);
-			} catch(ShortBufferException badCipher) {
-				throw new RuntimeException(badCipher);
-			}
-		} else {
-			// We're not at EOF but we have a whole block to decrypt
-			try {
-				int i = packetCipher.update(cipherBuf, 0, off, plainBuf);
-				if(i < off) throw new RuntimeException();
-			} catch(ShortBufferException badCipher) {
-				throw new RuntimeException(badCipher);
-			}
-		}
-		return off;
-	}
-
-	@Override
-	public int read(byte[] b) throws IOException {
-		if(betweenPackets) throw new IllegalStateException();
-		if(bufLen == 0) {
-			int read = readBlock();
-			if(read == 0) return -1;
-			bufOff = 0;
-			bufLen = read;
-		}
-		int length = Math.min(b.length, bufLen);
-		System.arraycopy(plainBuf, bufOff, b, 0, length);
-		bufOff += length;
-		bufLen -= length;
-		return length;
-	}
-
-	@Override
-	public int read(byte[] b, int off, int len) throws IOException {
-		if(betweenPackets) throw new IllegalStateException();
-		if(bufLen == 0) {
-			int read = readBlock();
-			if(read == 0) return -1;
-			bufOff = 0;
-			bufLen = read;
-		}
-		int length = Math.min(len, bufLen);
-		System.arraycopy(plainBuf, bufOff, b, off, length);
-		bufOff += length;
-		bufLen -= length;
-		return length;
-	}
-}
diff --git a/components/net/sf/briar/transport/PacketEncrypter.java b/components/net/sf/briar/transport/PacketEncrypter.java
deleted file mode 100644
index 7652753b2611fbe74e58ba66ac96fbb95b3085af..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketEncrypter.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package net.sf.briar.transport;
-
-import java.io.IOException;
-import java.io.OutputStream;
-
-interface PacketEncrypter {
-
-	/** Returns the output stream to which packets should be written. */
-	OutputStream getOutputStream();
-
-	/** Encrypts the given tag and writes it to the underlying output stream. */
-	void writeTag(byte[] tag) throws IOException;
-
-	/** Finishes writing the current packet. */
-	void finishPacket() throws IOException;
-}
diff --git a/components/net/sf/briar/transport/PacketEncrypterImpl.java b/components/net/sf/briar/transport/PacketEncrypterImpl.java
deleted file mode 100644
index bd9b5e5a766165e6330e4eb45fd476de7486ae99..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketEncrypterImpl.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.io.FilterOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.security.InvalidAlgorithmParameterException;
-import java.security.InvalidKeyException;
-
-import javax.crypto.BadPaddingException;
-import javax.crypto.Cipher;
-import javax.crypto.IllegalBlockSizeException;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-
-class PacketEncrypterImpl extends FilterOutputStream
-implements PacketEncrypter {
-
-	private final Cipher tagCipher, packetCipher;
-	private final SecretKey packetKey;
-
-	PacketEncrypterImpl(OutputStream out, Cipher tagCipher,
-			Cipher packetCipher, SecretKey tagKey, SecretKey packetKey) {
-		super(out);
-		this.tagCipher = tagCipher;
-		this.packetCipher = packetCipher;
-		this.packetKey = packetKey;
-		try {
-			tagCipher.init(Cipher.ENCRYPT_MODE, tagKey);
-		} catch(InvalidKeyException e) {
-			throw new IllegalArgumentException(e);
-		}
-		if(tagCipher.getOutputSize(TAG_LENGTH) != TAG_LENGTH)
-			throw new IllegalArgumentException();
-	}
-
-	public OutputStream getOutputStream() {
-		return this;
-	}
-
-	public void writeTag(byte[] tag) throws IOException {
-		if(tag.length != TAG_LENGTH) throw new IllegalArgumentException();
-		IvParameterSpec iv = new IvParameterSpec(tag);
-		try {
-			out.write(tagCipher.doFinal(tag));
-			packetCipher.init(Cipher.ENCRYPT_MODE, packetKey, iv);
-		} catch(BadPaddingException badCipher) {
-			throw new IOException(badCipher);
-		} catch(IllegalBlockSizeException badCipher) {
-			throw new RuntimeException(badCipher);
-		} catch(InvalidAlgorithmParameterException badIv) {
-			throw new RuntimeException(badIv);
-		} catch(InvalidKeyException badKey) {
-			throw new RuntimeException(badKey);
-		}
-	}
-
-	public void finishPacket() throws IOException {
-		try {
-			out.write(packetCipher.doFinal());
-		} catch(BadPaddingException badCipher) {
-			throw new IOException(badCipher);
-		} catch(IllegalBlockSizeException badCipher) {
-			throw new RuntimeException(badCipher);
-		}
-	}
-
-	@Override
-	public void write(int b) throws IOException {
-		byte[] ciphertext = packetCipher.update(new byte[] {(byte) b});
-		if(ciphertext != null) out.write(ciphertext);
-	}
-
-	@Override
-	public void write(byte[] b) throws IOException {
-		byte[] ciphertext = packetCipher.update(b);
-		if(ciphertext != null) out.write(ciphertext);
-	}
-
-	@Override
-	public void write(byte[] b, int off, int len) throws IOException {
-		byte[] ciphertext = packetCipher.update(b, off, len);
-		if(ciphertext != null) out.write(ciphertext);
-	}
-}
diff --git a/components/net/sf/briar/transport/PacketReaderFactoryImpl.java b/components/net/sf/briar/transport/PacketReaderFactoryImpl.java
deleted file mode 100644
index 3d8ccbdafe380b8ec5de4ae6163abcb048a4bc36..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketReaderFactoryImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.sf.briar.transport;
-
-import java.io.InputStream;
-import java.security.InvalidKeyException;
-
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.transport.PacketReader;
-import net.sf.briar.api.transport.PacketReaderFactory;
-
-import com.google.inject.Inject;
-
-class PacketReaderFactoryImpl implements PacketReaderFactory {
-
-	private final CryptoComponent crypto;
-
-	@Inject
-	PacketReaderFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
-	}
-
-	public PacketReader createPacketReader(byte[] firstTag, InputStream in,
-			int transportId, long connection, byte[] secret) {
-		SecretKey macKey = crypto.deriveIncomingMacKey(secret);
-		SecretKey tagKey = crypto.deriveIncomingTagKey(secret);
-		SecretKey packetKey = crypto.deriveIncomingPacketKey(secret);
-		Cipher tagCipher = crypto.getTagCipher();
-		Cipher packetCipher = crypto.getPacketCipher();
-		Mac mac = crypto.getMac();
-		try {
-			mac.init(macKey);
-		} catch(InvalidKeyException e) {
-			throw new IllegalArgumentException(e);
-		}
-		PacketDecrypter decrypter = new PacketDecrypterImpl(firstTag, in,
-				tagCipher, packetCipher, tagKey, packetKey);
-		return new PacketReaderImpl(decrypter, mac, transportId, connection);
-	}
-}
diff --git a/components/net/sf/briar/transport/PacketReaderImpl.java b/components/net/sf/briar/transport/PacketReaderImpl.java
deleted file mode 100644
index 2db8bafefe53750316b113e56d058971a72eab8c..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketReaderImpl.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.MAX_32_BIT_UNSIGNED;
-
-import java.io.FilterInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-import javax.crypto.Mac;
-
-import net.sf.briar.api.serial.FormatException;
-import net.sf.briar.api.transport.PacketReader;
-
-class PacketReaderImpl extends FilterInputStream implements PacketReader {
-
-	private final PacketDecrypter decrypter;
-	private final Mac mac;
-	private final int macLength, transportId;
-	private final long connection;
-
-	private long packet = 0L;
-	private boolean betweenPackets = true;
-
-	PacketReaderImpl(PacketDecrypter decrypter, Mac mac, int transportId,
-			long connection) {
-		super(decrypter.getInputStream());
-		this.decrypter = decrypter;
-		this.mac = mac;
-		macLength = mac.getMacLength();
-		this.transportId = transportId;
-		this.connection = connection;
-	}
-
-	public InputStream getInputStream() {
-		return this;
-	}
-
-	public void finishPacket() throws IOException, GeneralSecurityException {
-		if(!betweenPackets) readMac();
-	}
-
-	@Override
-	public int read() throws IOException {
-		if(betweenPackets) readTag();
-		int i = in.read();
-		if(i != -1) mac.update((byte) i);
-		return i;
-	}
-
-	@Override
-	public int read(byte[] b) throws IOException {
-		return read(b, 0, b.length);
-	}
-
-	@Override
-	public int read(byte[] b, int off, int len) throws IOException {
-		if(betweenPackets) readTag();
-		int i = in.read(b, off, len);
-		if(i != -1) mac.update(b, off, i);
-		return i;
-	}
-
-	private void readMac() throws IOException, GeneralSecurityException {
-		byte[] expectedMac = mac.doFinal();
-		byte[] actualMac = new byte[macLength];
-		InputStream in = decrypter.getInputStream();
-		int offset = 0;
-		while(offset < macLength) {
-			int read = in.read(actualMac, offset, actualMac.length - offset);
-			if(read == -1) break;
-			offset += read;
-		}
-		if(offset < macLength) throw new GeneralSecurityException();
-		if(!Arrays.equals(expectedMac, actualMac))
-			throw new GeneralSecurityException();
-		betweenPackets = true;
-	}
-
-	private void readTag() throws IOException {
-		assert betweenPackets;
-		if(packet > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
-		byte[] tag = decrypter.readTag();
-		if(tag == null) return; // EOF
-		if(!TagDecoder.decodeTag(tag, transportId, connection, packet))
-			throw new FormatException();
-		mac.update(tag);
-		packet++;
-		betweenPackets = false;
-	}
-}
diff --git a/components/net/sf/briar/transport/PacketWriterFactoryImpl.java b/components/net/sf/briar/transport/PacketWriterFactoryImpl.java
deleted file mode 100644
index a59d201c4c27ae2ffc88b4101872fb38eed11c2a..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketWriterFactoryImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.sf.briar.transport;
-
-import java.io.OutputStream;
-import java.security.InvalidKeyException;
-
-import javax.crypto.Cipher;
-import javax.crypto.Mac;
-import javax.crypto.SecretKey;
-
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.transport.PacketWriter;
-import net.sf.briar.api.transport.PacketWriterFactory;
-
-import com.google.inject.Inject;
-
-class PacketWriterFactoryImpl implements PacketWriterFactory {
-
-	private final CryptoComponent crypto;
-
-	@Inject
-	public PacketWriterFactoryImpl(CryptoComponent crypto) {
-		this.crypto = crypto;
-	}
-
-	public PacketWriter createPacketWriter(OutputStream out, int transportId,
-			long connection, byte[] secret) {
-		SecretKey macKey = crypto.deriveOutgoingMacKey(secret);
-		SecretKey tagKey = crypto.deriveOutgoingTagKey(secret);
-		SecretKey packetKey = crypto.deriveOutgoingPacketKey(secret);
-		Cipher tagCipher = crypto.getTagCipher();
-		Cipher packetCipher = crypto.getPacketCipher();
-		Mac mac = crypto.getMac();
-		try {
-			mac.init(macKey);
-		} catch(InvalidKeyException e) {
-			throw new IllegalArgumentException(e);
-		}
-		PacketEncrypter encrypter = new PacketEncrypterImpl(out, tagCipher,
-				packetCipher, tagKey, packetKey);
-		return new PacketWriterImpl(encrypter, mac, transportId, connection);
-	}
-}
diff --git a/components/net/sf/briar/transport/PacketWriterImpl.java b/components/net/sf/briar/transport/PacketWriterImpl.java
deleted file mode 100644
index 7ca9f9289d223c8a6c94c1704d4fdddfffc87058..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/PacketWriterImpl.java
+++ /dev/null
@@ -1,83 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.MAX_16_BIT_UNSIGNED;
-import static net.sf.briar.api.transport.TransportConstants.MAX_32_BIT_UNSIGNED;
-
-import java.io.FilterOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-
-import javax.crypto.Mac;
-
-import net.sf.briar.api.transport.PacketWriter;
-
-class PacketWriterImpl extends FilterOutputStream implements PacketWriter {
-
-	private final PacketEncrypter encrypter;
-	private final Mac mac;
-	private final int transportId;
-	private final long connection;
-
-	private long packet = 0L;
-	private boolean betweenPackets = true;
-
-	PacketWriterImpl(PacketEncrypter encrypter, Mac mac, int transportId,
-			long connection) {
-		super(encrypter.getOutputStream());
-		this.encrypter = encrypter;
-		this.mac = mac;
-		if(transportId < 0) throw new IllegalArgumentException();
-		if(transportId > MAX_16_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		this.transportId = transportId;
-		if(connection < 0L) throw new IllegalArgumentException();
-		if(connection > MAX_32_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		this.connection = connection;
-	}
-
-	public OutputStream getOutputStream() {
-		return this;
-	}
-
-	public void finishPacket() throws IOException {
-		if(!betweenPackets) writeMac();
-	}
-
-	@Override
-	public void write(int b) throws IOException {
-		if(betweenPackets) writeTag();
-		out.write(b);
-		mac.update((byte) b);
-	}
-
-	@Override
-	public void write(byte[] b) throws IOException {
-		write(b, 0, b.length);
-	}
-
-	@Override
-	public void write(byte[] b, int off, int len) throws IOException {
-		if(betweenPackets) writeTag();
-		out.write(b, off, len);
-		mac.update(b, off, len);
-	}
-
-	private void writeMac() throws IOException {
-		out.write(mac.doFinal());
-		encrypter.finishPacket();
-		betweenPackets = true;
-	}
-
-	private void writeTag() throws IOException {
-		assert betweenPackets;
-		if(packet > MAX_32_BIT_UNSIGNED) throw new IllegalStateException();
-		byte[] tag = TagEncoder.encodeTag(transportId, connection,
-				packet);
-		// Write the tag to the encrypter and start calculating the MAC
-		encrypter.writeTag(tag);
-		mac.update(tag);
-		packet++;
-		betweenPackets = false;
-	}
-}
diff --git a/components/net/sf/briar/transport/TagDecoder.java b/components/net/sf/briar/transport/TagDecoder.java
index 169f1ad15175372aaff44c011454dddd54b2ba8b..a83be3d847f5b68e77c387d44b7c840c0c4727d0 100644
--- a/components/net/sf/briar/transport/TagDecoder.java
+++ b/components/net/sf/briar/transport/TagDecoder.java
@@ -1,35 +1,20 @@
 package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
+import net.sf.briar.util.ByteUtils;
 
 class TagDecoder {
 
-	static boolean decodeTag(byte[] tag, int transportId, long connection,
-			long packet) {
+	static boolean decodeTag(byte[] tag, int transportId, long connection) {
 		if(tag.length != TAG_LENGTH) return false;
-		// First 16 bits must be zero
-		if(readUint16(tag, 0) != 0) return false;
+		// First 32 bits must be zero (reserved)
+		for(int i = 0; i < 4; i++) if(tag[i] != 0) return false;
 		// Transport identifier is encoded as an unsigned 16-bit integer
-		if(readUint16(tag, 2) != transportId) return false;
+		if(ByteUtils.readUint16(tag, 4) != transportId) return false;
 		// Connection number is encoded as an unsigned 32-bit integer
-		if(readUint32(tag, 4) != connection) return false;
-		// Packet number is encoded as an unsigned 32-bit integer
-		if(readUint32(tag, 8) != packet) return false;
-		// Last 32 bits must be zero
-		if(readUint32(tag, 12) != 0L) return false;
+		if(ByteUtils.readUint32(tag, 6) != connection) return false;
+		// Last 48 bits must be zero (frame number and block number)
+		for(int i = 10; i < 16; i++) if(tag[i] != 0) return false;
 		return true;
 	}
-
-	// Package access for testing
-	static int readUint16(byte[] b, int offset) {
-		assert b.length >= offset + 2;
-		return ((b[offset] & 0xFF) << 8) | (b[offset + 1] & 0xFF);
-	}
-
-	// Package access for testing
-	static long readUint32(byte[] b, int offset) {
-		assert b.length >= offset + 4;
-		return ((b[offset] & 0xFFL) << 24) | ((b[offset + 1] & 0xFFL) << 16)
-		| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
-	}
 }
diff --git a/components/net/sf/briar/transport/TagEncoder.java b/components/net/sf/briar/transport/TagEncoder.java
index 1660c437120e5f110943e27f809b693a9e9dbbfd..dbde2fa50538021ad218f4ff86d2b653a55ad209 100644
--- a/components/net/sf/briar/transport/TagEncoder.java
+++ b/components/net/sf/briar/transport/TagEncoder.java
@@ -1,40 +1,31 @@
 package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAX_16_BIT_UNSIGNED;
-import static net.sf.briar.api.transport.TransportConstants.MAX_32_BIT_UNSIGNED;
+import net.sf.briar.util.ByteUtils;
 
 class TagEncoder {
 
-	static byte[] encodeTag(int transportId, long connection,
-			long packet) {
+	static byte[] encodeTag(int transportId, long connection) {
 		byte[] tag = new byte[TAG_LENGTH];
 		// Encode the transport identifier as an unsigned 16-bit integer
-		writeUint16(transportId, tag, 2);
+		ByteUtils.writeUint16(transportId, tag, 4);
 		// Encode the connection number as an unsigned 32-bit integer
-		writeUint32(connection, tag, 4);
-		// Encode the packet number as an unsigned 32-bit integer
-		writeUint32(packet, tag, 8);
+		ByteUtils.writeUint32(connection, tag, 6);
 		return tag;
 	}
 
-	// Package access for testing
-	static void writeUint16(int i, byte[] b, int offset) {
-		assert i >= 0;
-		assert i <= MAX_16_BIT_UNSIGNED;
-		assert b.length >= offset + 2;
-		b[offset] = (byte) (i >> 8);
-		b[offset + 1] = (byte) (i & 0xFF);
-	}
-
-	// Package access for testing
-	static void writeUint32(long i, byte[] b, int offset) {
-		assert i >= 0L;
-		assert i <= MAX_32_BIT_UNSIGNED;
-		assert b.length >= offset + 4;
-		b[offset] = (byte) (i >> 24);
-		b[offset + 1] = (byte) (i >> 16 & 0xFF);
-		b[offset + 2] = (byte) (i >> 8 & 0xFF);
-		b[offset + 3] = (byte) (i & 0xFF);
+	static void encodeTag(byte[] tag, int transportId, long connection,
+			long frame) {
+		if(tag.length != TAG_LENGTH) throw new IllegalArgumentException();
+		// The first 16 bits of the tag must be zero (reserved)
+		ByteUtils.writeUint16(0, tag, 0);
+		// Encode the transport identifier as an unsigned 16-bit integer
+		ByteUtils.writeUint16(transportId, tag, 4);
+		// Encode the connection number as an unsigned 32-bit integer
+		ByteUtils.writeUint32(connection, tag, 6);
+		// Encode the frame number as an unsigned 32-bit integer
+		ByteUtils.writeUint32(frame, tag, 10);
+		// The last 16 bits of the tag must be zero (block number)
+		ByteUtils.writeUint16(0, tag, 14);
 	}
 }
diff --git a/components/net/sf/briar/transport/TransportModule.java b/components/net/sf/briar/transport/TransportModule.java
index 1fefd05eafab81a84782e5887fbb02fa9592cf03..d952d534ec6223b77b09f7a97b0e2c2770e4ea60 100644
--- a/components/net/sf/briar/transport/TransportModule.java
+++ b/components/net/sf/briar/transport/TransportModule.java
@@ -1,8 +1,8 @@
 package net.sf.briar.transport;
 
+import net.sf.briar.api.transport.ConnectionReaderFactory;
 import net.sf.briar.api.transport.ConnectionWindowFactory;
-import net.sf.briar.api.transport.PacketReaderFactory;
-import net.sf.briar.api.transport.PacketWriterFactory;
+import net.sf.briar.api.transport.ConnectionWriterFactory;
 
 import com.google.inject.AbstractModule;
 
@@ -10,9 +10,11 @@ public class TransportModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
+		bind(ConnectionReaderFactory.class).to(
+				ConnectionReaderFactoryImpl.class);
 		bind(ConnectionWindowFactory.class).to(
 				ConnectionWindowFactoryImpl.class);
-		bind(PacketReaderFactory.class).to(PacketReaderFactoryImpl.class);
-		bind(PacketWriterFactory.class).to(PacketWriterFactoryImpl.class);
+		bind(ConnectionWriterFactory.class).to(
+				ConnectionWriterFactoryImpl.class);
 	}
 }
diff --git a/test/build.xml b/test/build.xml
index f63488bbfffcdf781d493a939c4e0da5bd8d5b76..f6bf3ddb1025aec127b87dd6c206901086de5f4a 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -36,15 +36,14 @@
 			<test name='net.sf.briar.serial.ReaderImplTest'/>
 			<test name='net.sf.briar.serial.WriterImplTest'/>
 			<test name='net.sf.briar.setup.SetupWorkerTest'/>
+			<test name='net.sf.briar.transport.ConnectionDecrypterImplTest'/>
+			<test name='net.sf.briar.transport.ConnectionEncrypterImplTest'/>
+			<test name='net.sf.briar.transport.ConnectionReaderImplTest'/>
 			<test name='net.sf.briar.transport.ConnectionRecogniserImplTest'/>
 			<test name='net.sf.briar.transport.ConnectionWindowImplTest'/>
-			<test name='net.sf.briar.transport.PacketDecrypterImplTest'/>
-			<test name='net.sf.briar.transport.PacketEncrypterImplTest'/>
-			<test name='net.sf.briar.transport.PacketReaderImplTest'/>
-			<test name='net.sf.briar.transport.PacketReadWriteTest'/>
-			<test name='net.sf.briar.transport.PacketWriterImplTest'/>
-			<test name='net.sf.briar.transport.TagDecoderTest'/>
-			<test name='net.sf.briar.transport.TagEncoderTest'/>
+			<test name='net.sf.briar.transport.ConnectionWriterImplTest'/>
+			<test name='net.sf.briar.transport.FrameReadWriteTest'/>
+			<test name='net.sf.briar.util.ByteUtilsTest'/>
 			<test name='net.sf.briar.util.FileUtilsTest'/>
 			<test name='net.sf.briar.util.StringUtilsTest'/>
 			<test name='net.sf.briar.util.ZipUtilsTest'/>
diff --git a/test/net/sf/briar/FileReadWriteTest.java b/test/net/sf/briar/FileReadWriteTest.java
index 14ac71a9c4e7e1b5bef0ca021994ce66e750e56a..e7a6c86c9b5cd2b56808ef875d4f0243c8b8617a 100644
--- a/test/net/sf/briar/FileReadWriteTest.java
+++ b/test/net/sf/briar/FileReadWriteTest.java
@@ -41,10 +41,10 @@ import net.sf.briar.api.protocol.writers.ProtocolWriterFactory;
 import net.sf.briar.api.protocol.writers.RequestWriter;
 import net.sf.briar.api.protocol.writers.SubscriptionWriter;
 import net.sf.briar.api.protocol.writers.TransportWriter;
-import net.sf.briar.api.transport.PacketReader;
-import net.sf.briar.api.transport.PacketReaderFactory;
-import net.sf.briar.api.transport.PacketWriter;
-import net.sf.briar.api.transport.PacketWriterFactory;
+import net.sf.briar.api.transport.ConnectionReader;
+import net.sf.briar.api.transport.ConnectionReaderFactory;
+import net.sf.briar.api.transport.ConnectionWriter;
+import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.crypto.CryptoModule;
 import net.sf.briar.protocol.ProtocolModule;
 import net.sf.briar.protocol.writers.WritersModule;
@@ -66,8 +66,8 @@ public class FileReadWriteTest extends TestCase {
 	private final BatchId ack = new BatchId(TestUtils.getRandomId());
 	private final long timestamp = System.currentTimeMillis();
 
-	private final PacketReaderFactory packetReaderFactory;
-	private final PacketWriterFactory packetWriterFactory;
+	private final ConnectionReaderFactory connectionReaderFactory;
+	private final ConnectionWriterFactory connectionWriterFactory;
 	private final ProtocolReaderFactory protocolReaderFactory;
 	private final ProtocolWriterFactory protocolWriterFactory;
 	private final CryptoComponent crypto;
@@ -87,8 +87,8 @@ public class FileReadWriteTest extends TestCase {
 		Injector i = Guice.createInjector(new CryptoModule(),
 				new ProtocolModule(), new SerialModule(), new TransportModule(),
 				new WritersModule());
-		packetReaderFactory = i.getInstance(PacketReaderFactory.class);
-		packetWriterFactory = i.getInstance(PacketWriterFactory.class);
+		connectionReaderFactory = i.getInstance(ConnectionReaderFactory.class);
+		connectionWriterFactory = i.getInstance(ConnectionWriterFactory.class);
 		protocolReaderFactory = i.getInstance(ProtocolReaderFactory.class);
 		protocolWriterFactory = i.getInstance(ProtocolWriterFactory.class);
 		crypto = i.getInstance(CryptoComponent.class);
@@ -134,14 +134,13 @@ public class FileReadWriteTest extends TestCase {
 	public void testWriteFile() throws Exception {
 		OutputStream out = new FileOutputStream(file);
 		// Use Alice's secret for writing
-		PacketWriter packetWriter = packetWriterFactory.createPacketWriter(out,
+		ConnectionWriter w = connectionWriterFactory.createConnectionWriter(out,
 				transportId, connection, aliceSecret);
-		out = packetWriter.getOutputStream();
+		out = w.getOutputStream();
 
 		AckWriter a = protocolWriterFactory.createAckWriter(out);
 		assertTrue(a.writeBatchId(ack));
 		a.finish();
-		packetWriter.finishPacket();
 
 		BatchWriter b = protocolWriterFactory.createBatchWriter(out);
 		assertTrue(b.writeMessage(message.getBytes()));
@@ -149,7 +148,6 @@ public class FileReadWriteTest extends TestCase {
 		assertTrue(b.writeMessage(message2.getBytes()));
 		assertTrue(b.writeMessage(message3.getBytes()));
 		b.finish();
-		packetWriter.finishPacket();
 
 		OfferWriter o = protocolWriterFactory.createOfferWriter(out);
 		assertTrue(o.writeMessageId(message.getId()));
@@ -157,14 +155,12 @@ public class FileReadWriteTest extends TestCase {
 		assertTrue(o.writeMessageId(message2.getId()));
 		assertTrue(o.writeMessageId(message3.getId()));
 		o.finish();
-		packetWriter.finishPacket();
 
 		RequestWriter r = protocolWriterFactory.createRequestWriter(out);
 		BitSet requested = new BitSet(4);
 		requested.set(1);
 		requested.set(3);
 		r.writeRequest(offerId, requested, 4);
-		packetWriter.finishPacket();
 
 		SubscriptionWriter s =
 			protocolWriterFactory.createSubscriptionWriter(out);
@@ -173,14 +169,12 @@ public class FileReadWriteTest extends TestCase {
 		subs.put(group, 0L);
 		subs.put(group1, 0L);
 		s.writeSubscriptions(subs, timestamp);
-		packetWriter.finishPacket();
 
 		TransportWriter t = protocolWriterFactory.createTransportWriter(out);
 		t.writeTransports(transports, timestamp);
-		packetWriter.finishPacket();
 
-		out.flush();
-		out.close();
+		w.getOutputStream().flush();
+		w.getOutputStream().close();
 		assertTrue(file.exists());
 		assertTrue(file.length() > message.getSize());
 	}
@@ -200,22 +194,20 @@ public class FileReadWriteTest extends TestCase {
 		}
 		assertEquals(16, offset);
 		// Use Bob's secret for reading
-		PacketReader packetReader = packetReaderFactory.createPacketReader(
-				firstTag, in, transportId, connection, bobSecret);
-		in = packetReader.getInputStream();
+		ConnectionReader r = connectionReaderFactory.createConnectionReader(in,
+				transportId, connection, bobSecret);
+		in = r.getInputStream();
 		ProtocolReader protocolReader =
 			protocolReaderFactory.createProtocolReader(in);
 
 		// Read the ack
 		assertTrue(protocolReader.hasAck());
 		Ack a = protocolReader.readAck();
-		packetReader.finishPacket();
 		assertEquals(Collections.singletonList(ack), a.getBatchIds());
 
 		// Read the batch
 		assertTrue(protocolReader.hasBatch());
 		Batch b = protocolReader.readBatch();
-		packetReader.finishPacket();
 		Collection<Message> messages = b.getMessages();
 		assertEquals(4, messages.size());
 		Iterator<Message> it = messages.iterator();
@@ -227,7 +219,6 @@ public class FileReadWriteTest extends TestCase {
 		// Read the offer
 		assertTrue(protocolReader.hasOffer());
 		Offer o = protocolReader.readOffer();
-		packetReader.finishPacket();
 		Collection<MessageId> offered = o.getMessageIds();
 		assertEquals(4, offered.size());
 		Iterator<MessageId> it1 = offered.iterator();
@@ -238,10 +229,9 @@ public class FileReadWriteTest extends TestCase {
 
 		// Read the request
 		assertTrue(protocolReader.hasRequest());
-		Request r = protocolReader.readRequest();
-		packetReader.finishPacket();
-		assertEquals(offerId, r.getOfferId());
-		BitSet requested = r.getBitmap();
+		Request req = protocolReader.readRequest();
+		assertEquals(offerId, req.getOfferId());
+		BitSet requested = req.getBitmap();
 		assertFalse(requested.get(0));
 		assertTrue(requested.get(1));
 		assertFalse(requested.get(2));
@@ -252,7 +242,6 @@ public class FileReadWriteTest extends TestCase {
 		// Read the subscription update
 		assertTrue(protocolReader.hasSubscriptionUpdate());
 		SubscriptionUpdate s = protocolReader.readSubscriptionUpdate();
-		packetReader.finishPacket();
 		Map<Group, Long> subs = s.getSubscriptions();
 		assertEquals(2, subs.size());
 		assertEquals(Long.valueOf(0L), subs.get(group));
@@ -262,7 +251,6 @@ public class FileReadWriteTest extends TestCase {
 		// Read the transport update
 		assertTrue(protocolReader.hasTransportUpdate());
 		TransportUpdate t = protocolReader.readTransportUpdate();
-		packetReader.finishPacket();
 		assertEquals(transports, t.getTransports());
 		assertTrue(t.getTimestamp() == timestamp);
 
diff --git a/test/net/sf/briar/TestUtils.java b/test/net/sf/briar/TestUtils.java
index d8c25231b78bca3463c96c424ac795da3a7c0f59..6ecb32a4c68cf92c2b4f0b2741d8b29d96070c0f 100644
--- a/test/net/sf/briar/TestUtils.java
+++ b/test/net/sf/briar/TestUtils.java
@@ -3,10 +3,12 @@ package net.sf.briar;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.PrintStream;
 import java.util.Random;
 import java.util.concurrent.atomic.AtomicInteger;
 
+import junit.framework.TestCase;
 import net.sf.briar.api.protocol.UniqueId;
 
 public class TestUtils {
@@ -60,4 +62,14 @@ public class TestUtils {
 		random.nextBytes(b);
 		return b;
 	}
+
+	public static void readFully(InputStream in, byte[] b) throws IOException {
+		int offset = 0;
+		while(offset < b.length) {
+			int read = in.read(b, offset, b.length - offset);
+			if(read == -1) break;
+			offset += read;
+		}
+		TestCase.assertEquals(b.length, offset);
+	}
 }
diff --git a/test/net/sf/briar/crypto/CounterModeTest.java b/test/net/sf/briar/crypto/CounterModeTest.java
index 7ce6dbcc29515fb98e09f59a458c2bad2ba50385..ff664429db30fbc03253ad8f65dc0ff34654cba1 100644
--- a/test/net/sf/briar/crypto/CounterModeTest.java
+++ b/test/net/sf/briar/crypto/CounterModeTest.java
@@ -87,31 +87,28 @@ public class CounterModeTest extends TestCase {
 	@Test
 	public void testLeastSignificantBitsUsedAsCounter()
 	throws GeneralSecurityException {
-		// Initialise the least significant 32 bits of the IV to zero and
+		// Initialise the least significant 16 bits of the IV to zero and
 		// encrypt ten blocks of zeroes
 		byte[] plaintext = new byte[BLOCK_SIZE_BYTES * 10];
 		byte[] ivBytes = new byte[BLOCK_SIZE_BYTES];
 		random.nextBytes(ivBytes);
-		for(int i = BLOCK_SIZE_BYTES - 4; i < BLOCK_SIZE_BYTES; i++) {
-			ivBytes[i] = 0;
-		}
+		ivBytes[BLOCK_SIZE_BYTES - 2] = 0;
+		ivBytes[BLOCK_SIZE_BYTES - 1] = 0;
 		IvParameterSpec iv = new IvParameterSpec(ivBytes);
 		Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
 		cipher.init(Cipher.ENCRYPT_MODE, key, iv);
-		byte[] ciphertext =
-			new byte[cipher.getOutputSize(plaintext.length)];
+		byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)];
 		cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0);
-		// Initialise the least significant 32 bits of the IV to one and
+		// Make sure the IV array hasn't been modified
+		assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 2]);
+		assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 1]);
+		// Initialise the least significant 16 bits of the IV to one and
 		// encrypt another ten blocks of zeroes
-		for(int i = BLOCK_SIZE_BYTES - 4; i < BLOCK_SIZE_BYTES; i++) {
-			assertEquals(0, ivBytes[i]);
-		}
 		ivBytes[BLOCK_SIZE_BYTES - 1] = 1;
 		iv = new IvParameterSpec(ivBytes);
 		cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
 		cipher.init(Cipher.ENCRYPT_MODE, key, iv);
-		byte[] ciphertext1 =
-			new byte[cipher.getOutputSize(plaintext.length)];
+		byte[] ciphertext1 = new byte[cipher.getOutputSize(plaintext.length)];
 		cipher.doFinal(plaintext, 0, plaintext.length, ciphertext1, 0);
 		// The last nine blocks of the first ciphertext should be identical to
 		// the first nine blocks of the second ciphertext
@@ -121,38 +118,34 @@ public class CounterModeTest extends TestCase {
 	}
 
 	@Test
-	public void testCounterUsesMoreThan32Bits()
+	public void testCounterUsesMoreThan16Bits()
 	throws GeneralSecurityException {
-		// Initialise the least significant bits of the IV to 2^32-1 and
+		// Initialise the least significant bits of the IV to 2^16-1 and
 		// encrypt ten blocks of zeroes
 		byte[] plaintext = new byte[BLOCK_SIZE_BYTES * 10];
 		byte[] ivBytes = new byte[BLOCK_SIZE_BYTES];
 		random.nextBytes(ivBytes);
-		ivBytes[BLOCK_SIZE_BYTES - 5] = 0;
-		for(int i = BLOCK_SIZE_BYTES - 4; i < BLOCK_SIZE_BYTES; i++) {
-			ivBytes[i] = (byte) 255;
-		}
+		ivBytes[BLOCK_SIZE_BYTES - 3] = 0;
+		ivBytes[BLOCK_SIZE_BYTES - 2] = (byte) 255;
+		ivBytes[BLOCK_SIZE_BYTES - 1] = (byte) 255;
 		IvParameterSpec iv = new IvParameterSpec(ivBytes);
 		Cipher cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
 		cipher.init(Cipher.ENCRYPT_MODE, key, iv);
-		byte[] ciphertext =
-			new byte[cipher.getOutputSize(plaintext.length)];
+		byte[] ciphertext = new byte[cipher.getOutputSize(plaintext.length)];
 		cipher.doFinal(plaintext, 0, plaintext.length, ciphertext, 0);
-		// Initialise the least significant bits of the IV to 2^32 and
+		// Make sure the IV array hasn't been modified
+		assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 3]);
+		assertEquals((byte) 255, ivBytes[BLOCK_SIZE_BYTES - 2]);
+		assertEquals((byte) 255, ivBytes[BLOCK_SIZE_BYTES - 1]);
+		// Initialise the least significant bits of the IV to 2^16 and
 		// encrypt another ten blocks of zeroes
-		assertEquals(0, ivBytes[BLOCK_SIZE_BYTES - 5]);
-		for(int i = BLOCK_SIZE_BYTES - 4; i < BLOCK_SIZE_BYTES; i++) {
-			assertEquals((byte) 255, ivBytes[i]);
-		}
-		ivBytes[BLOCK_SIZE_BYTES - 5] = 1;
-		for(int i = BLOCK_SIZE_BYTES - 4; i < BLOCK_SIZE_BYTES; i++) {
-			ivBytes[i] = 0;
-		}
+		ivBytes[BLOCK_SIZE_BYTES - 3] = 1;
+		ivBytes[BLOCK_SIZE_BYTES - 2] = 0;
+		ivBytes[BLOCK_SIZE_BYTES - 1] = 0;
 		iv = new IvParameterSpec(ivBytes);
 		cipher = Cipher.getInstance(CIPHER_MODE, PROVIDER);
 		cipher.init(Cipher.ENCRYPT_MODE, key, iv);
-		byte[] ciphertext1 =
-			new byte[cipher.getOutputSize(plaintext.length)];
+		byte[] ciphertext1 = new byte[cipher.getOutputSize(plaintext.length)];
 		cipher.doFinal(plaintext, 0, plaintext.length, ciphertext1, 0);
 		// The last nine blocks of the first ciphertext should be identical to
 		// the first nine blocks of the second ciphertext
diff --git a/test/net/sf/briar/crypto/CryptoComponentTest.java b/test/net/sf/briar/crypto/CryptoComponentTest.java
index d7dad93ed9b8933ad64725111c383580401161f9..b8ea8ade6a8a36e136374a323035a58bb791c7e8 100644
--- a/test/net/sf/briar/crypto/CryptoComponentTest.java
+++ b/test/net/sf/briar/crypto/CryptoComponentTest.java
@@ -27,22 +27,22 @@ public class CryptoComponentTest extends TestCase {
 		// Check that Alice's incoming keys match Bob's outgoing keys
 		assertEquals(crypto.deriveIncomingMacKey(aliceSecret),
 				crypto.deriveOutgoingMacKey(bobSecret));
-		assertEquals(crypto.deriveIncomingPacketKey(aliceSecret),
-				crypto.deriveOutgoingPacketKey(bobSecret));
+		assertEquals(crypto.deriveIncomingFrameKey(aliceSecret),
+				crypto.deriveOutgoingFrameKey(bobSecret));
 		assertEquals(crypto.deriveIncomingTagKey(aliceSecret),
 				crypto.deriveOutgoingTagKey(bobSecret));
 		// Check that Alice's outgoing keys match Bob's incoming keys
 		assertEquals(crypto.deriveOutgoingMacKey(aliceSecret),
 				crypto.deriveIncomingMacKey(bobSecret));
-		assertEquals(crypto.deriveOutgoingPacketKey(aliceSecret),
-				crypto.deriveIncomingPacketKey(bobSecret));
+		assertEquals(crypto.deriveOutgoingFrameKey(aliceSecret),
+				crypto.deriveIncomingFrameKey(bobSecret));
 		assertEquals(crypto.deriveOutgoingTagKey(aliceSecret),
 				crypto.deriveIncomingTagKey(bobSecret));
 		// Check that Alice's incoming and outgoing keys are different
 		assertFalse(crypto.deriveIncomingMacKey(aliceSecret).equals(
 				crypto.deriveOutgoingMacKey(aliceSecret)));
-		assertFalse(crypto.deriveIncomingPacketKey(aliceSecret).equals(
-				crypto.deriveOutgoingPacketKey(aliceSecret)));
+		assertFalse(crypto.deriveIncomingFrameKey(aliceSecret).equals(
+				crypto.deriveOutgoingFrameKey(aliceSecret)));
 		assertFalse(crypto.deriveIncomingTagKey(aliceSecret).equals(
 				crypto.deriveOutgoingTagKey(aliceSecret)));
 	}
diff --git a/test/net/sf/briar/protocol/AckReaderTest.java b/test/net/sf/briar/protocol/AckReaderTest.java
index 67f08091e979d5cd2902b3fd22165816eaaf8a4d..42dffb2d0afce319d9299b99bad30433b7ef6728 100644
--- a/test/net/sf/briar/protocol/AckReaderTest.java
+++ b/test/net/sf/briar/protocol/AckReaderTest.java
@@ -7,12 +7,12 @@ import java.util.Collections;
 import java.util.Random;
 
 import junit.framework.TestCase;
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.Ack;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
 import net.sf.briar.api.serial.Writer;
diff --git a/test/net/sf/briar/protocol/BatchReaderTest.java b/test/net/sf/briar/protocol/BatchReaderTest.java
index 9c7407dbbaebdae5f17f4f0b8af1a9f8beac2792..8306b683eb9138dacfd6ac976e6766400d610099 100644
--- a/test/net/sf/briar/protocol/BatchReaderTest.java
+++ b/test/net/sf/briar/protocol/BatchReaderTest.java
@@ -7,13 +7,13 @@ import java.security.MessageDigest;
 import java.util.Collections;
 
 import junit.framework.TestCase;
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.BatchId;
 import net.sf.briar.api.protocol.Message;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Tags;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
diff --git a/test/net/sf/briar/protocol/ConsumersTest.java b/test/net/sf/briar/protocol/ConsumersTest.java
index fbd839d9b43a0ae117e56e785f2581f960b5a03f..cb03476b616886a23c6e0924eb978d6089b8efab 100644
--- a/test/net/sf/briar/protocol/ConsumersTest.java
+++ b/test/net/sf/briar/protocol/ConsumersTest.java
@@ -7,8 +7,8 @@ import java.util.Arrays;
 import java.util.Random;
 
 import junit.framework.TestCase;
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.crypto.CryptoModule;
 
 import org.junit.Before;
diff --git a/test/net/sf/briar/protocol/RequestReaderTest.java b/test/net/sf/briar/protocol/RequestReaderTest.java
index 5b10e496488f01a7796080b43d607494bd4ac442..8be4c17049b808a2f7357cfd73c359796cfdb379 100644
--- a/test/net/sf/briar/protocol/RequestReaderTest.java
+++ b/test/net/sf/briar/protocol/RequestReaderTest.java
@@ -5,12 +5,12 @@ import java.io.ByteArrayOutputStream;
 import java.util.BitSet;
 
 import junit.framework.TestCase;
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.protocol.OfferId;
 import net.sf.briar.api.protocol.ProtocolConstants;
 import net.sf.briar.api.protocol.Request;
 import net.sf.briar.api.protocol.Tags;
 import net.sf.briar.api.protocol.UniqueId;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.api.serial.ReaderFactory;
 import net.sf.briar.api.serial.Writer;
diff --git a/test/net/sf/briar/serial/ReaderImplTest.java b/test/net/sf/briar/serial/ReaderImplTest.java
index 28670b8b91751fb013d46a5a7da4ce5166757488..ddd310accb9d726971b643952e501fc2d5a8dbba 100644
--- a/test/net/sf/briar/serial/ReaderImplTest.java
+++ b/test/net/sf/briar/serial/ReaderImplTest.java
@@ -11,8 +11,8 @@ import java.util.Map.Entry;
 
 import junit.framework.TestCase;
 import net.sf.briar.api.Bytes;
+import net.sf.briar.api.FormatException;
 import net.sf.briar.api.serial.Consumer;
-import net.sf.briar.api.serial.FormatException;
 import net.sf.briar.api.serial.ObjectReader;
 import net.sf.briar.api.serial.Reader;
 import net.sf.briar.util.StringUtils;
diff --git a/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java b/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..9bf306fd844dc7470d0a80e28c5b1fc75e876e87
--- /dev/null
+++ b/test/net/sf/briar/transport/ConnectionDecrypterImplTest.java
@@ -0,0 +1,100 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
+
+import java.io.ByteArrayInputStream;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.crypto.CryptoModule;
+
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class ConnectionDecrypterImplTest extends TestCase {
+
+	private static final int MAC_LENGTH = 32;
+
+	private final Cipher frameCipher;
+	private final SecretKey frameKey;
+	private final int transportId = 1234;
+	private final long connection = 12345L;
+
+	public ConnectionDecrypterImplTest() {
+		super();
+		Injector i = Guice.createInjector(new CryptoModule());
+		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
+		frameCipher = crypto.getFrameCipher();
+		frameKey = crypto.generateSecretKey();
+	}
+
+	@Test
+	public void testSingleByteFrame() throws Exception {
+		// Create a fake ciphertext frame: one byte plus a MAC
+		byte[] ciphertext = new byte[1 + MAC_LENGTH];
+		ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
+		// Check that one byte plus a MAC can be read
+		ConnectionDecrypter d = new ConnectionDecrypterImpl(in, transportId,
+				connection, frameCipher, frameKey);
+		assertFalse(d.getInputStream().read() == -1);
+		d.readMac(new byte[MAC_LENGTH]);
+		assertTrue(d.getInputStream().read() == -1);
+	}
+
+	@Test
+	public void testDecryption() throws Exception {
+		// Calculate the expected plaintext for the first frame
+		byte[] ciphertext = new byte[123];
+		byte[] ivBytes = new byte[TAG_LENGTH];
+		TagEncoder.encodeTag(ivBytes, transportId, connection, 0L);
+		IvParameterSpec iv = new IvParameterSpec(ivBytes);
+		frameCipher.init(Cipher.DECRYPT_MODE, frameKey, iv);
+		byte[] plaintext = frameCipher.doFinal(ciphertext);
+		// Calculate the expected plaintext for the second frame
+		byte[] ciphertext1 = new byte[1234];
+		TagEncoder.encodeTag(ivBytes, transportId, connection, 1L);
+		iv = new IvParameterSpec(ivBytes);
+		frameCipher.init(Cipher.DECRYPT_MODE, frameKey, iv);
+		byte[] plaintext1 = frameCipher.doFinal(ciphertext1);
+		assertEquals(ciphertext1.length, plaintext1.length);
+		// Concatenate the ciphertexts
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(ciphertext);
+		out.write(ciphertext1);
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		// Use a ConnectionDecrypter to decrypt the ciphertext
+		ConnectionDecrypter d = new ConnectionDecrypterImpl(in, transportId,
+				connection, frameCipher, frameKey);
+		// First frame
+		byte[] decrypted = new byte[plaintext.length - MAC_LENGTH];
+		TestUtils.readFully(d.getInputStream(), decrypted);
+		byte[] decryptedMac = new byte[MAC_LENGTH];
+		d.readMac(decryptedMac);
+		// Second frame
+		byte[] decrypted1 = new byte[plaintext1.length - MAC_LENGTH];
+		TestUtils.readFully(d.getInputStream(), decrypted1);
+		byte[] decryptedMac1 = new byte[MAC_LENGTH];
+		d.readMac(decryptedMac1);
+		// Check that the actual plaintext matches the expected plaintext
+		out.reset();
+		out.write(plaintext);
+		out.write(plaintext1);
+		byte[] expected = out.toByteArray();
+		out.reset();
+		out.write(decrypted);
+		out.write(decryptedMac);
+		out.write(decrypted1);
+		out.write(decryptedMac1);
+		byte[] actual = out.toByteArray();
+		assertTrue(Arrays.equals(expected, actual));
+	}
+}
diff --git a/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java b/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b3a9145d2d3811e0fb18ab6eec23dc68d3acb64
--- /dev/null
+++ b/test/net/sf/briar/transport/ConnectionEncrypterImplTest.java
@@ -0,0 +1,98 @@
+package net.sf.briar.transport;
+
+import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
+
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+import javax.crypto.Cipher;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.IvParameterSpec;
+
+import junit.framework.TestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.crypto.CryptoModule;
+
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class ConnectionEncrypterImplTest extends TestCase {
+
+	private static final int MAC_LENGTH = 32;
+
+	private final Cipher tagCipher, frameCipher;
+	private final SecretKey tagKey, frameKey;
+	private final int transportId = 1234;
+	private final long connection = 12345L;
+
+	public ConnectionEncrypterImplTest() {
+		super();
+		Injector i = Guice.createInjector(new CryptoModule());
+		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
+		tagCipher = crypto.getTagCipher();
+		frameCipher = crypto.getFrameCipher();
+		tagKey = crypto.generateSecretKey();
+		frameKey = crypto.generateSecretKey();
+	}
+
+	@Test
+	public void testSingleByteFrame() throws Exception {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new ConnectionEncrypterImpl(out, transportId,
+				connection, tagCipher, frameCipher, tagKey, frameKey);
+		e.getOutputStream().write((byte) 0);
+		e.writeMac(new byte[MAC_LENGTH]);
+		assertEquals(TAG_LENGTH + 1 + MAC_LENGTH, out.toByteArray().length);
+	}
+
+	@Test
+	public void testEncryption() throws Exception {
+		// Calculate the expected ciphertext for the tag
+		byte[] plaintextTag = TagEncoder.encodeTag(transportId, connection);
+		assertEquals(TAG_LENGTH, plaintextTag.length);
+		tagCipher.init(Cipher.ENCRYPT_MODE, tagKey);
+		byte[] tag = tagCipher.doFinal(plaintextTag);
+		assertEquals(TAG_LENGTH, tag.length);
+		// Calculate the expected ciphertext for the first frame
+		byte[] plaintext = new byte[123];
+		byte[] plaintextMac = new byte[MAC_LENGTH];
+		byte[] ivBytes = new byte[TAG_LENGTH];
+		TagEncoder.encodeTag(ivBytes, transportId, connection, 0L);
+		IvParameterSpec iv = new IvParameterSpec(ivBytes);
+		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, iv);
+		byte[] ciphertext = new byte[plaintext.length + plaintextMac.length];
+		int offset = frameCipher.update(plaintext, 0, plaintext.length,
+				ciphertext);
+		frameCipher.doFinal(plaintextMac, 0, plaintextMac.length, ciphertext,
+				offset);
+		// Calculate the expected ciphertext for the second frame
+		byte[] plaintext1 = new byte[1234];
+		TagEncoder.encodeTag(ivBytes, transportId, connection, 1L);
+		iv = new IvParameterSpec(ivBytes);
+		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, iv);
+		byte[] ciphertext1 = new byte[plaintext1.length + plaintextMac.length];
+		offset = frameCipher.update(plaintext1, 0, plaintext1.length,
+				ciphertext1);
+		frameCipher.doFinal(plaintextMac, 0, plaintextMac.length, ciphertext1,
+				offset);
+		// Concatenate the ciphertexts
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(tag);
+		out.write(ciphertext);
+		out.write(ciphertext1);
+		byte[] expected = out.toByteArray();
+		// Use a ConnectionEncrypter to encrypt the plaintext
+		out.reset();
+		ConnectionEncrypter e = new ConnectionEncrypterImpl(out, transportId,
+				connection, tagCipher, frameCipher, tagKey, frameKey);
+		e.getOutputStream().write(plaintext);
+		e.writeMac(plaintextMac);
+		e.getOutputStream().write(plaintext1);
+		e.writeMac(plaintextMac);
+		byte[] actual = out.toByteArray();
+		// Check that the actual ciphertext matches the expected ciphertext
+		assertTrue(Arrays.equals(expected, actual));
+	}
+}
diff --git a/test/net/sf/briar/transport/ConnectionReaderImplTest.java b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d73e43676e82a382dbfdd90fa3886447da9cf719
--- /dev/null
+++ b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
@@ -0,0 +1,106 @@
+package net.sf.briar.transport;
+
+import java.io.ByteArrayInputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+
+import junit.framework.TestCase;
+import net.sf.briar.TestUtils;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.transport.ConnectionReader;
+import net.sf.briar.crypto.CryptoModule;
+import net.sf.briar.util.ByteUtils;
+
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class ConnectionReaderImplTest extends TestCase {
+
+	private final Mac mac;
+
+	public ConnectionReaderImplTest() throws Exception {
+		super();
+		Injector i = Guice.createInjector(new CryptoModule());
+		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
+		mac = crypto.getMac();
+		mac.init(crypto.generateSecretKey());
+	}
+
+	@Test
+	public void testSingleByteFrame() throws Exception {
+		// Six bytes for the header, one for the payload
+		byte[] frame = new byte[6 + 1 + mac.getMacLength()];
+		ByteUtils.writeUint16(1, frame, 4); // Payload length = 1
+		// Calculate the MAC
+		mac.update(frame, 0, 6 + 1);
+		mac.doFinal(frame, 6 + 1);
+		// Read the frame
+		ByteArrayInputStream in = new ByteArrayInputStream(frame);
+		ConnectionDecrypter d = new NullConnectionDecrypter(in);
+		ConnectionReader r = new ConnectionReaderImpl(d, mac);
+		// There should be one byte available before EOF
+		assertEquals(0, r.getInputStream().read());
+		assertEquals(-1, r.getInputStream().read());
+	}
+
+	@Test
+	public void testMultipleFrames() throws Exception {
+		// First frame: 123-byte payload
+		byte[] frame = new byte[6 + 123 + mac.getMacLength()];
+		ByteUtils.writeUint16(123, frame, 4);
+		mac.update(frame, 0, 6 + 123);
+		mac.doFinal(frame, 6 + 123);
+		// Second frame: 1234-byte payload
+		byte[] frame1 = new byte[6 + 1234 + mac.getMacLength()];
+		ByteUtils.writeUint32(1, frame1, 0);
+		ByteUtils.writeUint16(1234, frame1, 4);
+		mac.update(frame1, 0, 6 + 1234);
+		mac.doFinal(frame1, 6 + 1234);
+		// Concatenate the frames
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(frame);
+		out.write(frame1);
+		// Read the frames
+		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
+		ConnectionDecrypter d = new NullConnectionDecrypter(in);
+		ConnectionReader r = new ConnectionReaderImpl(d, mac);
+		byte[] read = new byte[123];
+		TestUtils.readFully(r.getInputStream(), read);
+		assertTrue(Arrays.equals(new byte[123], read));
+		byte[] read1 = new byte[1234];
+		TestUtils.readFully(r.getInputStream(), read1);
+		assertTrue(Arrays.equals(new byte[1234], read1));
+	}
+
+	/** A ConnectionDecrypter that performs no decryption. */
+	private static class NullConnectionDecrypter
+	implements ConnectionDecrypter {
+
+		private final InputStream in;
+
+		private NullConnectionDecrypter(InputStream in) {
+			this.in = in;
+		}
+
+		public InputStream getInputStream() {
+			return in;
+		}
+
+		public void readMac(byte[] mac) throws IOException {
+			int offset = 0;
+			while(offset < mac.length) {
+				int read = in.read(mac, offset, mac.length - offset);
+				if(read == -1) break;
+				offset += read;
+			}
+			if(offset < mac.length) throw new EOFException();
+		}
+	}
+}
diff --git a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
index 52ed9148b5b6db6760ba0976404e9c390c77fd65..c94b242a8a57c9538276596b77517afc7604316b 100644
--- a/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionRecogniserImplTest.java
@@ -65,7 +65,7 @@ public class ConnectionRecogniserImplTest extends TestCase {
 		SecretKey tagKey = crypto.deriveIncomingTagKey(secret);
 		Cipher tagCipher = crypto.getTagCipher();
 		tagCipher.init(Cipher.ENCRYPT_MODE, tagKey);
-		byte[] tag = TagEncoder.encodeTag(transportId, 3L, 0);
+		byte[] tag = TagEncoder.encodeTag(transportId, 3L);
 		byte[] encryptedTag = tagCipher.doFinal(tag);
 
 		Mockery context = new Mockery();
diff --git a/test/net/sf/briar/transport/ConnectionWriterImplTest.java b/test/net/sf/briar/transport/ConnectionWriterImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..4892d58c9724296d8e9c08ecf37b14534d3c9f50
--- /dev/null
+++ b/test/net/sf/briar/transport/ConnectionWriterImplTest.java
@@ -0,0 +1,98 @@
+package net.sf.briar.transport;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Arrays;
+
+import javax.crypto.Mac;
+
+import junit.framework.TestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.transport.ConnectionWriter;
+import net.sf.briar.crypto.CryptoModule;
+import net.sf.briar.util.ByteUtils;
+
+import org.junit.Test;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+
+public class ConnectionWriterImplTest extends TestCase {
+
+	private final Mac mac;
+
+	public ConnectionWriterImplTest() throws Exception {
+		super();
+		Injector i = Guice.createInjector(new CryptoModule());
+		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
+		mac = crypto.getMac();
+		mac.init(crypto.generateSecretKey());
+	}
+
+	@Test
+	public void testSingleByteFrame() throws Exception {
+		// Six bytes for the header, one for the payload
+		byte[] frame = new byte[6 + 1 + mac.getMacLength()];
+		ByteUtils.writeUint16(1, frame, 4); // Payload length = 1
+		// Calculate the MAC
+		mac.update(frame, 0, 6 + 1);
+		mac.doFinal(frame, 6 + 1);
+		// Check that the ConnectionWriter gets the same results
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		ConnectionWriter w = new ConnectionWriterImpl(e, mac);
+		w.getOutputStream().write(0);
+		w.getOutputStream().flush();
+		assertTrue(Arrays.equals(frame, out.toByteArray()));
+	}
+
+	@Test
+	public void testMultipleFrames() throws Exception {
+		// First frame: 123-byte payload
+		byte[] frame = new byte[6 + 123 + mac.getMacLength()];
+		ByteUtils.writeUint16(123, frame, 4);
+		mac.update(frame, 0, 6 + 123);
+		mac.doFinal(frame, 6 + 123);
+		// Second frame: 1234-byte payload
+		byte[] frame1 = new byte[6 + 1234 + mac.getMacLength()];
+		ByteUtils.writeUint32(1, frame1, 0);
+		ByteUtils.writeUint16(1234, frame1, 4);
+		mac.update(frame1, 0, 6 + 1234);
+		mac.doFinal(frame1, 6 + 1234);
+		// Concatenate the frames
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		out.write(frame);
+		out.write(frame1);
+		byte[] expected = out.toByteArray();
+		// Check that the ConnectionWriter gets the same results
+		out.reset();
+		ConnectionEncrypter e = new NullConnectionEncrypter(out);
+		ConnectionWriter w = new ConnectionWriterImpl(e, mac);
+		w.getOutputStream().write(new byte[123]);
+		w.getOutputStream().flush();
+		w.getOutputStream().write(new byte[1234]);
+		w.getOutputStream().flush();
+		byte[] actual = out.toByteArray();
+		assertTrue(Arrays.equals(expected, actual));
+	}
+
+	/** A ConnectionEncrypter that performs no encryption. */
+	private static class NullConnectionEncrypter
+	implements ConnectionEncrypter {
+
+		private final OutputStream out;
+
+		private NullConnectionEncrypter(OutputStream out) {
+			this.out = out;
+		}
+
+		public OutputStream getOutputStream() {
+			return out;
+		}
+
+		public void writeMac(byte[] mac) throws IOException {
+			out.write(mac);
+		}
+	}
+}
diff --git a/test/net/sf/briar/transport/PacketReadWriteTest.java b/test/net/sf/briar/transport/FrameReadWriteTest.java
similarity index 53%
rename from test/net/sf/briar/transport/PacketReadWriteTest.java
rename to test/net/sf/briar/transport/FrameReadWriteTest.java
index 27b1f35491103804c773a42c5237103f566a7347..7fb8ef717728d91e14b14aa7a2e4bceb4fbe4187 100644
--- a/test/net/sf/briar/transport/PacketReadWriteTest.java
+++ b/test/net/sf/briar/transport/FrameReadWriteTest.java
@@ -15,8 +15,8 @@ import javax.crypto.SecretKey;
 
 import junit.framework.TestCase;
 import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.transport.PacketReader;
-import net.sf.briar.api.transport.PacketWriter;
+import net.sf.briar.api.transport.ConnectionReader;
+import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.crypto.CryptoModule;
 
 import org.junit.Test;
@@ -24,60 +24,66 @@ import org.junit.Test;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
-public class PacketReadWriteTest extends TestCase {
+public class FrameReadWriteTest extends TestCase {
 
 	private final CryptoComponent crypto;
-	private final Cipher tagCipher, packetCipher;
-	private final SecretKey macKey, tagKey, packetKey;
+	private final Cipher tagCipher, frameCipher;
+	private final SecretKey macKey, tagKey, frameKey;
 	private final Mac mac;
 	private final Random random;
 	private final byte[] secret = new byte[100];
 	private final int transportId = 999;
 	private final long connection = 1234L;
 
-	public PacketReadWriteTest() {
+	public FrameReadWriteTest() {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule());
 		crypto = i.getInstance(CryptoComponent.class);
 		tagCipher = crypto.getTagCipher();
-		packetCipher = crypto.getPacketCipher();
+		frameCipher = crypto.getFrameCipher();
 		// Since we're sending packets to ourselves, we only need outgoing keys
 		macKey = crypto.deriveOutgoingMacKey(secret);
 		tagKey = crypto.deriveOutgoingTagKey(secret);
-		packetKey = crypto.deriveOutgoingPacketKey(secret);
+		frameKey = crypto.deriveOutgoingFrameKey(secret);
 		mac = crypto.getMac();
 		random = new Random();
 	}
 
 	@Test
 	public void testWriteAndRead() throws Exception {
-		// Generate two random packets
-		byte[] packet = new byte[12345];
-		random.nextBytes(packet);
-		byte[] packet1 = new byte[321];
-		random.nextBytes(packet1);
-		// Write the packets
+		// Calculate the expected ciphertext for the tag
+		byte[] plaintextTag = TagEncoder.encodeTag(transportId, connection);
+		assertEquals(TAG_LENGTH, plaintextTag.length);
+		tagCipher.init(Cipher.ENCRYPT_MODE, tagKey);
+		byte[] tag = tagCipher.doFinal(plaintextTag);
+		assertEquals(TAG_LENGTH, tag.length);
+		// Generate two random frames
+		byte[] frame = new byte[12345];
+		random.nextBytes(frame);
+		byte[] frame1 = new byte[321];
+		random.nextBytes(frame1);
+		// Write the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter encrypter = new PacketEncrypterImpl(out, tagCipher,
-				packetCipher, tagKey, packetKey);
+		ConnectionEncrypter encrypter = new ConnectionEncrypterImpl(out,
+				transportId, connection, tagCipher, frameCipher, tagKey,
+				frameKey);
 		mac.init(macKey);
-		PacketWriter writer = new PacketWriterImpl(encrypter, mac, transportId,
-				connection);
+		ConnectionWriter writer = new ConnectionWriterImpl(encrypter, mac);
 		OutputStream out1 = writer.getOutputStream();
-		out1.write(packet);
-		writer.finishPacket();
-		out1.write(packet1);
-		writer.finishPacket();
-		// Read the packets back
+		out1.write(frame);
+		out1.flush();
+		out1.write(frame1);
+		out1.flush();
+		// Read the frames back
 		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
-		byte[] firstTag = new byte[TAG_LENGTH];
-		assertEquals(TAG_LENGTH, in.read(firstTag));
-		PacketDecrypter decrypter = new PacketDecrypterImpl(firstTag, in,
-				tagCipher, packetCipher, tagKey, packetKey);
-		PacketReader reader = new PacketReaderImpl(decrypter, mac, transportId,
-				connection);
+		byte[] recoveredTag = new byte[TAG_LENGTH];
+		assertEquals(TAG_LENGTH, in.read(recoveredTag));
+		assertTrue(Arrays.equals(tag, recoveredTag));
+		ConnectionDecrypter decrypter = new ConnectionDecrypterImpl(in,
+				transportId, connection, frameCipher, frameKey);
+		ConnectionReader reader = new ConnectionReaderImpl(decrypter, mac);
 		InputStream in1 = reader.getInputStream();
-		byte[] recovered = new byte[packet.length];
+		byte[] recovered = new byte[frame.length];
 		int offset = 0;
 		while(offset < recovered.length) {
 			int read = in1.read(recovered, offset, recovered.length - offset);
@@ -85,9 +91,8 @@ public class PacketReadWriteTest extends TestCase {
 			offset += read;
 		}
 		assertEquals(recovered.length, offset);
-		reader.finishPacket();
-		assertTrue(Arrays.equals(packet, recovered));
-		byte[] recovered1 = new byte[packet1.length];
+		assertTrue(Arrays.equals(frame, recovered));
+		byte[] recovered1 = new byte[frame1.length];
 		offset = 0;
 		while(offset < recovered1.length) {
 			int read = in1.read(recovered1, offset, recovered1.length - offset);
@@ -95,7 +100,6 @@ public class PacketReadWriteTest extends TestCase {
 			offset += read;
 		}
 		assertEquals(recovered1.length, offset);
-		reader.finishPacket();
-		assertTrue(Arrays.equals(packet1, recovered1));
+		assertTrue(Arrays.equals(frame1, recovered1));
 	}
 }
diff --git a/test/net/sf/briar/transport/PacketDecrypterImplTest.java b/test/net/sf/briar/transport/PacketDecrypterImplTest.java
deleted file mode 100644
index c001a20aaf6b99168b276795d11e8d86ce4f043e..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/PacketDecrypterImplTest.java
+++ /dev/null
@@ -1,115 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.io.ByteArrayInputStream;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-
-import junit.framework.TestCase;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.crypto.CryptoModule;
-
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class PacketDecrypterImplTest extends TestCase {
-
-	private final Cipher tagCipher, packetCipher;
-	private final SecretKey tagKey, packetKey;
-
-	public PacketDecrypterImplTest() {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		tagCipher = crypto.getTagCipher();
-		packetCipher = crypto.getPacketCipher();
-		tagKey = crypto.generateSecretKey();
-		packetKey = crypto.generateSecretKey();
-	}
-
-	@Test
-	public void testSingleBytePackets() throws Exception {
-		byte[] ciphertext = new byte[(TAG_LENGTH + 1) * 2];
-		ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
-		byte[] firstTag = new byte[TAG_LENGTH];
-		assertEquals(TAG_LENGTH, in.read(firstTag));
-		PacketDecrypter p = new PacketDecrypterImpl(firstTag, in, tagCipher,
-				packetCipher, tagKey, packetKey);
-		byte[] decryptedTag = p.readTag();
-		assertEquals(TAG_LENGTH, decryptedTag.length);
-		assertTrue(p.getInputStream().read() > -1);
-		byte[] decryptedTag1 = p.readTag();
-		assertEquals(TAG_LENGTH, decryptedTag1.length);
-		assertTrue(p.getInputStream().read() > -1);		
-	}
-
-	@Test
-	public void testDecryption() throws Exception {
-		byte[] tag = new byte[TAG_LENGTH];
-		byte[] packet = new byte[123];
-		byte[] tag1 = new byte[TAG_LENGTH];
-		byte[] packet1 = new byte[234];
-		// Calculate the first expected decrypted tag
-		tagCipher.init(Cipher.DECRYPT_MODE, tagKey);
-		byte[] expectedTag = tagCipher.doFinal(tag);
-		assertEquals(tag.length, expectedTag.length);
-		// Calculate the first expected decrypted packet
-		IvParameterSpec iv = new IvParameterSpec(expectedTag);
-		packetCipher.init(Cipher.DECRYPT_MODE, packetKey, iv);
-		byte[] expectedPacket = packetCipher.doFinal(packet);
-		assertEquals(packet.length, expectedPacket.length);
-		// Calculate the second expected decrypted tag
-		tagCipher.init(Cipher.DECRYPT_MODE, tagKey);
-		byte[] expectedTag1 = tagCipher.doFinal(tag1);
-		assertEquals(tag1.length, expectedTag1.length);
-		// Calculate the second expected decrypted packet
-		IvParameterSpec iv1 = new IvParameterSpec(expectedTag1);
-		packetCipher.init(Cipher.DECRYPT_MODE, packetKey, iv1);
-		byte[] expectedPacket1 = packetCipher.doFinal(packet1);
-		assertEquals(packet1.length, expectedPacket1.length);
-		// Check that the PacketDecrypter gets the same results
-		byte[] ciphertext = new byte[tag.length + packet.length
-		                             + tag1.length + packet1.length];
-		System.arraycopy(tag, 0, ciphertext, 0, tag.length);
-		System.arraycopy(packet, 0, ciphertext, tag.length, packet.length);
-		System.arraycopy(tag1, 0, ciphertext, tag.length + packet.length,
-				tag1.length);
-		System.arraycopy(packet1, 0, ciphertext,
-				tag.length + packet.length + tag1.length, packet1.length);
-		ByteArrayInputStream in = new ByteArrayInputStream(ciphertext);
-		PacketDecrypter p = new PacketDecrypterImpl(tag, in, tagCipher,
-				packetCipher, tagKey, packetKey);
-		// First tag
-		assertTrue(Arrays.equals(expectedTag, p.readTag()));
-		// First packet
-		byte[] actualPacket = new byte[packet.length];
-		int offset = 0;
-		while(offset < actualPacket.length) {
-			int read = p.getInputStream().read(actualPacket, offset,
-					actualPacket.length - offset);
-			if(read == -1) break;
-			offset += read;
-		}
-		assertEquals(actualPacket.length, offset);
-		assertTrue(Arrays.equals(expectedPacket, actualPacket));
-		// Second tag
-		assertTrue(Arrays.equals(expectedTag1, p.readTag()));
-		// Second packet
-		byte[] actualPacket1 = new byte[packet1.length];
-		offset = 0;
-		while(offset < actualPacket1.length) {
-			int read = p.getInputStream().read(actualPacket1, offset,
-					actualPacket1.length - offset);
-			if(read == -1) break;
-			offset += read;
-		}
-		assertEquals(actualPacket1.length, offset);
-		assertTrue(Arrays.equals(expectedPacket1, actualPacket1));
-	}
-}
diff --git a/test/net/sf/briar/transport/PacketEncrypterImplTest.java b/test/net/sf/briar/transport/PacketEncrypterImplTest.java
deleted file mode 100644
index 06ea1b66a910fc7e4f2a63cf2b1895b1a35abbf4..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/PacketEncrypterImplTest.java
+++ /dev/null
@@ -1,79 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.io.ByteArrayOutputStream;
-import java.util.Arrays;
-
-import javax.crypto.Cipher;
-import javax.crypto.SecretKey;
-import javax.crypto.spec.IvParameterSpec;
-
-import junit.framework.TestCase;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.crypto.CryptoModule;
-
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class PacketEncrypterImplTest extends TestCase {
-
-	private final Cipher tagCipher, packetCipher;
-	private final SecretKey tagKey, packetKey;
-
-	public PacketEncrypterImplTest() {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		tagCipher = crypto.getTagCipher();
-		packetCipher = crypto.getPacketCipher();
-		tagKey = crypto.generateSecretKey();
-		packetKey = crypto.generateSecretKey();
-	}
-
-	@Test
-	public void testSingleBytePacket() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter p = new PacketEncrypterImpl(out, tagCipher,
-				packetCipher, tagKey, packetKey);
-		p.writeTag(new byte[TAG_LENGTH]);
-		p.getOutputStream().write((byte) 0);
-		p.finishPacket();
-		assertEquals(TAG_LENGTH + 1, out.toByteArray().length);
-	}
-
-	@Test
-	public void testEncryption() throws Exception {
-		byte[] tag = new byte[TAG_LENGTH];
-		byte[] packet = new byte[123];
-		// Calculate the expected encrypted tag
-		tagCipher.init(Cipher.ENCRYPT_MODE, tagKey);
-		byte[] expectedTag = tagCipher.doFinal(tag);
-		assertEquals(tag.length, expectedTag.length);
-		// Calculate the expected encrypted packet
-		IvParameterSpec iv = new IvParameterSpec(tag);
-		packetCipher.init(Cipher.ENCRYPT_MODE, packetKey, iv);
-		byte[] expectedPacket = packetCipher.doFinal(packet);
-		assertEquals(packet.length, expectedPacket.length);
-		// Check that the PacketEncrypter gets the same results
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter p = new PacketEncrypterImpl(out, tagCipher,
-				packetCipher, tagKey, packetKey);
-		p.writeTag(tag);
-		p.getOutputStream().write(packet);
-		p.finishPacket();
-		byte[] ciphertext = out.toByteArray();
-		assertEquals(TAG_LENGTH + packet.length, ciphertext.length);
-		// Check the tag
-		byte[] actualTag = new byte[TAG_LENGTH];
-		System.arraycopy(ciphertext, 0, actualTag, 0, TAG_LENGTH);
-		assertTrue(Arrays.equals(expectedTag, actualTag));
-		// Check the packet
-		byte[] actualPacket = new byte[packet.length];
-		System.arraycopy(ciphertext, TAG_LENGTH, actualPacket, 0,
-				actualPacket.length);
-		assertTrue(Arrays.equals(expectedPacket, actualPacket));
-	}
-}
diff --git a/test/net/sf/briar/transport/PacketReaderImplTest.java b/test/net/sf/briar/transport/PacketReaderImplTest.java
deleted file mode 100644
index d5e34c89c9c4e69dc96a210bc1997c5813cfe8e1..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/PacketReaderImplTest.java
+++ /dev/null
@@ -1,189 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.io.ByteArrayInputStream;
-import java.io.EOFException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.security.GeneralSecurityException;
-import java.util.Arrays;
-
-import javax.crypto.Mac;
-
-import junit.framework.TestCase;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.transport.PacketReader;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.util.StringUtils;
-
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class PacketReaderImplTest extends TestCase {
-
-	private final Mac mac;
-
-	public PacketReaderImplTest() throws Exception {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		mac = crypto.getMac();
-		mac.init(crypto.generateSecretKey());
-	}
-
-	@Test
-	public void testFirstReadTriggersTag() throws Exception {
-		// TAG_BYTES for the tag, 1 byte for the packet
-		byte[] b = new byte[TAG_LENGTH + 1];
-		ByteArrayInputStream in = new ByteArrayInputStream(b);
-		PacketDecrypter d = new NullPacketDecrypter(in);
-		PacketReader p = new PacketReaderImpl(d, mac, 0, 0L);
-		// There should be one byte available before EOF
-		assertEquals(0, p.getInputStream().read());
-		assertEquals(-1, p.getInputStream().read());
-	}
-
-	@Test
-	public void testFinishPacketAfterReadTriggersMac() throws Exception {
-		// TAG_BYTES for the tag, 1 byte for the packet
-		byte[] b = new byte[TAG_LENGTH + 1];
-		// Calculate the MAC and append it to the packet
-		mac.update(b);
-		byte[] macBytes = mac.doFinal();
-		byte[] b1 = Arrays.copyOf(b, b.length + macBytes.length);
-		System.arraycopy(macBytes, 0, b1, b.length, macBytes.length);
-		// Check that the PacketReader reads and verifies the MAC
-		ByteArrayInputStream in = new ByteArrayInputStream(b1);
-		PacketDecrypter d = new NullPacketDecrypter(in);
-		PacketReader p = new PacketReaderImpl(d, mac, 0, 0L);
-		assertEquals(0, p.getInputStream().read());
-		p.finishPacket();
-		// Reading the MAC should take us to EOF
-		assertEquals(-1, p.getInputStream().read());
-	}
-
-	@Test
-	public void testModifyingPacketInvalidatesMac() throws Exception {
-		// TAG_BYTES for the tag, 1 byte for the packet
-		byte[] b = new byte[TAG_LENGTH + 1];
-		// Calculate the MAC and append it to the packet
-		mac.update(b);
-		byte[] macBytes = mac.doFinal();
-		byte[] b1 = Arrays.copyOf(b, b.length + macBytes.length);
-		System.arraycopy(macBytes, 0, b1, b.length, macBytes.length);
-		// Modify the packet
-		b1[TAG_LENGTH] = (byte) 1;
-		// Check that the PacketReader reads and fails to verify the MAC
-		ByteArrayInputStream in = new ByteArrayInputStream(b1);
-		PacketDecrypter d = new NullPacketDecrypter(in);
-		PacketReader p = new PacketReaderImpl(d, mac, 0, 0L);
-		assertEquals(1, p.getInputStream().read());
-		try {
-			p.finishPacket();
-			fail();
-		} catch(GeneralSecurityException expected) {}
-	}
-
-	@Test
-	public void testExtraCallsToFinishPacketDoNothing() throws Exception {
-		// TAG_BYTES for the tag, 1 byte for the packet
-		byte[] b = new byte[TAG_LENGTH + 1];
-		// Calculate the MAC and append it to the packet
-		mac.update(b);
-		byte[] macBytes = mac.doFinal();
-		byte[] b1 = Arrays.copyOf(b, b.length + macBytes.length);
-		System.arraycopy(macBytes, 0, b1, b.length, macBytes.length);
-		// Check that the PacketReader reads and verifies the MAC
-		ByteArrayInputStream in = new ByteArrayInputStream(b1);
-		PacketDecrypter d = new NullPacketDecrypter(in);
-		PacketReader p = new PacketReaderImpl(d, mac, 0, 0L);
-		// Initial calls to finishPacket() should have no effect
-		p.finishPacket();
-		p.finishPacket();
-		p.finishPacket();
-		assertEquals(0, p.getInputStream().read());
-		p.finishPacket();
-		// Extra calls to finishPacket() should have no effect
-		p.finishPacket();
-		p.finishPacket();
-		p.finishPacket();
-		// Reading the MAC should take us to EOF
-		assertEquals(-1, p.getInputStream().read());
-	}
-
-	@Test
-	public void testPacketNumberIsIncremented() throws Exception {
-		byte[] tag = StringUtils.fromHexString(
-				"0000" // 16 bits reserved
-				+ "F00D" // 16 bits for the transport ID
-				+ "DEADBEEF" // 32 bits for the connection number
-				+ "00000000" // 32 bits for the packet number
-				+ "00000000" // 32 bits for the block number
-		);
-		assertEquals(TAG_LENGTH, tag.length);
-		byte[] tag1 = StringUtils.fromHexString(
-				"0000" // 16 bits reserved
-				+ "F00D" // 16 bits for the transport ID
-				+ "DEADBEEF" // 32 bits for the connection number
-				+ "00000001" // 32 bits for the packet number
-				+ "00000000" // 32 bits for the block number
-		);
-		assertEquals(TAG_LENGTH, tag1.length);
-		// Calculate the MAC on the first packet and append it to the packet
-		mac.update(tag);
-		mac.update((byte) 0);
-		byte[] macBytes = mac.doFinal();
-		byte[] b = Arrays.copyOf(tag, tag.length + 1 + macBytes.length);
-		System.arraycopy(macBytes, 0, b, tag.length + 1, macBytes.length);
-		// Calculate the MAC on the second packet and append it to the packet
-		mac.update(tag1);
-		mac.update((byte) 0);
-		byte[] macBytes1 = mac.doFinal();
-		byte[] b1 = Arrays.copyOf(tag1, tag1.length + 1 + macBytes1.length);
-		System.arraycopy(macBytes1, 0, b1, tag.length + 1, macBytes1.length);
-		// Check that the PacketReader accepts the correct tags and MACs
-		byte[] b2 = Arrays.copyOf(b, b.length + b1.length);
-		System.arraycopy(b1, 0, b2, b.length, b1.length);
-		ByteArrayInputStream in = new ByteArrayInputStream(b2);
-		PacketDecrypter d = new NullPacketDecrypter(in);
-		PacketReader p = new PacketReaderImpl(d, mac, 0xF00D, 0xDEADBEEFL);
-		// Packet one
-		assertEquals(0, p.getInputStream().read());
-		p.finishPacket();
-		// Packet two
-		assertEquals(0, p.getInputStream().read());
-		p.finishPacket();
-		// We should be at EOF
-		assertEquals(-1, p.getInputStream().read());
-	}
-
-	/** A PacketDecrypter that performs no decryption. */
-	private static class NullPacketDecrypter implements PacketDecrypter {
-
-		private final InputStream in;
-
-		private NullPacketDecrypter(InputStream in) {
-			this.in = in;
-		}
-
-		public InputStream getInputStream() {
-			return in;
-		}
-
-		public byte[] readTag() throws IOException {
-			byte[] tag = new byte[TAG_LENGTH];
-			int offset = 0;
-			while(offset < tag.length) {
-				int read = in.read(tag, offset, tag.length - offset);
-				if(read == -1) break;
-				offset += read;
-			}
-			if(offset == 0) return null; // EOF between packets is acceptable
-			if(offset < tag.length) throw new EOFException();
-			return tag;
-		}
-	}
-}
diff --git a/test/net/sf/briar/transport/PacketWriterImplTest.java b/test/net/sf/briar/transport/PacketWriterImplTest.java
deleted file mode 100644
index f99c51e5be46bd487775720bee594667d54c9295..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/PacketWriterImplTest.java
+++ /dev/null
@@ -1,174 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.Arrays;
-
-import javax.crypto.Mac;
-
-import junit.framework.TestCase;
-import net.sf.briar.api.crypto.CryptoComponent;
-import net.sf.briar.api.transport.PacketWriter;
-import net.sf.briar.crypto.CryptoModule;
-import net.sf.briar.util.StringUtils;
-
-import org.junit.Test;
-
-import com.google.inject.Guice;
-import com.google.inject.Injector;
-
-public class PacketWriterImplTest extends TestCase {
-
-	private final Mac mac;
-
-	public PacketWriterImplTest() throws Exception {
-		super();
-		Injector i = Guice.createInjector(new CryptoModule());
-		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		mac = crypto.getMac();
-		mac.init(crypto.generateSecretKey());
-	}
-
-	@Test
-	public void testFirstWriteTriggersTag() throws Exception {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter e = new NullPacketEncrypter(out);
-		PacketWriter p = new PacketWriterImpl(e, mac, 0, 0L);
-		p.getOutputStream().write(0);
-		// There should be TAG_BYTES bytes for the tag, 1 byte for the packet
-		assertTrue(Arrays.equals(new byte[TAG_LENGTH + 1],
-				out.toByteArray()));
-	}
-
-	@Test
-	public void testFinishPacketAfterWriteTriggersMac() throws Exception {
-		// Calculate what the MAC should be
-		mac.update(new byte[TAG_LENGTH + 1]);
-		byte[] expectedMac = mac.doFinal();
-		// Check that the PacketWriter calculates and writes the correct MAC
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter e = new NullPacketEncrypter(out);
-		PacketWriter p = new PacketWriterImpl(e, mac, 0, 0L);
-		p.getOutputStream().write(0);
-		p.finishPacket();
-		byte[] written = out.toByteArray();
-		assertEquals(TAG_LENGTH + 1 + expectedMac.length,
-				written.length);
-		byte[] actualMac = new byte[expectedMac.length];
-		System.arraycopy(written, TAG_LENGTH + 1, actualMac, 0,
-				actualMac.length);
-		assertTrue(Arrays.equals(expectedMac, actualMac));
-	}
-
-	@Test
-	public void testExtraCallsToFinishPacketDoNothing() throws Exception {
-		// Calculate what the MAC should be
-		mac.update(new byte[TAG_LENGTH + 1]);
-		byte[] expectedMac = mac.doFinal();
-		// Check that the PacketWriter calculates and writes the correct MAC
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter e = new NullPacketEncrypter(out);
-		PacketWriter p = new PacketWriterImpl(e, mac, 0, 0L);
-		// Initial calls to finishPacket() should have no effect
-		p.finishPacket();
-		p.finishPacket();
-		p.finishPacket();
-		p.getOutputStream().write(0);
-		p.finishPacket();
-		// Extra calls to finishPacket() should have no effect
-		p.finishPacket();
-		p.finishPacket();
-		p.finishPacket();
-		byte[] written = out.toByteArray();
-		assertEquals(TAG_LENGTH + 1 + expectedMac.length,
-				written.length);
-		byte[] actualMac = new byte[expectedMac.length];
-		System.arraycopy(written, TAG_LENGTH + 1, actualMac, 0,
-				actualMac.length);
-		assertTrue(Arrays.equals(expectedMac, actualMac));
-	}
-
-	@Test
-	public void testPacketNumberIsIncremented() throws Exception {
-		byte[] expectedTag = StringUtils.fromHexString(
-				"0000" // 16 bits reserved
-				+ "F00D" // 16 bits for the transport ID
-				+ "DEADBEEF" // 32 bits for the connection number
-				+ "00000000" // 32 bits for the packet number
-				+ "00000000" // 32 bits for the block number
-		);
-		assertEquals(TAG_LENGTH, expectedTag.length);
-		byte[] expectedTag1 = StringUtils.fromHexString(
-				"0000" // 16 bits reserved
-				+ "F00D" // 16 bits for the transport ID
-				+ "DEADBEEF" // 32 bits for the connection number
-				+ "00000001" // 32 bits for the packet number
-				+ "00000000" // 32 bits for the block number
-		);
-		assertEquals(TAG_LENGTH, expectedTag1.length);
-		// Calculate what the MAC on the first packet should be
-		mac.update(expectedTag);
-		mac.update((byte) 0);
-		byte[] expectedMac = mac.doFinal();
-		// Calculate what the MAC on the second packet should be
-		mac.update(expectedTag1);
-		mac.update((byte) 0);
-		byte[] expectedMac1 = mac.doFinal();
-		// Check that the PacketWriter writes the correct tags and MACs
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		PacketEncrypter e = new NullPacketEncrypter(out);
-		PacketWriter p = new PacketWriterImpl(e, mac, 0xF00D, 0xDEADBEEFL);
-		// Packet one
-		p.getOutputStream().write(0);
-		p.finishPacket();
-		// Packet two
-		p.getOutputStream().write(0);
-		p.finishPacket();
-		byte[] written = out.toByteArray();
-		assertEquals(TAG_LENGTH + 1 + expectedMac.length
-				+ TAG_LENGTH + 1 + expectedMac1.length,
-				written.length);
-		// Check the first packet's tag
-		byte[] actualTag = new byte[TAG_LENGTH];
-		System.arraycopy(written, 0, actualTag, 0, TAG_LENGTH);
-		assertTrue(Arrays.equals(expectedTag, actualTag));
-		// Check the first packet's MAC
-		byte[] actualMac = new byte[expectedMac.length];
-		System.arraycopy(written, TAG_LENGTH + 1, actualMac, 0,
-				actualMac.length);
-		assertTrue(Arrays.equals(expectedMac, actualMac));
-		// Check the second packet's tag
-		byte[] actualTag1 = new byte[TAG_LENGTH];
-		System.arraycopy(written, TAG_LENGTH + 1 + expectedMac.length,
-				actualTag1, 0, TAG_LENGTH);
-		assertTrue(Arrays.equals(expectedTag1, actualTag1));
-		// Check the second packet's MAC
-		byte[] actualMac1 = new byte[expectedMac1.length];
-		System.arraycopy(written, TAG_LENGTH + 1 + expectedMac.length
-				+ TAG_LENGTH + 1, actualMac1, 0, actualMac1.length);
-		assertTrue(Arrays.equals(expectedMac1, actualMac1));
-	}
-
-	/** A PacketEncrypter that performs no encryption. */
-	private static class NullPacketEncrypter implements PacketEncrypter {
-
-		private final OutputStream out;
-
-		private NullPacketEncrypter(OutputStream out) {
-			this.out = out;
-		}
-
-		public OutputStream getOutputStream() {
-			return out;
-		}
-
-		public void writeTag(byte[] tag) throws IOException {
-			out.write(tag);
-		}
-
-		public void finishPacket() {}
-	}
-}
diff --git a/test/net/sf/briar/transport/TagDecoderTest.java b/test/net/sf/briar/transport/TagDecoderTest.java
deleted file mode 100644
index d99e0d480cc6f8c93e31f80eb18b0b6880b3fec0..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/TagDecoderTest.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package net.sf.briar.transport;
-
-import junit.framework.TestCase;
-
-import net.sf.briar.util.StringUtils;
-
-import org.junit.Test;
-
-public class TagDecoderTest extends TestCase {
-
-	@Test
-	public void testReadUint16() {
-		byte[] b = StringUtils.fromHexString("000000");
-		assertEquals(0, TagDecoder.readUint16(b, 1));
-		b = StringUtils.fromHexString("000001");
-		assertEquals(1, TagDecoder.readUint16(b, 1));
-		b = StringUtils.fromHexString("00FFFF");
-		assertEquals(65535, TagDecoder.readUint16(b, 1));
-	}
-
-	@Test
-	public void testReadUint32() {
-		byte[] b = StringUtils.fromHexString("0000000000");
-		assertEquals(0L, TagDecoder.readUint32(b, 1));
-		b = StringUtils.fromHexString("0000000001");
-		assertEquals(1L, TagDecoder.readUint32(b, 1));
-		b = StringUtils.fromHexString("00FFFFFFFF");
-		assertEquals(4294967295L, TagDecoder.readUint32(b, 1));
-	}
-}
diff --git a/test/net/sf/briar/transport/TagEncoderTest.java b/test/net/sf/briar/transport/TagEncoderTest.java
deleted file mode 100644
index c521b052367e862350468e3a4b769034f6588b05..0000000000000000000000000000000000000000
--- a/test/net/sf/briar/transport/TagEncoderTest.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.sf.briar.transport;
-
-import net.sf.briar.util.StringUtils;
-
-import org.junit.Test;
-
-import junit.framework.TestCase;
-
-public class TagEncoderTest extends TestCase {
-
-	@Test
-	public void testWriteUint16() {
-		byte[] b = new byte[3];
-		TagEncoder.writeUint16(0, b, 1);
-		assertEquals("000000", StringUtils.toHexString(b));
-		TagEncoder.writeUint16(1, b, 1);
-		assertEquals("000001", StringUtils.toHexString(b));
-		TagEncoder.writeUint16(65535, b, 1);
-		assertEquals("00FFFF", StringUtils.toHexString(b));
-	}
-
-	@Test
-	public void testWriteUint32() {
-		byte[] b = new byte[5];
-		TagEncoder.writeUint32(0L, b, 1);
-		assertEquals("0000000000", StringUtils.toHexString(b));
-		TagEncoder.writeUint32(1L, b, 1);
-		assertEquals("0000000001", StringUtils.toHexString(b));
-		TagEncoder.writeUint32(4294967295L, b, 1);
-		assertEquals("00FFFFFFFF", StringUtils.toHexString(b));
-	}
-}
diff --git a/test/net/sf/briar/util/ByteUtilsTest.java b/test/net/sf/briar/util/ByteUtilsTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b850e8b197199b490810b75efbd7216be6348dc
--- /dev/null
+++ b/test/net/sf/briar/util/ByteUtilsTest.java
@@ -0,0 +1,51 @@
+package net.sf.briar.util;
+
+import junit.framework.TestCase;
+
+import org.junit.Test;
+
+public class ByteUtilsTest extends TestCase {
+
+	@Test
+	public void testReadUint16() {
+		byte[] b = StringUtils.fromHexString("000000");
+		assertEquals(0, ByteUtils.readUint16(b, 1));
+		b = StringUtils.fromHexString("000001");
+		assertEquals(1, ByteUtils.readUint16(b, 1));
+		b = StringUtils.fromHexString("00FFFF");
+		assertEquals(65535, ByteUtils.readUint16(b, 1));
+	}
+
+	@Test
+	public void testReadUint32() {
+		byte[] b = StringUtils.fromHexString("0000000000");
+		assertEquals(0L, ByteUtils.readUint32(b, 1));
+		b = StringUtils.fromHexString("0000000001");
+		assertEquals(1L, ByteUtils.readUint32(b, 1));
+		b = StringUtils.fromHexString("00FFFFFFFF");
+		assertEquals(4294967295L, ByteUtils.readUint32(b, 1));
+	}
+
+
+	@Test
+	public void testWriteUint16() {
+		byte[] b = new byte[3];
+		ByteUtils.writeUint16(0, b, 1);
+		assertEquals("000000", StringUtils.toHexString(b));
+		ByteUtils.writeUint16(1, b, 1);
+		assertEquals("000001", StringUtils.toHexString(b));
+		ByteUtils.writeUint16(65535, b, 1);
+		assertEquals("00FFFF", StringUtils.toHexString(b));
+	}
+
+	@Test
+	public void testWriteUint32() {
+		byte[] b = new byte[5];
+		ByteUtils.writeUint32(0L, b, 1);
+		assertEquals("0000000000", StringUtils.toHexString(b));
+		ByteUtils.writeUint32(1L, b, 1);
+		assertEquals("0000000001", StringUtils.toHexString(b));
+		ByteUtils.writeUint32(4294967295L, b, 1);
+		assertEquals("00FFFFFFFF", StringUtils.toHexString(b));
+	}
+}
diff --git a/util/net/sf/briar/util/ByteUtils.java b/util/net/sf/briar/util/ByteUtils.java
new file mode 100644
index 0000000000000000000000000000000000000000..2066e820a19e6ebe8a789767e9d24fb6a8464505
--- /dev/null
+++ b/util/net/sf/briar/util/ByteUtils.java
@@ -0,0 +1,43 @@
+package net.sf.briar.util;
+
+public class ByteUtils {
+
+	/**
+	 * The maximum value that can be represented as an unsigned 16-bit integer.
+	 */
+	public static final int MAX_16_BIT_UNSIGNED = 65535; // 2^16 - 1
+
+	/**
+	 * The maximum value that can be represented as an unsigned 32-bit integer.
+	 */
+	public static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1
+
+	public static void writeUint16(int i, byte[] b, int offset) {
+		if(i < 0) throw new IllegalArgumentException();
+		if(i > MAX_16_BIT_UNSIGNED) throw new IllegalArgumentException();
+		assert b.length >= offset + 2;
+		b[offset] = (byte) (i >> 8);
+		b[offset + 1] = (byte) (i & 0xFF);
+	}
+
+	public static void writeUint32(long i, byte[] b, int offset) {
+		if(i < 0L) throw new IllegalArgumentException();
+		if(i > MAX_32_BIT_UNSIGNED) throw new IllegalArgumentException();
+		assert b.length >= offset + 4;
+		b[offset] = (byte) (i >> 24);
+		b[offset + 1] = (byte) (i >> 16 & 0xFF);
+		b[offset + 2] = (byte) (i >> 8 & 0xFF);
+		b[offset + 3] = (byte) (i & 0xFF);
+	}
+
+	public static int readUint16(byte[] b, int offset) {
+		if(b.length < offset + 2) throw new IllegalArgumentException();
+		return ((b[offset] & 0xFF) << 8) | (b[offset + 1] & 0xFF);
+	}
+
+	public static long readUint32(byte[] b, int offset) {
+		if(b.length < offset + 4) throw new IllegalArgumentException();
+		return ((b[offset] & 0xFFL) << 24) | ((b[offset + 1] & 0xFFL) << 16)
+		| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
+	}
+}