From 1bc29fec067ef00113eacae52489bf080fee2de7 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 19 Apr 2018 17:06:31 -0300 Subject: [PATCH] IntroductionManager and Protocol Engines --- .../introduction2/IntroductionManager.java | 52 ++ .../briar/client/BdfIncomingMessageHook.java | 1 + .../introduction2/AbstractProtocolEngine.java | 197 +++++++ .../IntroduceeProtocolEngine.java | 510 ++++++++++++++++++ .../IntroducerProtocolEngine.java | 462 ++++++++++++++++ .../introduction2/IntroductionConstants.java | 3 + .../IntroductionManagerImpl.java | 459 ++++++++++++++++ .../briar/introduction2/ProtocolEngine.java | 40 ++ 8 files changed, 1724 insertions(+) create mode 100644 briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java create mode 100644 briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java new file mode 100644 index 000000000..f3d5c40fa --- /dev/null +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction2/IntroductionManager.java @@ -0,0 +1,52 @@ +package org.briarproject.briar.api.introduction2; + +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.ClientId; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient; + +import java.util.Collection; + +import javax.annotation.Nullable; + +@NotNullByDefault +public interface IntroductionManager extends ConversationClient { + + /** + * The unique ID of the introduction client. + */ + ClientId CLIENT_ID = new ClientId("org.briarproject.briar.introduction"); + + /** + * The current version of the introduction client. + */ + int CLIENT_VERSION = 1; + + /** + * Sends two initial introduction messages. + */ + void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, + long timestamp) throws DbException; + + /** + * Accepts an introduction. + */ + void acceptIntroduction(ContactId contactId, SessionId sessionId, + long timestamp) throws DbException; + + /** + * Declines an introduction. + */ + void declineIntroduction(ContactId contactId, SessionId sessionId, + long timestamp) throws DbException; + + /** + * Returns all introduction messages for the given contact. + */ + Collection getIntroductionMessages(ContactId contactId) + throws DbException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java b/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java index 19d8142c1..b7e16c8d6 100644 --- a/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java +++ b/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java @@ -40,6 +40,7 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook, /** * Called once for each incoming message that passes validation. * + * @return whether or not this message should be shared * @throws DbException Should only be used for real database errors. * If this is thrown, delivery will be attempted again at next startup, * whereas if a FormatException is thrown, the message will be permanently diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java new file mode 100644 index 000000000..d2c9f2270 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/AbstractProtocolEngine.java @@ -0,0 +1,197 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.SessionId; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_ID; +import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_VERSION; +import static org.briarproject.briar.introduction2.MessageType.ABORT; +import static org.briarproject.briar.introduction2.MessageType.ACCEPT; +import static org.briarproject.briar.introduction2.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction2.MessageType.AUTH; +import static org.briarproject.briar.introduction2.MessageType.DECLINE; +import static org.briarproject.briar.introduction2.MessageType.REQUEST; + +@Immutable +@NotNullByDefault +abstract class AbstractProtocolEngine + implements ProtocolEngine { + + protected final DatabaseComponent db; + protected final ClientHelper clientHelper; + protected final ContactManager contactManager; + protected final ContactGroupFactory contactGroupFactory; + protected final MessageTracker messageTracker; + protected final IdentityManager identityManager; + protected final MessageParser messageParser; + protected final MessageEncoder messageEncoder; + protected final Clock clock; + + AbstractProtocolEngine( + DatabaseComponent db, + ClientHelper clientHelper, + ContactManager contactManager, + ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, + IdentityManager identityManager, + MessageParser messageParser, + MessageEncoder messageEncoder, + Clock clock) { + this.db = db; + this.clientHelper = clientHelper; + this.contactManager = contactManager; + this.contactGroupFactory = contactGroupFactory; + this.messageTracker = messageTracker; + this.identityManager = identityManager; + this.messageParser = messageParser; + this.messageEncoder = messageEncoder; + this.clock = clock; + } + + Message sendRequestMessage(Transaction txn, PeerSession s, + long timestamp, Author author, @Nullable String message) + throws DbException { + Message m = messageEncoder + .encodeRequestMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), author, message); + sendMessage(txn, REQUEST, s.getSessionId(), m, true); + return m; + } + + Message sendAcceptMessage(Transaction txn, PeerSession s, long timestamp, + byte[] ephemeralPublicKey, long acceptTimestamp, + Map transportProperties, + boolean visible) + throws DbException { + Message m = messageEncoder + .encodeAcceptMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId(), + ephemeralPublicKey, acceptTimestamp, + transportProperties); + sendMessage(txn, ACCEPT, s.getSessionId(), m, visible); + return m; + } + + Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp, + boolean visible) throws DbException { + Message m = messageEncoder + .encodeDeclineMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId()); + sendMessage(txn, DECLINE, s.getSessionId(), m, visible); + return m; + } + + Message sendAuthMessage(Transaction txn, PeerSession s, long timestamp, + byte[] mac, byte[] signature) throws DbException { + Message m = messageEncoder + .encodeAuthMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId(), mac, + signature); + sendMessage(txn, AUTH, s.getSessionId(), m, false); + return m; + } + + Message sendActivateMessage(Transaction txn, PeerSession s, long timestamp) + throws DbException { + Message m = messageEncoder + .encodeActivateMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId()); + sendMessage(txn, ACTIVATE, s.getSessionId(), m, false); + return m; + } + + Message sendAbortMessage(Transaction txn, PeerSession s, long timestamp) + throws DbException { + Message m = messageEncoder + .encodeAbortMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId()); + sendMessage(txn, ABORT, s.getSessionId(), m, false); + return m; + } + + private void sendMessage(Transaction txn, MessageType type, + SessionId sessionId, Message m, boolean visibleInConversation) + throws DbException { + BdfDictionary meta = messageEncoder + .encodeMetadata(type, sessionId, m.getTimestamp(), true, true, + visibleInConversation); + try { + clientHelper.addLocalMessage(txn, m, meta, true); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + void markMessageVisibleInUi(Transaction txn, MessageId m) + throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setVisibleInUi(meta, true); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + void markRequestUnavailableToAnswer(Transaction txn, MessageId m) + throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setAvailableToAnswer(meta, false); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + Map getSessions(Transaction txn, + BdfDictionary query) throws DbException, FormatException { + return clientHelper + .getMessageMetadataAsDictionary(txn, getLocalGroup().getId(), + query); + } + + private Group getLocalGroup() { + return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION); + } + + boolean isInvalidDependency(@Nullable MessageId lastRemoteMessageId, + @Nullable MessageId dependency) { + if (dependency == null) return lastRemoteMessageId != null; + return lastRemoteMessageId == null || + !dependency.equals(lastRemoteMessageId); + } + + long getLocalTimestamp(long localTimestamp, long requestTimestamp) { + return Math.max( + clock.currentTimeMillis(), + Math.max( + localTimestamp, + requestTimestamp + ) + 1 + ); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java new file mode 100644 index 000000000..1efb6919b --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroduceeProtocolEngine.java @@ -0,0 +1,510 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.ContactExistsException; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.properties.TransportPropertyManager; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.ProtocolStateException; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction2.IntroductionRequest; +import org.briarproject.briar.api.introduction2.IntroductionResponse; +import org.briarproject.briar.api.introduction2.event.IntroductionRequestReceivedEvent; +import org.briarproject.briar.api.introduction2.event.IntroductionResponseReceivedEvent; +import org.briarproject.briar.api.introduction2.event.IntroductionSucceededEvent; + +import java.security.GeneralSecurityException; +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE; +import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_AUTH; +import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_RESPONSES; +import static org.briarproject.briar.introduction2.IntroduceeState.LOCAL_ACCEPTED; +import static org.briarproject.briar.introduction2.IntroduceeState.REMOTE_ACCEPTED; + +@Immutable +@NotNullByDefault +class IntroduceeProtocolEngine + extends AbstractProtocolEngine { + + private final IntroductionCrypto crypto; + private final KeyManager keyManager; + private final TransportPropertyManager transportPropertyManager; + + @Inject + IntroduceeProtocolEngine( + DatabaseComponent db, + ClientHelper clientHelper, + ContactManager contactManager, + ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, + IdentityManager identityManager, + MessageParser messageParser, + MessageEncoder messageEncoder, + Clock clock, + IntroductionCrypto crypto, + KeyManager keyManager, + TransportPropertyManager transportPropertyManager) { + super(db, clientHelper, contactManager, contactGroupFactory, + messageTracker, identityManager, messageParser, messageEncoder, + clock); + this.crypto = crypto; + this.keyManager = keyManager; + this.transportPropertyManager = transportPropertyManager; + } + + @Override + public IntroduceeSession onRequestAction(Transaction txn, + IntroduceeSession session, @Nullable String message, + long timestamp) { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public IntroduceeSession onAcceptAction(Transaction txn, + IntroduceeSession session, long timestamp) throws DbException { + switch (session.getState()) { + case AWAIT_RESPONSES: + case REMOTE_ACCEPTED: + return onLocalAccept(txn, session, timestamp); + case START: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onDeclineAction(Transaction txn, + IntroduceeSession session, long timestamp) throws DbException { + switch (session.getState()) { + case AWAIT_RESPONSES: + case REMOTE_ACCEPTED: + return onLocalDecline(txn, session, timestamp); + case START: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onRequestMessage(Transaction txn, + IntroduceeSession session, RequestMessage m) + throws DbException, FormatException { + switch (session.getState()) { + case START: + return onRemoteRequest(txn, session, m); + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onAcceptMessage(Transaction txn, + IntroduceeSession session, AcceptMessage m) + throws DbException, FormatException { + switch (session.getState()) { + case AWAIT_RESPONSES: + case LOCAL_ACCEPTED: + return onRemoteAccept(txn, session, m); + case START: + case LOCAL_DECLINED: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onDeclineMessage(Transaction txn, + IntroduceeSession session, DeclineMessage m) + throws DbException, FormatException { + switch (session.getState()) { + case START: + return session; // Ignore in the START state + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + return onRemoteDecline(txn, session, m); + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onAuthMessage(Transaction txn, + IntroduceeSession session, AuthMessage m) + throws DbException, FormatException { + switch (session.getState()) { + case AWAIT_AUTH: + return onRemoteAuth(txn, session, m); + case START: + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case REMOTE_ACCEPTED: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onActivateMessage(Transaction txn, + IntroduceeSession session, ActivateMessage m) + throws DbException, FormatException { + switch (session.getState()) { + case AWAIT_ACTIVATE: + return onRemoteActivate(txn, session, m); + case START: + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onAbortMessage(Transaction txn, + IntroduceeSession session, AbortMessage m) + throws DbException, FormatException { + return onRemoteAbort(txn, session, m); + } + + private IntroduceeSession onRemoteRequest(Transaction txn, + IntroduceeSession s, RequestMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Mark the request visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + + // Add SessionId to message metadata + addSessionId(txn, m.getMessageId(), s.getSessionId()); + + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Broadcast IntroductionRequestReceivedEvent + Contact c = contactManager.getContact(txn, s.getIntroducer().getId(), + identityManager.getLocalAuthor(txn).getId()); + boolean contactExists = false; // TODO + IntroductionRequest request = + new IntroductionRequest(s.getSessionId(), m.getMessageId(), + m.getGroupId(), INTRODUCEE, m.getTimestamp(), false, + false, false, false, m.getAuthor().getName(), false, + m.getMessage(), false, contactExists); + IntroductionRequestReceivedEvent e = + new IntroductionRequestReceivedEvent(c.getId(), request); + txn.attach(e); + + // Move to the AWAIT_RESPONSES state + return IntroduceeSession.addRemoteRequest(s, AWAIT_RESPONSES, m); + } + + private IntroduceeSession onLocalAccept(Transaction txn, + IntroduceeSession s, long timestamp) throws DbException { + // Mark the request message unavailable to answer + MessageId requestId = s.getLastRemoteMessageId(); + if (requestId == null) throw new IllegalStateException(); + markRequestUnavailableToAnswer(txn, requestId); + + // Create ephemeral key pair and get local transport properties + KeyPair keyPair = crypto.generateKeyPair(); + byte[] publicKey = keyPair.getPublic().getEncoded(); + byte[] privateKey = keyPair.getPrivate().getEncoded(); + Map transportProperties = + transportPropertyManager.getLocalProperties(txn); + + // Send a ACCEPT message + long localTimestamp = + Math.max(timestamp, getLocalTimestamp(s)); + Message sent = sendAcceptMessage(txn, s, localTimestamp, publicKey, + localTimestamp, transportProperties, true); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + + // Determine the next state + IntroduceeState state = + s.getState() == AWAIT_RESPONSES ? LOCAL_ACCEPTED : AWAIT_AUTH; + IntroduceeSession sNew = IntroduceeSession + .addLocalAccept(s, state, sent, publicKey, privateKey, + localTimestamp, transportProperties); + + if (state == AWAIT_AUTH) { + // Move to the AWAIT_AUTH state + return onLocalAuth(txn, sNew); + } + // Move to the LOCAL_ACCEPTED state + return sNew; + } + + private IntroduceeSession onLocalDecline(Transaction txn, + IntroduceeSession s, long timestamp) throws DbException { + // Mark the request message unavailable to answer + MessageId requestId = s.getLastRemoteMessageId(); + if (requestId == null) throw new IllegalStateException(); + markRequestUnavailableToAnswer(txn, requestId); + + // Send a DECLINE message + long localTimestamp = Math.max(timestamp, getLocalTimestamp(s)); + Message sent = sendDeclineMessage(txn, s, localTimestamp, true); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + + // Move to the START state + return IntroduceeSession.clear(s, sent.getId(), sent.getTimestamp(), + s.getLastRemoteMessageId()); + } + + private IntroduceeSession onRemoteAccept(Transaction txn, + IntroduceeSession s, AcceptMessage m) + throws DbException, FormatException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Broadcast IntroductionResponseReceivedEvent + Contact c = contactManager.getContact(s.getIntroducer().getId(), + identityManager.getLocalAuthor(txn).getId()); + IntroductionResponse request = + new IntroductionResponse(s.getSessionId(), m.getMessageId(), + m.getGroupId(), INTRODUCEE, m.getTimestamp(), false, + false, false, false, s.getRemoteAuthor().getName(), + true); + IntroductionResponseReceivedEvent e = + new IntroductionResponseReceivedEvent(c.getId(), request); + txn.attach(e); + + // Determine next state + IntroduceeState state = + s.getState() == AWAIT_RESPONSES ? REMOTE_ACCEPTED : AWAIT_AUTH; + + if (state == AWAIT_AUTH) { + // Move to the AWAIT_AUTH state and send own auth message + return onLocalAuth(txn, + IntroduceeSession.addRemoteAccept(s, AWAIT_AUTH, m)); + } + // Move to the REMOTE_ACCEPTED state + return IntroduceeSession.addRemoteAccept(s, state, m); + } + + private IntroduceeSession onRemoteDecline(Transaction txn, + IntroduceeSession s, DeclineMessage m) throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Move back to START state + return IntroduceeSession + .clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(), + m.getMessageId()); + } + + private IntroduceeSession onLocalAuth(Transaction txn, IntroduceeSession s) + throws DbException { + boolean alice = isAlice(txn, s); + byte[] mac; + byte[] signature; + SecretKey masterKey; + try { + masterKey = crypto.deriveMasterKey(s, alice); + SecretKey macKey = crypto.deriveMacKey(masterKey, alice); + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + mac = crypto.mac(macKey, s, localAuthor.getId(), alice); + signature = crypto.sign(macKey, localAuthor.getPrivateKey()); + } catch (GeneralSecurityException e) { + // TODO + return abort(txn, s); + } catch (FormatException e) { + throw new AssertionError(e); + } + if (s.getState() != AWAIT_AUTH) throw new AssertionError(); + Message sent = sendAuthMessage(txn, s, getLocalTimestamp(s), mac, + signature); + return IntroduceeSession.addLocalAuth(s, AWAIT_AUTH, masterKey, sent); + } + + private IntroduceeSession onRemoteAuth(Transaction txn, + IntroduceeSession s, AuthMessage m) + throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + try { + crypto.verifyMac(m.getMac(), s, localAuthor.getId()); + crypto.verifySignature(m.getSignature(), s, localAuthor.getId()); + } catch (GeneralSecurityException e) { + return abort(txn, s); + } + + try { + ContactId c = contactManager + .addContact(txn, s.getRemoteAuthor(), localAuthor.getId(), + false, false); + //noinspection ConstantConditions + transportPropertyManager.addRemoteProperties(txn, c, + s.getRemoteTransportProperties()); + } catch (ContactExistsException e) { + // TODO + } + + long timestamp = + Math.min(s.getAcceptTimestamp(), s.getRemoteAcceptTimestamp()); + if (timestamp == -1) throw new AssertionError(); + + //noinspection ConstantConditions + Map keys = keyManager + .addUnboundKeys(txn, new SecretKey(s.getMasterKey()), timestamp, + isAlice(txn, s)); + + Message sent = sendActivateMessage(txn, s, getLocalTimestamp(s)); + + // Move to AWAIT_ACTIVATE state and clear key material from session + return IntroduceeSession.awaitActivate(s, m, sent, keys); + } + + private IntroduceeSession onRemoteActivate(Transaction txn, + IntroduceeSession s, ActivateMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + Contact c = contactManager.getContact(txn, s.getRemoteAuthor().getId(), + identityManager.getLocalAuthor(txn).getId()); + keyManager.bindKeys(txn, c.getId(), s.getTransportKeys()); + keyManager.activateKeys(txn, s.getTransportKeys()); + + // TODO remove when concept of inactive contacts is removed + contactManager.setContactActive(txn, c.getId(), true); + + // Broadcast IntroductionSucceededEvent + IntroductionSucceededEvent e = new IntroductionSucceededEvent(c); + txn.attach(e); + + // Move back to START state + return IntroduceeSession + .clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(), + m.getMessageId()); + } + + private IntroduceeSession onRemoteAbort(Transaction txn, + IntroduceeSession s, AbortMessage m) + throws DbException { + // Mark the request message unavailable to answer + MessageId requestId = s.getLastRemoteMessageId(); + if (requestId == null) throw new IllegalStateException(); + markRequestUnavailableToAnswer(txn, requestId); + + // Reset the session back to initial state + return IntroduceeSession + .clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(), + m.getMessageId()); + } + + private IntroduceeSession abort(Transaction txn, IntroduceeSession s) + throws DbException { + // Mark the request message unavailable to answer + MessageId requestId = s.getLastRemoteMessageId(); + if (requestId == null) throw new IllegalStateException(); + markRequestUnavailableToAnswer(txn, requestId); + + // Send an ABORT message + Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s)); + + // Reset the session back to initial state + return IntroduceeSession.clear(s, sent.getId(), sent.getTimestamp(), + s.getLastRemoteMessageId()); + } + + private boolean isInvalidDependency(IntroduceeSession s, + @Nullable MessageId dependency) { + return isInvalidDependency(s.getLastRemoteMessageId(), dependency); + } + + private long getLocalTimestamp(IntroduceeSession s) { + return getLocalTimestamp(s.getLocalTimestamp(), + s.getRequestTimestamp()); + } + + private boolean isAlice(Transaction txn, IntroduceeSession s) + throws DbException { + Author localAuthor = identityManager.getLocalAuthor(txn); + return crypto.isAlice(localAuthor.getId(), s.getRemoteAuthor().getId()); + } + + private void addSessionId(Transaction txn, MessageId m, SessionId sessionId) + throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.addSessionId(meta, sessionId); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java new file mode 100644 index 000000000..a65f18301 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroducerProtocolEngine.java @@ -0,0 +1,462 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.ProtocolStateException; +import org.briarproject.briar.api.introduction2.IntroductionResponse; +import org.briarproject.briar.api.introduction2.event.IntroductionResponseReceivedEvent; +import org.briarproject.briar.introduction2.IntroducerSession.Introducee; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.api.introduction2.Role.INTRODUCER; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATES; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATE_A; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATE_B; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTHS; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTH_A; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTH_B; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSES; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSE_A; +import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSE_B; +import static org.briarproject.briar.introduction2.IntroducerState.START; + +@Immutable +@NotNullByDefault +class IntroducerProtocolEngine + extends AbstractProtocolEngine { + + @Inject + IntroducerProtocolEngine( + DatabaseComponent db, + ClientHelper clientHelper, + ContactManager contactManager, + ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, + IdentityManager identityManager, + MessageParser messageParser, + MessageEncoder messageEncoder, + Clock clock) { + super(db, clientHelper, contactManager, contactGroupFactory, + messageTracker, identityManager, messageParser, messageEncoder, + clock); + } + + @Override + public IntroducerSession onRequestAction(Transaction txn, + IntroducerSession s, @Nullable String message, long timestamp) + throws DbException { + switch (s.getState()) { + case START: + return onLocalRequest(txn, s, message, timestamp); + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onAcceptAction(Transaction txn, + IntroducerSession s, long timestamp) { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public IntroducerSession onDeclineAction(Transaction txn, + IntroducerSession s, long timestamp) { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public IntroducerSession onRequestMessage(Transaction txn, + IntroducerSession s, RequestMessage m) + throws DbException, FormatException { + return abort(txn, s); // Invalid in this role + } + + @Override + public IntroducerSession onAcceptMessage(Transaction txn, + IntroducerSession s, AcceptMessage m) + throws DbException, FormatException { + switch (s.getState()) { + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + return onRemoteAccept(txn, s, m); + case START: + // TODO check and update lastRemoteMsgId? + return s; // Ignored in this state + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onDeclineMessage(Transaction txn, + IntroducerSession s, DeclineMessage m) + throws DbException, FormatException { + switch (s.getState()) { + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + return onRemoteDecline(txn, s, m); + case START: + // TODO check and update lastRemoteMsgId? + return s; // Ignored in this state + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onAuthMessage(Transaction txn, IntroducerSession s, + AuthMessage m) throws DbException, FormatException { + switch (s.getState()) { + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + return onRemoteAuth(txn, s, m); + case START: + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onActivateMessage(Transaction txn, + IntroducerSession s, ActivateMessage m) + throws DbException, FormatException { + switch (s.getState()) { + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return onRemoteActivate(txn, s, m); + case START: + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onAbortMessage(Transaction txn, + IntroducerSession s, AbortMessage m) + throws DbException, FormatException { + return onRemoteAbort(txn, s, m); + } + + private IntroducerSession onLocalRequest(Transaction txn, + IntroducerSession s, + @Nullable String message, long timestamp) throws DbException { + // Send REQUEST messages + long localTimestamp = + Math.max(timestamp, getLocalTimestamp(s, s.getIntroducee1())); + Message sent1 = sendRequestMessage(txn, s.getIntroducee1(), + localTimestamp, s.getIntroducee2().author, message + ); + Message sent2 = sendRequestMessage(txn, s.getIntroducee2(), + localTimestamp, s.getIntroducee1().author, message + ); + // Track the messages + messageTracker.trackOutgoingMessage(txn, sent1); + messageTracker.trackOutgoingMessage(txn, sent2); + // Move to the AWAIT_RESPONSES state + Introducee introducee1 = new Introducee(s.getIntroducee1(), sent1); + Introducee introducee2 = new Introducee(s.getIntroducee2(), sent2); + return new IntroducerSession(s.getSessionId(), AWAIT_RESPONSES, + localTimestamp, introducee1, introducee2); + } + + private IntroducerSession onRemoteAccept(Transaction txn, + IntroducerSession s, AcceptMessage m) + throws DbException, FormatException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Forward ACCEPT message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = + sendAcceptMessage(txn, i, timestamp, m.getEphemeralPublicKey(), + m.getAcceptTimestamp(), m.getTransportProperties(), + false); + + // Move to the next state + IntroducerState state = AWAIT_AUTHS; + Introducee introducee1, introducee2; + Contact c; + if (i.equals(s.getIntroducee1())) { + if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A; + introducee1 = new Introducee(s.getIntroducee1(), sent); + introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId()); + c = contactManager.getContact(s.getIntroducee2().author.getId(), + identityManager.getLocalAuthor(txn).getId()); + } else if (i.equals(s.getIntroducee2())) { + if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B; + introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId()); + introducee2 = new Introducee(s.getIntroducee2(), sent); + c = contactManager.getContact(s.getIntroducee1().author.getId(), + identityManager.getLocalAuthor(txn).getId()); + } else throw new AssertionError(); + + // Broadcast IntroductionResponseReceivedEvent + IntroductionResponse request = + new IntroductionResponse(s.getSessionId(), m.getMessageId(), + m.getGroupId(), INTRODUCER, m.getTimestamp(), false, + false, false, false, c.getAuthor().getName(), true); + IntroductionResponseReceivedEvent e = + new IntroductionResponseReceivedEvent(c.getId(), request); + txn.attach(e); + + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introducee1, introducee2); + } + + private IntroducerSession onRemoteDecline(Transaction txn, + IntroducerSession s, DeclineMessage m) + throws DbException, FormatException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Forward DECLINE message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendDeclineMessage(txn, i, timestamp, false); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + + // Move to the START state + Introducee introducee1, introducee2; + Contact c; + if (i.equals(s.getIntroducee1())) { + introducee1 = new Introducee(s.getIntroducee1(), sent); + introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId()); + c = contactManager.getContact(s.getIntroducee2().author.getId(), + identityManager.getLocalAuthor(txn).getId()); + } else if (i.equals(s.getIntroducee2())) { + introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId()); + introducee2 = new Introducee(s.getIntroducee2(), sent); + c = contactManager.getContact(s.getIntroducee2().author.getId(), + identityManager.getLocalAuthor(txn).getId()); + } else throw new AssertionError(); + + // Broadcast IntroductionResponseReceivedEvent + IntroductionResponse request = + new IntroductionResponse(s.getSessionId(), m.getMessageId(), + m.getGroupId(), INTRODUCER, m.getTimestamp(), false, + false, false, false, c.getAuthor().getName(), false); + IntroductionResponseReceivedEvent e = + new IntroductionResponseReceivedEvent(c.getId(), request); + txn.attach(e); + + return new IntroducerSession(s.getSessionId(), START, + s.getRequestTimestamp(), introducee1, introducee2); + } + + private IntroducerSession onRemoteAuth(Transaction txn, + IntroducerSession s, AuthMessage m) + throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + + // Forward AUTH message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendAuthMessage(txn, i, timestamp, m.getMac(), + m.getSignature()); + + // Move to the next state + IntroducerState state = AWAIT_ACTIVATES; + Introducee introducee1, introducee2; + if (i.equals(s.getIntroducee1())) { + if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_A; + introducee1 = new Introducee(s.getIntroducee1(), sent); + introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId()); + } else if (i.equals(s.getIntroducee2())) { + if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_B; + introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId()); + introducee2 = new Introducee(s.getIntroducee2(), sent); + } else throw new AssertionError(); + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introducee1, introducee2); + } + + private IntroducerSession onRemoteActivate(Transaction txn, + IntroducerSession s, ActivateMessage m) + throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + + // Forward AUTH message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendActivateMessage(txn, i, timestamp); + + // Move to the next state + IntroducerState state = START; + Introducee introducee1, introducee2; + if (i.equals(s.getIntroducee1())) { + if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_A; + introducee1 = new Introducee(s.getIntroducee1(), sent); + introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId()); + } else if (i.equals(s.getIntroducee2())) { + if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_B; + introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId()); + introducee2 = new Introducee(s.getIntroducee2(), sent); + } else throw new AssertionError(); + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introducee1, introducee2); + } + + private IntroducerSession onRemoteAbort(Transaction txn, + IntroducerSession s, AbortMessage m) + throws DbException, FormatException { + // Mark any REQUEST messages in the session unavailable to answer + markRequestsUnavailableToAnswer(txn, s); + + // Forward ABORT message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendAbortMessage(txn, i, timestamp); + + // Reset the session back to initial state + Introducee introducee1, introducee2; + if (i.equals(s.getIntroducee1())) { + introducee1 = new Introducee(s.getIntroducee1(), sent); + introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId()); + } else if (i.equals(s.getIntroducee2())) { + introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId()); + introducee2 = new Introducee(s.getIntroducee2(), sent); + } else throw new AssertionError(); + return new IntroducerSession(s.getSessionId(), START, + s.getRequestTimestamp(), introducee1, introducee2); + } + + private IntroducerSession abort(Transaction txn, + IntroducerSession s) throws DbException, FormatException { + // Mark any REQUEST messages in the session unavailable to answer + markRequestsUnavailableToAnswer(txn, s); + // Send an ABORT message to both introducees + long timestamp1 = getLocalTimestamp(s, s.getIntroducee1()); + Message sent1 = sendAbortMessage(txn, s.getIntroducee1(), timestamp1); + long timestamp2 = getLocalTimestamp(s, s.getIntroducee2()); + Message sent2 = sendAbortMessage(txn, s.getIntroducee2(), timestamp2); + // Reset the session back to initial state + Introducee introducee1 = new Introducee(s.getIntroducee1(), sent1); + Introducee introducee2 = new Introducee(s.getIntroducee2(), sent2); + return new IntroducerSession(s.getSessionId(), START, + s.getRequestTimestamp(), introducee1, introducee2); + } + + private void markRequestsUnavailableToAnswer(Transaction txn, Session s) + throws DbException, FormatException { + BdfDictionary query = messageParser + .getInvitesAvailableToAnswerQuery(s.getSessionId()); + Map results = getSessions(txn, query); + for (MessageId m : results.keySet()) + markRequestUnavailableToAnswer(txn, m); + } + + private Introducee getIntroducee(IntroducerSession s, GroupId g) { + if (s.getIntroducee1().groupId.equals(g)) return s.getIntroducee1(); + else if (s.getIntroducee2().groupId.equals(g)) + return s.getIntroducee2(); + else throw new AssertionError(); + } + + private Introducee getOtherIntroducee(IntroducerSession s, GroupId g) { + if (s.getIntroducee1().groupId.equals(g)) return s.getIntroducee2(); + else if (s.getIntroducee2().groupId.equals(g)) + return s.getIntroducee1(); + else throw new AssertionError(); + } + + private boolean isInvalidDependency(IntroducerSession session, + GroupId contactGroupId, @Nullable MessageId dependency) { + MessageId expected = + getIntroducee(session, contactGroupId).lastRemoteMessageId; + return isInvalidDependency(expected, dependency); + } + + private long getLocalTimestamp(IntroducerSession s, PeerSession p) { + return getLocalTimestamp(p.getLocalTimestamp(), + s.getRequestTimestamp()); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java index ea5734468..634c0fff1 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionConstants.java @@ -2,6 +2,9 @@ package org.briarproject.briar.introduction2; interface IntroductionConstants { + // Group metadata keys + String GROUP_KEY_CONTACT_ID = "contactId"; + // Message metadata keys String MSG_KEY_MESSAGE_TYPE = "messageType"; String MSG_KEY_SESSION_ID = "sessionId"; diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java new file mode 100644 index 000000000..55a6e552a --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/IntroductionManagerImpl.java @@ -0,0 +1,459 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager.ContactHook; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.data.MetadataParser; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Metadata; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.Client; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.sync.MessageStatus; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction2.IntroductionManager; +import org.briarproject.briar.api.introduction2.IntroductionMessage; +import org.briarproject.briar.api.introduction2.IntroductionRequest; +import org.briarproject.briar.api.introduction2.IntroductionResponse; +import org.briarproject.briar.api.introduction2.Role; +import org.briarproject.briar.client.ConversationClientImpl; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; +import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE; +import static org.briarproject.briar.api.introduction2.Role.INTRODUCER; +import static org.briarproject.briar.introduction2.IntroductionConstants.GROUP_KEY_CONTACT_ID; +import static org.briarproject.briar.introduction2.MessageType.ABORT; +import static org.briarproject.briar.introduction2.MessageType.ACCEPT; +import static org.briarproject.briar.introduction2.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction2.MessageType.AUTH; +import static org.briarproject.briar.introduction2.MessageType.DECLINE; +import static org.briarproject.briar.introduction2.MessageType.REQUEST; + +@Immutable +@NotNullByDefault +class IntroductionManagerImpl extends ConversationClientImpl + implements IntroductionManager, Client, ContactHook { + + private final ContactGroupFactory contactGroupFactory; + private final MessageParser messageParser; + private final SessionEncoder sessionEncoder; + private final SessionParser sessionParser; + private final IntroducerProtocolEngine introducerEngine; + private final IntroduceeProtocolEngine introduceeEngine; + private final IntroductionCrypto crypto; + private final IdentityManager identityManager; + + @Inject + IntroductionManagerImpl( + DatabaseComponent db, + ClientHelper clientHelper, + MetadataParser metadataParser, + MessageTracker messageTracker, + ContactGroupFactory contactGroupFactory, + MessageParser messageParser, + SessionEncoder sessionEncoder, + SessionParser sessionParser, + IntroducerProtocolEngine introducerEngine, + IntroduceeProtocolEngine introduceeEngine, + IntroductionCrypto crypto, + IdentityManager identityManager) { + super(db, clientHelper, metadataParser, messageTracker); + this.contactGroupFactory = contactGroupFactory; + this.messageParser = messageParser; + this.sessionEncoder = sessionEncoder; + this.sessionParser = sessionParser; + this.introducerEngine = introducerEngine; + this.introduceeEngine = introduceeEngine; + this.crypto = crypto; + this.identityManager = identityManager; + } + + @Override + public void createLocalState(Transaction txn) throws DbException { + // Create a local group to store protocol sessions + Group localGroup = getLocalGroup(); + if (db.containsGroup(txn, localGroup.getId())) return; + db.addGroup(txn, localGroup); + // Set up groups for communication with any pre-existing contacts + for (Contact c : db.getContacts(txn)) addingContact(txn, c); + } + + @Override + // TODO adapt to use upcoming ClientVersioning client + public void addingContact(Transaction txn, Contact c) throws DbException { + // Create a group to share with the contact + Group g = getContactGroup(c); + // Return if we've already set things up for this contact + if (db.containsGroup(txn, g.getId())) return; + // Store the group and share it with the contact + db.addGroup(txn, g); + db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); + // Attach the contact ID to the group + BdfDictionary meta = new BdfDictionary(); + meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt()); + try { + clientHelper.mergeGroupMetadata(txn, g.getId(), meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + @Override + public void removingContact(Transaction txn, Contact c) throws DbException { + // Remove the contact group (all messages will be removed with it) + db.removeGroup(txn, getContactGroup(c)); + // TODO abort other sessions the contact is involved in + } + + @Override + public Group getContactGroup(Contact c) { + return contactGroupFactory + .createContactGroup(CLIENT_ID, CLIENT_VERSION, c); + } + + @Override + protected boolean incomingMessage(Transaction txn, Message m, BdfList body, + BdfDictionary bdfMeta) throws DbException, FormatException { + // Parse the metadata + MessageMetadata meta = messageParser.parseMetadata(bdfMeta); + // Look up the session, if there is one + SessionId sessionId = meta.getSessionId(); + IntroduceeSession newIntroduceeSession = null; + if (sessionId == null) { + if (meta.getMessageType() != REQUEST) throw new AssertionError(); + newIntroduceeSession = createNewIntroduceeSession(txn, m, body); + sessionId = newIntroduceeSession.getSessionId(); + } + StoredSession ss = getSession(txn, sessionId); + // Handle the message + Session session; + MessageId storageId; + if (ss == null) { + if (meta.getMessageType() != REQUEST) throw new FormatException(); + if (newIntroduceeSession == null) throw new AssertionError(); + storageId = createStorageId(txn); + session = handleMessage(txn, m, body, meta.getMessageType(), + newIntroduceeSession, introduceeEngine); + } else { + storageId = ss.storageId; + Role role = sessionParser.getRole(ss.bdfSession); + if (role == INTRODUCER) { + session = handleMessage(txn, m, body, meta.getMessageType(), + sessionParser.parseIntroducerSession(ss.bdfSession), + introducerEngine); + } else if (role == INTRODUCEE) { + session = handleMessage(txn, m, body, meta.getMessageType(), + sessionParser.parseIntroduceeSession(m.getGroupId(), + ss.bdfSession), introduceeEngine); + } else throw new AssertionError(); + } + // Store the updated session + storeSession(txn, storageId, session); + return false; + } + + private IntroduceeSession createNewIntroduceeSession(Transaction txn, + Message m, BdfList body) throws DbException, FormatException { + ContactId introducerId = getContactId(txn, m.getGroupId()); + Author introducer = db.getContact(txn, introducerId).getAuthor(); + Author alice = identityManager.getLocalAuthor(txn); + Author bob = messageParser.parseRequestMessage(m, body).getAuthor(); + SessionId sessionId = crypto.getSessionId(introducer, alice, bob); + return IntroduceeSession + .getInitial(m.getGroupId(), sessionId, introducer, bob); + } + + private S handleMessage(Transaction txn, Message m, + BdfList body, MessageType type, S session, ProtocolEngine engine) + throws DbException, FormatException { + if (type == REQUEST) { + RequestMessage request = messageParser.parseRequestMessage(m, body); + return engine.onRequestMessage(txn, session, request); + } else if (type == ACCEPT) { + AcceptMessage accept = messageParser.parseAcceptMessage(m, body); + return engine.onAcceptMessage(txn, session, accept); + } else if (type == DECLINE) { + DeclineMessage decline = messageParser.parseDeclineMessage(m, body); + return engine.onDeclineMessage(txn, session, decline); + } else if (type == AUTH) { + AuthMessage auth = messageParser.parseAuthMessage(m, body); + return engine.onAuthMessage(txn, session, auth); + } else if (type == ACTIVATE) { + ActivateMessage activate = + messageParser.parseActivateMessage(m, body); + return engine.onActivateMessage(txn, session, activate); + } else if (type == ABORT) { + AbortMessage abort = messageParser.parseAbortMessage(m, body); + return engine.onAbortMessage(txn, session, abort); + } else { + throw new AssertionError(); + } + } + + @Nullable + private StoredSession getSession(Transaction txn, + @Nullable SessionId sessionId) throws DbException, FormatException { + if (sessionId == null) return null; + BdfDictionary query = sessionParser.getSessionQuery(sessionId); + Map results = clientHelper + .getMessageMetadataAsDictionary(txn, getLocalGroup().getId(), + query); + if (results.size() > 1) throw new DbException(); + if (results.isEmpty()) return null; + return new StoredSession(results.keySet().iterator().next(), + results.values().iterator().next()); + } + + private ContactId getContactId(Transaction txn, GroupId contactGroupId) + throws DbException, FormatException { + BdfDictionary meta = + clientHelper.getGroupMetadataAsDictionary(txn, contactGroupId); + return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue()); + } + + private MessageId createStorageId(Transaction txn) throws DbException { + Message m = clientHelper + .createMessageForStoringMetadata(getLocalGroup().getId()); + db.addLocalMessage(txn, m, new Metadata(), false); + return m.getId(); + } + + private void storeSession(Transaction txn, MessageId storageId, + Session session) throws DbException, FormatException { + BdfDictionary d; + if (session.getRole() == INTRODUCER) { + d = sessionEncoder + .encodeIntroducerSession((IntroducerSession) session); + } else if (session.getRole() == INTRODUCEE) { + d = sessionEncoder + .encodeIntroduceeSession((IntroduceeSession) session); + } else { + throw new AssertionError(); + } + clientHelper.mergeMessageMetadata(txn, storageId, d); + } + + @Override + public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, + long timestamp) throws DbException { + Transaction txn = db.startTransaction(false); + try { + // Look up the session, if there is one + Author introducer = identityManager.getLocalAuthor(txn); + SessionId sessionId = + crypto.getSessionId(introducer, c1.getAuthor(), + c2.getAuthor()); + StoredSession ss = getSession(txn, sessionId); + // Create or parse the session + IntroducerSession session; + MessageId storageId; + if (ss == null) { + // This is the first request - create a new session + GroupId groupId1 = getContactGroup(c1).getId(); + GroupId groupId2 = getContactGroup(c2).getId(); + session = new IntroducerSession(sessionId, groupId1, + c1.getAuthor(), groupId2, c2.getAuthor()); + storageId = createStorageId(txn); + } else { + // An earlier request exists, so we already have a session + session = sessionParser.parseIntroducerSession(ss.bdfSession); + storageId = ss.storageId; + } + // Handle the request action + session = introducerEngine + .onRequestAction(txn, session, msg, timestamp); + // Store the updated session + storeSession(txn, storageId, session); + db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + @Override + public void acceptIntroduction(ContactId contactId, SessionId sessionId, + long timestamp) throws DbException { + respondToRequest(contactId, sessionId, timestamp, true); + } + + @Override + public void declineIntroduction(ContactId contactId, SessionId sessionId, + long timestamp) throws DbException { + respondToRequest(contactId, sessionId, timestamp, false); + } + + private void respondToRequest(ContactId contactId, SessionId sessionId, + long timestamp, boolean accept) throws DbException { + Transaction txn = db.startTransaction(false); + try { + // Look up the session + StoredSession ss = getSession(txn, sessionId); + if (ss == null) throw new IllegalArgumentException(); + // Parse the session + Contact contact = db.getContact(txn, contactId); + GroupId contactGroupId = getContactGroup(contact).getId(); + IntroduceeSession session = sessionParser + .parseIntroduceeSession(contactGroupId, ss.bdfSession); + // Handle the join or leave action + if (accept) { + session = introduceeEngine + .onAcceptAction(txn, session, timestamp); + } else { + session = introduceeEngine + .onDeclineAction(txn, session, timestamp); + } + // Store the updated session + storeSession(txn, ss.storageId, session); + db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + @Override + public Collection getIntroductionMessages(ContactId c) + throws DbException { + List messages; + Transaction txn = db.startTransaction(true); + try { + Contact contact = db.getContact(txn, c); + GroupId contactGroupId = getContactGroup(contact).getId(); + BdfDictionary query = messageParser.getMessagesVisibleInUiQuery(); + Map results = clientHelper + .getMessageMetadataAsDictionary(txn, contactGroupId, query); + messages = new ArrayList<>(results.size()); + for (Map.Entry e : results.entrySet()) { + MessageId m = e.getKey(); + MessageMetadata meta = + messageParser.parseMetadata(e.getValue()); + MessageStatus status = db.getMessageStatus(txn, c, m); + StoredSession ss = getSession(txn, meta.getSessionId()); + if (ss == null) throw new AssertionError(); + MessageType type = meta.getMessageType(); + if (type == REQUEST) { + messages.add( + parseInvitationRequest(txn, contactGroupId, m, + meta, status, ss.bdfSession)); + } else if (type == ACCEPT) { + messages.add( + parseInvitationResponse(txn, contactGroupId, m, + meta, status, ss.bdfSession, true)); + } else if (type == DECLINE) { + messages.add( + parseInvitationResponse(txn, contactGroupId, m, + meta, status, ss.bdfSession, false)); + } + } + db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + return messages; + } + + private IntroductionRequest parseInvitationRequest(Transaction txn, + GroupId contactGroupId, MessageId m, MessageMetadata meta, + MessageStatus status, BdfDictionary bdfSession) + throws DbException, FormatException { + Role role = sessionParser.getRole(bdfSession); + SessionId sessionId; + Author author; + if (role == INTRODUCER) { + IntroducerSession session = + sessionParser.parseIntroducerSession(bdfSession); + sessionId = session.getSessionId(); + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + if (localAuthor.equals(session.getIntroducee1().author)) { + author = session.getIntroducee2().author; + } else { + author = session.getIntroducee1().author; + } + } else if (role == INTRODUCEE) { + IntroduceeSession session = sessionParser + .parseIntroduceeSession(contactGroupId, bdfSession); + sessionId = session.getSessionId(); + author = session.getRemoteAuthor(); + } else throw new AssertionError(); + String message = ""; // TODO + boolean contactExists = false; // TODO + + return new IntroductionRequest(sessionId, m, contactGroupId, + role, meta.getTimestamp(), meta.isLocal(), + status.isSent(), status.isSeen(), meta.isRead(), + author.getName(), false, message, !meta.isAvailableToAnswer(), + contactExists); + } + + private IntroductionResponse parseInvitationResponse(Transaction txn, + GroupId contactGroupId, MessageId m, MessageMetadata meta, + MessageStatus status, BdfDictionary bdfSession, boolean accept) + throws FormatException, DbException { + Role role = sessionParser.getRole(bdfSession); + SessionId sessionId; + Author author; + if (role == INTRODUCER) { + IntroducerSession session = + sessionParser.parseIntroducerSession(bdfSession); + sessionId = session.getSessionId(); + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + if (localAuthor.equals(session.getIntroducee1().author)) { + author = session.getIntroducee2().author; + } else { + author = session.getIntroducee1().author; + } + } else if (role == INTRODUCEE) { + IntroduceeSession session = sessionParser + .parseIntroduceeSession(contactGroupId, bdfSession); + sessionId = session.getSessionId(); + author = session.getRemoteAuthor(); + } else throw new AssertionError(); + return new IntroductionResponse(sessionId, m, contactGroupId, + role, meta.getTimestamp(), meta.isLocal(), status.isSent(), + status.isSeen(), meta.isRead(), author.getName(), accept); + } + + private Group getLocalGroup() { + return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION); + } + + private static class StoredSession { + + private final MessageId storageId; + private final BdfDictionary bdfSession; + + private StoredSession(MessageId storageId, BdfDictionary bdfSession) { + this.storageId = storageId; + this.bdfSession = bdfSession; + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java new file mode 100644 index 000000000..083296ca0 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction2/ProtocolEngine.java @@ -0,0 +1,40 @@ +package org.briarproject.briar.introduction2; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; + +@NotNullByDefault +interface ProtocolEngine { + + S onRequestAction(Transaction txn, S session, @Nullable String message, + long timestamp) throws DbException; + + S onAcceptAction(Transaction txn, S session, long timestamp) + throws DbException; + + S onDeclineAction(Transaction txn, S session, long timestamp) + throws DbException; + + S onRequestMessage(Transaction txn, S session, RequestMessage m) + throws DbException, FormatException; + + S onAcceptMessage(Transaction txn, S session, AcceptMessage m) + throws DbException, FormatException; + + S onDeclineMessage(Transaction txn, S session, DeclineMessage m) + throws DbException, FormatException; + + S onAuthMessage(Transaction txn, S session, AuthMessage m) + throws DbException, FormatException; + + S onActivateMessage(Transaction txn, S session, ActivateMessage m) + throws DbException, FormatException; + + S onAbortMessage(Transaction txn, S session, AbortMessage m) + throws DbException, FormatException; + +}