diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
index dc96bd47fb7e6bb51d1dfb480ff2b25a53df0e21..b3f562949b34658b1f690376dd7f83c0b091f9b6 100644
--- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
+++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java
@@ -19,6 +19,7 @@ import android.widget.Toast;
 import org.briarproject.R;
 import org.briarproject.android.BriarActivity;
 import org.briarproject.android.util.BriarRecyclerView;
+import org.briarproject.api.FormatException;
 import org.briarproject.api.android.AndroidNotificationManager;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
@@ -47,8 +48,6 @@ import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
-import java.io.IOException;
-import java.security.GeneralSecurityException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -430,9 +429,7 @@ public class ConversationActivity extends BriarActivity
 				try {
 					storeMessage(privateMessageFactory.createPrivateMessage(
 							groupId, timestamp, null, "text/plain", body));
-				} catch (GeneralSecurityException e) {
-					throw new RuntimeException(e);
-				} catch (IOException e) {
+				} catch (FormatException e) {
 					throw new RuntimeException(e);
 				}
 			}
diff --git a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
index a38433f311e324066248a455671078095a0fec57..6bd289e3eeda0c9583f5c5ea3f2a9a16194e3465 100644
--- a/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/WriteForumPostActivity.java
@@ -23,6 +23,7 @@ import org.briarproject.android.identity.LocalAuthorItemComparator;
 import org.briarproject.android.identity.LocalAuthorSpinnerAdapter;
 import org.briarproject.android.util.CommonLayoutParams;
 import org.briarproject.android.util.LayoutUtils;
+import org.briarproject.api.FormatException;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.CryptoExecutor;
 import org.briarproject.api.crypto.KeyParser;
@@ -39,7 +40,6 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.Collection;
 import java.util.concurrent.Executor;
@@ -281,7 +281,7 @@ implements OnItemSelectedListener, OnClickListener {
 					}
 				} catch (GeneralSecurityException e) {
 					throw new RuntimeException(e);
-				} catch (IOException e) {
+				} catch (FormatException e) {
 					throw new RuntimeException(e);
 				}
 				storePost(p);
diff --git a/briar-api/src/org/briarproject/api/clients/ClientHelper.java b/briar-api/src/org/briarproject/api/clients/ClientHelper.java
index 04397bf6e5a6ebdf0ae8d0395620f1da2847b836..06b5c7c5cb3bfea4ec8d747e633c883e8cb1c7bc 100644
--- a/briar-api/src/org/briarproject/api/clients/ClientHelper.java
+++ b/briar-api/src/org/briarproject/api/clients/ClientHelper.java
@@ -5,6 +5,7 @@ import org.briarproject.api.data.BdfDictionary;
 import org.briarproject.api.data.BdfList;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -13,6 +14,13 @@ import java.util.Map;
 
 public interface ClientHelper {
 
+	void addLocalMessage(Message m, ClientId c, BdfDictionary metadata,
+			boolean shared) throws DbException, FormatException;
+
+	void addLocalMessage(Transaction txn, Message m, ClientId c,
+			BdfDictionary metadata, boolean shared) throws DbException,
+			FormatException;
+
 	Message createMessage(GroupId g, long timestamp, BdfDictionary body)
 			throws FormatException;
 
@@ -59,4 +67,13 @@ public interface ClientHelper {
 
 	void mergeMessageMetadata(Transaction txn, MessageId m,
 			BdfDictionary metadata) throws DbException, FormatException;
+
+	byte[] toByteArray(BdfDictionary dictionary) throws FormatException;
+
+	byte[] toByteArray(BdfList list) throws FormatException;
+
+	BdfDictionary toDictionary(byte[] b, int off, int len)
+			throws FormatException;
+
+	BdfList toList(byte[] b, int off, int len) throws FormatException;
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java b/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java
index 3bcfd7b7d0db546d0358b6660e58cf80fdb80138..e05edc8831b3538458c44175e8a005d59d56d878 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumPostFactory.java
@@ -1,21 +1,21 @@
 package org.briarproject.api.forum;
 
+import org.briarproject.api.FormatException;
 import org.briarproject.api.crypto.PrivateKey;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 public interface ForumPostFactory {
 
 	ForumPost createAnonymousPost(GroupId groupId, long timestamp,
 			MessageId parent, String contentType, byte[] body)
-			throws IOException, GeneralSecurityException;
+			throws FormatException;
 
 	ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
 			MessageId parent, Author author, String contentType, byte[] body,
-			PrivateKey privateKey) throws IOException,
+			PrivateKey privateKey) throws FormatException,
 			GeneralSecurityException;
 }
diff --git a/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java b/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java
index 72fded05b20215a0d16e617c2cb7cda26c1b9113..0c30b6e6125753c9906186f6608913e8238daf08 100644
--- a/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java
+++ b/briar-api/src/org/briarproject/api/messaging/PrivateMessageFactory.java
@@ -1,14 +1,12 @@
 package org.briarproject.api.messaging;
 
+import org.briarproject.api.FormatException;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-
 public interface PrivateMessageFactory {
 
 	PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
 			MessageId parent, String contentType, byte[] body)
-			throws IOException, GeneralSecurityException;
+			throws FormatException;
 }
diff --git a/briar-core/src/org/briarproject/clients/BdfMessageValidator.java b/briar-core/src/org/briarproject/clients/BdfMessageValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd170aa7c89d70e5e754a85924df59c593e5debe
--- /dev/null
+++ b/briar-core/src/org/briarproject/clients/BdfMessageValidator.java
@@ -0,0 +1,114 @@
+package org.briarproject.clients;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.data.BdfDictionary;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.data.MetadataEncoder;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.sync.Group;
+import org.briarproject.api.sync.Message;
+import org.briarproject.api.sync.MessageValidator;
+import org.briarproject.api.system.Clock;
+import org.briarproject.util.StringUtils;
+
+import java.util.logging.Logger;
+
+import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
+
+public abstract class BdfMessageValidator implements MessageValidator {
+
+	protected static final Logger LOG =
+			Logger.getLogger(BdfMessageValidator.class.getName());
+
+	protected final ClientHelper clientHelper;
+	protected final MetadataEncoder metadataEncoder;
+	protected final Clock clock;
+
+	protected BdfMessageValidator(ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		this.clientHelper = clientHelper;
+		this.metadataEncoder = metadataEncoder;
+		this.clock = clock;
+	}
+
+	protected abstract BdfDictionary validateMessage(BdfList message, Group g,
+			long timestamp) throws FormatException;
+
+	@Override
+	public Metadata validateMessage(Message m, Group g) {
+		// Reject the message if it's too far in the future
+		long now = clock.currentTimeMillis();
+		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
+			LOG.info("Timestamp is too far in the future");
+			return null;
+		}
+		byte[] raw = m.getRaw();
+		try {
+			BdfList message = clientHelper.toList(raw, MESSAGE_HEADER_LENGTH,
+					raw.length - MESSAGE_HEADER_LENGTH);
+			BdfDictionary meta = validateMessage(message, g, m.getTimestamp());
+			if (meta == null) {
+				LOG.info("Invalid message");
+				return null;
+			}
+			return metadataEncoder.encode(meta);
+		} catch (FormatException e) {
+			LOG.info("Invalid message");
+			return null;
+		}
+	}
+
+	protected void checkLength(String s, int minLength, int maxLength)
+			throws FormatException {
+		if (s != null) {
+			int length = StringUtils.toUtf8(s).length;
+			if (length < minLength) throw new FormatException();
+			if (length > maxLength) throw new FormatException();
+		}
+	}
+
+	protected void checkLength(String s, int length) throws FormatException {
+		if (s != null && StringUtils.toUtf8(s).length != length)
+			throw new FormatException();
+	}
+
+	protected void checkLength(byte[] b, int minLength, int maxLength)
+			throws FormatException {
+		if (b != null) {
+			if (b.length < minLength) throw new FormatException();
+			if (b.length > maxLength) throw new FormatException();
+		}
+	}
+
+	protected void checkLength(byte[] b, int length) throws FormatException {
+		if (b != null && b.length != length) throw new FormatException();
+	}
+
+	protected void checkSize(BdfList list, int minSize, int maxSize)
+		throws FormatException {
+		if (list != null) {
+			if (list.size() < minSize) throw new FormatException();
+			if (list.size() > maxSize) throw new FormatException();
+		}
+	}
+
+	protected void checkSize(BdfList list, int size) throws FormatException {
+		if (list != null && list.size() != size) throw new FormatException();
+	}
+
+	protected void checkSize(BdfDictionary dictionary, int minSize,
+			int maxSize) throws FormatException {
+		if (dictionary != null) {
+			if (dictionary.size() < minSize) throw new FormatException();
+			if (dictionary.size() > maxSize) throw new FormatException();
+		}
+	}
+
+	protected void checkSize(BdfDictionary dictionary, int size)
+			throws FormatException {
+		if (dictionary != null && dictionary.size() != size)
+			throw new FormatException();
+	}
+}
diff --git a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java
index 9826dc4342d06a937ed4c0d2e008ce4b5b6c2ef2..65740a391c13e21461a2251dbfd5fe648302dead 100644
--- a/briar-core/src/org/briarproject/clients/ClientHelperImpl.java
+++ b/briar-core/src/org/briarproject/clients/ClientHelperImpl.java
@@ -16,6 +16,7 @@ import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
+import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageFactory;
@@ -54,37 +55,34 @@ class ClientHelperImpl implements ClientHelper {
 	}
 
 	@Override
