diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
index 6e67eb867ae034672ceea7fc4ba28f0bc245e60d..b8a9554caa6eaa3b99ab7e81febf1fbd6e366214 100644
--- a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
+++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java
@@ -57,6 +57,7 @@ import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
 import static android.view.View.INVISIBLE;
 import static android.view.View.VISIBLE;
 import static android.widget.Toast.LENGTH_LONG;
+import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
 public class ShowQrCodeFragment extends BaseEventFragment
@@ -244,8 +245,10 @@ public class ShowQrCodeFragment extends BaseEventFragment
 	@UiThread
 	private void qrCodeScanned(String content) {
 		try {
-			Payload remotePayload = payloadParser.parse(
-					Base64.decode(content, 0));
+			byte[] encoded = Base64.decode(content, 0);
+			if (LOG.isLoggable(INFO))
+				LOG.info("Remote payload is " + encoded.length + " bytes");
+			Payload remotePayload = payloadParser.parse(encoded);
 			cameraView.setVisibility(INVISIBLE);
 			statusView.setVisibility(VISIBLE);
 			status.setText(R.string.connecting_to_device);
@@ -293,9 +296,10 @@ public class ShowQrCodeFragment extends BaseEventFragment
 
 			@Override
 			protected Bitmap doInBackground(Void... params) {
-				String input =
-						Base64.encodeToString(payloadEncoder.encode(payload),
-								0);
+				byte[] encoded = payloadEncoder.encode(payload);
+				if (LOG.isLoggable(INFO))
+					LOG.info("Local payload is " + encoded.length + " bytes");
+				String input = Base64.encodeToString(encoded, 0);
 				return QrCodeUtils.createQrCode(dm, input);
 			}
 
diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
index 686d15b615411ed05858061fcb46f9944c98ba68..d1ba7d858208ceea83441b838ca8093d30b6bca2 100644
--- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java
@@ -11,12 +11,15 @@ import android.content.IntentFilter;
 
 import org.briarproject.android.api.AndroidExecutor;
 import org.briarproject.android.util.AndroidUtils;
+import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementConnection;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -24,6 +27,7 @@ import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
 import org.briarproject.api.properties.TransportProperties;
 import org.briarproject.util.StringUtils;
 
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.InputStream;
 import java.security.SecureRandom;
@@ -44,6 +48,8 @@ import java.util.concurrent.Future;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
+import javax.annotation.Nullable;
+
 import static android.bluetooth.BluetoothAdapter.ACTION_SCAN_MODE_CHANGED;
 import static android.bluetooth.BluetoothAdapter.ACTION_STATE_CHANGED;
 import static android.bluetooth.BluetoothAdapter.EXTRA_SCAN_MODE;
@@ -57,8 +63,11 @@ import static android.bluetooth.BluetoothDevice.EXTRA_DEVICE;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
 import static org.briarproject.util.PrivacyUtils.scrubMacAddress;
 
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
 class DroidtoothPlugin implements DuplexPlugin {
 
 	// Share an ID with the J2SE Bluetooth plugin
@@ -217,7 +226,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 		return UUID.fromString(uuid);
 	}
 
-	private void tryToClose(BluetoothServerSocket ss) {
+	private void tryToClose(@Nullable BluetoothServerSocket ss) {
 		try {
 			if (ss != null) ss.close();
 		} catch (IOException e) {
@@ -340,9 +349,9 @@ class DroidtoothPlugin implements DuplexPlugin {
 		}
 	}
 
-	private void tryToClose(BluetoothSocket s) {
+	private void tryToClose(@Nullable Closeable c) {
 		try {
-			if (s != null) s.close();
+			if (c != null) c.close();
 		} catch (IOException e) {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 		}
@@ -420,7 +429,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 	}
 
 	private void closeSockets(final List<Future<BluetoothSocket>> futures,
-			final BluetoothSocket chosen) {
+			@Nullable final BluetoothSocket chosen) {
 		ioExecutor.execute(new Runnable() {
 			@Override
 			public void run() {
@@ -454,6 +463,9 @@ class DroidtoothPlugin implements DuplexPlugin {
 	@Override
 	public KeyAgreementListener createKeyAgreementListener(byte[] commitment) {
 		if (!isRunning()) return null;
+		// There's no point listening if we can't discover our own address
+		String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
+		if (address.isEmpty()) return null;
 		// No truncation necessary because COMMIT_LENGTH = 16
 		UUID uuid = UUID.nameUUIDFromBytes(commitment);
 		if (LOG.isLoggable(INFO)) LOG.info("Key agreement UUID " + uuid);
@@ -466,23 +478,23 @@ class DroidtoothPlugin implements DuplexPlugin {
 			if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
 			return null;
 		}
-		TransportProperties p = new TransportProperties();
-		String address = AndroidUtils.getBluetoothAddress(appContext, adapter);
-		if (!StringUtils.isNullOrEmpty(address))
-			p.put(PROP_ADDRESS, address);
-		TransportDescriptor d = new TransportDescriptor(ID, p);
-		return new BluetoothKeyAgreementListener(d, ss);
+		BdfList descriptor = new BdfList();
+		descriptor.add(TRANSPORT_ID_BLUETOOTH);
+		descriptor.add(StringUtils.macToBytes(address));
+		return new BluetoothKeyAgreementListener(descriptor, ss);
 	}
 
 	@Override
 	public DuplexTransportConnection createKeyAgreementConnection(
-			byte[] commitment, TransportDescriptor d, long timeout) {
+			byte[] commitment, BdfList descriptor, long timeout) {
 		if (!isRunning()) return null;
-		if (!ID.equals(d.getIdentifier())) return null;
-		TransportProperties p = d.getProperties();
-		if (p == null) return null;
-		String address = p.get(PROP_ADDRESS);
-		if (StringUtils.isNullOrEmpty(address)) return null;
+		String address;
+		try {
+			address = parseAddress(descriptor);
+		} catch (FormatException e) {
+			LOG.info("Invalid address in key agreement descriptor");
+			return null;
+		}
 		// No truncation necessary because COMMIT_LENGTH = 16
 		UUID uuid = UUID.nameUUIDFromBytes(commitment);
 		if (LOG.isLoggable(INFO))
@@ -492,6 +504,12 @@ class DroidtoothPlugin implements DuplexPlugin {
 		return new DroidtoothTransportConnection(this, s);
 	}
 
+	private String parseAddress(BdfList descriptor) throws FormatException {
+		byte[] mac = descriptor.getRaw(1);
+		if (mac.length != 6) throw new FormatException();
+		return StringUtils.macToString(mac);
+	}
+
 	private class BluetoothStateReceiver extends BroadcastReceiver {
 
 		@Override
@@ -626,7 +644,7 @@ class DroidtoothPlugin implements DuplexPlugin {
 
 		private final BluetoothServerSocket ss;
 
-		BluetoothKeyAgreementListener(TransportDescriptor descriptor,
+		BluetoothKeyAgreementListener(BdfList descriptor,
 				BluetoothServerSocket ss) {
 			super(descriptor);
 			this.ss = ss;
diff --git a/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java b/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java
index 598e914b6d695d1373fba80145c392e2abe32e7f..b96aefa0893fa8c2dcf1e87d02649500d9fc7841 100644
--- a/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tcp/AndroidLanTcpPlugin.java
@@ -7,16 +7,20 @@ import android.content.IntentFilter;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 
 import java.util.concurrent.Executor;
 import java.util.logging.Logger;
 
+import javax.annotation.Nullable;
+
 import static android.content.Context.CONNECTIVITY_SERVICE;
 import static android.net.ConnectivityManager.CONNECTIVITY_ACTION;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 
+@NotNullByDefault
 class AndroidLanTcpPlugin extends LanTcpPlugin {
 
 	private static final Logger LOG =
@@ -24,6 +28,7 @@ class AndroidLanTcpPlugin extends LanTcpPlugin {
 
 	private final Context appContext;
 
+	@Nullable
 	private volatile BroadcastReceiver networkStateReceiver = null;
 
 	AndroidLanTcpPlugin(Executor ioExecutor, Backoff backoff,
diff --git a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
index e66e0a6583e9ca7acfff0a018e5411d621ea9422..2a8b58991bcafe11f4b2e4ed24d33286aca18519 100644
--- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
+++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java
@@ -19,11 +19,12 @@ import org.briarproject.android.util.AndroidUtils;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.event.Event;
 import org.briarproject.api.event.EventListener;
 import org.briarproject.api.event.SettingsUpdatedEvent;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.TorConstants;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
@@ -60,6 +61,7 @@ import java.util.logging.Logger;
 import java.util.regex.Pattern;
 import java.util.zip.ZipInputStream;
 
+import javax.annotation.Nullable;
 import javax.net.SocketFactory;
 
 import static android.content.Context.CONNECTIVITY_SERVICE;
@@ -76,6 +78,7 @@ import static net.freehaven.tor.control.TorControlCommands.HS_PRIVKEY;
 import static org.briarproject.api.plugins.TorConstants.CONTROL_PORT;
 import static org.briarproject.util.PrivacyUtils.scrubOnion;
 
+@NotNullByDefault
 class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 
 	private static final String PROP_ONION = "onion";
@@ -104,9 +107,13 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 	private final AtomicBoolean used = new AtomicBoolean(false);
 
 	private volatile boolean running = false;
+	@Nullable
 	private volatile ServerSocket socket = null;
+	@Nullable
 	private volatile Socket controlSocket = null;
+	@Nullable
 	private volatile TorControlConnection controlConnection = null;
+	@Nullable
 	private volatile BroadcastReceiver networkStateReceiver = null;
 
 	TorPlugin(Executor ioExecutor, Context appContext,
@@ -289,7 +296,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		return appContext.getResources().getAssets().open("torrc");
 	}
 
-	private void tryToClose(Closeable c) {
+	private void tryToClose(@Nullable Closeable c) {
 		try {
 			if (c != null) c.close();
 		} catch (IOException e) {
@@ -297,7 +304,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		}
 	}
 
-	private void tryToClose(Socket s) {
+	private void tryToClose(@Nullable Socket s) {
 		try {
 			if (s != null) s.close();
 		} catch (IOException e) {
@@ -385,7 +392,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 		});
 	}
 
-	private void tryToClose(ServerSocket ss) {
+	private void tryToClose(@Nullable ServerSocket ss) {
 		try {
 			if (ss != null) ss.close();
 		} catch (IOException e) {
@@ -574,7 +581,7 @@ class TorPlugin implements DuplexPlugin, EventHandler, EventListener {
 
 	@Override
 	public DuplexTransportConnection createKeyAgreementConnection(
-			byte[] commitment, TransportDescriptor d, long timeout) {
+			byte[] commitment, BdfList descriptor, long timeout) {
 		throw new UnsupportedOperationException();
 	}
 
diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java
index f239111269124e8d938f15bbc5561a4670ed5d0a..9961a2b9949a74dde7ec7b6ff1fc3d645518347d 100644
--- a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java
+++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementConstants.java
@@ -4,7 +4,7 @@ package org.briarproject.api.keyagreement;
 public interface KeyAgreementConstants {
 
 	/** The current version of the BQP protocol. */
-	byte PROTOCOL_VERSION = 1;
+	byte PROTOCOL_VERSION = 2;
 
 	/** The length of the record header in bytes. */
 	int RECORD_HEADER_LENGTH = 4;
@@ -16,4 +16,10 @@ public interface KeyAgreementConstants {
 	int COMMIT_LENGTH = 16;
 
 	long CONNECTION_TIMEOUT = 20 * 1000; // Milliseconds
+
+	/** The transport identifier for Bluetooth. */
+	int TRANSPORT_ID_BLUETOOTH = 0;
+
+	/** The transport identifier for LAN. */
+	int TRANSPORT_ID_LAN = 1;
 }
diff --git a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java
index 05163614cc13d2ab383c1041813fc48b63977ae0..41b18a7de3264de851655550a3ba2db41f00ed4b 100644
--- a/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java
+++ b/briar-api/src/org/briarproject/api/keyagreement/KeyAgreementListener.java
@@ -1,5 +1,7 @@
 package org.briarproject.api.keyagreement;
 
+import org.briarproject.api.data.BdfList;
+
 import java.util.concurrent.Callable;
 
 /**
@@ -7,9 +9,9 @@ import java.util.concurrent.Callable;
  */
 public abstract class KeyAgreementListener {
 
-	private final TransportDescriptor descriptor;
+	private final BdfList descriptor;
 
-	public KeyAgreementListener(TransportDescriptor descriptor) {
+	public KeyAgreementListener(BdfList descriptor) {
 		this.descriptor = descriptor;
 	}
 
@@ -17,7 +19,7 @@ public abstract class KeyAgreementListener {
 	 * Returns the descriptor that a remote peer can use to connect to this
 	 * listener.
 	 */
-	public TransportDescriptor getDescriptor() {
+	public BdfList getDescriptor() {
 		return descriptor;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/keyagreement/Payload.java b/briar-api/src/org/briarproject/api/keyagreement/Payload.java
index 0c749da53ec9e2a70c652cc3dcdf70f5a9d079a6..60cbb45a363115f782556cdde20d00a15d28c7dd 100644
--- a/briar-api/src/org/briarproject/api/keyagreement/Payload.java
+++ b/briar-api/src/org/briarproject/api/keyagreement/Payload.java
@@ -1,29 +1,40 @@
 package org.briarproject.api.keyagreement;
 
 import org.briarproject.api.Bytes;
+import org.briarproject.api.TransportId;
+import org.briarproject.api.data.BdfList;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
-import java.util.List;
+import java.util.Map;
+
+import javax.annotation.concurrent.Immutable;
 
 /**
  * A BQP payload.
  */
+@Immutable
+@NotNullByDefault
 public class Payload implements Comparable<Payload> {
 
 	private final Bytes commitment;
-	private final List<TransportDescriptor> descriptors;
+	private final Map<TransportId, BdfList> descriptors;
 
-	public Payload(byte[] commitment, List<TransportDescriptor> descriptors) {
+	public Payload(byte[] commitment, Map<TransportId, BdfList> descriptors) {
 		this.commitment = new Bytes(commitment);
 		this.descriptors = descriptors;
 	}
 
-	/** Returns the commitment contained in this payload. */
+	/**
+	 * Returns the commitment contained in this payload.
+	 */
 	public byte[] getCommitment() {
 		return commitment.getBytes();
 	}
 
-	/** Returns the transport descriptors contained in this payload. */
-	public List<TransportDescriptor> getTransportDescriptors() {
+	/**
+	 * Returns the transport descriptors contained in this payload.
+	 */
+	public Map<TransportId, BdfList> getTransportDescriptors() {
 		return descriptors;
 	}
 
diff --git a/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java b/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java
index 0df9c653d418d464af843f74547981bc06f7a223..0ada113f360d046ed81100f4a3fe83a3b7bbb1eb 100644
--- a/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java
+++ b/briar-api/src/org/briarproject/api/keyagreement/PayloadParser.java
@@ -1,7 +1,10 @@
 package org.briarproject.api.keyagreement;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
 import java.io.IOException;
 
+@NotNullByDefault
 public interface PayloadParser {
 
 	Payload parse(byte[] raw) throws IOException;
diff --git a/briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java b/briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java
deleted file mode 100644
index cdaa5a579464e7287f06ada392cb16d9b2ffcb5d..0000000000000000000000000000000000000000
--- a/briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package org.briarproject.api.keyagreement;
-
-import org.briarproject.api.TransportId;
-import org.briarproject.api.properties.TransportProperties;
-
-/**
- * Describes how to connect to a device over a short-range transport.
- */
-public class TransportDescriptor {
-
-	private final TransportId id;
-	private final TransportProperties properties;
-
-	public TransportDescriptor(TransportId id, TransportProperties properties) {
-		this.id = id;
-		this.properties = properties;
-	}
-
-	/** Returns the transport identifier. */
-	public TransportId getIdentifier() {
-		return id;
-	}
-
-	/** Returns the transport properties. */
-	public TransportProperties getProperties() {
-		return properties;
-	}
-}
diff --git a/briar-api/src/org/briarproject/api/plugins/Plugin.java b/briar-api/src/org/briarproject/api/plugins/Plugin.java
index 393fa27ea1737e86a8c794eacc3d4b506a7a6eb7..e54822cceccd94854febc9be2cc247bb9fd17f0e 100644
--- a/briar-api/src/org/briarproject/api/plugins/Plugin.java
+++ b/briar-api/src/org/briarproject/api/plugins/Plugin.java
@@ -2,28 +2,42 @@ package org.briarproject.api.plugins;
 
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
 import java.io.IOException;
 import java.util.Collection;
 
+@NotNullByDefault
 public interface Plugin {
 
-	/** Returns the plugin's transport identifier. */
+	/**
+	 * Returns the plugin's transport identifier.
+	 */
 	TransportId getId();
 
-	/** Returns the transport's maximum latency in milliseconds. */
+	/**
+	 * Returns the transport's maximum latency in milliseconds.
+	 */
 	int getMaxLatency();
 
-	/** Returns the transport's maximum idle time in milliseconds. */
+	/**
+	 * Returns the transport's maximum idle time in milliseconds.
+	 */
 	int getMaxIdleTime();
 
-	/** Starts the plugin and returns true if it started successfully. */
+	/**
+	 * Starts the plugin and returns true if it started successfully.
+	 */
 	boolean start() throws IOException;
 
-	/** Stops the plugin. */
+	/**
+	 * Stops the plugin.
+	 */
 	void stop() throws IOException;
 
-	/** Returns true if the plugin is running. */
+	/**
+	 * Returns true if the plugin is running.
+	 */
 	boolean isRunning();
 
 	/**
diff --git a/briar-api/src/org/briarproject/api/plugins/PluginManager.java b/briar-api/src/org/briarproject/api/plugins/PluginManager.java
index 18e71c2ec2e75ae4790c7e1580b7b95e75b1700c..3f1fcf31a22d3a00833b944c64f78622e73ed9f6 100644
--- a/briar-api/src/org/briarproject/api/plugins/PluginManager.java
+++ b/briar-api/src/org/briarproject/api/plugins/PluginManager.java
@@ -1,21 +1,26 @@
 package org.briarproject.api.plugins;
 
 import org.briarproject.api.TransportId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.simplex.SimplexPlugin;
 
 import java.util.Collection;
 
+import javax.annotation.Nullable;
+
 /**
  * Responsible for starting transport plugins at startup and stopping them at
  * shutdown.
  */
+@NotNullByDefault
 public interface PluginManager {
 
 	/**
 	 * Returns the plugin for the given transport, or null if no such plugin
 	 * has been created.
 	 */
+	@Nullable
 	Plugin getPlugin(TransportId t);
 
 	/**
diff --git a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java
index ff7db3b51d78526d10b2a54bcc342c3a691583c2..c45d5bab3b53ff69b5f712b7d0d8b71ddc3f2cd1 100644
--- a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java
+++ b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java
@@ -2,21 +2,30 @@ package org.briarproject.api.plugins.duplex;
 
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.Plugin;
 
-/** An interface for transport plugins that support duplex communication. */
+import javax.annotation.Nullable;
+
+/**
+ * An interface for transport plugins that support duplex communication.
+ */
+@NotNullByDefault
 public interface DuplexPlugin extends Plugin {
 
 	/**
 	 * Attempts to create and return a connection to the given contact using
 	 * the current transport and configuration properties. Returns null if a
-	 * connection could not be created.
+	 * connection cannot be created.
 	 */
+	@Nullable
 	DuplexTransportConnection createConnection(ContactId c);
 
-	/** Returns true if the plugin supports exchanging invitations. */
+	/**
+	 * Returns true if the plugin supports exchanging invitations.
+	 */
 	boolean supportsInvitations();
 
 	/**
@@ -24,21 +33,27 @@ public interface DuplexPlugin extends Plugin {
 	 * peer. Returns null if no connection can be established within the given
 	 * time.
 	 */
+	@Nullable
 	DuplexTransportConnection createInvitationConnection(PseudoRandom r,
 			long timeout, boolean alice);
 
-	/** Returns true if the plugin supports short-range key agreement. */
+	/**
+	 * Returns true if the plugin supports short-range key agreement.
+	 */
 	boolean supportsKeyAgreement();
 
 	/**
-	 * Returns a listener that can be used to perform key agreement.
+	 * Attempts to create and return a listener that can be used to perform key
+	 * agreement. Returns null if a listener cannot be created.
 	 */
+	@Nullable
 	KeyAgreementListener createKeyAgreementListener(byte[] localCommitment);
 
 	/**
 	 * Attempts to connect to the remote peer specified in the given descriptor.
 	 * Returns null if no connection can be established within the given time.
 	 */
+	@Nullable
 	DuplexTransportConnection createKeyAgreementConnection(
-			byte[] remoteCommitment, TransportDescriptor d, long timeout);
+			byte[] remoteCommitment, BdfList descriptor, long timeout);
 }
diff --git a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java
index 36e2ed438393090fa71cea2dde44f4b744298326..cf9f5ebec3543cad0407d9c25d0c72d44bffc831 100644
--- a/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java
+++ b/briar-api/src/org/briarproject/api/plugins/simplex/SimplexPlugin.java
@@ -1,24 +1,32 @@
 package org.briarproject.api.plugins.simplex;
 
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.Plugin;
 import org.briarproject.api.plugins.TransportConnectionReader;
 import org.briarproject.api.plugins.TransportConnectionWriter;
 
-/** An interface for transport plugins that support simplex communication. */
+import javax.annotation.Nullable;
+
+/**
+ * An interface for transport plugins that support simplex communication.
+ */
+@NotNullByDefault
 public interface SimplexPlugin extends Plugin {
 
 	/**
 	 * Attempts to create and return a reader for the given contact using the
 	 * current transport and configuration properties. Returns null if a reader
-	 * could not be created.
+	 * cannot be created.
 	 */
+	@Nullable
 	TransportConnectionReader createReader(ContactId c);
 
 	/**
 	 * Attempts to create and return a writer for the given contact using the
 	 * current transport and configuration properties. Returns null if a writer
-	 * could not be created.
+	 * cannot be created.
 	 */
+	@Nullable
 	TransportConnectionWriter createWriter(ContactId c);
 }
diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java
index 190a138502f9dce896ed93834fac2485b1f7cd4e..44636b31d65606fb46ba16a5be21b9940cd06754 100644
--- a/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java
+++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java
@@ -1,11 +1,13 @@
 package org.briarproject.keyagreement;
 
+import org.briarproject.api.TransportId;
 import org.briarproject.api.crypto.CryptoComponent;
 import org.briarproject.api.crypto.KeyPair;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementConnection;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
 import org.briarproject.api.keyagreement.Payload;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.plugins.Plugin;
 import org.briarproject.api.plugins.PluginManager;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
@@ -14,7 +16,10 @@ import org.briarproject.api.system.Clock;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
 import java.util.concurrent.Callable;
 import java.util.concurrent.CompletionService;
 import java.util.concurrent.ExecutionException;
@@ -68,14 +73,13 @@ class KeyAgreementConnector {
 		byte[] commitment = crypto.deriveKeyCommitment(
 				localKeyPair.getPublic().getEncoded());
 		// Start all listeners and collect their descriptors
-		List<TransportDescriptor> descriptors =
-				new ArrayList<TransportDescriptor>();
+		Map<TransportId, BdfList> descriptors =
+				new HashMap<TransportId, BdfList>();
 		for (DuplexPlugin plugin : pluginManager.getKeyAgreementPlugins()) {
-			KeyAgreementListener l = plugin.createKeyAgreementListener(
-					commitment);
+			KeyAgreementListener l =
+					plugin.createKeyAgreementListener(commitment);
 			if (l != null) {
-				TransportDescriptor d = l.getDescriptor();
-				descriptors.add(d);
+				descriptors.put(plugin.getId(), l.getDescriptor());
 				pending.add(connect.submit(new ReadableTask(l.listen())));
 				listeners.add(l);
 			}
@@ -100,13 +104,16 @@ class KeyAgreementConnector {
 
 		// Start connecting over supported transports
 		LOG.info("Starting outgoing BQP connections");
-		for (TransportDescriptor d : remotePayload.getTransportDescriptors()) {
-			DuplexPlugin plugin = (DuplexPlugin) pluginManager.getPlugin(
-					d.getIdentifier());
-			if (plugin != null)
+		Map<TransportId, BdfList> descriptors =
+				remotePayload.getTransportDescriptors();
+		for (Entry<TransportId, BdfList> e : descriptors.entrySet()) {
+			Plugin p = pluginManager.getPlugin(e.getKey());
+			if (p instanceof DuplexPlugin) {
+				DuplexPlugin plugin = (DuplexPlugin) p;
 				pending.add(connect.submit(new ReadableTask(
 						new ConnectorTask(plugin, remotePayload.getCommitment(),
-								d, end))));
+								e.getValue(), end))));
+			}
 		}
 
 		// Get chosen connection
@@ -170,12 +177,12 @@ class KeyAgreementConnector {
 	private class ConnectorTask implements Callable<KeyAgreementConnection> {
 
 		private final byte[] commitment;
-		private final TransportDescriptor descriptor;
+		private final BdfList descriptor;
 		private final long end;
 		private final DuplexPlugin plugin;
 
 		private ConnectorTask(DuplexPlugin plugin, byte[] commitment,
-				TransportDescriptor descriptor, long end) {
+				BdfList descriptor, long end) {
 			this.plugin = plugin;
 			this.commitment = commitment;
 			this.descriptor = descriptor;
diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java
index 8a26f9405bd17128dc7cfde4ca89bb90859f13d0..32342993c1810479f8ca441615a7e9c611639884 100644
--- a/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java
+++ b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java
@@ -1,24 +1,30 @@
 package org.briarproject.keyagreement;
 
+import org.briarproject.api.TransportId;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.BdfWriter;
 import org.briarproject.api.data.BdfWriterFactory;
 import org.briarproject.api.keyagreement.Payload;
 import org.briarproject.api.keyagreement.PayloadEncoder;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.util.Map;
 
+import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
 
+@Immutable
+@NotNullByDefault
 class PayloadEncoderImpl implements PayloadEncoder {
 
 	private final BdfWriterFactory bdfWriterFactory;
 
 	@Inject
-	public PayloadEncoderImpl(BdfWriterFactory bdfWriterFactory) {
+	PayloadEncoderImpl(BdfWriterFactory bdfWriterFactory) {
 		this.bdfWriterFactory = bdfWriterFactory;
 	}
 
@@ -30,18 +36,13 @@ class PayloadEncoderImpl implements PayloadEncoder {
 			w.writeListStart(); // Payload start
 			w.writeLong(PROTOCOL_VERSION);
 			w.writeRaw(p.getCommitment());
-			w.writeListStart(); // Descriptors start
-			for (TransportDescriptor d : p.getTransportDescriptors()) {
-				w.writeListStart();
-				w.writeString(d.getIdentifier().getString());
-				w.writeDictionary(d.getProperties());
-				w.writeListEnd();
-			}
-			w.writeListEnd(); // Descriptors end
+			Map<TransportId, BdfList> descriptors = p.getTransportDescriptors();
+			for (BdfList descriptor : descriptors.values())
+				w.writeList(descriptor);
 			w.writeListEnd(); // Payload end
 		} catch (IOException e) {
 			// Shouldn't happen with ByteArrayOutputStream
-			throw new RuntimeException(e);
+			throw new AssertionError(e);
 		}
 		return out.toByteArray();
 	}
diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java
index d13f9ff73228af96b26a814a6d55e19a3e4d9a2b..105311811bd3268928083ec2bbb5f40d746dc0e4 100644
--- a/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java
+++ b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java
@@ -2,30 +2,34 @@ package org.briarproject.keyagreement;
 
 import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.data.BdfReader;
 import org.briarproject.api.data.BdfReaderFactory;
 import org.briarproject.api.keyagreement.Payload;
 import org.briarproject.api.keyagreement.PayloadParser;
-import org.briarproject.api.keyagreement.TransportDescriptor;
-import org.briarproject.api.properties.TransportProperties;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
 
+import javax.annotation.concurrent.Immutable;
 import javax.inject.Inject;
 
 import static org.briarproject.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
 import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION;
-import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN;
 
+@Immutable
+@NotNullByDefault
 class PayloadParserImpl implements PayloadParser {
 
 	private final BdfReaderFactory bdfReaderFactory;
 
 	@Inject
-	public PayloadParserImpl(BdfReaderFactory bdfReaderFactory) {
+	PayloadParserImpl(BdfReaderFactory bdfReaderFactory) {
 		this.bdfReaderFactory = bdfReaderFactory;
 	}
 
@@ -33,36 +37,28 @@ class PayloadParserImpl implements PayloadParser {
 	public Payload parse(byte[] raw) throws IOException {
 		ByteArrayInputStream in = new ByteArrayInputStream(raw);
 		BdfReader r = bdfReaderFactory.createReader(in);
-		r.readListStart(); // Payload start
-		int proto = (int) r.readLong();
-		if (proto != PROTOCOL_VERSION)
-			throw new FormatException();
-		byte[] commitment = r.readRaw(COMMIT_LENGTH);
-		if (commitment.length != COMMIT_LENGTH)
-			throw new FormatException();
-		List<TransportDescriptor> descriptors = new ArrayList<TransportDescriptor>();
-		r.readListStart(); // Descriptors start
-		while (r.hasList()) {
-			r.readListStart();
-			while (!r.hasListEnd()) {
-				TransportId id =
-						new TransportId(r.readString(MAX_PROPERTY_LENGTH));
-				TransportProperties p = new TransportProperties();
-				r.readDictionaryStart();
-				while (!r.hasDictionaryEnd()) {
-					String key = r.readString(MAX_PROPERTY_LENGTH);
-					String value = r.readString(MAX_PROPERTY_LENGTH);
-					p.put(key, value);
-				}
-				r.readDictionaryEnd();
-				descriptors.add(new TransportDescriptor(id, p));
+		// The payload is a BDF list with two or more elements
+		BdfList payload = r.readList();
+		if (payload.size() < 2) throw new FormatException();
+		if (!r.eof()) throw new FormatException();
+		// First element: the protocol version
+		long protocolVersion = payload.getLong(0);
+		if (protocolVersion != PROTOCOL_VERSION) throw new FormatException();
+		// Second element: the public key commitment
+		byte[] commitment = payload.getRaw(1);
+		if (commitment.length != COMMIT_LENGTH) throw new FormatException();
+		// Remaining elements: transport descriptors
+		Map<TransportId, BdfList> recognised =
+				new HashMap<TransportId, BdfList>();
+		for (int i = 2; i < payload.size(); i++) {
+			BdfList descriptor = payload.getList(i);
+			long transportId = descriptor.getLong(0);
+			if (transportId == TRANSPORT_ID_BLUETOOTH) {
+				recognised.put(new TransportId("bt"), descriptor);
+			} else if (transportId == TRANSPORT_ID_LAN) {
+				recognised.put(new TransportId("lan"), descriptor);
 			}
-			r.readListEnd();
 		}
-		r.readListEnd(); // Descriptors end
-		r.readListEnd(); // Payload end
-		if (!r.eof())
-			throw new FormatException();
-		return new Payload(commitment, descriptors);
+		return new Payload(commitment, recognised);
 	}
 }
diff --git a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
index eaefc8e5f2f54ee71f509ea9e504bfc0a294b870..f9aa0ec1796bcb1fe041063d1558b2c881a77259 100644
--- a/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
+++ b/briar-core/src/org/briarproject/plugins/PluginManagerImpl.java
@@ -9,6 +9,7 @@ import org.briarproject.api.event.TransportEnabledEvent;
 import org.briarproject.api.lifecycle.IoExecutor;
 import org.briarproject.api.lifecycle.Service;
 import org.briarproject.api.lifecycle.ServiceException;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.ConnectionManager;
 import org.briarproject.api.plugins.Plugin;
 import org.briarproject.api.plugins.PluginCallback;
@@ -42,11 +43,14 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
+import javax.annotation.concurrent.ThreadSafe;
 import javax.inject.Inject;
 
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
+@ThreadSafe
+@NotNullByDefault
 class PluginManagerImpl implements PluginManager, Service {
 
 	private static final Logger LOG =
diff --git a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
index 9819235539b04318dcef32152617f4e32f3b7254..108db3c7b25ef17715d87ab4357701e275cc1c73 100644
--- a/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
+++ b/briar-core/src/org/briarproject/plugins/file/FilePlugin.java
@@ -1,6 +1,7 @@
 package org.briarproject.plugins.file;
 
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.TransportConnectionReader;
 import org.briarproject.api.plugins.TransportConnectionWriter;
 import org.briarproject.api.plugins.simplex.SimplexPlugin;
@@ -17,10 +18,13 @@ import java.util.concurrent.Executor;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
+import javax.annotation.Nullable;
+
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.api.transport.TransportConstants.MIN_STREAM_LENGTH;
 
-public abstract class FilePlugin implements SimplexPlugin {
+@NotNullByDefault
+abstract class FilePlugin implements SimplexPlugin {
 
 	private static final Logger LOG =
 			Logger.getLogger(FilePlugin.class.getName());
@@ -32,6 +36,7 @@ public abstract class FilePlugin implements SimplexPlugin {
 
 	protected volatile boolean running = false;
 
+	@Nullable
 	protected abstract File chooseOutputDirectory();
 	protected abstract Collection<File> findFilesByName(String filename);
 	protected abstract void writerFinished(File f);
@@ -82,6 +87,7 @@ public abstract class FilePlugin implements SimplexPlugin {
 		return filename.toLowerCase(Locale.US).matches("[a-z]{8}\\.dat");
 	}
 
+	@Nullable
 	private TransportConnectionWriter createWriter(String filename) {
 		if (!running) return null;
 		File dir = chooseOutputDirectory();
diff --git a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
index 87ba78fab88d5a8c40b11303b395ed83860bd763..e3527f74dd52129501dbc42b183677c5b91a53d5 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java
@@ -1,10 +1,12 @@
 package org.briarproject.plugins.tcp;
 
+import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementConnection;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 import org.briarproject.api.plugins.duplex.DuplexTransportConnection;
@@ -19,6 +21,7 @@ import java.net.InetSocketAddress;
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.net.SocketAddress;
+import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedList;
@@ -29,8 +32,11 @@ import java.util.logging.Logger;
 
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN;
+import static org.briarproject.util.ByteUtils.MAX_16_BIT_UNSIGNED;
 import static org.briarproject.util.PrivacyUtils.scrubSocketAddress;
 
+@NotNullByDefault
 class LanTcpPlugin extends TcpPlugin {
 
 	static final TransportId ID = new TransportId("lan");
@@ -40,7 +46,6 @@ class LanTcpPlugin extends TcpPlugin {
 
 	private static final int MAX_ADDRESSES = 5;
 	private static final String PROP_IP_PORTS = "ipPorts";
-	private static final String PROP_IP_PORT = "ipPort";
 	private static final String SEPARATOR = ",";
 
 	LanTcpPlugin(Executor ioExecutor, Backoff backoff,
@@ -186,23 +191,26 @@ class LanTcpPlugin extends TcpPlugin {
 			LOG.info("Could not bind server socket for key agreement");
 			return null;
 		}
-		TransportProperties p = new TransportProperties();
-		SocketAddress local = ss.getLocalSocketAddress();
-		p.put(PROP_IP_PORT, getIpPortString((InetSocketAddress) local));
-		TransportDescriptor d = new TransportDescriptor(ID, p);
-		return new LanKeyAgreementListener(d, ss);
+		BdfList descriptor = new BdfList();
+		descriptor.add(TRANSPORT_ID_LAN);
+		InetSocketAddress local =
+				(InetSocketAddress) ss.getLocalSocketAddress();
+		descriptor.add(local.getAddress().getAddress());
+		descriptor.add(local.getPort());
+		return new LanKeyAgreementListener(descriptor, ss);
 	}
 
 	@Override
 	public DuplexTransportConnection createKeyAgreementConnection(
-			byte[] commitment, TransportDescriptor d, long timeout) {
+			byte[] commitment, BdfList descriptor, long timeout) {
 		if (!isRunning()) return null;
-		if (!ID.equals(d.getIdentifier())) return null;
-		TransportProperties p = d.getProperties();
-		if (p == null) return null;
-		String ipPort = p.get(PROP_IP_PORT);
-		InetSocketAddress remote = parseSocketAddress(ipPort);
-		if (remote == null) return null;
+		InetSocketAddress remote;
+		try {
+			remote = parseSocketAddress(descriptor);
+		} catch (FormatException e) {
+			LOG.info("Invalid IP/port in key agreement descriptor");
+			return null;
+		}
 		if (!isConnectable(remote)) {
 			if (LOG.isLoggable(INFO)) {
 				SocketAddress local = socket.getLocalSocketAddress();
@@ -228,11 +236,25 @@ class LanTcpPlugin extends TcpPlugin {
 		}
 	}
 
+	private InetSocketAddress parseSocketAddress(BdfList descriptor)
+			throws FormatException {
+		byte[] address = descriptor.getRaw(1);
+		int port = descriptor.getLong(2).intValue();
+		if (port < 1 || port > MAX_16_BIT_UNSIGNED) throw new FormatException();
+		try {
+			InetAddress addr = InetAddress.getByAddress(address);
+			return new InetSocketAddress(addr, port);
+		} catch (UnknownHostException e) {
+			// Invalid address length
+			throw new FormatException();
+		}
+	}
+
 	private class LanKeyAgreementListener extends KeyAgreementListener {
 
 		private final ServerSocket ss;
 
-		public LanKeyAgreementListener(TransportDescriptor descriptor,
+		private LanKeyAgreementListener(BdfList descriptor,
 				ServerSocket ss) {
 			super(descriptor);
 			this.ss = ss;
diff --git a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
index 9ba84875bd40da38467b0899af4d566d6c26df2b..76da5bf7dd8899d0fdf0da01cf8ae35180c49939 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java
@@ -2,8 +2,10 @@ package org.briarproject.plugins.tcp;
 
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -28,10 +30,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 import java.util.regex.Pattern;
 
+import javax.annotation.Nullable;
+
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static org.briarproject.util.PrivacyUtils.scrubSocketAddress;
 
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
 abstract class TcpPlugin implements DuplexPlugin {
 
 	private static final Pattern DOTTED_QUAD =
@@ -141,7 +147,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 		});
 	}
 
-	protected void tryToClose(ServerSocket ss) {
+	protected void tryToClose(@Nullable ServerSocket ss) {
 		try {
 			if (ss != null) ss.close();
 		} catch (IOException e) {
@@ -252,6 +258,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 		return null;
 	}
 
+	@Nullable
 	protected InetSocketAddress parseSocketAddress(String ipPort) {
 		if (StringUtils.isNullOrEmpty(ipPort)) return null;
 		String[] split = ipPort.split(":");
@@ -298,7 +305,7 @@ abstract class TcpPlugin implements DuplexPlugin {
 
 	@Override
 	public DuplexTransportConnection createKeyAgreementConnection(
-			byte[] commitment, TransportDescriptor d, long timeout) {
+			byte[] commitment, BdfList descriptor, long timeout) {
 		throw new UnsupportedOperationException();
 	}
 
diff --git a/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
index 82c3221a5b65cf99311a255321dbf87b16cd173d..3f297d14825d9f5120bd15461f1a310ee86494b0 100644
--- a/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
+++ b/briar-core/src/org/briarproject/plugins/tcp/WanTcpPlugin.java
@@ -2,6 +2,8 @@ package org.briarproject.plugins.tcp;
 
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
 import org.briarproject.api.properties.TransportProperties;
@@ -14,6 +16,8 @@ import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.Executor;
 
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
 class WanTcpPlugin extends TcpPlugin {
 
 	static final TransportId ID = new TransportId("wan");
diff --git a/briar-core/src/org/briarproject/util/StringUtils.java b/briar-core/src/org/briarproject/util/StringUtils.java
index 5acedc34e419be205764b27537e55812732ac613..af622cc270130458cf276941667b011ca55db5d6 100644
--- a/briar-core/src/org/briarproject/util/StringUtils.java
+++ b/briar-core/src/org/briarproject/util/StringUtils.java
@@ -1,24 +1,34 @@
 package org.briarproject.util;
 
+import org.briarproject.api.nullsafety.NotNullByDefault;
+
 import java.io.UnsupportedEncodingException;
 import java.nio.ByteBuffer;
 import java.nio.charset.CharacterCodingException;
 import java.nio.charset.Charset;
 import java.nio.charset.CharsetDecoder;
 import java.util.Collection;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
 
 import static java.nio.charset.CodingErrorAction.IGNORE;
+import static java.util.regex.Pattern.CASE_INSENSITIVE;
 
+@NotNullByDefault
 public class StringUtils {
 
 	private static final Charset UTF_8 = Charset.forName("UTF-8");
+	private static Pattern MAC = Pattern.compile("[0-9a-f]{2}:[0-9a-f]{2}:" +
+					"[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}",
+			CASE_INSENSITIVE);
 
 	private static final char[] HEX = new char[] {
-		'0', '1', '2', '3', '4', '5', '6', '7',
-		'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
+			'0', '1', '2', '3', '4', '5', '6', '7',
+			'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'
 	};
 
-	public static boolean isNullOrEmpty(String s) {
+	public static boolean isNullOrEmpty(@Nullable String s) {
 		return s == null || s.length() == 0;
 	}
 
@@ -61,7 +71,9 @@ public class StringUtils {
 		return fromUtf8(utf8, 0, maxUtf8Length);
 	}
 
-	/** Converts the given byte array to a hex character array. */
+	/**
+	 * Converts the given byte array to a hex character array.
+	 */
 	private static char[] toHexChars(byte[] bytes) {
 		char[] hex = new char[bytes.length * 2];
 		for (int i = 0, j = 0; i < bytes.length; i++) {
@@ -71,12 +83,16 @@ public class StringUtils {
 		return hex;
 	}
 
-	/** Converts the given byte array to a hex string. */
+	/**
+	 * Converts the given byte array to a hex string.
+	 */
 	public static String toHexString(byte[] bytes) {
 		return new String(toHexChars(bytes));
 	}
 
-	/** Converts the given hex string to a byte array. */
+	/**
+	 * Converts the given hex string to a byte array.
+	 */
 	public static byte[] fromHexString(String hex) {
 		int len = hex.length();
 		if (len % 2 != 0)
@@ -107,4 +123,20 @@ public class StringUtils {
 	public static boolean utf8IsTooLong(String s, int maxLength) {
 		return toUtf8(s).length > maxLength;
 	}
+
+	public static byte[] macToBytes(String mac) {
+		if (!MAC.matcher(mac).matches()) throw new IllegalArgumentException();
+		return fromHexString(mac.replaceAll(":", ""));
+	}
+
+	public static String macToString(byte[] mac) {
+		if (mac.length != 6) throw new IllegalArgumentException();
+		StringBuilder s = new StringBuilder();
+		for (byte b : mac) {
+			if (s.length() > 0) s.append(':');
+			s.append(HEX[(b >> 4) & 0xF]);
+			s.append(HEX[b & 0xF]);
+		}
+		return s.toString();
+	}
 }
diff --git a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
index f7b639bba93d5df223d729277ba8bdc39a7c9214..c945ca8154803711a0cddf181c3ca5dd66a80403 100644
--- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java
@@ -1,11 +1,14 @@
 package org.briarproject.plugins.bluetooth;
 
+import org.briarproject.api.FormatException;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementConnection;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -33,6 +36,7 @@ import java.util.concurrent.Semaphore;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.logging.Logger;
 
+import javax.annotation.Nullable;
 import javax.bluetooth.BluetoothStateException;
 import javax.bluetooth.DiscoveryAgent;
 import javax.bluetooth.LocalDevice;
@@ -44,7 +48,10 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 import static javax.bluetooth.DiscoveryAgent.GIAC;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_BLUETOOTH;
 
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
 class BluetoothPlugin implements DuplexPlugin {
 
 	// Share an ID with the Android Bluetooth plugin
@@ -161,7 +168,7 @@ class BluetoothPlugin implements DuplexPlugin {
 		return uuid;
 	}
 
-	private void tryToClose(StreamConnectionNotifier ss) {
+	private void tryToClose(@Nullable StreamConnectionNotifier ss) {
 		try {
 			if (ss != null) ss.close();
 		} catch (IOException e) {
@@ -330,7 +337,7 @@ class BluetoothPlugin implements DuplexPlugin {
 	}
 
 	private void closeSockets(final List<Future<StreamConnection>> futures,
-			final StreamConnection chosen) {
+			@Nullable final StreamConnection chosen) {
 		ioExecutor.execute(new Runnable() {
 			@Override
 			public void run() {
@@ -382,21 +389,24 @@ class BluetoothPlugin implements DuplexPlugin {
 			tryToClose(ss);
 			return null;
 		}
-		TransportProperties p = new TransportProperties();
-		p.put(PROP_ADDRESS, localDevice.getBluetoothAddress());
-		TransportDescriptor d = new TransportDescriptor(ID, p);
-		return new BluetoothKeyAgreementListener(d, ss);
+		BdfList descriptor = new BdfList();
+		descriptor.add(TRANSPORT_ID_BLUETOOTH);
+		String address = localDevice.getBluetoothAddress();
+		descriptor.add(StringUtils.macToBytes(address));
+		return new BluetoothKeyAgreementListener(descriptor, ss);
 	}
 
 	@Override
 	public DuplexTransportConnection createKeyAgreementConnection(
-			byte[] commitment, TransportDescriptor d, long timeout) {
+			byte[] commitment, BdfList descriptor, long timeout) {
 		if (!isRunning()) return null;
-		if (!ID.equals(d.getIdentifier())) return null;
-		TransportProperties p = d.getProperties();
-		if (p == null) return null;
-		String address = p.get(PROP_ADDRESS);
-		if (StringUtils.isNullOrEmpty(address)) return null;
+		String address;
+		try {
+			address = parseAddress(descriptor);
+		} catch (FormatException e) {
+			LOG.info("Invalid address in key agreement descriptor");
+			return null;
+		}
 		// No truncation necessary because COMMIT_LENGTH = 16
 		String uuid = UUID.nameUUIDFromBytes(commitment).toString();
 		if (LOG.isLoggable(INFO))
@@ -407,6 +417,12 @@ class BluetoothPlugin implements DuplexPlugin {
 		return new BluetoothTransportConnection(this, s);
 	}
 
+	private String parseAddress(BdfList descriptor) throws FormatException {
+		byte[] mac = descriptor.getRaw(1);
+		if (mac.length != 6) throw new FormatException();
+		return StringUtils.macToString(mac);
+	}
+
 	private void makeDeviceDiscoverable() {
 		// Try to make the device discoverable (requires root on Linux)
 		try {
@@ -491,7 +507,7 @@ class BluetoothPlugin implements DuplexPlugin {
 
 		private final StreamConnectionNotifier ss;
 
-		BluetoothKeyAgreementListener(TransportDescriptor descriptor,
+		BluetoothKeyAgreementListener(BdfList descriptor,
 				StreamConnectionNotifier ss) {
 			super(descriptor);
 			this.ss = ss;
diff --git a/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java b/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java
index 926f8d314800774f623b08688439bcc4dca36ca2..4a463eb3eb02c5a49b12f20fdc7a56e4e943e2e8 100644
--- a/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/file/RemovableDrivePlugin.java
@@ -2,6 +2,7 @@ package org.briarproject.plugins.file;
 
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.nullsafety.NotNullByDefault;
 import org.briarproject.api.plugins.simplex.SimplexPluginCallback;
 
 import java.io.File;
@@ -15,6 +16,7 @@ import java.util.logging.Logger;
 
 import static java.util.logging.Level.WARNING;
 
+@NotNullByDefault
 class RemovableDrivePlugin extends FilePlugin
 implements RemovableDriveMonitor.Callback {
 
diff --git a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
index ce2d2d57e7c0cf5ed7e4c19c5de94b8aa6344f43..fe3b89e2c32f99139694d4e7fe519366d796f342 100644
--- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
+++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java
@@ -3,8 +3,10 @@ package org.briarproject.plugins.modem;
 import org.briarproject.api.TransportId;
 import org.briarproject.api.contact.ContactId;
 import org.briarproject.api.crypto.PseudoRandom;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
+import org.briarproject.api.nullsafety.MethodsNotNullByDefault;
+import org.briarproject.api.nullsafety.ParametersNotNullByDefault;
 import org.briarproject.api.plugins.duplex.AbstractDuplexTransportConnection;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -22,6 +24,8 @@ import java.util.logging.Logger;
 import static java.util.logging.Level.INFO;
 import static java.util.logging.Level.WARNING;
 
+@MethodsNotNullByDefault
+@ParametersNotNullByDefault
 class ModemPlugin implements DuplexPlugin, Modem.Callback {
 
 	static final TransportId ID = new TransportId("modem");
@@ -114,7 +118,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 		throw new UnsupportedOperationException();
 	}
 
-	boolean resetModem() {
+	private boolean resetModem() {
 		if (!running) return false;
 		for (String portName : serialPortList.getPortNames()) {
 			if (LOG.isLoggable(INFO))
@@ -184,7 +188,7 @@ class ModemPlugin implements DuplexPlugin, Modem.Callback {
 
 	@Override
 	public DuplexTransportConnection createKeyAgreementConnection(
-			byte[] commitment, TransportDescriptor d, long timeout) {
+			byte[] commitment, BdfList descriptor, long timeout) {
 		throw new UnsupportedOperationException();
 	}
 
diff --git a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java
index 622bb59aa0d7e6dec97374ad96eae4912219414f..2f8e349c4caeae61580035583e058b6d761bae0a 100644
--- a/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java
+++ b/briar-tests/src/org/briarproject/plugins/tcp/LanTcpPluginTest.java
@@ -2,9 +2,9 @@ package org.briarproject.plugins.tcp;
 
 import org.briarproject.BriarTestCase;
 import org.briarproject.api.contact.ContactId;
+import org.briarproject.api.data.BdfList;
 import org.briarproject.api.keyagreement.KeyAgreementConnection;
 import org.briarproject.api.keyagreement.KeyAgreementListener;
-import org.briarproject.api.keyagreement.TransportDescriptor;
 import org.briarproject.api.plugins.Backoff;
 import org.briarproject.api.plugins.duplex.DuplexPlugin;
 import org.briarproject.api.plugins.duplex.DuplexPluginCallback;
@@ -27,11 +27,12 @@ import java.util.concurrent.Callable;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.COMMIT_LENGTH;
+import static org.briarproject.api.keyagreement.KeyAgreementConstants.TRANSPORT_ID_LAN;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
@@ -188,23 +189,22 @@ public class LanTcpPluginTest extends BriarTestCase {
 				0, 0);
 		plugin.start();
 		assertTrue(callback.propertiesLatch.await(5, SECONDS));
-		KeyAgreementListener kal = plugin.createKeyAgreementListener(null);
+		KeyAgreementListener kal =
+				plugin.createKeyAgreementListener(new byte[COMMIT_LENGTH]);
+		assertNotNull(kal);
 		Callable<KeyAgreementConnection> c = kal.listen();
 		FutureTask<KeyAgreementConnection> f = new FutureTask<>(c);
 		new Thread(f).start();
 		// The plugin should have bound a socket and stored the port number
-		TransportDescriptor d = kal.getDescriptor();
-		TransportProperties p = d.getProperties();
-		String ipPort = p.get("ipPort");
-		assertNotNull(ipPort);
-		String[] split = ipPort.split(":");
-		assertEquals(2, split.length);
-		String addrString = split[0], portString = split[1];
-		InetAddress addr = InetAddress.getByName(addrString);
+		BdfList descriptor = kal.getDescriptor();
+		assertEquals(3, descriptor.size());
+		assertEquals(TRANSPORT_ID_LAN, descriptor.getLong(0).longValue());
+		byte[] address = descriptor.getRaw(1);
+		InetAddress addr = InetAddress.getByAddress(address);
 		assertTrue(addr instanceof Inet4Address);
 		assertFalse(addr.isLoopbackAddress());
 		assertTrue(addr.isLinkLocalAddress() || addr.isSiteLocalAddress());
-		int port = Integer.parseInt(portString);
+		int port = descriptor.getLong(2).intValue();
 		assertTrue(port > 0 && port < 65536);
 		// The plugin should be listening on the port
 		InetSocketAddress socketAddr = new InetSocketAddress(addr, port);
@@ -240,7 +240,6 @@ public class LanTcpPluginTest extends BriarTestCase {
 		// Listen on the same interface as the plugin
 		final ServerSocket ss = new ServerSocket();
 		ss.bind(new InetSocketAddress(addrString, 0), 10);
-		int port = ss.getLocalPort();
 		final CountDownLatch latch = new CountDownLatch(1);
 		final AtomicBoolean error = new AtomicBoolean(false);
 		new Thread() {
@@ -255,12 +254,15 @@ public class LanTcpPluginTest extends BriarTestCase {
 			}
 		}.start();
 		// Tell the plugin about the port
-		TransportProperties p = new TransportProperties();
-		p.put("ipPort", addrString + ":" + port);
-		TransportDescriptor desc = new TransportDescriptor(plugin.getId(), p);
+		BdfList descriptor = new BdfList();
+		descriptor.add(TRANSPORT_ID_LAN);
+		InetSocketAddress local =
+				(InetSocketAddress) ss.getLocalSocketAddress();
+		descriptor.add(local.getAddress().getAddress());
+		descriptor.add(local.getPort());
 		// Connect to the port
-		DuplexTransportConnection d =
-				plugin.createKeyAgreementConnection(null, desc, 5000);
+		DuplexTransportConnection d = plugin.createKeyAgreementConnection(
+				new byte[COMMIT_LENGTH], descriptor, 5000);
 		assertNotNull(d);
 		// Check that the connection was accepted
 		assertTrue(latch.await(5, SECONDS));
@@ -291,61 +293,76 @@ public class LanTcpPluginTest extends BriarTestCase {
 		private final CountDownLatch connectionsLatch = new CountDownLatch(1);
 		private final TransportProperties local = new TransportProperties();
 
+		@Override
 		public Settings getSettings() {
 			return new Settings();
 		}
 
+		@Override
 		public TransportProperties getLocalProperties() {
 			return local;
 		}
 
+		@Override
 		public Map<ContactId, TransportProperties> getRemoteProperties() {
 			return remote;
 		}
 
+		@Override
 		public void mergeSettings(Settings s) {
 		}
 
+		@Override
 		public void mergeLocalProperties(TransportProperties p) {
 			local.putAll(p);
 			propertiesLatch.countDown();
 		}
 
+		@Override
 		public int showChoice(String[] options, String... message) {
 			return -1;
 		}
 
+		@Override
 		public boolean showConfirmationMessage(String... message) {
 			return false;
 		}
 
+		@Override
 		public void showMessage(String... message) {
 		}
 
+		@Override
 		public void incomingConnectionCreated(DuplexTransportConnection d) {
 			connectionsLatch.countDown();
 		}
 
+		@Override
 		public void outgoingConnectionCreated(ContactId c,
 				DuplexTransportConnection d) {
 		}
 
+		@Override
 		public void transportEnabled() {
 		}
 
+		@Override
 		public void transportDisabled() {
 		}
 	}
 
 	private static class TestBackoff implements Backoff {
 
+		@Override
 		public int getPollingInterval() {
 			return 60 * 1000;
 		}
 
+		@Override
 		public void increment() {
 		}
 
+		@Override
 		public void reset() {
 		}
 	}
diff --git a/briar-tests/src/org/briarproject/util/StringUtilsTest.java b/briar-tests/src/org/briarproject/util/StringUtilsTest.java
index 76f830df3df83c2e4ba22fbb016a173118bb7056..92aeaf54c7331046128a7e26f784d4a610defd21 100644
--- a/briar-tests/src/org/briarproject/util/StringUtilsTest.java
+++ b/briar-tests/src/org/briarproject/util/StringUtilsTest.java
@@ -173,4 +173,55 @@ public class StringUtilsTest extends BriarTestCase {
 	public void testTruncateUtf8EmptyInput() {
 		assertEquals("", StringUtils.truncateUtf8("", 123));
 	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMacToBytesRejectsShortMac() {
+		StringUtils.macToBytes("00:00:00:00:00");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMacToBytesRejectsLongMac() {
+		StringUtils.macToBytes("00:00:00:00:00:00:00");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMacToBytesRejectsInvalidCharacter() {
+		StringUtils.macToBytes("00:00:00:00:00:0g");
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMacToBytesRejectsInvalidFormat() {
+		StringUtils.macToBytes("0:000:00:00:00:00");
+	}
+
+	@Test
+	public void testMacToBytesUpperCase() {
+		byte[] expected = new byte[] {0x0A, 0x1B, 0x2C, 0x3D, 0x4E, 0x5F};
+		String mac = "0A:1B:2C:3D:4E:5F";
+		assertArrayEquals(expected, StringUtils.macToBytes(mac));
+	}
+
+	@Test
+	public void testMacToBytesLowerCase() {
+		byte[] expected = new byte[] {0x0A, 0x1B, 0x2C, 0x3D, 0x4E, 0x5F};
+		String mac = "0a:1b:2c:3d:4e:5f";
+		assertArrayEquals(expected, StringUtils.macToBytes(mac));
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMacToStringRejectsShortMac() {
+		StringUtils.macToString(new byte[5]);
+	}
+
+	@Test(expected = IllegalArgumentException.class)
+	public void testMacToStringRejectsLongMac() {
+		StringUtils.macToString(new byte[7]);
+	}
+
+	@Test
+	public void testMacToString() {
+		byte[] mac = new byte[] {0x0a, 0x1b, 0x2c, 0x3d, 0x4e, 0x5f};
+		String expected = "0A:1B:2C:3D:4E:5F";
+		assertEquals(expected, StringUtils.macToString(mac));
+	}
 }