From 04d4ecad05dc7625e13a8e562d15f21945d5d92c Mon Sep 17 00:00:00 2001 From: akwizgran <akwizgran@users.sourceforge.net> Date: Mon, 7 Nov 2016 16:25:30 +0000 Subject: [PATCH] Encode transport properties more compactly in QR codes. --- .../plugins/droidtooth/DroidtoothPlugin.java | 40 +++++++---- .../briarproject/plugins/tor/TorPlugin.java | 4 +- .../keyagreement/KeyAgreementConstants.java | 8 ++- .../keyagreement/KeyAgreementListener.java | 8 ++- .../api/keyagreement/Payload.java | 23 +++++-- .../api/keyagreement/PayloadParser.java | 3 + .../api/keyagreement/TransportDescriptor.java | 28 -------- .../api/plugins/PluginManager.java | 5 ++ .../api/plugins/duplex/DuplexPlugin.java | 4 +- .../keyagreement/KeyAgreementConnector.java | 35 ++++++---- .../keyagreement/PayloadEncoderImpl.java | 23 +++---- .../keyagreement/PayloadParserImpl.java | 66 +++++++++---------- .../plugins/PluginManagerImpl.java | 4 ++ .../plugins/tcp/LanTcpPlugin.java | 53 +++++++++++---- .../briarproject/plugins/tcp/TcpPlugin.java | 4 +- .../org/briarproject/util/StringUtils.java | 44 +++++++++++-- .../plugins/bluetooth/BluetoothPlugin.java | 39 +++++++---- .../plugins/modem/ModemPlugin.java | 6 +- .../plugins/tcp/LanTcpPluginTest.java | 46 ++++++++----- .../briarproject/util/StringUtilsTest.java | 51 ++++++++++++++ 20 files changed, 326 insertions(+), 168 deletions(-) delete mode 100644 briar-api/src/org/briarproject/api/keyagreement/TransportDescriptor.java diff --git a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java index 686d15b615..c5841d76e6 100644 --- a/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java +++ b/briar-android/src/org/briarproject/plugins/droidtooth/DroidtoothPlugin.java @@ -11,12 +11,13 @@ 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.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -44,6 +45,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,6 +60,7 @@ 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; class DroidtoothPlugin implements DuplexPlugin { @@ -466,23 +470,25 @@ class DroidtoothPlugin implements DuplexPlugin { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); return null; } - TransportProperties p = new TransportProperties(); + BdfList descriptor = new BdfList(); + descriptor.add(TRANSPORT_ID_BLUETOOTH); 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); + if (!address.isEmpty()) 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; + } + if (address == null) return null; // No truncation necessary because COMMIT_LENGTH = 16 UUID uuid = UUID.nameUUIDFromBytes(commitment); if (LOG.isLoggable(INFO)) @@ -492,6 +498,14 @@ class DroidtoothPlugin implements DuplexPlugin { return new DroidtoothTransportConnection(this, s); } + @Nullable + private String parseAddress(BdfList descriptor) throws FormatException { + if (descriptor.size() < 2) return null; + byte[] mac = descriptor.getRaw(1); + if (mac.length != 6) throw new FormatException(); + return StringUtils.macToString(mac); + } + private class BluetoothStateReceiver extends BroadcastReceiver { @Override @@ -626,7 +640,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/tor/TorPlugin.java b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java index e66e0a6583..ee62791d2e 100644 --- a/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java +++ b/briar-android/src/org/briarproject/plugins/tor/TorPlugin.java @@ -19,11 +19,11 @@ 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.plugins.Backoff; import org.briarproject.api.plugins.TorConstants; import org.briarproject.api.plugins.duplex.DuplexPlugin; @@ -574,7 +574,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 f239111269..9961a2b994 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 05163614cc..41b18a7de3 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 0c749da53e..60cbb45a36 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 0df9c653d4..0ada113f36 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 cdaa5a5794..0000000000 --- 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/PluginManager.java b/briar-api/src/org/briarproject/api/plugins/PluginManager.java index 18e71c2ec2..3f1fcf31a2 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 ff7db3b51d..a77bd090ea 100644 --- a/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java +++ b/briar-api/src/org/briarproject/api/plugins/duplex/DuplexPlugin.java @@ -2,8 +2,8 @@ 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.plugins.Plugin; /** An interface for transport plugins that support duplex communication. */ @@ -40,5 +40,5 @@ public interface DuplexPlugin extends Plugin { * Returns null if no connection can be established within the given time. */ DuplexTransportConnection createKeyAgreementConnection( - byte[] remoteCommitment, TransportDescriptor d, long timeout); + byte[] remoteCommitment, BdfList descriptor, long timeout); } diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java index 190a138502..44636b31d6 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 8a26f9405b..32342993c1 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 d13f9ff732..105311811b 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 eaefc8e5f2..f9aa0ec179 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/tcp/LanTcpPlugin.java b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java index 87ba78fab8..01928d3737 100644 --- a/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java +++ b/briar-core/src/org/briarproject/plugins/tcp/LanTcpPlugin.java @@ -1,10 +1,11 @@ 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.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; import org.briarproject.api.plugins.duplex.DuplexTransportConnection; @@ -19,6 +20,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; @@ -27,8 +29,12 @@ import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.logging.Logger; +import javax.annotation.Nullable; + 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; class LanTcpPlugin extends TcpPlugin { @@ -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,22 +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); + InetSocketAddress remote; + try { + remote = parseSocketAddress(descriptor); + } catch (FormatException e) { + LOG.info("Invalid IP/port in key agreement descriptor"); + return null; + } if (remote == null) return null; if (!isConnectable(remote)) { if (LOG.isLoggable(INFO)) { @@ -228,11 +237,27 @@ class LanTcpPlugin extends TcpPlugin { } } + @Nullable + private InetSocketAddress parseSocketAddress(BdfList descriptor) + throws FormatException { + if (descriptor.size() < 3) return null; + 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 9ba84875bd..7227e1a0f4 100644 --- a/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java +++ b/briar-core/src/org/briarproject/plugins/tcp/TcpPlugin.java @@ -2,8 +2,8 @@ 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.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -298,7 +298,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/util/StringUtils.java b/briar-core/src/org/briarproject/util/StringUtils.java index 5acedc34e4..af622cc270 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 f7b639bba9..c6b607d32b 100644 --- a/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java +++ b/briar-desktop/src/org/briarproject/plugins/bluetooth/BluetoothPlugin.java @@ -1,11 +1,12 @@ 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.plugins.Backoff; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -33,6 +34,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,6 +46,7 @@ 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; class BluetoothPlugin implements DuplexPlugin { @@ -382,21 +385,25 @@ 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(); + if (!address.isEmpty()) 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; + } + if (address == null) return null; // No truncation necessary because COMMIT_LENGTH = 16 String uuid = UUID.nameUUIDFromBytes(commitment).toString(); if (LOG.isLoggable(INFO)) @@ -407,6 +414,14 @@ class BluetoothPlugin implements DuplexPlugin { return new BluetoothTransportConnection(this, s); } + @Nullable + private String parseAddress(BdfList descriptor) throws FormatException { + if (descriptor.size() < 2) return null; + 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 +506,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/modem/ModemPlugin.java b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java index ce2d2d57e7..4a32bf4618 100644 --- a/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java +++ b/briar-desktop/src/org/briarproject/plugins/modem/ModemPlugin.java @@ -3,8 +3,8 @@ 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.plugins.duplex.AbstractDuplexTransportConnection; import org.briarproject.api.plugins.duplex.DuplexPlugin; import org.briarproject.api.plugins.duplex.DuplexPluginCallback; @@ -114,7 +114,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 +184,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 622bb59aa0..33f8d3a908 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,11 @@ 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.TRANSPORT_ID_LAN; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -193,18 +193,15 @@ public class LanTcpPluginTest extends BriarTestCase { 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 +237,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 +251,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); + plugin.createKeyAgreementConnection(null, descriptor, 5000); assertNotNull(d); // Check that the connection was accepted assertTrue(latch.await(5, SECONDS)); @@ -291,61 +290,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 76f830df3d..92aeaf54c7 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)); + } } -- GitLab