diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java
index 3ee8d70af467ff5e18bc73c7cddd6c941af5bd4b..5750c0e3b4d203a5bf6d829619383f8e89fdd0a4 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/ContactExchangeTask.java
@@ -12,6 +12,27 @@ import org.briarproject.bramble.api.plugin.duplex.DuplexTransportConnection;
 @NotNullByDefault
 public interface ContactExchangeTask {
 
+	/**
+	 * Label for deriving Alice's header key from the master secret.
+	 */
+	String ALICE_KEY_LABEL =
+			"org.briarproject.bramble.contact/ALICE_HEADER_KEY";
+
+	/**
+	 * Label for deriving Bob's header key from the master secret.
+	 */
+	String BOB_KEY_LABEL = "org.briarproject.bramble.contact/BOB_HEADER_KEY";
+
+	/**
+	 * Label for deriving Alice's key binding nonce from the master secret.
+	 */
+	String ALICE_NONCE_LABEL = "org.briarproject.bramble.contact/ALICE_NONCE";
+
+	/**
+	 * Label for deriving Bob's key binding nonce from the master secret.
+	 */
+	String BOB_NONCE_LABEL = "org.briarproject.bramble.contact/BOB_NONCE";
+
 	/**
 	 * Exchanges contact information with a remote peer.
 	 */
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoComponent.java b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoComponent.java
index 5da0e421887896682ab8c8bc7ab7b76941c2eb42..3a2be304a6fbc9d2e1529eebf72fc7f8794c1832 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoComponent.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoComponent.java
@@ -1,8 +1,5 @@
 package org.briarproject.bramble.api.crypto;
 
-import org.briarproject.bramble.api.plugin.TransportId;
-import org.briarproject.bramble.api.transport.TransportKeys;
-
 import java.security.GeneralSecurityException;
 import java.security.SecureRandom;
 
@@ -27,115 +24,35 @@ public interface CryptoComponent {
 	KeyParser getMessageKeyParser();
 
 	/**
-	 * Derives a stream header key from the given master secret.
-	 * @param alice whether the key is for use by Alice or Bob.
-	 */
-	SecretKey deriveHeaderKey(SecretKey master, boolean alice);
-
-	/**
-	 * Derives a message authentication code key from the given master secret.
-	 * @param alice whether the key is for use by Alice or Bob.
-	 */
-	SecretKey deriveMacKey(SecretKey master, boolean alice);
-
-	/**
-	 * Derives a nonce from the given master secret for one of the parties to
-	 * sign.
-	 * @param alice whether the nonce is for use by Alice or Bob.
-	 */
-	byte[] deriveSignatureNonce(SecretKey master, boolean alice);
-
-	/**
-	 * Derives a commitment to the provided public key.
-	 * <p/>
-	 * Part of BQP.
+	 * Derives another secret key from the given secret key.
 	 *
-	 * @param publicKey the public key
-	 * @return the commitment to the provided public key.
+	 * @param label a namespaced label indicating the purpose of the derived
+	 * key, to prevent it from being repurposed or colliding with a key derived
+	 * for another purpose
 	 */
-	byte[] deriveKeyCommitment(byte[] publicKey);
+	SecretKey deriveKey(String label, SecretKey k, byte[]... inputs);
 
 	/**
 	 * Derives a common shared secret from two public keys and one of the
 	 * corresponding private keys.
-	 * <p/>
-	 * Part of BQP.
-	 *
-	 * @param theirPublicKey the ephemeral public key of the remote party
-	 * @param ourKeyPair our ephemeral keypair
-	 * @param alice true if ourKeyPair belongs to Alice
-	 * @return the shared secret
-	 */
-	SecretKey deriveSharedSecret(byte[] theirPublicKey, KeyPair ourKeyPair,
-			boolean alice) throws GeneralSecurityException;
-
-	/**
-	 * Derives the content of a confirmation record.
-	 * <p/>
-	 * Part of BQP.
-	 *
-	 * @param sharedSecret the common shared secret
-	 * @param theirPayload the commit payload from the remote party
-	 * @param ourPayload the commit payload we sent
-	 * @param theirPublicKey the ephemeral public key of the remote party
-	 * @param ourKeyPair our ephemeral keypair
-	 * @param alice true if ourKeyPair belongs to Alice
-	 * @param aliceRecord true if the confirmation record is for use by Alice
-	 * @return the confirmation record
-	 */
-	byte[] deriveConfirmationRecord(SecretKey sharedSecret,
-			byte[] theirPayload, byte[] ourPayload,
-			byte[] theirPublicKey, KeyPair ourKeyPair,
-			boolean alice, boolean aliceRecord);
-
-	/**
-	 * Derives a master secret from the given shared secret.
-	 * <p/>
-	 * Part of BQP.
 	 *
-	 * @param sharedSecret the common shared secret
-	 * @return the master secret
-	 */
-	SecretKey deriveMasterSecret(SecretKey sharedSecret);
-
-	/**
-	 * Derives a master secret from two public keys and one of the corresponding
-	 * private keys.
-	 * <p/>
-	 * This is a helper method that calls
-	 * deriveMasterSecret(deriveSharedSecret(theirPublicKey, ourKeyPair, alice))
-	 *
-	 * @param theirPublicKey the ephemeral public key of the remote party
-	 * @param ourKeyPair our ephemeral keypair
-	 * @param alice true if ourKeyPair belongs to Alice
+	 * @param label a namespaced label indicating the purpose of this shared
+	 * secret, to prevent it from being repurposed or colliding with a shared
+	 * secret derived for another purpose
+	 * @param theirPublicKey the public key of the remote party
+	 * @param ourKeyPair the key pair of the local party
+	 * @param alice true if the local party is Alice
 	 * @return the shared secret
 	 */
-	SecretKey deriveMasterSecret(byte[] theirPublicKey, KeyPair ourKeyPair,
-			boolean alice) throws GeneralSecurityException;
-
-	/**
-	 * Derives initial transport keys for the given transport in the given
-	 * rotation period from the given master secret.
-	 * @param alice whether the keys are for use by Alice or Bob.
-	 */
-	TransportKeys deriveTransportKeys(TransportId t, SecretKey master,
-			long rotationPeriod, boolean alice);
-
-	/**
-	 * Rotates the given transport keys to the given rotation period. If the
-	 * keys are for a future rotation period they are not rotated.
-	 */
-	TransportKeys rotateTransportKeys(TransportKeys k, long rotationPeriod);
-
-	/** Encodes the pseudo-random tag that is used to recognise a stream. */
-	void encodeTag(byte[] tag, SecretKey tagKey, int protocolVersion,
-			long streamNumber);
+	SecretKey deriveSharedSecret(String label, PublicKey theirPublicKey,
+			KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException;
 
 	/**
 	 * Signs the given byte[] with the given ECDSA private key.
 	 *
-	 * @param label A label specific to this signature
-	 *              to ensure that the signature cannot be repurposed
+	 * @param label a namespaced label indicating the purpose of this
+	 * signature, to prevent it from being repurposed or colliding with a
+	 * signature created for another purpose
 	 */
 	byte[] sign(String label, byte[] toSign, byte[] privateKey)
 			throws GeneralSecurityException;
@@ -153,8 +70,9 @@ public interface CryptoComponent {
 	 * Verifies that the given signature is valid for the signed data
 	 * and the given ECDSA public key.
 	 *
-	 * @param label A label that was specific to this signature
-	 *              to ensure that the signature cannot be repurposed
+	 * @param label a namespaced label indicating the purpose of this
+	 * signature, to prevent it from being repurposed or colliding with a
+	 * signature created for another purpose
 	 * @return true if the signature was valid, false otherwise.
 	 */
 	boolean verify(String label, byte[] signedData, byte[] publicKey,
@@ -175,23 +93,22 @@ public interface CryptoComponent {
 	 * Returns the hash of the given inputs. The inputs are unambiguously
 	 * combined by prefixing each input with its length.
 	 *
-	 * @param label A label specific to this hash to ensure that hashes
-	 *              calculated for distinct purposes don't collide.
+	 * @param label a namespaced label indicating the purpose of this hash, to
+	 * prevent it from being repurposed or colliding with a hash created for
+	 * another purpose
 	 */
 	byte[] hash(String label, byte[]... inputs);
 
-	/**
-	 * Returns the length of hashes produced by
-	 * the {@link CryptoComponent#hash(String, byte[]...)} method.
-	 */
-	int getHashLength();
-
 	/**
 	 * Returns a message authentication code with the given key over the
 	 * given inputs. The inputs are unambiguously combined by prefixing each
 	 * input with its length.
+	 *
+	 * @param label a namespaced label indicating the purpose of this MAC, to
+	 * prevent it from being repurposed or colliding with a MAC created for
+	 * another purpose
 	 */
-	byte[] mac(SecretKey macKey, byte[]... inputs);
+	byte[] mac(String label, SecretKey macKey, byte[]... inputs);
 
 	/**
 	 * Encrypts and authenticates the given plaintext so it can be written to
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/KeyAgreementCrypto.java b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/KeyAgreementCrypto.java
new file mode 100644
index 0000000000000000000000000000000000000000..a35cd3114e26e3ff69e991fb2e35138faed2c042
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/KeyAgreementCrypto.java
@@ -0,0 +1,50 @@
+package org.briarproject.bramble.api.crypto;
+
+/**
+ * Crypto operations for the key agreement protocol - see
+ * https://code.briarproject.org/akwizgran/briar-spec/blob/master/protocols/BQP.md
+ */
+public interface KeyAgreementCrypto {
+
+	/**
+	 * Hash label for public key commitment.
+	 */
+	String COMMIT_LABEL = "org.briarproject.bramble.keyagreement/COMMIT";
+
+	/**
+	 * Key derivation label for confirmation record.
+	 */
+	String CONFIRMATION_KEY_LABEL =
+			"org.briarproject.bramble.keyagreement/CONFIRMATION_KEY";
+
+	/**
+	 * MAC label for confirmation record.
+	 */
+	String CONFIRMATION_MAC_LABEL =
+			"org.briarproject.bramble.keyagreement/CONFIRMATION_MAC";
+
+	/**
+	 * Derives a commitment to the provided public key.
+	 *
+	 * @param publicKey the public key
+	 * @return the commitment to the provided public key.
+	 */
+	byte[] deriveKeyCommitment(PublicKey publicKey);
+
+	/**
+	 * Derives the content of a confirmation record.
+	 *
+	 * @param sharedSecret the common shared secret
+	 * @param theirPayload the key exchange payload of the remote party
+	 * @param ourPayload the key exchange payload of the local party
+	 * @param theirPublicKey the ephemeral public key of the remote party
+	 * @param ourKeyPair our ephemeral key pair of the local party
+	 * @param alice true if the local party is Alice
+	 * @param aliceRecord true if the confirmation record is for use by Alice
+	 * @return the confirmation record
+	 */
+	byte[] deriveConfirmationRecord(SecretKey sharedSecret,
+			byte[] theirPayload, byte[] ourPayload,
+			PublicKey theirPublicKey, KeyPair ourKeyPair,
+			boolean alice, boolean aliceRecord);
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/TransportCrypto.java b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/TransportCrypto.java
new file mode 100644
index 0000000000000000000000000000000000000000..6385d1f015bbf7d0dff30d0c03390caf7af11196
--- /dev/null
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/TransportCrypto.java
@@ -0,0 +1,32 @@
+package org.briarproject.bramble.api.crypto;
+
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.transport.TransportKeys;
+
+/**
+ * Crypto operations for the transport security protocol - see
+ * https://code.briarproject.org/akwizgran/briar-spec/blob/master/protocols/BTP.md
+ */
+public interface TransportCrypto {
+
+	/**
+	 * Derives initial transport keys for the given transport in the given
+	 * rotation period from the given master secret.
+	 *
+	 * @param alice whether the keys are for use by Alice or Bob.
+	 */
+	TransportKeys deriveTransportKeys(TransportId t, SecretKey master,
+			long rotationPeriod, boolean alice);
+
+	/**
+	 * Rotates the given transport keys to the given rotation period. If the
+	 * keys are for the given period or any later period they are not rotated.
+	 */
+	TransportKeys rotateTransportKeys(TransportKeys k, long rotationPeriod);
+
+	/**
+	 * Encodes the pseudo-random tag that is used to recognise a stream.
+	 */
+	void encodeTag(byte[] tag, SecretKey tagKey, int protocolVersion,
+			long streamNumber);
+}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/identity/AuthorId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/identity/AuthorId.java
index 0963e0049e20ca3fb3ac8dc14950e86c0dae3898..b9793b4775d694c59e58774f804fad1298a57013 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/identity/AuthorId.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/identity/AuthorId.java
@@ -16,7 +16,7 @@ public class AuthorId extends UniqueId {
 	/**
 	 * Label for hashing authors to calculate their identities.
 	 */
-	public static final String LABEL = "org.briarproject.bramble.AUTHOR_ID";
+	public static final String LABEL = "org.briarproject.bramble/AUTHOR_ID";
 
 	public AuthorId(byte[] id) {
 		super(id);
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java
index 76204bdfbeb2b48f7bad458bd82801eb7f39df42..807055d6e048118cc4482fd878eb0e33808e2680 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementConstants.java
@@ -5,7 +5,7 @@ public interface KeyAgreementConstants {
 	/**
 	 * The current version of the BQP protocol.
 	 */
-	byte PROTOCOL_VERSION = 2;
+	byte PROTOCOL_VERSION = 3;
 
 	/**
 	 * The length of the record header in bytes.
@@ -22,7 +22,10 @@ public interface KeyAgreementConstants {
 	 */
 	int COMMIT_LENGTH = 16;
 
-	long CONNECTION_TIMEOUT = 20 * 1000; // Milliseconds
+	/**
+	 * The connection timeout in milliseconds.
+	 */
+	long CONNECTION_TIMEOUT = 20 * 1000;
 
 	/**
 	 * The transport identifier for Bluetooth.
@@ -33,4 +36,16 @@ public interface KeyAgreementConstants {
 	 * The transport identifier for LAN.
 	 */
 	int TRANSPORT_ID_LAN = 1;
+
+	/**
+	 * Label for deriving the shared secret.
+	 */
+	String SHARED_SECRET_LABEL =
+			"org.briarproject.bramble.keyagreement/SHARED_SECRET";
+
+	/**
+	 * Label for deriving the master secret.
+	 */
+	String MASTER_SECRET_LABEL =
+			"org.briarproject.bramble.keyagreement/MASTER_SECRET";
 }
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementTaskFactory.java b/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementTaskFactory.java
deleted file mode 100644
index 40d875cc9d792e53a34c3d6f52ac6611c6fd9dea..0000000000000000000000000000000000000000
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/keyagreement/KeyAgreementTaskFactory.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package org.briarproject.bramble.api.keyagreement;
-
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-
-/**
- * Manages tasks for conducting key agreements with remote peers.
- */
-@NotNullByDefault
-public interface KeyAgreementTaskFactory {
-
-	/**
-	 * Gets the current key agreement task.
-	 */
-	KeyAgreementTask createTask();
-}
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupId.java
index cdd6b2d3fba5dd4ad95968e6e3360b1be120a695..d118ed9c0eb65dea8c678afa5c02b9385c93890b 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupId.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/GroupId.java
@@ -15,7 +15,7 @@ public class GroupId extends UniqueId {
 	/**
 	 * Label for hashing groups to calculate their identifiers.
 	 */
-	public static final String LABEL = "org.briarproject.bramble.GROUP_ID";
+	public static final String LABEL = "org.briarproject.bramble/GROUP_ID";
 
 	public GroupId(byte[] id) {
 		super(id);
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/MessageId.java b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/MessageId.java
index 84389bbcc83371980d7e71a62ca5137079e011d3..07487eb8c949e4c18561f6988af4130571bff836 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/sync/MessageId.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/sync/MessageId.java
@@ -16,7 +16,7 @@ public class MessageId extends UniqueId {
 	/**
 	 * Label for hashing messages to calculate their identifiers.
 	 */
-	public static final String LABEL = "org.briarproject.bramble.MESSAGE_ID";
+	public static final String LABEL = "org.briarproject.bramble/MESSAGE_ID";
 
 	public MessageId(byte[] id) {
 		super(id);
diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/TransportConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/TransportConstants.java
index af9d09731dcb40aa8549797a7573037161f0d3c5..cf5502a5ff212c523cfef42c659bbc8cf26f8534 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/transport/TransportConstants.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/transport/TransportConstants.java
@@ -7,7 +7,7 @@ public interface TransportConstants {
 	/**
 	 * The current version of the transport protocol.
 	 */
-	int PROTOCOL_VERSION = 3;
+	int PROTOCOL_VERSION = 4;
 
 	/**
 	 * The length of the pseudo-random tag in bytes.
@@ -80,4 +80,32 @@ public interface TransportConstants {
 	 * The size of the reordering window.
 	 */
 	int REORDERING_WINDOW_SIZE = 32;
+
+	/**
+	 * Label for deriving Alice's initial tag key from the master secret.
+	 */
+	String ALICE_TAG_LABEL = "org.briarproject.bramble.transport/ALICE_TAG_KEY";
+
+	/**
+	 * Label for deriving Bob's initial tag key from the master secret.
+	 */
+	String BOB_TAG_LABEL = "org.briarproject.bramble.transport/BOB_TAG_KEY";
+
+	/**
+	 * Label for deriving Alice's initial header key from the master secret.
+	 */
+	String ALICE_HEADER_LABEL =
+			"org.briarproject.bramble.transport/ALICE_HEADER_KEY";
+
+	/**
+	 * Label for deriving Bob's initial header key from the master secret.
+	 */
+	String BOB_HEADER_LABEL =
+			"org.briarproject.bramble.transport/BOB_HEADER_KEY";
+
+	/**
+	 * Label for deriving the next period's key in key rotation.
+	 */
+	String ROTATE_LABEL = "org.briarproject.bramble.transport/ROTATE";
+
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java
index c49360e5bca20a9b99de30a3e710053c7ce8aea1..50a4f841cba5c01c95113c3c52bdb4eba537784c 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactExchangeTaskImpl.java
@@ -141,8 +141,9 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 		}
 
 		// Derive the header keys for the transport streams
-		SecretKey aliceHeaderKey = crypto.deriveHeaderKey(masterSecret, true);
-		SecretKey bobHeaderKey = crypto.deriveHeaderKey(masterSecret, false);
+		SecretKey aliceHeaderKey = crypto.deriveKey(ALICE_KEY_LABEL,
+				masterSecret);
+		SecretKey bobHeaderKey = crypto.deriveKey(BOB_KEY_LABEL, masterSecret);
 
 		// Create the readers
 		InputStream streamReader =
@@ -156,8 +157,8 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 		BdfWriter w = bdfWriterFactory.createWriter(streamWriter);
 
 		// Derive the nonces to be signed
-		byte[] aliceNonce = crypto.deriveSignatureNonce(masterSecret, true);
-		byte[] bobNonce = crypto.deriveSignatureNonce(masterSecret, false);
+		byte[] aliceNonce = crypto.mac(ALICE_NONCE_LABEL, masterSecret);
+		byte[] bobNonce = crypto.mac(BOB_NONCE_LABEL, masterSecret);
 
 		// Exchange pseudonyms, signed nonces, and timestamps
 		long localTimestamp = clock.currentTimeMillis();
@@ -312,8 +313,7 @@ class ContactExchangeTaskImpl extends Thread implements ContactExchangeTask {
 		return contactId;
 	}
 
-	private void tryToClose(DuplexTransportConnection conn,
-			boolean exception) {
+	private void tryToClose(DuplexTransportConnection conn, boolean exception) {
 		try {
 			LOG.info("Closing connection");
 			conn.getReader().dispose(exception, true);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoComponentImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoComponentImpl.java
index e92babdc038c6218508c5bdfb9e068cd5d46fda5..37af381df94e92420b5437bb5fc334d1521cc7ec 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoComponentImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoComponentImpl.java
@@ -10,11 +10,7 @@ import org.briarproject.bramble.api.crypto.KeyParser;
 import org.briarproject.bramble.api.crypto.PrivateKey;
 import org.briarproject.bramble.api.crypto.PublicKey;
 import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.system.SecureRandomProvider;
-import org.briarproject.bramble.api.transport.IncomingKeys;
-import org.briarproject.bramble.api.transport.OutgoingKeys;
-import org.briarproject.bramble.api.transport.TransportKeys;
 import org.briarproject.bramble.util.ByteUtils;
 import org.briarproject.bramble.util.StringUtils;
 import org.spongycastle.crypto.AsymmetricCipherKeyPair;
@@ -30,7 +26,6 @@ import org.spongycastle.crypto.params.ECPrivateKeyParameters;
 import org.spongycastle.crypto.params.ECPublicKeyParameters;
 import org.spongycastle.crypto.params.KeyParameter;
 
-import java.nio.charset.Charset;
 import java.security.GeneralSecurityException;
 import java.security.NoSuchAlgorithmException;
 import java.security.Provider;
@@ -44,14 +39,8 @@ import java.util.logging.Logger;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
-import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
-import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
 import static org.briarproject.bramble.crypto.EllipticCurveConstants.PARAMETERS;
-import static org.briarproject.bramble.util.ByteUtils.INT_16_BYTES;
 import static org.briarproject.bramble.util.ByteUtils.INT_32_BYTES;
-import static org.briarproject.bramble.util.ByteUtils.INT_64_BYTES;
-import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED;
-import static org.briarproject.bramble.util.ByteUtils.MAX_32_BIT_UNSIGNED;
 
 class CryptoComponentImpl implements CryptoComponent {
 
@@ -65,39 +54,6 @@ class CryptoComponentImpl implements CryptoComponent {
 	private static final int PBKDF_SALT_BYTES = 32; // 256 bits
 	private static final int PBKDF_TARGET_MILLIS = 500;
 	private static final int PBKDF_SAMPLES = 30;
-	private static final int HASH_SIZE = 256 / 8;
-
-	private static byte[] ascii(String s) {
-		return s.getBytes(Charset.forName("US-ASCII"));
-	}
-
-	// KDF labels for contact exchange stream header key derivation
-	private static final byte[] A_INVITE = ascii("ALICE_INVITATION_KEY");
-	private static final byte[] B_INVITE = ascii("BOB_INVITATION_KEY");
-	// KDF labels for contact exchange signature nonce derivation
-	private static final byte[] A_SIG_NONCE = ascii("ALICE_SIGNATURE_NONCE");
-	private static final byte[] B_SIG_NONCE = ascii("BOB_SIGNATURE_NONCE");
-	// Hash label for BQP public key commitment derivation
-	private static final String COMMIT =
-			"org.briarproject.bramble.COMMIT";
-	// Hash label for shared secret derivation
-	private static final String SHARED_SECRET =
-			"org.briarproject.bramble.SHARED_SECRET";
-	// KDF label for BQP confirmation key derivation
-	private static final byte[] CONFIRMATION_KEY = ascii("CONFIRMATION_KEY");
-	// KDF label for master key derivation
-	private static final byte[] MASTER_KEY = ascii("MASTER_KEY");
-	// KDF labels for tag key derivation
-	private static final byte[] A_TAG = ascii("ALICE_TAG_KEY");
-	private static final byte[] B_TAG = ascii("BOB_TAG_KEY");
-	// KDF labels for header key derivation
-	private static final byte[] A_HEADER = ascii("ALICE_HEADER_KEY");
-	private static final byte[] B_HEADER = ascii("BOB_HEADER_KEY");
-	// KDF labels for MAC key derivation
-	private static final byte[] A_MAC = ascii("ALICE_MAC_KEY");
-	private static final byte[] B_MAC = ascii("BOB_MAC_KEY");
-	// KDF label for key rotation
-	private static final byte[] ROTATE = ascii("ROTATE");
 
 	private final SecureRandom secureRandom;
 	private final ECKeyPairGenerator agreementKeyPairGenerator;
@@ -263,179 +219,26 @@ class CryptoComponentImpl implements CryptoComponent {
 	}
 
 	@Override
-	public SecretKey deriveHeaderKey(SecretKey master,
-			boolean alice) {
-		return new SecretKey(macKdf(master, alice ? A_INVITE : B_INVITE));
-	}
-
-	@Override
-	public SecretKey deriveMacKey(SecretKey master, boolean alice) {
-		return new SecretKey(macKdf(master, alice ? A_MAC : B_MAC));
-	}
-
-	@Override
-	public byte[] deriveSignatureNonce(SecretKey master,
-			boolean alice) {
-		return macKdf(master, alice ? A_SIG_NONCE : B_SIG_NONCE);
+	public SecretKey deriveKey(String label, SecretKey k, byte[]... inputs) {
+		byte[] mac = mac(label, k, inputs);
+		if (mac.length != SecretKey.LENGTH) throw new IllegalStateException();
+		return new SecretKey(mac);
 	}
 
 	@Override
-	public byte[] deriveKeyCommitment(byte[] publicKey) {
-		byte[] hash = hash(COMMIT, publicKey);
-		// The output is the first COMMIT_LENGTH bytes of the hash
-		byte[] commitment = new byte[COMMIT_LENGTH];
-		System.arraycopy(hash, 0, commitment, 0, COMMIT_LENGTH);
-		return commitment;
-	}
-
-	@Override
-	public SecretKey deriveSharedSecret(byte[] theirPublicKey,
+	public SecretKey deriveSharedSecret(String label, PublicKey theirPublicKey,
 			KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException {
 		PrivateKey ourPriv = ourKeyPair.getPrivate();
-		PublicKey theirPub = agreementKeyParser.parsePublicKey(theirPublicKey);
-		byte[] raw = performRawKeyAgreement(ourPriv, theirPub);
+		byte[] raw = performRawKeyAgreement(ourPriv, theirPublicKey);
 		byte[] alicePub, bobPub;
 		if (alice) {
 			alicePub = ourKeyPair.getPublic().getEncoded();
-			bobPub = theirPublicKey;
-		} else {
-			alicePub = theirPublicKey;
-			bobPub = ourKeyPair.getPublic().getEncoded();
-		}
-		return new SecretKey(hash(SHARED_SECRET, raw, alicePub, bobPub));
-	}
-
-	@Override
-	public byte[] deriveConfirmationRecord(SecretKey sharedSecret,
-			byte[] theirPayload, byte[] ourPayload, byte[] theirPublicKey,
-			KeyPair ourKeyPair, boolean alice, boolean aliceRecord) {
-		SecretKey ck = new SecretKey(macKdf(sharedSecret, CONFIRMATION_KEY));
-		byte[] alicePayload, alicePub, bobPayload, bobPub;
-		if (alice) {
-			alicePayload = ourPayload;
-			alicePub = ourKeyPair.getPublic().getEncoded();
-			bobPayload = theirPayload;
-			bobPub = theirPublicKey;
+			bobPub = theirPublicKey.getEncoded();
 		} else {
-			alicePayload = theirPayload;
-			alicePub = theirPublicKey;
-			bobPayload = ourPayload;
+			alicePub = theirPublicKey.getEncoded();
 			bobPub = ourKeyPair.getPublic().getEncoded();
 		}
-		if (aliceRecord)
-			return macKdf(ck, alicePayload, alicePub, bobPayload, bobPub);
-		else
-			return macKdf(ck, bobPayload, bobPub, alicePayload, alicePub);
-	}
-
-	@Override
-	public SecretKey deriveMasterSecret(SecretKey sharedSecret) {
-		return new SecretKey(macKdf(sharedSecret, MASTER_KEY));
-	}
-
-	@Override
-	public SecretKey deriveMasterSecret(byte[] theirPublicKey,
-			KeyPair ourKeyPair, boolean alice) throws GeneralSecurityException {
-		return deriveMasterSecret(deriveSharedSecret(
-				theirPublicKey, ourKeyPair, alice));
-	}
-
-	@Override
-	public TransportKeys deriveTransportKeys(TransportId t,
-			SecretKey master, long rotationPeriod, boolean alice) {
-		// Keys for the previous period are derived from the master secret
-		SecretKey inTagPrev = deriveTagKey(master, t, !alice);
-		SecretKey inHeaderPrev = deriveHeaderKey(master, t, !alice);
-		SecretKey outTagPrev = deriveTagKey(master, t, alice);
-		SecretKey outHeaderPrev = deriveHeaderKey(master, t, alice);
-		// Derive the keys for the current and next periods
-		SecretKey inTagCurr = rotateKey(inTagPrev, rotationPeriod);
-		SecretKey inHeaderCurr = rotateKey(inHeaderPrev, rotationPeriod);
-		SecretKey inTagNext = rotateKey(inTagCurr, rotationPeriod + 1);
-		SecretKey inHeaderNext = rotateKey(inHeaderCurr, rotationPeriod + 1);
-		SecretKey outTagCurr = rotateKey(outTagPrev, rotationPeriod);
-		SecretKey outHeaderCurr = rotateKey(outHeaderPrev, rotationPeriod);
-		// Initialise the reordering windows and stream counters
-		IncomingKeys inPrev = new IncomingKeys(inTagPrev, inHeaderPrev,
-				rotationPeriod - 1);
-		IncomingKeys inCurr = new IncomingKeys(inTagCurr, inHeaderCurr,
-				rotationPeriod);
-		IncomingKeys inNext = new IncomingKeys(inTagNext, inHeaderNext,
-				rotationPeriod + 1);
-		OutgoingKeys outCurr = new OutgoingKeys(outTagCurr, outHeaderCurr,
-				rotationPeriod);
-		// Collect and return the keys
-		return new TransportKeys(t, inPrev, inCurr, inNext, outCurr);
-	}
-
-	@Override
-	public TransportKeys rotateTransportKeys(TransportKeys k,
-			long rotationPeriod) {
-		if (k.getRotationPeriod() >= rotationPeriod) return k;
-		IncomingKeys inPrev = k.getPreviousIncomingKeys();
-		IncomingKeys inCurr = k.getCurrentIncomingKeys();
-		IncomingKeys inNext = k.getNextIncomingKeys();
-		OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
-		long startPeriod = outCurr.getRotationPeriod();
-		// Rotate the keys
-		for (long p = startPeriod + 1; p <= rotationPeriod; p++) {
-			inPrev = inCurr;
-			inCurr = inNext;
-			SecretKey inNextTag = rotateKey(inNext.getTagKey(), p + 1);
-			SecretKey inNextHeader = rotateKey(inNext.getHeaderKey(), p + 1);
-			inNext = new IncomingKeys(inNextTag, inNextHeader, p + 1);
-			SecretKey outCurrTag = rotateKey(outCurr.getTagKey(), p);
-			SecretKey outCurrHeader = rotateKey(outCurr.getHeaderKey(), p);
-			outCurr = new OutgoingKeys(outCurrTag, outCurrHeader, p);
-		}
-		// Collect and return the keys
-		return new TransportKeys(k.getTransportId(), inPrev, inCurr, inNext,
-				outCurr);
-	}
-
-	private SecretKey rotateKey(SecretKey k, long rotationPeriod) {
-		byte[] period = new byte[INT_64_BYTES];
-		ByteUtils.writeUint64(rotationPeriod, period, 0);
-		return new SecretKey(macKdf(k, ROTATE, period));
-	}
-
-	private SecretKey deriveTagKey(SecretKey master, TransportId t,
-			boolean alice) {
-		byte[] id = StringUtils.toUtf8(t.getString());
-		return new SecretKey(macKdf(master, alice ? A_TAG : B_TAG, id));
-	}
-
-	private SecretKey deriveHeaderKey(SecretKey master, TransportId t,
-			boolean alice) {
-		byte[] id = StringUtils.toUtf8(t.getString());
-		return new SecretKey(macKdf(master, alice ? A_HEADER : B_HEADER, id));
-	}
-
-	@Override
-	public void encodeTag(byte[] tag, SecretKey tagKey, int protocolVersion,
-			long streamNumber) {
-		if (tag.length < TAG_LENGTH) throw new IllegalArgumentException();
-		if (protocolVersion < 0 || protocolVersion > MAX_16_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		if (streamNumber < 0 || streamNumber > MAX_32_BIT_UNSIGNED)
-			throw new IllegalArgumentException();
-		// Initialise the PRF
-		Digest prf = new Blake2sDigest(tagKey.getBytes());
-		// The output of the PRF must be long enough to use as a tag
-		int macLength = prf.getDigestSize();
-		if (macLength < TAG_LENGTH) throw new IllegalStateException();
-		// The input is the protocol version as a 16-bit integer, followed by
-		// the stream number as a 64-bit integer
-		byte[] protocolVersionBytes = new byte[INT_16_BYTES];
-		ByteUtils.writeUint16(protocolVersion, protocolVersionBytes, 0);
-		prf.update(protocolVersionBytes, 0, protocolVersionBytes.length);
-		byte[] streamNumberBytes = new byte[INT_64_BYTES];
-		ByteUtils.writeUint64(streamNumber, streamNumberBytes, 0);
-		prf.update(streamNumberBytes, 0, streamNumberBytes.length);
-		byte[] mac = new byte[macLength];
-		prf.doFinal(mac, 0);
-		// The output is the first TAG_LENGTH bytes of the MAC
-		System.arraycopy(mac, 0, tag, 0, TAG_LENGTH);
+		return new SecretKey(hash(label, raw, alicePub, bobPub));
 	}
 
 	@Override
@@ -513,14 +316,13 @@ class CryptoComponentImpl implements CryptoComponent {
 	}
 
 	@Override
-	public int getHashLength() {
-		return HASH_SIZE;
-	}
-
-	@Override
-	public byte[] mac(SecretKey macKey, byte[]... inputs) {
+	public byte[] mac(String label, SecretKey macKey, byte[]... inputs) {
+		byte[] labelBytes = StringUtils.toUtf8(label);
 		Digest mac = new Blake2sDigest(macKey.getBytes());
 		byte[] length = new byte[INT_32_BYTES];
+		ByteUtils.writeUint32(labelBytes.length, length, 0);
+		mac.update(length, 0, length.length);
+		mac.update(labelBytes, 0, labelBytes.length);
 		for (byte[] input : inputs) {
 			ByteUtils.writeUint32(input.length, length, 0);
 			mac.update(length, 0, length.length);
@@ -612,30 +414,6 @@ class CryptoComponentImpl implements CryptoComponent {
 		return AsciiArmour.wrap(b, lineLength);
 	}
 
-	// Key derivation function based on a pseudo-random function - see
-	// NIST SP 800-108, section 5.1
-	private byte[] macKdf(SecretKey key, byte[]... inputs) {
-		// Initialise the PRF
-		Digest prf = new Blake2sDigest(key.getBytes());
-		// The output of the PRF must be long enough to use as a key
-		int macLength = prf.getDigestSize();
-		if (macLength < SecretKey.LENGTH) throw new IllegalStateException();
-		// Calculate the PRF over the concatenated length-prefixed inputs
-		byte[] length = new byte[INT_32_BYTES];
-		for (byte[] input : inputs) {
-			ByteUtils.writeUint32(input.length, length, 0);
-			prf.update(length, 0, length.length);
-			prf.update(input, 0, input.length);
-		}
-		byte[] mac = new byte[macLength];
-		prf.doFinal(mac, 0);
-		// The output is the first SecretKey.LENGTH bytes of the MAC
-		if (mac.length == SecretKey.LENGTH) return mac;
-		byte[] truncated = new byte[SecretKey.LENGTH];
-		System.arraycopy(mac, 0, truncated, 0, truncated.length);
-		return truncated;
-	}
-
 	// Password-based key derivation function - see PKCS#5 v2.1, section 5.2
 	private byte[] pbkdf2(String password, byte[] salt, int iterations) {
 		byte[] utf8 = StringUtils.toUtf8(password);
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java
index 37c37abf3ee2ad5d7fa3b22edbb323f91b6f2629..a40b656282556a5660b23973a5b611f4793dec3e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/CryptoModule.java
@@ -3,9 +3,11 @@ package org.briarproject.bramble.crypto;
 import org.briarproject.bramble.TimeLoggingExecutor;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.CryptoExecutor;
+import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
 import org.briarproject.bramble.api.crypto.PasswordStrengthEstimator;
 import org.briarproject.bramble.api.crypto.StreamDecrypterFactory;
 import org.briarproject.bramble.api.crypto.StreamEncrypterFactory;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
 import org.briarproject.bramble.api.system.SecureRandomProvider;
 
@@ -74,6 +76,12 @@ public class CryptoModule {
 		return new PasswordStrengthEstimatorImpl();
 	}
 
+	@Provides
+	TransportCrypto provideTransportCrypto(
+			TransportCryptoImpl transportCrypto) {
+		return transportCrypto;
+	}
+
 	@Provides
 	StreamDecrypterFactory provideStreamDecrypterFactory(
 			Provider<AuthenticatedCipher> cipherProvider) {
@@ -81,9 +89,17 @@ public class CryptoModule {
 	}
 
 	@Provides
-	StreamEncrypterFactory provideStreamEncrypterFactory(CryptoComponent crypto,
+	StreamEncrypterFactory provideStreamEncrypterFactory(
+			CryptoComponent crypto, TransportCrypto transportCrypto,
 			Provider<AuthenticatedCipher> cipherProvider) {
-		return new StreamEncrypterFactoryImpl(crypto, cipherProvider);
+		return new StreamEncrypterFactoryImpl(crypto, transportCrypto,
+				cipherProvider);
+	}
+
+	@Provides
+	KeyAgreementCrypto provideKeyAgreementCrypto(
+			KeyAgreementCryptoImpl keyAgreementCrypto) {
+		return keyAgreementCrypto;
 	}
 
 	@Provides
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/KeyAgreementCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/KeyAgreementCryptoImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..db2f19a7bc604b1200e47dfda0ed6e855fc2fcef
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/KeyAgreementCryptoImpl.java
@@ -0,0 +1,56 @@
+package org.briarproject.bramble.crypto;
+
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
+import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.PublicKey;
+import org.briarproject.bramble.api.crypto.SecretKey;
+
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
+
+class KeyAgreementCryptoImpl implements KeyAgreementCrypto {
+
+	private final CryptoComponent crypto;
+
+	@Inject
+	KeyAgreementCryptoImpl(CryptoComponent crypto) {
+		this.crypto = crypto;
+	}
+
+	@Override
+	public byte[] deriveKeyCommitment(PublicKey publicKey) {
+		byte[] hash = crypto.hash(COMMIT_LABEL, publicKey.getEncoded());
+		// The output is the first COMMIT_LENGTH bytes of the hash
+		byte[] commitment = new byte[COMMIT_LENGTH];
+		System.arraycopy(hash, 0, commitment, 0, COMMIT_LENGTH);
+		return commitment;
+	}
+
+	@Override
+	public byte[] deriveConfirmationRecord(SecretKey sharedSecret,
+			byte[] theirPayload, byte[] ourPayload, PublicKey theirPublicKey,
+			KeyPair ourKeyPair, boolean alice, boolean aliceRecord) {
+		SecretKey ck = crypto.deriveKey(CONFIRMATION_KEY_LABEL, sharedSecret);
+		byte[] alicePayload, alicePub, bobPayload, bobPub;
+		if (alice) {
+			alicePayload = ourPayload;
+			alicePub = ourKeyPair.getPublic().getEncoded();
+			bobPayload = theirPayload;
+			bobPub = theirPublicKey.getEncoded();
+		} else {
+			alicePayload = theirPayload;
+			alicePub = theirPublicKey.getEncoded();
+			bobPayload = ourPayload;
+			bobPub = ourKeyPair.getPublic().getEncoded();
+		}
+		if (aliceRecord) {
+			return crypto.mac(CONFIRMATION_MAC_LABEL, ck, alicePayload,
+					alicePub, bobPayload, bobPub);
+		} else {
+			return crypto.mac(CONFIRMATION_MAC_LABEL, ck, bobPayload, bobPub,
+					alicePayload, alicePub);
+		}
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/StreamEncrypterFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/StreamEncrypterFactoryImpl.java
index 0d9b4a0fc2bf65d2732d649575ed63bdbcd4d764..984d5e826271191676224b872ce5e95037ff12d0 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/crypto/StreamEncrypterFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/StreamEncrypterFactoryImpl.java
@@ -4,6 +4,7 @@ import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.crypto.StreamEncrypter;
 import org.briarproject.bramble.api.crypto.StreamEncrypterFactory;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.api.transport.StreamContext;
 
@@ -22,12 +23,15 @@ import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENG
 class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
 
 	private final CryptoComponent crypto;
+	private final TransportCrypto transportCrypto;
 	private final Provider<AuthenticatedCipher> cipherProvider;
 
 	@Inject
 	StreamEncrypterFactoryImpl(CryptoComponent crypto,
+			TransportCrypto transportCrypto,
 			Provider<AuthenticatedCipher> cipherProvider) {
 		this.crypto = crypto;
+		this.transportCrypto = transportCrypto;
 		this.cipherProvider = cipherProvider;
 	}
 
@@ -37,7 +41,8 @@ class StreamEncrypterFactoryImpl implements StreamEncrypterFactory {
 		AuthenticatedCipher cipher = cipherProvider.get();
 		long streamNumber = ctx.getStreamNumber();
 		byte[] tag = new byte[TAG_LENGTH];
-		crypto.encodeTag(tag, ctx.getTagKey(), PROTOCOL_VERSION, streamNumber);
+		transportCrypto.encodeTag(tag, ctx.getTagKey(), PROTOCOL_VERSION,
+				streamNumber);
 		byte[] streamHeaderNonce = new byte[STREAM_HEADER_NONCE_LENGTH];
 		crypto.getSecureRandom().nextBytes(streamHeaderNonce);
 		SecretKey frameKey = crypto.generateSecretKey();
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/crypto/TransportCryptoImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/crypto/TransportCryptoImpl.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea992491716e6f0e6f57231703086857aacefc6b
--- /dev/null
+++ b/bramble-core/src/main/java/org/briarproject/bramble/crypto/TransportCryptoImpl.java
@@ -0,0 +1,135 @@
+package org.briarproject.bramble.crypto;
+
+import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
+import org.briarproject.bramble.api.plugin.TransportId;
+import org.briarproject.bramble.api.transport.IncomingKeys;
+import org.briarproject.bramble.api.transport.OutgoingKeys;
+import org.briarproject.bramble.api.transport.TransportKeys;
+import org.briarproject.bramble.util.ByteUtils;
+import org.briarproject.bramble.util.StringUtils;
+import org.spongycastle.crypto.Digest;
+
+import javax.inject.Inject;
+
+import static org.briarproject.bramble.api.transport.TransportConstants.ALICE_HEADER_LABEL;
+import static org.briarproject.bramble.api.transport.TransportConstants.ALICE_TAG_LABEL;
+import static org.briarproject.bramble.api.transport.TransportConstants.BOB_HEADER_LABEL;
+import static org.briarproject.bramble.api.transport.TransportConstants.BOB_TAG_LABEL;
+import static org.briarproject.bramble.api.transport.TransportConstants.ROTATE_LABEL;
+import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
+import static org.briarproject.bramble.util.ByteUtils.INT_16_BYTES;
+import static org.briarproject.bramble.util.ByteUtils.INT_64_BYTES;
+import static org.briarproject.bramble.util.ByteUtils.MAX_16_BIT_UNSIGNED;
+import static org.briarproject.bramble.util.ByteUtils.MAX_32_BIT_UNSIGNED;
+
+class TransportCryptoImpl implements TransportCrypto {
+
+	private final CryptoComponent crypto;
+
+	@Inject
+	TransportCryptoImpl(CryptoComponent crypto) {
+		this.crypto = crypto;
+	}
+
+	@Override
+	public TransportKeys deriveTransportKeys(TransportId t,
+			SecretKey master, long rotationPeriod, boolean alice) {
+		// Keys for the previous period are derived from the master secret
+		SecretKey inTagPrev = deriveTagKey(master, t, !alice);
+		SecretKey inHeaderPrev = deriveHeaderKey(master, t, !alice);
+		SecretKey outTagPrev = deriveTagKey(master, t, alice);
+		SecretKey outHeaderPrev = deriveHeaderKey(master, t, alice);
+		// Derive the keys for the current and next periods
+		SecretKey inTagCurr = rotateKey(inTagPrev, rotationPeriod);
+		SecretKey inHeaderCurr = rotateKey(inHeaderPrev, rotationPeriod);
+		SecretKey inTagNext = rotateKey(inTagCurr, rotationPeriod + 1);
+		SecretKey inHeaderNext = rotateKey(inHeaderCurr, rotationPeriod + 1);
+		SecretKey outTagCurr = rotateKey(outTagPrev, rotationPeriod);
+		SecretKey outHeaderCurr = rotateKey(outHeaderPrev, rotationPeriod);
+		// Initialise the reordering windows and stream counters
+		IncomingKeys inPrev = new IncomingKeys(inTagPrev, inHeaderPrev,
+				rotationPeriod - 1);
+		IncomingKeys inCurr = new IncomingKeys(inTagCurr, inHeaderCurr,
+				rotationPeriod);
+		IncomingKeys inNext = new IncomingKeys(inTagNext, inHeaderNext,
+				rotationPeriod + 1);
+		OutgoingKeys outCurr = new OutgoingKeys(outTagCurr, outHeaderCurr,
+				rotationPeriod);
+		// Collect and return the keys
+		return new TransportKeys(t, inPrev, inCurr, inNext, outCurr);
+	}
+
+	@Override
+	public TransportKeys rotateTransportKeys(TransportKeys k,
+			long rotationPeriod) {
+		if (k.getRotationPeriod() >= rotationPeriod) return k;
+		IncomingKeys inPrev = k.getPreviousIncomingKeys();
+		IncomingKeys inCurr = k.getCurrentIncomingKeys();
+		IncomingKeys inNext = k.getNextIncomingKeys();
+		OutgoingKeys outCurr = k.getCurrentOutgoingKeys();
+		long startPeriod = outCurr.getRotationPeriod();
+		// Rotate the keys
+		for (long p = startPeriod + 1; p <= rotationPeriod; p++) {
+			inPrev = inCurr;
+			inCurr = inNext;
+			SecretKey inNextTag = rotateKey(inNext.getTagKey(), p + 1);
+			SecretKey inNextHeader = rotateKey(inNext.getHeaderKey(), p + 1);
+			inNext = new IncomingKeys(inNextTag, inNextHeader, p + 1);
+			SecretKey outCurrTag = rotateKey(outCurr.getTagKey(), p);
+			SecretKey outCurrHeader = rotateKey(outCurr.getHeaderKey(), p);
+			outCurr = new OutgoingKeys(outCurrTag, outCurrHeader, p);
+		}
+		// Collect and return the keys
+		return new TransportKeys(k.getTransportId(), inPrev, inCurr, inNext,
+				outCurr);
+	}
+
+	private SecretKey rotateKey(SecretKey k, long rotationPeriod) {
+		byte[] period = new byte[INT_64_BYTES];
+		ByteUtils.writeUint64(rotationPeriod, period, 0);
+		return crypto.deriveKey(ROTATE_LABEL, k, period);
+	}
+
+	private SecretKey deriveTagKey(SecretKey master, TransportId t,
+			boolean alice) {
+		String label = alice ? ALICE_TAG_LABEL : BOB_TAG_LABEL;
+		byte[] id = StringUtils.toUtf8(t.getString());
+		return crypto.deriveKey(label, master, id);
+	}
+
+	private SecretKey deriveHeaderKey(SecretKey master, TransportId t,
+			boolean alice) {
+		String label = alice ? ALICE_HEADER_LABEL : BOB_HEADER_LABEL;
+		byte[] id = StringUtils.toUtf8(t.getString());
+		return crypto.deriveKey(label, master, id);
+	}
+
+	@Override
+	public void encodeTag(byte[] tag, SecretKey tagKey, int protocolVersion,
+			long streamNumber) {
+		if (tag.length < TAG_LENGTH) throw new IllegalArgumentException();
+		if (protocolVersion < 0 || protocolVersion > MAX_16_BIT_UNSIGNED)
+			throw new IllegalArgumentException();
+		if (streamNumber < 0 || streamNumber > MAX_32_BIT_UNSIGNED)
+			throw new IllegalArgumentException();
+		// Initialise the PRF
+		Digest prf = new Blake2sDigest(tagKey.getBytes());
+		// The output of the PRF must be long enough to use as a tag
+		int macLength = prf.getDigestSize();
+		if (macLength < TAG_LENGTH) throw new IllegalStateException();
+		// The input is the protocol version as a 16-bit integer, followed by
+		// the stream number as a 64-bit integer
+		byte[] protocolVersionBytes = new byte[INT_16_BYTES];
+		ByteUtils.writeUint16(protocolVersion, protocolVersionBytes, 0);
+		prf.update(protocolVersionBytes, 0, protocolVersionBytes.length);
+		byte[] streamNumberBytes = new byte[INT_64_BYTES];
+		ByteUtils.writeUint64(streamNumber, streamNumberBytes, 0);
+		prf.update(streamNumberBytes, 0, streamNumberBytes.length);
+		byte[] mac = new byte[macLength];
+		prf.doFinal(mac, 0);
+		// The output is the first TAG_LENGTH bytes of the MAC
+		System.arraycopy(mac, 0, tag, 0, TAG_LENGTH);
+	}
+}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java
index 992cb006b1daa923e021326066208b87fab7b966..89bf01ee0d8fd0d88cbb8b4c14103b5b752d1ed0 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementConnector.java
@@ -1,6 +1,6 @@
 package org.briarproject.bramble.keyagreement;
 
-import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
 import org.briarproject.bramble.api.crypto.KeyPair;
 import org.briarproject.bramble.api.data.BdfList;
 import org.briarproject.bramble.api.keyagreement.KeyAgreementConnection;
@@ -46,7 +46,7 @@ class KeyAgreementConnector {
 
 	private final Callbacks callbacks;
 	private final Clock clock;
-	private final CryptoComponent crypto;
+	private final KeyAgreementCrypto keyAgreementCrypto;
 	private final PluginManager pluginManager;
 	private final CompletionService<KeyAgreementConnection> connect;
 
@@ -58,11 +58,11 @@ class KeyAgreementConnector {
 	private volatile boolean alice = false;
 
 	KeyAgreementConnector(Callbacks callbacks, Clock clock,
-			CryptoComponent crypto, PluginManager pluginManager,
+			KeyAgreementCrypto keyAgreementCrypto, PluginManager pluginManager,
 			Executor ioExecutor) {
 		this.callbacks = callbacks;
 		this.clock = clock;
-		this.crypto = crypto;
+		this.keyAgreementCrypto = keyAgreementCrypto;
 		this.pluginManager = pluginManager;
 		connect = new ExecutorCompletionService<>(ioExecutor);
 	}
@@ -70,8 +70,8 @@ class KeyAgreementConnector {
 	public Payload listen(KeyPair localKeyPair) {
 		LOG.info("Starting BQP listeners");
 		// Derive commitment
-		byte[] commitment = crypto.deriveKeyCommitment(
-				localKeyPair.getPublic().getEncoded());
+		byte[] commitment = keyAgreementCrypto.deriveKeyCommitment(
+				localKeyPair.getPublic());
 		// Start all listeners and collect their descriptors
 		List<TransportDescriptor> descriptors = new ArrayList<>();
 		for (DuplexPlugin plugin : pluginManager.getKeyAgreementPlugins()) {
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementModule.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementModule.java
index 9875387db916d282d9bc382077b25d37a8d40286..e7ec82dabb1b8b010c73e5188ef00f8d536714d3 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementModule.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementModule.java
@@ -1,19 +1,10 @@
 package org.briarproject.bramble.keyagreement;
 
-import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.data.BdfReaderFactory;
 import org.briarproject.bramble.api.data.BdfWriterFactory;
-import org.briarproject.bramble.api.event.EventBus;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
 import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
 import org.briarproject.bramble.api.keyagreement.PayloadParser;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
-import org.briarproject.bramble.api.plugin.PluginManager;
-import org.briarproject.bramble.api.system.Clock;
-
-import java.util.concurrent.Executor;
-
-import javax.inject.Singleton;
 
 import dagger.Module;
 import dagger.Provides;
@@ -22,13 +13,9 @@ import dagger.Provides;
 public class KeyAgreementModule {
 
 	@Provides
-	@Singleton
-	KeyAgreementTaskFactory provideKeyAgreementTaskFactory(Clock clock,
-			CryptoComponent crypto, EventBus eventBus,
-			@IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder,
-			PluginManager pluginManager) {
-		return new KeyAgreementTaskFactoryImpl(clock, crypto, eventBus,
-				ioExecutor, payloadEncoder, pluginManager);
+	KeyAgreementTask provideKeyAgreementTask(
+			KeyAgreementTaskImpl keyAgreementTask) {
+		return keyAgreementTask;
 	}
 
 	@Provides
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocol.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocol.java
index 3fb1c8b16e7cbad7e401d5c7ca703e58634dfcc4..a5c24c6e4231101717ba2fc892f98875196b666c 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocol.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocol.java
@@ -1,7 +1,10 @@
 package org.briarproject.bramble.keyagreement;
 
 import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
 import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.KeyParser;
+import org.briarproject.bramble.api.crypto.PublicKey;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.keyagreement.Payload;
 import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
@@ -11,6 +14,9 @@ import java.io.IOException;
 import java.security.GeneralSecurityException;
 import java.util.Arrays;
 
+import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.MASTER_SECRET_LABEL;
+import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.SHARED_SECRET_LABEL;
+
 /**
  * Implementation of the BQP protocol.
  * <p/>
@@ -57,6 +63,7 @@ class KeyAgreementProtocol {
 
 	private final Callbacks callbacks;
 	private final CryptoComponent crypto;
+	private final KeyAgreementCrypto keyAgreementCrypto;
 	private final PayloadEncoder payloadEncoder;
 	private final KeyAgreementTransport transport;
 	private final Payload theirPayload, ourPayload;
@@ -64,11 +71,13 @@ class KeyAgreementProtocol {
 	private final boolean alice;
 
 	KeyAgreementProtocol(Callbacks callbacks, CryptoComponent crypto,
+			KeyAgreementCrypto keyAgreementCrypto,
 			PayloadEncoder payloadEncoder, KeyAgreementTransport transport,
 			Payload theirPayload, Payload ourPayload, KeyPair ourKeyPair,
 			boolean alice) {
 		this.callbacks = callbacks;
 		this.crypto = crypto;
+		this.keyAgreementCrypto = keyAgreementCrypto;
 		this.payloadEncoder = payloadEncoder;
 		this.transport = transport;
 		this.theirPayload = theirPayload;
@@ -86,7 +95,7 @@ class KeyAgreementProtocol {
 	 */
 	SecretKey perform() throws AbortException, IOException {
 		try {
-			byte[] theirPublicKey;
+			PublicKey theirPublicKey;
 			if (alice) {
 				sendKey();
 				// Alice waits here until Bob obtains her payload.
@@ -104,7 +113,7 @@ class KeyAgreementProtocol {
 				receiveConfirm(s, theirPublicKey);
 				sendConfirm(s, theirPublicKey);
 			}
-			return crypto.deriveMasterSecret(s);
+			return crypto.deriveKey(MASTER_SECRET_LABEL, s);
 		} catch (AbortException e) {
 			sendAbort(e.getCause() != null);
 			throw e;
@@ -115,27 +124,34 @@ class KeyAgreementProtocol {
 		transport.sendKey(ourKeyPair.getPublic().getEncoded());
 	}
 
-	private byte[] receiveKey() throws AbortException {
-		byte[] publicKey = transport.receiveKey();
+	private PublicKey receiveKey() throws AbortException {
+		byte[] publicKeyBytes = transport.receiveKey();
 		callbacks.initialRecordReceived();
-		byte[] expected = crypto.deriveKeyCommitment(publicKey);
-		if (!Arrays.equals(expected, theirPayload.getCommitment()))
+		KeyParser keyParser = crypto.getAgreementKeyParser();
+		try {
+			PublicKey publicKey = keyParser.parsePublicKey(publicKeyBytes);
+			byte[] expected = keyAgreementCrypto.deriveKeyCommitment(publicKey);
+			if (!Arrays.equals(expected, theirPayload.getCommitment()))
+				throw new AbortException();
+			return publicKey;
+		} catch (GeneralSecurityException e) {
 			throw new AbortException();
-		return publicKey;
+		}
 	}
 
-	private SecretKey deriveSharedSecret(byte[] theirPublicKey)
+	private SecretKey deriveSharedSecret(PublicKey theirPublicKey)
 			throws AbortException {
 		try {
-			return crypto.deriveSharedSecret(theirPublicKey, ourKeyPair, alice);
+			return crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
+					theirPublicKey, ourKeyPair, alice);
 		} catch (GeneralSecurityException e) {
 			throw new AbortException(e);
 		}
 	}
 
-	private void sendConfirm(SecretKey s, byte[] theirPublicKey)
+	private void sendConfirm(SecretKey s, PublicKey theirPublicKey)
 			throws IOException {
-		byte[] confirm = crypto.deriveConfirmationRecord(s,
+		byte[] confirm = keyAgreementCrypto.deriveConfirmationRecord(s,
 				payloadEncoder.encode(theirPayload),
 				payloadEncoder.encode(ourPayload),
 				theirPublicKey, ourKeyPair,
@@ -143,10 +159,10 @@ class KeyAgreementProtocol {
 		transport.sendConfirm(confirm);
 	}
 
-	private void receiveConfirm(SecretKey s, byte[] theirPublicKey)
+	private void receiveConfirm(SecretKey s, PublicKey theirPublicKey)
 			throws AbortException {
 		byte[] confirm = transport.receiveConfirm();
-		byte[] expected = crypto.deriveConfirmationRecord(s,
+		byte[] expected = keyAgreementCrypto.deriveConfirmationRecord(s,
 				payloadEncoder.encode(theirPayload),
 				payloadEncoder.encode(ourPayload),
 				theirPublicKey, ourKeyPair,
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskFactoryImpl.java
deleted file mode 100644
index 4d9dc00cf684cb5d2cf4d2454719768d218015f0..0000000000000000000000000000000000000000
--- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskFactoryImpl.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.briarproject.bramble.keyagreement;
-
-import org.briarproject.bramble.api.crypto.CryptoComponent;
-import org.briarproject.bramble.api.event.EventBus;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
-import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
-import org.briarproject.bramble.api.lifecycle.IoExecutor;
-import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
-import org.briarproject.bramble.api.plugin.PluginManager;
-import org.briarproject.bramble.api.system.Clock;
-
-import java.util.concurrent.Executor;
-
-import javax.annotation.concurrent.Immutable;
-import javax.inject.Inject;
-
-@Immutable
-@NotNullByDefault
-class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory {
-
-	private final Clock clock;
-	private final CryptoComponent crypto;
-	private final EventBus eventBus;
-	private final Executor ioExecutor;
-	private final PayloadEncoder payloadEncoder;
-	private final PluginManager pluginManager;
-
-	@Inject
-	KeyAgreementTaskFactoryImpl(Clock clock, CryptoComponent crypto,
-			EventBus eventBus, @IoExecutor Executor ioExecutor,
-			PayloadEncoder payloadEncoder, PluginManager pluginManager) {
-		this.clock = clock;
-		this.crypto = crypto;
-		this.eventBus = eventBus;
-		this.ioExecutor = ioExecutor;
-		this.payloadEncoder = payloadEncoder;
-		this.pluginManager = pluginManager;
-	}
-
-	@Override
-	public KeyAgreementTask createTask() {
-		return new KeyAgreementTaskImpl(clock, crypto, eventBus, payloadEncoder,
-				pluginManager, ioExecutor);
-	}
-}
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskImpl.java
index de6dcecf753b19c351413aa7dfeab9fbc11f8fe7..e0d97313dc9ed6b1e8dc76427b1ea76502ebf50e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/keyagreement/KeyAgreementTaskImpl.java
@@ -1,6 +1,7 @@
 package org.briarproject.bramble.keyagreement;
 
 import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
 import org.briarproject.bramble.api.crypto.KeyPair;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.event.EventBus;
@@ -14,6 +15,7 @@ import org.briarproject.bramble.api.keyagreement.event.KeyAgreementFinishedEvent
 import org.briarproject.bramble.api.keyagreement.event.KeyAgreementListeningEvent;
 import org.briarproject.bramble.api.keyagreement.event.KeyAgreementStartedEvent;
 import org.briarproject.bramble.api.keyagreement.event.KeyAgreementWaitingEvent;
+import org.briarproject.bramble.api.lifecycle.IoExecutor;
 import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
 import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.bramble.api.plugin.PluginManager;
@@ -23,6 +25,8 @@ import java.io.IOException;
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
+import javax.inject.Inject;
+
 import static java.util.logging.Level.WARNING;
 
 @MethodsNotNullByDefault
@@ -35,6 +39,7 @@ class KeyAgreementTaskImpl extends Thread implements
 			Logger.getLogger(KeyAgreementTaskImpl.class.getName());
 
 	private final CryptoComponent crypto;
+	private final KeyAgreementCrypto keyAgreementCrypto;
 	private final EventBus eventBus;
 	private final PayloadEncoder payloadEncoder;
 	private final KeyPair localKeyPair;
@@ -43,14 +48,17 @@ class KeyAgreementTaskImpl extends Thread implements
 	private Payload localPayload;
 	private Payload remotePayload;
 
+	@Inject
 	KeyAgreementTaskImpl(Clock clock, CryptoComponent crypto,
-			EventBus eventBus, PayloadEncoder payloadEncoder,
-			PluginManager pluginManager, Executor ioExecutor) {
+			KeyAgreementCrypto keyAgreementCrypto, EventBus eventBus,
+			PayloadEncoder payloadEncoder, PluginManager pluginManager,
+			@IoExecutor Executor ioExecutor) {
 		this.crypto = crypto;
+		this.keyAgreementCrypto = keyAgreementCrypto;
 		this.eventBus = eventBus;
 		this.payloadEncoder = payloadEncoder;
 		localKeyPair = crypto.generateAgreementKeyPair();
-		connector = new KeyAgreementConnector(this, clock, crypto,
+		connector = new KeyAgreementConnector(this, clock, keyAgreementCrypto,
 				pluginManager, ioExecutor);
 	}
 
@@ -100,8 +108,8 @@ class KeyAgreementTaskImpl extends Thread implements
 		// Run BQP protocol over the connection
 		LOG.info("Starting BQP protocol");
 		KeyAgreementProtocol protocol = new KeyAgreementProtocol(this, crypto,
-				payloadEncoder, transport, remotePayload, localPayload,
-				localKeyPair, alice);
+				keyAgreementCrypto, payloadEncoder, transport, remotePayload,
+				localPayload, localKeyPair, alice);
 		try {
 			SecretKey master = protocol.perform();
 			KeyAgreementResult result =
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerFactoryImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerFactoryImpl.java
index d212a027b98f78304f31e6e2e3b6d9b544fca8b0..3a04f121a656837de2f02789f277d1ea7c65a095 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerFactoryImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerFactoryImpl.java
@@ -1,6 +1,6 @@
 package org.briarproject.bramble.transport;
 
-import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DatabaseExecutor;
 import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
@@ -20,17 +20,18 @@ class TransportKeyManagerFactoryImpl implements
 		TransportKeyManagerFactory {
 
 	private final DatabaseComponent db;
-	private final CryptoComponent crypto;
+	private final TransportCrypto transportCrypto;
 	private final Executor dbExecutor;
 	private final ScheduledExecutorService scheduler;
 	private final Clock clock;
 
 	@Inject
-	TransportKeyManagerFactoryImpl(DatabaseComponent db, CryptoComponent crypto,
+	TransportKeyManagerFactoryImpl(DatabaseComponent db,
+			TransportCrypto transportCrypto,
 			@DatabaseExecutor Executor dbExecutor,
 			@Scheduler ScheduledExecutorService scheduler, Clock clock) {
 		this.db = db;
-		this.crypto = crypto;
+		this.transportCrypto = transportCrypto;
 		this.dbExecutor = dbExecutor;
 		this.scheduler = scheduler;
 		this.clock = clock;
@@ -39,8 +40,8 @@ class TransportKeyManagerFactoryImpl implements
 	@Override
 	public TransportKeyManager createTransportKeyManager(
 			TransportId transportId, long maxLatency) {
-		return new TransportKeyManagerImpl(db, crypto, dbExecutor, scheduler,
-				clock, transportId, maxLatency);
+		return new TransportKeyManagerImpl(db, transportCrypto, dbExecutor,
+				scheduler, clock, transportId, maxLatency);
 	}
 
 }
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerImpl.java
index 95e49f55346b6fc13ce6cf04b668bc5d12a41818..60b48427ff18f07703a147f827ae39639c6f629e 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/transport/TransportKeyManagerImpl.java
@@ -2,8 +2,8 @@ package org.briarproject.bramble.transport;
 
 import org.briarproject.bramble.api.Bytes;
 import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.DbException;
 import org.briarproject.bramble.api.db.Transaction;
@@ -41,7 +41,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 			Logger.getLogger(TransportKeyManagerImpl.class.getName());
 
 	private final DatabaseComponent db;
-	private final CryptoComponent crypto;
+	private final TransportCrypto transportCrypto;
 	private final Executor dbExecutor;
 	private final ScheduledExecutorService scheduler;
 	private final Clock clock;
@@ -54,11 +54,12 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 	private final Map<ContactId, MutableOutgoingKeys> outContexts;
 	private final Map<ContactId, MutableTransportKeys> keys;
 
-	TransportKeyManagerImpl(DatabaseComponent db, CryptoComponent crypto,
-			Executor dbExecutor, @Scheduler ScheduledExecutorService scheduler,
-			Clock clock, TransportId transportId, long maxLatency) {
+	TransportKeyManagerImpl(DatabaseComponent db,
+			TransportCrypto transportCrypto, Executor dbExecutor,
+			@Scheduler ScheduledExecutorService scheduler, Clock clock,
+			TransportId transportId, long maxLatency) {
 		this.db = db;
-		this.crypto = crypto;
+		this.transportCrypto = transportCrypto;
 		this.dbExecutor = dbExecutor;
 		this.scheduler = scheduler;
 		this.clock = clock;
@@ -99,7 +100,8 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 		for (Entry<ContactId, TransportKeys> e : keys.entrySet()) {
 			ContactId c = e.getKey();
 			TransportKeys k = e.getValue();
-			TransportKeys k1 = crypto.rotateTransportKeys(k, rotationPeriod);
+			TransportKeys k1 =
+					transportCrypto.rotateTransportKeys(k, rotationPeriod);
 			if (k1.getRotationPeriod() > k.getRotationPeriod())
 				rotationResult.rotated.put(c, k1);
 			rotationResult.current.put(c, k1);
@@ -127,7 +129,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 		for (long streamNumber : inKeys.getWindow().getUnseen()) {
 			TagContext tagCtx = new TagContext(c, inKeys, streamNumber);
 			byte[] tag = new byte[TAG_LENGTH];
-			crypto.encodeTag(tag, inKeys.getTagKey(), PROTOCOL_VERSION,
+			transportCrypto.encodeTag(tag, inKeys.getTagKey(), PROTOCOL_VERSION,
 					streamNumber);
 			inContexts.put(new Bytes(tag), tagCtx);
 		}
@@ -162,11 +164,11 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 			// Work out what rotation period the timestamp belongs to
 			long rotationPeriod = timestamp / rotationPeriodLength;
 			// Derive the transport keys
-			TransportKeys k = crypto.deriveTransportKeys(transportId, master,
-					rotationPeriod, alice);
+			TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
+					master, rotationPeriod, alice);
 			// Rotate the keys to the current rotation period if necessary
 			rotationPeriod = clock.currentTimeMillis() / rotationPeriodLength;
-			k = crypto.rotateTransportKeys(k, rotationPeriod);
+			k = transportCrypto.rotateTransportKeys(k, rotationPeriod);
 			// Initialise mutable state for the contact
 			addKeys(c, new MutableTransportKeys(k));
 			// Write the keys back to the DB
@@ -234,8 +236,8 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 			// Add tags for any stream numbers added to the window
 			for (long streamNumber : change.getAdded()) {
 				byte[] addTag = new byte[TAG_LENGTH];
-				crypto.encodeTag(addTag, inKeys.getTagKey(), PROTOCOL_VERSION,
-						streamNumber);
+				transportCrypto.encodeTag(addTag, inKeys.getTagKey(),
+						PROTOCOL_VERSION, streamNumber);
 				inContexts.put(new Bytes(addTag), new TagContext(
 						tagCtx.contactId, inKeys, streamNumber));
 			}
@@ -243,7 +245,7 @@ class TransportKeyManagerImpl implements TransportKeyManager {
 			for (long streamNumber : change.getRemoved()) {
 				if (streamNumber == tagCtx.streamNumber) continue;
 				byte[] removeTag = new byte[TAG_LENGTH];
-				crypto.encodeTag(removeTag, inKeys.getTagKey(),
+				transportCrypto.encodeTag(removeTag, inKeys.getTagKey(),
 						PROTOCOL_VERSION, streamNumber);
 				inContexts.remove(new Bytes(removeTag));
 			}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyAgreementTest.java b/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyAgreementTest.java
index ad3b0dad6bb1b3c2d8a5d5cbe0662c4960be26d3..1aec3b78fe14e7f5a5c9923a4698de8edf59f50f 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyAgreementTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyAgreementTest.java
@@ -3,40 +3,25 @@ package org.briarproject.bramble.crypto;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.KeyPair;
 import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.api.system.SecureRandomProvider;
 import org.briarproject.bramble.test.BrambleTestCase;
 import org.briarproject.bramble.test.TestSecureRandomProvider;
 import org.junit.Test;
 
+import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.SHARED_SECRET_LABEL;
 import static org.junit.Assert.assertArrayEquals;
 
 public class KeyAgreementTest extends BrambleTestCase {
 
-	@Test
-	public void testDeriveMasterSecret() throws Exception {
-		SecureRandomProvider
-				secureRandomProvider = new TestSecureRandomProvider();
-		CryptoComponent crypto = new CryptoComponentImpl(secureRandomProvider);
-		KeyPair aPair = crypto.generateAgreementKeyPair();
-		byte[] aPub = aPair.getPublic().getEncoded();
-		KeyPair bPair = crypto.generateAgreementKeyPair();
-		byte[] bPub = bPair.getPublic().getEncoded();
-		SecretKey aMaster = crypto.deriveMasterSecret(aPub, bPair, true);
-		SecretKey bMaster = crypto.deriveMasterSecret(bPub, aPair, false);
-		assertArrayEquals(aMaster.getBytes(), bMaster.getBytes());
-	}
-
 	@Test
 	public void testDeriveSharedSecret() throws Exception {
-		SecureRandomProvider
-				secureRandomProvider = new TestSecureRandomProvider();
-		CryptoComponent crypto = new CryptoComponentImpl(secureRandomProvider);
+		CryptoComponent crypto =
+				new CryptoComponentImpl(new TestSecureRandomProvider());
 		KeyPair aPair = crypto.generateAgreementKeyPair();
-		byte[] aPub = aPair.getPublic().getEncoded();
 		KeyPair bPair = crypto.generateAgreementKeyPair();
-		byte[] bPub = bPair.getPublic().getEncoded();
-		SecretKey aShared = crypto.deriveSharedSecret(bPub, aPair, true);
-		SecretKey bShared = crypto.deriveSharedSecret(aPub, bPair, false);
+		SecretKey aShared = crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
+				bPair.getPublic(), aPair, true);
+		SecretKey bShared = crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
+				aPair.getPublic(), bPair, false);
 		assertArrayEquals(aShared.getBytes(), bShared.getBytes());
 	}
 }
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyDerivationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyDerivationTest.java
index 7a3e67c4c2f5d09a5773585a2122d740e5c24f1d..805816e9233064e35064335069014e438d38670d 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyDerivationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/crypto/KeyDerivationTest.java
@@ -3,11 +3,11 @@ package org.briarproject.bramble.crypto;
 import org.briarproject.bramble.api.Bytes;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.transport.TransportKeys;
 import org.briarproject.bramble.test.BrambleTestCase;
 import org.briarproject.bramble.test.TestSecureRandomProvider;
-import org.briarproject.bramble.test.TestUtils;
 import org.junit.Test;
 
 import java.util.ArrayList;
@@ -16,35 +16,34 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 public class KeyDerivationTest extends BrambleTestCase {
 
+	private final CryptoComponent crypto =
+			new CryptoComponentImpl(new TestSecureRandomProvider());
+	private final TransportCrypto transportCrypto =
+			new TransportCryptoImpl(crypto);
 	private final TransportId transportId = new TransportId("id");
-	private final CryptoComponent crypto;
-	private final SecretKey master;
-
-	public KeyDerivationTest() {
-		crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
-		master = TestUtils.getSecretKey();
-	}
+	private final SecretKey master = getSecretKey();
 
 	@Test
 	public void testKeysAreDistinct() {
-		TransportKeys k = crypto.deriveTransportKeys(transportId, master,
-				123, true);
+		TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, true);
 		assertAllDifferent(k);
 	}
 
 	@Test
 	public void testCurrentKeysMatchCurrentKeysOfContact() {
 		// Start in rotation period 123
-		TransportKeys kA = crypto.deriveTransportKeys(transportId, master,
-				123, true);
-		TransportKeys kB = crypto.deriveTransportKeys(transportId, master,
-				123, false);
+		TransportKeys kA = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, true);
+		TransportKeys kB = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, false);
 		// Alice's incoming keys should equal Bob's outgoing keys
 		assertArrayEquals(kA.getCurrentIncomingKeys().getTagKey().getBytes(),
 				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
@@ -56,8 +55,8 @@ public class KeyDerivationTest extends BrambleTestCase {
 		assertArrayEquals(kA.getCurrentOutgoingKeys().getHeaderKey().getBytes(),
 				kB.getCurrentIncomingKeys().getHeaderKey().getBytes());
 		// Rotate into the future
-		kA = crypto.rotateTransportKeys(kA, 456);
-		kB = crypto.rotateTransportKeys(kB, 456);
+		kA = transportCrypto.rotateTransportKeys(kA, 456);
+		kB = transportCrypto.rotateTransportKeys(kB, 456);
 		// Alice's incoming keys should equal Bob's outgoing keys
 		assertArrayEquals(kA.getCurrentIncomingKeys().getTagKey().getBytes(),
 				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
@@ -73,22 +72,23 @@ public class KeyDerivationTest extends BrambleTestCase {
 	@Test
 	public void testPreviousKeysMatchPreviousKeysOfContact() {
 		// Start in rotation period 123
-		TransportKeys kA = crypto.deriveTransportKeys(transportId, master,
-				123, true);
-		TransportKeys kB = crypto.deriveTransportKeys(transportId, master,
-				123, false);
+		TransportKeys kA = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, true);
+		TransportKeys kB = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, false);
 		// Compare Alice's previous keys in period 456 with Bob's current keys
 		// in period 455
-		kA = crypto.rotateTransportKeys(kA, 456);
-		kB = crypto.rotateTransportKeys(kB, 455);
+		kA = transportCrypto.rotateTransportKeys(kA, 456);
+		kB = transportCrypto.rotateTransportKeys(kB, 455);
 		// Alice's previous incoming keys should equal Bob's outgoing keys
 		assertArrayEquals(kA.getPreviousIncomingKeys().getTagKey().getBytes(),
 				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
-		assertArrayEquals(kA.getPreviousIncomingKeys().getHeaderKey().getBytes(),
+		assertArrayEquals(
+				kA.getPreviousIncomingKeys().getHeaderKey().getBytes(),
 				kB.getCurrentOutgoingKeys().getHeaderKey().getBytes());
 		// Compare Alice's current keys in period 456 with Bob's previous keys
 		// in period 457
-		kB = crypto.rotateTransportKeys(kB, 457);
+		kB = transportCrypto.rotateTransportKeys(kB, 457);
 		// Alice's outgoing keys should equal Bob's previous incoming keys
 		assertArrayEquals(kA.getCurrentOutgoingKeys().getTagKey().getBytes(),
 				kB.getPreviousIncomingKeys().getTagKey().getBytes());
@@ -99,14 +99,14 @@ public class KeyDerivationTest extends BrambleTestCase {
 	@Test
 	public void testNextKeysMatchNextKeysOfContact() {
 		// Start in rotation period 123
-		TransportKeys kA = crypto.deriveTransportKeys(transportId, master,
-				123, true);
-		TransportKeys kB = crypto.deriveTransportKeys(transportId, master,
-				123, false);
+		TransportKeys kA = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, true);
+		TransportKeys kB = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, false);
 		// Compare Alice's current keys in period 456 with Bob's next keys in
 		// period 455
-		kA = crypto.rotateTransportKeys(kA, 456);
-		kB = crypto.rotateTransportKeys(kB, 455);
+		kA = transportCrypto.rotateTransportKeys(kA, 456);
+		kB = transportCrypto.rotateTransportKeys(kB, 455);
 		// Alice's outgoing keys should equal Bob's next incoming keys
 		assertArrayEquals(kA.getCurrentOutgoingKeys().getTagKey().getBytes(),
 				kB.getNextIncomingKeys().getTagKey().getBytes());
@@ -114,7 +114,7 @@ public class KeyDerivationTest extends BrambleTestCase {
 				kB.getNextIncomingKeys().getHeaderKey().getBytes());
 		// Compare Alice's next keys in period 456 with Bob's current keys
 		// in period 457
-		kB = crypto.rotateTransportKeys(kB, 457);
+		kB = transportCrypto.rotateTransportKeys(kB, 457);
 		// Alice's next incoming keys should equal Bob's outgoing keys
 		assertArrayEquals(kA.getNextIncomingKeys().getTagKey().getBytes(),
 				kB.getCurrentOutgoingKeys().getTagKey().getBytes());
@@ -124,12 +124,12 @@ public class KeyDerivationTest extends BrambleTestCase {
 
 	@Test
 	public void testMasterKeyAffectsOutput() {
-		SecretKey master1 = TestUtils.getSecretKey();
+		SecretKey master1 = getSecretKey();
 		assertFalse(Arrays.equals(master.getBytes(), master1.getBytes()));
-		TransportKeys k = crypto.deriveTransportKeys(transportId, master,
-				123, true);
-		TransportKeys k1 = crypto.deriveTransportKeys(transportId, master1,
-				123, true);
+		TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, true);
+		TransportKeys k1 = transportCrypto.deriveTransportKeys(transportId,
+				master1, 123, true);
 		assertAllDifferent(k, k1);
 	}
 
@@ -137,10 +137,10 @@ public class KeyDerivationTest extends BrambleTestCase {
 	public void testTransportIdAffectsOutput() {
 		TransportId transportId1 = new TransportId("id1");
 		assertFalse(transportId.getString().equals(transportId1.getString()));
-		TransportKeys k = crypto.deriveTransportKeys(transportId, master,
-				123, true);
-		TransportKeys k1 = crypto.deriveTransportKeys(transportId1, master,
-				123, true);
+		TransportKeys k = transportCrypto.deriveTransportKeys(transportId,
+				master, 123, true);
+		TransportKeys k1 = transportCrypto.deriveTransportKeys(transportId1,
+				master, 123, true);
 		assertAllDifferent(k, k1);
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/crypto/MacTest.java b/bramble-core/src/test/java/org/briarproject/bramble/crypto/MacTest.java
index f6760bf3e66ace6db806c241d129c11b95ddc2a2..8541c4a1d3c5ed4193700698c41d5358bc08fd67 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/crypto/MacTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/crypto/MacTest.java
@@ -4,42 +4,49 @@ import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.test.BrambleTestCase;
 import org.briarproject.bramble.test.TestSecureRandomProvider;
-import org.briarproject.bramble.test.TestUtils;
 import org.junit.Test;
 
 import java.util.Arrays;
 
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
+import static org.briarproject.bramble.util.StringUtils.getRandomString;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertFalse;
 
 public class MacTest extends BrambleTestCase {
 
-	private final CryptoComponent crypto;
+	private final CryptoComponent crypto =
+			new CryptoComponentImpl(new TestSecureRandomProvider());
 
-	private final SecretKey k = TestUtils.getSecretKey();
-	private final byte[] inputBytes = TestUtils.getRandomBytes(123);
-	private final byte[] inputBytes1 = TestUtils.getRandomBytes(234);
-	private final byte[] inputBytes2 = new byte[0];
-
-	public MacTest() {
-		crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
-	}
+	private final SecretKey key1 = getSecretKey(), key2 = getSecretKey();
+	private final String label1 = getRandomString(123);
+	private final String label2 = getRandomString(123);
+	private final byte[] input1 = getRandomBytes(123);
+	private final byte[] input2 = getRandomBytes(234);
+	private final byte[] input3 = new byte[0];
 
 	@Test
 	public void testIdenticalKeysAndInputsProduceIdenticalMacs() {
 		// Calculate the MAC twice - the results should be identical
-		byte[] mac = crypto.mac(k, inputBytes, inputBytes1, inputBytes2);
-		byte[] mac1 = crypto.mac(k, inputBytes, inputBytes1, inputBytes2);
+		byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
+		byte[] mac1 = crypto.mac(label1, key1, input1, input2, input3);
 		assertArrayEquals(mac, mac1);
 	}
 
+	@Test
+	public void testDifferentLabelsProduceDifferentMacs() {
+		// Calculate the MAC with each label - the results should be different
+		byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
+		byte[] mac1 = crypto.mac(label2, key1, input1, input2, input3);
+		assertFalse(Arrays.equals(mac, mac1));
+	}
+
 	@Test
 	public void testDifferentKeysProduceDifferentMacs() {
-		// Generate second random key
-		SecretKey k1 = TestUtils.getSecretKey();
 		// Calculate the MAC with each key - the results should be different
-		byte[] mac = crypto.mac(k, inputBytes, inputBytes1, inputBytes2);
-		byte[] mac1 = crypto.mac(k1, inputBytes, inputBytes1, inputBytes2);
+		byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
+		byte[] mac1 = crypto.mac(label1, key2, input1, input2, input3);
 		assertFalse(Arrays.equals(mac, mac1));
 	}
 
@@ -47,8 +54,8 @@ public class MacTest extends BrambleTestCase {
 	public void testDifferentInputsProduceDifferentMacs() {
 		// Calculate the MAC with the inputs in different orders - the results
 		// should be different
-		byte[] mac = crypto.mac(k, inputBytes, inputBytes1, inputBytes2);
-		byte[] mac1 = crypto.mac(k, inputBytes2, inputBytes1, inputBytes);
+		byte[] mac = crypto.mac(label1, key1, input1, input2, input3);
+		byte[] mac1 = crypto.mac(label1, key1, input3, input2, input1);
 		assertFalse(Arrays.equals(mac, mac1));
 	}
 
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/crypto/TagEncodingTest.java b/bramble-core/src/test/java/org/briarproject/bramble/crypto/TagEncodingTest.java
index cfbc6d37529510c2ce37260eaa53c8a05ee4493d..b6cce5ae242432a2bf9ccb9409d20bd494df5d9e 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/crypto/TagEncodingTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/crypto/TagEncodingTest.java
@@ -3,9 +3,8 @@ package org.briarproject.bramble.crypto;
 import org.briarproject.bramble.api.Bytes;
 import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
-import org.briarproject.bramble.test.BrambleTestCase;
-import org.briarproject.bramble.test.TestSecureRandomProvider;
-import org.briarproject.bramble.test.TestUtils;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
+import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.junit.Test;
 
 import java.util.HashSet;
@@ -14,25 +13,25 @@ import java.util.Set;
 import static junit.framework.TestCase.assertTrue;
 import static org.briarproject.bramble.api.transport.TransportConstants.PROTOCOL_VERSION;
 import static org.briarproject.bramble.api.transport.TransportConstants.TAG_LENGTH;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
 
-public class TagEncodingTest extends BrambleTestCase {
+public class TagEncodingTest extends BrambleMockTestCase {
 
-	private final CryptoComponent crypto;
-	private final SecretKey tagKey;
-	private final long streamNumber = 1234567890;
+	private final CryptoComponent crypto = context.mock(CryptoComponent.class);
 
-	public TagEncodingTest() {
-		crypto = new CryptoComponentImpl(new TestSecureRandomProvider());
-		tagKey = TestUtils.getSecretKey();
-	}
+	private final TransportCrypto transportCrypto =
+			new TransportCryptoImpl(crypto);
+	private final SecretKey tagKey = getSecretKey();
+	private final long streamNumber = 1234567890;
 
 	@Test
 	public void testKeyAffectsTag() throws Exception {
 		Set<Bytes> set = new HashSet<>();
 		for (int i = 0; i < 100; i++) {
 			byte[] tag = new byte[TAG_LENGTH];
-			SecretKey tagKey = TestUtils.getSecretKey();
-			crypto.encodeTag(tag, tagKey, PROTOCOL_VERSION, streamNumber);
+			SecretKey tagKey = getSecretKey();
+			transportCrypto.encodeTag(tag, tagKey, PROTOCOL_VERSION,
+					streamNumber);
 			assertTrue(set.add(new Bytes(tag)));
 		}
 	}
@@ -42,7 +41,8 @@ public class TagEncodingTest extends BrambleTestCase {
 		Set<Bytes> set = new HashSet<>();
 		for (int i = 0; i < 100; i++) {
 			byte[] tag = new byte[TAG_LENGTH];
-			crypto.encodeTag(tag, tagKey, PROTOCOL_VERSION + i, streamNumber);
+			transportCrypto.encodeTag(tag, tagKey, PROTOCOL_VERSION + i,
+					streamNumber);
 			assertTrue(set.add(new Bytes(tag)));
 		}
 	}
@@ -52,7 +52,8 @@ public class TagEncodingTest extends BrambleTestCase {
 		Set<Bytes> set = new HashSet<>();
 		for (int i = 0; i < 100; i++) {
 			byte[] tag = new byte[TAG_LENGTH];
-			crypto.encodeTag(tag, tagKey, PROTOCOL_VERSION, streamNumber + i);
+			transportCrypto.encodeTag(tag, tagKey, PROTOCOL_VERSION,
+					streamNumber + i);
 			assertTrue(set.add(new Bytes(tag)));
 		}
 	}
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocolTest.java b/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocolTest.java
index a737328780331e290cdd73a0346541098a141d23..c6c55ce0407b765f9832aa3c88eaa59ef3aaf64f 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocolTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/keyagreement/KeyAgreementProtocolTest.java
@@ -1,13 +1,14 @@
 package org.briarproject.bramble.keyagreement;
 
 import org.briarproject.bramble.api.crypto.CryptoComponent;
+import org.briarproject.bramble.api.crypto.KeyAgreementCrypto;
 import org.briarproject.bramble.api.crypto.KeyPair;
+import org.briarproject.bramble.api.crypto.KeyParser;
 import org.briarproject.bramble.api.crypto.PublicKey;
 import org.briarproject.bramble.api.crypto.SecretKey;
 import org.briarproject.bramble.api.keyagreement.Payload;
 import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
 import org.briarproject.bramble.test.BrambleTestCase;
-import org.briarproject.bramble.test.TestUtils;
 import org.jmock.Expectations;
 import org.jmock.auto.Mock;
 import org.jmock.integration.junit4.JUnitRuleMockery;
@@ -16,6 +17,10 @@ import org.junit.Rule;
 import org.junit.Test;
 
 import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
+import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.MASTER_SECRET_LABEL;
+import static org.briarproject.bramble.api.keyagreement.KeyAgreementConstants.SHARED_SECRET_LABEL;
+import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
+import static org.briarproject.bramble.test.TestUtils.getSecretKey;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.is;
 import static org.junit.Assert.assertThat;
@@ -28,34 +33,33 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 		setImposteriser(ClassImposteriser.INSTANCE);
 	}};
 
-	private static final byte[] ALICE_PUBKEY = TestUtils.getRandomBytes(32);
-	private static final byte[] ALICE_COMMIT =
-			TestUtils.getRandomBytes(COMMIT_LENGTH);
-	private static final byte[] ALICE_PAYLOAD =
-			TestUtils.getRandomBytes(COMMIT_LENGTH + 8);
-
-	private static final byte[] BOB_PUBKEY = TestUtils.getRandomBytes(32);
-	private static final byte[] BOB_COMMIT =
-			TestUtils.getRandomBytes(COMMIT_LENGTH);
-	private static final byte[] BOB_PAYLOAD =
-			TestUtils.getRandomBytes(COMMIT_LENGTH + 19);
-
-	private static final byte[] ALICE_CONFIRM =
-			TestUtils.getRandomBytes(SecretKey.LENGTH);
-	private static final byte[] BOB_CONFIRM =
-			TestUtils.getRandomBytes(SecretKey.LENGTH);
-
-	private static final byte[] BAD_PUBKEY = TestUtils.getRandomBytes(32);
-	private static final byte[] BAD_COMMIT =
-			TestUtils.getRandomBytes(COMMIT_LENGTH);
-	private static final byte[] BAD_CONFIRM =
-			TestUtils.getRandomBytes(SecretKey.LENGTH);
+	private final PublicKey alicePubKey =
+			context.mock(PublicKey.class, "alice");
+	private final byte[] alicePubKeyBytes = getRandomBytes(32);
+	private final byte[] aliceCommit = getRandomBytes(COMMIT_LENGTH);
+	private final byte[] alicePayload = getRandomBytes(COMMIT_LENGTH + 8);
+	private final byte[] aliceConfirm = getRandomBytes(SecretKey.LENGTH);
+
+	private final PublicKey bobPubKey = context.mock(PublicKey.class, "bob");
+	private final byte[] bobPubKeyBytes = getRandomBytes(32);
+	private final byte[] bobCommit = getRandomBytes(COMMIT_LENGTH);
+	private final byte[] bobPayload = getRandomBytes(COMMIT_LENGTH + 19);
+	private final byte[] bobConfirm = getRandomBytes(SecretKey.LENGTH);
+
+	private final PublicKey badPubKey = context.mock(PublicKey.class, "bad");
+	private final byte[] badPubKeyBytes = getRandomBytes(32);
+	private final byte[] badCommit = getRandomBytes(COMMIT_LENGTH);
+	private final byte[] badConfirm = getRandomBytes(SecretKey.LENGTH);
 
 	@Mock
 	KeyAgreementProtocol.Callbacks callbacks;
 	@Mock
 	CryptoComponent crypto;
 	@Mock
+	KeyAgreementCrypto keyAgreementCrypto;
+	@Mock
+	KeyParser keyParser;
+	@Mock
 	PayloadEncoder payloadEncoder;
 	@Mock
 	KeyAgreementTransport transport;
@@ -65,60 +69,67 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 	@Test
 	public void testAliceProtocol() throws Exception {
 		// set up
-		Payload theirPayload = new Payload(BOB_COMMIT, null);
-		Payload ourPayload = new Payload(ALICE_COMMIT, null);
+		Payload theirPayload = new Payload(bobCommit, null);
+		Payload ourPayload = new Payload(aliceCommit, null);
 		KeyPair ourKeyPair = new KeyPair(ourPubKey, null);
-		SecretKey sharedSecret = TestUtils.getSecretKey();
-		SecretKey masterSecret = TestUtils.getSecretKey();
+		SecretKey sharedSecret = getSecretKey();
+		SecretKey masterSecret = getSecretKey();
 
-		KeyAgreementProtocol protocol =
-				new KeyAgreementProtocol(callbacks, crypto, payloadEncoder,
-						transport, theirPayload, ourPayload, ourKeyPair, true);
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(callbacks,
+				crypto, keyAgreementCrypto, payloadEncoder, transport,
+				theirPayload, ourPayload, ourKeyPair, true);
 
 		// expectations
 		context.checking(new Expectations() {{
 			// Helpers
 			allowing(payloadEncoder).encode(ourPayload);
-			will(returnValue(ALICE_PAYLOAD));
+			will(returnValue(alicePayload));
 			allowing(payloadEncoder).encode(theirPayload);
-			will(returnValue(BOB_PAYLOAD));
+			will(returnValue(bobPayload));
 			allowing(ourPubKey).getEncoded();
-			will(returnValue(ALICE_PUBKEY));
+			will(returnValue(alicePubKeyBytes));
+			allowing(crypto).getAgreementKeyParser();
+			will(returnValue(keyParser));
 
 			// Alice sends her public key
-			oneOf(transport).sendKey(ALICE_PUBKEY);
+			oneOf(transport).sendKey(alicePubKeyBytes);
 
 			// Alice receives Bob's public key
 			oneOf(callbacks).connectionWaiting();
 			oneOf(transport).receiveKey();
-			will(returnValue(BOB_PUBKEY));
+			will(returnValue(bobPubKeyBytes));
 			oneOf(callbacks).initialRecordReceived();
+			oneOf(keyParser).parsePublicKey(bobPubKeyBytes);
+			will(returnValue(bobPubKey));
 
 			// Alice verifies Bob's public key
-			oneOf(crypto).deriveKeyCommitment(BOB_PUBKEY);
-			will(returnValue(BOB_COMMIT));
+			oneOf(keyAgreementCrypto).deriveKeyCommitment(bobPubKey);
+			will(returnValue(bobCommit));
 
 			// Alice computes shared secret
-			oneOf(crypto).deriveSharedSecret(BOB_PUBKEY, ourKeyPair, true);
+			oneOf(crypto).deriveSharedSecret(SHARED_SECRET_LABEL, bobPubKey,
+					ourKeyPair, true);
 			will(returnValue(sharedSecret));
 
 			// Alice sends her confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD,
-					ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, true);
-			will(returnValue(ALICE_CONFIRM));
-			oneOf(transport).sendConfirm(ALICE_CONFIRM);
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					bobPayload, alicePayload, bobPubKey, ourKeyPair,
+					true, true);
+			will(returnValue(aliceConfirm));
+			oneOf(transport).sendConfirm(aliceConfirm);
 
 			// Alice receives Bob's confirmation record
 			oneOf(transport).receiveConfirm();
-			will(returnValue(BOB_CONFIRM));
+			will(returnValue(bobConfirm));
 
 			// Alice verifies Bob's confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD,
-					ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, false);
-			will(returnValue(BOB_CONFIRM));
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					bobPayload, alicePayload, bobPubKey, ourKeyPair,
+					true, false);
+			will(returnValue(bobConfirm));
 
 			// Alice computes master secret
-			oneOf(crypto).deriveMasterSecret(sharedSecret);
+			oneOf(crypto).deriveKey(MASTER_SECRET_LABEL, sharedSecret);
 			will(returnValue(masterSecret));
 		}});
 
@@ -129,59 +140,66 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 	@Test
 	public void testBobProtocol() throws Exception {
 		// set up
-		Payload theirPayload = new Payload(ALICE_COMMIT, null);
-		Payload ourPayload = new Payload(BOB_COMMIT, null);
+		Payload theirPayload = new Payload(aliceCommit, null);
+		Payload ourPayload = new Payload(bobCommit, null);
 		KeyPair ourKeyPair = new KeyPair(ourPubKey, null);
-		SecretKey sharedSecret = TestUtils.getSecretKey();
-		SecretKey masterSecret = TestUtils.getSecretKey();
+		SecretKey sharedSecret = getSecretKey();
+		SecretKey masterSecret = getSecretKey();
 
-		KeyAgreementProtocol protocol =
-				new KeyAgreementProtocol(callbacks, crypto, payloadEncoder,
-						transport, theirPayload, ourPayload, ourKeyPair, false);
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(callbacks,
+				crypto, keyAgreementCrypto, payloadEncoder, transport,
+				theirPayload, ourPayload, ourKeyPair, false);
 
 		// expectations
 		context.checking(new Expectations() {{
 			// Helpers
 			allowing(payloadEncoder).encode(ourPayload);
-			will(returnValue(BOB_PAYLOAD));
+			will(returnValue(bobPayload));
 			allowing(payloadEncoder).encode(theirPayload);
-			will(returnValue(ALICE_PAYLOAD));
+			will(returnValue(alicePayload));
 			allowing(ourPubKey).getEncoded();
-			will(returnValue(BOB_PUBKEY));
+			will(returnValue(bobPubKeyBytes));
+			allowing(crypto).getAgreementKeyParser();
+			will(returnValue(keyParser));
 
 			// Bob receives Alice's public key
 			oneOf(transport).receiveKey();
-			will(returnValue(ALICE_PUBKEY));
+			will(returnValue(alicePubKeyBytes));
 			oneOf(callbacks).initialRecordReceived();
+			oneOf(keyParser).parsePublicKey(alicePubKeyBytes);
+			will(returnValue(alicePubKey));
 
 			// Bob verifies Alice's public key
-			oneOf(crypto).deriveKeyCommitment(ALICE_PUBKEY);
-			will(returnValue(ALICE_COMMIT));
+			oneOf(keyAgreementCrypto).deriveKeyCommitment(alicePubKey);
+			will(returnValue(aliceCommit));
 
 			// Bob sends his public key
-			oneOf(transport).sendKey(BOB_PUBKEY);
+			oneOf(transport).sendKey(bobPubKeyBytes);
 
 			// Bob computes shared secret
-			oneOf(crypto).deriveSharedSecret(ALICE_PUBKEY, ourKeyPair, false);
+			oneOf(crypto).deriveSharedSecret(SHARED_SECRET_LABEL, alicePubKey,
+					ourKeyPair, false);
 			will(returnValue(sharedSecret));
 
 			// Bob receives Alices's confirmation record
 			oneOf(transport).receiveConfirm();
-			will(returnValue(ALICE_CONFIRM));
+			will(returnValue(aliceConfirm));
 
 			// Bob verifies Alice's confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD,
-					BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, true);
-			will(returnValue(ALICE_CONFIRM));
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					alicePayload, bobPayload, alicePubKey, ourKeyPair,
+					false, true);
+			will(returnValue(aliceConfirm));
 
 			// Bob sends his confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD,
-					BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, false);
-			will(returnValue(BOB_CONFIRM));
-			oneOf(transport).sendConfirm(BOB_CONFIRM);
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					alicePayload, bobPayload, alicePubKey, ourKeyPair,
+					false, false);
+			will(returnValue(bobConfirm));
+			oneOf(transport).sendConfirm(bobConfirm);
 
 			// Bob computes master secret
-			oneOf(crypto).deriveMasterSecret(sharedSecret);
+			oneOf(crypto).deriveKey(MASTER_SECRET_LABEL, sharedSecret);
 			will(returnValue(masterSecret));
 		}});
 
@@ -192,38 +210,43 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 	@Test(expected = AbortException.class)
 	public void testAliceProtocolAbortOnBadKey() throws Exception {
 		// set up
-		Payload theirPayload = new Payload(BOB_COMMIT, null);
-		Payload ourPayload = new Payload(ALICE_COMMIT, null);
+		Payload theirPayload = new Payload(bobCommit, null);
+		Payload ourPayload = new Payload(aliceCommit, null);
 		KeyPair ourKeyPair = new KeyPair(ourPubKey, null);
 
-		KeyAgreementProtocol protocol =
-				new KeyAgreementProtocol(callbacks, crypto, payloadEncoder,
-						transport, theirPayload, ourPayload, ourKeyPair, true);
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(callbacks,
+				crypto, keyAgreementCrypto, payloadEncoder, transport,
+				theirPayload, ourPayload, ourKeyPair, true);
 
 		// expectations
 		context.checking(new Expectations() {{
 			// Helpers
 			allowing(ourPubKey).getEncoded();
-			will(returnValue(ALICE_PUBKEY));
+			will(returnValue(alicePubKeyBytes));
+			allowing(crypto).getAgreementKeyParser();
+			will(returnValue(keyParser));
 
 			// Alice sends her public key
-			oneOf(transport).sendKey(ALICE_PUBKEY);
+			oneOf(transport).sendKey(alicePubKeyBytes);
 
 			// Alice receives a bad public key
 			oneOf(callbacks).connectionWaiting();
 			oneOf(transport).receiveKey();
-			will(returnValue(BAD_PUBKEY));
+			will(returnValue(badPubKeyBytes));
 			oneOf(callbacks).initialRecordReceived();
+			oneOf(keyParser).parsePublicKey(badPubKeyBytes);
+			will(returnValue(badPubKey));
 
 			// Alice verifies Bob's public key
-			oneOf(crypto).deriveKeyCommitment(BAD_PUBKEY);
-			will(returnValue(BAD_COMMIT));
+			oneOf(keyAgreementCrypto).deriveKeyCommitment(badPubKey);
+			will(returnValue(badCommit));
 
 			// Alice aborts
 			oneOf(transport).sendAbort(false);
 
 			// Alice never computes shared secret
-			never(crypto).deriveSharedSecret(BAD_PUBKEY, ourKeyPair, true);
+			never(crypto).deriveSharedSecret(SHARED_SECRET_LABEL, badPubKey,
+					ourKeyPair, true);
 		}});
 
 		// execute
@@ -233,34 +256,38 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 	@Test(expected = AbortException.class)
 	public void testBobProtocolAbortOnBadKey() throws Exception {
 		// set up
-		Payload theirPayload = new Payload(ALICE_COMMIT, null);
-		Payload ourPayload = new Payload(BOB_COMMIT, null);
+		Payload theirPayload = new Payload(aliceCommit, null);
+		Payload ourPayload = new Payload(bobCommit, null);
 		KeyPair ourKeyPair = new KeyPair(ourPubKey, null);
 
-		KeyAgreementProtocol protocol =
-				new KeyAgreementProtocol(callbacks, crypto, payloadEncoder,
-						transport, theirPayload, ourPayload, ourKeyPair, false);
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(callbacks,
+				crypto, keyAgreementCrypto, payloadEncoder, transport,
+				theirPayload, ourPayload, ourKeyPair, false);
 
 		// expectations
 		context.checking(new Expectations() {{
 			// Helpers
 			allowing(ourPubKey).getEncoded();
-			will(returnValue(BOB_PUBKEY));
+			will(returnValue(bobPubKeyBytes));
+			allowing(crypto).getAgreementKeyParser();
+			will(returnValue(keyParser));
 
 			// Bob receives a bad public key
 			oneOf(transport).receiveKey();
-			will(returnValue(BAD_PUBKEY));
+			will(returnValue(badPubKeyBytes));
 			oneOf(callbacks).initialRecordReceived();
+			oneOf(keyParser).parsePublicKey(badPubKeyBytes);
+			will(returnValue(badPubKey));
 
 			// Bob verifies Alice's public key
-			oneOf(crypto).deriveKeyCommitment(BAD_PUBKEY);
-			will(returnValue(BAD_COMMIT));
+			oneOf(keyAgreementCrypto).deriveKeyCommitment(badPubKey);
+			will(returnValue(badCommit));
 
 			// Bob aborts
 			oneOf(transport).sendAbort(false);
 
 			// Bob never sends his public key
-			never(transport).sendKey(BOB_PUBKEY);
+			never(transport).sendKey(bobPubKeyBytes);
 		}});
 
 		// execute
@@ -270,62 +297,69 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 	@Test(expected = AbortException.class)
 	public void testAliceProtocolAbortOnBadConfirm() throws Exception {
 		// set up
-		Payload theirPayload = new Payload(BOB_COMMIT, null);
-		Payload ourPayload = new Payload(ALICE_COMMIT, null);
+		Payload theirPayload = new Payload(bobCommit, null);
+		Payload ourPayload = new Payload(aliceCommit, null);
 		KeyPair ourKeyPair = new KeyPair(ourPubKey, null);
-		SecretKey sharedSecret = TestUtils.getSecretKey();
+		SecretKey sharedSecret = getSecretKey();
 
-		KeyAgreementProtocol protocol =
-				new KeyAgreementProtocol(callbacks, crypto, payloadEncoder,
-						transport, theirPayload, ourPayload, ourKeyPair, true);
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(callbacks,
+				crypto, keyAgreementCrypto, payloadEncoder, transport,
+				theirPayload, ourPayload, ourKeyPair, true);
 
 		// expectations
 		context.checking(new Expectations() {{
 			// Helpers
 			allowing(payloadEncoder).encode(ourPayload);
-			will(returnValue(ALICE_PAYLOAD));
+			will(returnValue(alicePayload));
 			allowing(payloadEncoder).encode(theirPayload);
-			will(returnValue(BOB_PAYLOAD));
+			will(returnValue(bobPayload));
 			allowing(ourPubKey).getEncoded();
-			will(returnValue(ALICE_PUBKEY));
+			will(returnValue(alicePubKeyBytes));
+			allowing(crypto).getAgreementKeyParser();
+			will(returnValue(keyParser));
 
 			// Alice sends her public key
-			oneOf(transport).sendKey(ALICE_PUBKEY);
+			oneOf(transport).sendKey(alicePubKeyBytes);
 
 			// Alice receives Bob's public key
 			oneOf(callbacks).connectionWaiting();
 			oneOf(transport).receiveKey();
-			will(returnValue(BOB_PUBKEY));
+			will(returnValue(bobPubKeyBytes));
 			oneOf(callbacks).initialRecordReceived();
+			oneOf(keyParser).parsePublicKey(bobPubKeyBytes);
+			will(returnValue(bobPubKey));
 
 			// Alice verifies Bob's public key
-			oneOf(crypto).deriveKeyCommitment(BOB_PUBKEY);
-			will(returnValue(BOB_COMMIT));
+			oneOf(keyAgreementCrypto).deriveKeyCommitment(bobPubKey);
+			will(returnValue(bobCommit));
 
 			// Alice computes shared secret
-			oneOf(crypto).deriveSharedSecret(BOB_PUBKEY, ourKeyPair, true);
+			oneOf(crypto).deriveSharedSecret(SHARED_SECRET_LABEL, bobPubKey,
+					ourKeyPair, true);
 			will(returnValue(sharedSecret));
 
 			// Alice sends her confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD,
-					ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, true);
-			will(returnValue(ALICE_CONFIRM));
-			oneOf(transport).sendConfirm(ALICE_CONFIRM);
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					bobPayload, alicePayload, bobPubKey, ourKeyPair,
+					true, true);
+			will(returnValue(aliceConfirm));
+			oneOf(transport).sendConfirm(aliceConfirm);
 
 			// Alice receives a bad confirmation record
 			oneOf(transport).receiveConfirm();
-			will(returnValue(BAD_CONFIRM));
+			will(returnValue(badConfirm));
 
 			// Alice verifies Bob's confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, BOB_PAYLOAD,
-					ALICE_PAYLOAD, BOB_PUBKEY, ourKeyPair, true, false);
-			will(returnValue(BOB_CONFIRM));
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					bobPayload, alicePayload, bobPubKey, ourKeyPair,
+					true, false);
+			will(returnValue(bobConfirm));
 
 			// Alice aborts
 			oneOf(transport).sendAbort(false);
 
 			// Alice never computes master secret
-			never(crypto).deriveMasterSecret(sharedSecret);
+			never(crypto).deriveKey(MASTER_SECRET_LABEL, sharedSecret);
 		}});
 
 		// execute
@@ -335,56 +369,63 @@ public class KeyAgreementProtocolTest extends BrambleTestCase {
 	@Test(expected = AbortException.class)
 	public void testBobProtocolAbortOnBadConfirm() throws Exception {
 		// set up
-		Payload theirPayload = new Payload(ALICE_COMMIT, null);
-		Payload ourPayload = new Payload(BOB_COMMIT, null);
+		Payload theirPayload = new Payload(aliceCommit, null);
+		Payload ourPayload = new Payload(bobCommit, null);
 		KeyPair ourKeyPair = new KeyPair(ourPubKey, null);
-		SecretKey sharedSecret = TestUtils.getSecretKey();
+		SecretKey sharedSecret = getSecretKey();
 
-		KeyAgreementProtocol protocol =
-				new KeyAgreementProtocol(callbacks, crypto, payloadEncoder,
-						transport, theirPayload, ourPayload, ourKeyPair, false);
+		KeyAgreementProtocol protocol = new KeyAgreementProtocol(callbacks,
+				crypto, keyAgreementCrypto, payloadEncoder, transport,
+				theirPayload, ourPayload, ourKeyPair, false);
 
 		// expectations
 		context.checking(new Expectations() {{
 			// Helpers
 			allowing(payloadEncoder).encode(ourPayload);
-			will(returnValue(BOB_PAYLOAD));
+			will(returnValue(bobPayload));
 			allowing(payloadEncoder).encode(theirPayload);
-			will(returnValue(ALICE_PAYLOAD));
+			will(returnValue(alicePayload));
 			allowing(ourPubKey).getEncoded();
-			will(returnValue(BOB_PUBKEY));
+			will(returnValue(bobPubKeyBytes));
+			allowing(crypto).getAgreementKeyParser();
+			will(returnValue(keyParser));
 
 			// Bob receives Alice's public key
 			oneOf(transport).receiveKey();
-			will(returnValue(ALICE_PUBKEY));
+			will(returnValue(alicePubKeyBytes));
 			oneOf(callbacks).initialRecordReceived();
+			oneOf(keyParser).parsePublicKey(alicePubKeyBytes);
+			will(returnValue(alicePubKey));
 
 			// Bob verifies Alice's public key
-			oneOf(crypto).deriveKeyCommitment(ALICE_PUBKEY);
-			will(returnValue(ALICE_COMMIT));
+			oneOf(keyAgreementCrypto).deriveKeyCommitment(alicePubKey);
+			will(returnValue(aliceCommit));
 
 			// Bob sends his public key
-			oneOf(transport).sendKey(BOB_PUBKEY);
+			oneOf(transport).sendKey(bobPubKeyBytes);
 
 			// Bob computes shared secret
-			oneOf(crypto).deriveSharedSecret(ALICE_PUBKEY, ourKeyPair, false);
+			oneOf(crypto).deriveSharedSecret(SHARED_SECRET_LABEL, alicePubKey,
+					ourKeyPair, false);
 			will(returnValue(sharedSecret));
 
 			// Bob receives a bad confirmation record
 			oneOf(transport).receiveConfirm();
-			will(returnValue(BAD_CONFIRM));
+			will(returnValue(badConfirm));
 
 			// Bob verifies Alice's confirmation record
-			oneOf(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD,
-					BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, true);
-			will(returnValue(ALICE_CONFIRM));
+			oneOf(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					alicePayload, bobPayload, alicePubKey, ourKeyPair,
+					false, true);
+			will(returnValue(aliceConfirm));
 
 			// Bob aborts
 			oneOf(transport).sendAbort(false);
 
 			// Bob never sends his confirmation record
-			never(crypto).deriveConfirmationRecord(sharedSecret, ALICE_PAYLOAD,
-					BOB_PAYLOAD, ALICE_PUBKEY, ourKeyPair, false, false);
+			never(keyAgreementCrypto).deriveConfirmationRecord(sharedSecret,
+					alicePayload, bobPayload, alicePubKey, ourKeyPair,
+					false, false);
 		}});
 
 		// execute
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
index bc5c86a8d5e1ec844885ddcc6ff760039f9c2a9c..7bb53e8435c05d56a0eec01b4eb944c4ab6a1cff 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/sync/SyncIntegrationTest.java
@@ -1,8 +1,8 @@
 package org.briarproject.bramble.sync;
 
 import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.plugin.TransportId;
 import org.briarproject.bramble.api.sync.Ack;
 import org.briarproject.bramble.api.sync.ClientId;
@@ -57,7 +57,7 @@ public class SyncIntegrationTest extends BrambleTestCase {
 	@Inject
 	RecordWriterFactory recordWriterFactory;
 	@Inject
-	CryptoComponent crypto;
+	TransportCrypto transportCrypto;
 
 	private final ContactId contactId;
 	private final TransportId transportId;
@@ -117,7 +117,8 @@ public class SyncIntegrationTest extends BrambleTestCase {
 	private void read(byte[] connectionData) throws Exception {
 		// Calculate the expected tag
 		byte[] expectedTag = new byte[TAG_LENGTH];
-		crypto.encodeTag(expectedTag, tagKey, PROTOCOL_VERSION, streamNumber);
+		transportCrypto.encodeTag(expectedTag, tagKey, PROTOCOL_VERSION,
+				streamNumber);
 
 		// Read the tag
 		InputStream in = new ByteArrayInputStream(connectionData);
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/transport/TransportKeyManagerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/transport/TransportKeyManagerImplTest.java
index 0cdfaeb561228cfd7d22c991a7dae780ba5becb4..4dc1f980240d9d91daa57860c5da1ea40d47b93d 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/transport/TransportKeyManagerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/transport/TransportKeyManagerImplTest.java
@@ -1,8 +1,8 @@
 package org.briarproject.bramble.transport;
 
 import org.briarproject.bramble.api.contact.ContactId;
-import org.briarproject.bramble.api.crypto.CryptoComponent;
 import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.crypto.TransportCrypto;
 import org.briarproject.bramble.api.db.DatabaseComponent;
 import org.briarproject.bramble.api.db.Transaction;
 import org.briarproject.bramble.api.plugin.TransportId;
@@ -11,12 +11,11 @@ import org.briarproject.bramble.api.transport.IncomingKeys;
 import org.briarproject.bramble.api.transport.OutgoingKeys;
 import org.briarproject.bramble.api.transport.StreamContext;
 import org.briarproject.bramble.api.transport.TransportKeys;
-import org.briarproject.bramble.test.BrambleTestCase;
+import org.briarproject.bramble.test.BrambleMockTestCase;
 import org.briarproject.bramble.test.RunAction;
 import org.briarproject.bramble.test.TestUtils;
 import org.hamcrest.Description;
 import org.jmock.Expectations;
-import org.jmock.Mockery;
 import org.jmock.api.Action;
 import org.jmock.api.Invocation;
 import org.junit.Test;
@@ -41,7 +40,15 @@ import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
-public class TransportKeyManagerImplTest extends BrambleTestCase {
+public class TransportKeyManagerImplTest extends BrambleMockTestCase {
+
+	private final DatabaseComponent db = context.mock(DatabaseComponent.class);
+	private final TransportCrypto transportCrypto =
+			context.mock(TransportCrypto.class);
+	private final Executor dbExecutor = context.mock(Executor.class);
+	private final ScheduledExecutorService scheduler =
+			context.mock(ScheduledExecutorService.class);
+	private final Clock clock = context.mock(Clock.class);
 
 	private final TransportId transportId = new TransportId("id");
 	private final long maxLatency = 30 * 1000; // 30 seconds
@@ -55,14 +62,6 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 
 	@Test
 	public void testKeysAreRotatedAtStartup() throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
 		Map<ContactId, TransportKeys> loaded = new LinkedHashMap<>();
 		TransportKeys shouldRotate = createTransportKeys(900, 0);
 		TransportKeys shouldNotRotate = createTransportKeys(1000, 0);
@@ -79,14 +78,15 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 			oneOf(db).getTransportKeys(txn, transportId);
 			will(returnValue(loaded));
 			// Rotate the transport keys
-			oneOf(crypto).rotateTransportKeys(shouldRotate, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(shouldRotate, 1000);
 			will(returnValue(rotated));
-			oneOf(crypto).rotateTransportKeys(shouldNotRotate, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(shouldNotRotate, 1000);
 			will(returnValue(shouldNotRotate));
 			// Encode the tags (3 sets per contact)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(6).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(6).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Save the keys that were rotated
@@ -97,161 +97,124 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 					with(rotationPeriodLength - 1), with(MILLISECONDS));
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		transportKeyManager.start(txn);
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testKeysAreRotatedWhenAddingContact() throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
-		boolean alice = true;
+		boolean alice = random.nextBoolean();
 		TransportKeys transportKeys = createTransportKeys(999, 0);
 		TransportKeys rotated = createTransportKeys(1000, 0);
 		Transaction txn = new Transaction(null, false);
 
 		context.checking(new Expectations() {{
-			oneOf(crypto).deriveTransportKeys(transportId, masterKey, 999,
-					alice);
+			oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
+					999, alice);
 			will(returnValue(transportKeys));
 			// Get the current time (1 ms after start of rotation period 1000)
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(rotationPeriodLength * 1000 + 1));
 			// Rotate the transport keys
-			oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
 			will(returnValue(rotated));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Save the keys
 			oneOf(db).addTransportKeys(txn, contactId, rotated);
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		// The timestamp is 1 ms before the start of rotation period 1000
 		long timestamp = rotationPeriodLength * 1000 - 1;
 		transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
 				alice);
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testOutgoingStreamContextIsNullIfContactIsNotFound()
 			throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
 		Transaction txn = new Transaction(null, false);
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		assertNull(transportKeyManager.getStreamContext(txn, contactId));
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testOutgoingStreamContextIsNullIfStreamCounterIsExhausted()
 			throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
-		boolean alice = true;
+		boolean alice = random.nextBoolean();
 		// The stream counter has been exhausted
 		TransportKeys transportKeys = createTransportKeys(1000,
 				MAX_32_BIT_UNSIGNED + 1);
 		Transaction txn = new Transaction(null, false);
 
 		context.checking(new Expectations() {{
-			oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
-					alice);
+			oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
+					1000, alice);
 			will(returnValue(transportKeys));
 			// Get the current time (the start of rotation period 1000)
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(rotationPeriodLength * 1000));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Rotate the transport keys (the keys are unaffected)
-			oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
 			will(returnValue(transportKeys));
 			// Save the keys
 			oneOf(db).addTransportKeys(txn, contactId, transportKeys);
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		// The timestamp is at the start of rotation period 1000
 		long timestamp = rotationPeriodLength * 1000;
 		transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
 				alice);
 		assertNull(transportKeyManager.getStreamContext(txn, contactId));
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testOutgoingStreamCounterIsIncremented() throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
-		boolean alice = true;
+		boolean alice = random.nextBoolean();
 		// The stream counter can be used one more time before being exhausted
 		TransportKeys transportKeys = createTransportKeys(1000,
 				MAX_32_BIT_UNSIGNED);
 		Transaction txn = new Transaction(null, false);
 
 		context.checking(new Expectations() {{
-			oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
-					alice);
+			oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
+					1000, alice);
 			will(returnValue(transportKeys));
 			// Get the current time (the start of rotation period 1000)
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(rotationPeriodLength * 1000));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Rotate the transport keys (the keys are unaffected)
-			oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
 			will(returnValue(transportKeys));
 			// Save the keys
 			oneOf(db).addTransportKeys(txn, contactId, transportKeys);
@@ -259,9 +222,9 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 			oneOf(db).incrementStreamCounter(txn, contactId, transportId, 1000);
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		// The timestamp is at the start of rotation period 1000
 		long timestamp = rotationPeriodLength * 1000;
 		transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
@@ -277,94 +240,76 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 		assertEquals(MAX_32_BIT_UNSIGNED, ctx.getStreamNumber());
 		// The second request should return null, the counter is exhausted
 		assertNull(transportKeyManager.getStreamContext(txn, contactId));
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testIncomingStreamContextIsNullIfTagIsNotFound()
 			throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
-		boolean alice = true;
+		boolean alice = random.nextBoolean();
 		TransportKeys transportKeys = createTransportKeys(1000, 0);
 		Transaction txn = new Transaction(null, false);
 
 		context.checking(new Expectations() {{
-			oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
-					alice);
+			oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
+					1000, alice);
 			will(returnValue(transportKeys));
 			// Get the current time (the start of rotation period 1000)
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(rotationPeriodLength * 1000));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Rotate the transport keys (the keys are unaffected)
-			oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
 			will(returnValue(transportKeys));
 			// Save the keys
 			oneOf(db).addTransportKeys(txn, contactId, transportKeys);
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		// The timestamp is at the start of rotation period 1000
 		long timestamp = rotationPeriodLength * 1000;
 		transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
 				alice);
 		assertNull(transportKeyManager.getStreamContext(txn,
 				new byte[TAG_LENGTH]));
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testTagIsNotRecognisedTwice() throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
-		boolean alice = true;
+		boolean alice = random.nextBoolean();
 		TransportKeys transportKeys = createTransportKeys(1000, 0);
 		// Keep a copy of the tags
 		List<byte[]> tags = new ArrayList<>();
 		Transaction txn = new Transaction(null, false);
 
 		context.checking(new Expectations() {{
-			oneOf(crypto).deriveTransportKeys(transportId, masterKey, 1000,
-					alice);
+			oneOf(transportCrypto).deriveTransportKeys(transportId, masterKey,
+					1000, alice);
 			will(returnValue(transportKeys));
 			// Get the current time (the start of rotation period 1000)
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(rotationPeriodLength * 1000));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction(tags));
 			}
 			// Rotate the transport keys (the keys are unaffected)
-			oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
 			will(returnValue(transportKeys));
 			// Save the keys
 			oneOf(db).addTransportKeys(txn, contactId, transportKeys);
 			// Encode a new tag after sliding the window
-			oneOf(crypto).encodeTag(with(any(byte[].class)),
+			oneOf(transportCrypto).encodeTag(with(any(byte[].class)),
 					with(tagKey), with(PROTOCOL_VERSION),
 					with((long) REORDERING_WINDOW_SIZE));
 			will(new EncodeTagAction(tags));
@@ -373,9 +318,9 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 					1, new byte[REORDERING_WINDOW_SIZE / 8]);
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		// The timestamp is at the start of rotation period 1000
 		long timestamp = rotationPeriodLength * 1000;
 		transportKeyManager.addContact(txn, contactId, masterKey, timestamp,
@@ -395,20 +340,10 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 		assertEquals(REORDERING_WINDOW_SIZE * 3 + 1, tags.size());
 		// The second request should return null, the tag has already been used
 		assertNull(transportKeyManager.getStreamContext(txn, tag));
-
-		context.assertIsSatisfied();
 	}
 
 	@Test
 	public void testKeysAreRotatedToCurrentPeriod() throws Exception {
-		Mockery context = new Mockery();
-		DatabaseComponent db = context.mock(DatabaseComponent.class);
-		CryptoComponent crypto = context.mock(CryptoComponent.class);
-		Executor dbExecutor = context.mock(Executor.class);
-		ScheduledExecutorService scheduler =
-				context.mock(ScheduledExecutorService.class);
-		Clock clock = context.mock(Clock.class);
-
 		TransportKeys transportKeys = createTransportKeys(1000, 0);
 		Map<ContactId, TransportKeys> loaded =
 				Collections.singletonMap(contactId, transportKeys);
@@ -424,12 +359,13 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 			oneOf(db).getTransportKeys(txn, transportId);
 			will(returnValue(loaded));
 			// Rotate the transport keys (the keys are unaffected)
-			oneOf(crypto).rotateTransportKeys(transportKeys, 1000);
+			oneOf(transportCrypto).rotateTransportKeys(transportKeys, 1000);
 			will(returnValue(transportKeys));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Schedule key rotation at the start of the next rotation period
@@ -445,13 +381,14 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(rotationPeriodLength * 1001));
 			// Rotate the transport keys
-			oneOf(crypto).rotateTransportKeys(with(any(TransportKeys.class)),
-					with(1001L));
+			oneOf(transportCrypto).rotateTransportKeys(
+					with(any(TransportKeys.class)), with(1001L));
 			will(returnValue(rotated));
 			// Encode the tags (3 sets)
 			for (long i = 0; i < REORDERING_WINDOW_SIZE; i++) {
-				exactly(3).of(crypto).encodeTag(with(any(byte[].class)),
-						with(tagKey), with(PROTOCOL_VERSION), with(i));
+				exactly(3).of(transportCrypto).encodeTag(
+						with(any(byte[].class)), with(tagKey),
+						with(PROTOCOL_VERSION), with(i));
 				will(new EncodeTagAction());
 			}
 			// Save the keys that were rotated
@@ -465,12 +402,10 @@ public class TransportKeyManagerImplTest extends BrambleTestCase {
 			oneOf(db).endTransaction(txn1);
 		}});
 
-		TransportKeyManager
-				transportKeyManager = new TransportKeyManagerImpl(db,
-				crypto, dbExecutor, scheduler, clock, transportId, maxLatency);
+		TransportKeyManager transportKeyManager = new TransportKeyManagerImpl(
+				db, transportCrypto, dbExecutor, scheduler, clock, transportId,
+				maxLatency);
 		transportKeyManager.start(txn);
-
-		context.assertIsSatisfied();
 	}
 
 	private TransportKeys createTransportKeys(long rotationPeriod,
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
index 763c50332da6d0ff26bf6ee5f683281c1900b95f..1c6cc9d86f359dc248f9c30f54198fc4274d94c6 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/AndroidComponent.java
@@ -12,7 +12,7 @@ import org.briarproject.bramble.api.db.DatabaseConfig;
 import org.briarproject.bramble.api.db.DatabaseExecutor;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.identity.IdentityManager;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
+import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
 import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
 import org.briarproject.bramble.api.keyagreement.PayloadParser;
 import org.briarproject.bramble.api.lifecycle.IoExecutor;
@@ -125,7 +125,7 @@ public interface AndroidComponent
 
 	ContactExchangeTask contactExchangeTask();
 
-	KeyAgreementTaskFactory keyAgreementTaskFactory();
+	KeyAgreementTask keyAgreementTask();
 
 	PayloadEncoder payloadEncoder();
 
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java
index 54df56f3ae9caab020390a4e14aacd670a42c82d..fd0d46445404da75628165430afafab21f88aec5 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/keyagreement/ShowQrCodeFragment.java
@@ -24,7 +24,6 @@ import com.google.zxing.Result;
 import org.briarproject.bramble.api.event.Event;
 import org.briarproject.bramble.api.event.EventBus;
 import org.briarproject.bramble.api.keyagreement.KeyAgreementTask;
-import org.briarproject.bramble.api.keyagreement.KeyAgreementTaskFactory;
 import org.briarproject.bramble.api.keyagreement.Payload;
 import org.briarproject.bramble.api.keyagreement.PayloadEncoder;
 import org.briarproject.bramble.api.keyagreement.PayloadParser;
@@ -48,6 +47,7 @@ import java.util.logging.Logger;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
+import javax.inject.Provider;
 
 import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
 import static android.bluetooth.BluetoothAdapter.EXTRA_STATE;
@@ -68,7 +68,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	private static final Logger LOG = Logger.getLogger(TAG);
 
 	@Inject
-	KeyAgreementTaskFactory keyAgreementTaskFactory;
+	Provider<KeyAgreementTask> keyAgreementTaskProvider;
 	@Inject
 	PayloadEncoder payloadEncoder;
 	@Inject
@@ -187,7 +187,7 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	@UiThread
 	private void startListening() {
 		KeyAgreementTask oldTask = task;
-		KeyAgreementTask newTask = keyAgreementTaskFactory.createTask();
+		KeyAgreementTask newTask = keyAgreementTaskProvider.get();
 		task = newTask;
 		ioExecutor.execute(() -> {
 			if (oldTask != null) oldTask.stopListening();
diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java
index a0ee65e00b072c7e5439389d99353d15b69d6f0b..72e6030b10b207a179abf99be29e7897af2777cb 100644
--- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java
+++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java
@@ -86,4 +86,44 @@ public interface IntroductionConstants {
 	int TASK_ACTIVATE_CONTACT = 1;
 	int TASK_ABORT = 2;
 
+	/**
+	 * Label for deriving the shared secret.
+	 */
+	String SHARED_SECRET_LABEL =
+			"org.briarproject.briar.introduction/SHARED_SECRET";
+
+	/**
+	 * Label for deriving Alice's key binding nonce from the shared secret.
+	 */
+	String ALICE_NONCE_LABEL =
+			"org.briarproject.briar.introduction/ALICE_NONCE";
+
+	/**
+	 * Label for deriving Bob's key binding nonce from the shared secret.
+	 */
+	String BOB_NONCE_LABEL =
+			"org.briarproject.briar.introduction/BOB_NONCE";
+
+	/**
+	 * Label for deriving Alice's MAC key from the shared secret.
+	 */
+	String ALICE_MAC_KEY_LABEL =
+			"org.briarproject.briar.introduction/ALICE_MAC_KEY";
+
+	/**
+	 * Label for deriving Bob's MAC key from the shared secret.
+	 */
+	String BOB_MAC_KEY_LABEL =
+			"org.briarproject.briar.introduction/BOB_MAC_KEY";
+
+	/**
+	 * Label for signing the introduction response.
+	 */
+	String SIGNING_LABEL =
+			"org.briarproject.briar.introduction/RESPONSE_SIGNATURE";
+
+	/**
+	 * Label for MACing the introduction response.
+	 */
+	String MAC_LABEL = "org.briarproject.briar.introduction/RESPONSE_MAC";
 }
diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java
index 846b8b4f6587e20624e6147ebd76358531e52caa..6ecfcef49cfa0b54d7ef7c305a7eedf66153f352 100644
--- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java
+++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java
@@ -50,7 +50,11 @@ import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE;
 import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.ADDED_CONTACT_ID;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_MAC_KEY_LABEL;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_NONCE_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.BOB_MAC_KEY_LABEL;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.BOB_NONCE_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS;
@@ -60,6 +64,7 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.INTR
 import static org.briarproject.briar.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
@@ -76,7 +81,9 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.REMO
 import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.SHARED_SECRET_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK;
@@ -89,7 +96,6 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID;
 
 @Immutable
 @NotNullByDefault
@@ -98,9 +104,6 @@ class IntroduceeManager {
 	private static final Logger LOG =
 			Logger.getLogger(IntroduceeManager.class.getName());
 
-	static final String SIGNING_LABEL_RESPONSE =
-			CLIENT_ID.getString() + "/RESPONSE";
-
 	private final MessageSender messageSender;
 	private final DatabaseComponent db;
 	private final ClientHelper clientHelper;
@@ -288,8 +291,7 @@ class IntroduceeManager {
 
 	@Nullable
 	private BdfDictionary performTasks(Transaction txn,
-			BdfDictionary localState)
-			throws FormatException, DbException {
+			BdfDictionary localState) throws FormatException, DbException {
 
 		if (!localState.containsKey(TASK) || localState.get(TASK) == NULL_VALUE)
 			return null;
@@ -306,22 +308,21 @@ class IntroduceeManager {
 			}
 
 			// figure out who takes which role by comparing public keys
-			byte[] publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY);
-			byte[] theirEphemeralKey = localState.getRaw(E_PUBLIC_KEY);
-			int comp = Bytes.COMPARATOR.compare(new Bytes(publicKeyBytes),
-					new Bytes(theirEphemeralKey));
+			byte[] ourPublicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY);
+			byte[] theirPublicKeyBytes = localState.getRaw(E_PUBLIC_KEY);
+			int comp = Bytes.COMPARATOR.compare(new Bytes(ourPublicKeyBytes),
+					new Bytes(theirPublicKeyBytes));
 			boolean alice = comp < 0;
 
 			// get our local author
 			LocalAuthor author = identityManager.getLocalAuthor(txn);
 
 			SecretKey secretKey;
-			byte[] privateKeyBytes = localState.getRaw(OUR_PRIVATE_KEY);
+			byte[] ourPrivateKeyBytes = localState.getRaw(OUR_PRIVATE_KEY);
 			try {
 				// derive secret master key
-				secretKey =
-						deriveSecretKey(publicKeyBytes, privateKeyBytes, alice,
-								theirEphemeralKey);
+				secretKey = deriveSecretKey(ourPublicKeyBytes,
+						ourPrivateKeyBytes, alice, theirPublicKeyBytes);
 				// derive MAC keys and nonces, sign our nonce and calculate MAC
 				deriveMacKeysAndNonces(localState, author, secretKey, alice);
 			} catch (GeneralSecurityException e) {
@@ -410,34 +411,36 @@ class IntroduceeManager {
 		return null;
 	}
 
-	private SecretKey deriveSecretKey(byte[] publicKeyBytes,
-			byte[] privateKeyBytes, boolean alice, byte[] theirPublicKey)
-			throws GeneralSecurityException {
+	private SecretKey deriveSecretKey(byte[] ourPublicKeyBytes,
+			byte[] ourPrivateKeyBytes, boolean alice,
+			byte[] theirPublicKeyBytes) throws GeneralSecurityException {
 		// parse the local ephemeral key pair
 		KeyParser keyParser = cryptoComponent.getAgreementKeyParser();
-		PublicKey publicKey;
-		PrivateKey privateKey;
+		PublicKey ourPublicKey;
+		PrivateKey ourPrivateKey;
 		try {
-			publicKey = keyParser.parsePublicKey(publicKeyBytes);
-			privateKey = keyParser.parsePrivateKey(privateKeyBytes);
+			ourPublicKey = keyParser.parsePublicKey(ourPublicKeyBytes);
+			ourPrivateKey = keyParser.parsePrivateKey(ourPrivateKeyBytes);
 		} catch (GeneralSecurityException e) {
 			if (LOG.isLoggable(WARNING)) {
 				LOG.log(WARNING, e.toString(), e);
 			}
 			throw new RuntimeException("Our own ephemeral key is invalid");
 		}
-		KeyPair keyPair = new KeyPair(publicKey, privateKey);
+		KeyPair ourKeyPair = new KeyPair(ourPublicKey, ourPrivateKey);
+		PublicKey theirPublicKey =
+				keyParser.parsePublicKey(theirPublicKeyBytes);
 
-		// The master secret is derived from the local ephemeral key pair
+		// The shared secret is derived from the local ephemeral key pair
 		// and the remote ephemeral public key
-		return cryptoComponent
-				.deriveMasterSecret(theirPublicKey, keyPair, alice);
+		return cryptoComponent.deriveSharedSecret(SHARED_SECRET_LABEL,
+				theirPublicKey, ourKeyPair, alice);
 	}
 
 	/**
 	 * Derives nonces, signs our nonce and calculates MAC
 	 * <p>
-	 * Derives two nonces and two mac keys from the secret master key.
+	 * Derives two nonces and two MAC keys from the shared secret key.
 	 * The other introducee's nonce and MAC key are added to the localState.
 	 * <p>
 	 * Our nonce is signed with the local author's long-term private key.
@@ -448,21 +451,24 @@ class IntroduceeManager {
 	private void deriveMacKeysAndNonces(BdfDictionary localState,
 			LocalAuthor author, SecretKey secretKey, boolean alice)
 			throws FormatException, GeneralSecurityException {
-		// Derive two nonces and a MAC key from the secret master key
-		byte[] ourNonce =
-				cryptoComponent.deriveSignatureNonce(secretKey, alice);
-		byte[] theirNonce =
-				cryptoComponent.deriveSignatureNonce(secretKey, !alice);
-		SecretKey macKey = cryptoComponent.deriveMacKey(secretKey, alice);
-		SecretKey theirMacKey = cryptoComponent.deriveMacKey(secretKey, !alice);
+		// Derive two nonces and two MAC keys from the shared secret key
+		String ourNonceLabel = alice ? ALICE_NONCE_LABEL : BOB_NONCE_LABEL;
+		String theirNonceLabel = alice ? BOB_NONCE_LABEL : ALICE_NONCE_LABEL;
+		byte[] ourNonce = cryptoComponent.mac(ourNonceLabel, secretKey);
+		byte[] theirNonce = cryptoComponent.mac(theirNonceLabel, secretKey);
+		String ourKeyLabel = alice ? ALICE_MAC_KEY_LABEL : BOB_MAC_KEY_LABEL;
+		String theirKeyLabel = alice ? BOB_MAC_KEY_LABEL : ALICE_MAC_KEY_LABEL;
+		SecretKey ourMacKey = cryptoComponent.deriveKey(ourKeyLabel, secretKey);
+		SecretKey theirMacKey =
+				cryptoComponent.deriveKey(theirKeyLabel, secretKey);
 
 		// Save the other nonce and MAC key for the verification
 		localState.put(NONCE, theirNonce);
 		localState.put(MAC_KEY, theirMacKey.getBytes());
 
 		// Sign our nonce with our long-term identity public key
-		byte[] sig = cryptoComponent
-				.sign(SIGNING_LABEL_RESPONSE, ourNonce, author.getPrivateKey());
+		byte[] sig = cryptoComponent.sign(SIGNING_LABEL, ourNonce,
+				author.getPrivateKey());
 
 		// Calculate a MAC over identity public key, ephemeral public key,
 		// transport properties and timestamp.
@@ -472,7 +478,7 @@ class IntroduceeManager {
 		BdfList toMacList = BdfList.of(author.getPublicKey(),
 				publicKeyBytes, tp, ourTime);
 		byte[] toMac = clientHelper.toByteArray(toMacList);
-		byte[] mac = cryptoComponent.mac(macKey, toMac);
+		byte[] mac = cryptoComponent.mac(MAC_LABEL, ourMacKey, toMac);
 
 		// Add MAC and signature to localState, so it can be included in ACK
 		localState.put(OUR_MAC, mac);
@@ -486,7 +492,7 @@ class IntroduceeManager {
 		byte[] key = localState.getRaw(PUBLIC_KEY);
 
 		// Verify the signature
-		if (!cryptoComponent.verify(SIGNING_LABEL_RESPONSE, nonce, key, sig)) {
+		if (!cryptoComponent.verify(SIGNING_LABEL, nonce, key, sig)) {
 			LOG.warning("Invalid nonce signature in ACK");
 			throw new GeneralSecurityException();
 		}
@@ -506,7 +512,7 @@ class IntroduceeManager {
 		long timestamp = localState.getLong(TIME);
 		BdfList toMacList = BdfList.of(pubKey, ePubKey, tp, timestamp);
 		byte[] toMac = clientHelper.toByteArray(toMacList);
-		byte[] calculatedMac = cryptoComponent.mac(macKey, toMac);
+		byte[] calculatedMac = cryptoComponent.mac(MAC_LABEL, macKey, toMac);
 		if (!Arrays.equals(mac, calculatedMac)) {
 			LOG.warning("Received ACK with invalid MAC");
 			throw new GeneralSecurityException();
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java
index 0453530c9a42833d23958ddfd22b91b4cd12fb86..2144b49799cc6f109d79d79cd50298affbe5310f 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java
@@ -53,6 +53,7 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.INTR
 import static org.briarproject.briar.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
@@ -66,6 +67,7 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE
 import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
@@ -74,7 +76,6 @@ import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.introduction.IntroduceeManager.SIGNING_LABEL_RESPONSE;
 import static org.hamcrest.Matchers.array;
 import static org.hamcrest.Matchers.samePropertyValuesAs;
 import static org.junit.Assert.assertFalse;
@@ -266,7 +267,7 @@ public class IntroduceeManagerTest extends BriarTestCase {
 		);
 
 		context.checking(new Expectations() {{
-			oneOf(cryptoComponent).verify(SIGNING_LABEL_RESPONSE, nonce,
+			oneOf(cryptoComponent).verify(SIGNING_LABEL, nonce,
 					introducee2.getAuthor().getPublicKey(), sig);
 			will(returnValue(false));
 		}});
@@ -296,7 +297,7 @@ public class IntroduceeManagerTest extends BriarTestCase {
 		state.put(SIGNATURE, sig);
 
 		context.checking(new Expectations() {{
-			oneOf(cryptoComponent).verify(SIGNING_LABEL_RESPONSE, nonce,
+			oneOf(cryptoComponent).verify(SIGNING_LABEL, nonce,
 					publicKeyBytes, sig);
 			will(returnValue(true));
 		}});
@@ -330,7 +331,8 @@ public class IntroduceeManagerTest extends BriarTestCase {
 					BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time));
 			will(returnValue(signBytes));
 			//noinspection unchecked
-			oneOf(cryptoComponent).mac(with(samePropertyValuesAs(macKey)),
+			oneOf(cryptoComponent).mac(with(MAC_LABEL),
+					with(samePropertyValuesAs(macKey)),
 					with(array(equal(signBytes))));
 			will(returnValue(mac));
 		}});
@@ -343,14 +345,15 @@ public class IntroduceeManagerTest extends BriarTestCase {
 					BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time));
 			will(returnValue(signBytes));
 			//noinspection unchecked
-			oneOf(cryptoComponent).mac(with(samePropertyValuesAs(macKey)),
+			oneOf(cryptoComponent).mac(with(MAC_LABEL),
+					with(samePropertyValuesAs(macKey)),
 					with(array(equal(signBytes))));
 			will(returnValue(TestUtils.getRandomBytes(MAC_LENGTH)));
 		}});
 		try {
 			introduceeManager.verifyMac(state);
 			fail();
-		} catch(GeneralSecurityException e) {
+		} catch (GeneralSecurityException e) {
 			// expected
 		}
 		context.assertIsSatisfied();
diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
index 4902f58c1fc216312e7f84ab9e84668073f74504..b6b088f9d5293f86e3a29ed9e630ca0cf54fb268 100644
--- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
+++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java
@@ -56,21 +56,25 @@ import javax.inject.Inject;
 import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
 import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID;
 import static org.briarproject.briar.api.client.MessageQueueManager.QUEUE_STATE_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_MAC_KEY_LABEL;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_NONCE_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.SHARED_SECRET_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
+import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
 import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
-import static org.briarproject.briar.introduction.IntroduceeManager.SIGNING_LABEL_RESPONSE;
 import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
@@ -635,7 +639,7 @@ public class IntroductionIntegrationTest
 		// adapt outgoing message queue to removed message
 		Group g2 = introductionGroupFactory
 				.createIntroductionGroup(contact2From0);
-		decreaseOutgoingMessageCounter(ch, g2.getId(), 1);
+		decreaseOutgoingMessageCounter(ch, g2.getId());
 
 		// allow visitor to modify response
 		boolean earlyAbort = visitor.visit(response);
@@ -746,34 +750,32 @@ public class IntroductionIntegrationTest
 		// create keys
 		KeyPair keyPair1 = crypto.generateSignatureKeyPair();
 		KeyPair eKeyPair1 = crypto.generateAgreementKeyPair();
-		byte[] ePublicKeyBytes1 = eKeyPair1.getPublic().getEncoded();
 		KeyPair eKeyPair2 = crypto.generateAgreementKeyPair();
-		byte[] ePublicKeyBytes2 = eKeyPair2.getPublic().getEncoded();
 
 		// Nonce 1
-		SecretKey secretKey =
-				crypto.deriveMasterSecret(ePublicKeyBytes2, eKeyPair1, true);
-		byte[] nonce1 = crypto.deriveSignatureNonce(secretKey, true);
+		SecretKey sharedSecret = crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
+				eKeyPair2.getPublic(), eKeyPair1, true);
+		byte[] nonce1 = crypto.mac(ALICE_NONCE_LABEL, sharedSecret);
 
 		// Signature 1
-		byte[] sig1 = crypto.sign(SIGNING_LABEL_RESPONSE, nonce1,
+		byte[] sig1 = crypto.sign(SIGNING_LABEL, nonce1,
 				keyPair1.getPrivate().getEncoded());
 
 		// MAC 1
-		SecretKey macKey1 = crypto.deriveMacKey(secretKey, true);
+		SecretKey macKey1 = crypto.deriveKey(ALICE_MAC_KEY_LABEL, sharedSecret);
 		BdfDictionary tp1 = BdfDictionary.of(new BdfEntry("fake", "fake"));
 		long time1 = clock.currentTimeMillis();
 		BdfList toMacList = BdfList.of(keyPair1.getPublic().getEncoded(),
-				ePublicKeyBytes1, tp1, time1);
+				eKeyPair1.getPublic().getEncoded(), tp1, time1);
 		byte[] toMac = clientHelper.toByteArray(toMacList);
-		byte[] mac1 = crypto.mac(macKey1, toMac);
+		byte[] mac1 = crypto.mac(MAC_LABEL, macKey1, toMac);
 
 		// create only relevant part of state for introducee2
 		BdfDictionary state = new BdfDictionary();
 		state.put(PUBLIC_KEY, keyPair1.getPublic().getEncoded());
 		state.put(TRANSPORT, tp1);
 		state.put(TIME, time1);
-		state.put(E_PUBLIC_KEY, ePublicKeyBytes1);
+		state.put(E_PUBLIC_KEY, eKeyPair1.getPublic().getEncoded());
 		state.put(MAC, mac1);
 		state.put(MAC_KEY, macKey1.getBytes());
 		state.put(NONCE, nonce1);
@@ -786,16 +788,16 @@ public class IntroductionIntegrationTest
 		// replace ephemeral key pair and recalculate matching keys and nonce
 		KeyPair eKeyPair1f = crypto.generateAgreementKeyPair();
 		byte[] ePublicKeyBytes1f = eKeyPair1f.getPublic().getEncoded();
-		secretKey =
-				crypto.deriveMasterSecret(ePublicKeyBytes2, eKeyPair1f, true);
-		nonce1 = crypto.deriveSignatureNonce(secretKey, true);
+		sharedSecret = crypto.deriveSharedSecret(SHARED_SECRET_LABEL,
+				eKeyPair2.getPublic(), eKeyPair1f, true);
+		nonce1 = crypto.mac(ALICE_NONCE_LABEL, sharedSecret);
 
 		// recalculate MAC
-		macKey1 = crypto.deriveMacKey(secretKey, true);
+		macKey1 = crypto.deriveKey(ALICE_MAC_KEY_LABEL, sharedSecret);
 		toMacList = BdfList.of(keyPair1.getPublic().getEncoded(),
 				ePublicKeyBytes1f, tp1, time1);
 		toMac = clientHelper.toByteArray(toMacList);
-		mac1 = crypto.mac(macKey1, toMac);
+		mac1 = crypto.mac(MAC_LABEL, macKey1, toMac);
 
 		// update state with faked information
 		state.put(E_PUBLIC_KEY, ePublicKeyBytes1f);
@@ -970,12 +972,12 @@ public class IntroductionIntegrationTest
 
 	}
 
-	private void decreaseOutgoingMessageCounter(ClientHelper ch, GroupId g,
-			int num) throws FormatException, DbException {
+	private void decreaseOutgoingMessageCounter(ClientHelper ch, GroupId g)
+			throws FormatException, DbException {
 		BdfDictionary gD = ch.getGroupMetadataAsDictionary(g);
 		LOG.warning(gD.toString());
 		BdfDictionary queue = gD.getDictionary(QUEUE_STATE_KEY);
-		queue.put("nextOut", queue.getLong("nextOut") - num);
+		queue.put("nextOut", queue.getLong("nextOut") - 1);
 		gD.put(QUEUE_STATE_KEY, queue);
 		ch.mergeGroupMetadata(g, gD);
 	}