-	public Message createMessage(GroupId g, long timestamp, BdfDictionary body)
-			throws FormatException {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter writer = bdfWriterFactory.createWriter(out);
+	public void addLocalMessage(Message m, ClientId c, BdfDictionary metadata,
+			boolean shared) throws DbException, FormatException {
+		Transaction txn = db.startTransaction();
 		try {
-			writer.writeDictionary(body);
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
+			addLocalMessage(txn, m, c, metadata, shared);
+			txn.setComplete();
+		} finally {
+			db.endTransaction(txn);
 		}
-		byte[] raw = out.toByteArray();
-		return messageFactory.createMessage(g, timestamp, raw);
+	}
+
+	@Override
+	public void addLocalMessage(Transaction txn, Message m, ClientId c,
+			BdfDictionary metadata, boolean shared)
+			throws DbException, FormatException {
+		db.addLocalMessage(txn, m, c, metadataEncoder.encode(metadata), shared);
+	}
+
+	@Override
+	public Message createMessage(GroupId g, long timestamp, BdfDictionary body)
+			throws FormatException {
+		return messageFactory.createMessage(g, timestamp, toByteArray(body));
 	}
 
 	@Override
 	public Message createMessage(GroupId g, long timestamp, BdfList body)
 			throws FormatException {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter writer = bdfWriterFactory.createWriter(out);
-		try {
-			writer.writeList(body);
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
-		}
-		byte[] raw = out.toByteArray();
-		return messageFactory.createMessage(g, timestamp, raw);
+		return messageFactory.createMessage(g, timestamp, toByteArray(body));
 	}
 
 	@Override
@@ -106,20 +104,8 @@ class ClientHelperImpl implements ClientHelper {
 			throws DbException, FormatException {
 		byte[] raw = db.getRawMessage(txn, m);
 		if (raw == null) return null;
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader reader = bdfReaderFactory.createReader(in);
-		BdfDictionary dictionary;
-		try {
-			dictionary = reader.readDictionary();
-			if (!reader.eof()) throw new FormatException();
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
-		}
-		return dictionary;
+		return toDictionary(raw, MESSAGE_HEADER_LENGTH,
+				raw.length - MESSAGE_HEADER_LENGTH);
 	}
 
 	@Override
@@ -141,20 +127,8 @@ class ClientHelperImpl implements ClientHelper {
 			throws DbException, FormatException {
 		byte[] raw = db.getRawMessage(txn, m);
 		if (raw == null) return null;
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader reader = bdfReaderFactory.createReader(in);
-		BdfList list;
-		try {
-			list = reader.readList();
-			if (!reader.eof()) throw new FormatException();
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
-		}
-		return list;
+		return toList(raw, MESSAGE_HEADER_LENGTH,
+				raw.length - MESSAGE_HEADER_LENGTH);
 	}
 
 	@Override
@@ -259,4 +233,63 @@ class ClientHelperImpl implements ClientHelper {
 			BdfDictionary metadata) throws DbException, FormatException {
 		db.mergeMessageMetadata(txn, m, metadataEncoder.encode(metadata));
 	}
+
+	@Override
+	public byte[] toByteArray(BdfDictionary dictionary) throws FormatException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter writer = bdfWriterFactory.createWriter(out);
+		try {
+			writer.writeDictionary(dictionary);
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+		return out.toByteArray();
+	}
+
+	@Override
+	public byte[] toByteArray(BdfList list) throws FormatException {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter writer = bdfWriterFactory.createWriter(out);
+		try {
+			writer.writeList(list);
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+		return out.toByteArray();
+	}
+
+	@Override
+	public BdfDictionary toDictionary(byte[] b, int off, int len)
+			throws FormatException {
+		ByteArrayInputStream in = new ByteArrayInputStream(b, off, len);
+		BdfReader reader = bdfReaderFactory.createReader(in);
+		try {
+			BdfDictionary dictionary = reader.readDictionary();
+			if (!reader.eof()) throw new FormatException();
+			return dictionary;
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	@Override
+	public BdfList toList(byte[] b, int off, int len) throws FormatException {
+		ByteArrayInputStream in = new ByteArrayInputStream(b, off, len);
+		BdfReader reader = bdfReaderFactory.createReader(in);
+		try {
+			BdfList list = reader.readList();
+			if (!reader.eof()) throw new FormatException();
+			return list;
+		} catch (FormatException e) {
+			throw e;
+		} catch (IOException e) {
+			throw new RuntimeException(e);
+		}
+	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumListValidator.java b/briar-core/src/org/briarproject/forum/ForumListValidator.java
index 0a8e04e3226147a03757544112926878a2be37e1..e4b6e4edc6b3775e57ca3332c092b989f85c1735 100644
--- a/briar-core/src/org/briarproject/forum/ForumListValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumListValidator.java
@@ -1,69 +1,47 @@
 package org.briarproject.forum;
 
 import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageValidator;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.util.logging.Logger;
+import org.briarproject.api.system.Clock;
+import org.briarproject.clients.BdfMessageValidator;
 
 import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-
-class ForumListValidator implements MessageValidator {
-
-	private static final Logger LOG =
-			Logger.getLogger(ForumListValidator.class.getName());
 
-	private final BdfReaderFactory bdfReaderFactory;
-	private final MetadataEncoder metadataEncoder;
+class ForumListValidator extends BdfMessageValidator {
 
-	ForumListValidator(BdfReaderFactory bdfReaderFactory,
-			MetadataEncoder metadataEncoder) {
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.metadataEncoder = metadataEncoder;
+	ForumListValidator(ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
 	}
 
 	@Override
-	public Metadata validateMessage(Message m, Group g) {
-		try {
-			// Parse the message body
-			byte[] raw = m.getRaw();
-			ByteArrayInputStream in = new ByteArrayInputStream(raw,
-					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-			BdfReader r = bdfReaderFactory.createReader(in);
-			r.readListStart();
-			long version = r.readLong();
-			if (version < 0) throw new FormatException();
-			r.readListStart();
-			while (!r.hasListEnd()) {
-				r.readListStart();
-				String name = r.readString(MAX_FORUM_NAME_LENGTH);
-				if (name.length() == 0) throw new FormatException();
-				byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
-				if (salt.length != FORUM_SALT_LENGTH)
-					throw new FormatException();
-				r.readListEnd();
-			}
-			r.readListEnd();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			// Return the metadata
-			BdfDictionary d = new BdfDictionary();
-			d.put("version", version);
-			d.put("local", false);
-			return metadataEncoder.encode(d);
-		} catch (IOException e) {
-			LOG.info("Invalid forum list");
-			return null;
+	public BdfDictionary validateMessage(BdfList message, Group g,
+			long timestamp) throws FormatException {
+		// Version, forum list
+		checkSize(message, 2);
+		// Version
+		long version = message.getLong(0);
+		if (version < 0) throw new FormatException();
+		// Forum list
+		BdfList forumList = message.getList(1);
+		for (int i = 0; i < forumList.size(); i++) {
+			BdfList forum = forumList.getList(i);
+			// Name, salt
+			checkSize(forum, 2);
+			String name = forum.getString(0);
+			checkLength(name, 1, MAX_FORUM_NAME_LENGTH);
+			byte[] salt = forum.getRaw(1);
+			checkLength(salt, FORUM_SALT_LENGTH);
 		}
+		// Return the metadata
+		BdfDictionary meta = new BdfDictionary();
+		meta.put("version", version);
+		meta.put("local", false);
+		return meta;
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index 174f23147b6ab1c2e5a3ef287a2ead6a7a51392e..fe51b778b32eb7de93fd65a635c281674cfab2ac 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -3,15 +3,12 @@ package org.briarproject.forum;
 import com.google.inject.Inject;
 
 import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
@@ -26,8 +23,6 @@ import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -36,16 +31,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.logging.Logger;
 
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.forum.ForumConstants.FORUM_SALT_LENGTH;
-import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_NAME_LENGTH;
-import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
 import static org.briarproject.api.identity.Author.Status.ANONYMOUS;
 import static org.briarproject.api.identity.Author.Status.UNKNOWN;
 import static org.briarproject.api.identity.Author.Status.VERIFIED;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 
 class ForumManagerImpl implements ForumManager {
 
@@ -53,21 +42,13 @@ class ForumManagerImpl implements ForumManager {
 			"859a7be50dca035b64bd6902fb797097"
 					+ "795af837abbf8c16d750b3c2ccc186ea"));
 
-	private static final Logger LOG =
-			Logger.getLogger(ForumManagerImpl.class.getName());
-
 	private final DatabaseComponent db;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final MetadataEncoder metadataEncoder;
-	private final MetadataParser metadataParser;
+	private final ClientHelper clientHelper;
 
 	@Inject
-	ForumManagerImpl(DatabaseComponent db, BdfReaderFactory bdfReaderFactory,
-			MetadataEncoder metadataEncoder, MetadataParser metadataParser) {
+	ForumManagerImpl(DatabaseComponent db, ClientHelper clientHelper) {
 		this.db = db;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.metadataEncoder = metadataEncoder;
-		this.metadataParser = metadataParser;
+		this.clientHelper = clientHelper;
 	}
 
 	@Override
@@ -78,29 +59,21 @@ class ForumManagerImpl implements ForumManager {
 	@Override
 	public void addLocalPost(ForumPost p) throws DbException {
 		try {
-			BdfDictionary d = new BdfDictionary();
-			d.put("timestamp", p.getMessage().getTimestamp());
-			if (p.getParent() != null)
-				d.put("parent", p.getParent().getBytes());
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("timestamp", p.getMessage().getTimestamp());
+			if (p.getParent() != null) meta.put("parent", p.getParent());
 			if (p.getAuthor() != null) {
 				Author a = p.getAuthor();
-				BdfDictionary d1 = new BdfDictionary();
-				d1.put("id", a.getId().getBytes());
-				d1.put("name", a.getName());
-				d1.put("publicKey", a.getPublicKey());
-				d.put("author", d1);
-			}
-			d.put("contentType", p.getContentType());
-			d.put("local", true);
-			d.put("read", true);
-			Metadata meta = metadataEncoder.encode(d);
-			Transaction txn = db.startTransaction();
-			try {
-				db.addLocalMessage(txn, p.getMessage(), CLIENT_ID, meta, true);
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
+				BdfDictionary authorMeta = new BdfDictionary();
+				authorMeta.put("id", a.getId());
+				authorMeta.put("name", a.getName());
+				authorMeta.put("publicKey", a.getPublicKey());
+				meta.put("author", authorMeta);
 			}
+			meta.put("contentType", p.getContentType());
+			meta.put("local", true);
+			meta.put("read", true);
+			clientHelper.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
@@ -145,34 +118,11 @@ class ForumManagerImpl implements ForumManager {
 	@Override
 	public byte[] getPostBody(MessageId m) throws DbException {
 		try {
-			byte[] raw;
-			Transaction txn = db.startTransaction();
-			try {
-				raw = db.getRawMessage(txn, m);
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
-			}
-			ByteArrayInputStream in = new ByteArrayInputStream(raw,
-					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-			BdfReader r = bdfReaderFactory.createReader(in);
-			r.readListStart();
-			if (r.hasRaw()) r.skipRaw(); // Parent ID
-			else r.skipNull(); // No parent
-			if (r.hasList()) r.skipList(); // Author
-			else r.skipNull(); // No author
-			r.skipString(); // Content type
-			byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
-			if (r.hasRaw()) r.skipRaw(); // Signature
-			else r.skipNull();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return postBody;
+			// Parent ID, author, content type, forum post body, signature
+			BdfList message = clientHelper.getMessageAsList(m);
+			return message.getRaw(3);
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
 		}
 	}
 
@@ -181,7 +131,7 @@ class ForumManagerImpl implements ForumManager {
 			throws DbException {
 		Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
 		Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
-		Map<MessageId, Metadata> metadata;
+		Map<MessageId, BdfDictionary> metadata;
 		Transaction txn = db.startTransaction();
 		try {
 			// Load the IDs of the user's identities
@@ -191,20 +141,22 @@ class ForumManagerImpl implements ForumManager {
 			for (Contact c : db.getContacts(txn))
 				contactAuthorIds.add(c.getAuthor().getId());
 			// Load the metadata
-			metadata = db.getMessageMetadata(txn, g);
+			metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
 			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
 		// Parse the metadata
 		Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>();
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+		for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
 			try {
-				BdfDictionary d = metadataParser.parse(e.getValue());
-				long timestamp = d.getLong("timestamp");
+				BdfDictionary meta = entry.getValue();
+				long timestamp = meta.getLong("timestamp");
 				Author author = null;
 				Author.Status authorStatus = ANONYMOUS;
-				BdfDictionary d1 = d.getDictionary("author", null);
+				BdfDictionary d1 = meta.getDictionary("author", null);
 				if (d1 != null) {
 					AuthorId authorId = new AuthorId(d1.getRaw("id"));
 					String name = d1.getString("name");
@@ -216,13 +168,12 @@ class ForumManagerImpl implements ForumManager {
 						authorStatus = VERIFIED;
 					else authorStatus = UNKNOWN;
 				}
-				String contentType = d.getString("contentType");
-				boolean read = d.getBoolean("read");
-				headers.add(new ForumPostHeader(e.getKey(), timestamp, author,
-						authorStatus, contentType, read));
-			} catch (FormatException ex) {
-				if (LOG.isLoggable(WARNING))
-					LOG.log(WARNING, ex.toString(), ex);
+				String contentType = meta.getString("contentType");
+				boolean read = meta.getBoolean("read");
+				headers.add(new ForumPostHeader(entry.getKey(), timestamp,
+						author, authorStatus, contentType, read));
+			} catch (FormatException e) {
+				throw new DbException(e);
 			}
 		}
 		return headers;
@@ -231,36 +182,18 @@ class ForumManagerImpl implements ForumManager {
 	@Override
 	public void setReadFlag(MessageId m, boolean read) throws DbException {
 		try {
-			BdfDictionary d = new BdfDictionary();
-			d.put("read", read);
-			Metadata meta = metadataEncoder.encode(d);
-			Transaction txn = db.startTransaction();
-			try {
-				db.mergeMessageMetadata(txn, m, meta);
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
-			}
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("read", read);
+			clientHelper.mergeMessageMetadata(m, meta);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
 	}
 
 	private Forum parseForum(Group g) throws FormatException {
-		ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor());
-		BdfReader r = bdfReaderFactory.createReader(in);
-		try {
-			r.readListStart();
-			String name = r.readString(MAX_FORUM_NAME_LENGTH);
-			byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return new Forum(g, name, salt);
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
-		}
+		byte[] descriptor = g.getDescriptor();
+		// Name, salt
+		BdfList forum = clientHelper.toList(descriptor, 0, descriptor.length);
+		return new Forum(g, forum.getString(0), forum.getRaw(1));
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index caa066d344da3d9b8d4b60005c9af7e862bbc553..56c18546f86f56ec10e675d811f45fdee6803e30 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -3,16 +3,14 @@ package org.briarproject.forum;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.crypto.CryptoComponent;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.BdfWriterFactory;
 import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.data.ObjectReader;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostFactory;
 import org.briarproject.api.forum.ForumSharingManager;
-import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.sync.ValidationManager;
 import org.briarproject.api.system.Clock;
 
@@ -29,13 +27,10 @@ public class ForumModule extends AbstractModule {
 	@Provides @Singleton
 	ForumPostValidator getForumPostValidator(
 			ValidationManager validationManager, CryptoComponent crypto,
-			BdfReaderFactory bdfReaderFactory,
-			BdfWriterFactory bdfWriterFactory,
-			ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder,
-			Clock clock) {
+			AuthorFactory authorFactory, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
 		ForumPostValidator validator = new ForumPostValidator(crypto,
-				bdfReaderFactory, bdfWriterFactory, authorReader,
-				metadataEncoder, clock);
+				authorFactory, clientHelper, metadataEncoder, clock);
 		validationManager.registerMessageValidator(
 				ForumManagerImpl.CLIENT_ID, validator);
 		return validator;
@@ -43,11 +38,10 @@ public class ForumModule extends AbstractModule {
 
 	@Provides @Singleton
 	ForumListValidator getForumListValidator(
-			ValidationManager validationManager,
-			BdfReaderFactory bdfReaderFactory,
-			MetadataEncoder metadataEncoder) {
-		ForumListValidator validator = new ForumListValidator(bdfReaderFactory,
-				metadataEncoder);
+			ValidationManager validationManager, ClientHelper clientHelper,
+			MetadataEncoder metadataEncoder, Clock clock) {
+		ForumListValidator validator = new ForumListValidator(clientHelper,
+				metadataEncoder, clock);
 		validationManager.registerMessageValidator(
 				ForumSharingManagerImpl.CLIENT_ID, validator);
 		return validator;
diff --git a/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java b/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java
index c598dc1c3162d45483f79fb76aa66bcd5abcfa57..c3c2067f19170e3a5b41cd515deab31ad5d82b54 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostFactoryImpl.java
@@ -1,21 +1,19 @@
 package org.briarproject.forum;
 
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.PrivateKey;
 import org.briarproject.api.crypto.Signature;
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.forum.ForumPost;
 import org.briarproject.api.forum.ForumPostFactory;
 import org.briarproject.api.identity.Author;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
 
 import javax.inject.Inject;
@@ -26,46 +24,33 @@ import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENG
 class ForumPostFactoryImpl implements ForumPostFactory {
 
 	private final CryptoComponent crypto;
-	private final MessageFactory messageFactory;
-	private final BdfWriterFactory bdfWriterFactory;
+	private final ClientHelper clientHelper;
 
 	@Inject
-	ForumPostFactoryImpl(CryptoComponent crypto, MessageFactory messageFactory,
-			BdfWriterFactory bdfWriterFactory) {
+	ForumPostFactoryImpl(CryptoComponent crypto, ClientHelper clientHelper) {
 		this.crypto = crypto;
-		this.messageFactory = messageFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
+		this.clientHelper = clientHelper;
 	}
 
 	@Override
 	public ForumPost createAnonymousPost(GroupId groupId, long timestamp,
 			MessageId parent, String contentType, byte[] body)
-			throws IOException, GeneralSecurityException {
+			throws FormatException {
 		// Validate the arguments
 		if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
 			throw new IllegalArgumentException();
 		if (body.length > MAX_FORUM_POST_BODY_LENGTH)
 			throw new IllegalArgumentException();
-		// Serialise the message to a buffer
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		w.writeListStart();
-		if (parent == null) w.writeNull();
-		else w.writeRaw(parent.getBytes());
-		w.writeNull(); // No author
-		w.writeString(contentType);
-		w.writeRaw(body);
-		w.writeNull(); // No signature
-		w.writeListEnd();
-		Message m = messageFactory.createMessage(groupId, timestamp,
-				out.toByteArray());
+		// Serialise the message
+		BdfList message = BdfList.of(parent, null, contentType, body, null);
+		Message m = clientHelper.createMessage(groupId, timestamp, message);
 		return new ForumPost(m, parent, null, contentType);
 	}
 
 	@Override
 	public ForumPost createPseudonymousPost(GroupId groupId, long timestamp,
 			MessageId parent, Author author, String contentType, byte[] body,
-			PrivateKey privateKey) throws IOException,
+			PrivateKey privateKey) throws FormatException,
 			GeneralSecurityException {
 		// Validate the arguments
 		if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
@@ -73,42 +58,19 @@ class ForumPostFactoryImpl implements ForumPostFactory {
 		if (body.length > MAX_FORUM_POST_BODY_LENGTH)
 			throw new IllegalArgumentException();
 		// Serialise the data to be signed
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		w.writeListStart();
-		w.writeRaw(groupId.getBytes());
-		w.writeLong(timestamp);
-		if (parent == null) w.writeNull();
-		else w.writeRaw(parent.getBytes());
-		writeAuthor(w, author);
-		w.writeString(contentType);
-		w.writeRaw(body);
-		w.writeListEnd();
+		BdfList authorList = BdfList.of(author.getName(),
+				author.getPublicKey());
+		BdfList signed = BdfList.of(groupId, timestamp, parent, authorList,
+				contentType, body);
 		// Generate the signature
 		Signature signature = crypto.getSignature();
 		signature.initSign(privateKey);
-		signature.update(out.toByteArray());
+		signature.update(clientHelper.toByteArray(signed));
 		byte[] sig = signature.sign();
 		// Serialise the signed message
-		out.reset();
-		w = bdfWriterFactory.createWriter(out);
-		w.writeListStart();
-		if (parent == null) w.writeNull();
-		else w.writeRaw(parent.getBytes());
-		writeAuthor(w, author);
-		w.writeString(contentType);
-		w.writeRaw(body);
-		w.writeRaw(sig);
-		w.writeListEnd();
-		Message m = messageFactory.createMessage(groupId, timestamp,
-				out.toByteArray());
+		BdfList message = BdfList.of(parent, authorList, contentType, body,
+				sig);
+		Message m = clientHelper.createMessage(groupId, timestamp, message);
 		return new ForumPost(m, parent, author, contentType);
 	}
-
-	private void writeAuthor(BdfWriter w, Author a) throws IOException {
-		w.writeListStart();
-		w.writeString(a.getName());
-		w.writeRaw(a.getPublicKey());
-		w.writeListEnd();
-	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
index 3fbcf2f08fcbc0820f1493b18ad5210ecf522fbd..8bc2734502bde6e0d654bca17a1356217b19611e 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
@@ -2,164 +2,114 @@ package org.briarproject.forum;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.UniqueId;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyParser;
 import org.briarproject.api.crypto.PublicKey;
 import org.briarproject.api.crypto.Signature;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.data.ObjectReader;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.identity.Author;
+import org.briarproject.api.identity.AuthorFactory;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.api.sync.MessageValidator;
 import org.briarproject.api.system.Clock;
+import org.briarproject.clients.BdfMessageValidator;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.security.GeneralSecurityException;
-import java.util.logging.Logger;
 
 import static org.briarproject.api.forum.ForumConstants.MAX_CONTENT_TYPE_LENGTH;
 import static org.briarproject.api.forum.ForumConstants.MAX_FORUM_POST_BODY_LENGTH;
+import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
+import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
 
-class ForumPostValidator implements MessageValidator {
-
-	private static final Logger LOG =
-			Logger.getLogger(ForumPostValidator.class.getName());
+class ForumPostValidator extends BdfMessageValidator {
 
 	private final CryptoComponent crypto;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final BdfWriterFactory bdfWriterFactory;
-	private final ObjectReader<Author> authorReader;
-	private final MetadataEncoder metadataEncoder;
-	private final Clock clock;
-	private final KeyParser keyParser;
+	private final AuthorFactory authorFactory;
 
-	ForumPostValidator(CryptoComponent crypto,
-			BdfReaderFactory bdfReaderFactory,
-			BdfWriterFactory bdfWriterFactory,
-			ObjectReader<Author> authorReader,
-			MetadataEncoder metadataEncoder, Clock clock) {
+	ForumPostValidator(CryptoComponent crypto, AuthorFactory authorFactory,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
+			Clock clock) {
+		super(clientHelper, metadataEncoder, clock);
 		this.crypto = crypto;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
-		this.authorReader = authorReader;
-		this.metadataEncoder = metadataEncoder;
-		this.clock = clock;
-		keyParser = crypto.getSignatureKeyParser();
+		this.authorFactory = authorFactory;
 	}
 
 	@Override
-	public Metadata validateMessage(Message m, Group g) {
-		// Reject the message if it's too far in the future
-		long now = clock.currentTimeMillis();
-		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
-			LOG.info("Timestamp is too far in the future");
+	protected BdfDictionary validateMessage(BdfList message, Group g,
+			long timestamp) throws FormatException {
+		// Parent ID, author, content type, forum post body, signature
+		checkSize(message, 5);
+		// Parent ID is optional
+		byte[] parent = message.getOptionalRaw(0);
+		checkLength(parent, UniqueId.LENGTH);
+		// Author is optional
+		Author author = null;
+		BdfList authorList = message.getOptionalList(1);
+		if (authorList != null) {
+			// Name, public key
+			checkSize(authorList, 2);
+			String name = authorList.getString(0);
+			checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
+			byte[] publicKey = authorList.getRaw(1);
+			checkLength(publicKey, 0, MAX_PUBLIC_KEY_LENGTH);
+			author = authorFactory.createAuthor(name, publicKey);
+		}
+		// Content type
+		String contentType = message.getString(2);
+		checkLength(contentType, 0, MAX_CONTENT_TYPE_LENGTH);
+		// Forum post body
+		byte[] body = message.getRaw(3);
+		checkLength(body, 0, MAX_FORUM_POST_BODY_LENGTH);
+		// Signature is optional
+		byte[] sig = message.getOptionalRaw(4);
+		checkLength(sig, 0, MAX_SIGNATURE_LENGTH);
+		// If there's an author there must be a signature and vice versa
+		if (author != null && sig == null) {
+			LOG.info("Author without signature");
 			return null;
 		}
-		try {
-			// Parse the message body
-			byte[] raw = m.getRaw();
-			ByteArrayInputStream in = new ByteArrayInputStream(raw,
-					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-			BdfReader r = bdfReaderFactory.createReader(in);
-			MessageId parent = null;
-			Author author = null;
-			byte[] sig = null;
-			r.readListStart();
-			// Read the parent ID, if any
-			if (r.hasRaw()) {
-				byte[] id = r.readRaw(UniqueId.LENGTH);
-				if (id.length < UniqueId.LENGTH) throw new FormatException();
-				parent = new MessageId(id);
-			} else {
-				r.readNull();
-			}
-			// Read the author, if any
-			if (r.hasList()) author = authorReader.readObject(r);
-			else r.readNull();
-			// Read the content type
-			String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
-			// Read the forum post body
-			byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
-
-			// Read the signature, if any
-			if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH);
-			else r.readNull();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			// If there's an author there must be a signature and vice versa
-			if (author != null && sig == null) {
-				LOG.info("Author without signature");
-				return null;
-			}
-			if (author == null && sig != null) {
-				LOG.info("Signature without author");
-				return null;
-			}
-			// Verify the signature, if any
-			if (author != null) {
+		if (author == null && sig != null) {
+			LOG.info("Signature without author");
+			return null;
+		}
+		// Verify the signature, if any
+		if (author != null) {
+			try {
 				// Parse the public key
+				KeyParser keyParser = crypto.getSignatureKeyParser();
 				PublicKey key = keyParser.parsePublicKey(author.getPublicKey());
 				// Serialise the data to be signed
-				ByteArrayOutputStream out = new ByteArrayOutputStream();
-				BdfWriter w = bdfWriterFactory.createWriter(out);
-				w.writeListStart();
-				w.writeRaw(m.getGroupId().getBytes());
-				w.writeLong(m.getTimestamp());
-				if (parent == null) w.writeNull();
-				else w.writeRaw(parent.getBytes());
-				writeAuthor(w, author);
-				w.writeString(contentType);
-				w.writeRaw(postBody);
-				w.writeListEnd();
+				BdfList signed = BdfList.of(g.getId(), timestamp, parent,
+						authorList, contentType, body);
 				// Verify the signature
 				Signature signature = crypto.getSignature();
 				signature.initVerify(key);
-				signature.update(out.toByteArray());
+				signature.update(clientHelper.toByteArray(signed));
 				if (!signature.verify(sig)) {
 					LOG.info("Invalid signature");
 					return null;
 				}
+			} catch (GeneralSecurityException e) {
+				LOG.info("Invalid public key");
+				return null;
 			}
-			// Return the metadata
-			BdfDictionary d = new BdfDictionary();
-			d.put("timestamp", m.getTimestamp());
-			if (parent != null) d.put("parent", parent.getBytes());
-			if (author != null) {
-				BdfDictionary d1 = new BdfDictionary();
-				d1.put("id", author.getId().getBytes());
-				d1.put("name", author.getName());
-				d1.put("publicKey", author.getPublicKey());
-				d.put("author", d1);
-			}
-			d.put("contentType", contentType);
-			d.put("read", false);
-			return metadataEncoder.encode(d);
-		} catch (IOException e) {
-			LOG.info("Invalid forum post");
-			return null;
-		} catch (GeneralSecurityException e) {
-			LOG.info("Invalid public key");
-			return null;
 		}
-	}
-
-	private void writeAuthor(BdfWriter w, Author a) throws IOException {
-		w.writeListStart();
-		w.writeString(a.getName());
-		w.writeRaw(a.getPublicKey());
-		w.writeListEnd();
+		// Return the metadata
+		BdfDictionary meta = new BdfDictionary();
+		meta.put("timestamp", timestamp);
+		if (parent != null) meta.put("parent", parent);
+		if (author != null) {
+			BdfDictionary authorMeta = new BdfDictionary();
+			authorMeta.put("id", author.getId());
+			authorMeta.put("name", author.getName());
+			authorMeta.put("publicKey", author.getPublicKey());
+			meta.put("author", authorMeta);
+		}
+		meta.put("contentType", contentType);
+		meta.put("read", false);
+		return meta;
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
index c80de0a21a450b42f34fb3bd74036f02756aadb5..834cb94ddb8c787baa1882126d68ae14a9c4d7d0 100644
--- a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
@@ -3,18 +3,14 @@ package org.briarproject.forum;
 import com.google.inject.Inject;
 
 import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.PrivateGroupFactory;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
-import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.Metadata;
@@ -27,14 +23,11 @@ import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.ValidationManager.ValidationHook;
 import org.briarproject.api.system.Clock;
 import org.briarproject.util.StringUtils;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.security.SecureRandom;
 import java.util.ArrayList;
@@ -61,33 +54,23 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 
 	private final DatabaseComponent db;
 	private final ForumManager forumManager;
+	private final ClientHelper clientHelper;
 	private final GroupFactory groupFactory;
 	private final PrivateGroupFactory privateGroupFactory;
-	private final MessageFactory messageFactory;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final BdfWriterFactory bdfWriterFactory;
-	private final MetadataEncoder metadataEncoder;
-	private final MetadataParser metadataParser;
 	private final SecureRandom random;
 	private final Clock clock;
 	private final Group localGroup;
 
 	@Inject
-	ForumSharingManagerImpl(DatabaseComponent db,
-			ForumManager forumManager, GroupFactory groupFactory,
-			PrivateGroupFactory privateGroupFactory,
-			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
-			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
-			MetadataParser metadataParser, SecureRandom random, Clock clock) {
+	ForumSharingManagerImpl(DatabaseComponent db, ForumManager forumManager,
+			ClientHelper clientHelper, GroupFactory groupFactory,
+			PrivateGroupFactory privateGroupFactory, SecureRandom random,
+			Clock clock) {
 		this.db = db;
 		this.forumManager = forumManager;
+		this.clientHelper = clientHelper;
 		this.groupFactory = groupFactory;
 		this.privateGroupFactory = privateGroupFactory;
-		this.messageFactory = messageFactory;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
-		this.metadataEncoder = metadataEncoder;
-		this.metadataParser = metadataParser;
 		this.random = random;
 		this.clock = clock;
 		localGroup = groupFactory.createGroup(CLIENT_ID,
@@ -103,9 +86,9 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 			db.addGroup(txn, g);
 			db.setVisibleToContact(txn, c.getId(), g.getId(), true);
 			// Attach the contact ID to the group
-			BdfDictionary d = new BdfDictionary();
-			d.put("contactId", c.getId().getInt());
-			db.mergeGroupMetadata(txn, g.getId(), metadataEncoder.encode(d));
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("contactId", c.getId().getInt());
+			clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
 			// Share any forums that are shared with all contacts
 			List<Forum> shared = getForumsSharedWithAllContacts(txn);
 			storeMessage(txn, g.getId(), shared, 0);
@@ -193,8 +176,9 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 					LatestUpdate latest = findLatest(txn, g.getId(), false);
 					if (latest != null) {
 						// Retrieve and parse the latest update
-						byte[] raw = db.getRawMessage(txn, latest.messageId);
-						for (Forum f : parseForumList(raw)) {
+						BdfList message = clientHelper.getMessageAsList(txn,
+								latest.messageId);
+						for (Forum f : parseForumList(message)) {
 							if (!subscribed.contains(f.getGroup()))
 								available.add(f);
 						}
@@ -321,94 +305,64 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		LatestUpdate latest = findLatest(txn, localGroup.getId(), true);
 		if (latest == null) return Collections.emptyList();
 		// Retrieve and parse the latest update
-		return parseForumList(db.getRawMessage(txn, latest.messageId));
+		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
+		return parseForumList(message);
 	}
 
 	private LatestUpdate findLatest(Transaction txn, GroupId g, boolean local)
 			throws DbException, FormatException {
 		LatestUpdate latest = null;
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			BdfDictionary d = metadataParser.parse(e.getValue());
-			if (d.getBoolean("local") != local) continue;
-			long version = d.getLong("version");
+		Map<MessageId, BdfDictionary> metadata =
+				clientHelper.getMessageMetadataAsDictionary(txn, g);
+		for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
+			BdfDictionary meta = e.getValue();
+			if (meta.getBoolean("local") != local) continue;
+			long version = meta.getLong("version");
 			if (latest == null || version > latest.version)
 				latest = new LatestUpdate(e.getKey(), version);
 		}
 		return latest;
 	}
 
-	private List<Forum> parseForumList(byte[] raw) throws FormatException {
-		List<Forum> forums = new ArrayList<Forum>();
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
-		try {
-			r.readListStart();
-			r.skipLong(); // Version
-			r.readListStart();
-			while (!r.hasListEnd()) {
-				r.readListStart();
-				String name = r.readString(MAX_FORUM_NAME_LENGTH);
-				byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
-				r.readListEnd();
-				forums.add(createForum(name, salt));
-			}
-			r.readListEnd();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return forums;
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
+	private List<Forum> parseForumList(BdfList message) throws FormatException {
+		// Version, forum list
+		BdfList forumList = message.getList(1);
+		List<Forum> forums = new ArrayList<Forum>(forumList.size());
+		for (int i = 0; i < forumList.size(); i++) {
+			// Name, salt
+			BdfList forum = forumList.getList(i);
+			forums.add(createForum(forum.getString(0), forum.getRaw(1)));
 		}
+		return forums;
 	}
 
 	private void storeMessage(Transaction txn, GroupId g, List<Forum> forums,
 			long version) throws DbException {
 		try {
-			byte[] body = encodeForumList(forums, version);
+			BdfList body = encodeForumList(forums, version);
 			long now = clock.currentTimeMillis();
-			Message m = messageFactory.createMessage(g, now, body);
-			BdfDictionary d = new BdfDictionary();
-			d.put("version", version);
-			d.put("local", true);
-			Metadata meta = metadataEncoder.encode(d);
-			db.addLocalMessage(txn, m, CLIENT_ID, meta, true);
+			Message m = clientHelper.createMessage(g, now, body);
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("version", version);
+			meta.put("local", true);
+			clientHelper.addLocalMessage(txn, m, CLIENT_ID, meta, true);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
 	}
 
-	private byte[] encodeForumList(List<Forum> forums, long version) {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		try {
-			w.writeListStart();
-			w.writeLong(version);
-			w.writeListStart();
-			for (Forum f : forums) {
-				w.writeListStart();
-				w.writeString(f.getName());
-				w.writeRaw(f.getSalt());
-				w.writeListEnd();
-			}
-			w.writeListEnd();
-			w.writeListEnd();
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
-		}
-		return out.toByteArray();
+	private BdfList encodeForumList(List<Forum> forums, long version) {
+		BdfList forumList = new BdfList();
+		for (Forum f : forums)
+			forumList.add(BdfList.of(f.getName(), f.getSalt()));
+		return BdfList.of(version, forumList);
 	}
 
 	private ContactId getContactId(Transaction txn, GroupId contactGroupId)
 			throws DbException, FormatException {
-		Metadata meta = db.getGroupMetadata(txn, contactGroupId);
-		BdfDictionary d = metadataParser.parse(meta);
-		return new ContactId(d.getLong("contactId").intValue());
+		BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
+				contactGroupId);
+		return new ContactId(meta.getLong("contactId").intValue());
 	}
 
 	private Set<GroupId> getVisibleForums(Transaction txn,
@@ -418,9 +372,13 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 		// If there's no local update, no forums are visible
 		if (local == null) return Collections.emptySet();
 		// Intersect the sets of shared forums
-		byte[] localRaw = db.getRawMessage(txn, local.messageId);
-		Set<Forum> shared = new HashSet<Forum>(parseForumList(localRaw));
-		shared.retainAll(parseForumList(remoteUpdate.getRaw()));
+		BdfList localMessage = clientHelper.getMessageAsList(txn,
+				local.messageId);
+		Set<Forum> shared = new HashSet<Forum>(parseForumList(localMessage));
+		byte[] raw = remoteUpdate.getRaw();
+		BdfList remoteMessage = clientHelper.toList(raw, MESSAGE_HEADER_LENGTH,
+				raw.length - MESSAGE_HEADER_LENGTH);
+		shared.retainAll(parseForumList(remoteMessage));
 		// Forums in the intersection should be visible
 		Set<GroupId> visible = new HashSet<GroupId>(shared.size());
 		for (Forum f : shared) visible.add(f.getId());
@@ -440,46 +398,30 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 	}
 
 	private Forum createForum(String name, byte[] salt) {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
 		try {
-			w.writeListStart();
-			w.writeString(name);
-			w.writeRaw(salt);
-			w.writeListEnd();
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
+			BdfList forum = BdfList.of(name, salt);
+			byte[] descriptor = clientHelper.toByteArray(forum);
+			Group g = groupFactory.createGroup(forumManager.getClientId(),
+					descriptor);
+			return new Forum(g, name, salt);
+		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
-		Group g = groupFactory.createGroup(forumManager.getClientId(),
-				out.toByteArray());
-		return new Forum(g, name, salt);
 	}
 
 	private Forum parseForum(Group g) throws FormatException {
-		ByteArrayInputStream in = new ByteArrayInputStream(g.getDescriptor());
-		BdfReader r = bdfReaderFactory.createReader(in);
-		try {
-			r.readListStart();
-			String name = r.readString(MAX_FORUM_NAME_LENGTH);
-			byte[] salt = r.readRaw(FORUM_SALT_LENGTH);
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return new Forum(g, name, salt);
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
-		}
+		byte[] descriptor = g.getDescriptor();
+		// Name, salt
+		BdfList forum = clientHelper.toList(descriptor, 0, descriptor.length);
+		return new Forum(g, forum.getString(0), forum.getRaw(1));
 	}
 
 	private boolean listContains(Transaction txn, GroupId g, GroupId forum,
 			boolean local) throws DbException, FormatException {
 		LatestUpdate latest = findLatest(txn, g, local);
 		if (latest == null) return false;
-		byte[] raw = db.getRawMessage(txn, latest.messageId);
-		List<Forum> list = parseForumList(raw);
+		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
+		List<Forum> list = parseForumList(message);
 		for (Forum f : list) if (f.getId().equals(forum)) return true;
 		return false;
 	}
@@ -491,8 +433,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 			storeMessage(txn, g, Collections.singletonList(f), 0);
 			return true;
 		}
-		byte[] raw = db.getRawMessage(txn, latest.messageId);
-		List<Forum> list = parseForumList(raw);
+		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
+		List<Forum> list = parseForumList(message);
 		if (list.contains(f)) return false;
 		list.add(f);
 		storeMessage(txn, g, list, latest.version + 1);
@@ -503,8 +445,8 @@ class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
 			throws DbException, FormatException {
 		LatestUpdate latest = findLatest(txn, g, true);
 		if (latest == null) return;
-		byte[] raw = db.getRawMessage(txn, latest.messageId);
-		List<Forum> list = parseForumList(raw);
+		BdfList message = clientHelper.getMessageAsList(txn, latest.messageId);
+		List<Forum> list = parseForumList(message);
 		if (list.remove(f)) storeMessage(txn, g, list, latest.version + 1);
 	}
 
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 64a0a6c4346f50f3a75187b6da37c644dfdbf995..928b793bf35be07d3275394e6920df4885d3d827 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -3,19 +3,16 @@ package org.briarproject.messaging;
 import com.google.inject.Inject;
 
 import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.PrivateGroupFactory;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessage;
@@ -27,16 +24,9 @@ import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageStatus;
 import org.briarproject.util.StringUtils;
 
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
-import java.util.logging.Logger;
-
-import static java.util.logging.Level.WARNING;
-import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
 
 class MessagingManagerImpl implements MessagingManager, AddContactHook,
 		RemoveContactHook {
@@ -45,25 +35,16 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			"6bcdc006c0910b0f44e40644c3b31f1a"
 					+ "8bf9a6d6021d40d219c86b731b903070"));
 
-	private static final Logger LOG =
-			Logger.getLogger(MessagingManagerImpl.class.getName());
-
 	private final DatabaseComponent db;
+	private final ClientHelper clientHelper;
 	private final PrivateGroupFactory privateGroupFactory;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final MetadataEncoder metadataEncoder;
-	private final MetadataParser metadataParser;
 
 	@Inject
-	MessagingManagerImpl(DatabaseComponent db,
-			PrivateGroupFactory privateGroupFactory,
-			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
-			MetadataParser metadataParser) {
+	MessagingManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
+			PrivateGroupFactory privateGroupFactory) {
 		this.db = db;
+		this.clientHelper = clientHelper;
 		this.privateGroupFactory = privateGroupFactory;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.metadataEncoder = metadataEncoder;
-		this.metadataParser = metadataParser;
 	}
 
 	@Override
@@ -77,7 +58,7 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			// Attach the contact ID to the group
 			BdfDictionary d = new BdfDictionary();
 			d.put("contactId", c.getId().getInt());
-			db.mergeGroupMetadata(txn, g.getId(), metadataEncoder.encode(d));
+			clientHelper.mergeGroupMetadata(txn, g.getId(), d);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
@@ -100,21 +81,13 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	@Override
 	public void addLocalMessage(PrivateMessage m) throws DbException {
 		try {
-			BdfDictionary d = new BdfDictionary();
-			d.put("timestamp", m.getMessage().getTimestamp());
-			if (m.getParent() != null)
-				d.put("parent", m.getParent().getBytes());
-			d.put("contentType", m.getContentType());
-			d.put("local", true);
-			d.put("read", true);
-			Metadata meta = metadataEncoder.encode(d);
-			Transaction txn = db.startTransaction();
-			try {
-				db.addLocalMessage(txn, m.getMessage(), CLIENT_ID, meta, true);
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
-			}
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("timestamp", m.getMessage().getTimestamp());
+			if (m.getParent() != null) meta.put("parent", m.getParent());
+			meta.put("contentType", m.getContentType());
+			meta.put("local", true);
+			meta.put("read", true);
+			clientHelper.addLocalMessage(m.getMessage(), CLIENT_ID, meta, true);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
@@ -123,16 +96,8 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	@Override
 	public ContactId getContactId(GroupId g) throws DbException {
 		try {
-			Metadata meta;
-			Transaction txn = db.startTransaction();
-			try {
-				meta = db.getGroupMetadata(txn, g);
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
-			}
-			BdfDictionary d = metadataParser.parse(meta);
-			return new ContactId(d.getLong("contactId").intValue());
+			BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(g);
+			return new ContactId(meta.getLong("contactId").intValue());
 		} catch (FormatException e) {
 			throw new DbException(e);
 		}
@@ -154,14 +119,16 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	@Override
 	public Collection<PrivateMessageHeader> getMessageHeaders(ContactId c)
 			throws DbException {
-		Map<MessageId, Metadata> metadata;
+		Map<MessageId, BdfDictionary> metadata;
 		Collection<MessageStatus> statuses;
 		Transaction txn = db.startTransaction();
 		try {
 			GroupId g = getContactGroup(db.getContact(txn, c)).getId();
-			metadata = db.getMessageMetadata(txn, g);
+			metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
 			statuses = db.getMessageStatus(txn, c, g);
 			txn.setComplete();
+		} catch (FormatException e) {
+			throw new DbException(e);
 		} finally {
 			db.endTransaction(txn);
 		}
@@ -169,18 +136,17 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 				new ArrayList<PrivateMessageHeader>();
 		for (MessageStatus s : statuses) {
 			MessageId id = s.getMessageId();
-			Metadata m = metadata.get(id);
-			if (m == null) continue;
+			BdfDictionary meta = metadata.get(id);
+			if (meta == null) continue;
 			try {
-				BdfDictionary d = metadataParser.parse(m);
-				long timestamp = d.getLong("timestamp");
-				String contentType = d.getString("contentType");
-				boolean local = d.getBoolean("local");
-				boolean read = d.getBoolean("read");
+				long timestamp = meta.getLong("timestamp");
+				String contentType = meta.getString("contentType");
+				boolean local = meta.getBoolean("local");
+				boolean read = meta.getBoolean("read");
 				headers.add(new PrivateMessageHeader(id, timestamp, contentType,
 						local, read, s.isSent(), s.isSeen()));
 			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+				throw new DbException(e);
 			}
 		}
 		return headers;
@@ -188,47 +154,21 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 
 	@Override
 	public byte[] getMessageBody(MessageId m) throws DbException {
-		byte[] raw;
-		Transaction txn = db.startTransaction();
 		try {
-			raw = db.getRawMessage(txn, m);
-			txn.setComplete();
-		} finally {
-			db.endTransaction(txn);
-		}
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
-		try {
-			r.readListStart();
-			if (r.hasRaw()) r.skipRaw(); // Parent ID
-			else r.skipNull(); // No parent
-			r.skipString(); // Content type
-			byte[] messageBody = r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return messageBody;
+			// Parent ID, content type, private message body
+			BdfList message = clientHelper.getMessageAsList(m);
+			return message.getRaw(2);
 		} catch (FormatException e) {
 			throw new DbException(e);
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
 		}
 	}
 
 	@Override
 	public void setReadFlag(MessageId m, boolean read) throws DbException {
 		try {
-			BdfDictionary d = new BdfDictionary();
-			d.put("read", read);
-			Metadata meta = metadataEncoder.encode(d);
-			Transaction txn = db.startTransaction();
-			try {
-				db.mergeMessageMetadata(txn, m, meta);
-				txn.setComplete();
-			} finally {
-				db.endTransaction(txn);
-			}
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("read", read);
+			clientHelper.mergeMessageMetadata(m, meta);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
diff --git a/briar-core/src/org/briarproject/messaging/MessagingModule.java b/briar-core/src/org/briarproject/messaging/MessagingModule.java
index fe531f715e84e9d51561c85ed7742c918fba0e35..4ea10f9078b84d8aae170d86422eefd377abc4f1 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingModule.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingModule.java
@@ -3,8 +3,8 @@ package org.briarproject.messaging;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.ContactManager;
-import org.briarproject.api.data.BdfReaderFactory;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.messaging.MessagingManager;
 import org.briarproject.api.messaging.PrivateMessageFactory;
@@ -24,10 +24,10 @@ public class MessagingModule extends AbstractModule {
 
 	@Provides @Singleton
 	PrivateMessageValidator getValidator(ValidationManager validationManager,
-			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
 			Clock clock) {
 		PrivateMessageValidator validator = new PrivateMessageValidator(
-				bdfReaderFactory, metadataEncoder, clock);
+				clientHelper, metadataEncoder, clock);
 		validationManager.registerMessageValidator(CLIENT_ID, validator);
 		return validator;
 	}
diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java b/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java
index da8112c95f8b7cb9f02fc394504cc3124e57850e..98349bcbba290e08b4ef4c0e14c0f2b63f18cd3d 100644
--- a/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java
+++ b/briar-core/src/org/briarproject/messaging/PrivateMessageFactoryImpl.java
@@ -1,19 +1,15 @@
 package org.briarproject.messaging;
 
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
+import org.briarproject.api.FormatException;
+import org.briarproject.api.clients.ClientHelper;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.messaging.PrivateMessage;
 import org.briarproject.api.messaging.PrivateMessageFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.util.StringUtils;
 
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.security.GeneralSecurityException;
-
 import javax.inject.Inject;
 
 import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
@@ -21,36 +17,25 @@ import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESS
 
 class PrivateMessageFactoryImpl implements PrivateMessageFactory {
 
-	private final MessageFactory messageFactory;
-	private final BdfWriterFactory bdfWriterFactory;
+	private final ClientHelper clientHelper;
 
 	@Inject
-	PrivateMessageFactoryImpl(MessageFactory messageFactory,
-			BdfWriterFactory bdfWriterFactory) {
-		this.messageFactory = messageFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
+	PrivateMessageFactoryImpl(ClientHelper clientHelper) {
+		this.clientHelper = clientHelper;
 	}
 
 	@Override
 	public PrivateMessage createPrivateMessage(GroupId groupId, long timestamp,
 			MessageId parent, String contentType, byte[] body)
-			throws IOException, GeneralSecurityException {
+			throws FormatException {
 		// Validate the arguments
 		if (StringUtils.toUtf8(contentType).length > MAX_CONTENT_TYPE_LENGTH)
 			throw new IllegalArgumentException();
 		if (body.length > MAX_PRIVATE_MESSAGE_BODY_LENGTH)
 			throw new IllegalArgumentException();
 		// Serialise the message
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		w.writeListStart();
-		if (parent == null) w.writeNull();
-		else w.writeRaw(parent.getBytes());
-		w.writeString(contentType);
-		w.writeRaw(body);
-		w.writeListEnd();
-		Message m = messageFactory.createMessage(groupId, timestamp,
-				out.toByteArray());
+		BdfList message = BdfList.of(parent, contentType, body);
+		Message m = clientHelper.createMessage(groupId, timestamp, message);
 		return new PrivateMessage(m, parent, contentType);
 	}
 }
diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
index fa3e0ec5f3d9cd90c2d9cafa1b08089c7db290ca..0475da174b0d120de0c38f6628e1d2ec6f0fc10f 100644
--- a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
+++ b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
@@ -2,83 +2,45 @@ package org.briarproject.messaging;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.UniqueId;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageId;
-import org.briarproject.api.sync.MessageValidator;
 import org.briarproject.api.system.Clock;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.util.logging.Logger;
+import org.briarproject.clients.BdfMessageValidator;
 
 import static org.briarproject.api.messaging.MessagingConstants.MAX_CONTENT_TYPE_LENGTH;
 import static org.briarproject.api.messaging.MessagingConstants.MAX_PRIVATE_MESSAGE_BODY_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
-
-class PrivateMessageValidator implements MessageValidator {
-
-	private static final Logger LOG =
-			Logger.getLogger(PrivateMessageValidator.class.getName());
 
-	private final BdfReaderFactory bdfReaderFactory;
-	private final MetadataEncoder metadataEncoder;
-	private final Clock clock;
+class PrivateMessageValidator extends BdfMessageValidator {
 
-	PrivateMessageValidator(BdfReaderFactory bdfReaderFactory,
+	PrivateMessageValidator(ClientHelper clientHelper,
 			MetadataEncoder metadataEncoder, Clock clock) {
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.metadataEncoder = metadataEncoder;
-		this.clock = clock;
+		super(clientHelper, metadataEncoder, clock);
 	}
 
 	@Override
-	public Metadata validateMessage(Message m, Group g) {
-		// Reject the message if it's too far in the future
-		long now = clock.currentTimeMillis();
-		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
-			LOG.info("Timestamp is too far in the future");
-			return null;
-		}
-		try {
-			// Parse the message body
-			byte[] raw = m.getRaw();
-			ByteArrayInputStream in = new ByteArrayInputStream(raw,
-					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-			BdfReader r = bdfReaderFactory.createReader(in);
-			MessageId parent = null;
-			r.readListStart();
-			// Read the parent ID, if any
-			if (r.hasRaw()) {
-				byte[] id = r.readRaw(UniqueId.LENGTH);
-				if (id.length < UniqueId.LENGTH) throw new FormatException();
-				parent = new MessageId(id);
-			} else {
-				r.readNull();
-			}
-			// Read the content type
-			String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
-			// Read the private message body
-			r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			// Return the metadata
-			BdfDictionary d = new BdfDictionary();
-			d.put("timestamp", m.getTimestamp());
-			if (parent != null) d.put("parent", parent.getBytes());
-			d.put("contentType", contentType);
-			d.put("local", false);
-			d.put("read", false);
-			return metadataEncoder.encode(d);
-		} catch (IOException e) {
-			LOG.info("Invalid private message");
-			return null;
-		}
+	protected BdfDictionary validateMessage(BdfList message, Group g,
+			long timestamp) throws FormatException {
+		// Parent ID, content type, private message body
+		checkSize(message, 3);
+		// Parent ID is optional
+		byte[] parentId = message.getOptionalRaw(0);
+		checkLength(parentId, UniqueId.LENGTH);
+		// Content type
+		String contentType = message.getString(1);
+		checkLength(contentType, 0, MAX_CONTENT_TYPE_LENGTH);
+		// Private message body
+		byte[] body = message.getRaw(2);
+		checkLength(body, 0, MAX_PRIVATE_MESSAGE_BODY_LENGTH);
+		// Return the metadata
+		BdfDictionary meta = new BdfDictionary();
+		meta.put("timestamp", timestamp);
+		if (parentId != null) meta.put("parent", parentId);
+		meta.put("contentType", contentType);
+		meta.put("local", false);
+		meta.put("read", false);
+		return meta;
 	}
 }
diff --git a/briar-core/src/org/briarproject/properties/PropertiesModule.java b/briar-core/src/org/briarproject/properties/PropertiesModule.java
index 9edbf7e16a8e76edc7b18f46447f05d65eeb8642..cd40134deb03775a106d60987f3f560bae812ba7 100644
--- a/briar-core/src/org/briarproject/properties/PropertiesModule.java
+++ b/briar-core/src/org/briarproject/properties/PropertiesModule.java
@@ -3,8 +3,8 @@ package org.briarproject.properties;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.contact.ContactManager;
-import org.briarproject.api.data.BdfReaderFactory;
 import org.briarproject.api.data.MetadataEncoder;
 import org.briarproject.api.properties.TransportPropertyManager;
 import org.briarproject.api.sync.ValidationManager;
@@ -21,10 +21,10 @@ public class PropertiesModule extends AbstractModule {
 
 	@Provides @Singleton
 	TransportPropertyValidator getValidator(ValidationManager validationManager,
-			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
+			ClientHelper clientHelper, MetadataEncoder metadataEncoder,
 			Clock clock) {
 		TransportPropertyValidator validator = new TransportPropertyValidator(
-				bdfReaderFactory, metadataEncoder, clock);
+				clientHelper, metadataEncoder, clock);
 		validationManager.registerMessageValidator(CLIENT_ID, validator);
 		return validator;
 	}
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index 38400d1913e5db1a6579189285d01518b102d0ae..9c04d99fe9c2e670714c5a682f17ad379c632035 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -5,21 +5,16 @@ import com.google.inject.Inject;
 import org.briarproject.api.DeviceId;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.clients.PrivateGroupFactory;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
-import org.briarproject.api.data.BdfWriter;
-import org.briarproject.api.data.BdfWriterFactory;
-import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.data.MetadataParser;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.db.DatabaseComponent;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.db.Transaction;
 import org.briarproject.api.properties.TransportProperties;
@@ -29,22 +24,15 @@ import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageFactory;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.system.Clock;
 import org.briarproject.util.StringUtils;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 
-import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-
 class TransportPropertyManagerImpl implements TransportPropertyManager,
 		AddContactHook, RemoveContactHook {
 
@@ -55,28 +43,18 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
 
 	private final DatabaseComponent db;
+	private final ClientHelper clientHelper;
 	private final PrivateGroupFactory privateGroupFactory;
-	private final MessageFactory messageFactory;
-	private final BdfReaderFactory bdfReaderFactory;
-	private final BdfWriterFactory bdfWriterFactory;
-	private final MetadataEncoder metadataEncoder;
-	private final MetadataParser metadataParser;
 	private final Clock clock;
 	private final Group localGroup;
 
 	@Inject
 	TransportPropertyManagerImpl(DatabaseComponent db,
-			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
-			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
-			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
-			MetadataParser metadataParser, Clock clock) {
+			ClientHelper clientHelper, GroupFactory groupFactory,
+			PrivateGroupFactory privateGroupFactory, Clock clock) {
 		this.db = db;
+		this.clientHelper = clientHelper;
 		this.privateGroupFactory = privateGroupFactory;
-		this.messageFactory = messageFactory;
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
-		this.metadataEncoder = metadataEncoder;
-		this.metadataParser = metadataParser;
 		this.clock = clock;
 		localGroup = groupFactory.createGroup(CLIENT_ID,
 				LOCAL_GROUP_DESCRIPTOR);
@@ -145,8 +123,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 						true);
 				if (latest != null) {
 					// Retrieve and parse the latest local properties
-					byte[] raw = db.getRawMessage(txn, latest.messageId);
-					p = parseProperties(raw);
+					BdfList message = clientHelper.getMessageAsList(txn,
+							latest.messageId);
+					p = parseProperties(message);
 				}
 				txn.setComplete();
 			} finally {
@@ -175,8 +154,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 					LatestUpdate latest = findLatest(txn, g.getId(), t, false);
 					if (latest != null) {
 						// Retrieve and parse the latest remote properties
-						byte[] raw = db.getRawMessage(txn, latest.messageId);
-						remote.put(c.getId(), parseProperties(raw));
+						BdfList message = clientHelper.getMessageAsList(txn,
+								latest.messageId);
+						remote.put(c.getId(), parseProperties(message));
 					}
 				}
 				txn.setComplete();
@@ -206,8 +186,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 					merged = p;
 					changed = true;
 				} else {
-					byte[] raw = db.getRawMessage(txn, latest.messageId);
-					TransportProperties old = parseProperties(raw);
+					BdfList message = clientHelper.getMessageAsList(txn,
+							latest.messageId);
+					TransportProperties old = parseProperties(message);
 					merged = new TransportProperties(old);
 					merged.putAll(p);
 					changed = !merged.equals(old);
@@ -250,8 +231,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 					localGroup.getId(), true);
 			// Retrieve and parse the latest local properties
 			for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
-				byte[] raw = db.getRawMessage(txn, e.getValue().messageId);
-				local.put(e.getKey(), parseProperties(raw));
+				BdfList message = clientHelper.getMessageAsList(txn,
+						e.getValue().messageId);
+				local.put(e.getKey(), parseProperties(message));
 			}
 			return local;
 		} catch (NoSuchGroupException e) {
@@ -266,48 +248,35 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			TransportId t, TransportProperties p, long version, boolean local,
 			boolean shared) throws DbException {
 		try {
-			byte[] body = encodeProperties(dev, t, p, version);
+			BdfList body = encodeProperties(dev, t, p, version);
 			long now = clock.currentTimeMillis();
-			Message m = messageFactory.createMessage(g, now, body);
-			BdfDictionary d = new BdfDictionary();
-			d.put("transportId", t.getString());
-			d.put("version", version);
-			d.put("local", local);
-			Metadata meta = metadataEncoder.encode(d);
-			db.addLocalMessage(txn, m, CLIENT_ID, meta, shared);
+			Message m = clientHelper.createMessage(g, now, body);
+			BdfDictionary meta = new BdfDictionary();
+			meta.put("transportId", t.getString());
+			meta.put("version", version);
+			meta.put("local", local);
+			clientHelper.addLocalMessage(txn, m, CLIENT_ID, meta, shared);
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
 		}
 	}
 
-	private byte[] encodeProperties(DeviceId dev, TransportId t,
+	private BdfList encodeProperties(DeviceId dev, TransportId t,
 			TransportProperties p, long version) {
-		ByteArrayOutputStream out = new ByteArrayOutputStream();
-		BdfWriter w = bdfWriterFactory.createWriter(out);
-		try {
-			w.writeListStart();
-			w.writeRaw(dev.getBytes());
-			w.writeString(t.getString());
-			w.writeLong(version);
-			w.writeDictionary(p);
-			w.writeListEnd();
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
-		}
-		return out.toByteArray();
+		return BdfList.of(dev, t.getString(), version, p);
 	}
 
 	private Map<TransportId, LatestUpdate> findLatest(Transaction txn,
 			GroupId g, boolean local) throws DbException, FormatException {
 		Map<TransportId, LatestUpdate> latestUpdates =
 				new HashMap<TransportId, LatestUpdate>();
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			BdfDictionary d = metadataParser.parse(e.getValue());
-			if (d.getBoolean("local") == local) {
-				TransportId t = new TransportId(d.getString("transportId"));
-				long version = d.getLong("version");
+		Map<MessageId, BdfDictionary> metadata =
+				clientHelper.getMessageMetadataAsDictionary(txn, g);
+		for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
+			BdfDictionary meta = e.getValue();
+			if (meta.getBoolean("local") == local) {
+				TransportId t = new TransportId(meta.getString("transportId"));
+				long version = meta.getLong("version");
 				LatestUpdate latest = latestUpdates.get(t);
 				if (latest == null || version > latest.version)
 					latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
@@ -319,12 +288,13 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 	private LatestUpdate findLatest(Transaction txn, GroupId g, TransportId t,
 			boolean local) throws DbException, FormatException {
 		LatestUpdate latest = null;
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(txn, g);
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			BdfDictionary d = metadataParser.parse(e.getValue());
-			if (d.getString("transportId").equals(t.getString())
-					&& d.getBoolean("local") == local) {
-				long version = d.getLong("version");
+		Map<MessageId, BdfDictionary> metadata =
+				clientHelper.getMessageMetadataAsDictionary(txn, g);
+		for (Entry<MessageId, BdfDictionary> e : metadata.entrySet()) {
+			BdfDictionary meta = e.getValue();
+			if (meta.getString("transportId").equals(t.getString())
+					&& meta.getBoolean("local") == local) {
+				long version = meta.getLong("version");
 				if (latest == null || version > latest.version)
 					latest = new LatestUpdate(e.getKey(), version);
 			}
@@ -332,33 +302,14 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		return latest;
 	}
 
-	private TransportProperties parseProperties(byte[] raw)
+	private TransportProperties parseProperties(BdfList message)
 			throws FormatException {
+		// Device ID, transport ID, version, properties
+		BdfDictionary dictionary = message.getDictionary(3);
 		TransportProperties p = new TransportProperties();
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
-		try {
-			r.readListStart();
-			r.skipRaw(); // Device ID
-			r.skipString(); // Transport ID
-			r.skipLong(); // Version
-			r.readDictionaryStart();
-			while (!r.hasDictionaryEnd()) {
-				String key = r.readString(MAX_PROPERTY_LENGTH);
-				String value = r.readString(MAX_PROPERTY_LENGTH);
-				p.put(key, value);
-			}
-			r.readDictionaryEnd();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			return p;
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
-			throw new RuntimeException(e);
-		}
+		for (String key : dictionary.keySet())
+			p.put(key, dictionary.getString(key));
+		return p;
 	}
 
 	private static class LatestUpdate {
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
index 06973ae7d8986e7dacd4c50d65b14f66fe7110ff..e6fcd69a6c6c96c9eb87033f2c882962ba6ea99b 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
@@ -2,82 +2,52 @@ package org.briarproject.properties;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.UniqueId;
+import org.briarproject.api.clients.ClientHelper;
 import org.briarproject.api.data.BdfDictionary;
-import org.briarproject.api.data.BdfReader;
-import org.briarproject.api.data.BdfReaderFactory;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.MetadataEncoder;
-import org.briarproject.api.db.Metadata;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.Message;
-import org.briarproject.api.sync.MessageValidator;
 import org.briarproject.api.system.Clock;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.util.logging.Logger;
+import org.briarproject.clients.BdfMessageValidator;
 
 import static org.briarproject.api.TransportId.MAX_TRANSPORT_ID_LENGTH;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
 import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
-import static org.briarproject.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
-import static org.briarproject.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE;
-
-class TransportPropertyValidator implements MessageValidator {
 
-	private static final Logger LOG =
-			Logger.getLogger(TransportPropertyValidator.class.getName());
+class TransportPropertyValidator extends BdfMessageValidator {
 
-	private final BdfReaderFactory bdfReaderFactory;
-	private final MetadataEncoder metadataEncoder;
-	private final Clock clock;
-
-	TransportPropertyValidator(BdfReaderFactory bdfReaderFactory,
+	TransportPropertyValidator(ClientHelper clientHelper,
 			MetadataEncoder metadataEncoder, Clock clock) {
-		this.bdfReaderFactory = bdfReaderFactory;
-		this.metadataEncoder = metadataEncoder;
-		this.clock = clock;
+		super(clientHelper, metadataEncoder, clock);
 	}
 
 	@Override
-	public Metadata validateMessage(Message m, Group g) {
-		// Reject the message if it's too far in the future
-		long now = clock.currentTimeMillis();
-		if (m.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) {
-			LOG.info("Timestamp is too far in the future");
-			return null;
-		}
-		try {
-			// Parse the message body
-			byte[] raw = m.getRaw();
-			ByteArrayInputStream in = new ByteArrayInputStream(raw,
-					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-			BdfReader r = bdfReaderFactory.createReader(in);
-			r.readListStart();
-			byte[] deviceId = r.readRaw(UniqueId.LENGTH);
-			if (deviceId.length != UniqueId.LENGTH) throw new FormatException();
-			String transportId = r.readString(MAX_TRANSPORT_ID_LENGTH);
-			if (transportId.length() == 0) throw new FormatException();
-			long version = r.readLong();
-			if (version < 0) throw new FormatException();
-			r.readDictionaryStart();
-			for (int i = 0; !r.hasDictionaryEnd(); i++) {
-				if (i == MAX_PROPERTIES_PER_TRANSPORT)
-					throw new FormatException();
-				r.readString(MAX_PROPERTY_LENGTH);
-				r.readString(MAX_PROPERTY_LENGTH);
-			}
-			r.readDictionaryEnd();
-			r.readListEnd();
-			if (!r.eof()) throw new FormatException();
-			// Return the metadata
-			BdfDictionary d = new BdfDictionary();
-			d.put("transportId", transportId);
-			d.put("version", version);
-			d.put("local", false);
-			return metadataEncoder.encode(d);
-		} catch (IOException e) {
-			LOG.info("Invalid transport update");
-			return null;
+	protected BdfDictionary validateMessage(BdfList message, Group g,
+			long timestamp) throws FormatException {
+		// Device ID, transport ID, version, properties
+		checkSize(message, 4);
+		// Device ID
+		byte[] deviceId = message.getRaw(0);
+		checkLength(deviceId, UniqueId.LENGTH);
+		// Transport ID
+		String transportId = message.getString(1);
+		checkLength(transportId, 1, MAX_TRANSPORT_ID_LENGTH);
+		// Version
+		long version = message.getLong(2);
+		if (version < 0) throw new FormatException();
+		// Properties
+		BdfDictionary dictionary = message.getDictionary(3);
+		checkSize(dictionary, 0, MAX_PROPERTIES_PER_TRANSPORT);
+		for (String key : dictionary.keySet()) {
+			checkLength(key, 0, MAX_PROPERTY_LENGTH);
+			String value = dictionary.getString(key);
+			checkLength(value, 0, MAX_PROPERTY_LENGTH);
 		}
+		// Return the metadata
+		BdfDictionary meta = new BdfDictionary();
+		meta.put("transportId", transportId);
+		meta.put("version", version);
+		meta.put("local", false);
+		return meta;
 	}
 }