diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java index 11d6c87b0ac55d85e9a16c3e2de12a872c8b41d4..6303f210b3129e5a28e7121509aa423101c5c03c 100644 --- a/briar-core/src/org/briarproject/CoreModule.java +++ b/briar-core/src/org/briarproject/CoreModule.java @@ -9,6 +9,7 @@ import org.briarproject.event.EventModule; import org.briarproject.forum.ForumModule; import org.briarproject.identity.IdentityModule; import org.briarproject.invitation.InvitationModule; +import org.briarproject.keyagreement.KeyAgreementModule; import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.messaging.MessagingModule; import org.briarproject.plugins.PluginsModule; @@ -23,7 +24,8 @@ import dagger.Module; @Module(includes = {DatabaseModule.class, CryptoModule.class, LifecycleModule.class, ReliabilityModule.class, - MessagingModule.class, InvitationModule.class, ForumModule.class, + MessagingModule.class, InvitationModule.class, KeyAgreementModule.class, + ForumModule.class, IdentityModule.class, EventModule.class, DataModule.class, ContactModule.class, PropertiesModule.class, TransportModule.class, SyncModule.class, SettingsModule.class, ClientsModule.class, diff --git a/briar-core/src/org/briarproject/keyagreement/AbortException.java b/briar-core/src/org/briarproject/keyagreement/AbortException.java new file mode 100644 index 0000000000000000000000000000000000000000..670bbc3ee14f63d6bd1d543d58d06652576c4437 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/AbortException.java @@ -0,0 +1,23 @@ +package org.briarproject.keyagreement; + +class AbortException extends Exception { + public boolean receivedAbort; + + public AbortException() { + this(false); + } + + public AbortException(boolean receivedAbort) { + super(); + this.receivedAbort = receivedAbort; + } + + public AbortException(Exception e) { + this(e, false); + } + + public AbortException(Exception e, boolean receivedAbort) { + super(e); + this.receivedAbort = receivedAbort; + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java new file mode 100644 index 0000000000000000000000000000000000000000..e87297af63402215582b79de7ad5bd98f63ec31e --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementConnector.java @@ -0,0 +1,236 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +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.PluginManager; +import org.briarproject.api.plugins.duplex.DuplexPlugin; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; +import org.briarproject.api.system.Clock; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.Future; +import java.util.logging.Logger; + +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.CONNECTION_TIMEOUT; + +class KeyAgreementConnector { + + interface Callbacks { + void connectionWaiting(); + } + + private static final Logger LOG = + Logger.getLogger(KeyAgreementConnector.class.getName()); + + private final Callbacks callbacks; + private final Clock clock; + private final CryptoComponent crypto; + private final PluginManager pluginManager; + private final CompletionService<KeyAgreementConnection> connect; + + private final List<KeyAgreementListener> listeners = + new ArrayList<KeyAgreementListener>(); + private final List<Future<KeyAgreementConnection>> pending = + new ArrayList<Future<KeyAgreementConnection>>(); + + private volatile boolean connecting = false; + private volatile boolean alice = false; + + public KeyAgreementConnector(Callbacks callbacks, Clock clock, + CryptoComponent crypto, PluginManager pluginManager, + Executor ioExecutor) { + this.callbacks = callbacks; + this.clock = clock; + this.crypto = crypto; + this.pluginManager = pluginManager; + connect = new ExecutorCompletionService<KeyAgreementConnection>( + ioExecutor); + } + + public Payload listen(KeyPair localKeyPair) { + LOG.info("Starting BQP listeners"); + // Derive commitment + byte[] commitment = crypto.deriveKeyCommitment( + localKeyPair.getPublic().getEncoded()); + // Start all listeners and collect their descriptors + List<TransportDescriptor> descriptors = + new ArrayList<TransportDescriptor>(); + for (DuplexPlugin plugin : pluginManager.getKeyAgreementPlugins()) { + KeyAgreementListener l = plugin.createKeyAgreementListener( + commitment); + if (l != null) { + TransportDescriptor d = l.getDescriptor(); + descriptors.add(d); + pending.add(connect.submit(new ReadableTask(l.listen()))); + listeners.add(l); + } + } + return new Payload(commitment, descriptors); + } + + public void stopListening() { + LOG.info("Stopping BQP listeners"); + for (KeyAgreementListener l : listeners) { + l.close(); + } + listeners.clear(); + } + + public KeyAgreementTransport connect(Payload remotePayload, + boolean alice) { + // Let the listeners know if we are Alice + this.connecting = true; + this.alice = alice; + long end = clock.currentTimeMillis() + CONNECTION_TIMEOUT; + + // 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) + pending.add(connect.submit(new ReadableTask( + new ConnectorTask(plugin, remotePayload.getCommitment(), + d, end)))); + } + + // Get chosen connection + KeyAgreementConnection chosen = null; + try { + long now = clock.currentTimeMillis(); + Future<KeyAgreementConnection> f = + connect.poll(end - now, MILLISECONDS); + if (f == null) + return null; // No task completed within the timeout. + chosen = f.get(); + return new KeyAgreementTransport(chosen); + } catch (InterruptedException e) { + LOG.info("Interrupted while waiting for connection"); + Thread.currentThread().interrupt(); + return null; + } catch (ExecutionException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } finally { + stopListening(); + // Close all other connections + closePending(chosen); + } + } + + private void closePending(KeyAgreementConnection chosen) { + for (Future<KeyAgreementConnection> f : pending) { + try { + if (f.cancel(true)) + LOG.info("Cancelled task"); + else if (!f.isCancelled()) { + KeyAgreementConnection c = f.get(); + if (c != null && c != chosen) + tryToClose(c.getConnection(), false); + } + } catch (InterruptedException e) { + LOG.info("Interrupted while closing sockets"); + Thread.currentThread().interrupt(); + return; + } catch (ExecutionException e) { + if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + } + } + } + + private void tryToClose(DuplexTransportConnection conn, boolean exception) { + try { + if (LOG.isLoggable(INFO)) + LOG.info("Closing connection, exception: " + exception); + conn.getReader().dispose(exception, true); + conn.getWriter().dispose(exception); + } catch (IOException e) { + if (LOG.isLoggable(INFO)) LOG.info(e.toString()); + } + } + + private class ConnectorTask implements Callable<KeyAgreementConnection> { + + private final byte[] commitment; + private final TransportDescriptor descriptor; + private final long end; + private final DuplexPlugin plugin; + + private ConnectorTask(DuplexPlugin plugin, byte[] commitment, + TransportDescriptor descriptor, long end) { + this.plugin = plugin; + this.commitment = commitment; + this.descriptor = descriptor; + this.end = end; + } + + @Override + public KeyAgreementConnection call() throws Exception { + // Repeat attempts until we connect or get interrupted + while (true) { + long now = clock.currentTimeMillis(); + DuplexTransportConnection conn = + plugin.createKeyAgreementConnection(commitment, + descriptor, end - now); + if (conn != null) { + if (LOG.isLoggable(INFO)) + LOG.info(plugin.getId().getString() + + ": Outgoing connection"); + return new KeyAgreementConnection(conn, plugin.getId()); + } + // Wait 2s before retry (to circumvent transient failures) + Thread.sleep(2000); + } + } + } + + private class ReadableTask + implements Callable<KeyAgreementConnection> { + + private final Callable<KeyAgreementConnection> connectionTask; + + private ReadableTask(Callable<KeyAgreementConnection> connectionTask) { + this.connectionTask = connectionTask; + } + + @Override + public KeyAgreementConnection call() + throws Exception { + KeyAgreementConnection c = connectionTask.call(); + InputStream in = c.getConnection().getReader().getInputStream(); + boolean waitingSent = false; + while (!alice && in.available() == 0) { + if (!waitingSent && connecting && !alice) { + // Bob waits here until Alice obtains his payload. + callbacks.connectionWaiting(); + waitingSent = true; + } + if (LOG.isLoggable(INFO)) + LOG.info(c.getTransportId().toString() + + ": Waiting for connection"); + Thread.sleep(1000); + } + if (!alice && LOG.isLoggable(INFO)) + LOG.info(c.getTransportId().toString() + ": Data available"); + return c; + } + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java new file mode 100644 index 0000000000000000000000000000000000000000..9f6a529028a668fc4cbde1231a3a61201730f847 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementModule.java @@ -0,0 +1,43 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfReaderFactory; +import org.briarproject.api.data.BdfWriterFactory; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.keyagreement.KeyAgreementTaskFactory; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.keyagreement.PayloadParser; +import org.briarproject.api.lifecycle.IoExecutor; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.system.Clock; + +import java.util.concurrent.Executor; + +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class KeyAgreementModule { + + @Provides + @Singleton + KeyAgreementTaskFactory provideKeyAgreementTaskFactory(Clock clock, + CryptoComponent crypto, EventBus eventBus, + @IoExecutor Executor ioExecutor, PayloadEncoder payloadEncoder, + PluginManager pluginManager) { + return new KeyAgreementTaskFactoryImpl(clock, crypto, eventBus, + ioExecutor, payloadEncoder, pluginManager); + } + + @Provides + PayloadEncoder providePayloadEncoder(BdfWriterFactory bdfWriterFactory) { + return new PayloadEncoderImpl(bdfWriterFactory); + } + + @Provides + PayloadParser providePayloadParser(BdfReaderFactory bdfReaderFactory) { + return new PayloadParserImpl(bdfReaderFactory); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..c832edd092e10c3227eeed2e538c7ed25b901f88 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementProtocol.java @@ -0,0 +1,157 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.Arrays; + +/** + * Implementation of the BQP protocol. + * <p/> + * Alice: + * <ul> + * <li>Send A_KEY</li> + * <li>Receive B_KEY + * <ul> + * <li>Check B_KEY matches B_COMMIT</li> + * </ul></li> + * <li>Calculate s</li> + * <li>Send A_CONFIRM</li> + * <li>Receive B_CONFIRM + * <ul> + * <li>Check B_CONFIRM matches expected</li> + * </ul></li> + * <li>Derive master</li> + * </ul><p/> + * Bob: + * <ul> + * <li>Receive A_KEY + * <ul> + * <li>Check A_KEY matches A_COMMIT</li> + * </ul></li> + * <li>Send B_KEY</li> + * <li>Calculate s</li> + * <li>Receive A_CONFIRM + * <ul> + * <li>Check A_CONFIRM matches expected</li> + * </ul></li> + * <li>Send B_CONFIRM</li> + * <li>Derive master</li> + * </ul> + */ +class KeyAgreementProtocol { + + interface Callbacks { + void connectionWaiting(); + void initialPacketReceived(); + } + + private Callbacks callbacks; + private CryptoComponent crypto; + private PayloadEncoder payloadEncoder; + private KeyAgreementTransport transport; + private Payload theirPayload, ourPayload; + private KeyPair ourKeyPair; + private boolean alice; + + public KeyAgreementProtocol(Callbacks callbacks, CryptoComponent crypto, + PayloadEncoder payloadEncoder, KeyAgreementTransport transport, + Payload theirPayload, Payload ourPayload, KeyPair ourKeyPair, + boolean alice) { + this.callbacks = callbacks; + this.crypto = crypto; + this.payloadEncoder = payloadEncoder; + this.transport = transport; + this.theirPayload = theirPayload; + this.ourPayload = ourPayload; + this.ourKeyPair = ourKeyPair; + this.alice = alice; + } + + /** + * Perform the BQP protocol. + * + * @return the negotiated master secret. + * @throws AbortException when the protocol may have been tampered with. + * @throws IOException for all other other connection errors. + */ + public SecretKey perform() throws AbortException, IOException { + try { + byte[] theirPublicKey; + if (alice) { + sendKey(); + // Alice waits here until Bob obtains her payload. + callbacks.connectionWaiting(); + theirPublicKey = receiveKey(); + } else { + theirPublicKey = receiveKey(); + sendKey(); + } + SecretKey s = deriveSharedSecret(theirPublicKey); + if (alice) { + sendConfirm(s, theirPublicKey); + receiveConfirm(s, theirPublicKey); + } else { + receiveConfirm(s, theirPublicKey); + sendConfirm(s, theirPublicKey); + } + return crypto.deriveMasterSecret(s); + } catch (AbortException e) { + sendAbort(e.getCause() != null); + throw e; + } + } + + private void sendKey() throws IOException { + transport.sendKey(ourKeyPair.getPublic().getEncoded()); + } + + private byte[] receiveKey() throws AbortException { + byte[] publicKey = transport.receiveKey(); + callbacks.initialPacketReceived(); + byte[] expected = crypto.deriveKeyCommitment(publicKey); + if (!Arrays.equals(expected, theirPayload.getCommitment())) + throw new AbortException(); + return publicKey; + } + + private SecretKey deriveSharedSecret(byte[] theirPublicKey) + throws AbortException { + try { + return crypto.deriveSharedSecret(theirPublicKey, ourKeyPair, alice); + } catch (GeneralSecurityException e) { + throw new AbortException(e); + } + } + + private void sendConfirm(SecretKey s, byte[] theirPublicKey) + throws IOException { + byte[] confirm = crypto.deriveConfirmationRecord(s, + payloadEncoder.encode(theirPayload), + payloadEncoder.encode(ourPayload), + theirPublicKey, ourKeyPair, + alice, alice); + transport.sendConfirm(confirm); + } + + private void receiveConfirm(SecretKey s, byte[] theirPublicKey) + throws AbortException { + byte[] confirm = transport.receiveConfirm(); + byte[] expected = crypto.deriveConfirmationRecord(s, + payloadEncoder.encode(theirPayload), + payloadEncoder.encode(ourPayload), + theirPublicKey, ourKeyPair, + alice, !alice); + if (!Arrays.equals(expected, confirm)) + throw new AbortException(); + } + + private void sendAbort(boolean exception) { + transport.sendAbort(exception); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..aafde49b3b1acb32aa01c995b9402a96cf615e41 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskFactoryImpl.java @@ -0,0 +1,46 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.EventListener; +import org.briarproject.api.event.KeyAgreementAbortedEvent; +import org.briarproject.api.event.KeyAgreementFailedEvent; +import org.briarproject.api.event.KeyAgreementFinishedEvent; +import org.briarproject.api.keyagreement.KeyAgreementTask; +import org.briarproject.api.keyagreement.KeyAgreementTaskFactory; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.lifecycle.IoExecutor; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.system.Clock; + +import java.util.concurrent.Executor; + +import javax.inject.Inject; + +class KeyAgreementTaskFactoryImpl implements KeyAgreementTaskFactory { + + private final Clock clock; + private final CryptoComponent crypto; + private final EventBus eventBus; + private final Executor ioExecutor; + private final PayloadEncoder payloadEncoder; + private final PluginManager pluginManager; + + @Inject + KeyAgreementTaskFactoryImpl(Clock clock, CryptoComponent crypto, + EventBus eventBus, @IoExecutor Executor ioExecutor, + PayloadEncoder payloadEncoder, PluginManager pluginManager) { + this.clock = clock; + this.crypto = crypto; + this.eventBus = eventBus; + this.ioExecutor = ioExecutor; + this.payloadEncoder = payloadEncoder; + this.pluginManager = pluginManager; + } + + public KeyAgreementTask getTask() { + return new KeyAgreementTaskImpl(clock, crypto, eventBus, payloadEncoder, + pluginManager, ioExecutor); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..12f7678740421fbae7f1251c7f7a107cb6b967f8 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTaskImpl.java @@ -0,0 +1,135 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.event.EventBus; +import org.briarproject.api.event.KeyAgreementAbortedEvent; +import org.briarproject.api.event.KeyAgreementFailedEvent; +import org.briarproject.api.event.KeyAgreementFinishedEvent; +import org.briarproject.api.event.KeyAgreementListeningEvent; +import org.briarproject.api.event.KeyAgreementStartedEvent; +import org.briarproject.api.event.KeyAgreementWaitingEvent; +import org.briarproject.api.keyagreement.KeyAgreementResult; +import org.briarproject.api.keyagreement.KeyAgreementTask; +import org.briarproject.api.keyagreement.Payload; +import org.briarproject.api.keyagreement.PayloadEncoder; +import org.briarproject.api.plugins.PluginManager; +import org.briarproject.api.system.Clock; + +import java.io.IOException; +import java.util.concurrent.Executor; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; + +class KeyAgreementTaskImpl extends Thread implements + KeyAgreementTask, KeyAgreementConnector.Callbacks, + KeyAgreementProtocol.Callbacks { + + private static final Logger LOG = + Logger.getLogger(KeyAgreementTaskImpl.class.getName()); + + private final CryptoComponent crypto; + private final EventBus eventBus; + private final PayloadEncoder payloadEncoder; + private final KeyPair localKeyPair; + private final KeyAgreementConnector connector; + + private Payload localPayload; + private Payload remotePayload; + + public KeyAgreementTaskImpl(Clock clock, CryptoComponent crypto, + EventBus eventBus, PayloadEncoder payloadEncoder, + PluginManager pluginManager, Executor ioExecutor) { + this.crypto = crypto; + this.eventBus = eventBus; + this.payloadEncoder = payloadEncoder; + localKeyPair = crypto.generateAgreementKeyPair(); + connector = new KeyAgreementConnector(this, clock, crypto, + pluginManager, ioExecutor); + } + + @Override + public synchronized void listen() { + if (localPayload == null) { + localPayload = connector.listen(localKeyPair); + eventBus.broadcast(new KeyAgreementListeningEvent(localPayload)); + } + } + + @Override + public synchronized void stopListening() { + if (localPayload != null) { + if (remotePayload == null) + connector.stopListening(); + else + interrupt(); + } + } + + @Override + public synchronized void connectAndRunProtocol(Payload remotePayload) { + if (this.localPayload == null) + throw new IllegalStateException( + "Must listen before connecting"); + if (this.remotePayload != null) + throw new IllegalStateException( + "Already provided remote payload for this task"); + this.remotePayload = remotePayload; + start(); + } + + @Override + public void run() { + boolean alice = localPayload.compareTo(remotePayload) < 0; + + // Open connection to remote device + KeyAgreementTransport transport = + connector.connect(remotePayload, alice); + if (transport == null) { + // Notify caller that the connection failed + eventBus.broadcast(new KeyAgreementFailedEvent()); + return; + } + + // Run BQP protocol over the connection + LOG.info("Starting BQP protocol"); + KeyAgreementProtocol protocol = new KeyAgreementProtocol(this, crypto, + payloadEncoder, transport, remotePayload, localPayload, + localKeyPair, alice); + try { + SecretKey master = protocol.perform(); + KeyAgreementResult result = + new KeyAgreementResult(master, transport.getConnection(), + transport.getTransportId(), alice); + LOG.info("Finished BQP protocol"); + // Broadcast result to caller + eventBus.broadcast(new KeyAgreementFinishedEvent(result)); + } catch (AbortException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + // Notify caller that the protocol was aborted + eventBus.broadcast(new KeyAgreementAbortedEvent(e.receivedAbort)); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + // Notify caller that the connection failed + eventBus.broadcast(new KeyAgreementFailedEvent()); + } + } + + @Override + public void connectionWaiting() { + eventBus.broadcast(new KeyAgreementWaitingEvent()); + } + + @Override + public void initialPacketReceived() { + // We send this here instead of when we create the protocol, so that + // if device A makes a connection after getting device B's payload and + // starts its protocol, device A's UI doesn't change to prevent device B + // from getting device A's payload. + eventBus.broadcast(new KeyAgreementStartedEvent()); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java new file mode 100644 index 0000000000000000000000000000000000000000..25a18962bad176a7d58535d2fd5009d6977b1a51 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/KeyAgreementTransport.java @@ -0,0 +1,131 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.TransportId; +import org.briarproject.api.keyagreement.KeyAgreementConnection; +import org.briarproject.api.plugins.duplex.DuplexTransportConnection; +import org.briarproject.util.ByteUtils; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_LENGTH; +import static org.briarproject.api.keyagreement.KeyAgreementConstants.RECORD_HEADER_PAYLOAD_LENGTH_OFFSET; +import static org.briarproject.api.keyagreement.RecordTypes.ABORT; +import static org.briarproject.api.keyagreement.RecordTypes.CONFIRM; +import static org.briarproject.api.keyagreement.RecordTypes.KEY; + +/** + * Handles the sending and receiving of BQP records. + */ +class KeyAgreementTransport { + + private static final Logger LOG = + Logger.getLogger(KeyAgreementTransport.class.getName()); + + private final KeyAgreementConnection kac; + private final InputStream in; + private final OutputStream out; + + public KeyAgreementTransport(KeyAgreementConnection kac) + throws IOException { + this.kac = kac; + in = kac.getConnection().getReader().getInputStream(); + out = kac.getConnection().getWriter().getOutputStream(); + } + + public DuplexTransportConnection getConnection() { + return kac.getConnection(); + } + + public TransportId getTransportId() { + return kac.getTransportId(); + } + + public void sendKey(byte[] key) throws IOException { + writeRecord(KEY, key); + } + + public byte[] receiveKey() throws AbortException { + return readRecord(KEY); + } + + public void sendConfirm(byte[] confirm) throws IOException { + writeRecord(CONFIRM, confirm); + } + + public byte[] receiveConfirm() throws AbortException { + return readRecord(CONFIRM); + } + + public void sendAbort(boolean exception) { + try { + writeRecord(ABORT, new byte[0]); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + exception = true; + } + tryToClose(exception); + } + + public void tryToClose(boolean exception) { + try { + LOG.info("Closing connection"); + kac.getConnection().getReader().dispose(exception, true); + kac.getConnection().getWriter().dispose(exception); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private void writeRecord(byte type, byte[] payload) throws IOException { + byte[] recordHeader = new byte[RECORD_HEADER_LENGTH]; + recordHeader[0] = PROTOCOL_VERSION; + recordHeader[1] = type; + ByteUtils.writeUint16(payload.length, recordHeader, + RECORD_HEADER_PAYLOAD_LENGTH_OFFSET); + out.write(recordHeader); + out.write(payload); + out.flush(); + } + + private byte[] readRecord(byte type) throws AbortException { + byte[] header = readHeader(); + if (header[0] != PROTOCOL_VERSION) + throw new AbortException(); // TODO handle? + if (header[1] != type) { + // Unexpected packet + throw new AbortException(header[1] == ABORT); + } + int len = ByteUtils.readUint16(header, + RECORD_HEADER_PAYLOAD_LENGTH_OFFSET); + try { + return readData(len); + } catch (IOException e) { + throw new AbortException(e); + } + } + + private byte[] readHeader() throws AbortException { + try { + return readData(RECORD_HEADER_LENGTH); + } catch (IOException e) { + throw new AbortException(e); + } + } + + private byte[] readData(int len) throws IOException { + byte[] data = new byte[len]; + int offset = 0; + while (offset < data.length) { + int read = in.read(data, offset, data.length - offset); + if (read == -1) throw new EOFException(); + offset += read; + } + return data; + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..8a26f9405bd17128dc7cfde4ca89bb90859f13d0 --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/PayloadEncoderImpl.java @@ -0,0 +1,48 @@ +package org.briarproject.keyagreement; + +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 java.io.ByteArrayOutputStream; +import java.io.IOException; + +import javax.inject.Inject; + +import static org.briarproject.api.keyagreement.KeyAgreementConstants.PROTOCOL_VERSION; + +class PayloadEncoderImpl implements PayloadEncoder { + + private final BdfWriterFactory bdfWriterFactory; + + @Inject + public PayloadEncoderImpl(BdfWriterFactory bdfWriterFactory) { + this.bdfWriterFactory = bdfWriterFactory; + } + + @Override + public byte[] encode(Payload p) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BdfWriter w = bdfWriterFactory.createWriter(out); + try { + 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 + w.writeListEnd(); // Payload end + } catch (IOException e) { + // Shouldn't happen with ByteArrayOutputStream + throw new RuntimeException(e); + } + return out.toByteArray(); + } +} diff --git a/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..d13f9ff73228af96b26a814a6d55e19a3e4d9a2b --- /dev/null +++ b/briar-core/src/org/briarproject/keyagreement/PayloadParserImpl.java @@ -0,0 +1,68 @@ +package org.briarproject.keyagreement; + +import org.briarproject.api.FormatException; +import org.briarproject.api.TransportId; +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 java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +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; + +class PayloadParserImpl implements PayloadParser { + + private final BdfReaderFactory bdfReaderFactory; + + @Inject + public PayloadParserImpl(BdfReaderFactory bdfReaderFactory) { + this.bdfReaderFactory = bdfReaderFactory; + } + + @Override + 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)); + } + r.readListEnd(); + } + r.readListEnd(); // Descriptors end + r.readListEnd(); // Payload end + if (!r.eof()) + throw new FormatException(); + return new Payload(commitment, descriptors); + } +}