From 2411e2008b06e5b208a5a42e9617dff53db2ece1 Mon Sep 17 00:00:00 2001
From: akwizgran <akwizgran@users.sourceforge.net>
Date: Fri, 19 Aug 2011 01:46:51 +0200
Subject: [PATCH] Frame the encrypted data independently of inter-packet
 boundaries and authenticate each frame before parsing its contents. Each
 connection starts with a tag, followed by any number of frames, each starting
 with the frame number (32 bits) and payload length (16 bits), and ending with
 a MAC (256 bits).

Tags have the following format: 32 bits reserved, 16 bits for the
transport ID, 32 bits for the connection number, 32 bits (set to zero
in the tag) for the frame number, and 16 bits (set to zero in the tag)
for the block number. The tag is encrypted with the tag key in
ECB mode.

Frame numbers for each connection must start from zero and must be
contiguous and strictly increasing. Each frame is encrypted with the
frame key in CTR mode, using the plaintext tag with the appropriate
frame number to initialise the counter.

The maximum frame size is 64 KiB, including header and footer. The
maximum amount of data that can be sent over a connection is 2^32
frames - roughly 2^48 bytes, or 8 terabytes, with the maximum frame
size of 64 KiB. If that isn't sufficient we can add another 16 bits to
the frame counter.
---
 .../api/{serial => }/FormatException.java     |   2 +-
 .../sf/briar/api/crypto/CryptoComponent.java  |   6 +-
 .../briar/api/transport/ConnectionReader.java |  13 ++
 .../transport/ConnectionReaderFactory.java    |   9 +
 .../briar/api/transport/ConnectionWriter.java |  13 ++
 .../transport/ConnectionWriterFactory.java    |   9 +
 .../sf/briar/api/transport/PacketReader.java  |  26 ---
 .../api/transport/PacketReaderFactory.java    |   9 -
 .../sf/briar/api/transport/PacketWriter.java  |  24 ---
 .../api/transport/PacketWriterFactory.java    |   9 -
 .../api/transport/TransportConstants.java     |  10 -
 .../transport/batch/BatchTransportPlugin.java |   8 +-
 .../stream/StreamTransportPlugin.java         |   8 +-
 .../sf/briar/crypto/CryptoComponentImpl.java  |  12 +-
 .../net/sf/briar/protocol/BatchIdReader.java  |   2 +-
 .../sf/briar/protocol/CountingConsumer.java   |   2 +-
 .../net/sf/briar/protocol/GroupIdReader.java  |   2 +-
 .../sf/briar/protocol/MessageEncoderImpl.java |   2 +
 .../sf/briar/protocol/MessageIdReader.java    |   2 +-
 .../net/sf/briar/protocol/MessageReader.java  |   2 +-
 .../net/sf/briar/protocol/OfferIdReader.java  |   2 +-
 .../net/sf/briar/serial/ReaderImpl.java       |   2 +-
 .../briar/transport/ConnectionDecrypter.java  |  14 ++
 .../transport/ConnectionDecrypterImpl.java    | 148 ++++++++++++++
 .../briar/transport/ConnectionEncrypter.java  |  14 ++
 .../transport/ConnectionEncrypterImpl.java    | 117 +++++++++++
 .../ConnectionReaderFactoryImpl.java          |  40 ++++
 .../briar/transport/ConnectionReaderImpl.java | 107 ++++++++++
 .../transport/ConnectionRecogniserImpl.java   |  11 +-
 .../briar/transport/ConnectionWindowImpl.java |   2 +-
 .../ConnectionWriterFactoryImpl.java          |  43 ++++
 .../briar/transport/ConnectionWriterImpl.java |  84 ++++++++
 .../sf/briar/transport/PacketDecrypter.java   |  17 --
 .../briar/transport/PacketDecrypterImpl.java  | 166 ---------------
 .../sf/briar/transport/PacketEncrypter.java   |  16 --
 .../briar/transport/PacketEncrypterImpl.java  |  86 --------
 .../transport/PacketReaderFactoryImpl.java    |  42 ----
 .../sf/briar/transport/PacketReaderImpl.java  |  92 ---------
 .../transport/PacketWriterFactoryImpl.java    |  42 ----
 .../sf/briar/transport/PacketWriterImpl.java  |  83 --------
 .../net/sf/briar/transport/TagDecoder.java    |  31 +--
 .../net/sf/briar/transport/TagEncoder.java    |  43 ++--
 .../sf/briar/transport/TransportModule.java   |  10 +-
 test/build.xml                                |  13 +-
 test/net/sf/briar/FileReadWriteTest.java      |  48 ++---
 test/net/sf/briar/TestUtils.java              |  12 ++
 test/net/sf/briar/crypto/CounterModeTest.java |  55 +++--
 .../sf/briar/crypto/CryptoComponentTest.java  |  12 +-
 test/net/sf/briar/protocol/AckReaderTest.java |   2 +-
 .../sf/briar/protocol/BatchReaderTest.java    |   2 +-
 test/net/sf/briar/protocol/ConsumersTest.java |   2 +-
 .../sf/briar/protocol/RequestReaderTest.java  |   2 +-
 test/net/sf/briar/serial/ReaderImplTest.java  |   2 +-
 .../ConnectionDecrypterImplTest.java          | 100 +++++++++
 .../ConnectionEncrypterImplTest.java          |  98 +++++++++
 .../transport/ConnectionReaderImplTest.java   | 106 ++++++++++
 .../ConnectionRecogniserImplTest.java         |   2 +-
 .../transport/ConnectionWriterImplTest.java   |  98 +++++++++
 ...WriteTest.java => FrameReadWriteTest.java} |  74 +++----
 .../transport/PacketDecrypterImplTest.java    | 115 -----------
 .../transport/PacketEncrypterImplTest.java    |  79 --------
 .../briar/transport/PacketReaderImplTest.java | 189 ------------------
 .../briar/transport/PacketWriterImplTest.java | 174 ----------------
 .../sf/briar/transport/TagDecoderTest.java    |  30 ---
 .../sf/briar/transport/TagEncoderTest.java    |  32 ---
 test/net/sf/briar/util/ByteUtilsTest.java     |  51 +++++
 util/net/sf/briar/util/ByteUtils.java         |  43 ++++
 67 files changed, 1283 insertions(+), 1440 deletions(-)
 rename api/net/sf/briar/api/{serial => }/FormatException.java (82%)
 create mode 100644 api/net/sf/briar/api/transport/ConnectionReader.java
 create mode 100644 api/net/sf/briar/api/transport/ConnectionReaderFactory.java
 create mode 100644 api/net/sf/briar/api/transport/ConnectionWriter.java
 create mode 100644 api/net/sf/briar/api/transport/ConnectionWriterFactory.java
 delete mode 100644 api/net/sf/briar/api/transport/PacketReader.java
 delete mode 100644 api/net/sf/briar/api/transport/PacketReaderFactory.java
 delete mode 100644 api/net/sf/briar/api/transport/PacketWriter.java
 delete mode 100644 api/net/sf/briar/api/transport/PacketWriterFactory.java
 create mode 100644 components/net/sf/briar/transport/ConnectionDecrypter.java
 create mode 100644 components/net/sf/briar/transport/ConnectionDecrypterImpl.java
 create mode 100644 components/net/sf/briar/transport/ConnectionEncrypter.java
 create mode 100644 components/net/sf/briar/transport/ConnectionEncrypterImpl.java
 create mode 100644 components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
 create mode 100644 components/net/sf/briar/transport/ConnectionReaderImpl.java
 create mode 100644 components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
 create mode 100644 components/net/sf/briar/transport/ConnectionWriterImpl.java
 delete mode 100644 components/net/sf/briar/transport/PacketDecrypter.java
 delete mode 100644 components/net/sf/briar/transport/PacketDecrypterImpl.java
 delete mode 100644 components/net/sf/briar/transport/PacketEncrypter.java
 delete mode 100644 components/net/sf/briar/transport/PacketEncrypterImpl.java
 delete mode 100644 components/net/sf/briar/transport/PacketReaderFactoryImpl.java
 delete mode 100644 components/net/sf/briar/transport/PacketReaderImpl.java
 delete mode 100644 components/net/sf/briar/transport/PacketWriterFactoryImpl.java
 delete mode 100644 components/net/sf/briar/transport/PacketWriterImpl.java
 create mode 100644 test/net/sf/briar/transport/ConnectionDecrypterImplTest.java
 create mode 100644 test/net/sf/briar/transport/ConnectionEncrypterImplTest.java
 create mode 100644 test/net/sf/briar/transport/ConnectionReaderImplTest.java
 create mode 100644 test/net/sf/briar/transport/ConnectionWriterImplTest.java
 rename test/net/sf/briar/transport/{PacketReadWriteTest.java => FrameReadWriteTest.java} (53%)
 delete mode 100644 test/net/sf/briar/transport/PacketDecrypterImplTest.java
 delete mode 100644 test/net/sf/briar/transport/PacketEncrypterImplTest.java
 delete mode 100644 test/net/sf/briar/transport/PacketReaderImplTest.java
 delete mode 100644 test/net/sf/briar/transport/PacketWriterImplTest.java
 delete mode 100644 test/net/sf/briar/transport/TagDecoderTest.java
 delete mode 100644 test/net/sf/briar/transport/TagEncoderTest.java
 create mode 100644 test/net/sf/briar/util/ByteUtilsTest.java
 create mode 100644 util/net/sf/briar/util/ByteUtils.java

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 784f44e306..b48f8ba51f 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 373daea801..504ed97055 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 0000000000..cac3fbe40c
--- /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 0000000000..536123f48e
--- /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 0000000000..c7ed2038ac
--- /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 0000000000..737096bfe8
--- /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 683f3cea08..0000000000
--- 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 8e2ec5b3b6..0000000000
--- 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 bc4d92a0a6..0000000000
--- 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 c93e41e756..0000000000
--- 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 3b8503f566..2746a8c0df 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 db2ff72c0b..bd810f8e61 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 67e04a204d..30c3504782 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 366c37fd54..f8816a1859 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 4542a0f5ec..21807eb475 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 c882ab0b2b..bb032341e0 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 07bf2233f1..bf9fd81b1e 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 5c2149ad5c..8e430f1a57 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 eecf32649f..32e4e905ac 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 e4c24078a6..1ee31dd152 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 52485760cf..3c1fe80aad 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 63b9e4ef0c..ffc7af901e 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 0000000000..80bd798eee
--- /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 0000000000..4ab3b618d7
--- /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 0000000000..595307547b
--- /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 0000000000..0fccc905f9
--- /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 0000000000..016e4fb43b
--- /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 0000000000..f3d54a9a15
--- /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 f9f0f9a636..5f662503c0 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 5ce228e7d3..f0485cb1ac 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 0000000000..ae12ef4b1c
--- /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 0000000000..882176a11d
--- /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 2394260216..0000000000
--- 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 b1ed147904..0000000000
--- 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 7652753b26..0000000000
--- 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 bd9b5e5a76..0000000000
--- 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 3d8ccbdafe..0000000000
--- 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 2db8bafefe..0000000000
--- 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 a59d201c4c..0000000000
--- 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 7ca9f9289d..0000000000
--- 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 169f1ad151..a83be3d847 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 1660c43712..dbde2fa505 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 1fefd05eaf..d952d534ec 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 f63488bbff..f6bf3ddb10 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 14ac71a9c4..e7a6c86c9b 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 d8c25231b7..6ecb32a4c6 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 7ce6dbcc29..ff664429db 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 d7dad93ed9..b8ea8ade6a 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 67f08091e9..42dffb2d0a 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 9c7407dbba..8306b683eb 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 fbd839d9b4..cb03476b61 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 5b10e49648..8be4c17049 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 28670b8b91..ddd310accb 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 0000000000..9bf306fd84
--- /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 0000000000..3b3a9145d2
--- /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 0000000000..d73e43676e
--- /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 52ed9148b5..c94b242a8a 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 0000000000..4892d58c97
--- /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 27b1f35491..7fb8ef7177 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 c001a20aaf..0000000000
--- 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 06ea1b66a9..0000000000
--- 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 d5e34c89c9..0000000000
--- 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 f99c51e5be..0000000000
--- 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 d99e0d480c..0000000000
--- 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 c521b05236..0000000000
--- 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 0000000000..3b850e8b19
--- /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 0000000000..2066e820a1
--- /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);
+	}
+}
-- 
GitLab