diff --git a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
index 90d52d1ba3e8b8b02f5df5d8b0f8305bfadbd919..339eeaaa09c2198c1bd32a9cfe51dbb904c47a65 100644
--- a/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/AvailableForumsActivity.java
@@ -17,11 +17,10 @@ import org.briarproject.api.db.NoSuchGroupException;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventBus;
 import org.briarproject.api.event.EventListener;
-import org.briarproject.api.event.GroupAddedEvent;
-import org.briarproject.api.event.GroupRemovedEvent;
+import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
-import org.briarproject.api.sync.GroupId;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.sync.ClientId;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -44,7 +43,7 @@ implements EventListener, OnItemClickListener {
 	private ListView list = null;
 
 	// Fields that are accessed from background threads must be volatile
-	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 	@Inject private volatile EventBus eventBus;
 
 	@Override
@@ -76,11 +75,10 @@ implements EventListener, OnItemClickListener {
 					Collection<ForumContacts> available =
 							new ArrayList<ForumContacts>();
 					long now = System.currentTimeMillis();
-					for (Forum f : forumManager.getAvailableForums()) {
+					for (Forum f : forumSharingManager.getAvailableForums()) {
 						try {
-							GroupId id = f.getId();
 							Collection<Contact> c =
-									forumManager.getSubscribers(id);
+									forumSharingManager.getSharedBy(f.getId());
 							available.add(new ForumContacts(f, c));
 						} catch (NoSuchGroupException e) {
 							// Continue
@@ -122,17 +120,12 @@ implements EventListener, OnItemClickListener {
 	}
 
 	public void eventOccurred(Event e) {
-		// TODO: What other events are needed here?
-		if (e instanceof GroupAddedEvent) {
-			GroupAddedEvent g = (GroupAddedEvent) e;
-			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
-				LOG.info("Forum added, reloading");
-				loadForums();
-			}
-		} else if (e instanceof GroupRemovedEvent) {
-			GroupRemovedEvent g = (GroupRemovedEvent) e;
-			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
-				LOG.info("Forum removed, reloading");
+		if (e instanceof MessageValidatedEvent) {
+			MessageValidatedEvent m = (MessageValidatedEvent) e;
+			ClientId c = m.getClientId();
+			if (m.isValid() && !m.isLocal()
+					&& c.equals(forumSharingManager.getClientId())) {
+				LOG.info("Available forums updated, reloading");
 				loadForums();
 			}
 		}
@@ -141,20 +134,20 @@ implements EventListener, OnItemClickListener {
 	public void onItemClick(AdapterView<?> parent, View view, int position,
 			long id) {
 		AvailableForumsItem item = adapter.getItem(position);
-		Collection<ContactId> visible = new ArrayList<ContactId>();
-		for (Contact c : item.getContacts()) visible.add(c.getId());
-		addSubscription(item.getForum(), visible);
+		Collection<ContactId> shared = new ArrayList<ContactId>();
+		for (Contact c : item.getContacts()) shared.add(c.getId());
+		subscribe(item.getForum(), shared);
 		String subscribed = getString(R.string.subscribed_toast);
 		Toast.makeText(this, subscribed, LENGTH_SHORT).show();
+		loadForums();
 	}
 
-	private void addSubscription(final Forum f,
-			final Collection<ContactId> visible) {
+	private void subscribe(final Forum f, final Collection<ContactId> shared) {
 		runOnDbThread(new Runnable() {
 			public void run() {
 				try {
-					forumManager.addForum(f);
-					forumManager.setVisibility(f.getId(), visible);
+					forumSharingManager.addForum(f);
+					forumSharingManager.setSharedWith(f.getId(), shared);
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
index 8ae263061408e2be827da4f176c3e0ef32de08a6..ca0b2ab13c8d3f0777937a2c293be8d2d5cf632e 100644
--- a/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/CreateForumActivity.java
@@ -18,7 +18,7 @@ import org.briarproject.android.BriarActivity;
 import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.forum.Forum;
-import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.util.StringUtils;
 
 import java.util.logging.Logger;
@@ -51,7 +51,7 @@ implements OnEditorActionListener, OnClickListener {
 	private TextView feedback = null;
 
 	// Fields that are accessed from background threads must be volatile
-	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 
 	@Override
 	public void onCreate(Bundle state) {
@@ -138,8 +138,8 @@ implements OnEditorActionListener, OnClickListener {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					Forum f = forumManager.createForum(name);
-					forumManager.addForum(f);
+					Forum f = forumSharingManager.createForum(name);
+					forumSharingManager.addForum(f);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Storing forum took " + duration + " ms");
diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
index 0b1606d76819ac6ef72692f32776a3ba7a0c60a4..c879e2bd0b10754d7a752f06f8d3ac8aa63b8509 100644
--- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
+++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java
@@ -25,6 +25,7 @@ import org.briarproject.android.util.LayoutUtils;
 import org.briarproject.android.util.ListLoadingProgressBar;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.db.NoSuchGroupException;
+import org.briarproject.api.event.ContactRemovedEvent;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.GroupAddedEvent;
 import org.briarproject.api.event.GroupRemovedEvent;
@@ -32,6 +33,7 @@ import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.forum.Forum;
 import org.briarproject.api.forum.ForumManager;
 import org.briarproject.api.forum.ForumPostHeader;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
 
@@ -81,8 +83,8 @@ public class ForumListFragment extends BaseEventFragment implements
 	private ImageButton newForumButton = null;
 
 	// Fields that are accessed from background threads must be volatile
-	@Inject
-	private volatile ForumManager forumManager;
+	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 
 	@Nullable
 	@Override
@@ -171,7 +173,8 @@ public class ForumListFragment extends BaseEventFragment implements
 							// Continue
 						}
 					}
-					int available = forumManager.getAvailableForums().size();
+					int available =
+							forumSharingManager.getAvailableForums().size();
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Full load took " + duration + " ms");
@@ -252,14 +255,9 @@ public class ForumListFragment extends BaseEventFragment implements
 	}
 
 	public void eventOccurred(Event e) {
-		// TODO: What other events are needed here?
-		if (e instanceof MessageValidatedEvent) {
-			MessageValidatedEvent m = (MessageValidatedEvent) e;
-			ClientId c = m.getClientId();
-			if (m.isValid() && c.equals(forumManager.getClientId())) {
-				LOG.info("Message added, reloading");
-				loadHeaders(m.getMessage().getGroupId());
-			}
+		if (e instanceof ContactRemovedEvent) {
+			LOG.info("Contact removed, reloading");
+			loadAvailable();
 		} else if (e instanceof GroupAddedEvent) {
 			GroupAddedEvent g = (GroupAddedEvent) e;
 			if (g.getGroup().getClientId().equals(forumManager.getClientId())) {
@@ -272,6 +270,18 @@ public class ForumListFragment extends BaseEventFragment implements
 				LOG.info("Forum removed, reloading");
 				loadHeaders();
 			}
+		} else if (e instanceof MessageValidatedEvent) {
+			MessageValidatedEvent m = (MessageValidatedEvent) e;
+			if (m.isValid()) {
+				ClientId c = m.getClientId();
+				if (c.equals(forumManager.getClientId())) {
+					LOG.info("Forum post added, reloading");
+					loadHeaders(m.getMessage().getGroupId());
+				} else if (c.equals(forumSharingManager.getClientId())) {
+					LOG.info("Available forums updated, reloading");
+					loadAvailable();
+				}
+			}
 		}
 	}
 
@@ -319,7 +329,8 @@ public class ForumListFragment extends BaseEventFragment implements
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					int available = forumManager.getAvailableForums().size();
+					int available =
+							forumSharingManager.getAvailableForums().size();
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Loading available took " + duration + " ms");
@@ -363,22 +374,22 @@ public class ForumListFragment extends BaseEventFragment implements
 			ContextMenuInfo info = menuItem.getMenuInfo();
 			int position = ((AdapterContextMenuInfo) info).position;
 			ForumListItem item = adapter.getItem(position);
-			removeSubscription(item.getForum());
+			unsubscribe(item.getForum());
 			String unsubscribed = getString(R.string.unsubscribed_toast);
 			Toast.makeText(getContext(), unsubscribed, LENGTH_SHORT).show();
 		}
 		return true;
 	}
 
-	private void removeSubscription(final Forum f) {
+	private void unsubscribe(final Forum f) {
 		listener.runOnDbThread(new Runnable() {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					forumManager.removeForum(f);
+					forumSharingManager.removeForum(f);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
-						LOG.info("Removing group took " + duration + " ms");
+						LOG.info("Removing forum took " + duration + " ms");
 				} catch (DbException e) {
 					if (LOG.isLoggable(WARNING))
 						LOG.log(WARNING, e.toString(), e);
diff --git a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
index 2b95559df059930f6c812a2a121f7e8ce7a7762b..2d237711938c893aa0e12040503e9f451e73620b 100644
--- a/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
+++ b/briar-android/src/org/briarproject/android/forum/ShareForumActivity.java
@@ -19,7 +19,7 @@ import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.db.DbException;
-import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
 import org.briarproject.api.sync.GroupId;
 
 import java.util.Collection;
@@ -52,7 +52,7 @@ SelectContactsDialog.Listener {
 
 	// Fields that are accessed from background threads must be volatile
 	@Inject private volatile ContactManager contactManager;
-	@Inject private volatile ForumManager forumManager;
+	@Inject private volatile ForumSharingManager forumSharingManager;
 	private volatile GroupId groupId = null;
 	private volatile Collection<Contact> contacts = null;
 	private volatile Collection<ContactId> selected = null;
@@ -139,7 +139,7 @@ SelectContactsDialog.Listener {
 				try {
 					long now = System.currentTimeMillis();
 					contacts = contactManager.getContacts();
-					selected = forumManager.getVisibility(groupId);
+					selected = forumSharingManager.getSharedWith(groupId);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Load took " + duration + " ms");
@@ -175,8 +175,9 @@ SelectContactsDialog.Listener {
 			public void run() {
 				try {
 					long now = System.currentTimeMillis();
-					forumManager.setVisibleToAll(groupId, all);
-					if (!all) forumManager.setVisibility(groupId, selected);
+					if (all) forumSharingManager.setSharedWithAll(groupId);
+					else forumSharingManager.setSharedWith(groupId,
+							selected);
 					long duration = System.currentTimeMillis() - now;
 					if (LOG.isLoggable(INFO))
 						LOG.info("Update took " + duration + " ms");
diff --git a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
index 2016396234633cbc8ccd114411aed646023a0286..7b6d598a4e4133809084a6b504012a96052a7d84 100644
--- a/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
+++ b/briar-android/src/org/briarproject/plugins/AndroidPluginsModule.java
@@ -1,27 +1,27 @@
 package org.briarproject.plugins;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.concurrent.Executor;
+import android.app.Application;
+import android.content.Context;
+
+import com.google.inject.Provides;
 
 import org.briarproject.api.android.AndroidExecutor;
-import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.event.EventBus;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
 import org.briarproject.api.plugins.duplex.DuplexPluginFactory;
 import org.briarproject.api.plugins.simplex.SimplexPluginConfig;
 import org.briarproject.api.plugins.simplex.SimplexPluginFactory;
 import org.briarproject.api.system.LocationUtils;
-import org.briarproject.api.event.EventBus;
 import org.briarproject.plugins.droidtooth.DroidtoothPluginFactory;
 import org.briarproject.plugins.tcp.AndroidLanTcpPluginFactory;
 import org.briarproject.plugins.tor.TorPluginFactory;
 
-import android.app.Application;
-import android.content.Context;
-
-import com.google.inject.Provides;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.concurrent.Executor;
 
 public class AndroidPluginsModule extends PluginsModule {
 
@@ -37,11 +37,11 @@ public class AndroidPluginsModule extends PluginsModule {
 	@Provides
 	DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor,
 			AndroidExecutor androidExecutor, Application app,
-			CryptoComponent crypto, LocationUtils locationUtils,
+			SecureRandom random, LocationUtils locationUtils,
 			EventBus eventBus) {
 		Context appContext = app.getApplicationContext();
 		DuplexPluginFactory bluetooth = new DroidtoothPluginFactory(ioExecutor,
-				androidExecutor, appContext, crypto.getSecureRandom());
+				androidExecutor, appContext, random);
 		DuplexPluginFactory tor = new TorPluginFactory(ioExecutor, appContext,
 				locationUtils, eventBus);
 		DuplexPluginFactory lan = new AndroidLanTcpPluginFactory(ioExecutor,
diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
index 3f01a4d40b45ea30262388b56dc41682a3f4b092..465a886bd7d4b2f8c8189d0bafdeb4068e215b9c 100644
--- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
+++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java
@@ -179,6 +179,9 @@ public interface DatabaseComponent {
 	void incrementStreamCounter(ContactId c, TransportId t, long rotationPeriod)
 			throws DbException;
 
+	/** Returns true if the given group is visible to the given contact. */
+	boolean isVisibleToContact(ContactId c, GroupId g) throws DbException;
+
 	/**
 	 * Merges the given metadata with the existing metadata for the given
 	 * group.
@@ -246,16 +249,14 @@ public interface DatabaseComponent {
 
 	/**
 	 * Makes a group visible to the given set of contacts and invisible to any
-	 * other current or future contacts.
+	 * other contacts.
 	 */
 	void setVisibility(GroupId g, Collection<ContactId> visible)
 			throws DbException;
 
-	/**
-	 * Makes a group visible to all current and future contacts, or invisible
-	 * to future contacts.
-	 */
-	void setVisibleToAll(GroupId g, boolean all) throws DbException;
+	/** Makes a group visible or invisible to a contact. */
+	void setVisibleToContact(ContactId c, GroupId g, boolean visible)
+			throws DbException;
 
 	/**
 	 * Stores the given transport keys, deleting any keys they have replaced.
diff --git a/briar-api/src/org/briarproject/api/forum/Forum.java b/briar-api/src/org/briarproject/api/forum/Forum.java
index 720744a9695976c2f847be5c769ac3499899bbfd..2eeadc21664f069e32db944305a5d0756ec05561 100644
--- a/briar-api/src/org/briarproject/api/forum/Forum.java
+++ b/briar-api/src/org/briarproject/api/forum/Forum.java
@@ -7,10 +7,12 @@ public class Forum {
 
 	private final Group group;
 	private final String name;
+	private final byte[] salt;
 
-	public Forum(Group group, String name) {
+	public Forum(Group group, String name, byte[] salt) {
 		this.group = group;
 		this.name = name;
+		this.salt = salt;
 	}
 
 	public GroupId getId() {
@@ -25,6 +27,10 @@ public class Forum {
 		return name;
 	}
 
+	public byte[] getSalt() {
+		return salt;
+	}
+
 	@Override
 	public int hashCode() {
 		return group.hashCode();
diff --git a/briar-api/src/org/briarproject/api/forum/ForumConstants.java b/briar-api/src/org/briarproject/api/forum/ForumConstants.java
index 4c66fd14295a6081a0c192b331aa7da5861a05cb..035a06cd1ef800f91b1789fa3ae3f51da0c5f457 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumConstants.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumConstants.java
@@ -1,17 +1,16 @@
 package org.briarproject.api.forum;
 
-import static org.briarproject.api.sync.SyncConstants.MAX_GROUP_DESCRIPTOR_LENGTH;
 import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
 
 public interface ForumConstants {
 
-	/** The maximum length of a forum's name in bytes. */
-	int MAX_FORUM_NAME_LENGTH = MAX_GROUP_DESCRIPTOR_LENGTH - 10;
+	/** The maximum length of a forum's name in UTF-8 bytes. */
+	int MAX_FORUM_NAME_LENGTH = 100;
 
 	/** The length of a forum's random salt in bytes. */
 	int FORUM_SALT_LENGTH = 32;
 
-	/** The maximum length of a forum post's content type in bytes. */
+	/** The maximum length of a forum post's content type in UTF-8 bytes. */
 	int MAX_CONTENT_TYPE_LENGTH = 50;
 
 	/** The maximum length of a forum post's body in bytes. */
diff --git a/briar-api/src/org/briarproject/api/forum/ForumManager.java b/briar-api/src/org/briarproject/api/forum/ForumManager.java
index 76464bac400574d8f1fe6fc3d063321d5887aaec..1d32544654c799eed46be9320f34749665d1889c 100644
--- a/briar-api/src/org/briarproject/api/forum/ForumManager.java
+++ b/briar-api/src/org/briarproject/api/forum/ForumManager.java
@@ -1,7 +1,5 @@
 package org.briarproject.api.forum;
 
-import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.db.DbException;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.GroupId;
@@ -14,18 +12,9 @@ public interface ForumManager {
 	/** Returns the unique ID of the forum client. */
 	ClientId getClientId();
 
-	/** Creates a forum with the given name. */
-	Forum createForum(String name);
-
-	/** Subscribes to a forum. */
-	void addForum(Forum f) throws DbException;
-
 	/** Stores a local forum post. */
 	void addLocalPost(ForumPost p) throws DbException;
 
-	/** Returns all forums to which the user could subscribe. */
-	Collection<Forum> getAvailableForums() throws DbException;
-
 	/** Returns the forum with the given ID. */
 	Forum getForum(GroupId g) throws DbException;
 
@@ -38,28 +27,6 @@ public interface ForumManager {
 	/** Returns the headers of all posts in the given forum. */
 	Collection<ForumPostHeader> getPostHeaders(GroupId g) throws DbException;
 
-	/** Returns all contacts who subscribe to the given forum. */
-	Collection<Contact> getSubscribers(GroupId g) throws DbException;
-
-	/** Returns the IDs of all contacts to which the given forum is visible. */
-	Collection<ContactId> getVisibility(GroupId g) throws DbException;
-
-	/** Unsubscribes from a forum. */
-	void removeForum(Forum f) throws DbException;
-
 	/** Marks a forum post as read or unread. */
 	void setReadFlag(MessageId m, boolean read) throws DbException;
-
-	/**
-	 * Makes a forum visible to the given set of contacts and invisible to any
-	 * other current or future contacts.
-	 */
-	void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException;
-
-	/**
-	 * Makes a forum visible to all current and future contacts, or invisible
-	 * to future contacts.
-	 */
-	void setVisibleToAll(GroupId g, boolean all) throws DbException;
 }
diff --git a/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..91b806e0e828d4d228c17e348ce830c5260fbc78
--- /dev/null
+++ b/briar-api/src/org/briarproject/api/forum/ForumSharingManager.java
@@ -0,0 +1,43 @@
+package org.briarproject.api.forum;
+
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.GroupId;
+
+import java.util.Collection;
+
+public interface ForumSharingManager {
+
+	/** Returns the unique ID of the forum sharing client. */
+	ClientId getClientId();
+
+	/** Creates a forum with the given name. */
+	Forum createForum(String name);
+
+	/** Subscribes to a forum. */
+	void addForum(Forum f) throws DbException;
+
+	/** Unsubscribes from a forum. */
+	void removeForum(Forum f) throws DbException;
+
+	/** Returns all forums to which the user could subscribe. */
+	Collection<Forum> getAvailableForums() throws DbException;
+
+	/** Returns all contacts who are sharing the given forum with the user. */
+	Collection<Contact> getSharedBy(GroupId g) throws DbException;
+
+	/** Returns the IDs of all contacts with whom the given forum is shared. */
+	Collection<ContactId> getSharedWith(GroupId g) throws DbException;
+
+	/**
+	 * Shares a forum with the given contacts and unshares it with any other
+	 * contacts.
+	 */
+	void setSharedWith(GroupId g, Collection<ContactId> shared)
+			throws DbException;
+
+	/** Shares a forum with all current and future contacts. */
+	void setSharedWithAll(GroupId g) throws DbException;
+}
diff --git a/briar-api/src/org/briarproject/api/sync/MessageValidator.java b/briar-api/src/org/briarproject/api/sync/MessageValidator.java
index d3d7ef520a2d0f29554f74dcb160417a28f86e19..58ee5dc2b96b980d0dea03a85a50951b0f23e4c5 100644
--- a/briar-api/src/org/briarproject/api/sync/MessageValidator.java
+++ b/briar-api/src/org/briarproject/api/sync/MessageValidator.java
@@ -8,5 +8,5 @@ public interface MessageValidator {
 	 * Validates the given message and returns its metadata if the message
 	 * is valid, or null if the message is invalid.
 	 */
-	Metadata validateMessage(Message m);
+	Metadata validateMessage(Message m, Group g);
 }
diff --git a/briar-core/src/org/briarproject/db/Database.java b/briar-core/src/org/briarproject/db/Database.java
index e5b65296d33857f32fe3eb02c6e25249e2a7a04e..a5a22b135cd93aeae3bcd274ee09192af0220e7c 100644
--- a/briar-core/src/org/briarproject/db/Database.java
+++ b/briar-core/src/org/briarproject/db/Database.java
@@ -598,13 +598,6 @@ interface Database<T> {
 	void setReorderingWindow(T txn, ContactId c, TransportId t,
 			long rotationPeriod, long base, byte[] bitmap) throws DbException;
 
-	/**
-	 * Makes a group visible or invisible to future contacts by default.
-	 * <p>
-	 * Locking: write.
-	 */
-	void setVisibleToAll(T txn, GroupId g, boolean all) throws DbException;
-
 	/**
 	 * Updates the transmission count and expiry time of the given message
 	 * with respect to the given contact, using the latency of the transport
diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
index 44c177dc8226ea6ff90eb2ee9c9c10e6f41abfb9..9e929d86c83865c158c4c58e3a4cab9f4b191109 100644
--- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
+++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java
@@ -223,6 +223,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		if (added) {
 			eventBus.broadcast(new MessageAddedEvent(m, null));
 			eventBus.broadcast(new MessageValidatedEvent(m, c, true, true));
+			if (shared) eventBus.broadcast(new MessageSharedEvent(m));
 		}
 	}
 
@@ -801,6 +802,28 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		}
 	}
 
+	public boolean isVisibleToContact(ContactId c, GroupId g)
+			throws DbException {
+		lock.readLock().lock();
+		try {
+			T txn = db.startTransaction();
+			try {
+				if (!db.containsContact(txn, c))
+					throw new NoSuchContactException();
+				if (!db.containsGroup(txn, g))
+					throw new NoSuchGroupException();
+				boolean visible = db.containsVisibleGroup(txn, c, g);
+				db.commitTransaction(txn);
+				return visible;
+			} catch (DbException e) {
+				db.abortTransaction(txn);
+				throw e;
+			}
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
 	public void mergeGroupMetadata(GroupId g, Metadata meta)
 			throws DbException {
 		lock.writeLock().lock();
@@ -900,7 +923,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				duplicate = db.containsMessage(txn, m.getId());
 				visible = db.containsVisibleGroup(txn, c, m.getGroupId());
 				if (visible) {
-					if (!duplicate) addMessage(txn, m, UNKNOWN, true, c);
+					if (!duplicate) addMessage(txn, m, UNKNOWN, false, c);
 					db.raiseAckFlag(txn, c, m.getId());
 				}
 				db.commitTransaction(txn);
@@ -1162,7 +1185,7 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 				if (!db.containsGroup(txn, g))
 					throw new NoSuchGroupException();
 				// Use HashSets for O(1) lookups, O(n) overall running time
-				HashSet<ContactId> now = new HashSet<ContactId>(visible);
+				Collection<ContactId> now = new HashSet<ContactId>(visible);
 				Collection<ContactId> before = db.getVisibility(txn, g);
 				before = new HashSet<ContactId>(before);
 				// Set the group's visibility for each current contact
@@ -1177,8 +1200,6 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 						affected.add(c);
 					}
 				}
-				// Make the group invisible to future contacts
-				db.setVisibleToAll(txn, g, false);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1191,27 +1212,20 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 			eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
 	}
 
-	public void setVisibleToAll(GroupId g, boolean all) throws DbException {
-		Collection<ContactId> affected = new ArrayList<ContactId>();
+	public void setVisibleToContact(ContactId c, GroupId g, boolean visible)
+			throws DbException {
+		boolean wasVisible = false;
 		lock.writeLock().lock();
 		try {
 			T txn = db.startTransaction();
 			try {
+				if (!db.containsContact(txn, c))
+					throw new NoSuchContactException();
 				if (!db.containsGroup(txn, g))
 					throw new NoSuchGroupException();
-				// Make the group visible or invisible to future contacts
-				db.setVisibleToAll(txn, g, all);
-				if (all) {
-					// Make the group visible to all current contacts
-					Collection<ContactId> before = db.getVisibility(txn, g);
-					before = new HashSet<ContactId>(before);
-					for (ContactId c : db.getContactIds(txn)) {
-						if (!before.contains(c)) {
-							db.addVisibility(txn, c, g);
-							affected.add(c);
-						}
-					}
-				}
+				wasVisible = db.containsVisibleGroup(txn, c, g);
+				if (visible && !wasVisible) db.addVisibility(txn, c, g);
+				else if (!visible && wasVisible) db.removeVisibility(txn, c, g);
 				db.commitTransaction(txn);
 			} catch (DbException e) {
 				db.abortTransaction(txn);
@@ -1220,8 +1234,10 @@ class DatabaseComponentImpl<T> implements DatabaseComponent {
 		} finally {
 			lock.writeLock().unlock();
 		}
-		if (!affected.isEmpty())
-			eventBus.broadcast(new GroupVisibilityUpdatedEvent(affected));
+		if (visible != wasVisible) {
+			eventBus.broadcast(new GroupVisibilityUpdatedEvent(
+					Collections.singletonList(c)));
+		}
 	}
 
 	public void updateTransportKeys(Map<ContactId, TransportKeys> keys)
diff --git a/briar-core/src/org/briarproject/db/JdbcDatabase.java b/briar-core/src/org/briarproject/db/JdbcDatabase.java
index f922c548c724e2c3b5b783b3b0a9baef4336fbae..79c07da039a7ec53fe8a477c4d99be11dbfa76b9 100644
--- a/briar-core/src/org/briarproject/db/JdbcDatabase.java
+++ b/briar-core/src/org/briarproject/db/JdbcDatabase.java
@@ -66,8 +66,8 @@ import static org.briarproject.db.ExponentialBackoff.calculateExpiry;
  */
 abstract class JdbcDatabase implements Database<Connection> {
 
-	private static final int SCHEMA_VERSION = 19;
-	private static final int MIN_SCHEMA_VERSION = 19;
+	private static final int SCHEMA_VERSION = 20;
+	private static final int MIN_SCHEMA_VERSION = 20;
 
 	private static final String CREATE_SETTINGS =
 			"CREATE TABLE settings"
@@ -104,7 +104,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					+ " (groupId HASH NOT NULL,"
 					+ " clientId HASH NOT NULL,"
 					+ " descriptor BINARY NOT NULL,"
-					+ " visibleToAll BOOLEAN NOT NULL,"
 					+ " PRIMARY KEY (groupId))";
 
 	private static final String CREATE_GROUP_METADATA =
@@ -511,30 +510,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 					if (rows != 1) throw new DbStateException();
 				ps.close();
 			}
-			// Make groups that are visible to everyone visible to this contact
-			sql = "SELECT groupId FROM groups WHERE visibleToAll = TRUE";
-			ps = txn.prepareStatement(sql);
-			rs = ps.executeQuery();
-			ids = new ArrayList<byte[]>();
-			while (rs.next()) ids.add(rs.getBytes(1));
-			rs.close();
-			ps.close();
-			if (!ids.isEmpty()) {
-				sql = "INSERT INTO groupVisibilities (contactId, groupId)"
-						+ " VALUES (?, ?)";
-				ps = txn.prepareStatement(sql);
-				ps.setInt(1, c.getInt());
-				for (byte[] id : ids) {
-					ps.setBytes(2, id);
-					ps.addBatch();
-				}
-				int[] batchAffected = ps.executeBatch();
-				if (batchAffected.length != ids.size())
-					throw new DbStateException();
-				for (int rows : batchAffected)
-					if (rows != 1) throw new DbStateException();
-				ps.close();
-			}
 			return c;
 		} catch (SQLException e) {
 			tryToClose(rs);
@@ -546,9 +521,8 @@ abstract class JdbcDatabase implements Database<Connection> {
 	public void addGroup(Connection txn, Group g) throws DbException {
 		PreparedStatement ps = null;
 		try {
-			String sql = "INSERT INTO groups"
-					+ " (groupId, clientId, descriptor, visibleToAll)"
-					+ " VALUES (?, ?, ?, FALSE)";
+			String sql = "INSERT INTO groups (groupId, clientId, descriptor)"
+					+ " VALUES (?, ?, ?)";
 			ps = txn.prepareStatement(sql);
 			ps.setBytes(1, g.getId().getBytes());
 			ps.setBytes(2, g.getClientId().getBytes());
@@ -2137,23 +2111,6 @@ abstract class JdbcDatabase implements Database<Connection> {
 		}
 	}
 
-	public void setVisibleToAll(Connection txn, GroupId g, boolean all)
-			throws DbException {
-		PreparedStatement ps = null;
-		try {
-			String sql = "UPDATE groups SET visibleToAll = ? WHERE groupId = ?";
-			ps = txn.prepareStatement(sql);
-			ps.setBoolean(1, all);
-			ps.setBytes(2, g.getBytes());
-			int affected = ps.executeUpdate();
-			if (affected < 0 || affected > 1) throw new DbStateException();
-			ps.close();
-		} catch (SQLException e) {
-			tryToClose(ps);
-			throw new DbException(e);
-		}
-	}
-
 	public void updateExpiryTime(Connection txn, ContactId c, MessageId m,
 			int maxLatency) throws DbException {
 		PreparedStatement ps = null;
diff --git a/briar-core/src/org/briarproject/forum/ForumListValidator.java b/briar-core/src/org/briarproject/forum/ForumListValidator.java
new file mode 100644
index 0000000000000000000000000000000000000000..98d905c2989f081368cb4ae104460d17f73ebe6d
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/ForumListValidator.java
@@ -0,0 +1,69 @@
+package org.briarproject.forum;
+
+import org.briarproject.api.FormatException;
+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.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 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;
+
+	ForumListValidator(BdfReaderFactory bdfReaderFactory,
+			MetadataEncoder metadataEncoder) {
+		this.bdfReaderFactory = bdfReaderFactory;
+		this.metadataEncoder = metadataEncoder;
+	}
+
+	@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.readInteger();
+			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;
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
index 872974c299ad37372f299cc14dbe2ec6aa187405..0e2c581c84e293d97fbe61eb8f6ca947ad6cea18 100644
--- a/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
+++ b/briar-core/src/org/briarproject/forum/ForumManagerImpl.java
@@ -4,13 +4,10 @@ import com.google.inject.Inject;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
-import org.briarproject.api.contact.ContactId;
-import org.briarproject.api.crypto.CryptoComponent;
+import org.briarproject.api.contact.ContactManager;
 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.db.DatabaseComponent;
@@ -25,15 +22,12 @@ import org.briarproject.api.identity.AuthorId;
 import org.briarproject.api.identity.LocalAuthor;
 import org.briarproject.api.sync.ClientId;
 import org.briarproject.api.sync.Group;
-import org.briarproject.api.sync.GroupFactory;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.MessageId;
 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;
 import java.util.Collection;
 import java.util.Collections;
@@ -42,6 +36,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
@@ -63,25 +58,23 @@ class ForumManagerImpl implements ForumManager {
 			Logger.getLogger(ForumManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
-	private final GroupFactory groupFactory;
+	private final ContactManager contactManager;
 	private final BdfReaderFactory bdfReaderFactory;
-	private final BdfWriterFactory bdfWriterFactory;
 	private final MetadataEncoder metadataEncoder;
 	private final MetadataParser metadataParser;
-	private final SecureRandom random;
+
+	/** Ensures isolation between database reads and writes. */
+	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
 
 	@Inject
-	ForumManagerImpl(CryptoComponent crypto, DatabaseComponent db,
-			GroupFactory groupFactory, BdfReaderFactory bdfReaderFactory,
-			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
+	ForumManagerImpl(DatabaseComponent db, ContactManager contactManager,
+			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser) {
 		this.db = db;
-		this.groupFactory = groupFactory;
+		this.contactManager = contactManager;
 		this.bdfReaderFactory = bdfReaderFactory;
-		this.bdfWriterFactory = bdfWriterFactory;
 		this.metadataEncoder = metadataEncoder;
 		this.metadataParser = metadataParser;
-		random = crypto.getSecureRandom();
 	}
 
 	@Override
@@ -89,214 +82,171 @@ class ForumManagerImpl implements ForumManager {
 		return CLIENT_ID;
 	}
 
-	@Override
-	public Forum createForum(String name) {
-		int length = StringUtils.toUtf8(name).length;
-		if (length == 0) throw new IllegalArgumentException();
-		if (length > MAX_FORUM_NAME_LENGTH)
-			throw new IllegalArgumentException();
-		byte[] salt = new byte[FORUM_SALT_LENGTH];
-		random.nextBytes(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
-			throw new RuntimeException(e);
-		}
-		Group g = groupFactory.createGroup(CLIENT_ID, out.toByteArray());
-		return new Forum(g, name);
-	}
-
-	@Override
-	public void addForum(Forum f) throws DbException {
-		db.addGroup(f.getGroup());
-	}
-
 	@Override
 	public void addLocalPost(ForumPost p) throws DbException {
-		BdfDictionary d = new BdfDictionary();
-		d.put("timestamp", p.getMessage().getTimestamp());
-		if (p.getParent() != null) d.put("parent", p.getParent().getBytes());
-		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);
+		lock.writeLock().lock();
 		try {
+			BdfDictionary d = new BdfDictionary();
+			d.put("timestamp", p.getMessage().getTimestamp());
+			if (p.getParent() != null)
+				d.put("parent", p.getParent().getBytes());
+			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);
 			db.addLocalMessage(p.getMessage(), CLIENT_ID, meta, true);
 		} catch (FormatException e) {
-			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-		}
-	}
-
-	@Override
-	public Collection<Forum> getAvailableForums() throws DbException {
-		// TODO
-		return Collections.emptyList();
-	}
-
-	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);
-			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();
-			if (!r.eof()) throw new FormatException();
-			return new Forum(g, name);
-		} catch (FormatException e) {
-			throw e;
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
+		} finally {
+			lock.writeLock().unlock();
 		}
 	}
 
 	@Override
 	public Forum getForum(GroupId g) throws DbException {
-		Group group = db.getGroup(g);
-		if (!group.getClientId().equals(CLIENT_ID))
-			throw new IllegalArgumentException();
+		lock.readLock().lock();
 		try {
-			return parseForum(group);
+			return parseForum(db.getGroup(g));
 		} catch (FormatException e) {
-			throw new IllegalArgumentException();
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<Forum> getForums() throws DbException {
-		Collection<Group> groups = db.getGroups(CLIENT_ID);
-		List<Forum> forums = new ArrayList<Forum>(groups.size());
-		for (Group g : groups) {
-			try {
-				forums.add(parseForum(g));
-			} catch (FormatException e) {
-				if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
-			}
+		lock.readLock().lock();
+		try {
+			List<Forum> forums = new ArrayList<Forum>();
+			for (Group g : db.getGroups(CLIENT_ID)) forums.add(parseForum(g));
+			return Collections.unmodifiableList(forums);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
 		}
-		return Collections.unmodifiableList(forums);
 	}
 
 	@Override
 	public byte[] getPostBody(MessageId m) throws DbException {
-		byte[] raw = db.getRawMessage(m);
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
+		lock.readLock().lock();
 		try {
-			// Extract the forum post body
+			byte[] raw = db.getRawMessage(m);
+			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
-			return r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
+			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;
 		} catch (FormatException e) {
-			// Not a valid forum post
-			throw new IllegalArgumentException();
+			throw new DbException(e);
 		} catch (IOException e) {
 			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
+		} finally {
+			lock.readLock().unlock();
 		}
 	}
 
 	@Override
 	public Collection<ForumPostHeader> getPostHeaders(GroupId g)
 			throws DbException {
-		// Load the IDs of the user's own identities and contacts' identities
-		Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
-		for (LocalAuthor a : db.getLocalAuthors())
-			localAuthorIds.add(a.getId());
-		Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
-		for (Contact c : db.getContacts())
-			contactAuthorIds.add(c.getAuthor().getId());
-		// Load and parse the metadata
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
-		Collection<ForumPostHeader> headers = new ArrayList<ForumPostHeader>();
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			MessageId messageId = e.getKey();
-			Metadata meta = e.getValue();
-			try {
-				BdfDictionary d = metadataParser.parse(meta);
-				long timestamp = d.getInteger("timestamp");
-				Author author = null;
-				Author.Status authorStatus = ANONYMOUS;
-				BdfDictionary d1 = d.getDictionary("author", null);
-				if (d1 != null) {
-					AuthorId authorId = new AuthorId(d1.getRaw("id"));
-					String name = d1.getString("name");
-					byte[] publicKey = d1.getRaw("publicKey");
-					author = new Author(authorId, name, publicKey);
-					if (localAuthorIds.contains(authorId))
-						authorStatus = VERIFIED;
-					else if (contactAuthorIds.contains(authorId))
-						authorStatus = VERIFIED;
-					else authorStatus = UNKNOWN;
+		lock.readLock().lock();
+		try {
+			// Load the IDs of the user's identities
+			Set<AuthorId> localAuthorIds = new HashSet<AuthorId>();
+			for (LocalAuthor a : db.getLocalAuthors())
+				localAuthorIds.add(a.getId());
+			// Load the IDs of contacts' identities
+			Set<AuthorId> contactAuthorIds = new HashSet<AuthorId>();
+			for (Contact c : contactManager.getContacts())
+				contactAuthorIds.add(c.getAuthor().getId());
+			// Load and parse the metadata
+			Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+			Collection<ForumPostHeader> headers =
+					new ArrayList<ForumPostHeader>();
+			for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+				MessageId messageId = e.getKey();
+				Metadata meta = e.getValue();
+				try {
+					BdfDictionary d = metadataParser.parse(meta);
+					long timestamp = d.getInteger("timestamp");
+					Author author = null;
+					Author.Status authorStatus = ANONYMOUS;
+					BdfDictionary d1 = d.getDictionary("author", null);
+					if (d1 != null) {
+						AuthorId authorId = new AuthorId(d1.getRaw("id"));
+						String name = d1.getString("name");
+						byte[] publicKey = d1.getRaw("publicKey");
+						author = new Author(authorId, name, publicKey);
+						if (localAuthorIds.contains(authorId))
+							authorStatus = VERIFIED;
+						else if (contactAuthorIds.contains(authorId))
+							authorStatus = VERIFIED;
+						else authorStatus = UNKNOWN;
+					}
+					String contentType = d.getString("contentType");
+					boolean read = d.getBoolean("read");
+					headers.add(new ForumPostHeader(messageId, timestamp,
+							author, authorStatus, contentType, read));
+				} catch (FormatException ex) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, ex.toString(), ex);
 				}
-				String contentType = d.getString("contentType");
-				boolean read = d.getBoolean("read");
-				headers.add(new ForumPostHeader(messageId, timestamp, author,
-						authorStatus, contentType, read));
-			} catch (FormatException ex) {
-				if (LOG.isLoggable(WARNING))
-					LOG.log(WARNING, ex.toString(), ex);
 			}
+			return headers;
+		} finally {
+			lock.readLock().unlock();
 		}
-		return headers;
-	}
-
-	@Override
-	public Collection<Contact> getSubscribers(GroupId g) throws DbException {
-		// TODO
-		return Collections.emptyList();
-	}
-
-	@Override
-	public Collection<ContactId> getVisibility(GroupId g) throws DbException {
-		return db.getVisibility(g);
-	}
-
-	@Override
-	public void removeForum(Forum f) throws DbException {
-		db.removeGroup(f.getGroup());
 	}
 
 	@Override
 	public void setReadFlag(MessageId m, boolean read) throws DbException {
-		BdfDictionary d = new BdfDictionary();
-		d.put("read", read);
+		lock.writeLock().lock();
 		try {
+			BdfDictionary d = new BdfDictionary();
+			d.put("read", read);
 			db.mergeMessageMetadata(m, metadataEncoder.encode(d));
 		} catch (FormatException e) {
 			throw new RuntimeException(e);
+		} finally {
+			lock.writeLock().unlock();
 		}
 	}
 
-	@Override
-	public void setVisibility(GroupId g, Collection<ContactId> visible)
-			throws DbException {
-		db.setVisibility(g, visible);
-	}
-
-	@Override
-	public void setVisibleToAll(GroupId g, boolean all) throws DbException {
-		db.setVisibleToAll(g, all);
+	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);
+		}
 	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumModule.java b/briar-core/src/org/briarproject/forum/ForumModule.java
index 9dd05f22d69963fb7d209bfa106e63e490b40263..fc24c6c00cf1835bf0e23521b9084e007ed5c58f 100644
--- a/briar-core/src/org/briarproject/forum/ForumModule.java
+++ b/briar-core/src/org/briarproject/forum/ForumModule.java
@@ -3,39 +3,63 @@ package org.briarproject.forum;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 
+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.event.EventBus;
 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.sync.ValidationManager;
 import org.briarproject.api.system.Clock;
 
 import javax.inject.Singleton;
 
-import static org.briarproject.forum.ForumManagerImpl.CLIENT_ID;
-
 public class ForumModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
-		bind(ForumManager.class).to(ForumManagerImpl.class);
+		bind(ForumManager.class).to(ForumManagerImpl.class).in(Singleton.class);
 		bind(ForumPostFactory.class).to(ForumPostFactoryImpl.class);
 	}
 
 	@Provides @Singleton
-	ForumPostValidator getValidator(ValidationManager validationManager,
-			CryptoComponent crypto, BdfReaderFactory bdfReaderFactory,
+	ForumPostValidator getForumPostValidator(
+			ValidationManager validationManager, CryptoComponent crypto,
+			BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory,
 			ObjectReader<Author> authorReader, MetadataEncoder metadataEncoder,
 			Clock clock) {
 		ForumPostValidator validator = new ForumPostValidator(crypto,
 				bdfReaderFactory, bdfWriterFactory, authorReader,
 				metadataEncoder, clock);
-		validationManager.registerMessageValidator(CLIENT_ID, validator);
+		validationManager.registerMessageValidator(
+				ForumManagerImpl.CLIENT_ID, validator);
+		return validator;
+	}
+
+	@Provides @Singleton
+	ForumListValidator getForumListValidator(
+			ValidationManager validationManager,
+			BdfReaderFactory bdfReaderFactory,
+			MetadataEncoder metadataEncoder) {
+		ForumListValidator validator = new ForumListValidator(bdfReaderFactory,
+				metadataEncoder);
+		validationManager.registerMessageValidator(
+				ForumSharingManagerImpl.CLIENT_ID, validator);
 		return validator;
 	}
+
+	@Provides @Singleton
+	ForumSharingManager getForumSharingManager(ContactManager contactManager,
+			EventBus eventBus, ForumSharingManagerImpl forumSharingManager) {
+		contactManager.registerAddContactHook(forumSharingManager);
+		contactManager.registerRemoveContactHook(forumSharingManager);
+		eventBus.addListener(forumSharingManager);
+		return forumSharingManager;
+	}
 }
diff --git a/briar-core/src/org/briarproject/forum/ForumPostValidator.java b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
index 1ee770cf1875bc3294df0ee9151cd87891110b80..a21f546252e5c3151e6bada39fe02cf2506ccf9f 100644
--- a/briar-core/src/org/briarproject/forum/ForumPostValidator.java
+++ b/briar-core/src/org/briarproject/forum/ForumPostValidator.java
@@ -15,6 +15,7 @@ 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.sync.Group;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
 import org.briarproject.api.sync.MessageValidator;
@@ -26,8 +27,6 @@ import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 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_SIGNATURE_LENGTH;
@@ -47,7 +46,6 @@ class ForumPostValidator implements MessageValidator {
 	private final Clock clock;
 	private final KeyParser keyParser;
 
-	@Inject
 	ForumPostValidator(CryptoComponent crypto,
 			BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory,
@@ -63,7 +61,7 @@ class ForumPostValidator implements MessageValidator {
 	}
 
 	@Override
-	public Metadata validateMessage(Message m) {
+	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) {
@@ -78,8 +76,7 @@ class ForumPostValidator implements MessageValidator {
 			BdfReader r = bdfReaderFactory.createReader(in);
 			MessageId parent = null;
 			Author author = null;
-			String contentType;
-			byte[] postBody, sig = null;
+			byte[] sig = null;
 			r.readListStart();
 			// Read the parent ID, if any
 			if (r.hasRaw()) {
@@ -93,9 +90,9 @@ class ForumPostValidator implements MessageValidator {
 			if (r.hasList()) author = authorReader.readObject(r);
 			else r.readNull();
 			// Read the content type
-			contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
+			String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
 			// Read the forum post body
-			postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
+			byte[] postBody = r.readRaw(MAX_FORUM_POST_BODY_LENGTH);
 
 			// Read the signature, if any
 			if (r.hasRaw()) sig = r.readRaw(MAX_SIGNATURE_LENGTH);
diff --git a/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..f415051a1c7c5445f5174a2138bd62071000fcd1
--- /dev/null
+++ b/briar-core/src/org/briarproject/forum/ForumSharingManagerImpl.java
@@ -0,0 +1,558 @@
+package org.briarproject.forum;
+
+import com.google.inject.Inject;
+
+import org.briarproject.api.FormatException;
+import org.briarproject.api.contact.Contact;
+import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
+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.db.DatabaseComponent;
+import org.briarproject.api.db.DatabaseExecutor;
+import org.briarproject.api.db.DbException;
+import org.briarproject.api.db.Metadata;
+import org.briarproject.api.event.Event;
+import org.briarproject.api.event.EventListener;
+import org.briarproject.api.event.MessageValidatedEvent;
+import org.briarproject.api.forum.Forum;
+import org.briarproject.api.forum.ForumManager;
+import org.briarproject.api.forum.ForumSharingManager;
+import org.briarproject.api.sync.ClientId;
+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.PrivateGroupFactory;
+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;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+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.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
+
+class ForumSharingManagerImpl implements ForumSharingManager, AddContactHook,
+		RemoveContactHook, EventListener {
+
+	static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
+			"cd11a5d04dccd9e2931d6fc3df456313"
+					+ "63bb3e9d9d0e9405fccdb051f41f5449"));
+
+	private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
+
+	private static final Logger LOG =
+			Logger.getLogger(ForumSharingManagerImpl.class.getName());
+
+	private final DatabaseComponent db;
+	private final Executor dbExecutor;
+	private final ContactManager contactManager;
+	private final ForumManager forumManager;
+	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;
+
+	/** Ensures isolation between database reads and writes. */
+	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
+
+	@Inject
+	ForumSharingManagerImpl(DatabaseComponent db,
+			@DatabaseExecutor Executor dbExecutor,
+			ContactManager contactManager, ForumManager forumManager,
+			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
+			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
+			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
+			MetadataParser metadataParser, SecureRandom random, Clock clock) {
+		this.db = db;
+		this.dbExecutor = dbExecutor;
+		this.contactManager = contactManager;
+		this.forumManager = forumManager;
+		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,
+				LOCAL_GROUP_DESCRIPTOR);
+	}
+
+	@Override
+	public void addingContact(ContactId c) {
+		lock.writeLock().lock();
+		try {
+			// Create a group to share with the contact
+			Group g = getContactGroup(db.getContact(c));
+			// Store the group and share it with the contact
+			db.addGroup(g);
+			db.setVisibility(g.getId(), Collections.singletonList(c));
+			// Attach the contact ID to the group
+			BdfDictionary d = new BdfDictionary();
+			d.put("contactId", c.getInt());
+			db.mergeGroupMetadata(g.getId(), metadataEncoder.encode(d));
+			// Share any forums that are shared with all contacts
+			List<Forum> shared = getForumsSharedWithAllContacts();
+			storeMessage(g.getId(), shared, 0);
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} catch (FormatException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void removingContact(ContactId c) {
+		lock.writeLock().lock();
+		try {
+			db.removeGroup(getContactGroup(db.getContact(c)));
+		} catch (DbException e) {
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void eventOccurred(Event e) {
+		if (e instanceof MessageValidatedEvent) {
+			MessageValidatedEvent m = (MessageValidatedEvent) e;
+			ClientId c = m.getClientId();
+			if (m.isValid() && !m.isLocal() && c.equals(CLIENT_ID))
+				remoteForumsUpdated(m.getMessage().getGroupId());
+		}
+	}
+
+	@Override
+	public ClientId getClientId() {
+		return CLIENT_ID;
+	}
+
+	@Override
+	public Forum createForum(String name) {
+		int length = StringUtils.toUtf8(name).length;
+		if (length == 0) throw new IllegalArgumentException();
+		if (length > MAX_FORUM_NAME_LENGTH)
+			throw new IllegalArgumentException();
+		byte[] salt = new byte[FORUM_SALT_LENGTH];
+		random.nextBytes(salt);
+		return createForum(name, salt);
+	}
+
+	@Override
+	public void addForum(Forum f) throws DbException {
+		lock.writeLock().lock();
+		try {
+			db.addGroup(f.getGroup());
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void removeForum(Forum f) throws DbException {
+		lock.writeLock().lock();
+		try {
+			// Update the list of forums shared with each contact
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				removeFromList(contactGroup.getId(), f);
+			}
+			db.removeGroup(f.getGroup());
+		} catch (IOException e) {
+			throw new DbException(e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public Collection<Forum> getAvailableForums() throws DbException {
+		lock.readLock().lock();
+		try {
+			// Get any forums we subscribe to
+			Set<Group> subscribed = new HashSet<Group>(db.getGroups(
+					forumManager.getClientId()));
+			// Get all forums shared by contacts
+			Set<Forum> available = new HashSet<Forum>();
+			for (Contact c : contactManager.getContacts()) {
+				Group g = getContactGroup(c);
+				// Find the latest update version
+				LatestUpdate latest = findLatest(g.getId(), false);
+				if (latest != null) {
+					// Retrieve and parse the latest update
+					byte[] raw = db.getRawMessage(latest.messageId);
+					for (Forum f : parseForumList(raw)) {
+						if (!subscribed.contains(f.getGroup()))
+							available.add(f);
+					}
+				}
+			}
+			return Collections.unmodifiableSet(available);
+		} catch (IOException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	@Override
+	public Collection<Contact> getSharedBy(GroupId g) throws DbException {
+		lock.readLock().lock();
+		try {
+			List<Contact> subscribers = new ArrayList<Contact>();
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (listContains(contactGroup.getId(), g, false))
+					subscribers.add(c);
+			}
+			return Collections.unmodifiableList(subscribers);
+		} catch (IOException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	@Override
+	public Collection<ContactId> getSharedWith(GroupId g) throws DbException {
+		lock.readLock().lock();
+		try {
+			List<ContactId> shared = new ArrayList<ContactId>();
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (listContains(contactGroup.getId(), g, true))
+					shared.add(c.getId());
+			}
+			return Collections.unmodifiableList(shared);
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.readLock().unlock();
+		}
+	}
+
+	@Override
+	public void setSharedWith(GroupId g, Collection<ContactId> shared)
+			throws DbException {
+		lock.writeLock().lock();
+		try {
+			// Retrieve the forum
+			Forum f = parseForum(db.getGroup(g));
+			// Remove the forum from the list of forums shared with all contacts
+			removeFromList(localGroup.getId(), f);
+			// Update the list of forums shared with each contact
+			shared = new HashSet<ContactId>(shared);
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (shared.contains(c.getId())) {
+					if (addToList(contactGroup.getId(), f)) {
+						// If the contact is sharing the forum, make it visible
+						if (listContains(contactGroup.getId(), g, false))
+							db.setVisibleToContact(c.getId(), g, true);
+					}
+				} else {
+					removeFromList(contactGroup.getId(), f);
+					db.setVisibleToContact(c.getId(), g, false);
+				}
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	@Override
+	public void setSharedWithAll(GroupId g) throws DbException {
+		lock.writeLock().lock();
+		try {
+			// Retrieve the forum
+			Forum f = parseForum(db.getGroup(g));
+			// Add the forum to the list of forums shared with all contacts
+			addToList(localGroup.getId(), f);
+			// Add the forum to the list of forums shared with each contact
+			for (Contact c : contactManager.getContacts()) {
+				Group contactGroup = getContactGroup(c);
+				if (addToList(contactGroup.getId(), f)) {
+					// If the contact is sharing the forum, make it visible
+					if (listContains(contactGroup.getId(), g, false))
+						db.setVisibleToContact(getContactId(g), g, true);
+				}
+			}
+		} catch (FormatException e) {
+			throw new DbException(e);
+		} finally {
+			lock.writeLock().unlock();
+		}
+	}
+
+	private Group getContactGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	}
+
+	// Locking: lock.writeLock
+	private List<Forum> getForumsSharedWithAllContacts() throws DbException,
+			FormatException {
+		// Ensure the local group exists
+		db.addGroup(localGroup);
+		// Find the latest update in the local group
+		LatestUpdate latest = findLatest(localGroup.getId(), true);
+		if (latest == null) return Collections.emptyList();
+		// Retrieve and parse the latest update
+		return parseForumList(db.getRawMessage(latest.messageId));
+	}
+
+	// Locking: lock.readLock
+	private LatestUpdate findLatest(GroupId g, boolean local)
+			throws DbException, FormatException {
+		LatestUpdate latest = null;
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
+		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
+			BdfDictionary d = metadataParser.parse(e.getValue());
+			if (d.getBoolean("local") != local) continue;
+			long version = d.getInteger("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.skipInteger(); // 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);
+		}
+	}
+
+	// Locking: lock.writeLock
+	private void storeMessage(GroupId g, List<Forum> forums, long version)
+			throws DbException, FormatException {
+		byte[] 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);
+		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), true);
+	}
+
+	private byte[] encodeForumList(List<Forum> forums, long version) {
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		BdfWriter w = bdfWriterFactory.createWriter(out);
+		try {
+			w.writeListStart();
+			w.writeInteger(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 void remoteForumsUpdated(final GroupId g) {
+		dbExecutor.execute(new Runnable() {
+			public void run() {
+				lock.writeLock().lock();
+				try {
+					setForumVisibility(getContactId(g), getVisibleForums(g));
+				} catch (DbException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} catch (FormatException e) {
+					if (LOG.isLoggable(WARNING))
+						LOG.log(WARNING, e.toString(), e);
+				} finally {
+					lock.writeLock().unlock();
+				}
+			}
+		});
+	}
+
+	// Locking: lock.readLock
+	private ContactId getContactId(GroupId contactGroupId) throws DbException,
+			FormatException {
+		Metadata meta = db.getGroupMetadata(contactGroupId);
+		BdfDictionary d = metadataParser.parse(meta);
+		int id = d.getInteger("contactId").intValue();
+		return new ContactId(id);
+	}
+
+	// Locking: lock.readLock
+	private Set<GroupId> getVisibleForums(GroupId contactGroupId)
+			throws DbException, FormatException {
+		// Get the latest local and remote updates
+		LatestUpdate local = findLatest(contactGroupId, true);
+		LatestUpdate remote = findLatest(contactGroupId, false);
+		// If there's no local and/or remote update, no forums are visible
+		if (local == null || remote == null) return Collections.emptySet();
+		// Intersect the sets of shared forums
+		byte[] localRaw = db.getRawMessage(local.messageId);
+		Set<Forum> shared = new HashSet<Forum>(parseForumList(localRaw));
+		byte[] remoteRaw = db.getRawMessage(remote.messageId);
+		shared.retainAll(parseForumList(remoteRaw));
+		// Forums in the intersection should be visible
+		Set<GroupId> visible = new HashSet<GroupId>(shared.size());
+		for (Forum f : shared) visible.add(f.getId());
+		return visible;
+	}
+
+	// Locking: lock.writeLock
+	private void setForumVisibility(ContactId c, Set<GroupId> visible)
+			throws DbException {
+		for (Group g : db.getGroups(forumManager.getClientId())) {
+			boolean isVisible = db.isVisibleToContact(c, g.getId());
+			boolean shouldBeVisible = visible.contains(g.getId());
+			if (isVisible && !shouldBeVisible)
+				db.setVisibleToContact(c, g.getId(), false);
+			else if (!isVisible && shouldBeVisible)
+				db.setVisibleToContact(c, g.getId(), true);
+		}
+	}
+
+	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
+			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);
+		}
+	}
+
+	// Locking: lock.readLock
+	private boolean listContains(GroupId g, GroupId forum, boolean local)
+			throws DbException, FormatException {
+		LatestUpdate latest = findLatest(g, local);
+		if (latest == null) return false;
+		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		for (Forum f : list) if (f.getId().equals(forum)) return true;
+		return false;
+	}
+
+	// Locking: lock.writeLock
+	private boolean addToList(GroupId g, Forum f) throws DbException,
+			FormatException {
+		LatestUpdate latest = findLatest(g, true);
+		if (latest == null) {
+			storeMessage(g, Collections.singletonList(f), 0);
+			return true;
+		}
+		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		if (list.contains(f)) return false;
+		list.add(f);
+		storeMessage(g, list, latest.version + 1);
+		return true;
+	}
+
+	// Locking: lock.writeLock
+	private void removeFromList(GroupId g, Forum f) throws DbException,
+			FormatException {
+		LatestUpdate latest = findLatest(g, true);
+		if (latest == null) return;
+		List<Forum> list = parseForumList(db.getRawMessage(latest.messageId));
+		if (list.remove(f)) storeMessage(g, list, latest.version + 1);
+	}
+
+	private static class LatestUpdate {
+
+		private final MessageId messageId;
+		private final long version;
+
+		private LatestUpdate(MessageId messageId, long version) {
+			this.messageId = messageId;
+			this.version = version;
+		}
+	}
+}
diff --git a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
index 1260fdff1b7c89fa0b76b512e0ad3454db45e8d3..65f0432893f5d5d54c9806719b8d2be007f92cb6 100644
--- a/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
+++ b/briar-core/src/org/briarproject/messaging/MessagingManagerImpl.java
@@ -5,6 +5,7 @@ import com.google.inject.Inject;
 import org.briarproject.api.FormatException;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -50,18 +51,19 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 			Logger.getLogger(MessagingManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
+	private final ContactManager contactManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final BdfReaderFactory bdfReaderFactory;
 	private final MetadataEncoder metadataEncoder;
 	private final MetadataParser metadataParser;
 
 	@Inject
-	MessagingManagerImpl(DatabaseComponent db,
+	MessagingManagerImpl(DatabaseComponent db, ContactManager contactManager,
 			PrivateGroupFactory privateGroupFactory,
-			BdfReaderFactory bdfReaderFactory,
-			MetadataEncoder metadataEncoder,
+			BdfReaderFactory bdfReaderFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser) {
 		this.db = db;
+		this.contactManager = contactManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.bdfReaderFactory = bdfReaderFactory;
 		this.metadataEncoder = metadataEncoder;
@@ -71,8 +73,8 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 	@Override
 	public void addingContact(ContactId c) {
 		try {
-			// Create the conversation group
-			Group g = getConversationGroup(db.getContact(c));
+			// Create a group to share with the contact
+			Group g = getContactGroup(db.getContact(c));
 			// Store the group and share it with the contact
 			db.addGroup(g);
 			db.setVisibility(g.getId(), Collections.singletonList(c));
@@ -87,14 +89,14 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 		}
 	}
 
-	private Group getConversationGroup(Contact c) {
+	private Group getContactGroup(Contact c) {
 		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
 	}
 
 	@Override
 	public void removingContact(ContactId c) {
 		try {
-			db.removeGroup(getConversationGroup(db.getContact(c)));
+			db.removeGroup(getContactGroup(db.getContact(c)));
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
@@ -135,7 +137,7 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 
 	@Override
 	public GroupId getConversationId(ContactId c) throws DbException {
-		return getConversationGroup(db.getContact(c)).getId();
+		return getContactGroup(contactManager.getContact(c)).getId();
 	}
 
 	@Override
@@ -172,15 +174,16 @@ class MessagingManagerImpl implements MessagingManager, AddContactHook,
 				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 		BdfReader r = bdfReaderFactory.createReader(in);
 		try {
-			// Extract the private message body
 			r.readListStart();
 			if (r.hasRaw()) r.skipRaw(); // Parent ID
 			else r.skipNull(); // No parent
 			r.skipString(); // Content type
-			return r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
+			byte[] messageBody = r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
+			r.readListEnd();
+			if (!r.eof()) throw new FormatException();
+			return messageBody;
 		} catch (FormatException e) {
-			// Not a valid private message
-			throw new IllegalArgumentException();
+			throw new DbException(e);
 		} catch (IOException e) {
 			// Shouldn't happen with ByteArrayInputStream
 			throw new RuntimeException(e);
diff --git a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
index c0134934f1283334830f0910ca152eebb7917148..fa3e0ec5f3d9cd90c2d9cafa1b08089c7db290ca 100644
--- a/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
+++ b/briar-core/src/org/briarproject/messaging/PrivateMessageValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
 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;
@@ -16,8 +17,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 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;
@@ -32,7 +31,6 @@ class PrivateMessageValidator implements MessageValidator {
 	private final MetadataEncoder metadataEncoder;
 	private final Clock clock;
 
-	@Inject
 	PrivateMessageValidator(BdfReaderFactory bdfReaderFactory,
 			MetadataEncoder metadataEncoder, Clock clock) {
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -41,7 +39,7 @@ class PrivateMessageValidator implements MessageValidator {
 	}
 
 	@Override
-	public Metadata validateMessage(Message m) {
+	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) {
@@ -55,7 +53,6 @@ class PrivateMessageValidator implements MessageValidator {
 					MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
 			BdfReader r = bdfReaderFactory.createReader(in);
 			MessageId parent = null;
-			String contentType;
 			r.readListStart();
 			// Read the parent ID, if any
 			if (r.hasRaw()) {
@@ -66,7 +63,7 @@ class PrivateMessageValidator implements MessageValidator {
 				r.readNull();
 			}
 			// Read the content type
-			contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
+			String contentType = r.readString(MAX_CONTENT_TYPE_LENGTH);
 			// Read the private message body
 			r.readRaw(MAX_PRIVATE_MESSAGE_BODY_LENGTH);
 			r.readListEnd();
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
index 135b74931e109f0f260cbe947903937fb1cb868a..bbc6cd57d16f68433e7debf2e87977ced7094c3e 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java
@@ -7,6 +7,7 @@ import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.Contact;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.contact.ContactManager;
 import org.briarproject.api.contact.ContactManager.AddContactHook;
 import org.briarproject.api.contact.ContactManager.RemoveContactHook;
 import org.briarproject.api.data.BdfDictionary;
@@ -60,6 +61,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			Logger.getLogger(TransportPropertyManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
+	private final ContactManager contactManager;
 	private final PrivateGroupFactory privateGroupFactory;
 	private final MessageFactory messageFactory;
 	private final BdfReaderFactory bdfReaderFactory;
@@ -74,11 +76,13 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 
 	@Inject
 	TransportPropertyManagerImpl(DatabaseComponent db,
-			GroupFactory groupFactory, PrivateGroupFactory privateGroupFactory,
+			ContactManager contactManager, GroupFactory groupFactory,
+			PrivateGroupFactory privateGroupFactory,
 			MessageFactory messageFactory, BdfReaderFactory bdfReaderFactory,
 			BdfWriterFactory bdfWriterFactory, MetadataEncoder metadataEncoder,
 			MetadataParser metadataParser, Clock clock) {
 		this.db = db;
+		this.contactManager = contactManager;
 		this.privateGroupFactory = privateGroupFactory;
 		this.messageFactory = messageFactory;
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -109,47 +113,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		} catch (DbException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} catch (FormatException e) {
-			throw new RuntimeException(e);
+			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		} finally {
 			lock.writeLock().unlock();
 		}
 	}
 
-	private Group getContactGroup(Contact c) {
-		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
-	}
-
-	private void storeMessage(GroupId g, DeviceId dev, TransportId t,
-			TransportProperties p, long version, boolean local, boolean shared)
-			throws DbException, FormatException {
-		byte[] 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);
-		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared);
-	}
-
-	private byte[] 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.writeInteger(version);
-			w.writeDictionary(p);
-			w.writeListEnd();
-		} catch (IOException e) {
-			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
-		}
-		return out.toByteArray();
-	}
-
 	@Override
 	public void removingContact(ContactId c) {
 		lock.writeLock().lock();
@@ -172,7 +141,7 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 				storeMessage(g.getId(), dev, e.getKey(), e.getValue(), 0, false,
 						false);
 			}
-		} catch (IOException e) {
+		} catch (FormatException e) {
 			throw new DbException(e);
 		} finally {
 			lock.writeLock().unlock();
@@ -184,15 +153,15 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			throws DbException {
 		lock.readLock().lock();
 		try {
-			// Find the latest local version for each transport
+			// Find the latest local update for each transport
 			Map<TransportId, LatestUpdate> latest =
 					findLatest(localGroup.getId(), true);
-			// Retrieve and decode the latest local properties
+			// Retrieve and parse the latest local properties
 			Map<TransportId, TransportProperties> local =
 					new HashMap<TransportId, TransportProperties>();
 			for (Entry<TransportId, LatestUpdate> e : latest.entrySet()) {
 				byte[] raw = db.getRawMessage(e.getValue().messageId);
-				local.put(e.getKey(), decodeProperties(raw));
+				local.put(e.getKey(), parseProperties(raw));
 			}
 			return Collections.unmodifiableMap(local);
 		} catch (NoSuchGroupException e) {
@@ -205,53 +174,16 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		}
 	}
 
-	private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local)
-			throws DbException, FormatException {
-		// TODO: Use metadata queries
-		Map<TransportId, LatestUpdate> latestUpdates =
-				new HashMap<TransportId, LatestUpdate>();
-		Map<MessageId, Metadata> metadata = db.getMessageMetadata(g);
-		for (Entry<MessageId, Metadata> e : metadata.entrySet()) {
-			BdfDictionary d = metadataParser.parse(e.getValue());
-			if (d.getBoolean("local") != local) continue;
-			TransportId t = new TransportId(d.getString("transportId"));
-			long version = d.getInteger("version");
-			LatestUpdate latest = latestUpdates.get(t);
-			if (latest == null || version > latest.version)
-				latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
-		}
-		return latestUpdates;
-	}
-
-	private TransportProperties decodeProperties(byte[] raw)
-			throws IOException {
-		TransportProperties p = new TransportProperties();
-		ByteArrayInputStream in = new ByteArrayInputStream(raw,
-				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
-		BdfReader r = bdfReaderFactory.createReader(in);
-		r.readListStart();
-		r.skipRaw(); // Device ID
-		r.skipString(); // Transport ID
-		r.skipInteger(); // Version
-		r.readDictionaryStart();
-		while (!r.hasDictionaryEnd()) {
-			String key = r.readString(MAX_PROPERTY_LENGTH);
-			String value = r.readString(MAX_PROPERTY_LENGTH);
-			p.put(key, value);
-		}
-		return p;
-	}
-
 	@Override
 	public TransportProperties getLocalProperties(TransportId t)
 			throws DbException {
 		lock.readLock().lock();
 		try {
-			// Find the latest local version
-			LatestUpdate latest = findLatest(localGroup.getId(), true).get(t);
+			// Find the latest local update
+			LatestUpdate latest = findLatest(localGroup.getId(), t, true);
 			if (latest == null) return null;
-			// Retrieve and decode the latest local properties
-			return decodeProperties(db.getRawMessage(latest.messageId));
+			// Retrieve and parse the latest local properties
+			return parseProperties(db.getRawMessage(latest.messageId));
 		} catch (NoSuchGroupException e) {
 			// Local group doesn't exist - there are no local properties
 			return null;
@@ -269,14 +201,14 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		try {
 			Map<ContactId, TransportProperties> remote =
 					new HashMap<ContactId, TransportProperties>();
-			for (Contact c : db.getContacts())  {
+			for (Contact c : contactManager.getContacts())  {
 				Group g = getContactGroup(c);
-				// Find the latest remote version
-				LatestUpdate latest = findLatest(g.getId(), false).get(t);
+				// Find the latest remote update
+				LatestUpdate latest = findLatest(g.getId(), t, false);
 				if (latest != null) {
-					// Retrieve and decode the latest remote properties
+					// Retrieve and parse the latest remote properties
 					byte[] raw = db.getRawMessage(latest.messageId);
-					remote.put(c.getId(), decodeProperties(raw));
+					remote.put(c.getId(), parseProperties(raw));
 				}
 			}
 			return Collections.unmodifiableMap(remote);
@@ -296,12 +228,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			db.addGroup(localGroup);
 			// Merge the new properties with any existing properties
 			TransportProperties merged;
-			LatestUpdate latest = findLatest(localGroup.getId(), true).get(t);
+			LatestUpdate latest = findLatest(localGroup.getId(), t, true);
 			if (latest == null) {
 				merged = p;
 			} else {
 				byte[] raw = db.getRawMessage(latest.messageId);
-				TransportProperties old = decodeProperties(raw);
+				TransportProperties old = parseProperties(raw);
 				merged = new TransportProperties(old);
 				merged.putAll(p);
 				if (merged.equals(old)) return; // Unchanged
@@ -312,9 +244,9 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 			storeMessage(localGroup.getId(), dev, t, merged, version, true,
 					false);
 			// Store the merged properties in each contact's group
-			for (Contact c : db.getContacts()) {
+			for (Contact c : contactManager.getContacts()) {
 				Group g = getContactGroup(c);
-				latest = findLatest(g.getId(), true).get(t);
+				latest = findLatest(g.getId(), t, true);
 				version = latest == null ? 1 : latest.version + 1;
 				storeMessage(g.getId(), dev, t, merged, version, true, true);
 			}
@@ -325,6 +257,100 @@ class TransportPropertyManagerImpl implements TransportPropertyManager,
 		}
 	}
 
+	private Group getContactGroup(Contact c) {
+		return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
+	}
+
+	// Locking: lock.writeLock
+	private void storeMessage(GroupId g, DeviceId dev, TransportId t,
+			TransportProperties p, long version, boolean local, boolean shared)
+			throws DbException, FormatException {
+		byte[] 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);
+		db.addLocalMessage(m, CLIENT_ID, metadataEncoder.encode(d), shared);
+	}
+
+	private byte[] 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.writeInteger(version);
+			w.writeDictionary(p);
+			w.writeListEnd();
+		} catch (IOException e) {
+			// Shouldn't happen with ByteArrayOutputStream
+			throw new RuntimeException(e);
+		}
+		return out.toByteArray();
+	}
+
+	// Locking: lock.readLock
+	private Map<TransportId, LatestUpdate> findLatest(GroupId g, boolean local)
+			throws DbException, FormatException {
+		Map<TransportId, LatestUpdate> latestUpdates =
+				new HashMap<TransportId, LatestUpdate>();
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(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.getInteger("version");
+				LatestUpdate latest = latestUpdates.get(t);
+				if (latest == null || version > latest.version)
+					latestUpdates.put(t, new LatestUpdate(e.getKey(), version));
+			}
+		}
+		return latestUpdates;
+	}
+
+	// Locking: lock.readLock
+	private LatestUpdate findLatest(GroupId g, TransportId t, boolean local)
+			throws DbException, FormatException {
+		LatestUpdate latest = null;
+		Map<MessageId, Metadata> metadata = db.getMessageMetadata(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.getInteger("version");
+				if (latest == null || version > latest.version)
+					latest = new LatestUpdate(e.getKey(), version);
+			}
+		}
+		return latest;
+	}
+
+	private TransportProperties parseProperties(byte[] raw)
+			throws IOException {
+		TransportProperties p = new TransportProperties();
+		ByteArrayInputStream in = new ByteArrayInputStream(raw,
+				MESSAGE_HEADER_LENGTH, raw.length - MESSAGE_HEADER_LENGTH);
+		BdfReader r = bdfReaderFactory.createReader(in);
+		r.readListStart();
+		r.skipRaw(); // Device ID
+		r.skipString(); // Transport ID
+		r.skipInteger(); // 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;
+	}
+
 	private static class LatestUpdate {
 
 		private final MessageId messageId;
diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
index eeee9f333447c2489c62e27d42da582eca6bde9c..a3b7c17832ac5997d3ece03b33418e5ebee9d3c7 100644
--- a/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
+++ b/briar-core/src/org/briarproject/properties/TransportPropertyValidator.java
@@ -7,6 +7,7 @@ import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
 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;
@@ -15,8 +16,6 @@ import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.util.logging.Logger;
 
-import javax.inject.Inject;
-
 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;
@@ -32,7 +31,6 @@ class TransportPropertyValidator implements MessageValidator {
 	private final MetadataEncoder metadataEncoder;
 	private final Clock clock;
 
-	@Inject
 	TransportPropertyValidator(BdfReaderFactory bdfReaderFactory,
 			MetadataEncoder metadataEncoder, Clock clock) {
 		this.bdfReaderFactory = bdfReaderFactory;
@@ -41,7 +39,7 @@ class TransportPropertyValidator implements MessageValidator {
 	}
 
 	@Override
-	public Metadata validateMessage(Message m) {
+	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) {
diff --git a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
index dfb93ec3fe7deab3e663d1bf0bf11db715249353..f6ec4779598c335b2493ef1c070fba258978ee5b 100644
--- a/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
+++ b/briar-core/src/org/briarproject/sync/DuplexOutgoingSession.java
@@ -13,7 +13,6 @@ import org.briarproject.api.event.MessageRequestedEvent;
 import org.briarproject.api.event.MessageSharedEvent;
 import org.briarproject.api.event.MessageToAckEvent;
 import org.briarproject.api.event.MessageToRequestEvent;
-import org.briarproject.api.event.MessageValidatedEvent;
 import org.briarproject.api.event.ShutdownEvent;
 import org.briarproject.api.event.TransportRemovedEvent;
 import org.briarproject.api.sync.Ack;
@@ -151,9 +150,6 @@ class DuplexOutgoingSession implements SyncSession, EventListener {
 			if (c.getContactId().equals(contactId)) interrupt();
 		} else if (e instanceof MessageSharedEvent) {
 			dbExecutor.execute(new GenerateOffer());
-		} else if (e instanceof MessageValidatedEvent) {
-			if (((MessageValidatedEvent) e).isValid())
-				dbExecutor.execute(new GenerateOffer());
 		} else if (e instanceof GroupVisibilityUpdatedEvent) {
 			GroupVisibilityUpdatedEvent g = (GroupVisibilityUpdatedEvent) e;
 			if (g.getAffectedContacts().contains(contactId))
diff --git a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
index 855f59b0df73b98739ebfe3ce858b3f339478a8f..5fca1458452be5ed09fc776e6de3d3f3b1c1f773 100644
--- a/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
+++ b/briar-core/src/org/briarproject/sync/ValidationManagerImpl.java
@@ -15,6 +15,7 @@ import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.lifecycle.Service;
 import org.briarproject.api.sync.ClientId;
+import org.briarproject.api.sync.Group;
 import org.briarproject.api.sync.GroupId;
 import org.briarproject.api.sync.Message;
 import org.briarproject.api.sync.MessageId;
@@ -84,7 +85,8 @@ class ValidationManagerImpl implements ValidationManager, Service,
 					for (MessageId id : db.getMessagesToValidate(c)) {
 						try {
 							Message m = parseMessage(id, db.getRawMessage(id));
-							validateMessage(m, c);
+							Group g = db.getGroup(m.getGroupId());
+							validateMessage(m, g);
 						} catch (NoSuchMessageException e) {
 							LOG.info("Message removed before validation");
 						}
@@ -106,15 +108,15 @@ class ValidationManagerImpl implements ValidationManager, Service,
 		return new Message(id, new GroupId(groupId), timestamp, raw);
 	}
 
-	private void validateMessage(final Message m, final ClientId c) {
+	private void validateMessage(final Message m, final Group g) {
 		cryptoExecutor.execute(new Runnable() {
 			public void run() {
-				MessageValidator v = validators.get(c);
+				MessageValidator v = validators.get(g.getClientId());
 				if (v == null) {
 					LOG.warning("No validator");
 				} else {
-					Metadata meta = v.validateMessage(m);
-					storeValidationResult(m, c, meta);
+					Metadata meta = v.validateMessage(m, g);
+					storeValidationResult(m, g.getClientId(), meta);
 				}
 			}
 		});
@@ -132,6 +134,7 @@ class ValidationManagerImpl implements ValidationManager, Service,
 							hook.validatingMessage(m, c, meta);
 						db.mergeMessageMetadata(m.getId(), meta);
 						db.setMessageValid(m, c, true);
+						db.setMessageShared(m, true);
 					}
 				} catch (NoSuchMessageException e) {
 					LOG.info("Message removed during validation");
@@ -146,18 +149,17 @@ class ValidationManagerImpl implements ValidationManager, Service,
 	@Override
 	public void eventOccurred(Event e) {
 		if (e instanceof MessageAddedEvent) {
-			MessageAddedEvent m = (MessageAddedEvent) e;
 			// Validate the message if it wasn't created locally
-			if (m.getContactId() != null) loadClientId(m.getMessage());
+			MessageAddedEvent m = (MessageAddedEvent) e;
+			if (m.getContactId() != null) loadGroup(m.getMessage());
 		}
 	}
 
-	private void loadClientId(final Message m) {
+	private void loadGroup(final Message m) {
 		dbExecutor.execute(new Runnable() {
 			public void run() {
 				try {
-					ClientId c = db.getGroup(m.getGroupId()).getClientId();
-					validateMessage(m, c);
+					validateMessage(m, db.getGroup(m.getGroupId()));
 				} catch (NoSuchGroupException e) {
 					LOG.info("Group removed before validation");
 				} catch (DbException e) {
diff --git a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
index 92f2d671751809b3f5c68c588a5c9f910208daf4..0b05dc0120adc42580da2e4d22f5072bfe97f5e9 100644
--- a/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
+++ b/briar-desktop/src/org/briarproject/plugins/DesktopPluginsModule.java
@@ -2,7 +2,6 @@ package org.briarproject.plugins;
 
 import com.google.inject.Provides;
 
-import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.lifecycle.ShutdownManager;
 import org.briarproject.api.plugins.duplex.DuplexPluginConfig;
@@ -16,6 +15,7 @@ import org.briarproject.plugins.modem.ModemPluginFactory;
 import org.briarproject.plugins.tcp.LanTcpPluginFactory;
 import org.briarproject.plugins.tcp.WanTcpPluginFactory;
 
+import java.security.SecureRandom;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -39,10 +39,10 @@ public class DesktopPluginsModule extends PluginsModule {
 
 	@Provides
 	DuplexPluginConfig getDuplexPluginConfig(@IoExecutor Executor ioExecutor,
-			CryptoComponent crypto, ReliabilityLayerFactory reliabilityFactory,
+			SecureRandom random, ReliabilityLayerFactory reliabilityFactory,
 			ShutdownManager shutdownManager) {
-		DuplexPluginFactory bluetooth = new BluetoothPluginFactory(
-				ioExecutor, crypto.getSecureRandom());
+		DuplexPluginFactory bluetooth = new BluetoothPluginFactory(ioExecutor,
+				random);
 		DuplexPluginFactory modem = new ModemPluginFactory(ioExecutor,
 				reliabilityFactory);
 		DuplexPluginFactory lan = new LanTcpPluginFactory(ioExecutor);
diff --git a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
index 238b62970b05dd3fcda690283a7aa81545117332..b0930805f3613372bff049863e98c2ac5bf43015 100644
--- a/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
+++ b/briar-tests/src/org/briarproject/db/DatabaseComponentImplTest.java
@@ -20,6 +20,7 @@ import org.briarproject.api.event.GroupRemovedEvent;
 import org.briarproject.api.event.GroupVisibilityUpdatedEvent;
 import org.briarproject.api.event.MessageAddedEvent;
 import org.briarproject.api.event.MessageRequestedEvent;
+import org.briarproject.api.event.MessageSharedEvent;
 import org.briarproject.api.event.MessageToAckEvent;
 import org.briarproject.api.event.MessageToRequestEvent;
 import org.briarproject.api.event.MessageValidatedEvent;
@@ -246,6 +247,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// The message was added, so the listeners should be called
 			oneOf(eventBus).broadcast(with(any(MessageAddedEvent.class)));
 			oneOf(eventBus).broadcast(with(any(MessageValidatedEvent.class)));
+			oneOf(eventBus).broadcast(with(any(MessageSharedEvent.class)));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
@@ -265,11 +267,11 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
 			// Check whether the contact is in the DB (which it's not)
-			exactly(15).of(database).startTransaction();
+			exactly(17).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(15).of(database).containsContact(txn, contactId);
+			exactly(17).of(database).containsContact(txn, contactId);
 			will(returnValue(false));
-			exactly(15).of(database).abortTransaction(txn);
+			exactly(17).of(database).abortTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
 				shutdown);
@@ -337,6 +339,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.isVisibleToContact(contactId, groupId);
+			fail();
+		} catch (NoSuchContactException expected) {
+			// Expected
+		}
+
 		try {
 			Ack a = new Ack(Collections.singletonList(messageId));
 			db.receiveAck(contactId, a);
@@ -382,6 +391,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.setVisibleToContact(contactId, groupId, true);
+			fail();
+		} catch (NoSuchContactException expected) {
+			// Expected
+		}
+
 		context.assertIsSatisfied();
 	}
 
@@ -438,13 +454,14 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		final EventBus eventBus = context.mock(EventBus.class);
 		context.checking(new Expectations() {{
 			// Check whether the group is in the DB (which it's not)
-			exactly(7).of(database).startTransaction();
+			exactly(9).of(database).startTransaction();
 			will(returnValue(txn));
-			exactly(7).of(database).containsGroup(txn, groupId);
+			exactly(9).of(database).containsGroup(txn, groupId);
 			will(returnValue(false));
-			exactly(7).of(database).abortTransaction(txn);
-			// This is needed for getMessageStatus() to proceed
-			exactly(1).of(database).containsContact(txn, contactId);
+			exactly(9).of(database).abortTransaction(txn);
+			// This is needed for getMessageStatus(), isVisibleToContact(), and
+			// setVisibleToContact() to proceed
+			exactly(3).of(database).containsContact(txn, contactId);
 			will(returnValue(true));
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -478,6 +495,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.isVisibleToContact(contactId, groupId);
+			fail();
+		} catch (NoSuchGroupException expected) {
+			// Expected
+		}
+
 		try {
 			db.mergeGroupMetadata(groupId, metadata);
 			fail();
@@ -499,6 +523,13 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			// Expected
 		}
 
+		try {
+			db.setVisibleToContact(contactId, groupId, true);
+			fail();
+		} catch (NoSuchGroupException expected) {
+			// Expected
+		}
+
 		context.assertIsSatisfied();
 	}
 
@@ -847,7 +878,7 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(false));
 			oneOf(database).containsVisibleGroup(txn, contactId, groupId);
 			will(returnValue(true));
-			oneOf(database).addMessage(txn, message, UNKNOWN, true);
+			oneOf(database).addMessage(txn, message, UNKNOWN, false);
 			oneOf(database).getVisibility(txn, groupId);
 			will(returnValue(Collections.singletonList(contactId)));
 			oneOf(database).getContactIds(txn);
@@ -1019,7 +1050,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			oneOf(database).getContactIds(txn);
 			will(returnValue(both));
 			oneOf(database).removeVisibility(txn, contactId1, groupId);
-			oneOf(database).setVisibleToAll(txn, groupId, false);
 			oneOf(database).commitTransaction(txn);
 			oneOf(eventBus).broadcast(with(any(
 					GroupVisibilityUpdatedEvent.class)));
@@ -1051,7 +1081,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 			will(returnValue(both));
 			oneOf(database).getContactIds(txn);
 			will(returnValue(both));
-			oneOf(database).setVisibleToAll(txn, groupId, false);
 			oneOf(database).commitTransaction(txn);
 		}});
 		DatabaseComponent db = createDatabaseComponent(database, eventBus,
@@ -1062,55 +1091,6 @@ public class DatabaseComponentImplTest extends BriarTestCase {
 		context.assertIsSatisfied();
 	}
 
-	@Test
-	public void testSettingVisibleToAllAffectsCurrentContacts()
-			throws Exception {
-		final ContactId contactId1 = new ContactId(123);
-		final Collection<ContactId> both = Arrays.asList(contactId, contactId1);
-		Mockery context = new Mockery();
-		@SuppressWarnings("unchecked")
-		final Database<Object> database = context.mock(Database.class);
-		final ShutdownManager shutdown = context.mock(ShutdownManager.class);
-		final EventBus eventBus = context.mock(EventBus.class);
-		context.checking(new Expectations() {{
-			// setVisibility()
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsGroup(txn, groupId);
-			will(returnValue(true));
-			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Collections.emptyList()));
-			oneOf(database).getContactIds(txn);
-			will(returnValue(both));
-			oneOf(database).addVisibility(txn, contactId, groupId);
-			oneOf(database).setVisibleToAll(txn, groupId, false);
-			oneOf(database).commitTransaction(txn);
-			oneOf(eventBus).broadcast(with(any(
-					GroupVisibilityUpdatedEvent.class)));
-			// setVisibleToAll()
-			oneOf(database).startTransaction();
-			will(returnValue(txn));
-			oneOf(database).containsGroup(txn, groupId);
-			will(returnValue(true));
-			oneOf(database).setVisibleToAll(txn, groupId, true);
-			oneOf(database).getVisibility(txn, groupId);
-			will(returnValue(Collections.singletonList(contactId)));
-			oneOf(database).getContactIds(txn);
-			will(returnValue(both));
-			oneOf(database).addVisibility(txn, contactId1, groupId);
-			oneOf(database).commitTransaction(txn);
-			oneOf(eventBus).broadcast(with(any(
-					GroupVisibilityUpdatedEvent.class)));
-		}});
-		DatabaseComponent db = createDatabaseComponent(database, eventBus,
-				shutdown);
-
-		db.setVisibility(groupId, Collections.singletonList(contactId));
-		db.setVisibleToAll(groupId, true);
-
-		context.assertIsSatisfied();
-	}
-
 	@Test
 	public void testTransportKeys() throws Exception {
 		final TransportKeys keys = createTransportKeys();
diff --git a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
index cce4c883470f35b5c781ecbdb6b5ed4bf20400c5..2fe48c6c3e4f2240e5d00c09431581b400cc2882 100644
--- a/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
+++ b/briar-tests/src/org/briarproject/db/H2DatabaseTest.java
@@ -205,7 +205,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store an unvalidated message
+		// Add a contact, a group and an unvalidated message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
@@ -243,7 +243,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		Database<Connection> db = open(false);
 		Connection txn = db.startTransaction();
 
-		// Add a contact, subscribe to a group and store an unshared message
+		// Add a contact, a group and an unshared message
 		db.addLocalAuthor(txn, localAuthor);
 		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
 		db.addGroup(txn, group);
@@ -329,7 +329,7 @@ public class H2DatabaseTest extends BriarTestCase {
 		ids = db.getMessagesToOffer(txn, contactId, 100);
 		assertEquals(Collections.singletonList(messageId), ids);
 
-		// Making the subscription invisible should make the message unsendable
+		// Making the group invisible should make the message unsendable
 		db.removeVisibility(txn, contactId, groupId);
 		ids = db.getMessagesToSend(txn, contactId, ONE_MEGABYTE);
 		assertTrue(ids.isEmpty());
@@ -1017,6 +1017,31 @@ public class H2DatabaseTest extends BriarTestCase {
 		db.close();
 	}
 
+	@Test
+	public void testGroupsVisibleToContacts() throws Exception {
+		Database<Connection> db = open(false);
+		Connection txn = db.startTransaction();
+
+		// Add a contact and a group
+		db.addLocalAuthor(txn, localAuthor);
+		assertEquals(contactId, db.addContact(txn, author, localAuthorId));
+		db.addGroup(txn, group);
+
+		// The group should not be visible to the contact
+		assertFalse(db.containsVisibleGroup(txn, contactId, groupId));
+
+		// Make the group visible to the contact
+		db.addVisibility(txn, contactId, groupId);
+		assertTrue(db.containsVisibleGroup(txn, contactId, groupId));
+
+		// Make the group invisible to the contact
+		db.removeVisibility(txn, contactId, groupId);
+		assertFalse(db.containsVisibleGroup(txn, contactId, groupId));
+
+		db.commitTransaction(txn);
+		db.close();
+	}
+
 	@Test
 	public void testDifferentLocalPseudonymsCanHaveTheSameContact()
 			throws Exception {
diff --git a/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java b/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..e10623d584a3e65ed4e662d17b43b4ac672569d2
--- /dev/null
+++ b/briar-tests/src/org/briarproject/forum/ForumListValidatorTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.forum;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class ForumListValidatorTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}
diff --git a/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java b/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..07fc1ad80a77dacffcdf1093c7f09a954c1fffe1
--- /dev/null
+++ b/briar-tests/src/org/briarproject/forum/ForumSharingManagerImplTest.java
@@ -0,0 +1,14 @@
+package org.briarproject.forum;
+
+import org.briarproject.BriarTestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.fail;
+
+public class ForumSharingManagerImplTest extends BriarTestCase {
+
+	@Test
+	public void testUnitTestsExist() {
+		fail(); // FIXME: Write tests
+	}
+}