diff --git a/api/net/sf/briar/api/crypto/KeyParser.java b/api/net/sf/briar/api/crypto/KeyParser.java
new file mode 100644
index 0000000000000000000000000000000000000000..2363dee5d88846a26cf8b685f85a3afeae94e9c1
--- /dev/null
+++ b/api/net/sf/briar/api/crypto/KeyParser.java
@@ -0,0 +1,9 @@
+package net.sf.briar.api.crypto;
+
+import java.security.GeneralSecurityException;
+import java.security.PublicKey;
+
+public interface KeyParser {
+
+	PublicKey parsePublicKey(byte[] encodedKey) throws GeneralSecurityException;
+}
diff --git a/api/net/sf/briar/api/protocol/Message.java b/api/net/sf/briar/api/protocol/Message.java
index 715f1115aed2177ba5fe48b4189c688af13ab80b..3b1739fdfdef30f3169bb52711962c97a9d6ae6e 100644
--- a/api/net/sf/briar/api/protocol/Message.java
+++ b/api/net/sf/briar/api/protocol/Message.java
@@ -4,6 +4,8 @@ import net.sf.briar.api.serial.Raw;
 
 public interface Message extends Raw {
 
+	static final int MAX_SIZE = 1024 * 1023; // Not a typo
+
 	/** Returns the message's unique identifier. */
 	MessageId getId();
 
diff --git a/api/net/sf/briar/api/protocol/MessageEncoder.java b/api/net/sf/briar/api/protocol/MessageEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..95b26e1bf9f828d4ce3cc532ddebdb2f16c8433e
--- /dev/null
+++ b/api/net/sf/briar/api/protocol/MessageEncoder.java
@@ -0,0 +1,12 @@
+package net.sf.briar.api.protocol;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+public interface MessageEncoder {
+
+	Message encodeMessage(MessageId parent, GroupId group, String nick,
+			KeyPair keyPair, byte[] body) throws IOException,
+			GeneralSecurityException;
+}
diff --git a/api/net/sf/briar/api/protocol/MessageParser.java b/api/net/sf/briar/api/protocol/MessageParser.java
index 95a44e4cdd3528501098983de80ef60e31747a21..756ed84d3c7cadab27ff3e041c3568e936417b23 100644
--- a/api/net/sf/briar/api/protocol/MessageParser.java
+++ b/api/net/sf/briar/api/protocol/MessageParser.java
@@ -1,10 +1,9 @@
 package net.sf.briar.api.protocol;
 
-import java.security.SignatureException;
-
-import net.sf.briar.api.serial.FormatException;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
 
 public interface MessageParser {
 
-	Message parseMessage(byte[] body) throws FormatException, SignatureException;
+	Message parseMessage(byte[] raw) throws IOException, GeneralSecurityException;
 }
diff --git a/components/net/sf/briar/protocol/MessageEncoderImpl.java b/components/net/sf/briar/protocol/MessageEncoderImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..fac373dd1c4b409cdbdfa0cc2dfa54ea21f139b9
--- /dev/null
+++ b/components/net/sf/briar/protocol/MessageEncoderImpl.java
@@ -0,0 +1,63 @@
+package net.sf.briar.protocol;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.MessageDigest;
+import java.security.Signature;
+
+import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageEncoder;
+import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.serial.Writer;
+import net.sf.briar.api.serial.WriterFactory;
+
+class MessageEncoderImpl implements MessageEncoder {
+
+	private final Signature signature;
+	private final MessageDigest messageDigest;
+	private final WriterFactory writerFactory;
+
+	MessageEncoderImpl(Signature signature, MessageDigest messageDigest,
+			WriterFactory writerFactory) {
+		this.signature = signature;
+		this.messageDigest = messageDigest;
+		this.writerFactory = writerFactory;
+	}
+
+	public Message encodeMessage(MessageId parent, GroupId group, String nick,
+			KeyPair keyPair, byte[] body) throws IOException,
+			GeneralSecurityException {
+		long timestamp = System.currentTimeMillis();
+		byte[] encodedKey = keyPair.getPublic().getEncoded();
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		Writer w = writerFactory.createWriter(out);
+		w.writeRaw(parent);
+		w.writeRaw(group);
+		w.writeInt64(timestamp);
+		w.writeUtf8(nick);
+		w.writeRaw(encodedKey);
+		w.writeRaw(body);
+		byte[] signable = out.toByteArray();
+		signature.initSign(keyPair.getPrivate());
+		signature.update(signable);
+		byte[] sig = signature.sign();
+		w.writeRaw(sig);
+		byte[] raw = out.toByteArray();
+		w.close();
+		// The message ID is the hash of the entire message
+		messageDigest.reset();
+		messageDigest.update(raw);
+		MessageId id = new MessageId(messageDigest.digest());
+		// The author ID is the hash of the author's nick and public key
+		messageDigest.reset();
+		messageDigest.update(nick.getBytes("UTF-8"));
+		messageDigest.update((byte) 0); // Null separator
+		messageDigest.update(encodedKey);
+		AuthorId author = new AuthorId(messageDigest.digest());
+		return new MessageImpl(id, parent, group, author, timestamp, raw);
+	}
+}
diff --git a/components/net/sf/briar/protocol/MessageImpl.java b/components/net/sf/briar/protocol/MessageImpl.java
index 3fcbc273b96b3264944f65d7168af945c78965b4..653062c4181b231d07ac330583325c0f65fd5a49 100644
--- a/components/net/sf/briar/protocol/MessageImpl.java
+++ b/components/net/sf/briar/protocol/MessageImpl.java
@@ -12,16 +12,16 @@ public class MessageImpl implements Message {
 	private final GroupId group;
 	private final AuthorId author;
 	private final long timestamp;
-	private final byte[] body;
+	private final byte[] raw;
 
 	public MessageImpl(MessageId id, MessageId parent, GroupId group,
-			AuthorId author, long timestamp, byte[] body) {
+			AuthorId author, long timestamp, byte[] raw) {
 		this.id = id;
 		this.parent = parent;
 		this.group = group;
 		this.author = author;
 		this.timestamp = timestamp;
-		this.body = body;
+		this.raw = raw;
 	}
 
 	public MessageId getId() {
@@ -45,11 +45,11 @@ public class MessageImpl implements Message {
 	}
 
 	public int getSize() {
-		return body.length;
+		return raw.length;
 	}
 
 	public byte[] getBytes() {
-		return body;
+		return raw;
 	}
 
 	@Override
diff --git a/components/net/sf/briar/protocol/MessageParserImpl.java b/components/net/sf/briar/protocol/MessageParserImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f9b138a621da05bce164d88c6a96d15754e20935
--- /dev/null
+++ b/components/net/sf/briar/protocol/MessageParserImpl.java
@@ -0,0 +1,84 @@
+package net.sf.briar.protocol;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+
+import net.sf.briar.api.crypto.KeyParser;
+import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.protocol.GroupId;
+import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageId;
+import net.sf.briar.api.protocol.MessageParser;
+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;
+
+class MessageParserImpl implements MessageParser {
+
+	private final KeyParser keyParser;
+	private final Signature signature;
+	private final MessageDigest messageDigest;
+	private final ReaderFactory readerFactory;
+
+	MessageParserImpl(KeyParser keyParser, Signature signature,
+			MessageDigest messageDigest, ReaderFactory readerFactory) {
+		this.keyParser = keyParser;
+		this.signature = signature;
+		this.messageDigest = messageDigest;
+		this.readerFactory = readerFactory;
+	}
+
+	public Message parseMessage(byte[] raw) throws IOException,
+			GeneralSecurityException {
+		if(raw.length > Message.MAX_SIZE) throw new FormatException();
+		ByteArrayInputStream in = new ByteArrayInputStream(raw);
+		Reader r = readerFactory.createReader(in);
+		// Read the parent message ID
+		byte[] idBytes = r.readRaw();
+		if(idBytes.length != UniqueId.LENGTH) throw new FormatException();
+		MessageId parent = new MessageId(idBytes);
+		// Read the group ID
+		idBytes = r.readRaw();
+		if(idBytes.length != UniqueId.LENGTH) throw new FormatException();
+		GroupId group = new GroupId(idBytes);
+		// Read the timestamp
+		long timestamp = r.readInt64();
+		// Hash the author's nick and public key to get the author ID
+		String nick = r.readUtf8();
+		byte[] encodedKey = r.readRaw();
+		PublicKey publicKey = keyParser.parsePublicKey(encodedKey);
+		messageDigest.reset();
+		messageDigest.update(nick.getBytes("UTF-8"));
+		messageDigest.update((byte) 0); // Null separator
+		messageDigest.update(encodedKey);
+		AuthorId author = new AuthorId(messageDigest.digest());
+		// Skip the message body
+		r.readRaw();
+		// Read the signature and work out how long the signed message is
+		byte[] sig = r.readRaw();
+		int length = raw.length - sig.length - bytesToEncode(sig.length);
+		// Verify the signature
+		signature.initVerify(publicKey);
+		signature.update(raw, 0, length);
+		if(!signature.verify(sig)) throw new SignatureException();
+		// Hash the message, including the signature, to get the message ID
+		messageDigest.reset();
+		messageDigest.update(raw);
+		MessageId id = new MessageId(messageDigest.digest());
+		return new MessageImpl(id, parent, group, author, timestamp, raw);
+	}
+
+	// FIXME: Work out a better way of doing this
+	private int bytesToEncode(int i) {
+		if(i >= 0 && i <= Byte.MAX_VALUE) return 2;
+		if(i >= Byte.MIN_VALUE && i <= Byte.MAX_VALUE) return 3;
+		if(i >= Short.MIN_VALUE && i <= Short.MAX_VALUE) return 4;
+		return 6;
+	}
+}
diff --git a/test/net/sf/briar/protocol/BundleReadWriteTest.java b/test/net/sf/briar/protocol/BundleReadWriteTest.java
index 69ddd1241579ee7631242eba5543977c2ff43609..fcb26721e2d14064eff49a8cacad20c626bd70c0 100644
--- a/test/net/sf/briar/protocol/BundleReadWriteTest.java
+++ b/test/net/sf/briar/protocol/BundleReadWriteTest.java
@@ -5,19 +5,23 @@ import java.io.FileInputStream;
 import java.io.FileOutputStream;
 import java.io.RandomAccessFile;
 import java.security.GeneralSecurityException;
+import java.security.KeyFactory;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
 import java.security.Signature;
-import java.security.SignatureException;
+import java.security.spec.EncodedKeySpec;
+import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 
 import junit.framework.TestCase;
 import net.sf.briar.TestUtils;
-import net.sf.briar.api.protocol.AuthorId;
+import net.sf.briar.api.crypto.KeyParser;
 import net.sf.briar.api.protocol.Batch;
 import net.sf.briar.api.protocol.BatchBuilder;
 import net.sf.briar.api.protocol.BatchId;
@@ -27,11 +31,12 @@ import net.sf.briar.api.protocol.GroupId;
 import net.sf.briar.api.protocol.Header;
 import net.sf.briar.api.protocol.HeaderBuilder;
 import net.sf.briar.api.protocol.Message;
+import net.sf.briar.api.protocol.MessageEncoder;
 import net.sf.briar.api.protocol.MessageId;
 import net.sf.briar.api.protocol.MessageParser;
 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;
 import net.sf.briar.api.serial.WriterFactory;
 import net.sf.briar.serial.ReaderFactoryImpl;
@@ -59,27 +64,34 @@ public class BundleReadWriteTest extends TestCase {
 	private final Set<GroupId> subs = Collections.singleton(sub);
 	private final Map<String, String> transports =
 		Collections.singletonMap("foo", "bar");
+	private final String nick = "Foo Bar";
+	private final String messageBody = "This is the message body! Wooooooo!";
 
-	private final MessageId messageId = new MessageId(TestUtils.getRandomId());
-	private final AuthorId authorId = new AuthorId(TestUtils.getRandomId());
-	private final long timestamp = System.currentTimeMillis();
-	private final byte[] messageBody = new byte[123];
-	private final Message message = new MessageImpl(messageId, MessageId.NONE,
-			sub, authorId, timestamp, messageBody);
-
-	// FIXME: This test should not depend on an impl in another component
+	// FIXME: This test should not depend on impls in another component
+	private final ReaderFactory rf = new ReaderFactoryImpl();
 	private final WriterFactory wf = new WriterFactoryImpl();
 
 	private final KeyPair keyPair;
 	private final Signature sig;
 	private final MessageDigest digest;
+	private final KeyParser keyParser;
+	private final Message message;
 
-	public BundleReadWriteTest() throws NoSuchAlgorithmException {
+	public BundleReadWriteTest() throws Exception {
 		super();
 		keyPair = KeyPairGenerator.getInstance(KEY_PAIR_ALGO).generateKeyPair();
 		sig = Signature.getInstance(SIGNATURE_ALGO);
 		digest = MessageDigest.getInstance(DIGEST_ALGO);
+		keyParser = new KeyParser() {
+			public PublicKey parsePublicKey(byte[] encodedKey) throws GeneralSecurityException {
+				EncodedKeySpec e = new X509EncodedKeySpec(encodedKey);
+				return KeyFactory.getInstance(KEY_PAIR_ALGO).generatePublic(e);
+			}
+		};
 		assertEquals(digest.getDigestLength(), UniqueId.LENGTH);
+		MessageEncoder messageEncoder = new MessageEncoderImpl(sig, digest, wf);
+		message = messageEncoder.encodeMessage(MessageId.NONE, sub, nick,
+				keyPair, messageBody.getBytes("UTF-8"));
 	}
 
 	@Before
@@ -108,7 +120,7 @@ public class BundleReadWriteTest extends TestCase {
 		w.close();
 
 		assertTrue(bundle.exists());
-		assertTrue(bundle.length() > messageBody.length);
+		assertTrue(bundle.length() > message.getSize());
 	}
 
 	@Test
@@ -116,13 +128,8 @@ public class BundleReadWriteTest extends TestCase {
 
 		testWriteBundle();
 
-		MessageParser messageParser = new MessageParser() {
-			public Message parseMessage(byte[] body) throws FormatException,
-			SignatureException {
-				// FIXME: Really parse the message
-				return message;
-			}
-		};
+		MessageParser messageParser =
+			new MessageParserImpl(keyParser, sig, digest, rf);
 		Provider<HeaderBuilder> headerBuilderProvider =
 			new Provider<HeaderBuilder>() {
 			public HeaderBuilder get() {
@@ -146,7 +153,16 @@ public class BundleReadWriteTest extends TestCase {
 		assertEquals(subs, h.getSubscriptions());
 		assertEquals(transports, h.getTransports());
 		Batch b = r.getNextBatch();
-		assertEquals(Collections.singletonList(message), b.getMessages());
+		Iterator<Message> i = b.getMessages().iterator();
+		assertTrue(i.hasNext());
+		Message m = i.next();
+		assertEquals(message.getId(), m.getId());
+		assertEquals(message.getParent(), m.getParent());
+		assertEquals(message.getGroup(), m.getGroup());
+		assertEquals(message.getAuthor(), m.getAuthor());
+		assertEquals(message.getTimestamp(), m.getTimestamp());
+		assertTrue(Arrays.equals(message.getBytes(), m.getBytes()));
+		assertFalse(i.hasNext());
 		assertNull(r.getNextBatch());
 		r.close();
 	}
@@ -158,19 +174,14 @@ public class BundleReadWriteTest extends TestCase {
 		testWriteBundle();
 
 		RandomAccessFile f = new RandomAccessFile(bundle, "rw");
-		f.seek(bundle.length() - 50);
+		f.seek(bundle.length() - 150);
 		byte b = f.readByte();
-		f.seek(bundle.length() - 50);
+		f.seek(bundle.length() - 150);
 		f.writeByte(b + 1);
 		f.close();
 
-		MessageParser messageParser = new MessageParser() {
-			public Message parseMessage(byte[] body) throws FormatException,
-			SignatureException {
-				// FIXME: Really parse the message
-				return message;
-			}
-		};
+		MessageParser messageParser =
+			new MessageParserImpl(keyParser, sig, digest, rf);
 		Provider<HeaderBuilder> headerBuilderProvider =
 			new Provider<HeaderBuilder>() {
 			public HeaderBuilder get() {