Symmetric invitation protocol for the Droidtooth plugin.

This commit is contained in:
akwizgran
2013-06-06 15:17:35 +01:00
parent 4170e8a08b
commit 4431e502f7
2 changed files with 167 additions and 85 deletions

View File

@@ -10,10 +10,11 @@ import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING; import static java.util.logging.Level.WARNING;
import java.io.IOException; import java.io.IOException;
import java.net.SocketTimeoutException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.UUID; import java.util.UUID;
@@ -21,12 +22,15 @@ import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Logger; import java.util.logging.Logger;
import net.sf.briar.api.ContactId; import net.sf.briar.api.ContactId;
import net.sf.briar.api.TransportId; import net.sf.briar.api.TransportId;
import net.sf.briar.api.TransportProperties; import net.sf.briar.api.TransportProperties;
import net.sf.briar.api.android.AndroidExecutor; import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.clock.Clock;
import net.sf.briar.api.crypto.PseudoRandom; import net.sf.briar.api.crypto.PseudoRandom;
import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
@@ -61,6 +65,7 @@ class DroidtoothPlugin implements DuplexPlugin {
private final AndroidExecutor androidExecutor; private final AndroidExecutor androidExecutor;
private final Context appContext; private final Context appContext;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final Clock clock;
private final DuplexPluginCallback callback; private final DuplexPluginCallback callback;
private final int maxFrameLength; private final int maxFrameLength;
private final long maxLatency, pollingInterval; private final long maxLatency, pollingInterval;
@@ -72,13 +77,14 @@ class DroidtoothPlugin implements DuplexPlugin {
private volatile BluetoothAdapter adapter = null; private volatile BluetoothAdapter adapter = null;
DroidtoothPlugin(Executor pluginExecutor, AndroidExecutor androidExecutor, DroidtoothPlugin(Executor pluginExecutor, AndroidExecutor androidExecutor,
Context appContext, SecureRandom secureRandom, Context appContext, SecureRandom secureRandom, Clock clock,
DuplexPluginCallback callback, int maxFrameLength, long maxLatency, DuplexPluginCallback callback, int maxFrameLength, long maxLatency,
long pollingInterval) { long pollingInterval) {
this.pluginExecutor = pluginExecutor; this.pluginExecutor = pluginExecutor;
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.appContext = appContext; this.appContext = appContext;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
this.clock = clock;
this.callback = callback; this.callback = callback;
this.maxFrameLength = maxFrameLength; this.maxFrameLength = maxFrameLength;
this.maxLatency = maxLatency; this.maxLatency = maxLatency;
@@ -271,15 +277,18 @@ class DroidtoothPlugin implements DuplexPlugin {
pluginExecutor.execute(new Runnable() { pluginExecutor.execute(new Runnable() {
public void run() { public void run() {
if(!running) return; if(!running) return;
DuplexTransportConnection conn = connect(address, uuid); BluetoothSocket s = connect(address, uuid);
if(conn != null) if(s != null) {
callback.outgoingConnectionCreated(c, conn); callback.outgoingConnectionCreated(c,
new DroidtoothTransportConnection(
DroidtoothPlugin.this, s));
}
} }
}); });
} }
} }
private DuplexTransportConnection connect(String address, String uuid) { private BluetoothSocket connect(String address, String uuid) {
// Validate the address // Validate the address
if(!BluetoothAdapter.checkBluetoothAddress(address)) { if(!BluetoothAdapter.checkBluetoothAddress(address)) {
if(LOG.isLoggable(WARNING)) if(LOG.isLoggable(WARNING))
@@ -302,7 +311,7 @@ class DroidtoothPlugin implements DuplexPlugin {
if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + address); if(LOG.isLoggable(INFO)) LOG.info("Connecting to " + address);
s.connect(); s.connect();
if(LOG.isLoggable(INFO)) LOG.info("Connected to " + address); if(LOG.isLoggable(INFO)) LOG.info("Connected to " + address);
return new DroidtoothTransportConnection(this, s); return s;
} catch(IOException e) { } catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
tryToClose(s); tryToClose(s);
@@ -326,7 +335,9 @@ class DroidtoothPlugin implements DuplexPlugin {
if(StringUtils.isNullOrEmpty(address)) return null; if(StringUtils.isNullOrEmpty(address)) return null;
String uuid = p.get("uuid"); String uuid = p.get("uuid");
if(StringUtils.isNullOrEmpty(uuid)) return null; if(StringUtils.isNullOrEmpty(uuid)) return null;
return connect(address, uuid); BluetoothSocket s = connect(address, uuid);
if(s == null) return null;
return new DroidtoothTransportConnection(this, s);
} }
public boolean supportsInvitations() { public boolean supportsInvitations() {
@@ -336,36 +347,11 @@ class DroidtoothPlugin implements DuplexPlugin {
public DuplexTransportConnection sendInvitation(PseudoRandom r, public DuplexTransportConnection sendInvitation(PseudoRandom r,
long timeout) { long timeout) {
if(!running) return null; if(!running) return null;
// Use the same pseudo-random UUID as the contact // Use the invitation codes to generate the UUID
byte[] b = r.nextBytes(UUID_BYTES);
String uuid = UUID.nameUUIDFromBytes(b).toString();
if(LOG.isLoggable(INFO)) LOG.info("Sending invitation, UUID " + uuid);
// Register to receive Bluetooth discovery intents
IntentFilter filter = new IntentFilter();
filter.addAction(FOUND);
filter.addAction(DISCOVERY_FINISHED);
// Discover nearby devices and connect to any with the right UUID
DiscoveryReceiver receiver = new DiscoveryReceiver(uuid);
appContext.registerReceiver(receiver, filter);
adapter.startDiscovery();
try {
return receiver.waitForConnection(timeout);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while sending invitation");
Thread.currentThread().interrupt();
return null;
}
}
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
if(!running) return null;
// Use the same pseudo-random UUID as the contact
byte[] b = r.nextBytes(UUID_BYTES); byte[] b = r.nextBytes(UUID_BYTES);
UUID uuid = UUID.nameUUIDFromBytes(b); UUID uuid = UUID.nameUUIDFromBytes(b);
if(LOG.isLoggable(INFO)) LOG.info("Accepting invitation, UUID " + uuid); if(LOG.isLoggable(INFO)) LOG.info("Invitation UUID " + uuid);
// Bind a new server socket to accept the invitation connection // Bind a server socket for receiving invitation connections
BluetoothServerSocket ss = null; BluetoothServerSocket ss = null;
try { try {
ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid); ss = InsecureBluetooth.listen(adapter, "RFCOMM", uuid);
@@ -374,23 +360,29 @@ class DroidtoothPlugin implements DuplexPlugin {
tryToClose(ss); tryToClose(ss);
return null; return null;
} }
// Return the first connection received by the socket, if any // Start the background threads
SocketReceiver receiver = new SocketReceiver();
new DiscoveryThread(receiver, uuid.toString(), timeout).start();
new BluetoothListenerThread(receiver, ss).start();
// Wait for an incoming or outgoing connection
try { try {
BluetoothSocket s = ss.accept((int) timeout); BluetoothSocket s = receiver.waitForSocket(timeout);
if(LOG.isLoggable(INFO)) { if(s != null) return new DroidtoothTransportConnection(this, s);
String address = s.getRemoteDevice().getAddress(); } catch(InterruptedException e) {
LOG.info("Incoming connection from " + address); if(LOG.isLoggable(INFO))
} LOG.info("Interrupted while exchanging invitations");
return new DroidtoothTransportConnection(this, s); Thread.currentThread().interrupt();
} catch(SocketTimeoutException e) {
if(LOG.isLoggable(INFO)) LOG.info("Invitation timed out");
return null;
} catch(IOException e) {
if(LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
return null;
} finally { } finally {
// Closing the socket will terminate the listener thread
tryToClose(ss); tryToClose(ss);
} }
return null;
}
public DuplexTransportConnection acceptInvitation(PseudoRandom r,
long timeout) {
// FIXME
return sendInvitation(r, timeout);
} }
private static class BluetoothStateReceiver extends BroadcastReceiver { private static class BluetoothStateReceiver extends BroadcastReceiver {
@@ -403,67 +395,153 @@ class DroidtoothPlugin implements DuplexPlugin {
int state = intent.getIntExtra(EXTRA_STATE, 0); int state = intent.getIntExtra(EXTRA_STATE, 0);
if(state == STATE_ON) { if(state == STATE_ON) {
enabled = true; enabled = true;
finish(ctx); ctx.unregisterReceiver(this);
finished.countDown();
} else if(state == STATE_OFF) { } else if(state == STATE_OFF) {
finish(ctx); ctx.unregisterReceiver(this);
finished.countDown();
} }
} }
private void finish(Context ctx) {
ctx.unregisterReceiver(this);
finished.countDown();
}
boolean waitForStateChange() throws InterruptedException { boolean waitForStateChange() throws InterruptedException {
finished.await(); finished.await();
return enabled; return enabled;
} }
} }
private static class SocketReceiver {
private final CountDownLatch latch = new CountDownLatch(1);
private final AtomicReference<BluetoothSocket> socket =
new AtomicReference<BluetoothSocket>();
private boolean hasSocket() {
return socket.get() != null;
}
private boolean setSocket(BluetoothSocket s) {
if(socket.compareAndSet(null, s)) {
latch.countDown();
return true;
}
return false;
}
private BluetoothSocket waitForSocket(long timeout)
throws InterruptedException {
latch.await(timeout, TimeUnit.MILLISECONDS);
return socket.get();
}
}
private class DiscoveryThread extends Thread {
private final SocketReceiver receiver;
private final String uuid;
private final long timeout;
private DiscoveryThread(SocketReceiver receiver, String uuid,
long timeout) {
this.receiver = receiver;
this.uuid = uuid;
this.timeout = timeout;
}
@Override
public void run() {
long now = clock.currentTimeMillis();
long end = now + timeout;
while(now < end && running && !receiver.hasSocket()) {
// Discover nearby devices
if(LOG.isLoggable(INFO)) LOG.info("Discovering nearby devices");
List<String> addresses;
try {
addresses = discoverDevices(end - now);
} catch(InterruptedException e) {
if(LOG.isLoggable(INFO))
LOG.info("Interrupted while discovering devices");
return;
}
// Connect to any device with the right UUID
for(String address : addresses) {
now = clock.currentTimeMillis();
if(now < end && running && !receiver.hasSocket()) {
BluetoothSocket s = connect(address, uuid);
if(s == null) continue;
if(LOG.isLoggable(INFO))
LOG.info("Outgoing connection");
if(!receiver.setSocket(s)) {
if(LOG.isLoggable(INFO))
LOG.info("Closing redundant connection");
tryToClose(s);
}
return;
}
}
}
}
private List<String> discoverDevices(long timeout)
throws InterruptedException {
IntentFilter filter = new IntentFilter();
filter.addAction(FOUND);
filter.addAction(DISCOVERY_FINISHED);
DiscoveryReceiver disco = new DiscoveryReceiver();
appContext.registerReceiver(disco, filter);
adapter.startDiscovery();
return disco.waitForAddresses(timeout);
}
}
private class DiscoveryReceiver extends BroadcastReceiver { private class DiscoveryReceiver extends BroadcastReceiver {
private final CountDownLatch finished = new CountDownLatch(1); private final CountDownLatch finished = new CountDownLatch(1);
private final Collection<String> addresses = new ArrayList<String>(); private final List<String> addresses = new ArrayList<String>();
private final String uuid;
private volatile DuplexTransportConnection connection = null;
private DiscoveryReceiver(String uuid) {
this.uuid = uuid;
}
@Override @Override
public void onReceive(Context ctx, Intent intent) { public void onReceive(Context ctx, Intent intent) {
String action = intent.getAction(); String action = intent.getAction();
if(action.equals(DISCOVERY_FINISHED)) { if(action.equals(DISCOVERY_FINISHED)) {
ctx.unregisterReceiver(this); ctx.unregisterReceiver(this);
connectToDiscoveredDevices(); finished.countDown();
} else if(action.equals(FOUND)) { } else if(action.equals(FOUND)) {
BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE); BluetoothDevice d = intent.getParcelableExtra(EXTRA_DEVICE);
String address = d.getAddress(); addresses.add(d.getAddress());
addresses.add(address);
} }
} }
private void connectToDiscoveredDevices() { private List<String> waitForAddresses(long timeout)
for(final String address : addresses) {
pluginExecutor.execute(new Runnable() {
public void run() {
if(!running) return;
DuplexTransportConnection conn = connect(address, uuid);
if(conn != null) {
connection = conn;
finished.countDown();
}
}
});
}
}
private DuplexTransportConnection waitForConnection(long timeout)
throws InterruptedException { throws InterruptedException {
finished.await(timeout, MILLISECONDS); finished.await(timeout, MILLISECONDS);
return connection; return Collections.unmodifiableList(addresses);
}
}
private class BluetoothListenerThread extends Thread {
private final SocketReceiver receiver;
private final BluetoothServerSocket serverSocket;
private BluetoothListenerThread(SocketReceiver receiver,
BluetoothServerSocket serverSocket) {
this.receiver = receiver;
this.serverSocket = serverSocket;
}
@Override
public void run() {
try {
BluetoothSocket s = serverSocket.accept();
if(LOG.isLoggable(INFO)) LOG.info("Incoming connection");
if(!receiver.setSocket(s)) {
if(LOG.isLoggable(INFO))
LOG.info("Closing redundant connection");
tryToClose(s);
}
} catch(IOException e) {
// This is expected when the socket is closed
if(LOG.isLoggable(INFO)) LOG.log(INFO, e.toString(), e);
}
} }
} }
} }

View File

@@ -5,6 +5,8 @@ import java.util.concurrent.Executor;
import net.sf.briar.api.TransportId; import net.sf.briar.api.TransportId;
import net.sf.briar.api.android.AndroidExecutor; import net.sf.briar.api.android.AndroidExecutor;
import net.sf.briar.api.clock.Clock;
import net.sf.briar.api.clock.SystemClock;
import net.sf.briar.api.plugins.duplex.DuplexPlugin; import net.sf.briar.api.plugins.duplex.DuplexPlugin;
import net.sf.briar.api.plugins.duplex.DuplexPluginCallback; import net.sf.briar.api.plugins.duplex.DuplexPluginCallback;
import net.sf.briar.api.plugins.duplex.DuplexPluginFactory; import net.sf.briar.api.plugins.duplex.DuplexPluginFactory;
@@ -20,6 +22,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
private final AndroidExecutor androidExecutor; private final AndroidExecutor androidExecutor;
private final Context appContext; private final Context appContext;
private final SecureRandom secureRandom; private final SecureRandom secureRandom;
private final Clock clock;
public DroidtoothPluginFactory(Executor pluginExecutor, public DroidtoothPluginFactory(Executor pluginExecutor,
AndroidExecutor androidExecutor, Context appContext, AndroidExecutor androidExecutor, Context appContext,
@@ -28,6 +31,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
this.androidExecutor = androidExecutor; this.androidExecutor = androidExecutor;
this.appContext = appContext; this.appContext = appContext;
this.secureRandom = secureRandom; this.secureRandom = secureRandom;
clock = new SystemClock();
} }
public TransportId getId() { public TransportId getId() {
@@ -36,7 +40,7 @@ public class DroidtoothPluginFactory implements DuplexPluginFactory {
public DuplexPlugin createPlugin(DuplexPluginCallback callback) { public DuplexPlugin createPlugin(DuplexPluginCallback callback) {
return new DroidtoothPlugin(pluginExecutor, androidExecutor, appContext, return new DroidtoothPlugin(pluginExecutor, androidExecutor, appContext,
secureRandom, callback, MAX_FRAME_LENGTH, MAX_LATENCY, secureRandom, clock, callback, MAX_FRAME_LENGTH, MAX_LATENCY,
POLLING_INTERVAL); POLLING_INTERVAL);
} }
} }