diff --git a/api/net/sf/briar/api/crypto/CryptoComponent.java b/api/net/sf/briar/api/crypto/CryptoComponent.java
index 6801a40ed71e75d2a4c98bf856bd0f23305b58d2..70fe01c927ebd1ba3bd78d55b8d7df413706c8ef 100644
--- a/api/net/sf/briar/api/crypto/CryptoComponent.java
+++ b/api/net/sf/briar/api/crypto/CryptoComponent.java
@@ -15,6 +15,11 @@ public interface CryptoComponent {
 
 	ErasableKey deriveMacKey(byte[] secret, boolean initiator);
 
+	byte[][] deriveInitialSecrets(byte[] theirPublicKey, KeyPair ourKeyPair,
+			int invitationCode, boolean initiator);
+
+	int deriveConfirmationCode(byte[] secret, boolean initiator);
+
 	byte[] deriveNextSecret(byte[] secret, int index, long connection);
 
 	KeyPair generateKeyPair();
@@ -25,6 +30,8 @@ public interface CryptoComponent {
 
 	MessageDigest getMessageDigest();
 
+	PseudoRandom getPseudoRandom(int seed);
+
 	SecureRandom getSecureRandom();
 
 	Cipher getTagCipher();
diff --git a/api/net/sf/briar/api/crypto/PseudoRandom.java b/api/net/sf/briar/api/crypto/PseudoRandom.java
new file mode 100644
index 0000000000000000000000000000000000000000..6abca7c506ed2e559f843c7e87787f91ce076a68
--- /dev/null
+++ b/api/net/sf/briar/api/crypto/PseudoRandom.java
@@ -0,0 +1,6 @@
+package net.sf.briar.api.crypto;
+
+public interface PseudoRandom {
+
+	byte[] nextBytes(int bytes);
+}
diff --git a/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java b/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..64199a59c1e8e43357d0f7d7ff172898fec2e8a0
--- /dev/null
+++ b/api/net/sf/briar/api/plugins/IncomingInvitationCallback.java
@@ -0,0 +1,6 @@
+package net.sf.briar.api.plugins;
+
+public interface IncomingInvitationCallback extends InvitationCallback {
+
+	int enterInvitationCode();
+}
diff --git a/api/net/sf/briar/api/plugins/InvitationCallback.java b/api/net/sf/briar/api/plugins/InvitationCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..fb37cf40dd6b840d805d0f4c750f53218d31611c
--- /dev/null
+++ b/api/net/sf/briar/api/plugins/InvitationCallback.java
@@ -0,0 +1,14 @@
+package net.sf.briar.api.plugins;
+
+public interface InvitationCallback {
+
+	boolean isCancelled();
+
+	int enterConfirmationCode(int code);
+
+	void showProgress(String... message);
+
+	void showFailure(String... message);
+
+	void showSuccess();
+}
diff --git a/api/net/sf/briar/api/plugins/InvitationConstants.java b/api/net/sf/briar/api/plugins/InvitationConstants.java
new file mode 100644
index 0000000000000000000000000000000000000000..e1c113180173381f0e5c2842a4d63bdd3eb2b72d
--- /dev/null
+++ b/api/net/sf/briar/api/plugins/InvitationConstants.java
@@ -0,0 +1,14 @@
+package net.sf.briar.api.plugins;
+
+public interface InvitationConstants {
+
+	static final long INVITATION_TIMEOUT = 60 * 1000; // 1 minute
+
+	static final int CODE_BITS = 19; // Codes must fit into six decimal digits
+
+	static final int MAX_CODE = 1 << CODE_BITS - 1;
+
+	static final int HASH_LENGTH = 48;
+
+	static final int MAX_PUBLIC_KEY_LENGTH = 120;
+}
diff --git a/api/net/sf/briar/api/plugins/InvitationStarter.java b/api/net/sf/briar/api/plugins/InvitationStarter.java
new file mode 100644
index 0000000000000000000000000000000000000000..92c9acd65ade2b900bd8ee7808af5b4dc3d54e51
--- /dev/null
+++ b/api/net/sf/briar/api/plugins/InvitationStarter.java
@@ -0,0 +1,12 @@
+package net.sf.briar.api.plugins;
+
+import net.sf.briar.api.plugins.duplex.DuplexPlugin;
+
+public interface InvitationStarter {
+
+	void startIncomingInvitation(DuplexPlugin plugin,
+			IncomingInvitationCallback callback);
+
+	void startOutgoingInvitation(DuplexPlugin plugin,
+			OutgoingInvitationCallback callback);
+}
diff --git a/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java b/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java
new file mode 100644
index 0000000000000000000000000000000000000000..b80e82bc7c3beafd349d3e3deceb7962cebb0fb7
--- /dev/null
+++ b/api/net/sf/briar/api/plugins/OutgoingInvitationCallback.java
@@ -0,0 +1,6 @@
+package net.sf.briar.api.plugins;
+
+public interface OutgoingInvitationCallback extends InvitationCallback {
+
+	void showInvitationCode(int code);
+}
diff --git a/api/net/sf/briar/api/plugins/PluginManager.java b/api/net/sf/briar/api/plugins/PluginManager.java
index b2dbd44ae00ccffd22dee53f7f383354f63f0422..17887added8925eb1a738e5657fdc34075d9f9f8 100644
--- a/api/net/sf/briar/api/plugins/PluginManager.java
+++ b/api/net/sf/briar/api/plugins/PluginManager.java
@@ -1,5 +1,10 @@
 package net.sf.briar.api.plugins;
 
+import java.util.Collection;
+
+import net.sf.briar.api.plugins.duplex.DuplexPlugin;
+import net.sf.briar.api.plugins.simplex.SimplexPlugin;
+
 public interface PluginManager {
 
 	/**
@@ -12,4 +17,10 @@ public interface PluginManager {
 	 * Stops the plugins and returns the number of plugins successfully stopped.
 	 */
 	int stop();
+
+	/** Returns any duplex plugins that support invitations. */
+	Collection<DuplexPlugin> getDuplexInvitationPlugins();
+
+	/** Returns any simplex plugins that support invitations. */
+	Collection<SimplexPlugin> getSimplexInvitationPlugins();
 }
diff --git a/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java b/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java
index fed6e5fd63d822d8c3f2f5f7d61e304f55332124..2de756dfaeb870ed01443ed3fd012f7337efa329 100644
--- a/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java
+++ b/api/net/sf/briar/api/plugins/duplex/DuplexPlugin.java
@@ -1,6 +1,7 @@
 package net.sf.briar.api.plugins.duplex;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.Plugin;
 
 /** An interface for transport plugins that support duplex communication. */
@@ -17,11 +18,11 @@ public interface DuplexPlugin extends Plugin {
 	 * Starts the invitation process from the inviter's side. Returns null if
 	 * no connection can be established within the given timeout.
 	 */
-	DuplexTransportConnection sendInvitation(int code, long timeout);
+	DuplexTransportConnection sendInvitation(PseudoRandom r, long timeout);
 
 	/**
 	 * Starts the invitation process from the invitee's side. Returns null if
 	 * no connection can be established within the given timeout.
 	 */
-	DuplexTransportConnection acceptInvitation(int code, long timeout);
+	DuplexTransportConnection acceptInvitation(PseudoRandom r, long timeout);
 }
diff --git a/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java b/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java
index 4b06f54f3b0fb9a033888450cfa3240ba2778d88..fd11d6646c3f6802b1d5c608de4577fdcf50b1cc 100644
--- a/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java
+++ b/api/net/sf/briar/api/plugins/simplex/SimplexPlugin.java
@@ -1,6 +1,7 @@
 package net.sf.briar.api.plugins.simplex;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.Plugin;
 
 /** An interface for transport plugins that support simplex communication. */
@@ -24,23 +25,24 @@ public interface SimplexPlugin extends Plugin {
 	 * Starts the invitation process from the inviter's side. Returns null if
 	 * no connection can be established within the given timeout.
 	 */
-	SimplexTransportWriter sendInvitation(int code, long timeout);
+	SimplexTransportWriter sendInvitation(PseudoRandom r, long timeout);
 
 	/**
 	 * Starts the invitation process from the invitee's side. Returns null if
 	 * no connection can be established within the given timeout.
 	 */
-	SimplexTransportReader acceptInvitation(int code, long timeout);
+	SimplexTransportReader acceptInvitation(PseudoRandom r, long timeout);
 
 	/**
 	 * Continues the invitation process from the invitee's side. Returns null
 	 * if no connection can be established within the given timeout.
 	 */
-	SimplexTransportWriter sendInvitationResponse(int code, long timeout);
+	SimplexTransportWriter sendInvitationResponse(PseudoRandom r, long timeout);
 
 	/**
 	 * Continues the invitation process from the inviter's side. Returns null
 	 * if no connection can be established within the given timeout.
 	 */
-	SimplexTransportReader acceptInvitationResponse(int code, long timeout);
+	SimplexTransportReader acceptInvitationResponse(PseudoRandom r,
+			long timeout);
 }
diff --git a/components/net/sf/briar/crypto/CryptoComponentImpl.java b/components/net/sf/briar/crypto/CryptoComponentImpl.java
index 11d525bb5041154ad32e13efc7137c8ead371978..7eadb47cb1d0ed49e1cbbc9c92b381f4fb7fd50d 100644
--- a/components/net/sf/briar/crypto/CryptoComponentImpl.java
+++ b/components/net/sf/briar/crypto/CryptoComponentImpl.java
@@ -1,13 +1,17 @@
 package net.sf.briar.crypto;
 
+import static net.sf.briar.api.plugins.InvitationConstants.CODE_BITS;
+
 import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
+import java.security.PublicKey;
 import java.security.SecureRandom;
 import java.security.Security;
 import java.security.Signature;
 
 import javax.crypto.Cipher;
+import javax.crypto.KeyAgreement;
 import javax.crypto.Mac;
 import javax.crypto.spec.IvParameterSpec;
 
@@ -15,6 +19,7 @@ import net.sf.briar.api.crypto.CryptoComponent;
 import net.sf.briar.api.crypto.ErasableKey;
 import net.sf.briar.api.crypto.KeyParser;
 import net.sf.briar.api.crypto.MessageDigest;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.util.ByteUtils;
 
 import org.bouncycastle.jce.provider.BouncyCastleProvider;
@@ -26,6 +31,7 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final String PROVIDER = "BC";
 	private static final String KEY_PAIR_ALGO = "ECDSA";
 	private static final int KEY_PAIR_BITS = 384;
+	private static final String KEY_AGREEMENT_ALGO = "ECDHC";
 	private static final String SECRET_KEY_ALGO = "AES";
 	private static final int SECRET_KEY_BYTES = 32; // 256 bits
 	private static final int KEY_DERIVATION_IV_BYTES = 16; // 128 bits
@@ -40,13 +46,17 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final byte[] TAG = { 'T', 'A', 'G', 0 };
 	private static final byte[] FRAME = { 'F', 'R', 'A', 'M', 'E', 0 };
 	private static final byte[] MAC = { 'M', 'A', 'C', 0 };
+	// Labels for secret derivation, null-terminated
+	private static final byte[] FIRST = { 'F', 'I', 'R', 'S', 'T', 0 };
 	private static final byte[] NEXT = { 'N', 'E', 'X', 'T', 0 };
-	// Context strings for key derivation
+	// Label for confirmation code derivation, null-terminated
+	private static final byte[] CODE = { 'C', 'O', 'D', 'E', 0 };
+	// Context strings for key and confirmation code derivation
 	private static final byte[] INITIATOR = { 'I' };
 	private static final byte[] RESPONDER = { 'R' };
 	// Blank plaintext for key derivation
 	private static final byte[] KEY_DERIVATION_INPUT =
-		new byte[SECRET_KEY_BYTES];
+			new byte[SECRET_KEY_BYTES];
 
 	private final KeyParser keyParser;
 	private final KeyPairGenerator keyPairGenerator;
@@ -114,6 +124,72 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
+	public byte[][] deriveInitialSecrets(byte[] theirPublicKey,
+			KeyPair ourKeyPair, int invitationCode, boolean initiator) {
+		try {
+			PublicKey theirPublic = keyParser.parsePublicKey(theirPublicKey);
+			MessageDigest messageDigest = getMessageDigest();
+			byte[] ourPublicKey = ourKeyPair.getPublic().getEncoded();
+			byte[] ourHash = messageDigest.digest(ourPublicKey);
+			byte[] theirHash = messageDigest.digest(theirPublicKey);
+			// The initiator and responder info for the KDF are the hashes of
+			// the corresponding public keys
+			byte[] initiatorInfo, responderInfo;
+			if(initiator) {
+				initiatorInfo = ourHash;
+				responderInfo = theirHash;
+			} else {
+				initiatorInfo = theirHash;
+				responderInfo = ourHash;
+			}
+			// The public info for the KDF is the invitation code as a uint32
+			byte[] publicInfo = new byte[4];
+			ByteUtils.writeUint32(invitationCode, publicInfo, 0);
+			// The raw secret comes from the key agreement algorithm
+			KeyAgreement keyAgreement = KeyAgreement.getInstance(
+					KEY_AGREEMENT_ALGO, PROVIDER);
+			keyAgreement.init(ourKeyPair.getPrivate());
+			keyAgreement.doPhase(theirPublic, true);
+			byte[] rawSecret = keyAgreement.generateSecret();
+			// Derive the cooked secret from the raw secret
+			byte[] cookedSecret = concatenationKdf(rawSecret, FIRST,
+					initiatorInfo, responderInfo, publicInfo);
+			ByteUtils.erase(rawSecret);
+			// Derive the incoming and outgoing secrets from the cooked secret
+			byte[][] secrets = new byte[2][];
+			secrets[0] = counterModeKdf(cookedSecret, FIRST, INITIATOR);
+			secrets[1] = counterModeKdf(cookedSecret, FIRST, RESPONDER);
+			ByteUtils.erase(cookedSecret);
+			return secrets;
+		} catch(GeneralSecurityException e) {
+			return null;
+		}
+	}
+
+	// Key derivation function based on a hash function - see NIST SP 800-65A,
+	// section 5.8
+	private byte[] concatenationKdf(byte[] rawSecret, byte[] label,
+			byte[] initiatorInfo, byte[] responderInfo, byte[] publicInfo) {
+		// The output of the hash function must be long enough to use as a key
+		MessageDigest messageDigest = getMessageDigest();
+		if(messageDigest.getDigestLength() < SECRET_KEY_BYTES)
+			throw new RuntimeException();
+		byte[] rawSecretLength = new byte[4];
+		ByteUtils.writeUint32(rawSecret.length, rawSecretLength, 0);
+		messageDigest.update(rawSecretLength);
+		messageDigest.update(rawSecret);
+		messageDigest.update(label);
+		messageDigest.update(initiatorInfo);
+		messageDigest.update(responderInfo);
+		messageDigest.update(publicInfo);
+		byte[] hash = messageDigest.digest();
+		// The secret is the first SECRET_KEY_BYTES bytes of the hash
+		byte[] output = new byte[SECRET_KEY_BYTES];
+		System.arraycopy(hash, 0, output, 0, SECRET_KEY_BYTES);
+		ByteUtils.erase(hash);
+		return output;
+	}
+
 	public byte[] deriveNextSecret(byte[] secret, int index, long connection) {
 		if(index < 0 || index > ByteUtils.MAX_16_BIT_UNSIGNED)
 			throw new IllegalArgumentException();
@@ -125,6 +201,19 @@ class CryptoComponentImpl implements CryptoComponent {
 		return counterModeKdf(secret, NEXT, context);
 	}
 
+	public int deriveConfirmationCode(byte[] secret, boolean initiator) {
+		byte[] context = initiator ? INITIATOR : RESPONDER;
+		byte[] output = counterModeKdf(secret, CODE, context);
+		int code = extractCode(output);
+		ByteUtils.erase(output);
+		return code;
+	}
+
+	private int extractCode(byte[] secret) {
+		// Convert the first CODE_BITS bits of the secret into an unsigned int
+		return ByteUtils.readUint(secret, CODE_BITS);
+	}
+
 	public KeyPair generateKeyPair() {
 		return keyPairGenerator.generateKeyPair();
 	}
@@ -148,6 +237,10 @@ class CryptoComponentImpl implements CryptoComponent {
 		}
 	}
 
+	public PseudoRandom getPseudoRandom(int seed) {
+		return new PseudoRandomImpl(getMessageDigest(), seed);
+	}
+
 	public SecureRandom getSecureRandom() {
 		return secureRandom;
 	}
diff --git a/components/net/sf/briar/crypto/PseudoRandomImpl.java b/components/net/sf/briar/crypto/PseudoRandomImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..1f74586e4a829e9e58cc117ec3132c1b07dcd5fc
--- /dev/null
+++ b/components/net/sf/briar/crypto/PseudoRandomImpl.java
@@ -0,0 +1,40 @@
+package net.sf.briar.crypto;
+
+import net.sf.briar.api.crypto.MessageDigest;
+import net.sf.briar.api.crypto.PseudoRandom;
+import net.sf.briar.util.ByteUtils;
+
+class PseudoRandomImpl implements PseudoRandom {
+
+	private final MessageDigest messageDigest;
+
+	private byte[] state;
+	private int offset;
+
+	PseudoRandomImpl(MessageDigest messageDigest, int seed) {
+		this.messageDigest = messageDigest;
+		byte[] seedBytes = new byte[4];
+		ByteUtils.writeUint32(seed, seedBytes, 0);
+		messageDigest.update(seedBytes);
+		state = messageDigest.digest();
+		offset = 0;
+	}
+
+	public synchronized byte[] nextBytes(int bytes) {
+		byte[] b = new byte[bytes];
+		int half = state.length / 2;
+		int off = 0, len = b.length, available = half - offset;
+		while(available < len) {
+			System.arraycopy(state, offset, b, off, available);
+			off += available;
+			len -= available;
+			messageDigest.update(state, half, half);
+			state = messageDigest.digest();
+			offset = 0;
+			available = half;
+		}
+		System.arraycopy(state, offset, b, off, len);
+		offset += len;
+		return b;
+	}
+}
diff --git a/components/net/sf/briar/plugins/InvitationStarterImpl.java b/components/net/sf/briar/plugins/InvitationStarterImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..b61f1e2f4eb48e1c3e61ef987770f9b44ff0a961
--- /dev/null
+++ b/components/net/sf/briar/plugins/InvitationStarterImpl.java
@@ -0,0 +1,225 @@
+package net.sf.briar.plugins;
+
+import static net.sf.briar.api.plugins.InvitationConstants.HASH_LENGTH;
+import static net.sf.briar.api.plugins.InvitationConstants.INVITATION_TIMEOUT;
+import static net.sf.briar.api.plugins.InvitationConstants.MAX_CODE;
+import static net.sf.briar.api.plugins.InvitationConstants.MAX_PUBLIC_KEY_LENGTH;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.concurrent.Executor;
+
+import javax.inject.Inject;
+
+import net.sf.briar.api.crypto.CryptoComponent;
+import net.sf.briar.api.crypto.MessageDigest;
+import net.sf.briar.api.crypto.PseudoRandom;
+import net.sf.briar.api.db.DatabaseComponent;
+import net.sf.briar.api.db.DbException;
+import net.sf.briar.api.plugins.IncomingInvitationCallback;
+import net.sf.briar.api.plugins.InvitationStarter;
+import net.sf.briar.api.plugins.OutgoingInvitationCallback;
+import net.sf.briar.api.plugins.PluginExecutor;
+import net.sf.briar.api.plugins.duplex.DuplexPlugin;
+import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
+import net.sf.briar.api.serial.Reader;
+import net.sf.briar.api.serial.ReaderFactory;
+import net.sf.briar.api.serial.Writer;
+import net.sf.briar.api.serial.WriterFactory;
+import net.sf.briar.util.ByteUtils;
+
+// FIXME: Refactor this class to remove duplicated code
+class InvitationStarterImpl implements InvitationStarter {
+
+	private static final String TIMED_OUT = "INVITATION_TIMED_OUT";
+	private static final String IO_EXCEPTION = "INVITATION_IO_EXCEPTION";
+	private static final String INVALID_KEY = "INVITATION_INVALID_KEY";
+	private static final String WRONG_CODE = "INVITATION_WRONG_CODE";
+	private static final String DB_EXCEPTION = "INVITATION_DB_EXCEPTION";
+
+	private final Executor pluginExecutor;
+	private final CryptoComponent crypto;
+	private final DatabaseComponent db;
+	private final ReaderFactory readerFactory;
+	private final WriterFactory writerFactory;
+
+	@Inject
+	InvitationStarterImpl(@PluginExecutor Executor pluginExecutor,
+			CryptoComponent crypto, DatabaseComponent db,
+			ReaderFactory readerFactory, WriterFactory writerFactory) {
+		this.pluginExecutor = pluginExecutor;
+		this.crypto = crypto;
+		this.db = db;
+		this.readerFactory = readerFactory;
+		this.writerFactory = writerFactory;
+	}
+
+	public void startIncomingInvitation(final DuplexPlugin plugin,
+			final IncomingInvitationCallback callback) {
+		pluginExecutor.execute(new Runnable() {
+			public void run() {
+				long end = System.currentTimeMillis() + INVITATION_TIMEOUT;
+				// Get the invitation code from the inviter
+				int code = callback.enterInvitationCode();
+				if(code == -1) return;
+				long remaining = end - System.currentTimeMillis();
+				if(remaining <= 0) return;
+				// Use the invitation code to seed the PRNG
+				PseudoRandom r = crypto.getPseudoRandom(code);
+				// Connect to the inviter
+				DuplexTransportConnection conn = plugin.acceptInvitation(r,
+						remaining);
+				if(callback.isCancelled()) {
+					if(conn != null) conn.dispose(false, false);
+					return;
+				}
+				if(conn == null) {
+					callback.showFailure(TIMED_OUT);
+					return;
+				}
+				KeyPair ourKeyPair = crypto.generateKeyPair();
+				MessageDigest messageDigest = crypto.getMessageDigest();
+				byte[] ourKey = ourKeyPair.getPublic().getEncoded();
+				byte[] ourHash = messageDigest.digest(ourKey);
+				byte[] theirKey, theirHash;
+				try {
+					// Send the public key hash
+					OutputStream out = conn.getOutputStream();
+					Writer writer = writerFactory.createWriter(out);
+					writer.writeBytes(ourHash);
+					out.flush();
+					// Receive the public key hash
+					InputStream in = conn.getInputStream();
+					Reader reader = readerFactory.createReader(in);
+					theirHash = reader.readBytes(HASH_LENGTH);
+					// Send the public key
+					writer.writeBytes(ourKey);
+					out.flush();
+					// Receive the public key
+					theirKey = reader.readBytes(MAX_PUBLIC_KEY_LENGTH);
+				} catch(IOException e) {
+					conn.dispose(true, false);
+					callback.showFailure(IO_EXCEPTION);
+					return;
+				}
+				conn.dispose(false, false);
+				if(callback.isCancelled()) return;
+				// Check that the received hash matches the received key
+				if(!Arrays.equals(theirHash, messageDigest.digest(theirKey))) {
+					callback.showFailure(INVALID_KEY);
+					return;
+				}
+				// Derive the initial shared secrets and the confirmation codes
+				byte[][] secrets = crypto.deriveInitialSecrets(theirKey,
+						ourKeyPair, code, false);
+				if(secrets == null) {
+					callback.showFailure(INVALID_KEY);
+					return;
+				}
+				int theirCode = crypto.deriveConfirmationCode(secrets[0], true);
+				int ourCode = crypto.deriveConfirmationCode(secrets[1], false);
+				// Compare the confirmation codes
+				if(callback.enterConfirmationCode(ourCode) != theirCode) {
+					callback.showFailure(WRONG_CODE);
+					ByteUtils.erase(secrets[0]);
+					ByteUtils.erase(secrets[1]);
+					return;
+				}
+				// Add the contact to the database
+				try {
+					db.addContact(secrets[0], secrets[1]);
+				} catch(DbException e) {
+					callback.showFailure(DB_EXCEPTION);
+					ByteUtils.erase(secrets[0]);
+					ByteUtils.erase(secrets[1]);
+					return;
+				}
+				callback.showSuccess();
+			}
+		});
+	}
+
+	public void startOutgoingInvitation(final DuplexPlugin plugin,
+			final OutgoingInvitationCallback callback) {
+		pluginExecutor.execute(new Runnable() {
+			public void run() {
+				// Generate an invitation code and use it to seed the PRNG
+				int code = crypto.getSecureRandom().nextInt(MAX_CODE + 1);
+				PseudoRandom r = crypto.getPseudoRandom(code);
+				// Connect to the invitee
+				DuplexTransportConnection conn = plugin.sendInvitation(r,
+						INVITATION_TIMEOUT);
+				if(callback.isCancelled()) {
+					if(conn != null) conn.dispose(false, false);
+					return;
+				}
+				if(conn == null) {
+					callback.showFailure(TIMED_OUT);
+					return;
+				}
+				KeyPair ourKeyPair = crypto.generateKeyPair();
+				MessageDigest messageDigest = crypto.getMessageDigest();
+				byte[] ourKey = ourKeyPair.getPublic().getEncoded();
+				byte[] ourHash = messageDigest.digest(ourKey);
+				byte[] theirKey, theirHash;
+				try {
+					// Receive the public key hash
+					InputStream in = conn.getInputStream();
+					Reader reader = readerFactory.createReader(in);
+					theirHash = reader.readBytes(HASH_LENGTH);
+					// Send the public key hash
+					OutputStream out = conn.getOutputStream();
+					Writer writer = writerFactory.createWriter(out);
+					writer.writeBytes(ourHash);
+					out.flush();
+					// Receive the public key
+					theirKey = reader.readBytes(MAX_PUBLIC_KEY_LENGTH);
+					// Send the public key
+					writer.writeBytes(ourKey);
+					out.flush();
+				} catch(IOException e) {
+					conn.dispose(true, false);
+					callback.showFailure(IO_EXCEPTION);
+					return;
+				}
+				conn.dispose(false, false);
+				if(callback.isCancelled()) return;
+				// Check that the received hash matches the received key
+				if(!Arrays.equals(theirHash, messageDigest.digest(theirKey))) {
+					callback.showFailure(INVALID_KEY);
+					return;
+				}
+				// Derive the shared secret and the confirmation codes
+				byte[][] secrets = crypto.deriveInitialSecrets(theirKey,
+						ourKeyPair, code, true);
+				if(secrets == null) {
+					callback.showFailure(INVALID_KEY);
+					return;
+				}
+				int ourCode = crypto.deriveConfirmationCode(secrets[0], true);
+				int theirCode = crypto.deriveConfirmationCode(secrets[1],
+						false);
+				// Compare the confirmation codes
+				if(callback.enterConfirmationCode(ourCode) != theirCode) {
+					callback.showFailure(WRONG_CODE);
+					ByteUtils.erase(secrets[0]);
+					ByteUtils.erase(secrets[1]);
+					return;
+				}
+				// Add the contact to the database
+				try {
+					db.addContact(secrets[1], secrets[0]);
+				} catch(DbException e) {
+					callback.showFailure(DB_EXCEPTION);
+					ByteUtils.erase(secrets[0]);
+					ByteUtils.erase(secrets[1]);
+					return;
+				}
+				callback.showSuccess();
+			}
+		});
+	}
+}
diff --git a/components/net/sf/briar/plugins/PluginManagerImpl.java b/components/net/sf/briar/plugins/PluginManagerImpl.java
index 0b1b5532955eb9c5dfc0b4baf005337aab7fbe94..ab05b6de48f47c5fe0bec1a36ec412a67c3f0c4e 100644
--- a/components/net/sf/briar/plugins/PluginManagerImpl.java
+++ b/components/net/sf/briar/plugins/PluginManagerImpl.java
@@ -2,6 +2,7 @@ package net.sf.briar.plugins;
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -40,7 +41,7 @@ import com.google.inject.Inject;
 class PluginManagerImpl implements PluginManager {
 
 	private static final Logger LOG =
-		Logger.getLogger(PluginManagerImpl.class.getName());
+			Logger.getLogger(PluginManagerImpl.class.getName());
 
 	private static final String[] SIMPLEX_PLUGIN_FACTORIES = new String[] {
 		"net.sf.briar.plugins.file.RemovableDrivePluginFactory"
@@ -84,7 +85,7 @@ class PluginManagerImpl implements PluginManager {
 			try {
 				Class<?> c = Class.forName(s);
 				SimplexPluginFactory factory =
-					(SimplexPluginFactory) c.newInstance();
+						(SimplexPluginFactory) c.newInstance();
 				SimplexCallback callback = new SimplexCallback();
 				SimplexPlugin plugin = factory.createPlugin(pluginExecutor,
 						callback);
@@ -124,7 +125,7 @@ class PluginManagerImpl implements PluginManager {
 			try {
 				Class<?> c = Class.forName(s);
 				DuplexPluginFactory factory =
-					(DuplexPluginFactory) c.newInstance();
+						(DuplexPluginFactory) c.newInstance();
 				DuplexCallback callback = new DuplexCallback();
 				DuplexPlugin plugin = factory.createPlugin(pluginExecutor,
 						callback);
@@ -198,6 +199,26 @@ class PluginManagerImpl implements PluginManager {
 		return stopped;
 	}
 
+	public Collection<DuplexPlugin> getDuplexInvitationPlugins() {
+		Collection<DuplexPlugin> supported = new ArrayList<DuplexPlugin>();
+		synchronized(this) {
+			for(DuplexPlugin d : duplexPlugins) {
+				if(d.supportsInvitations()) supported.add(d);
+			}
+		}
+		return supported;
+	}
+
+	public Collection<SimplexPlugin> getSimplexInvitationPlugins() {
+		Collection<SimplexPlugin> supported = new ArrayList<SimplexPlugin>();
+		synchronized(this) {
+			for(SimplexPlugin s : simplexPlugins) {
+				if(s.supportsInvitations()) supported.add(s);
+			}
+		}
+		return supported;
+	}
+
 	private abstract class PluginCallbackImpl implements PluginCallback {
 
 		protected volatile TransportId id = null;
diff --git a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
index ae4141b05b76fdd5c423375c4998818943908a25..f7a57a551efdd66837fa6907cc1632ad00e8d1cd 100644
--- a/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
+++ b/components/net/sf/briar/plugins/bluetooth/BluetoothPlugin.java
@@ -25,6 +25,7 @@ import javax.microedition.io.StreamConnectionNotifier;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.PluginExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexPlugin;
 import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
@@ -304,22 +305,25 @@ class BluetoothPlugin implements DuplexPlugin {
 		return true;
 	}
 
-	public DuplexTransportConnection sendInvitation(int code, long timeout) {
-		return createInvitationConnection(code, timeout);
+	public DuplexTransportConnection sendInvitation(PseudoRandom r,
+			long timeout) {
+		return createInvitationConnection(r, timeout);
 	}
 
-	public DuplexTransportConnection acceptInvitation(int code, long timeout) {
-		return createInvitationConnection(code, timeout);
+	public DuplexTransportConnection acceptInvitation(PseudoRandom r,
+			long timeout) {
+		return createInvitationConnection(r, timeout);
 	}
 
-	private DuplexTransportConnection createInvitationConnection(int code,
+	private DuplexTransportConnection createInvitationConnection(PseudoRandom r,
 			long timeout) {
 		synchronized(this) {
 			if(!running) return null;
 		}
+		// Use the invitation code to generate the UUID
+		String uuid = StringUtils.toHexString(r.nextBytes(16));
 		// The invitee's device may not be discoverable, so both parties must
 		// try to initiate connections
-		String uuid = convertInvitationCodeToUuid(code);
 		final ConnectionCallback c = new ConnectionCallback(uuid, timeout);
 		pluginExecutor.execute(new Runnable() {
 			public void run() {
@@ -342,12 +346,6 @@ class BluetoothPlugin implements DuplexPlugin {
 		}
 	}
 
-	private String convertInvitationCodeToUuid(int code) {
-		byte[] b = new byte[16];
-		new Random(code).nextBytes(b);
-		return StringUtils.toHexString(b);
-	}
-
 	private void createInvitationConnection(ConnectionCallback c) {
 		LocalDevice localDevice;
 		synchronized(this) {
diff --git a/components/net/sf/briar/plugins/file/FilePlugin.java b/components/net/sf/briar/plugins/file/FilePlugin.java
index 5cd992a78af88b0838eacefcd263af88e9ac8b95..4a5987acf88ed05c1779fdb9e79a584324873de6 100644
--- a/components/net/sf/briar/plugins/file/FilePlugin.java
+++ b/components/net/sf/briar/plugins/file/FilePlugin.java
@@ -11,19 +11,21 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.PluginExecutor;
 import net.sf.briar.api.plugins.simplex.SimplexPlugin;
 import net.sf.briar.api.plugins.simplex.SimplexPluginCallback;
 import net.sf.briar.api.plugins.simplex.SimplexTransportReader;
 import net.sf.briar.api.plugins.simplex.SimplexTransportWriter;
 import net.sf.briar.api.transport.TransportConstants;
+import net.sf.briar.util.StringUtils;
 
 import org.apache.commons.io.FileSystemUtils;
 
 abstract class FilePlugin implements SimplexPlugin {
 
 	private static final Logger LOG =
-		Logger.getLogger(FilePlugin.class.getName());
+			Logger.getLogger(FilePlugin.class.getName());
 
 	protected final Executor pluginExecutor;
 	protected final SimplexPluginCallback callback;
@@ -92,26 +94,28 @@ abstract class FilePlugin implements SimplexPlugin {
 		pluginExecutor.execute(new ReaderCreator(f));
 	}
 
-	public SimplexTransportWriter sendInvitation(int code, long timeout) {
+	public SimplexTransportWriter sendInvitation(PseudoRandom r, long timeout) {
 		if(!running) return null;
-		return createWriter(createInvitationFilename(code, false));
+		return createWriter(createInvitationFilename(r, false));
 	}
 
-	public SimplexTransportReader acceptInvitation(int code, long timeout) {
+	public SimplexTransportReader acceptInvitation(PseudoRandom r,
+			long timeout) {
 		if(!running) return null;
-		String filename = createInvitationFilename(code, false);
+		String filename = createInvitationFilename(r, false);
 		return createInvitationReader(filename, timeout);
 	}
 
-	public SimplexTransportWriter sendInvitationResponse(int code, long timeout) {
+	public SimplexTransportWriter sendInvitationResponse(PseudoRandom r,
+			long timeout) {
 		if(!running) return null;
-		return createWriter(createInvitationFilename(code, true));
+		return createWriter(createInvitationFilename(r, true));
 	}
 
-	public SimplexTransportReader acceptInvitationResponse(int code,
+	public SimplexTransportReader acceptInvitationResponse(PseudoRandom r,
 			long timeout) {
 		if(!running) return null;
-		String filename = createInvitationFilename(code, true);
+		String filename = createInvitationFilename(r, true);
 		return createInvitationReader(filename, timeout);
 	}
 
@@ -149,15 +153,14 @@ abstract class FilePlugin implements SimplexPlugin {
 		return null;
 	}
 
-	private String createInvitationFilename(int code, boolean response) {
-		assert code >= 0;
-		assert code < 10 * 1000 * 1000;
-		return String.format("%c%7d.dat", response ? 'b' : 'a', code);
+	private String createInvitationFilename(PseudoRandom r, boolean response) {
+		String digits = StringUtils.toHexString(r.nextBytes(3));
+		return String.format("%c%s.dat", response ? 'b' : 'a', digits);
 	}
 
 	// Package access for testing
 	boolean isPossibleInvitationFilename(String filename) {
-		return filename.toLowerCase().matches("[ab][0-9]{7}.dat");
+		return filename.toLowerCase().matches("[ab][0-9a-f]{6}.dat");
 	}
 
 	private class ReaderCreator implements Runnable {
diff --git a/components/net/sf/briar/plugins/socket/LanSocketPlugin.java b/components/net/sf/briar/plugins/socket/LanSocketPlugin.java
index 53290881f4ac8af3067c10a12ea62af6e2133a46..2f4568775ba65770aaf7a4d5e1e783e5af8731fb 100644
--- a/components/net/sf/briar/plugins/socket/LanSocketPlugin.java
+++ b/components/net/sf/briar/plugins/socket/LanSocketPlugin.java
@@ -9,11 +9,11 @@ import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketTimeoutException;
 import java.net.UnknownHostException;
-import java.util.Random;
 import java.util.concurrent.Executor;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.PluginExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
@@ -23,7 +23,7 @@ import net.sf.briar.util.ByteUtils;
 class LanSocketPlugin extends SimpleSocketPlugin {
 
 	private static final Logger LOG =
-		Logger.getLogger(LanSocketPlugin.class.getName());
+			Logger.getLogger(LanSocketPlugin.class.getName());
 
 	LanSocketPlugin(@PluginExecutor Executor pluginExecutor,
 			DuplexPluginCallback callback, long pollingInterval) {
@@ -36,12 +36,13 @@ class LanSocketPlugin extends SimpleSocketPlugin {
 	}
 
 	@Override
-	public DuplexTransportConnection sendInvitation(int code, long timeout) {
+	public DuplexTransportConnection sendInvitation(PseudoRandom r,
+			long timeout) {
 		synchronized(this) {
 			if(!running) return null;
 		}
-		// Calculate the group address and port from the invitation code
-		InetSocketAddress mcast = convertInvitationCodeToMulticastGroup(code);
+		// Use the invitation code to choose the group address and port
+		InetSocketAddress mcast = chooseMulticastGroup(r);
 		// Bind a multicast socket for receiving packets
 		MulticastSocket ms = null;
 		try {
@@ -105,10 +106,8 @@ class LanSocketPlugin extends SimpleSocketPlugin {
 		ms.close();
 	}
 
-	private InetSocketAddress convertInvitationCodeToMulticastGroup(int code) {
-		Random r = new Random(code);
-		byte[] b = new byte[5];
-		r.nextBytes(b);
+	private InetSocketAddress chooseMulticastGroup(PseudoRandom r) {
+		byte[] b = r.nextBytes(5);
 		// The group address is 239.random.random.random, excluding 0 and 255
 		byte[] group = new byte[4];
 		group[0] = (byte) 239;
@@ -139,12 +138,13 @@ class LanSocketPlugin extends SimpleSocketPlugin {
 	}
 
 	@Override
-	public DuplexTransportConnection acceptInvitation(int code, long timeout) {
+	public DuplexTransportConnection acceptInvitation(PseudoRandom r,
+			long timeout) {
 		synchronized(this) {
 			if(!running) return null;
 		}
-		// Calculate the group address and port from the invitation code
-		InetSocketAddress mcast = convertInvitationCodeToMulticastGroup(code);
+		// Use the invitation code to choose the group address and port
+		InetSocketAddress mcast = chooseMulticastGroup(r);
 		// Bind a TCP socket for receiving connections
 		ServerSocket ss = null;
 		try {
diff --git a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
index b77b5c3e8e4b9861ba7841b4c5525cd4e35819b9..4a828f491276d4ebcee6fc09bd365ff11f59d79d 100644
--- a/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
+++ b/components/net/sf/briar/plugins/socket/SimpleSocketPlugin.java
@@ -15,6 +15,7 @@ import java.util.logging.Logger;
 
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.PluginExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
@@ -24,13 +25,13 @@ import net.sf.briar.util.StringUtils;
 class SimpleSocketPlugin extends SocketPlugin {
 
 	public static final byte[] TRANSPORT_ID =
-		StringUtils.fromHexString("58c66d999e492b85065924acfd739d80"
-				+ "c65a62f87e5a4fc6c284f95908b9007d"
-				+ "512a93ebf89bf68f50a29e96eebf97b6");
+			StringUtils.fromHexString("58c66d999e492b85065924acfd739d80"
+					+ "c65a62f87e5a4fc6c284f95908b9007d"
+					+ "512a93ebf89bf68f50a29e96eebf97b6");
 
 	private static final TransportId ID = new TransportId(TRANSPORT_ID);
 	private static final Logger LOG =
-		Logger.getLogger(SimpleSocketPlugin.class.getName());
+			Logger.getLogger(SimpleSocketPlugin.class.getName());
 
 	SimpleSocketPlugin(@PluginExecutor Executor pluginExecutor,
 			DuplexPluginCallback callback, long pollingInterval) {
@@ -68,7 +69,7 @@ class SimpleSocketPlugin extends SocketPlugin {
 
 	protected InetAddress chooseInterface(boolean lan) throws IOException {
 		List<NetworkInterface> ifaces =
-			Collections.list(NetworkInterface.getNetworkInterfaces());
+				Collections.list(NetworkInterface.getNetworkInterfaces());
 		// Try to find an interface of the preferred type (LAN or WAN)
 		for(NetworkInterface iface : ifaces) {
 			for(InetAddress addr : Collections.list(iface.getInetAddresses())) {
@@ -139,11 +140,13 @@ class SimpleSocketPlugin extends SocketPlugin {
 		return false;
 	}
 
-	public DuplexTransportConnection sendInvitation(int code, long timeout) {
+	public DuplexTransportConnection sendInvitation(PseudoRandom r,
+			long timeout) {
 		throw new UnsupportedOperationException();
 	}
 
-	public DuplexTransportConnection acceptInvitation(int code, long timeout) {
+	public DuplexTransportConnection acceptInvitation(PseudoRandom r,
+			long timeout) {
 		throw new UnsupportedOperationException();
 	}
 }
diff --git a/components/net/sf/briar/plugins/tor/TorPlugin.java b/components/net/sf/briar/plugins/tor/TorPlugin.java
index e7eaaf51374d313fc25c594dc9927b2369084a9f..e1b61f38585348b78aeb3ede19875f461b577a32 100644
--- a/components/net/sf/briar/plugins/tor/TorPlugin.java
+++ b/components/net/sf/briar/plugins/tor/TorPlugin.java
@@ -10,6 +10,7 @@ import java.util.logging.Logger;
 import net.sf.briar.api.ContactId;
 import net.sf.briar.api.TransportConfig;
 import net.sf.briar.api.TransportProperties;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.PluginExecutor;
 import net.sf.briar.api.plugins.duplex.DuplexPlugin;
 import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
@@ -33,13 +34,13 @@ import org.silvertunnel.netlib.layer.tor.util.RSAKeyPair;
 class TorPlugin implements DuplexPlugin {
 
 	public static final byte[] TRANSPORT_ID =
-		StringUtils.fromHexString("f264721575cb7ee710772f35abeb3db4"
-				+ "a91f474e14de346be296c2efc99effdd"
-				+ "f35921e6ed87a25c201f044da4767981");
+			StringUtils.fromHexString("f264721575cb7ee710772f35abeb3db4"
+					+ "a91f474e14de346be296c2efc99effdd"
+					+ "f35921e6ed87a25c201f044da4767981");
 
 	private static final TransportId ID = new TransportId(TRANSPORT_ID);
 	private static final Logger LOG =
-		Logger.getLogger(TorPlugin.class.getName());
+			Logger.getLogger(TorPlugin.class.getName());
 
 	private final Executor pluginExecutor;
 	private final DuplexPluginCallback callback;
@@ -89,7 +90,7 @@ class TorPlugin implements DuplexPlugin {
 			}
 		}
 		TorHiddenServicePortPrivateNetAddress addrPort =
-			new TorHiddenServicePortPrivateNetAddress(addr, 80);
+				new TorHiddenServicePortPrivateNetAddress(addr, 80);
 		// Connect to Tor
 		NetFactory netFactory = NetFactory.getInstance();
 		NetLayer nl = netFactory.getNetLayerById(NetLayerIDs.TOR);
@@ -129,7 +130,7 @@ class TorPlugin implements DuplexPlugin {
 	private TorHiddenServicePrivateNetAddress createHiddenServiceAddress(
 			TorNetLayerUtil util, TransportConfig c) {
 		TorHiddenServicePrivateNetAddress addr =
-			util.createNewTorHiddenServicePrivateNetAddress();
+				util.createNewTorHiddenServicePrivateNetAddress();
 		RSAKeyPair keyPair = addr.getKeyPair();
 		String privateKey = Encryption.getPEMStringFromRSAKeyPair(keyPair);
 		c.put("privateKey", privateKey);
@@ -199,7 +200,7 @@ class TorPlugin implements DuplexPlugin {
 			if(!running) return;
 		}
 		Map<ContactId, TransportProperties> remote =
-			callback.getRemoteProperties();
+				callback.getRemoteProperties();
 		for(final ContactId c : remote.keySet()) {
 			if(connected.contains(c)) continue;
 			pluginExecutor.execute(new Runnable() {
@@ -249,11 +250,13 @@ class TorPlugin implements DuplexPlugin {
 		}
 	}
 
-	public DuplexTransportConnection sendInvitation(int code, long timeout) {
+	public DuplexTransportConnection sendInvitation(PseudoRandom r,
+			long timeout) {
 		throw new UnsupportedOperationException();
 	}
 
-	public DuplexTransportConnection acceptInvitation(int code, long timeout) {
+	public DuplexTransportConnection acceptInvitation(PseudoRandom r,
+			long timeout) {
 		throw new UnsupportedOperationException();
 	}
 }
diff --git a/test/net/sf/briar/plugins/DuplexClientTest.java b/test/net/sf/briar/plugins/DuplexClientTest.java
index dd051cf37b5fc4465fff1f54a1adb593ec5bc677..2e58e14ba81dd2fc25d59007c57e7d7e56d54929 100644
--- a/test/net/sf/briar/plugins/DuplexClientTest.java
+++ b/test/net/sf/briar/plugins/DuplexClientTest.java
@@ -29,7 +29,7 @@ public abstract class DuplexClientTest extends DuplexTest {
 		}
 		// Try to send an invitation
 		System.out.println("Sending invitation");
-		d = plugin.sendInvitation(123, INVITATION_TIMEOUT);
+		d = plugin.sendInvitation(getPseudoRandom(123), INVITATION_TIMEOUT);
 		if(d == null) {
 			System.out.println("Connection failed");
 		} else {
@@ -38,7 +38,7 @@ public abstract class DuplexClientTest extends DuplexTest {
 		}
 		// Try to accept an invitation
 		System.out.println("Accepting invitation");
-		d = plugin.acceptInvitation(456, INVITATION_TIMEOUT);
+		d = plugin.acceptInvitation(getPseudoRandom(456), INVITATION_TIMEOUT);
 		if(d == null) {
 			System.out.println("Connection failed");
 		} else {
diff --git a/test/net/sf/briar/plugins/DuplexServerTest.java b/test/net/sf/briar/plugins/DuplexServerTest.java
index 008ab8108fd465686515d4918c1453e68d508d9b..662f299b89a80e4822e77b39786c41a26ff6d5f1 100644
--- a/test/net/sf/briar/plugins/DuplexServerTest.java
+++ b/test/net/sf/briar/plugins/DuplexServerTest.java
@@ -24,8 +24,8 @@ public abstract class DuplexServerTest extends DuplexTest {
 		callback.latch.await();
 		// Try to accept an invitation
 		System.out.println("Accepting invitation");
-		DuplexTransportConnection d = plugin.acceptInvitation(123,
-				INVITATION_TIMEOUT);
+		DuplexTransportConnection d = plugin.acceptInvitation(
+				getPseudoRandom(123), INVITATION_TIMEOUT);
 		if(d == null) {
 			System.out.println("Connection failed");
 		} else {
@@ -34,7 +34,7 @@ public abstract class DuplexServerTest extends DuplexTest {
 		}
 		// Try to send an invitation
 		System.out.println("Sending invitation");
-		d = plugin.sendInvitation(456, INVITATION_TIMEOUT);
+		d = plugin.sendInvitation(getPseudoRandom(456), INVITATION_TIMEOUT);
 		if(d == null) {
 			System.out.println("Connection failed");
 		} else {
diff --git a/test/net/sf/briar/plugins/DuplexTest.java b/test/net/sf/briar/plugins/DuplexTest.java
index 1eeb292b840123889201c394d0337f19ccdfb5ac..c247415e46079e5c59f91d570d6d08f0c8a8f1cd 100644
--- a/test/net/sf/briar/plugins/DuplexTest.java
+++ b/test/net/sf/briar/plugins/DuplexTest.java
@@ -2,9 +2,11 @@ package net.sf.briar.plugins;
 
 import java.io.IOException;
 import java.io.PrintStream;
+import java.util.Random;
 import java.util.Scanner;
 
 import net.sf.briar.api.ContactId;
+import net.sf.briar.api.crypto.PseudoRandom;
 import net.sf.briar.api.plugins.duplex.DuplexPlugin;
 import net.sf.briar.api.plugins.duplex.DuplexTransportConnection;
 
@@ -66,4 +68,23 @@ abstract class DuplexTest {
 			d.dispose(true, true);
 		}
 	}
+
+	protected PseudoRandom getPseudoRandom(int seed) {
+		return new TestPseudoRandom(seed);
+	}
+
+	private static class TestPseudoRandom implements PseudoRandom {
+
+		private final Random r;
+
+		private TestPseudoRandom(int seed) {
+			r = new Random(seed);
+		}
+
+		public byte[] nextBytes(int bytes) {
+			byte[] b = new byte[bytes];
+			r.nextBytes(b);
+			return b;
+		}
+	}
 }
diff --git a/test/net/sf/briar/util/ByteUtilsTest.java b/test/net/sf/briar/util/ByteUtilsTest.java
index 0c0334697204395197cd6e021f95280ec417592f..c672bc0e2af7e72bbcce2171f27a72952cbfa775 100644
--- a/test/net/sf/briar/util/ByteUtilsTest.java
+++ b/test/net/sf/briar/util/ByteUtilsTest.java
@@ -48,4 +48,19 @@ public class ByteUtilsTest extends BriarTestCase {
 		ByteUtils.writeUint32(4294967295L, b, 1);
 		assertEquals("00FFFFFFFF", StringUtils.toHexString(b));
 	}
+
+	@Test
+	public void testReadUint() {
+		byte[] b = new byte[1];
+		b[0] = (byte) 128;
+		for(int i = 0; i < 8; i++) {
+			assertEquals(1 << i, ByteUtils.readUint(b, i + 1));
+		}
+		b = new byte[2];
+		for(int i = 0; i < 65535; i++) {
+			ByteUtils.writeUint16(i, b, 0);
+			assertEquals(i, ByteUtils.readUint(b, 16));
+			assertEquals(i >> 1, ByteUtils.readUint(b, 15));
+		}
+	}
 }
diff --git a/util/net/sf/briar/util/ByteUtils.java b/util/net/sf/briar/util/ByteUtils.java
index 4871865437874801104b57b2d43f641b6b7e75e5..1c3f9bdc13d1ebb419ad97531d876eb2c4017190 100644
--- a/util/net/sf/briar/util/ByteUtils.java
+++ b/util/net/sf/briar/util/ByteUtils.java
@@ -38,10 +38,21 @@ public class ByteUtils {
 	public static long readUint32(byte[] b, int offset) {
 		if(b.length < offset + 4) throw new IllegalArgumentException();
 		return ((b[offset] & 0xFFL) << 24) | ((b[offset + 1] & 0xFFL) << 16)
-		| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
+				| ((b[offset + 2] & 0xFFL) << 8) | (b[offset + 3] & 0xFFL);
 	}
 
 	public static void erase(byte[] b) {
 		for(int i = 0; i < b.length; i++) b[i] = 0;
 	}
+
+	public static int readUint(byte[] b, int bits) {
+		if(b.length << 3 < bits) throw new IllegalArgumentException();
+		int result = 0;
+		for(int i = 0; i < bits; i++) {
+			if((b[i >> 3] & 128 >> (i & 7)) != 0) result |= 1 << bits - i - 1;
+		}
+		assert result >= 0;
+		assert result < 1 << bits;
+		return result;
+	}
 }