diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java
index d3f8d0353219d8a7a065910208082ab64f317fe9..59e5ae547b5886fa0e96d784f59e89fb93dbb800 100644
--- a/api/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/api/net/sf/briar/api/crypto/CryptoComponent.java
@@ -6,7 +6,6 @@ import java.security.SecureRandom;
 import java.security.Signature;
 
 import javax.crypto.Cipher;
-import javax.crypto.Mac;
 
 public interface CryptoComponent {
 
@@ -14,8 +13,6 @@ public interface CryptoComponent {
 
 	ErasableKey deriveFrameKey(byte[] secret, boolean initiator);
 
-	ErasableKey deriveMacKey(byte[] secret, boolean initiator);
-
 	byte[][] deriveInitialSecrets(byte[] ourPublicKey, byte[] theirPublicKey,
 			PrivateKey ourPrivateKey, int invitationCode, boolean initiator);
 
@@ -41,7 +38,11 @@ public interface CryptoComponent {
 
 	Cipher getFrameCipher();
 
-	Signature getSignature();
+	Cipher getFramePeekingCipher();
+
+	IvEncoder getFrameIvEncoder();
 
-	Mac getMac();
+	IvEncoder getFramePeekingIvEncoder();
+
+	Signature getSignature();
 }
diff --git a/api/net/sf/briar/api/crypto/IvEncoder.java b/api/net/sf/briar/api/crypto/IvEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..12d24a545e87a2920a647cf86fd1a1c4404d457c
--- /dev/null
+++ b/api/net/sf/briar/api/crypto/IvEncoder.java
@@ -0,0 +1,8 @@
+package net.sf.briar.api.crypto;
+
+public interface IvEncoder {
+
+	byte[] encodeIv(long frameNumber);
+
+	void updateIv(byte[] iv, long frameNumber);
+}
diff --git a/api/net/sf/briar/api/transport/TransportConstants.java b/api/net/sf/briar/api/transport/TransportConstants.java
index afd01abd8e231e559fb267ac4fd461aa303bd4f1..a37b810071d8952f03b4665a09b5c6cdf5907214 100644
--- a/api/net/sf/briar/api/transport/TransportConstants.java
+++ b/api/net/sf/briar/api/transport/TransportConstants.java
@@ -12,7 +12,7 @@ public interface TransportConstants {
 	static final int FRAME_HEADER_LENGTH = 9;
 
 	/** The length of the MAC in bytes. */
-	static final int MAC_LENGTH = 48;
+	static final int MAC_LENGTH = 16;
 
 	/**
 	 * The minimum connection length in bytes that all transport plugins must
diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java
index 4fd74b05f29644792a652935282647f441cca913..226c992215902f88b6f2410ef7c724e3e549eddc 100644
--- a/components/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -13,11 +13,11 @@ import java.security.Signature;
 
 import javax.crypto.Cipher;
 import javax.crypto.KeyAgreement;
-import javax.crypto.Mac;
 import javax.crypto.spec.IvParameterSpec;
 
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 import net.sf.briar.api.crypto.KeyParser;
 import net.sf.briar.api.crypto.MessageDigest;
 import net.sf.briar.api.crypto.PseudoRandom;
@@ -35,20 +35,19 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final String AGREEMENT_ALGO = "ECDHC";
 	private static final String SECRET_KEY_ALGO = "AES";
 	private static final int SECRET_KEY_BYTES = 32; // 256 bits
-	private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits
 	private static final String KEY_DERIVATION_ALGO = "AES/CTR/NoPadding";
+	private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits
 	private static final String DIGEST_ALGO = "SHA-384";
 	private static final String SIGNATURE_KEY_PAIR_ALGO = "ECDSA";
 	private static final int SIGNATURE_KEY_PAIR_BITS = 384;
 	private static final String SIGNATURE_ALGO = "ECDSA";
 	private static final String TAG_CIPHER_ALGO = "AES/ECB/NoPadding";
-	private static final String FRAME_CIPHER_ALGO = "AES/CTR/NoPadding";
-	private static final String MAC_ALGO = "HMacSHA384";
+	private static final String FRAME_CIPHER_ALGO = "AES/GCM/NoPadding";
+	private static final String FRAME_PEEKING_CIPHER_ALGO = "AES/CTR/NoPadding";
 
 	// Labels for key derivation
 	private static final byte[] TAG = { 'T', 'A', 'G' };
 	private static final byte[] FRAME = { 'F', 'R', 'A', 'M', 'E' };
-	private static final byte[] MAC = { 'M', 'A', 'C' };
 	// Labels for secret derivation
 	private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T' };
 	private static final byte[] NEXT = { 'N', 'E', 'X', 'T' };
@@ -96,11 +95,6 @@ class CryptoComponentImpl implements CryptoComponent {
 		else return deriveKey(secret, FRAME, RESPONDER);
 	}
 
-	public ErasableKey deriveMacKey(byte[] secret, boolean initiator) {
-		if(initiator) return deriveKey(secret, MAC, INITIATOR);
-		else return deriveKey(secret, MAC, RESPONDER);
-	}
-
 	private ErasableKey deriveKey(byte[] secret, byte[] label, byte[] context) {
 		byte[] key = counterModeKdf(secret, label, context);
 		return new ErasableKeyImpl(key, SECRET_KEY_ALGO);
@@ -289,11 +283,19 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
-	public Mac getMac() {
+	public Cipher getFramePeekingCipher() {
 		try {
-			return Mac.getInstance(MAC_ALGO, PROVIDER);
+			return Cipher.getInstance(FRAME_PEEKING_CIPHER_ALGO, PROVIDER);
 		} catch(GeneralSecurityException e) {
 			throw new RuntimeException(e);
 		}
 	}
+
+	public IvEncoder getFrameIvEncoder() {
+		return new FrameIvEncoder();
+	}
+
+	public IvEncoder getFramePeekingIvEncoder() {
+		return new FramePeekingIvEncoder();
+	}
 }
diff --git a/components/net/sf/briar/crypto/FrameIvEncoder.java b/components/net/sf/briar/crypto/FrameIvEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..6703669f69f66da50af0c34f97627120a9b4d03f
--- /dev/null
+++ b/components/net/sf/briar/crypto/FrameIvEncoder.java
@@ -0,0 +1,26 @@
+package net.sf.briar.crypto;
+
+import net.sf.briar.api.crypto.IvEncoder;
+import net.sf.briar.util.ByteUtils;
+
+class FrameIvEncoder implements IvEncoder {
+
+	// AES-GCM uses a 96-bit IV; the bytes 0x00, 0x00, 0x00, 0x02 are
+	// appended internally (see NIST SP 800-38D, section 7.1)
+	private static final int IV_LENGTH = 12;
+
+	public byte[] encodeIv(long frame) {
+		if(frame < 0 || frame > ByteUtils.MAX_32_BIT_UNSIGNED)
+			throw new IllegalArgumentException();
+		byte[] iv = new byte[IV_LENGTH];
+		updateIv(iv, frame);
+		return iv;
+	}
+
+	public void updateIv(byte[] iv, long frame) {
+		if(frame < 0 || frame > ByteUtils.MAX_32_BIT_UNSIGNED)
+			throw new IllegalArgumentException();
+		// Encode the frame number as a uint32
+		ByteUtils.writeUint32(frame, iv, 0);
+	}
+}
diff --git a/components/net/sf/briar/crypto/FramePeekingIvEncoder.java b/components/net/sf/briar/crypto/FramePeekingIvEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..85d5dbb06d4c0f0889dd3cc6c1814f8ec1bf3d8a
--- /dev/null
+++ b/components/net/sf/briar/crypto/FramePeekingIvEncoder.java
@@ -0,0 +1,20 @@
+package net.sf.briar.crypto;
+
+import net.sf.briar.util.ByteUtils;
+
+class FramePeekingIvEncoder extends FrameIvEncoder {
+
+	// AES/CTR uses a 128-bit IV; to match the AES/GCM IV we have to append
+	// the bytes 0x00, 0x00, 0x00, 0x02 (see NIST SP 800-38D, section 7.1)
+	private static final int IV_LENGTH = 16;
+
+	@Override
+	public byte[] encodeIv(long frame) {
+		if(frame < 0 || frame > ByteUtils.MAX_32_BIT_UNSIGNED)
+			throw new IllegalArgumentException();
+		byte[] iv = new byte[IV_LENGTH];
+		iv[IV_LENGTH - 1] = 2;
+		updateIv(iv, frame);
+		return iv;
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
index 8163e047ffa6b31e371ba30ab63e7a6bb2d2e14a..0bed032dd28587f8ede3cae72a9677b3dcbb6eb7 100644
--- a/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
+++ b/components/net/sf/briar/transport/ConnectionReaderFactoryImpl.java
@@ -3,10 +3,10 @@ package net.sf.briar.transport;
 import java.io.InputStream;
 
 import javax.crypto.Cipher;
-import javax.crypto.Mac;
 
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionReaderFactory;
 import net.sf.briar.util.ByteUtils;
@@ -27,18 +27,16 @@ class ConnectionReaderFactoryImpl implements ConnectionReaderFactory {
 		// Derive the keys and erase the secret
 		ErasableKey tagKey = crypto.deriveTagKey(secret, initiator);
 		ErasableKey frameKey = crypto.deriveFrameKey(secret, initiator);
-		ErasableKey macKey = crypto.deriveMacKey(secret, initiator);
 		ByteUtils.erase(secret);
-		// Encryption
+		// Create the reader
 		Cipher tagCipher = crypto.getTagCipher();
 		Cipher frameCipher = crypto.getFrameCipher();
+		Cipher framePeekingCipher = crypto.getFramePeekingCipher();
+		IvEncoder frameIvEncoder = crypto.getFrameIvEncoder();
+		IvEncoder framePeekingIvEncoder = crypto.getFramePeekingIvEncoder();
 		FrameReader encryption = new IncomingEncryptionLayerImpl(in, tagCipher,
-				frameCipher, tagKey, frameKey, !initiator);
-		// Authentication
-		Mac mac = crypto.getMac();
-		FrameReader authentication = new IncomingAuthenticationLayerImpl(
-				encryption, mac, macKey);
-		// Create the reader
-		return new ConnectionReaderImpl(authentication);
+				frameCipher, framePeekingCipher, frameIvEncoder,
+				framePeekingIvEncoder, tagKey, frameKey, !initiator);
+		return new ConnectionReaderImpl(encryption);
 	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionReaderImpl.java b/components/net/sf/briar/transport/ConnectionReaderImpl.java
index cb84c45cd18c4e168a758ddb6191461c0faa6c3f..4ef2de96c10203aacbacac6844fba29be9f6fcd6 100644
--- a/components/net/sf/briar/transport/ConnectionReaderImpl.java
+++ b/components/net/sf/briar/transport/ConnectionReaderImpl.java
@@ -53,14 +53,20 @@ class ConnectionReaderImpl extends InputStream implements ConnectionReader {
 
 	private boolean readFrame() throws IOException {
 		assert length == 0;
-		if(HeaderEncoder.isLastFrame(frame.getBuffer())) {
+		byte[] buf = frame.getBuffer();
+		if(HeaderEncoder.isLastFrame(buf)) {
 			length = -1;
 			return false;
 		}
 		frame.reset();
 		if(!in.readFrame(frame)) throw new FormatException();
 		offset = FRAME_HEADER_LENGTH;
-		length = HeaderEncoder.getPayloadLength(frame.getBuffer());
+		length = HeaderEncoder.getPayloadLength(buf);
+		// The padding must be all zeroes
+		int padding = HeaderEncoder.getPaddingLength(buf);
+		for(int i = offset + length; i < offset + length + padding; i++) {
+			if(buf[i] != 0) throw new FormatException();
+		}
 		return true;
 	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
index cffa5521861e6455282129057a26ac5688ec5671..ea36c2c2c51a9c30ec944e788333f726ac55538b 100644
--- a/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWriterFactoryImpl.java
@@ -3,10 +3,10 @@ package net.sf.briar.transport;
 import java.io.OutputStream;
 
 import javax.crypto.Cipher;
-import javax.crypto.Mac;
 
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.api.transport.ConnectionWriterFactory;
 import net.sf.briar.util.ByteUtils;
@@ -27,18 +27,14 @@ class ConnectionWriterFactoryImpl implements ConnectionWriterFactory {
 		// Derive the keys and erase the secret
 		ErasableKey tagKey = crypto.deriveTagKey(secret, initiator);
 		ErasableKey frameKey = crypto.deriveFrameKey(secret, initiator);
-		ErasableKey macKey = crypto.deriveMacKey(secret, initiator);
 		ByteUtils.erase(secret);
-		// Encryption
+		// Create the writer
 		Cipher tagCipher = crypto.getTagCipher();
 		Cipher frameCipher = crypto.getFrameCipher();
+		IvEncoder frameIvEncoder = crypto.getFrameIvEncoder();
 		FrameWriter encryption = new OutgoingEncryptionLayerImpl(
-				out, capacity, tagCipher, frameCipher, tagKey, frameKey);
-		// Authentication
-		Mac mac = crypto.getMac();
-		FrameWriter authentication =
-			new OutgoingAuthenticationLayerImpl(encryption, mac, macKey);
-		// Create the writer
-		return new ConnectionWriterImpl(authentication);
+				out, capacity, tagCipher, frameCipher, frameIvEncoder, tagKey,
+				frameKey);
+		return new ConnectionWriterImpl(encryption);
 	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionWriterImpl.java b/components/net/sf/briar/transport/ConnectionWriterImpl.java
index e0a1a16bc0e281de6fdd940114683f6cdb4eae0e..be97ff2de48b87766c57e40ebdb28f3a7659f6b1 100644
--- a/components/net/sf/briar/transport/ConnectionWriterImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWriterImpl.java
@@ -91,7 +91,7 @@ class ConnectionWriterImpl extends OutputStream implements ConnectionWriter {
 		assert payload >= 0;
 		HeaderEncoder.encodeHeader(frame.getBuffer(), frameNumber, payload, 0,
 				lastFrame);
-		frame.setLength(offset + MAC_LENGTH);
+		frame.setLength(offset);
 		out.writeFrame(frame);
 		frame.reset();
 		offset = FRAME_HEADER_LENGTH;
diff --git a/components/net/sf/briar/transport/Frame.java b/components/net/sf/briar/transport/Frame.java
index 93ef395c36e7ce6151bb76c2e8f229e51f9d9ff7..23271149c22f864bdaeea0587f08c4355ffbe265 100644
--- a/components/net/sf/briar/transport/Frame.java
+++ b/components/net/sf/briar/transport/Frame.java
@@ -1,7 +1,6 @@
 package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 
 class Frame {
@@ -24,7 +23,7 @@ class Frame {
 	}
 
 	public void setLength(int length) {
-		if(length < FRAME_HEADER_LENGTH + MAC_LENGTH || length > buf.length)
+		if(length < FRAME_HEADER_LENGTH || length > buf.length)
 			throw new IllegalArgumentException();
 		this.length = length;
 	}
diff --git a/components/net/sf/briar/transport/IncomingAuthenticationLayerImpl.java b/components/net/sf/briar/transport/IncomingAuthenticationLayerImpl.java
deleted file mode 100644
index d682c9e622eaa352038b6dac9d1857816ceca2da..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/IncomingAuthenticationLayerImpl.java
+++ /dev/null
@@ -1,61 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-
-import javax.crypto.Mac;
-
-import net.sf.briar.api.FormatException;
-import net.sf.briar.api.crypto.ErasableKey;
-
-class IncomingAuthenticationLayerImpl implements FrameReader {
-
-	private final FrameReader in;
-	private final Mac mac;
-
-	IncomingAuthenticationLayerImpl(FrameReader in, Mac mac,
-			ErasableKey macKey) {
-		this.in = in;
-		this.mac = mac;
-		try {
-			mac.init(macKey);
-		} catch(InvalidKeyException e) {
-			throw new IllegalArgumentException(e);
-		}
-		macKey.erase();
-		if(mac.getMacLength() != MAC_LENGTH)
-			throw new IllegalArgumentException();
-	}
-
-	public boolean readFrame(Frame f) throws IOException {
-		// Read a frame
-		if(!in.readFrame(f)) return false;
-		// Check that the length is legal
-		int length = f.getLength();
-		if(length < FRAME_HEADER_LENGTH + MAC_LENGTH)
-			throw new FormatException();
-		if(length > MAX_FRAME_LENGTH) throw new FormatException();
-		// Check that the header fields are legal and match the length
-		byte[] buf = f.getBuffer();
-		if(!HeaderEncoder.checkHeader(buf, length)) throw new FormatException();
-		// Check that the padding is all zeroes
-		int payload = HeaderEncoder.getPayloadLength(buf);
-		int padding = HeaderEncoder.getPaddingLength(buf);
-		int paddingStart = FRAME_HEADER_LENGTH + payload;
-		for(int i = paddingStart; i < paddingStart + padding; i++) {
-			if(buf[i] != 0) throw new FormatException();
-		}
-		// Verify the MAC
-		int macStart = FRAME_HEADER_LENGTH + payload + padding;
-		mac.update(buf, 0, macStart);
-		byte[] expectedMac = mac.doFinal();
-		for(int i = 0; i < expectedMac.length; i++) {
-			if(expectedMac[i] != buf[macStart + i]) throw new FormatException();
-		}
-		return true;
-	}
-}
diff --git a/components/net/sf/briar/transport/IncomingEncryptionLayerImpl.java b/components/net/sf/briar/transport/IncomingEncryptionLayerImpl.java
index ef8e1f61e0956ffa9b871c92e9966ab15bfb1e0d..66c49ee806bbd585ad966de4a25f298d6d4fc8eb 100644
--- a/components/net/sf/briar/transport/IncomingEncryptionLayerImpl.java
+++ b/components/net/sf/briar/transport/IncomingEncryptionLayerImpl.java
@@ -15,31 +15,38 @@ import javax.crypto.spec.IvParameterSpec;
 
 import net.sf.briar.api.FormatException;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 
 class IncomingEncryptionLayerImpl implements FrameReader {
 
 	private final InputStream in;
-	private final Cipher tagCipher, frameCipher;
+	private final Cipher tagCipher, frameCipher, framePeekingCipher;
+	private final IvEncoder frameIvEncoder, framePeekingIvEncoder;
 	private final ErasableKey tagKey, frameKey;
 	private final int blockSize;
-	private final byte[] iv, ciphertext;
+	private final byte[] frameIv, framePeekingIv, ciphertext;
 
 	private boolean readTag;
 	private long frameNumber;
 
 	IncomingEncryptionLayerImpl(InputStream in, Cipher tagCipher,
-			Cipher frameCipher, ErasableKey tagKey, ErasableKey frameKey,
-			boolean readTag) {
+			Cipher frameCipher, Cipher framePeekingCipher,
+			IvEncoder frameIvEncoder, IvEncoder framePeekingIvEncoder,
+			ErasableKey tagKey, ErasableKey frameKey, boolean readTag) {
 		this.in = in;
 		this.tagCipher = tagCipher;
 		this.frameCipher = frameCipher;
+		this.framePeekingCipher = framePeekingCipher;
+		this.frameIvEncoder = frameIvEncoder;
+		this.framePeekingIvEncoder = framePeekingIvEncoder;
 		this.tagKey = tagKey;
 		this.frameKey = frameKey;
 		this.readTag = readTag;
 		blockSize = frameCipher.getBlockSize();
 		if(blockSize < FRAME_HEADER_LENGTH)
 			throw new IllegalArgumentException();
-		iv = IvEncoder.encodeIv(0L, blockSize);
+		frameIv = frameIvEncoder.encodeIv(0L);
+		framePeekingIv = framePeekingIvEncoder.encodeIv(0L);
 		ciphertext = new byte[MAX_FRAME_LENGTH];
 		frameNumber = 0L;
 	}
@@ -65,21 +72,18 @@ class IncomingEncryptionLayerImpl implements FrameReader {
 			int offset = 0;
 			while(offset < blockSize) {
 				int read = in.read(ciphertext, offset, blockSize - offset);
-				if(read == -1) {
-					if(offset == 0 && !readTag) return false;
-					throw new EOFException();
-				}
+				if(read == -1) throw new EOFException();
 				offset += read;
 			}
 			readTag = false;
-			// Decrypt the first block of the frame
+			// Decrypt the first block of the frame to peek at the header
+			framePeekingIvEncoder.updateIv(framePeekingIv, frameNumber);
+			IvParameterSpec ivSpec = new IvParameterSpec(framePeekingIv);
 			byte[] plaintext = f.getBuffer();
 			try {
-				IvEncoder.updateIv(iv, frameNumber);
-				IvParameterSpec ivSpec = new IvParameterSpec(iv);
-				frameCipher.init(Cipher.DECRYPT_MODE, frameKey, ivSpec);
-				int decrypted = frameCipher.update(ciphertext, 0, blockSize,
-						plaintext);
+				framePeekingCipher.init(Cipher.DECRYPT_MODE, frameKey, ivSpec);
+				int decrypted = framePeekingCipher.update(ciphertext, 0,
+						blockSize, plaintext);
 				if(decrypted != blockSize) throw new RuntimeException();
 			} catch(GeneralSecurityException badCipher) {
 				throw new RuntimeException(badCipher);
@@ -95,16 +99,19 @@ class IncomingEncryptionLayerImpl implements FrameReader {
 				if(read == -1) throw new EOFException();
 				offset += read;
 			}
-			// Decrypt the remainder of the frame
+			// Decrypt and authenticate the entire frame
+			frameIvEncoder.updateIv(frameIv, frameNumber);
+			ivSpec = new IvParameterSpec(frameIv);
 			try {
-				int decrypted = frameCipher.doFinal(ciphertext, blockSize,
-						length - blockSize, plaintext, blockSize);
-				if(decrypted != length - blockSize)
+				frameCipher.init(Cipher.DECRYPT_MODE, frameKey, ivSpec);
+				int decrypted = frameCipher.doFinal(ciphertext, 0, length,
+						plaintext);
+				if(decrypted != length - MAC_LENGTH)
 					throw new RuntimeException();
 			} catch(GeneralSecurityException badCipher) {
 				throw new RuntimeException(badCipher);
 			}
-			f.setLength(length);
+			f.setLength(length - MAC_LENGTH);
 			frameNumber++;
 			return true;
 		} catch(IOException e) {
diff --git a/components/net/sf/briar/transport/IvEncoder.java b/components/net/sf/briar/transport/IvEncoder.java
deleted file mode 100644
index c922cadc81fe94b39e9a9c3d6d2c213bef3b33ad..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/IvEncoder.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.sf.briar.transport;
-
-import net.sf.briar.util.ByteUtils;
-
-class IvEncoder {
-
-	static byte[] encodeIv(long frame, int blockSize) {
-		if(frame < 0 || frame > ByteUtils.MAX_32_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		byte[] iv = new byte[blockSize];
-		updateIv(iv, frame);
-		return iv;
-	}
-
-	static void updateIv(byte[] iv, long frame) {
-		// Encode the frame number as a uint32
-		ByteUtils.writeUint32(frame, iv, 0);
-	}
-}
diff --git a/components/net/sf/briar/transport/OutgoingAuthenticationLayerImpl.java b/components/net/sf/briar/transport/OutgoingAuthenticationLayerImpl.java
deleted file mode 100644
index 2e3798dc36b54560f73872578b9eef8852e07536..0000000000000000000000000000000000000000
--- a/components/net/sf/briar/transport/OutgoingAuthenticationLayerImpl.java
+++ /dev/null
@@ -1,51 +0,0 @@
-package net.sf.briar.transport;
-
-import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
-
-import java.io.IOException;
-import java.security.InvalidKeyException;
-
-import javax.crypto.Mac;
-import javax.crypto.ShortBufferException;
-
-import net.sf.briar.api.crypto.ErasableKey;
-
-class OutgoingAuthenticationLayerImpl implements FrameWriter {
-
-	private final FrameWriter out;
-	private final Mac mac;
-
-	OutgoingAuthenticationLayerImpl(FrameWriter out, Mac mac,
-			ErasableKey macKey) {
-		this.out = out;
-		this.mac = mac;
-		try {
-			mac.init(macKey);
-		} catch(InvalidKeyException badKey) {
-			throw new IllegalArgumentException(badKey);
-		}
-		macKey.erase();
-		if(mac.getMacLength() != MAC_LENGTH)
-			throw new IllegalArgumentException();
-	}
-
-	public void writeFrame(Frame f) throws IOException {
-		byte[] buf = f.getBuffer();
-		int length = f.getLength() - MAC_LENGTH;
-		mac.update(buf, 0, length);
-		try {
-			mac.doFinal(buf, length);
-		} catch(ShortBufferException badMac) {
-			throw new RuntimeException(badMac);
-		}
-		out.writeFrame(f);
-	}
-
-	public void flush() throws IOException {
-		out.flush();
-	}
-
-	public long getRemainingCapacity() {
-		return out.getRemainingCapacity();
-	}
-}
diff --git a/components/net/sf/briar/transport/OutgoingEncryptionLayerImpl.java b/components/net/sf/briar/transport/OutgoingEncryptionLayerImpl.java
index f79b6831d6c4f9642287f2c803973e2212a4eb98..d79d498b1f76dd77b14f681ef4b42cd32c7c65e2 100644
--- a/components/net/sf/briar/transport/OutgoingEncryptionLayerImpl.java
+++ b/components/net/sf/briar/transport/OutgoingEncryptionLayerImpl.java
@@ -1,5 +1,6 @@
 package net.sf.briar.transport;
 
+import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
@@ -11,56 +12,58 @@ import javax.crypto.Cipher;
 import javax.crypto.spec.IvParameterSpec;
 
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 
 class OutgoingEncryptionLayerImpl implements FrameWriter {
 
 	private final OutputStream out;
 	private final Cipher tagCipher, frameCipher;
+	private final IvEncoder frameIvEncoder;
 	private final ErasableKey tagKey, frameKey;
-	private final byte[] iv, ciphertext;
+	private final byte[] frameIv, ciphertext;
 
 	private long capacity, frameNumber;
 
 	OutgoingEncryptionLayerImpl(OutputStream out, long capacity,
-			Cipher tagCipher, Cipher frameCipher, ErasableKey tagKey,
-			ErasableKey frameKey) {
+			Cipher tagCipher, Cipher frameCipher, IvEncoder frameIvEncoder,
+			ErasableKey tagKey, ErasableKey frameKey) {
 		this.out = out;
 		this.capacity = capacity;
 		this.tagCipher = tagCipher;
 		this.frameCipher = frameCipher;
+		this.frameIvEncoder = frameIvEncoder;
 		this.tagKey = tagKey;
 		this.frameKey = frameKey;
-		iv = IvEncoder.encodeIv(0L, frameCipher.getBlockSize());
+		frameIv = frameIvEncoder.encodeIv(0L);
 		ciphertext = new byte[TAG_LENGTH + MAX_FRAME_LENGTH];
 		frameNumber = 0L;
 	}
 
 	public void writeFrame(Frame f) throws IOException {
 		byte[] plaintext = f.getBuffer();
-		int length = f.getLength();
-		int offset = 0;
+		int offset = 0, length = f.getLength();
 		if(frameNumber == 0) {
 			TagEncoder.encodeTag(ciphertext, tagCipher, tagKey);
 			offset = TAG_LENGTH;
 		}
-		IvEncoder.updateIv(iv, frameNumber);
-		IvParameterSpec ivSpec = new IvParameterSpec(iv);
+		frameIvEncoder.updateIv(frameIv, frameNumber);
+		IvParameterSpec ivSpec = new IvParameterSpec(frameIv);
 		try {
 			frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
 			int encrypted = frameCipher.doFinal(plaintext, 0, length,
 					ciphertext, offset);
-			if(encrypted != length) throw new RuntimeException();
+			if(encrypted != length + MAC_LENGTH) throw new RuntimeException();
 		} catch(GeneralSecurityException badCipher) {
 			throw new RuntimeException(badCipher);
 		}
 		try {
-			out.write(ciphertext, 0, offset + length);
+			out.write(ciphertext, 0, offset + length + MAC_LENGTH);
 		} catch(IOException e) {
 			frameKey.erase();
 			tagKey.erase();
 			throw e;
 		}
-		capacity -= offset + length;
+		capacity -= offset + length + MAC_LENGTH;
 		frameNumber++;
 	}
 
diff --git a/test/build.xml b/test/build.xml
index 725693a361aa9d68e1fddd767fec1759836a3182..9fe9705e6536e7ac5d4c7d260ee937a358d4e235 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -18,6 +18,7 @@
 			<test name='net.sf.briar.ProtocolIntegrationTest'/>
 			<test name='net.sf.briar.crypto.CounterModeTest'/>
 			<test name='net.sf.briar.crypto.ErasableKeyTest'/>
+			<test name='net.sf.briar.crypto.FramePeekingTest'/>
 			<test name='net.sf.briar.crypto.KeyDerivationTest'/>
 			<test name='net.sf.briar.db.BasicH2Test'/>
 			<test name='net.sf.briar.db.DatabaseCleanerImplTest'/>
diff --git a/test/net/sf/briar/crypto/FramePeekingTest.java b/test/net/sf/briar/crypto/FramePeekingTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..f99a9ba1920bdce216140fd5ede3948685ec5bdb
--- /dev/null
+++ b/test/net/sf/briar/crypto/FramePeekingTest.java
@@ -0,0 +1,44 @@
+package net.sf.briar.crypto;
+
+import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+
+import net.sf.briar.BriarTestCase;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
+import net.sf.briar.util.ByteUtils;
+
+import org.junit.Test;
+
+public class FramePeekingTest extends BriarTestCase {
+
+	@Test
+	public void testFramePeeking() throws Exception {
+		CryptoComponent crypto = new CryptoComponentImpl();
+		ErasableKey key = crypto.generateTestKey();
+
+		Cipher frameCipher = crypto.getFrameCipher();
+		IvEncoder frameIvEncoder = crypto.getFrameIvEncoder();
+		byte[] iv = frameIvEncoder.encodeIv(ByteUtils.MAX_32_BIT_UNSIGNED);
+		IvParameterSpec ivSpec = new IvParameterSpec(iv);
+		frameCipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
+
+		Cipher framePeekingCipher = crypto.getFramePeekingCipher();
+		IvEncoder framePeekingIvEncoder = crypto.getFramePeekingIvEncoder();
+		iv = framePeekingIvEncoder.encodeIv(ByteUtils.MAX_32_BIT_UNSIGNED);
+		ivSpec = new IvParameterSpec(iv);
+		framePeekingCipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
+
+		// The ciphers should produce the same ciphertext, apart from the MAC
+		byte[] plaintext = new byte[123];
+		byte[] ciphertext = frameCipher.doFinal(plaintext);
+		byte[] peekingCiphertext = framePeekingCipher.doFinal(plaintext);
+		assertEquals(ciphertext.length, peekingCiphertext.length + MAC_LENGTH);
+		for(int i = 0; i < peekingCiphertext.length; i++) {
+			assertEquals(ciphertext[i], peekingCiphertext[i]);
+		}
+	}
+}
diff --git a/test/net/sf/briar/crypto/KeyDerivationTest.java b/test/net/sf/briar/crypto/KeyDerivationTest.java
index b777cd7921491bafd8a483db0a65d9b8bec5b803..cc4335c9ae36e2843451b7d1fd8f28516da4abb0 100644
--- a/test/net/sf/briar/crypto/KeyDerivationTest.java
+++ b/test/net/sf/briar/crypto/KeyDerivationTest.java
@@ -25,17 +25,15 @@ public class KeyDerivationTest extends BriarTestCase {
 	}
 
 	@Test
-	public void testSixKeysAreDistinct() {
+	public void testKeysAreDistinct() {
 		List<ErasableKey> keys = new ArrayList<ErasableKey>();
 		keys.add(crypto.deriveFrameKey(secret, true));
 		keys.add(crypto.deriveFrameKey(secret, false));
 		keys.add(crypto.deriveTagKey(secret, true));
 		keys.add(crypto.deriveTagKey(secret, false));
-		keys.add(crypto.deriveMacKey(secret, true));
-		keys.add(crypto.deriveMacKey(secret, false));
-		for(int i = 0; i < 6; i++) {
+		for(int i = 0; i < 4; i++) {
 			byte[] keyI = keys.get(i).getEncoded();
-			for(int j = 0; j < 6; j++) {
+			for(int j = 0; j < 4; j++) {
 				byte[] keyJ = keys.get(j).getEncoded();
 				assertEquals(i == j, Arrays.equals(keyI, keyJ));
 			}
diff --git a/test/net/sf/briar/transport/ConnectionReaderImplTest.java b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
index da0c140a29071d43926822a3a4757cf4e5deedb3..78b024c9110771e99b833028cfa505de2df2e196 100644
--- a/test/net/sf/briar/transport/ConnectionReaderImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionReaderImplTest.java
@@ -23,14 +23,8 @@ public class ConnectionReaderImplTest extends TransportTest {
 
 	@Test
 	public void testLengthZero() throws Exception {
-		int payloadLength = 0;
-		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
-		                        + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, true);
-		// Calculate the MAC
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
+		byte[] frame = new byte[FRAME_HEADER_LENGTH + MAC_LENGTH];
+		HeaderEncoder.encodeHeader(frame, 0, 0, 0, true);
 		// Read the frame
 		ByteArrayInputStream in = new ByteArrayInputStream(frame);
 		ConnectionReader r = createConnectionReader(in);
@@ -40,14 +34,8 @@ public class ConnectionReaderImplTest extends TransportTest {
 
 	@Test
 	public void testLengthOne() throws Exception {
-		int payloadLength = 1;
-		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
-		                        + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, true);
-		// Calculate the MAC
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
+		byte[] frame = new byte[FRAME_HEADER_LENGTH + 1 + MAC_LENGTH];
+		HeaderEncoder.encodeHeader(frame, 0, 1, 0, true);
 		// Read the frame
 		ByteArrayInputStream in = new ByteArrayInputStream(frame);
 		ConnectionReader r = createConnectionReader(in);
@@ -61,14 +49,9 @@ public class ConnectionReaderImplTest extends TransportTest {
 		// First frame: max payload length
 		byte[] frame = new byte[MAX_FRAME_LENGTH];
 		HeaderEncoder.encodeHeader(frame, 0, MAX_PAYLOAD_LENGTH, 0, false);
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH);
 		// Second frame: max payload length plus one
 		byte[] frame1 = new byte[MAX_FRAME_LENGTH + 1];
 		HeaderEncoder.encodeHeader(frame1, 1, MAX_PAYLOAD_LENGTH + 1, 0, false);
-		mac.update(frame1, 0, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH + 1);
-		mac.doFinal(frame1, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH + 1);
 		// Concatenate the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		out.write(frame);
@@ -93,15 +76,10 @@ public class ConnectionReaderImplTest extends TransportTest {
 		byte[] frame = new byte[MAX_FRAME_LENGTH];
 		HeaderEncoder.encodeHeader(frame, 0, MAX_PAYLOAD_LENGTH - paddingLength,
 				paddingLength, false);
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH);
 		// Second frame: max payload length plus one, including padding
 		byte[] frame1 = new byte[MAX_FRAME_LENGTH + 1];
 		HeaderEncoder.encodeHeader(frame1, 1,
 				MAX_PAYLOAD_LENGTH + 1 - paddingLength, paddingLength, false);
-		mac.update(frame1, 0, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH + 1);
-		mac.doFinal(frame1, FRAME_HEADER_LENGTH + MAX_PAYLOAD_LENGTH + 1);
 		// Concatenate the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		out.write(frame);
@@ -128,10 +106,6 @@ public class ConnectionReaderImplTest extends TransportTest {
 				false);
 		// Set a byte of the padding to a non-zero value
 		frame[FRAME_HEADER_LENGTH + payloadLength] = 1;
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength
-				+ paddingLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength + paddingLength);
 		// Read the frame
 		ByteArrayInputStream in = new ByteArrayInputStream(frame);
 		ConnectionReader r = createConnectionReader(in);
@@ -149,16 +123,11 @@ public class ConnectionReaderImplTest extends TransportTest {
 		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
 		                        + MAC_LENGTH];
 		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, false);
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
 		// Second frame: 1234-byte payload
 		int payloadLength1 = 1234;
 		byte[] frame1 = new byte[FRAME_HEADER_LENGTH + payloadLength1
 		                         + MAC_LENGTH];
 		HeaderEncoder.encodeHeader(frame1, 1, payloadLength1, 0, true);
-		mac.update(frame1, 0, FRAME_HEADER_LENGTH + payloadLength1);
-		mac.doFinal(frame1, FRAME_HEADER_LENGTH + payloadLength1);
 		// Concatenate the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		out.write(frame);
@@ -182,16 +151,11 @@ public class ConnectionReaderImplTest extends TransportTest {
 		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
 		                        + MAC_LENGTH];
 		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, false);
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
 		// Second frame: 1234-byte payload
 		int payloadLength1 = 1234;
 		byte[] frame1 = new byte[FRAME_HEADER_LENGTH + payloadLength1
 		                         + MAC_LENGTH];
 		HeaderEncoder.encodeHeader(frame1, 1, payloadLength1, 0, false);
-		mac.update(frame1, 0, FRAME_HEADER_LENGTH + payloadLength1);
-		mac.doFinal(frame1, FRAME_HEADER_LENGTH + payloadLength1);
 		// Concatenate the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		out.write(frame);
@@ -211,52 +175,8 @@ public class ConnectionReaderImplTest extends TransportTest {
 		} catch(FormatException expected) {}
 	}
 
-	@Test
-	public void testCorruptPayload() throws Exception {
-		int payloadLength = 8;
-		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
-		                        + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, false);
-		// Calculate the MAC
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
-		// Modify the payload
-		frame[12] ^= 1;
-		// Try to read the frame - not a single byte should be read
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		ConnectionReader r = createConnectionReader(in);
-		try {
-			r.getInputStream().read();
-			fail();
-		} catch(FormatException expected) {}
-	}
-
-	@Test
-	public void testCorruptMac() throws Exception {
-		int payloadLength = 8;
-		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
-		                        + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, false);
-		// Calculate the MAC
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
-		// Modify the MAC
-		frame[17] ^= 1;
-		// Try to read the frame - not a single byte should be read
-		ByteArrayInputStream in = new ByteArrayInputStream(frame);
-		ConnectionReader r = createConnectionReader(in);
-		try {
-			r.getInputStream().read();
-			fail();
-		} catch(FormatException expected) {}
-	}
-
 	private ConnectionReader createConnectionReader(InputStream in) {
 		FrameReader encryption = new NullIncomingEncryptionLayer(in);
-		FrameReader authentication = new IncomingAuthenticationLayerImpl(
-				encryption, mac, macKey);
-		return new ConnectionReaderImpl(authentication);
+		return new ConnectionReaderImpl(encryption);
 	}
 }
diff --git a/test/net/sf/briar/transport/ConnectionWriterImplTest.java b/test/net/sf/briar/transport/ConnectionWriterImplTest.java
index 5f825e1711839412a1be295132c30af0a45348b6..9f37b1b032ca26cb661d8d737b7c395b27ea0dcd 100644
--- a/test/net/sf/briar/transport/ConnectionWriterImplTest.java
+++ b/test/net/sf/briar/transport/ConnectionWriterImplTest.java
@@ -30,14 +30,9 @@ public class ConnectionWriterImplTest extends TransportTest {
 
 	@Test
 	public void testSingleByteFrame() throws Exception {
-		int payloadLength = 1;
-		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
-		                        + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, false);
-		// Calculate the MAC
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
+		// Create a single-byte frame
+		byte[] frame = new byte[FRAME_HEADER_LENGTH + 1 + MAC_LENGTH];
+		HeaderEncoder.encodeHeader(frame, 0, 1, 0, false);
 		// Check that the ConnectionWriter gets the same results
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		ConnectionWriter w = createConnectionWriter(out);
@@ -75,20 +70,11 @@ public class ConnectionWriterImplTest extends TransportTest {
 	@Test
 	public void testMultipleFrames() throws Exception {
 		// First frame: 123-byte payload
-		int payloadLength = 123;
-		byte[] frame = new byte[FRAME_HEADER_LENGTH + payloadLength
-		                        + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame, 0, payloadLength, 0, false);
-		mac.init(macKey);
-		mac.update(frame, 0, FRAME_HEADER_LENGTH + payloadLength);
-		mac.doFinal(frame, FRAME_HEADER_LENGTH + payloadLength);
+		byte[] frame = new byte[FRAME_HEADER_LENGTH + 123 + MAC_LENGTH];
+		HeaderEncoder.encodeHeader(frame, 0, 123, 0, false);
 		// Second frame: 1234-byte payload
-		int payloadLength1 = 1234;
-		byte[] frame1 = new byte[FRAME_HEADER_LENGTH + payloadLength1
-		                         + MAC_LENGTH];
-		HeaderEncoder.encodeHeader(frame1, 1, payloadLength1, 0, false);
-		mac.update(frame1, 0, FRAME_HEADER_LENGTH + 1234);
-		mac.doFinal(frame1, FRAME_HEADER_LENGTH + 1234);
+		byte[] frame1 = new byte[FRAME_HEADER_LENGTH + 1234 + MAC_LENGTH];
+		HeaderEncoder.encodeHeader(frame1, 1, 1234, 0, false);
 		// Concatenate the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		out.write(frame);
@@ -107,8 +93,6 @@ public class ConnectionWriterImplTest extends TransportTest {
 
 	private ConnectionWriter createConnectionWriter(OutputStream out) {
 		FrameWriter encryption = new NullOutgoingEncryptionLayer(out);
-		FrameWriter authentication =
-			new OutgoingAuthenticationLayerImpl(encryption, mac, macKey);
-		return new ConnectionWriterImpl(authentication);
+		return new ConnectionWriterImpl(encryption);
 	}
 }
diff --git a/test/net/sf/briar/transport/FrameReadWriteTest.java b/test/net/sf/briar/transport/FrameReadWriteTest.java
index 97af64c4b3bf05b0dfb9a791ef975948f6a0e220..11de90329ec8a8b1898df65ed04ab0e69da2e945 100644
--- a/test/net/sf/briar/transport/FrameReadWriteTest.java
+++ b/test/net/sf/briar/transport/FrameReadWriteTest.java
@@ -10,11 +10,11 @@ import java.io.OutputStream;
 import java.util.Random;
 
 import javax.crypto.Cipher;
-import javax.crypto.Mac;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 import net.sf.briar.api.transport.ConnectionReader;
 import net.sf.briar.api.transport.ConnectionWriter;
 import net.sf.briar.crypto.CryptoModule;
@@ -27,11 +27,11 @@ import com.google.inject.Injector;
 public class FrameReadWriteTest extends BriarTestCase {
 
 	private final CryptoComponent crypto;
-	private final Cipher tagCipher, frameCipher;
-	private final Mac mac;
+	private final Cipher tagCipher, frameCipher, framePeekingCipher;
+	private final IvEncoder frameIvEncoder, framePeekingIvEncoder;
 	private final Random random;
 	private final byte[] outSecret;
-	private final ErasableKey tagKey, frameKey, macKey;
+	private final ErasableKey tagKey, frameKey;
 
 	public FrameReadWriteTest() {
 		super();
@@ -39,14 +39,15 @@ public class FrameReadWriteTest extends BriarTestCase {
 		crypto = i.getInstance(CryptoComponent.class);
 		tagCipher = crypto.getTagCipher();
 		frameCipher = crypto.getFrameCipher();
-		mac = crypto.getMac();
+		framePeekingCipher = crypto.getFramePeekingCipher();
+		frameIvEncoder = crypto.getFrameIvEncoder();
+		framePeekingIvEncoder = crypto.getFramePeekingIvEncoder();
 		random = new Random();
 		// Since we're sending frames to ourselves, we only need outgoing keys
 		outSecret = new byte[32];
 		random.nextBytes(outSecret);
 		tagKey = crypto.deriveTagKey(outSecret, true);
 		frameKey = crypto.deriveFrameKey(outSecret, true);
-		macKey = crypto.deriveMacKey(outSecret, true);
 	}
 
 	@Test
@@ -71,14 +72,12 @@ public class FrameReadWriteTest extends BriarTestCase {
 		// Copy the keys - the copies will be erased
 		ErasableKey tagCopy = tagKey.copy();
 		ErasableKey frameCopy = frameKey.copy();
-		ErasableKey macCopy = macKey.copy();
 		// Write the frames
 		ByteArrayOutputStream out = new ByteArrayOutputStream();
 		FrameWriter encryptionOut = new OutgoingEncryptionLayerImpl(out,
-				Long.MAX_VALUE, tagCipher, frameCipher, tagCopy, frameCopy);
-		FrameWriter authenticationOut = new OutgoingAuthenticationLayerImpl(
-				encryptionOut, mac, macCopy);
-		ConnectionWriter writer = new ConnectionWriterImpl(authenticationOut);
+				Long.MAX_VALUE, tagCipher, frameCipher, frameIvEncoder, tagCopy,
+				frameCopy);
+		ConnectionWriter writer = new ConnectionWriterImpl(encryptionOut);
 		OutputStream out1 = writer.getOutputStream();
 		out1.write(frame);
 		out1.flush();
@@ -92,10 +91,9 @@ public class FrameReadWriteTest extends BriarTestCase {
 		assertTrue(TagEncoder.decodeTag(recoveredTag, tagCipher, tagKey));
 		// Read the frames back
 		FrameReader encryptionIn = new IncomingEncryptionLayerImpl(in,
-				tagCipher, frameCipher, tagKey, frameKey, false);
-		FrameReader authenticationIn = new IncomingAuthenticationLayerImpl(
-				encryptionIn, mac, macKey);
-		ConnectionReader reader = new ConnectionReaderImpl(authenticationIn);
+				tagCipher, frameCipher, framePeekingCipher, frameIvEncoder,
+				framePeekingIvEncoder, tagKey, frameKey, false);
+		ConnectionReader reader = new ConnectionReaderImpl(encryptionIn);
 		InputStream in1 = reader.getInputStream();
 		byte[] recovered = new byte[frame.length];
 		int offset = 0;
diff --git a/test/net/sf/briar/transport/IncomingEncryptionLayerImplTest.java b/test/net/sf/briar/transport/IncomingEncryptionLayerImplTest.java
index 1b3f09759393e1607980200a81846b9693b6abb7..06fa5d5e00926c9da794ca3fb849b96641e61be2 100644
--- a/test/net/sf/briar/transport/IncomingEncryptionLayerImplTest.java
+++ b/test/net/sf/briar/transport/IncomingEncryptionLayerImplTest.java
@@ -1,7 +1,6 @@
 package net.sf.briar.transport;
 
 import static net.sf.briar.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
-import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.TAG_LENGTH;
 
 import java.io.ByteArrayInputStream;
@@ -12,6 +11,7 @@ import javax.crypto.spec.IvParameterSpec;
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 import net.sf.briar.crypto.CryptoModule;
 
 import org.apache.commons.io.output.ByteArrayOutputStream;
@@ -22,7 +22,8 @@ import com.google.inject.Injector;
 
 public class IncomingEncryptionLayerImplTest extends BriarTestCase {
 
-	private final Cipher tagCipher, frameCipher;
+	private final Cipher tagCipher, frameCipher, framePeekingCipher;
+	private final IvEncoder frameIvEncoder, framePeekingIvEncoder;
 	private final ErasableKey tagKey, frameKey;
 
 	public IncomingEncryptionLayerImplTest() {
@@ -31,6 +32,9 @@ public class IncomingEncryptionLayerImplTest extends BriarTestCase {
 		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
 		tagCipher = crypto.getTagCipher();
 		frameCipher = crypto.getFrameCipher();
+		framePeekingCipher = crypto.getFramePeekingCipher();
+		frameIvEncoder = crypto.getFrameIvEncoder();
+		framePeekingIvEncoder = crypto.getFramePeekingIvEncoder();
 		tagKey = crypto.generateTestKey();
 		frameKey = crypto.generateTestKey();
 	}
@@ -41,16 +45,16 @@ public class IncomingEncryptionLayerImplTest extends BriarTestCase {
 		byte[] tag = new byte[TAG_LENGTH];
 		TagEncoder.encodeTag(tag, tagCipher, tagKey);
 		// Calculate the ciphertext for the first frame
-		byte[] plaintext = new byte[FRAME_HEADER_LENGTH + 123 + MAC_LENGTH];
+		byte[] plaintext = new byte[FRAME_HEADER_LENGTH + 123];
 		HeaderEncoder.encodeHeader(plaintext, 0L, 123, 0, false);
-		byte[] iv = IvEncoder.encodeIv(0L, frameCipher.getBlockSize());
+		byte[] iv = frameIvEncoder.encodeIv(0L);
 		IvParameterSpec ivSpec = new IvParameterSpec(iv);
 		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
-		byte[] ciphertext = frameCipher.doFinal(plaintext, 0, plaintext.length);
+		byte[] ciphertext = frameCipher.doFinal(plaintext);
 		// Calculate the ciphertext for the second frame
-		byte[] plaintext1 = new byte[FRAME_HEADER_LENGTH + 1234 + MAC_LENGTH];
+		byte[] plaintext1 = new byte[FRAME_HEADER_LENGTH + 1234];
 		HeaderEncoder.encodeHeader(plaintext1, 1L, 1234, 0, false);
-		IvEncoder.updateIv(iv, 1L);
+		frameIvEncoder.updateIv(iv, 1L);
 		ivSpec = new IvParameterSpec(iv);
 		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
 		byte[] ciphertext1 = frameCipher.doFinal(plaintext1, 0,
@@ -63,7 +67,8 @@ public class IncomingEncryptionLayerImplTest extends BriarTestCase {
 		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
 		// Use the encryption layer to decrypt the ciphertext
 		FrameReader decrypter = new IncomingEncryptionLayerImpl(in, tagCipher,
-				frameCipher, tagKey, frameKey, true);
+				frameCipher, framePeekingCipher, frameIvEncoder,
+				framePeekingIvEncoder, tagKey, frameKey, true);
 		// First frame
 		Frame f = new Frame();
 		assertTrue(decrypter.readFrame(f));
@@ -86,16 +91,16 @@ public class IncomingEncryptionLayerImplTest extends BriarTestCase {
 	@Test
 	public void testDecryptionWithoutTag() throws Exception {
 		// Calculate the ciphertext for the first frame
-		byte[] plaintext = new byte[FRAME_HEADER_LENGTH + 123 + MAC_LENGTH];
+		byte[] plaintext = new byte[FRAME_HEADER_LENGTH + 123];
 		HeaderEncoder.encodeHeader(plaintext, 0L, 123, 0, false);
-		byte[] iv = IvEncoder.encodeIv(0L, frameCipher.getBlockSize());
+		byte[] iv = frameIvEncoder.encodeIv(0L);
 		IvParameterSpec ivSpec = new IvParameterSpec(iv);
 		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
-		byte[] ciphertext = frameCipher.doFinal(plaintext, 0, plaintext.length);
+		byte[] ciphertext = frameCipher.doFinal(plaintext);
 		// Calculate the ciphertext for the second frame
-		byte[] plaintext1 = new byte[FRAME_HEADER_LENGTH + 1234 + MAC_LENGTH];
+		byte[] plaintext1 = new byte[FRAME_HEADER_LENGTH + 1234];
 		HeaderEncoder.encodeHeader(plaintext1, 1L, 1234, 0, false);
-		IvEncoder.updateIv(iv, 1L);
+		frameIvEncoder.updateIv(iv, 1L);
 		ivSpec = new IvParameterSpec(iv);
 		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
 		byte[] ciphertext1 = frameCipher.doFinal(plaintext1, 0,
@@ -107,7 +112,8 @@ public class IncomingEncryptionLayerImplTest extends BriarTestCase {
 		ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
 		// Use the encryption layer to decrypt the ciphertext
 		FrameReader decrypter = new IncomingEncryptionLayerImpl(in, tagCipher,
-				frameCipher, tagKey, frameKey, false);
+				frameCipher, framePeekingCipher, frameIvEncoder,
+				framePeekingIvEncoder, tagKey, frameKey, false);
 		// First frame
 		Frame f = new Frame();
 		assertTrue(decrypter.readFrame(f));
diff --git a/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java b/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java
index dcf0386702c2eced15b8afb961f28bc259890f9e..c5d4410f5397d0fd862bfc30419178e6061b4fb8 100644
--- a/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java
+++ b/test/net/sf/briar/transport/NullIncomingEncryptionLayer.java
@@ -42,7 +42,7 @@ class NullIncomingEncryptionLayer implements FrameReader {
 			if(read == -1) throw new EOFException();
 			offset += read;
 		}
-		f.setLength(length);
+		f.setLength(length - MAC_LENGTH);
 		return true;
 	}
 }
diff --git a/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java b/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java
index 598562877bcdf93c99c4dcc0df74805d663a0523..ae7ccdd174b43e439dfe282462f98c0da38d99ba 100644
--- a/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java
+++ b/test/net/sf/briar/transport/NullOutgoingEncryptionLayer.java
@@ -1,5 +1,7 @@
 package net.sf.briar.transport;
 
+import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
+
 import java.io.IOException;
 import java.io.OutputStream;
 
@@ -21,7 +23,7 @@ class NullOutgoingEncryptionLayer implements FrameWriter {
 	}
 
 	public void writeFrame(Frame f) throws IOException {
-		out.write(f.getBuffer(), 0, f.getLength());
+		out.write(f.getBuffer(), 0, f.getLength() + MAC_LENGTH);
 		capacity -= f.getLength();
 	}
 
diff --git a/test/net/sf/briar/transport/OutgoingEncryptionLayerImplTest.java b/test/net/sf/briar/transport/OutgoingEncryptionLayerImplTest.java
index aca3f7192be20c26a4d5867e29542abaf7156abf..3393f9b6bcf76379b6b712f6fda527bafeedeb90 100644
--- a/test/net/sf/briar/transport/OutgoingEncryptionLayerImplTest.java
+++ b/test/net/sf/briar/transport/OutgoingEncryptionLayerImplTest.java
@@ -11,6 +11,7 @@ import javax.crypto.spec.IvParameterSpec;
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
+import net.sf.briar.api.crypto.IvEncoder;
 import net.sf.briar.crypto.CryptoModule;
 
 import org.junit.Test;
@@ -20,9 +21,8 @@ import com.google.inject.Injector;
 
 public class OutgoingEncryptionLayerImplTest extends BriarTestCase {
 
-	private static final int MAC_LENGTH = 32;
-
 	private final Cipher tagCipher, frameCipher;
+	private final IvEncoder frameIvEncoder;
 	private final ErasableKey tagKey, frameKey;
 
 	public OutgoingEncryptionLayerImplTest() {
@@ -31,6 +31,7 @@ public class OutgoingEncryptionLayerImplTest extends BriarTestCase {
 		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
 		tagCipher = crypto.getTagCipher();
 		frameCipher = crypto.getFrameCipher();
+		frameIvEncoder = crypto.getFrameIvEncoder();
 		tagKey = crypto.generateTestKey();
 		frameKey = crypto.generateTestKey();
 	}
@@ -41,14 +42,14 @@ public class OutgoingEncryptionLayerImplTest extends BriarTestCase {
 		byte[] tag = new byte[TAG_LENGTH];
 		TagEncoder.encodeTag(tag, tagCipher, tagKey);
 		// Calculate the expected ciphertext for the first frame
-		byte[] iv = new byte[frameCipher.getBlockSize()];
-		byte[] plaintext = new byte[123 + MAC_LENGTH];
+		byte[] iv = frameIvEncoder.encodeIv(0L);
+		byte[] plaintext = new byte[123];
 		IvParameterSpec ivSpec = new IvParameterSpec(iv);
 		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
 		byte[] ciphertext = frameCipher.doFinal(plaintext);
 		// Calculate the expected ciphertext for the second frame
-		byte[] plaintext1 = new byte[1234 + MAC_LENGTH];
-		IvEncoder.updateIv(iv, 1L);
+		byte[] plaintext1 = new byte[1234];
+		frameIvEncoder.updateIv(iv, 1L);
 		ivSpec = new IvParameterSpec(iv);
 		frameCipher.init(Cipher.ENCRYPT_MODE, frameKey, ivSpec);
 		byte[] ciphertext1 = frameCipher.doFinal(plaintext1);
@@ -61,7 +62,8 @@ public class OutgoingEncryptionLayerImplTest extends BriarTestCase {
 		// Use the encryption layer to encrypt the plaintext
 		out.reset();
 		FrameWriter encrypter = new OutgoingEncryptionLayerImpl(out,
-				Long.MAX_VALUE, tagCipher, frameCipher, tagKey, frameKey);
+				Long.MAX_VALUE, tagCipher, frameCipher, frameIvEncoder, tagKey,
+				frameKey);
 		Frame f = new Frame();
 		System.arraycopy(plaintext, 0, f.getBuffer(), 0, plaintext.length);
 		f.setLength(plaintext.length);
diff --git a/test/net/sf/briar/transport/TransportTest.java b/test/net/sf/briar/transport/TransportTest.java
index c44e6fc2c76000ad8f8199168b9ea16e00f20f99..9764ec71d861104568411d87e3767fb9436acff6 100644
--- a/test/net/sf/briar/transport/TransportTest.java
+++ b/test/net/sf/briar/transport/TransportTest.java
@@ -4,7 +4,7 @@ import static net.sf.briar.api.transport.TransportConstants.FRAME_HEADER_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAC_LENGTH;
 import static net.sf.briar.api.transport.TransportConstants.MAX_FRAME_LENGTH;
 
-import javax.crypto.Mac;
+import javax.crypto.Cipher;
 
 import net.sf.briar.BriarTestCase;
 import net.sf.briar.api.crypto.CryptoComponent;
@@ -19,14 +19,14 @@ public abstract class TransportTest extends BriarTestCase {
 	static final int MAX_PAYLOAD_LENGTH =
 		MAX_FRAME_LENGTH - FRAME_HEADER_LENGTH - MAC_LENGTH;
 
-	protected final Mac mac;
-	protected final ErasableKey macKey;
+	protected final Cipher frameCipher;
+	protected final ErasableKey frameKey;
 
 	public TransportTest() throws Exception {
 		super();
 		Injector i = Guice.createInjector(new CryptoModule());
 		CryptoComponent crypto = i.getInstance(CryptoComponent.class);
-		mac = crypto.getMac();
-		macKey = crypto.generateTestKey();
+		frameCipher = crypto.getFrameCipher();
+		frameKey = crypto.generateTestKey();
 	}
 }