diff --git a/components/net/sf/briar/crypto/CryptoModule.java b/components/net/sf/briar/crypto/CryptoModule.java
index 1f6546a8cb2b91e72b899814875b2098a4f6a905..64efb11c12bb1062621dde37cd4d238f79db353d 100644
--- a/components/net/sf/briar/crypto/CryptoModule.java
+++ b/components/net/sf/briar/crypto/CryptoModule.java
@@ -1,6 +1,10 @@
 package net.sf.briar.crypto;
 
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
 import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.crypto.SecretStorageKey;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.Singleton;
@@ -9,6 +13,10 @@ public class CryptoModule extends AbstractModule {
 
 	@Override
 	protected void configure() {
-		bind(CryptoComponent.class).to(CryptoComponentImpl.class).in(Singleton.class);
+		bind(CryptoComponent.class).to(
+				CryptoComponentImpl.class).in(Singleton.class);
+		bind(SecretKey.class).annotatedWith(SecretStorageKey.class).toInstance(
+				new SecretKeySpec(new byte[32], "AES")); // FIXME
+				
 	}
 }
diff --git a/components/net/sf/briar/transport/ConnectionRecogniserImpl.java b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..6779c6ad720da280cd15720db6dee7d55b172280
--- /dev/null
+++ b/components/net/sf/briar/transport/ConnectionRecogniserImpl.java
@@ -0,0 +1,133 @@
+package net.sf.briar.transport;
+
+import java.security.InvalidKeyException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.SecretKey;
+
+import net.sf.briar.api.Bytes;
+import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DatabaseListener;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.db.NoSuchContactException;
+import net.sf.briar.api.transport.ConnectionRecogniser;
+import net.sf.briar.api.transport.ConnectionWindow;
+
+class ConnectionRecogniserImpl implements ConnectionRecogniser,
+DatabaseListener {
+
+	private final int transportId;
+	private final CryptoComponent crypto;
+	private final DatabaseComponent db;
+	private final Map<Bytes, ContactId> tagToContact;
+	private final Map<Bytes, Long> tagToConnectionNumber;
+	private final Map<ContactId, Map<Long, Bytes>> contactToTags;
+	private final Map<ContactId, Cipher> contactToCipher;
+	private final Map<ContactId, ConnectionWindow> contactToWindow;
+	private boolean initialised = false;
+
+	ConnectionRecogniserImpl(int transportId, CryptoComponent crypto,
+			DatabaseComponent db) {
+		this.transportId = transportId;
+		this.crypto = crypto;
+		this.db = db;
+		// FIXME: There's probably a tidier way of maintaining all this state
+		tagToContact = new HashMap<Bytes, ContactId>();
+		tagToConnectionNumber = new HashMap<Bytes, Long>();
+		contactToTags = new HashMap<ContactId, Map<Long, Bytes>>();
+		contactToCipher = new HashMap<ContactId, Cipher>();
+		contactToWindow = new HashMap<ContactId, ConnectionWindow>();
+		db.addListener(this);
+	}
+
+	private synchronized void initialise() throws DbException {
+		for(ContactId c : db.getContacts()) {
+			try {
+				// Initialise and store the contact's tag cipher
+				SecretKey tagKey = crypto.deriveTagKey(db.getSharedSecret(c));
+				Cipher cipher = crypto.getTagCipher();
+				try {
+					cipher.init(Cipher.ENCRYPT_MODE, tagKey);
+				} catch(InvalidKeyException badKey) {
+					throw new RuntimeException(badKey);
+				}
+				contactToCipher.put(c, cipher);
+				// Calculate the tags for the contact's connection window
+				ConnectionWindow w = db.getConnectionWindow(c, transportId);
+				Map<Long, Bytes> tags = new HashMap<Long, Bytes>();
+				for(Long unseen : w.getUnseenConnectionNumbers()) {
+					Bytes expectedTag = new Bytes(calculateTag(c, unseen));
+					tagToContact.put(expectedTag, c);
+					tagToConnectionNumber.put(expectedTag, unseen);
+					tags.put(unseen, expectedTag);
+				}
+				contactToTags.put(c, tags);
+				contactToWindow.put(c, w);
+			} catch(NoSuchContactException e) {
+				continue;
+			}
+		}
+		initialised = true;
+	}
+
+	private synchronized byte[] calculateTag(ContactId c,
+			long connectionNumber) {
+		byte[] tag = TagEncoder.encodeTag(transportId, connectionNumber, 0L);
+		Cipher cipher = contactToCipher.get(c);
+		assert cipher != null;
+		try {
+			return cipher.doFinal(tag);
+		} catch(BadPaddingException e) {
+			throw new RuntimeException(e);
+		} catch(IllegalBlockSizeException e) {
+			throw new RuntimeException(e);
+		}
+	}
+
+	public synchronized ContactId acceptConnection(byte[] tag)
+	throws DbException {
+		if(tag.length != 16) throw new IllegalArgumentException();
+		if(!initialised) initialise();
+		Bytes b = new Bytes(tag);
+		ContactId contactId = tagToContact.remove(b);
+		Long connectionNumber = tagToConnectionNumber.remove(b);
+		assert (contactId == null) == (connectionNumber == null);
+		if(contactId == null) return null;
+		// The tag was expected - update and save the connection window
+		ConnectionWindow w = contactToWindow.get(contactId);
+		assert w != null;
+		w.setSeen(connectionNumber);
+		db.setConnectionWindow(contactId, transportId, w);
+		// Update the set of expected tags
+		Map<Long, Bytes> oldTags = contactToTags.remove(contactId);
+		assert oldTags != null;
+		assert oldTags.containsKey(connectionNumber);
+		Map<Long, Bytes> newTags = new HashMap<Long, Bytes>();
+		for(Long unseen : w.getUnseenConnectionNumbers()) {
+			Bytes expectedTag = oldTags.get(unseen);
+			if(expectedTag == null) {
+				expectedTag = new Bytes(calculateTag(contactId, unseen));
+				tagToContact.put(expectedTag, contactId);
+				tagToConnectionNumber.put(expectedTag, connectionNumber);
+			}
+			newTags.put(unseen, expectedTag);
+		}
+		contactToTags.put(contactId, newTags);
+		return contactId;
+	}
+
+	public void eventOccurred(Event e) {
+		// When the set of contacts changes we need to re-initialise everything
+		if(e == Event.CONTACTS_UPDATED) {
+			synchronized(this) {
+				initialised = false;
+			}
+		}
+	}
+}
diff --git a/components/net/sf/briar/transport/ConnectionWindowImpl.java b/components/net/sf/briar/transport/ConnectionWindowImpl.java
index 956a5c722a072235edb610757dc22b0f0be81a48..af2c294870529366b19e3e41a1f0a761668b2090 100644
--- a/components/net/sf/briar/transport/ConnectionWindowImpl.java
+++ b/components/net/sf/briar/transport/ConnectionWindowImpl.java
@@ -7,8 +7,6 @@ import net.sf.briar.api.transport.ConnectionWindow;
 
 class ConnectionWindowImpl implements ConnectionWindow {
 
-	private static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1
-
 	private long centre;
 	private int bitmap;
 
@@ -33,7 +31,7 @@ class ConnectionWindowImpl implements ConnectionWindow {
 
 	private int getOffset(long connectionNumber) {
 		if(connectionNumber < 0L) throw new IllegalArgumentException();
-		if(connectionNumber > MAX_32_BIT_UNSIGNED)
+		if(connectionNumber > Constants.MAX_32_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
 		int offset = (int) (connectionNumber - centre) + 16;
 		if(offset < 0 || offset > 31) throw new IllegalArgumentException();
@@ -58,10 +56,11 @@ class ConnectionWindowImpl implements ConnectionWindow {
 			int mask = 0x80000000 >>> i;
 			if((bitmap & mask) == 0) {
 				long c = centre - 16 + i;
-				if(c >= 0L && c <= MAX_32_BIT_UNSIGNED) unseen.add(c);
+				if(c >= 0L && c <= Constants.MAX_32_BIT_UNSIGNED) unseen.add(c);
 			}
 		}
-		assert unseen.contains(centre) || centre == MAX_32_BIT_UNSIGNED + 1;
+		assert unseen.contains(centre)
+		|| centre == Constants.MAX_32_BIT_UNSIGNED + 1;
 		return unseen;
 	}
 }
diff --git a/components/net/sf/briar/transport/Constants.java b/components/net/sf/briar/transport/Constants.java
new file mode 100644
index 0000000000000000000000000000000000000000..07c96eaa065c3032cb01f007fb2a4d0d882acf80
--- /dev/null
+++ b/components/net/sf/briar/transport/Constants.java
@@ -0,0 +1,8 @@
+package net.sf.briar.transport;
+
+interface Constants {
+
+	static final int MAX_16_BIT_UNSIGNED = 65535; // 2^16 - 1
+	static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1
+
+}
diff --git a/components/net/sf/briar/transport/PacketWriterImpl.java b/components/net/sf/briar/transport/PacketWriterImpl.java
index c0f66d40e675e524a4c18d9d822394add76505cd..97c9626aa704a72c90abb3bc6cb6e699dfea696c 100644
--- a/components/net/sf/briar/transport/PacketWriterImpl.java
+++ b/components/net/sf/briar/transport/PacketWriterImpl.java
@@ -10,9 +10,6 @@ import net.sf.briar.api.transport.PacketWriter;
 
 class PacketWriterImpl extends FilterOutputStream implements PacketWriter {
 
-	private static final int MAX_16_BIT_UNSIGNED = 65535; // 2^16 - 1
-	private static final long MAX_32_BIT_UNSIGNED = 4294967295L; // 2^32 - 1
-
 	private final PacketEncrypter encrypter;
 	private final Mac mac;
 	private final int transportIdentifier;
@@ -27,11 +24,11 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter {
 		this.encrypter = encrypter;
 		this.mac = mac;
 		if(transportIdentifier < 0) throw new IllegalArgumentException();
-		if(transportIdentifier > MAX_16_BIT_UNSIGNED)
+		if(transportIdentifier > Constants.MAX_16_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
 		this.transportIdentifier = transportIdentifier;
 		if(connectionNumber < 0L) throw new IllegalArgumentException();
-		if(connectionNumber > MAX_32_BIT_UNSIGNED)
+		if(connectionNumber > Constants.MAX_32_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
 		this.connectionNumber = connectionNumber;
 	}
@@ -72,39 +69,14 @@ class PacketWriterImpl extends FilterOutputStream implements PacketWriter {
 	}
 
 	private void writeTag() throws IOException {
-		if(packetNumber > MAX_32_BIT_UNSIGNED)
+		if(packetNumber > Constants.MAX_32_BIT_UNSIGNED)
 			throw new IllegalStateException();
-		byte[] tag = new byte[16];
-		// Encode the transport identifier as an unsigned 16-bit integer
-		writeUint16(transportIdentifier, tag, 2);
-		// Encode the connection number as an unsigned 32-bit integer
-		writeUint32(connectionNumber, tag, 4);
-		// Encode the packet number as an unsigned 32-bit integer
-		writeUint32(packetNumber, tag, 8);
+		byte[] tag = TagEncoder.encodeTag(transportIdentifier, connectionNumber,
+				packetNumber);
 		// Write the tag to the encrypter and start calculating the MAC
 		encrypter.writeTag(tag);
 		mac.update(tag);
 		packetNumber++;
 		betweenPackets = false;
 	}
-
-	// Package access for testing
-	static void writeUint16(int i, byte[] b, int offset) {
-		assert i >= 0;
-		assert i <= MAX_16_BIT_UNSIGNED;
-		assert b.length >= offset + 2;
-		b[offset] = (byte) (i >> 8);
-		b[offset + 1] = (byte) (i & 0xFF);
-	}
-
-	// Package access for testing
-	static void writeUint32(long i, byte[] b, int offset) {
-		assert i >= 0L;
-		assert i <= MAX_32_BIT_UNSIGNED;
-		assert b.length >= offset + 4;
-		b[offset] = (byte) (i >> 24);
-		b[offset + 1] = (byte) (i >> 16 & 0xFF);
-		b[offset + 2] = (byte) (i >> 8 & 0xFF);
-		b[offset + 3] = (byte) (i & 0xFF);
-	}
 }
diff --git a/components/net/sf/briar/transport/TagEncoder.java b/components/net/sf/briar/transport/TagEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..aead6debbf2de9d251b8091ba03d4822e9c3f1f1
--- /dev/null
+++ b/components/net/sf/briar/transport/TagEncoder.java
@@ -0,0 +1,36 @@
+package net.sf.briar.transport;
+
+public class TagEncoder {
+
+	static byte[] encodeTag(int transportIdentifier, long connectionNumber,
+			long packetNumber) {
+		byte[] tag = new byte[16];
+		// Encode the transport identifier as an unsigned 16-bit integer
+		writeUint16(transportIdentifier, tag, 2);
+		// Encode the connection number as an unsigned 32-bit integer
+		writeUint32(connectionNumber, tag, 4);
+		// Encode the packet number as an unsigned 32-bit integer
+		writeUint32(packetNumber, tag, 8);
+		return tag;
+	}
+
+	// Package access for testing
+	static void writeUint16(int i, byte[] b, int offset) {
+		assert i >= 0;
+		assert i <= Constants.MAX_16_BIT_UNSIGNED;
+		assert b.length >= offset + 2;
+		b[offset] = (byte) (i >> 8);
+		b[offset + 1] = (byte) (i & 0xFF);
+	}
+
+	// Package access for testing
+	static void writeUint32(long i, byte[] b, int offset) {
+		assert i >= 0L;
+		assert i <= Constants.MAX_32_BIT_UNSIGNED;
+		assert b.length >= offset + 4;
+		b[offset] = (byte) (i >> 24);
+		b[offset + 1] = (byte) (i >> 16 & 0xFF);
+		b[offset + 2] = (byte) (i >> 8 & 0xFF);
+		b[offset + 3] = (byte) (i & 0xFF);
+	}
+}
diff --git a/test/build.xml b/test/build.xml
index f034b76f42367c161cb89c61bd5cf945672b6a6c..2791cc2a029c6e1bb278c112185650a450cd6bae 100644
--- a/test/build.xml
+++ b/test/build.xml
@@ -35,6 +35,7 @@
 			<test name='net.sf.briar.transport.ConnectionWindowImplTest'/>
 			<test name='net.sf.briar.transport.PacketEncrypterImplTest'/>
 			<test name='net.sf.briar.transport.PacketWriterImplTest'/>
+			<test name='net.sf.briar.transport.TagEncoderTest'/>
 			<test name='net.sf.briar.util.FileUtilsTest'/>
 			<test name='net.sf.briar.util.StringUtilsTest'/>
 			<test name='net.sf.briar.util.ZipUtilsTest'/>
diff --git a/test/net/sf/briar/transport/PacketWriterImplTest.java b/test/net/sf/briar/transport/PacketWriterImplTest.java
index 9bf4ee1166479c76067aa9a0fc35e8f5894a15a7..94783c7a28cd87bb7ebe2c2ef5b5ab038f568c68 100644
--- a/test/net/sf/briar/transport/PacketWriterImplTest.java
+++ b/test/net/sf/briar/transport/PacketWriterImplTest.java
@@ -140,28 +140,6 @@ public class PacketWriterImplTest extends TestCase {
 		assertTrue(Arrays.equals(expectedMac1, actualMac1));
 	}
 
-	@Test
-	public void testWriteUint16() throws Exception {
-		byte[] b = new byte[3];
-		PacketWriterImpl.writeUint16(0, b, 1);
-		assertEquals("000000", StringUtils.toHexString(b));
-		PacketWriterImpl.writeUint16(1, b, 1);
-		assertEquals("000001", StringUtils.toHexString(b));
-		PacketWriterImpl.writeUint16(65535, b, 1);
-		assertEquals("00FFFF", StringUtils.toHexString(b));
-	}
-
-	@Test
-	public void testWriteUint32() throws Exception {
-		byte[] b = new byte[5];
-		PacketWriterImpl.writeUint32(0L, b, 1);
-		assertEquals("0000000000", StringUtils.toHexString(b));
-		PacketWriterImpl.writeUint32(1L, b, 1);
-		assertEquals("0000000001", StringUtils.toHexString(b));
-		PacketWriterImpl.writeUint32(4294967295L, b, 1);
-		assertEquals("00FFFFFFFF", StringUtils.toHexString(b));
-	}
-
 	private static class NullPacketEncrypter implements PacketEncrypter {
 
 		private final OutputStream out;
diff --git a/test/net/sf/briar/transport/TagEncoderTest.java b/test/net/sf/briar/transport/TagEncoderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0f81d84a1a4d87b02a4047193b0920d445f76a15
--- /dev/null
+++ b/test/net/sf/briar/transport/TagEncoderTest.java
@@ -0,0 +1,32 @@
+package net.sf.briar.transport;
+
+import net.sf.briar.util.StringUtils;
+
+import org.junit.Test;
+
+import junit.framework.TestCase;
+
+public class TagEncoderTest extends TestCase {
+
+	@Test
+	public void testWriteUint16() throws Exception {
+		byte[] b = new byte[3];
+		TagEncoder.writeUint16(0, b, 1);
+		assertEquals("000000", StringUtils.toHexString(b));
+		TagEncoder.writeUint16(1, b, 1);
+		assertEquals("000001", StringUtils.toHexString(b));
+		TagEncoder.writeUint16(65535, b, 1);
+		assertEquals("00FFFF", StringUtils.toHexString(b));
+	}
+
+	@Test
+	public void testWriteUint32() throws Exception {
+		byte[] b = new byte[5];
+		TagEncoder.writeUint32(0L, b, 1);
+		assertEquals("0000000000", StringUtils.toHexString(b));
+		TagEncoder.writeUint32(1L, b, 1);
+		assertEquals("0000000001", StringUtils.toHexString(b));
+		TagEncoder.writeUint32(4294967295L, b, 1);
+		assertEquals("00FFFFFFFF", StringUtils.toHexString(b));
+	}
+}