From 51c3528efa475e2fa149281455b40eebdfaf38d4 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 16 Mar 2016 19:09:05 -0300 Subject: [PATCH 1/6] Add log statements to MessageQueueManagerImpl in order to find bug #272 --- .../clients/MessageQueueManagerImpl.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java b/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java index b2503b740..eb7b0fc6a 100644 --- a/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java +++ b/briar-core/src/org/briarproject/clients/MessageQueueManagerImpl.java @@ -21,6 +21,7 @@ import org.briarproject.api.sync.ValidationManager.IncomingMessageHook; import org.briarproject.util.ByteUtils; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map.Entry; @@ -63,9 +64,20 @@ class MessageQueueManagerImpl implements MessageQueueManager { QueueState queueState = loadQueueState(txn, queue.getId()); long queuePosition = queueState.outgoingPosition; queueState.outgoingPosition++; + if (LOG.isLoggable(INFO)) { + LOG.info("Sending message with position " + + queuePosition + " in group " + + queue.getId().hashCode() + " with transaction " + + txn.hashCode()); + } saveQueueState(txn, queue.getId(), queueState); QueueMessage q = queueMessageFactory.createMessage(queue.getId(), timestamp, queuePosition, body); + if (LOG.isLoggable(INFO)) { + LOG.info("First bytes of message: " + Arrays.toString( + Arrays.copyOfRange(q.getRaw(), 0, + QUEUE_MESSAGE_HEADER_LENGTH))); + } db.addLocalMessage(txn, q, queue.getClientId(), meta, true); return q; } @@ -196,7 +208,12 @@ class MessageQueueManagerImpl implements MessageQueueManager { if (LOG.isLoggable(INFO)) { LOG.info("Received message with position " + queuePosition + ", expecting " - + queueState.incomingPosition); + + queueState.incomingPosition + ". Received in group " + + m.getGroupId().hashCode() + " with transaction " + + txn.hashCode()); + LOG.info("First bytes of message: " + Arrays.toString( + Arrays.copyOfRange(m.getRaw(), 0, + QUEUE_MESSAGE_HEADER_LENGTH))); } if (queuePosition < queueState.incomingPosition) { // A message with this queue position has already been seen From e2d64e0a8c0bf2c5283f4c9e49ac95c4c6f34777 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 29 Feb 2016 15:29:18 -0300 Subject: [PATCH 2/6] allow adding contacts within an existing transactions --- .../api/contact/ContactManager.java | 13 +++++++++++-- .../contact/ContactManagerImpl.java | 19 ++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/briar-api/src/org/briarproject/api/contact/ContactManager.java b/briar-api/src/org/briarproject/api/contact/ContactManager.java index ba1d2c663..183378060 100644 --- a/briar-api/src/org/briarproject/api/contact/ContactManager.java +++ b/briar-api/src/org/briarproject/api/contact/ContactManager.java @@ -16,12 +16,21 @@ public interface ContactManager { /** Registers a hook to be called whenever a contact is removed. */ void registerRemoveContactHook(RemoveContactHook hook); + /** + * Stores a contact within the given transaction associated with the given + * local and remote pseudonyms, and returns an ID for the contact. + */ + ContactId addContact(Transaction txn, Author remote, AuthorId local, + SecretKey master, long timestamp, boolean alice, boolean active) + throws DbException; + /** * Stores a contact associated with the given local and remote pseudonyms, * and returns an ID for the contact. */ - ContactId addContact(Author remote, AuthorId local, SecretKey master, - long timestamp, boolean alice, boolean active) throws DbException; + ContactId addContact(Author remote, AuthorId local, + SecretKey master, long timestamp, boolean alice, boolean active) + throws DbException; /** Returns the contact with the given ID. */ Contact getContact(ContactId c) throws DbException; diff --git a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java index 7ff709b70..e04878d43 100644 --- a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java +++ b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java @@ -46,6 +46,18 @@ class ContactManagerImpl implements ContactManager, RemoveIdentityHook { removeHooks.add(hook); } + @Override + public ContactId addContact(Transaction txn, Author remote, AuthorId local, + SecretKey master,long timestamp, boolean alice, boolean active) + throws DbException { + ContactId c = db.addContact(txn, remote, local, active); + keyManager.addContact(txn, c, master, timestamp, alice); + Contact contact = db.getContact(txn, c); + for (AddContactHook hook : addHooks) + hook.addingContact(txn, contact); + return c; + } + @Override public ContactId addContact(Author remote, AuthorId local, SecretKey master, long timestamp, boolean alice, boolean active) @@ -53,11 +65,8 @@ class ContactManagerImpl implements ContactManager, RemoveIdentityHook { ContactId c; Transaction txn = db.startTransaction(false); try { - c = db.addContact(txn, remote, local, active); - keyManager.addContact(txn, c, master, timestamp, alice); - Contact contact = db.getContact(txn, c); - for (AddContactHook hook : addHooks) - hook.addingContact(txn, contact); + c = addContact(txn, remote, local, master, timestamp, alice, + active); txn.setComplete(); } finally { db.endTransaction(txn); From 5bde14c694b9a9547bba3e3ee0dea1a564db984c Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 18 Feb 2016 14:46:43 -0200 Subject: [PATCH 3/6] Add a `contactExists()` method to the `contactManager` This requires exposing the `containsContact()` method to the `DatabaseComponent` and is needed for finding out efficiently whether a contact already exists. --- .../api/contact/ContactManager.java | 9 +++++++++ .../api/db/DatabaseComponent.java | 7 +++++++ .../contact/ContactManagerImpl.java | 20 +++++++++++++++++++ .../db/DatabaseComponentImpl.java | 8 ++++++++ 4 files changed, 44 insertions(+) diff --git a/briar-api/src/org/briarproject/api/contact/ContactManager.java b/briar-api/src/org/briarproject/api/contact/ContactManager.java index 183378060..f18bd1dc3 100644 --- a/briar-api/src/org/briarproject/api/contact/ContactManager.java +++ b/briar-api/src/org/briarproject/api/contact/ContactManager.java @@ -5,6 +5,7 @@ import org.briarproject.api.db.DbException; import org.briarproject.api.db.Transaction; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.LocalAuthor; import java.util.Collection; @@ -44,6 +45,14 @@ public interface ContactManager { /** Marks a contact as active or inactive. */ void setContactActive(ContactId c, boolean active) throws DbException; + /** Return true if a contact with this name and public key already exists */ + boolean contactExists(Transaction txn, AuthorId remoteAuthorID, + AuthorId localAuthorId) throws DbException; + + /** Return true if a contact with this name and public key already exists */ + boolean contactExists(AuthorId remoteAuthorID, AuthorId localAuthorId) + throws DbException; + interface AddContactHook { void addingContact(Transaction txn, Contact c) throws DbException; } diff --git a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java index 2292bc0ba..a360fa98b 100644 --- a/briar-api/src/org/briarproject/api/db/DatabaseComponent.java +++ b/briar-api/src/org/briarproject/api/db/DatabaseComponent.java @@ -162,6 +162,13 @@ public interface DatabaseComponent { Collection getContacts(Transaction txn, AuthorId a) throws DbException; + /** + * Returns true if the database contains the given contact for the given + * local pseudonym. + */ + boolean containsContact(Transaction txn, AuthorId remote, AuthorId local) + throws DbException; + /** * Returns the unique ID for this device. *

diff --git a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java index e04878d43..23ec83880 100644 --- a/briar-core/src/org/briarproject/contact/ContactManagerImpl.java +++ b/briar-core/src/org/briarproject/contact/ContactManagerImpl.java @@ -125,6 +125,26 @@ class ContactManagerImpl implements ContactManager, RemoveIdentityHook { } } + @Override + public boolean contactExists(Transaction txn, AuthorId remoteAuthorID, + AuthorId localAuthorId) throws DbException { + return db.containsContact(txn, remoteAuthorID, localAuthorId); + } + + @Override + public boolean contactExists(AuthorId remoteAuthorID, + AuthorId localAuthorId) throws DbException { + boolean exists = false; + Transaction txn = db.startTransaction(true); + try { + exists = contactExists(txn, remoteAuthorID, localAuthorId); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + return exists; + } + private void removeContact(Transaction txn, ContactId c) throws DbException { Contact contact = db.getContact(txn, c); diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index d00159903..150d0a43a 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -342,6 +342,14 @@ class DatabaseComponentImpl implements DatabaseComponent { return db.getContacts(txn, a); } + public boolean containsContact(Transaction transaction, AuthorId remote, + AuthorId local) throws DbException { + T txn = unbox(transaction); + if (!db.containsLocalAuthor(txn, local)) + throw new NoSuchLocalAuthorException(); + return db.containsContact(txn, remote, local); + } + public DeviceId getDeviceId(Transaction transaction) throws DbException { T txn = unbox(transaction); return db.getDeviceId(txn); From e3459fb0a388ee6999f84040bfa49395c14d6c90 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 29 Feb 2016 13:19:32 -0300 Subject: [PATCH 4/6] Use given transaction when adding remote properties in TransportPropertyManager. --- .../api/properties/TransportPropertyManager.java | 3 ++- .../properties/TransportPropertyManagerImpl.java | 16 +++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java index 9dc655543..445329ba9 100644 --- a/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java +++ b/briar-api/src/org/briarproject/api/properties/TransportPropertyManager.java @@ -4,6 +4,7 @@ import org.briarproject.api.DeviceId; import org.briarproject.api.TransportId; import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; import java.util.Map; @@ -13,7 +14,7 @@ public interface TransportPropertyManager { * Stores the given properties received while adding a contact - they will * be superseded by any properties synced from the contact. */ - void addRemoteProperties(ContactId c, DeviceId dev, + void addRemoteProperties(Transaction txn, ContactId c, DeviceId dev, Map props) throws DbException; /** Returns the local transport properties for all transports. */ diff --git a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java index 9910ff42f..4b10c48ed 100644 --- a/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java +++ b/briar-core/src/org/briarproject/properties/TransportPropertyManagerImpl.java @@ -82,18 +82,12 @@ class TransportPropertyManagerImpl implements TransportPropertyManager, } @Override - public void addRemoteProperties(ContactId c, DeviceId dev, + public void addRemoteProperties(Transaction txn, ContactId c, DeviceId dev, Map props) throws DbException { - Transaction txn = db.startTransaction(false); - try { - Group g = getContactGroup(db.getContact(txn, c)); - for (Entry e : props.entrySet()) { - storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 0, - false, false); - } - txn.setComplete(); - } finally { - db.endTransaction(txn); + Group g = getContactGroup(db.getContact(txn, c)); + for (Entry e : props.entrySet()) { + storeMessage(txn, g.getId(), dev, e.getKey(), e.getValue(), 0, + false, false); } } From f44cb5ff946a53540e13daa66287e5cb395ea765 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 21 Jan 2016 10:10:40 -0200 Subject: [PATCH 5/6] Add an IntroductionManager and Validator This Introduction BSP Client uses its own group to communicate with existing contacts. It uses four types of messages to facilitate introductions: the introduction, the response, the ack and the abort. The protocol logic is encapsulated in two protocol engines, one for the introducer and one for the introducee. The introduction client keeps the local state for each engine, hands messages over to the engines and processes the result and state changes they return. --- .../org/briarproject/api/ProtocolEngine.java | 31 ++ .../api/contact/ContactManager.java | 1 - .../IntroductionRequestReceivedEvent.java | 26 + .../IntroductionResponseReceivedEvent.java | 25 + .../api/event/IntroductionSucceededEvent.java | 16 + .../api/event/MessageValidatedEvent.java | 1 + .../api/introduction/IntroduceeAction.java | 43 ++ .../introduction/IntroduceeProtocolState.java | 76 +++ .../api/introduction/IntroducerAction.java | 46 ++ .../introduction/IntroducerProtocolState.java | 94 ++++ .../introduction/IntroductionConstants.java | 68 +++ .../api/introduction/IntroductionManager.java | 62 +++ .../api/introduction/IntroductionMessage.java | 54 ++ .../api/introduction/IntroductionRequest.java | 36 ++ .../introduction/IntroductionResponse.java | 31 ++ .../api/introduction/SessionId.java | 19 + .../org/briarproject/CoreEagerSingletons.java | 2 + .../src/org/briarproject/CoreModule.java | 4 +- .../introduction/IntroduceeEngine.java | 371 +++++++++++++ .../introduction/IntroduceeManager.java | 406 ++++++++++++++ .../introduction/IntroducerEngine.java | 379 +++++++++++++ .../introduction/IntroducerManager.java | 169 ++++++ .../introduction/IntroductionManagerImpl.java | 509 ++++++++++++++++++ .../introduction/IntroductionModule.java | 56 ++ .../introduction/IntroductionValidator.java | 173 ++++++ .../introduction/MessageEncoder.java | 74 +++ 26 files changed, 2770 insertions(+), 2 deletions(-) create mode 100644 briar-api/src/org/briarproject/api/ProtocolEngine.java create mode 100644 briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java create mode 100644 briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java create mode 100644 briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroducerAction.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroductionManager.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java create mode 100644 briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java create mode 100644 briar-api/src/org/briarproject/api/introduction/SessionId.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroduceeEngine.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroduceeManager.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroducerEngine.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroducerManager.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroductionModule.java create mode 100644 briar-core/src/org/briarproject/introduction/IntroductionValidator.java create mode 100644 briar-core/src/org/briarproject/introduction/MessageEncoder.java diff --git a/briar-api/src/org/briarproject/api/ProtocolEngine.java b/briar-api/src/org/briarproject/api/ProtocolEngine.java new file mode 100644 index 000000000..65d9fe52e --- /dev/null +++ b/briar-api/src/org/briarproject/api/ProtocolEngine.java @@ -0,0 +1,31 @@ +package org.briarproject.api; + +import org.briarproject.api.event.Event; + +import java.util.List; + +public interface ProtocolEngine { + StateUpdate onLocalAction(S localState, A action); + + StateUpdate onMessageReceived(S localState, M received); + + StateUpdate onMessageDelivered(S localState, M delivered); + + class StateUpdate { + public final boolean deleteMessages; + public final boolean deleteState; + public final S localState; + public final List toSend; + public final List toBroadcast; + + public StateUpdate(boolean deleteMessages, boolean deleteState, + S localState, List toSend, List toBroadcast) { + + this.deleteMessages = deleteMessages; + this.deleteState = deleteState; + this.localState = localState; + this.toSend = toSend; + this.toBroadcast = toBroadcast; + } + } +} diff --git a/briar-api/src/org/briarproject/api/contact/ContactManager.java b/briar-api/src/org/briarproject/api/contact/ContactManager.java index f18bd1dc3..a40a2b08c 100644 --- a/briar-api/src/org/briarproject/api/contact/ContactManager.java +++ b/briar-api/src/org/briarproject/api/contact/ContactManager.java @@ -5,7 +5,6 @@ import org.briarproject.api.db.DbException; import org.briarproject.api.db.Transaction; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.AuthorId; -import org.briarproject.api.identity.LocalAuthor; import java.util.Collection; diff --git a/briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java new file mode 100644 index 000000000..58473e08c --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/IntroductionRequestReceivedEvent.java @@ -0,0 +1,26 @@ +package org.briarproject.api.event; + +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.introduction.IntroductionRequest; + +public class IntroductionRequestReceivedEvent extends Event { + + private final ContactId contactId; + private final IntroductionRequest introductionRequest; + + public IntroductionRequestReceivedEvent(ContactId contactId, + IntroductionRequest introductionRequest) { + + this.contactId = contactId; + this.introductionRequest = introductionRequest; + } + + public ContactId getContactId() { + return contactId; + } + + public IntroductionRequest getIntroductionRequest() { + return introductionRequest; + } + +} diff --git a/briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java new file mode 100644 index 000000000..2938c40db --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/IntroductionResponseReceivedEvent.java @@ -0,0 +1,25 @@ +package org.briarproject.api.event; + +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.introduction.IntroductionResponse; + +public class IntroductionResponseReceivedEvent extends Event { + + private final ContactId contactId; + private final IntroductionResponse introductionResponse; + + public IntroductionResponseReceivedEvent(ContactId contactId, + IntroductionResponse introductionResponse) { + + this.contactId = contactId; + this.introductionResponse = introductionResponse; + } + + public ContactId getContactId() { + return contactId; + } + + public IntroductionResponse getIntroductionResponse() { + return introductionResponse; + } +} diff --git a/briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java b/briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java new file mode 100644 index 000000000..dfbf3a319 --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/IntroductionSucceededEvent.java @@ -0,0 +1,16 @@ +package org.briarproject.api.event; + +import org.briarproject.api.contact.Contact; + +public class IntroductionSucceededEvent extends Event { + + private final Contact contact; + + public IntroductionSucceededEvent(Contact contact) { + this.contact = contact; + } + + public Contact getContact() { + return contact; + } +} diff --git a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java index c53a83636..26216a873 100644 --- a/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java +++ b/briar-api/src/org/briarproject/api/event/MessageValidatedEvent.java @@ -1,5 +1,6 @@ package org.briarproject.api.event; +import org.briarproject.api.db.Metadata; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.Message; diff --git a/briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java b/briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java new file mode 100644 index 000000000..db50467f0 --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroduceeAction.java @@ -0,0 +1,43 @@ +package org.briarproject.api.introduction; + +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +public enum IntroduceeAction { + + LOCAL_ACCEPT, + LOCAL_DECLINE, + LOCAL_ABORT, + REMOTE_REQUEST, + REMOTE_ACCEPT, + REMOTE_DECLINE, + REMOTE_ABORT, + ACK; + + public static IntroduceeAction getRemote(int type, boolean accept) { + if (type == TYPE_REQUEST) return REMOTE_REQUEST; + if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT; + if (type == TYPE_RESPONSE) return REMOTE_DECLINE; + if (type == TYPE_ACK) return ACK; + if (type == TYPE_ABORT) return REMOTE_ABORT; + return null; + } + + public static IntroduceeAction getRemote(int type) { + return getRemote(type, true); + } + + public static IntroduceeAction getLocal(int type, boolean accept) { + if (type == TYPE_RESPONSE && accept) return LOCAL_ACCEPT; + if (type == TYPE_RESPONSE) return LOCAL_DECLINE; + if (type == TYPE_ABORT) return LOCAL_ABORT; + return null; + } + + public static IntroduceeAction getLocal(int type) { + return getLocal(type, true); + } + +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java b/briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java new file mode 100644 index 000000000..8b46a0c1f --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroduceeProtocolState.java @@ -0,0 +1,76 @@ +package org.briarproject.api.introduction; + +import static org.briarproject.api.introduction.IntroduceeAction.ACK; +import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ACCEPT; +import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_DECLINE; +import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ACCEPT; +import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_DECLINE; +import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_REQUEST; + +public enum IntroduceeProtocolState { + + ERROR(0), + AWAIT_REQUEST(1) { + @Override + public IntroduceeProtocolState next(IntroduceeAction a) { + if (a == REMOTE_REQUEST) return AWAIT_RESPONSES; + return ERROR; + } + }, + AWAIT_RESPONSES(2) { + @Override + public IntroduceeProtocolState next(IntroduceeAction a) { + if (a == REMOTE_ACCEPT) return AWAIT_LOCAL_RESPONSE; + if (a == REMOTE_DECLINE) return FINISHED; + if (a == LOCAL_ACCEPT) return AWAIT_REMOTE_RESPONSE; + if (a == LOCAL_DECLINE) return FINISHED; + return ERROR; + } + }, + AWAIT_REMOTE_RESPONSE(3) { + @Override + public IntroduceeProtocolState next(IntroduceeAction a) { + if (a == REMOTE_ACCEPT) return AWAIT_ACK; + if (a == REMOTE_DECLINE) return FINISHED; + return ERROR; + } + }, + AWAIT_LOCAL_RESPONSE(4) { + @Override + public IntroduceeProtocolState next(IntroduceeAction a) { + if (a == LOCAL_ACCEPT) return AWAIT_ACK; + if (a == LOCAL_DECLINE) return FINISHED; + return ERROR; + } + }, + AWAIT_ACK(5) { + @Override + public IntroduceeProtocolState next(IntroduceeAction a) { + if (a == ACK) return FINISHED; + return ERROR; + } + }, + FINISHED(6); + + private final int value; + + IntroduceeProtocolState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static IntroduceeProtocolState fromValue(int value) { + for (IntroduceeProtocolState s : values()) { + if (s.value == value) return s; + } + throw new IllegalArgumentException(); + } + + public IntroduceeProtocolState next(IntroduceeAction a) { + return this; + } + +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroducerAction.java b/briar-api/src/org/briarproject/api/introduction/IntroducerAction.java new file mode 100644 index 000000000..d20433876 --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroducerAction.java @@ -0,0 +1,46 @@ +package org.briarproject.api.introduction; + +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +public enum IntroducerAction { + + LOCAL_REQUEST, + LOCAL_ABORT, + REMOTE_ACCEPT_1, + REMOTE_ACCEPT_2, + REMOTE_DECLINE_1, + REMOTE_DECLINE_2, + REMOTE_ABORT, + ACK_1, + ACK_2; + + public static IntroducerAction getLocal(int type) { + if (type == TYPE_REQUEST) return LOCAL_REQUEST; + if (type == TYPE_ABORT) return LOCAL_ABORT; + return null; + } + + public static IntroducerAction getRemote(int type, boolean one, + boolean accept) { + + if (one) { + if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT_1; + if (type == TYPE_RESPONSE) return REMOTE_DECLINE_1; + if (type == TYPE_ACK) return ACK_1; + } else { + if (type == TYPE_RESPONSE && accept) return REMOTE_ACCEPT_2; + if (type == TYPE_RESPONSE) return REMOTE_DECLINE_2; + if (type == TYPE_ACK) return ACK_2; + } + if (type == TYPE_ABORT) return REMOTE_ABORT; + return null; + } + + public static IntroducerAction getRemote(int type, boolean one) { + return getRemote(type, one, true); + } + +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java b/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java new file mode 100644 index 000000000..d132e0d68 --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroducerProtocolState.java @@ -0,0 +1,94 @@ +package org.briarproject.api.introduction; + +import static org.briarproject.api.introduction.IntroducerAction.ACK_1; +import static org.briarproject.api.introduction.IntroducerAction.ACK_2; +import static org.briarproject.api.introduction.IntroducerAction.LOCAL_REQUEST; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_1; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_2; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_1; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_2; + +public enum IntroducerProtocolState { + + ERROR(0), + PREPARE_REQUESTS(1) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == LOCAL_REQUEST) return AWAIT_RESPONSES; + return ERROR; + } + }, + AWAIT_RESPONSES(2) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == REMOTE_ACCEPT_1) return AWAIT_RESPONSE_2; + if (a == REMOTE_ACCEPT_2) return AWAIT_RESPONSE_1; + if (a == REMOTE_DECLINE_1) return FINISHED; + if (a == REMOTE_DECLINE_2) return FINISHED; + return ERROR; + } + }, + AWAIT_RESPONSE_1(3) { + public IntroducerProtocolState next(IntroducerAction a) { + if (a == REMOTE_ACCEPT_1) return AWAIT_ACKS; + if (a == REMOTE_DECLINE_1) return FINISHED; + return ERROR; + } + }, + AWAIT_RESPONSE_2(4) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == REMOTE_ACCEPT_2) return AWAIT_ACKS; + if (a == REMOTE_DECLINE_2) return FINISHED; + return ERROR; + } + }, + AWAIT_ACKS(5) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == ACK_1) return AWAIT_ACK_2; + if (a == ACK_2) return AWAIT_ACK_1; + return ERROR; + } + }, + AWAIT_ACK_1(6) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == ACK_1) return FINISHED; + return ERROR; + } + }, + AWAIT_ACK_2(7) { + @Override + public IntroducerProtocolState next(IntroducerAction a) { + if (a == ACK_2) return FINISHED; + return ERROR; + } + }, + FINISHED(8); + + private final int value; + + IntroducerProtocolState(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static IntroducerProtocolState fromValue(int value) { + for (IntroducerProtocolState s : values()) { + if (s.value == value) return s; + } + throw new IllegalArgumentException(); + } + + public static boolean isOngoing(IntroducerProtocolState state) { + return state != FINISHED && state != ERROR; + } + + public IntroducerProtocolState next(IntroducerAction a) { + return this; + } +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java b/briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java new file mode 100644 index 000000000..64bc30983 --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionConstants.java @@ -0,0 +1,68 @@ +package org.briarproject.api.introduction; + +public interface IntroductionConstants { + + /* Protocol roles */ + int ROLE_INTRODUCER = 0; + int ROLE_INTRODUCEE = 1; + + /* Message types */ + int TYPE_REQUEST = 1; + int TYPE_RESPONSE = 2; + int TYPE_ACK = 3; + int TYPE_ABORT = 4; + + /* Message Constants */ + String TYPE = "type"; + String GROUP_ID = "groupId"; + String SESSION_ID = "sessionId"; + String CONTACT = "contactId"; + String NAME = "name"; + String PUBLIC_KEY = "publicKey"; + String E_PUBLIC_KEY = "ephemeralPublicKey"; + String MSG = "msg"; + String ACCEPT = "accept"; + String TIME = "time"; + String DEVICE_ID = "deviceId"; + String TRANSPORT = "transport"; + String MESSAGE_ID = "messageId"; + String MESSAGE_TIME = "timestamp"; + + /* Introducer Local State Metadata */ + String STATE = "state"; + String ROLE = "role"; + String GROUP_ID_1 = "groupId1"; + String GROUP_ID_2 = "groupId2"; + String CONTACT_1 = "contact1"; + String CONTACT_2 = "contact2"; + String AUTHOR_ID_1 = "authorId1"; + String AUTHOR_ID_2 = "authorId2"; + String CONTACT_ID_1 = "contactId1"; + String CONTACT_ID_2 = "contactId2"; + String RESPONSE_1 = "response1"; + String RESPONSE_2 = "response2"; + String READ = "read"; + + /* Introduction Request Action */ + String PUBLIC_KEY1 = "publicKey1"; + String PUBLIC_KEY2 = "publicKey2"; + + /* Introducee Local State Metadata (without those already defined) */ + String STORAGE_ID = "storageId"; + String INTRODUCER = "introducer"; + String LOCAL_AUTHOR_ID = "localAuthorId"; + String REMOTE_AUTHOR_ID = "remoteAuthorId"; + String OUR_PUBLIC_KEY = "ourEphemeralPublicKey"; + String OUR_PRIVATE_KEY = "ourEphemeralPrivateKey"; + String OUR_TIME = "ourTime"; + String ADDED_CONTACT_ID = "addedContactId"; + String NOT_OUR_RESPONSE = "notOurResponse"; + String EXISTS = "contactExists"; + String ANSWERED = "answered"; + + String TASK = "task"; + int TASK_ADD_CONTACT = 0; + int TASK_ACTIVATE_CONTACT = 1; + int TASK_ABORT = 2; + +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java new file mode 100644 index 000000000..882ccbf11 --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionManager.java @@ -0,0 +1,62 @@ +package org.briarproject.api.introduction; + +import org.briarproject.api.FormatException; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.MessageId; + +import java.util.Collection; + +public interface IntroductionManager { + + /** Returns the unique ID of the introduction client. */ + ClientId getClientId(); + + /** + * sends two initial introduction messages + */ + void makeIntroduction(Contact c1, Contact c2, String msg) + throws DbException, FormatException; + + /** + * Accept an introduction that had been made + */ + void acceptIntroduction(final SessionId sessionId) + throws DbException, FormatException; + + /** + * Decline an introduction that had been made + */ + void declineIntroduction(final SessionId sessionId) + throws DbException, FormatException; + + /** + * Get all introduction messages for the contact with this contactId + */ + Collection getIntroductionMessages(ContactId contactId) + throws DbException; + + /** Marks an introduction message as read or unread. */ + void setReadFlag(MessageId m, boolean read) throws DbException; + + + /** Get the session state for the given session ID */ + BdfDictionary getSessionState(Transaction txn, byte[] sessionId) + throws DbException, FormatException; + + /** Gets the group used for introductions with Contact c */ + Group getIntroductionGroup(Contact c); + + /** Get the local group used to store session states */ + Group getLocalGroup(); + + /** Send an introduction message */ + void sendMessage(Transaction txn, BdfDictionary message) + throws DbException, FormatException; + +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java new file mode 100644 index 000000000..6ac98793f --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionMessage.java @@ -0,0 +1,54 @@ +package org.briarproject.api.introduction; + +import org.briarproject.api.sync.MessageId; + +abstract public class IntroductionMessage { + + private final SessionId sessionId; + private final MessageId messageId; + private final long time; + private final boolean local, sent, seen, read; + + public IntroductionMessage(SessionId sessionId, MessageId messageId, + long time, boolean local, boolean sent, boolean seen, + boolean read) { + + this.sessionId = sessionId; + this.messageId = messageId; + this.time = time; + this.local = local; + this.sent = sent; + this.seen = seen; + this.read = read; + } + + public SessionId getSessionId() { + return sessionId; + } + + public long getTime() { + return time; + } + + public MessageId getMessageId() { + return messageId; + } + + public boolean isLocal() { + return local; + } + + public boolean isSent() { + return sent; + } + + public boolean isSeen() { + return seen; + } + + public boolean isRead() { + return read; + } + +} + diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java new file mode 100644 index 000000000..facd7151f --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionRequest.java @@ -0,0 +1,36 @@ +package org.briarproject.api.introduction; + +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.sync.MessageId; + +public class IntroductionRequest extends IntroductionResponse { + + private final String message; + private final boolean answered, exists; + + public IntroductionRequest(SessionId sessionId, MessageId messageId, + long time, boolean local, boolean sent, boolean seen, boolean read, + AuthorId authorId, String name, boolean accepted, String message, + boolean answered, boolean exists) { + + super(sessionId, messageId, time, local, sent, seen, read, authorId, + name, accepted); + + this.message = message; + this.answered = answered; + this.exists = exists; + } + + public String getMessage() { + return message; + } + + public boolean wasAnswered() { + return answered; + } + + public boolean doesExist() { + return exists; + } + +} diff --git a/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java new file mode 100644 index 000000000..e73065bcc --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/IntroductionResponse.java @@ -0,0 +1,31 @@ +package org.briarproject.api.introduction; + +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.sync.MessageId; + +public class IntroductionResponse extends IntroductionMessage { + + private final AuthorId remoteAuthorId; + private final String name; + private final boolean accepted; + + public IntroductionResponse(SessionId sessionId, MessageId messageId, + long time, boolean local, boolean sent, boolean seen, boolean read, + AuthorId remoteAuthorId, String name, boolean accepted) { + + super(sessionId, messageId, time, local, sent, seen, read); + + this.remoteAuthorId = remoteAuthorId; + this.name = name; + this.accepted = accepted; + } + + public String getName() { + return name; + } + + public boolean wasAccepted() { + return accepted; + } + +} diff --git a/briar-api/src/org/briarproject/api/introduction/SessionId.java b/briar-api/src/org/briarproject/api/introduction/SessionId.java new file mode 100644 index 000000000..d68bf9e03 --- /dev/null +++ b/briar-api/src/org/briarproject/api/introduction/SessionId.java @@ -0,0 +1,19 @@ +package org.briarproject.api.introduction; + +import org.briarproject.api.sync.MessageId; + +/** + * Type-safe wrapper for a byte array that uniquely identifies an + * introduction session. + */ +public class SessionId extends MessageId { + + public SessionId(byte[] id) { + super(id); + } + + @Override + public boolean equals(Object o) { + return o instanceof SessionId && super.equals(o); + } +} diff --git a/briar-core/src/org/briarproject/CoreEagerSingletons.java b/briar-core/src/org/briarproject/CoreEagerSingletons.java index 00d46b1da..275b938a6 100644 --- a/briar-core/src/org/briarproject/CoreEagerSingletons.java +++ b/briar-core/src/org/briarproject/CoreEagerSingletons.java @@ -4,6 +4,7 @@ import org.briarproject.contact.ContactModule; import org.briarproject.crypto.CryptoModule; import org.briarproject.db.DatabaseModule; import org.briarproject.forum.ForumModule; +import org.briarproject.introduction.IntroductionModule; import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.messaging.MessagingModule; import org.briarproject.plugins.PluginsModule; @@ -16,6 +17,7 @@ public interface CoreEagerSingletons { void inject(CryptoModule.EagerSingletons init); void inject(DatabaseModule.EagerSingletons init); void inject(ForumModule.EagerSingletons init); + void inject(IntroductionModule.EagerSingletons init); void inject(LifecycleModule.EagerSingletons init); void inject(MessagingModule.EagerSingletons init); void inject(PluginsModule.EagerSingletons init); diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java index 11d6c87b0..e51b5a3c5 100644 --- a/briar-core/src/org/briarproject/CoreModule.java +++ b/briar-core/src/org/briarproject/CoreModule.java @@ -8,6 +8,7 @@ import org.briarproject.db.DatabaseModule; import org.briarproject.event.EventModule; import org.briarproject.forum.ForumModule; import org.briarproject.identity.IdentityModule; +import org.briarproject.introduction.IntroductionModule; import org.briarproject.invitation.InvitationModule; import org.briarproject.lifecycle.LifecycleModule; import org.briarproject.messaging.MessagingModule; @@ -27,7 +28,7 @@ import dagger.Module; IdentityModule.class, EventModule.class, DataModule.class, ContactModule.class, PropertiesModule.class, TransportModule.class, SyncModule.class, SettingsModule.class, ClientsModule.class, - SystemModule.class, PluginsModule.class}) + SystemModule.class, PluginsModule.class, IntroductionModule.class}) public class CoreModule { public static void initEagerSingletons(CoreEagerSingletons c) { @@ -41,5 +42,6 @@ public class CoreModule { c.inject(new PropertiesModule.EagerSingletons()); c.inject(new SyncModule.EagerSingletons()); c.inject(new TransportModule.EagerSingletons()); + c.inject(new IntroductionModule.EagerSingletons()); } } diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java new file mode 100644 index 000000000..7be318408 --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroduceeEngine.java @@ -0,0 +1,371 @@ +package org.briarproject.introduction; + +import org.briarproject.api.FormatException; +import org.briarproject.api.ProtocolEngine; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.IntroductionRequestReceivedEvent; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.introduction.IntroduceeAction; +import org.briarproject.api.introduction.IntroduceeProtocolState; +import org.briarproject.api.introduction.IntroductionRequest; +import org.briarproject.api.introduction.SessionId; +import org.briarproject.api.sync.MessageId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ABORT; +import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ACCEPT; +import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_DECLINE; +import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ABORT; +import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_ACK; +import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REMOTE_RESPONSE; +import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST; +import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_RESPONSES; +import static org.briarproject.api.introduction.IntroduceeProtocolState.ERROR; +import static org.briarproject.api.introduction.IntroduceeProtocolState.FINISHED; +import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.EXISTS; +import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; +import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; +import static org.briarproject.api.introduction.IntroductionConstants.MSG; +import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; +import static org.briarproject.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; +import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; +import static org.briarproject.api.introduction.IntroductionConstants.STATE; +import static org.briarproject.api.introduction.IntroductionConstants.TASK; +import static org.briarproject.api.introduction.IntroductionConstants.TASK_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT; +import static org.briarproject.api.introduction.IntroductionConstants.TASK_ADD_CONTACT; +import static org.briarproject.api.introduction.IntroductionConstants.TIME; +import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +public class IntroduceeEngine + implements ProtocolEngine { + + private static final Logger LOG = + Logger.getLogger(IntroduceeEngine.class.getName()); + + @Override + public StateUpdate onLocalAction( + BdfDictionary localState, BdfDictionary localAction) { + + try { + IntroduceeProtocolState currentState = + getState(localState.getLong(STATE)); + int type = localAction.getLong(TYPE).intValue(); + IntroduceeAction action; + if (localState.containsKey(ACCEPT)) action = IntroduceeAction + .getLocal(type, localState.getBoolean(ACCEPT)); + else action = IntroduceeAction.getLocal(type); + IntroduceeProtocolState nextState = currentState.next(action); + + if (action == LOCAL_ABORT && currentState != ERROR) { + return abortSession(currentState, localState); + } + + if (nextState == ERROR) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Error: Invalid action in state " + + currentState.name()); + } + if (currentState == ERROR) return noUpdate(localState); + else abortSession(currentState, localState); + } + + if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) { + localState.put(STATE, nextState.getValue()); + localState.put(ANSWERED, true); + List messages = new ArrayList(1); + // create the introduction response message + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, TYPE_RESPONSE); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg.put(ACCEPT, localState.getBoolean(ACCEPT)); + if (localState.getBoolean(ACCEPT)) { + msg.put(TIME, localState.getLong(OUR_TIME)); + msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY)); + msg.put(DEVICE_ID, localAction.getRaw(DEVICE_ID)); + msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT)); + } + messages.add(msg); + logAction(currentState, localState, msg); + + if (nextState == AWAIT_ACK) { + localState.put(TASK, TASK_ADD_CONTACT); + // also send ACK, because we already have the other response + BdfDictionary ack = getAckMessage(localState); + messages.add(ack); + } + List events = Collections.emptyList(); + return new StateUpdate(false, + false, + localState, messages, events); + } else { + throw new IllegalArgumentException(); + } + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public StateUpdate onMessageReceived( + BdfDictionary localState, BdfDictionary msg) { + + try { + IntroduceeProtocolState currentState = + getState(localState.getLong(STATE)); + int type = msg.getLong(TYPE).intValue(); + IntroduceeAction action = IntroduceeAction.getRemote(type); + IntroduceeProtocolState nextState = currentState.next(action); + + logMessageReceived(currentState, nextState, localState, type, msg); + + if (nextState == ERROR) { + if (currentState != ERROR && action != REMOTE_ABORT) { + return abortSession(currentState, localState); + } else { + return noUpdate(localState); + } + } + + // update local session state with next protocol state + localState.put(STATE, nextState.getValue()); + List messages; + List events; + // we received the introduction request + if (currentState == AWAIT_REQUEST) { + // remember the session ID used by the introducer + localState.put(SESSION_ID, msg.getRaw(SESSION_ID)); + + addRequestData(localState, msg); + messages = Collections.emptyList(); + events = Collections.singletonList(getEvent(localState, msg)); + } + // we had the request and now one response came in _OR_ + // we had sent our response already and now received the other one + else if (currentState == AWAIT_RESPONSES || + currentState == AWAIT_REMOTE_RESPONSE) { + // update next state based on message content + action = IntroduceeAction + .getRemote(type, msg.getBoolean(ACCEPT)); + nextState = currentState.next(action); + localState.put(STATE, nextState.getValue()); + + addResponseData(localState, msg); + if (nextState == AWAIT_ACK) { + localState.put(TASK, TASK_ADD_CONTACT); + messages = Collections + .singletonList(getAckMessage(localState)); + } else { + messages = Collections.emptyList(); + } + events = Collections.emptyList(); + } + // we already sent our ACK and now received the other one + else if (currentState == AWAIT_ACK) { + localState.put(TASK, TASK_ACTIVATE_CONTACT); + messages = Collections.emptyList(); + events = Collections.emptyList(); + } + // we are done (probably declined response) and ignore this message + else if (currentState == FINISHED) { + return noUpdate(localState); + } + // this should not happen + else { + throw new IllegalArgumentException(); + } + return new StateUpdate(false, false, + localState, messages, events); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + private void addRequestData(BdfDictionary localState, BdfDictionary msg) + throws FormatException { + + localState.put(NAME, msg.getString(NAME)); + localState.put(PUBLIC_KEY, msg.getRaw(PUBLIC_KEY)); + if (msg.containsKey(MSG)) { + localState.put(MSG, msg.getString(MSG)); + } + } + + private void addResponseData(BdfDictionary localState, BdfDictionary msg) + throws FormatException { + + if (localState.containsKey(ACCEPT)) { + localState.put(ACCEPT, + localState.getBoolean(ACCEPT) && msg.getBoolean(ACCEPT)); + } else { + localState.put(ACCEPT, msg.getBoolean(ACCEPT)); + } + localState.put(NOT_OUR_RESPONSE, msg.getRaw(MESSAGE_ID)); + + if (msg.getBoolean(ACCEPT)) { + localState.put(TIME, msg.getLong(TIME)); + localState.put(E_PUBLIC_KEY, msg.getRaw(E_PUBLIC_KEY)); + localState.put(DEVICE_ID, msg.getRaw(DEVICE_ID)); + localState.put(TRANSPORT, msg.getDictionary(TRANSPORT)); + } + } + + private BdfDictionary getAckMessage(BdfDictionary localState) + throws FormatException { + + BdfDictionary m = new BdfDictionary(); + m.put(TYPE, TYPE_ACK); + m.put(GROUP_ID, localState.getRaw(GROUP_ID)); + m.put(SESSION_ID, localState.getRaw(SESSION_ID)); + + if (LOG.isLoggable(INFO)) { + LOG.info("Sending ACK " + " to " + + localState.getString(INTRODUCER) + " for " + + localState.getString(NAME) + " with session ID " + + Arrays.hashCode(m.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(m.getRaw(GROUP_ID))); + } + return m; + } + + private void logAction(IntroduceeProtocolState state, + BdfDictionary localState, BdfDictionary msg) { + + if (!LOG.isLoggable(INFO)) return; + + try { + LOG.info("Sending " + + (localState.getBoolean(ACCEPT) ? "accept " : "decline ") + + "response in state " + state.name() + + " to " + localState.getString(INTRODUCER) + + " for " + localState.getString(NAME) + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + + getState(localState.getLong(STATE)).name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private void logMessageReceived(IntroduceeProtocolState currentState, + IntroduceeProtocolState nextState, BdfDictionary localState, + int type, BdfDictionary msg) { + + if (!LOG.isLoggable(INFO)) return; + + try { + String t = "unknown"; + if (type == TYPE_REQUEST) t = "Introduction"; + else if (type == TYPE_RESPONSE) t = "Response"; + else if (type == TYPE_ACK) t = "ACK"; + else if (type == TYPE_ABORT) t = "Abort"; + + LOG.info("Received " + t + " in state " + currentState.name() + + " from " + localState.getString(INTRODUCER) + + (localState.containsKey(NAME) ? + " related to " + localState.getString(NAME) : "") + + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + nextState.name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + @Override + public StateUpdate onMessageDelivered( + BdfDictionary localState, BdfDictionary delivered) { + try { + return noUpdate(localState); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } + } + + private IntroduceeProtocolState getState(Long state) { + return IntroduceeProtocolState.fromValue(state.intValue()); + } + + private Event getEvent(BdfDictionary localState, BdfDictionary msg) + throws FormatException { + + ContactId contactId = + new ContactId(localState.getLong(CONTACT_ID_1).intValue()); + AuthorId authorId = new AuthorId(localState.getRaw(REMOTE_AUTHOR_ID)); + + SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); + MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); + long time = msg.getLong(MESSAGE_TIME); + String name = msg.getString(NAME); + String message = msg.getOptionalString(MSG); + boolean exists = localState.getBoolean(EXISTS); + + IntroductionRequest ir = new IntroductionRequest(sessionId, messageId, + time, false, false, false, false, authorId, name, false, + message, false, exists); + return new IntroductionRequestReceivedEvent(contactId, ir); + } + + private StateUpdate abortSession( + IntroduceeProtocolState currentState, BdfDictionary localState) + throws FormatException { + + if (LOG.isLoggable(WARNING)) { + LOG.warning("Aborting protocol session " + + Arrays.hashCode(localState.getRaw(SESSION_ID)) + + " in state " + currentState.name()); + } + + localState.put(STATE, ERROR.getValue()); + localState.put(TASK, TASK_ABORT); + BdfDictionary msg = new BdfDictionary(); + msg.put(TYPE, TYPE_ABORT); + msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); + msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); + List messages = Collections.singletonList(msg); + // TODO inform about protocol abort via new Event? + List events = Collections.emptyList(); + return new StateUpdate(false, false, + localState, messages, events); + } + + private StateUpdate noUpdate( + BdfDictionary localState) throws FormatException { + + return new StateUpdate(false, false, + localState, new ArrayList(0), + new ArrayList(0)); + } + +} diff --git a/briar-core/src/org/briarproject/introduction/IntroduceeManager.java b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java new file mode 100644 index 000000000..2049fba3f --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroduceeManager.java @@ -0,0 +1,406 @@ +package org.briarproject.introduction; + + +import org.briarproject.api.Bytes; +import org.briarproject.api.DeviceId; +import org.briarproject.api.FormatException; +import org.briarproject.api.TransportId; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.crypto.KeyPair; +import org.briarproject.api.crypto.KeyParser; +import org.briarproject.api.crypto.PrivateKey; +import org.briarproject.api.crypto.PublicKey; +import org.briarproject.api.crypto.SecretKey; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.IntroductionSucceededEvent; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.introduction.IntroductionManager; +import org.briarproject.api.introduction.SessionId; +import org.briarproject.api.properties.TransportProperties; +import org.briarproject.api.properties.TransportPropertyManager; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.system.Clock; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.ADDED_CONTACT_ID; +import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.EXISTS; +import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; +import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER; +import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID; +import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; +import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; +import static org.briarproject.api.introduction.IntroductionConstants.STATE; +import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.TASK; +import static org.briarproject.api.introduction.IntroductionConstants.TASK_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT; +import static org.briarproject.api.introduction.IntroductionConstants.TASK_ADD_CONTACT; +import static org.briarproject.api.introduction.IntroductionConstants.TIME; +import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +class IntroduceeManager { + + + private static final Logger LOG = + Logger.getLogger(IntroduceeManager.class.getName()); + + private final DatabaseComponent db; + private final IntroductionManager introductionManager; + private final ClientHelper clientHelper; + private final Clock clock; + private final CryptoComponent cryptoComponent; + private final TransportPropertyManager transportPropertyManager; + private final AuthorFactory authorFactory; + private final ContactManager contactManager; + + IntroduceeManager(DatabaseComponent db, + IntroductionManager introductionManager, ClientHelper clientHelper, + Clock clock, CryptoComponent cryptoComponent, + TransportPropertyManager transportPropertyManager, + AuthorFactory authorFactory, ContactManager contactManager) { + + this.db = db; + this.introductionManager = introductionManager; + this.clientHelper = clientHelper; + this.clock = clock; + this.cryptoComponent = cryptoComponent; + this.transportPropertyManager = transportPropertyManager; + this.authorFactory = authorFactory; + this.contactManager = contactManager; + } + + public BdfDictionary initialize(Transaction txn, GroupId groupId, + BdfDictionary message) throws DbException, FormatException { + + // create local message to keep engine state + long now = clock.currentTimeMillis(); + Bytes salt = new Bytes(new byte[64]); + cryptoComponent.getSecureRandom().nextBytes(salt.getBytes()); + + Message localMsg = clientHelper + .createMessage(introductionManager.getLocalGroup().getId(), now, + BdfList.of(salt)); + MessageId storageId = localMsg.getId(); + + // find out who is introducing us + BdfDictionary gd = + clientHelper.getGroupMetadataAsDictionary(txn, groupId); + ContactId introducerId = + new ContactId(gd.getLong(CONTACT).intValue()); + Contact introducer = db.getContact(txn, introducerId); + + BdfDictionary d = new BdfDictionary(); + d.put(STORAGE_ID, storageId); + d.put(STATE, AWAIT_REQUEST.getValue()); + d.put(ROLE, ROLE_INTRODUCEE); + d.put(GROUP_ID, groupId); + d.put(INTRODUCER, introducer.getAuthor().getName()); + d.put(CONTACT_ID_1, introducer.getId().getInt()); + d.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes()); + d.put(NOT_OUR_RESPONSE, new byte[0]); + d.put(ANSWERED, false); + + // check if the contact we are introduced to does already exist + AuthorId remoteAuthorId = authorFactory + .createAuthor(message.getString(NAME), + message.getRaw(PUBLIC_KEY)).getId(); + boolean exists = contactManager.contactExists(txn, remoteAuthorId, + introducer.getLocalAuthorId()); + d.put(EXISTS, exists); + d.put(REMOTE_AUTHOR_ID, remoteAuthorId); + + // save local state to database + clientHelper.addLocalMessage(txn, localMsg, + introductionManager.getClientId(), d, false); + + return d; + } + + public void incomingMessage(Transaction txn, BdfDictionary state, + BdfDictionary message) throws DbException, FormatException { + + IntroduceeEngine engine = new IntroduceeEngine(); + processStateUpdate(txn, engine.onMessageReceived(state, message)); + } + + public void acceptIntroduction(Transaction txn, + final SessionId sessionId) throws DbException, FormatException { + + BdfDictionary state = + introductionManager.getSessionState(txn, sessionId.getBytes()); + + // get data to connect and derive a shared secret later + long now = clock.currentTimeMillis(); + byte[] deviceId = db.getDeviceId(txn).getBytes(); + KeyPair keyPair = cryptoComponent.generateAgreementKeyPair(); + byte[] publicKey = keyPair.getPublic().getEncoded(); + byte[] privateKey = keyPair.getPrivate().getEncoded(); + Map transportProperties = + transportPropertyManager.getLocalProperties(); + + // update session state for later + state.put(ACCEPT, true); + state.put(OUR_TIME, now); + state.put(OUR_PUBLIC_KEY, publicKey); + state.put(OUR_PRIVATE_KEY, privateKey); + + // define action + BdfDictionary localAction = new BdfDictionary(); + localAction.put(TYPE, TYPE_RESPONSE); + localAction.put(DEVICE_ID, deviceId); + localAction.put(TRANSPORT, + encodeTransportProperties(transportProperties)); + + // start engine and process its state update + IntroduceeEngine engine = new IntroduceeEngine(); + processStateUpdate(txn, + engine.onLocalAction(state, localAction)); + } + + public void declineIntroduction(Transaction txn, final SessionId sessionId) + throws DbException, FormatException { + + BdfDictionary state = + introductionManager.getSessionState(txn, sessionId.getBytes()); + + // update session state + state.put(ACCEPT, false); + + // define action + BdfDictionary localAction = new BdfDictionary(); + localAction.put(TYPE, TYPE_RESPONSE); + + // start engine and process its state update + IntroduceeEngine engine = new IntroduceeEngine(); + processStateUpdate(txn, + engine.onLocalAction(state, localAction)); + } + + private void processStateUpdate(Transaction txn, + IntroduceeEngine.StateUpdate + result) throws DbException, FormatException { + + // perform actions based on new local state + performTasks(txn, result.localState); + + // save new local state + MessageId storageId = + new MessageId(result.localState.getRaw(STORAGE_ID)); + clientHelper.mergeMessageMetadata(txn, storageId, result.localState); + + // send messages + for (BdfDictionary d : result.toSend) { + introductionManager.sendMessage(txn, d); + } + + // broadcast events + for (Event event : result.toBroadcast) { + txn.attach(event); + } + } + + private void performTasks(Transaction txn, BdfDictionary localState) + throws FormatException, DbException { + + if (!localState.containsKey(TASK)) return; + + // remember task and remove it from localState + long task = localState.getLong(TASK); + localState.put(TASK, BdfDictionary.NULL_VALUE); + + + + if (task == TASK_ADD_CONTACT) { + if (localState.getBoolean(EXISTS)) { + // we have this contact already, so do not perform actions + LOG.info("We have this contact already, do not add"); + return; + } + + LOG.info("Adding contact in inactive state"); + + // get all keys + KeyParser keyParser = cryptoComponent.getAgreementKeyParser(); + byte[] publicKeyBytes; + PublicKey publicKey; + PrivateKey privateKey; + try { + publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY); + publicKey = keyParser + .parsePublicKey(publicKeyBytes); + privateKey = keyParser.parsePrivateKey( + localState.getRaw(OUR_PRIVATE_KEY)); + } catch (GeneralSecurityException e) { + if (LOG.isLoggable(WARNING)) { + LOG.log(WARNING, e.toString(), e); + } + // we can not continue without the keys + throw new RuntimeException("Our own ephemeral key is invalid"); + } + KeyPair keyPair = new KeyPair(publicKey, privateKey); + byte[] theirEphemeralKey = localState.getRaw(E_PUBLIC_KEY); + + // figure out who takes which role by comparing public keys + int comp = Bytes.COMPARATOR.compare(new Bytes(publicKeyBytes), + new Bytes(theirEphemeralKey)); + boolean alice = comp < 0; + + // The master secret is derived from the local ephemeral key pair + // and the remote ephemeral public key + SecretKey secretKey; + try { + secretKey = cryptoComponent + .deriveMasterSecret(theirEphemeralKey, keyPair, alice); + } catch (GeneralSecurityException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + // we can not continue without the shared secret + throw new FormatException(); + } + + // The agreed timestamp is the minimum of the peers' timestamps + long ourTime = localState.getLong(OUR_TIME); + long theirTime = localState.getLong(TIME); + long timestamp = Math.min(ourTime, theirTime); + + // Add the contact to the database + AuthorId localAuthorId = + new AuthorId(localState.getRaw(LOCAL_AUTHOR_ID)); + Author remoteAuthor = authorFactory + .createAuthor(localState.getString(NAME), + localState.getRaw(PUBLIC_KEY)); + ContactId contactId = contactManager + .addContact(txn, remoteAuthor, localAuthorId, secretKey, + timestamp, alice, false); + + // Update local state with ContactId, so we know what to activate + localState.put(ADDED_CONTACT_ID, contactId.getInt()); + + // let the transport manager know how to connect to the contact + DeviceId deviceId = new DeviceId(localState.getRaw(DEVICE_ID)); + Map transportProperties = + parseTransportProperties(localState); + transportPropertyManager.addRemoteProperties(txn, contactId, + deviceId, transportProperties); + + // delete the ephemeral private key by overwriting with NULL value + // this ensures future ephemeral keys can not be recovered when + // this device should gets compromised + localState.put(OUR_PRIVATE_KEY, BdfDictionary.NULL_VALUE); + } + + // we sent and received an ACK, so activate contact + if (task == TASK_ACTIVATE_CONTACT) { + if (!localState.getBoolean(EXISTS) && + localState.containsKey(ADDED_CONTACT_ID)) { + + LOG.info("Activating Contact..."); + + ContactId contactId = new ContactId( + localState.getLong(ADDED_CONTACT_ID).intValue()); + + // activate and show contact in contact list + db.setContactActive(txn, contactId, true); + + // broadcast event informing of successful introduction + Contact contact = db.getContact(txn, contactId); + Event event = new IntroductionSucceededEvent(contact); + txn.attach(event); + } else { + LOG.info( + "We must have had this contact already, not activating..."); + } + } + + // we need to abort the protocol, clean up what has been done + if (task == TASK_ABORT) { + if (localState.containsKey(ADDED_CONTACT_ID)) { + LOG.info("Deleting added contact due to abort..."); + ContactId contactId = new ContactId( + localState.getLong(ADDED_CONTACT_ID).intValue()); + contactManager.removeContact(contactId); + } + } + + } + + public void abort(Transaction txn, BdfDictionary state) { + + IntroduceeEngine engine = new IntroduceeEngine(); + BdfDictionary localAction = new BdfDictionary(); + localAction.put(TYPE, TYPE_ABORT); + try { + processStateUpdate(txn, + engine.onLocalAction(state, localAction)); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private BdfDictionary encodeTransportProperties( + Map map) { + + BdfDictionary d = new BdfDictionary(); + for (Map.Entry e : map.entrySet()) { + d.put(e.getKey().getString(), e.getValue()); + } + return d; + } + + private Map parseTransportProperties( + BdfDictionary d) throws FormatException { + + Map tpMap = + new HashMap(); + BdfDictionary tpMapDict = d.getDictionary(TRANSPORT); + for (String key : tpMapDict.keySet()) { + TransportId transportId = new TransportId(key); + TransportProperties transportProperties = new TransportProperties(); + BdfDictionary tpDict = tpMapDict.getDictionary(key); + for (String tkey : tpDict.keySet()) { + transportProperties.put(tkey, tpDict.getString(tkey)); + } + tpMap.put(transportId, transportProperties); + } + return tpMap; + } + +} diff --git a/briar-core/src/org/briarproject/introduction/IntroducerEngine.java b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java new file mode 100644 index 000000000..fb88c28df --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroducerEngine.java @@ -0,0 +1,379 @@ +package org.briarproject.introduction; + +import org.briarproject.api.FormatException; +import org.briarproject.api.ProtocolEngine; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.event.Event; +import org.briarproject.api.event.IntroductionResponseReceivedEvent; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.introduction.IntroducerAction; +import org.briarproject.api.introduction.IntroducerProtocolState; +import org.briarproject.api.introduction.IntroductionResponse; +import org.briarproject.api.introduction.SessionId; +import org.briarproject.api.sync.MessageId; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.introduction.IntroducerAction.LOCAL_ABORT; +import static org.briarproject.api.introduction.IntroducerAction.LOCAL_REQUEST; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_1; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_2; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_1; +import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_2; +import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACKS; +import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACK_1; +import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACK_2; +import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES; +import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_1; +import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_2; +import static org.briarproject.api.introduction.IntroducerProtocolState.ERROR; +import static org.briarproject.api.introduction.IntroducerProtocolState.FINISHED; +import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; +import static org.briarproject.api.introduction.IntroductionConstants.MSG; +import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2; +import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1; +import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2; +import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; +import static org.briarproject.api.introduction.IntroductionConstants.STATE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +public class IntroducerEngine + implements ProtocolEngine { + + private static final Logger LOG = + Logger.getLogger(IntroducerEngine.class.getName()); + + @Override + public StateUpdate onLocalAction( + BdfDictionary localState, BdfDictionary localAction) { + + try { + IntroducerProtocolState currentState = + getState(localState.getLong(STATE)); + int type = localAction.getLong(TYPE).intValue(); + IntroducerAction action = IntroducerAction.getLocal(type); + IntroducerProtocolState nextState = currentState.next(action); + + if (action == LOCAL_ABORT && currentState != ERROR) { + return abortSession(currentState, localState); + } + + if (nextState == ERROR) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Error: Invalid action in state " + + currentState.name()); + } + return noUpdate(localState); + } + + localState.put(STATE, nextState.getValue()); + if (action == LOCAL_REQUEST) { + // create the introduction requests for both contacts + List messages = new ArrayList(2); + BdfDictionary msg1 = new BdfDictionary(); + msg1.put(TYPE, TYPE_REQUEST); + msg1.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); + msg1.put(NAME, localState.getString(CONTACT_2)); + msg1.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY2)); + if (localAction.containsKey(MSG)) { + msg1.put(MSG, localAction.getString(MSG)); + } + messages.add(msg1); + logLocalAction(currentState, localState, msg1); + BdfDictionary msg2 = new BdfDictionary(); + msg2.put(TYPE, TYPE_REQUEST); + msg2.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2)); + msg2.put(NAME, localState.getString(CONTACT_1)); + msg2.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY1)); + if (localAction.containsKey(MSG)) { + msg2.put(MSG, localAction.getString(MSG)); + } + messages.add(msg2); + logLocalAction(currentState, localState, msg2); + + List events = Collections.emptyList(); + return new StateUpdate(false, false, + localState, messages, events); + } else { + throw new IllegalArgumentException("Unknown Local Action"); + } + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public StateUpdate onMessageReceived( + BdfDictionary localState, BdfDictionary msg) { + + try { + IntroducerProtocolState currentState = + getState(localState.getLong(STATE)); + int type = msg.getLong(TYPE).intValue(); + boolean one = isContact1(localState, msg); + IntroducerAction action = IntroducerAction.getRemote(type, one); + IntroducerProtocolState nextState = currentState.next(action); + + logMessageReceived(currentState, nextState, localState, type, msg); + + if (nextState == ERROR) { + if (currentState != ERROR) { + return abortSession(currentState, localState); + } else { + return noUpdate(localState); + } + } + + List messages; + List events; + + // we have sent our requests and just got the 1st or 2nd response + if (currentState == AWAIT_RESPONSES || + currentState == AWAIT_RESPONSE_1 || + currentState == AWAIT_RESPONSE_2) { + // update next state based on message content + action = IntroducerAction + .getRemote(type, one, msg.getBoolean(ACCEPT)); + nextState = currentState.next(action); + localState.put(STATE, nextState.getValue()); + if (one) localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID)); + else localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID)); + + messages = forwardMessage(localState, msg); + events = Collections.singletonList(getEvent(localState, msg)); + } + // we have forwarded both responses and now received the 1st or 2nd ACK + else if (currentState == AWAIT_ACKS || + currentState == AWAIT_ACK_1 || + currentState == AWAIT_ACK_2) { + localState.put(STATE, nextState.getValue()); + messages = forwardMessage(localState, msg); + events = Collections.emptyList(); + } + // we probably received a response while already being FINISHED + else if (currentState == FINISHED) { + // if it was a response store it to be found later + if (action == REMOTE_ACCEPT_1 || action == REMOTE_DECLINE_1) { + localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID)); + messages = Collections.emptyList(); + events = Collections.singletonList(getEvent(localState, msg)); + } else if (action == REMOTE_ACCEPT_2 || + action == REMOTE_DECLINE_2) { + localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID)); + messages = Collections.emptyList(); + events = Collections.singletonList(getEvent(localState, msg)); + } else return noUpdate(localState); + } else { + throw new IllegalArgumentException("Bad state"); + } + return new StateUpdate(false, false, + localState, messages, events); + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + private void logLocalAction(IntroducerProtocolState state, + BdfDictionary localState, BdfDictionary msg) { + + if (!LOG.isLoggable(INFO)) return; + + try { + String to = getMessagePartner(localState, msg); + LOG.info("Sending introduction request in state " + state.name() + + " to " + to + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + + getState(localState.getLong(STATE)).name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private void logMessageReceived(IntroducerProtocolState currentState, + IntroducerProtocolState nextState, + BdfDictionary localState, int type, BdfDictionary msg) { + if (!LOG.isLoggable(INFO)) return; + + try { + String t = "unknown"; + if (type == TYPE_REQUEST) t = "Introduction"; + else if (type == TYPE_RESPONSE) t = "Response"; + else if (type == TYPE_ACK) t = "ACK"; + else if (type == TYPE_ABORT) t = "Abort"; + + String from = getMessagePartner(localState, msg); + String to = getOtherContact(localState, msg); + + LOG.info("Received " + t + " in state " + currentState.name() + " from " + + from + " to " + to + " with session ID " + + Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " + + Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " + + "Moving on to state " + nextState.name() + ); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + + private List forwardMessage(BdfDictionary localState, + BdfDictionary message) throws FormatException { + + // clone the message here, because we still need the original + BdfDictionary msg = (BdfDictionary) message.clone(); + if (isContact1(localState, msg)) { + msg.put(GROUP_ID, localState.getRaw(GROUP_ID_2)); + } else { + msg.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); + } + + if (LOG.isLoggable(INFO)) { + LOG.info("Forwarding message to group " + + Arrays.hashCode(msg.getRaw(GROUP_ID))); + } + + return Collections.singletonList(msg); + } + + @Override + public StateUpdate onMessageDelivered( + BdfDictionary localState, BdfDictionary delivered) { + try { + return noUpdate(localState); + } + catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + return null; + } + } + + private IntroducerProtocolState getState(Long state) { + return IntroducerProtocolState.fromValue(state.intValue()); + } + + private Event getEvent(BdfDictionary localState, BdfDictionary msg) + throws FormatException { + + ContactId contactId = + new ContactId(localState.getLong(CONTACT_ID_1).intValue()); + AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1, new byte[32])); // TODO remove byte[] + if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) { + contactId = + new ContactId(localState.getLong(CONTACT_ID_2).intValue()); + authorId = new AuthorId(localState.getRaw(AUTHOR_ID_2, new byte[32])); // TODO remove byte[] + } + + SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); + MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); + long time = msg.getLong(MESSAGE_TIME); + String name = getOtherContact(localState, msg); + boolean accept = msg.getBoolean(ACCEPT); + + IntroductionResponse ir = + new IntroductionResponse(sessionId, messageId, time, false, + false, false, false, authorId, name, accept); + return new IntroductionResponseReceivedEvent(contactId, ir); + } + + private boolean isContact1(BdfDictionary localState, BdfDictionary msg) + throws FormatException { + + byte[] group = msg.getRaw(GROUP_ID); + byte[] group1 = localState.getRaw(GROUP_ID_1); + byte[] group2 = localState.getRaw(GROUP_ID_2); + + if (Arrays.equals(group, group1)) { + return true; + } else if (Arrays.equals(group, group2)) { + return false; + } else { + throw new FormatException(); + } + } + + private String getMessagePartner(BdfDictionary localState, + BdfDictionary msg) throws FormatException { + + String from = localState.getString(CONTACT_1); + if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) { + from = localState.getString(CONTACT_2); + } + return from; + } + + private String getOtherContact(BdfDictionary localState, BdfDictionary msg) + throws FormatException { + + String to = localState.getString(CONTACT_2); + if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) { + to = localState.getString(CONTACT_1); + } + return to; + } + + private StateUpdate abortSession( + IntroducerProtocolState currentState, BdfDictionary localState) + throws FormatException { + + if (LOG.isLoggable(WARNING)) { + LOG.warning("Aborting protocol session " + + Arrays.hashCode(localState.getRaw(SESSION_ID)) + + " in state " + currentState.name()); + } + + localState.put(STATE, ERROR.getValue()); + List messages = new ArrayList(2); + BdfDictionary msg1 = new BdfDictionary(); + msg1.put(TYPE, TYPE_ABORT); + msg1.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); + messages.add(msg1); + BdfDictionary msg2 = new BdfDictionary(); + msg2.put(TYPE, TYPE_ABORT); + msg2.put(SESSION_ID, localState.getRaw(SESSION_ID)); + msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2)); + messages.add(msg2); + // TODO inform about protocol abort via new Event? + List events = Collections.emptyList(); + return new StateUpdate(false, false, + localState, messages, events); + } + + private StateUpdate noUpdate( + BdfDictionary localState) throws FormatException { + + return new StateUpdate(false, false, + localState, new ArrayList(0), + new ArrayList(0)); + } + +} diff --git a/briar-core/src/org/briarproject/introduction/IntroducerManager.java b/briar-core/src/org/briarproject/introduction/IntroducerManager.java new file mode 100644 index 000000000..1b35fc26c --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroducerManager.java @@ -0,0 +1,169 @@ +package org.briarproject.introduction; + +import org.briarproject.api.Bytes; +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.event.Event; +import org.briarproject.api.introduction.IntroductionManager; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.system.Clock; +import org.briarproject.util.StringUtils; + +import java.io.IOException; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS; +import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.MSG; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER; +import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; +import static org.briarproject.api.introduction.IntroductionConstants.STATE; +import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; + +class IntroducerManager { + + private static final Logger LOG = + Logger.getLogger(IntroducerManager.class.getName()); + + private final IntroductionManager introductionManager; + private final ClientHelper clientHelper; + private final Clock clock; + private final CryptoComponent cryptoComponent; + + IntroducerManager(IntroductionManager introductionManager, + ClientHelper clientHelper, Clock clock, + CryptoComponent cryptoComponent) { + + this.introductionManager = introductionManager; + this.clientHelper = clientHelper; + this.clock = clock; + this.cryptoComponent = cryptoComponent; + } + + public BdfDictionary initialize(Transaction txn, Contact c1, Contact c2) + throws FormatException, DbException { + + // create local message to keep engine state + long now = clock.currentTimeMillis(); + Bytes salt = new Bytes(new byte[64]); + cryptoComponent.getSecureRandom().nextBytes(salt.getBytes()); + + Message m = clientHelper + .createMessage(introductionManager.getLocalGroup().getId(), now, + BdfList.of(salt)); + MessageId sessionId = m.getId(); + + Group g1 = introductionManager.getIntroductionGroup(c1); + Group g2 = introductionManager.getIntroductionGroup(c2); + + BdfDictionary d = new BdfDictionary(); + d.put(SESSION_ID, sessionId); + d.put(STORAGE_ID, sessionId); + d.put(STATE, PREPARE_REQUESTS.getValue()); + d.put(ROLE, ROLE_INTRODUCER); + d.put(GROUP_ID_1, g1.getId()); + d.put(GROUP_ID_2, g2.getId()); + d.put(CONTACT_1, c1.getAuthor().getName()); + d.put(CONTACT_2, c2.getAuthor().getName()); + d.put(CONTACT_ID_1, c1.getId().getInt()); + d.put(CONTACT_ID_2, c2.getId().getInt()); + d.put(AUTHOR_ID_1, c1.getAuthor().getId()); + d.put(AUTHOR_ID_2, c2.getAuthor().getId()); + + // save local state to database + clientHelper.addLocalMessage(txn, m, introductionManager.getClientId(), d, false); + + return d; + } + + public void makeIntroduction(Transaction txn, Contact c1, Contact c2, + String msg) throws DbException, FormatException { + + // TODO check for existing session with those contacts? + // deny new introduction under which conditions? + + // initialize engine state + BdfDictionary localState = initialize(txn, c1, c2); + + // define action + BdfDictionary localAction = new BdfDictionary(); + localAction.put(TYPE, TYPE_REQUEST); + if (!StringUtils.isNullOrEmpty(msg)) { + localAction.put(MSG, msg); + } + localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey()); + localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey()); + + // start engine and process its state update + IntroducerEngine engine = new IntroducerEngine(); + processStateUpdate(txn, + engine.onLocalAction(localState, localAction)); + } + + public void incomingMessage(Transaction txn, BdfDictionary state, + BdfDictionary message) throws DbException, FormatException { + + IntroducerEngine engine = new IntroducerEngine(); + processStateUpdate(txn, + engine.onMessageReceived(state, message)); + } + + private void processStateUpdate(Transaction txn, + IntroducerEngine.StateUpdate + result) throws DbException, FormatException { + + // save new local state + MessageId storageId = new MessageId(result.localState.getRaw(STORAGE_ID)); + clientHelper.mergeMessageMetadata(txn, storageId, result.localState); + + // send messages + for (BdfDictionary d : result.toSend) { + introductionManager.sendMessage(txn, d); + } + + // broadcast events + for (Event event : result.toBroadcast) { + txn.attach(event); + } + } + + public void abort(Transaction txn, BdfDictionary state) { + + IntroducerEngine engine = new IntroducerEngine(); + BdfDictionary localAction = new BdfDictionary(); + localAction.put(TYPE, TYPE_ABORT); + try { + processStateUpdate(txn, + engine.onLocalAction(state, localAction)); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + +} diff --git a/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java new file mode 100644 index 000000000..a01c62b10 --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroductionManagerImpl.java @@ -0,0 +1,509 @@ +package org.briarproject.introduction; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.MessageQueueManager; +import org.briarproject.api.clients.PrivateGroupFactory; +import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.contact.ContactManager.AddContactHook; +import org.briarproject.api.contact.ContactManager.RemoveContactHook; +import org.briarproject.api.crypto.CryptoComponent; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfEntry; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.data.MetadataParser; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Metadata; +import org.briarproject.api.db.NoSuchMessageException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.AuthorFactory; +import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.introduction.IntroducerProtocolState; +import org.briarproject.api.introduction.IntroductionManager; +import org.briarproject.api.introduction.IntroductionMessage; +import org.briarproject.api.introduction.IntroductionRequest; +import org.briarproject.api.introduction.IntroductionResponse; +import org.briarproject.api.introduction.SessionId; +import org.briarproject.api.properties.TransportPropertyManager; +import org.briarproject.api.sync.ClientId; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupFactory; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.sync.MessageStatus; +import org.briarproject.api.system.Clock; +import org.briarproject.clients.BdfIncomingMessageHook; +import org.briarproject.util.StringUtils; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED; +import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1; +import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2; +import static org.briarproject.api.introduction.IntroductionConstants.EXISTS; +import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; +import static org.briarproject.api.introduction.IntroductionConstants.MSG; +import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; +import static org.briarproject.api.introduction.IntroductionConstants.READ; +import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; +import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1; +import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; +import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER; +import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; +import static org.briarproject.api.introduction.IntroductionConstants.STATE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +class IntroductionManagerImpl extends BdfIncomingMessageHook + implements IntroductionManager, AddContactHook, RemoveContactHook { + + static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString( + "23b1897c198a90ae75b976ac023d0f32" + + "80ca67b12f2346b2c23a34f34e2434c3")); + + private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0]; + + private static final Logger LOG = + Logger.getLogger(IntroductionManagerImpl.class.getName()); + + private final DatabaseComponent db; + private final MessageQueueManager messageQueueManager; + private final PrivateGroupFactory privateGroupFactory; + private final MetadataEncoder metadataEncoder; + private final IntroducerManager introducerManager; + private final IntroduceeManager introduceeManager; + private final Group localGroup; + + @Inject + IntroductionManagerImpl(DatabaseComponent db, + MessageQueueManager messageQueueManager, + ClientHelper clientHelper, GroupFactory groupFactory, + PrivateGroupFactory privateGroupFactory, + MetadataEncoder metadataEncoder, MetadataParser metadataParser, + CryptoComponent cryptoComponent, + TransportPropertyManager transportPropertyManager, + AuthorFactory authorFactory, ContactManager contactManager, + Clock clock) { + + super(clientHelper, metadataParser, clock); + this.db = db; + this.messageQueueManager = messageQueueManager; + this.privateGroupFactory = privateGroupFactory; + this.metadataEncoder = metadataEncoder; + this.introducerManager = + new IntroducerManager(this, clientHelper, clock, + cryptoComponent); + this.introduceeManager = + new IntroduceeManager(db, this, clientHelper, clock, + cryptoComponent, transportPropertyManager, + authorFactory, contactManager); + localGroup = + groupFactory.createGroup(CLIENT_ID, LOCAL_GROUP_DESCRIPTOR); + } + + @Override + public ClientId getClientId() { + return CLIENT_ID; + } + + @Override + public void addingContact(Transaction txn, Contact c) throws DbException { + try { + // create an introduction group for sending introduction messages + Group g = getIntroductionGroup(c); + db.addGroup(txn, g); + db.setVisibleToContact(txn, c.getId(), g.getId(), true); + // Attach the contact ID to the group + BdfDictionary gm = new BdfDictionary(); + gm.put(CONTACT, c.getId().getInt()); + clientHelper.mergeGroupMetadata(txn, g.getId(), gm); + } catch (FormatException e) { + throw new RuntimeException(e); + } + } + + @Override + public void removingContact(Transaction txn, Contact c) throws DbException { + // check for open sessions with that contact and abort those + Long id = (long) c.getId().getInt(); + try { + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId()); + for (Map.Entry entry : map.entrySet()) { + BdfDictionary d = entry.getValue(); + long role = d.getLong(ROLE, -1L); + if (role != ROLE_INTRODUCER) continue; + if (d.getLong(CONTACT_ID_1).equals(id) || + d.getLong(CONTACT_ID_2).equals(id)) { + + IntroducerProtocolState state = IntroducerProtocolState + .fromValue(d.getLong(STATE).intValue()); + if (IntroducerProtocolState.isOngoing(state)) { + introducerManager.abort(txn, d); + } + } + + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + + // remove the group (all messages will be removed with it) + // this contact won't get our abort message, but the other will + db.removeGroup(txn, getIntroductionGroup(c)); + } + + /** + * This is called when a new message arrived and is being validated. + * It is the central method where we determine which role we play + * in the introduction protocol and which engine we need to start. + */ + @Override + protected void incomingMessage(Transaction txn, Message m, BdfList body, + BdfDictionary message) throws DbException { + + // add local group for engine states to make sure it exists + db.addGroup(txn, localGroup); + + // Get message data and type + GroupId groupId = m.getGroupId(); + message.put(GROUP_ID, groupId); + long type = message.getLong(TYPE, -1L); + + // we are an introducee, need to initialize new state + if (type == TYPE_REQUEST) { + BdfDictionary state; + try { + state = introduceeManager.initialize(txn, groupId, message); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) { + LOG.warning("Could not initialize introducee state"); + LOG.log(WARNING, e.toString(), e); + } + return; + } + try { + introduceeManager.incomingMessage(txn, state, message); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + introduceeManager.abort(txn, state); + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + introduceeManager.abort(txn, state); + } + } + // our role can be anything + else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) { + BdfDictionary state; + try { + state = getSessionState(txn, + message.getRaw(SESSION_ID, new byte[0])); + } catch (FormatException e) { + LOG.warning("Could not find state for message, deleting..."); + deleteMessage(txn, m.getId()); + return; + } + + long role = state.getLong(ROLE, -1L); + try { + if (role == ROLE_INTRODUCER) { + introducerManager.incomingMessage(txn, state, message); + } else if (role == ROLE_INTRODUCEE) { + introduceeManager.incomingMessage(txn, state, message); + } else { + if(LOG.isLoggable(WARNING)) { + LOG.warning("Unknown role '" + role + + "'. Deleting message..."); + deleteMessage(txn, m.getId()); + } + } + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state); + else introduceeManager.abort(txn, state); + } catch (IOException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state); + else introduceeManager.abort(txn, state); + } + } else { + // the message has been validated, so this should not happen + if(LOG.isLoggable(WARNING)) { + LOG.warning("Unknown message type '" + type + "', deleting..."); + } + } + } + + @Override + public void makeIntroduction(Contact c1, Contact c2, String msg) + throws DbException, FormatException { + + Transaction txn = db.startTransaction(false); + try { + // add local group for session states to make sure it exists + db.addGroup(txn, getLocalGroup()); + introducerManager.makeIntroduction(txn, c1, c2, msg); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + } + + @Override + public void acceptIntroduction(final SessionId sessionId) + throws DbException, FormatException { + + Transaction txn = db.startTransaction(false); + try { + introduceeManager.acceptIntroduction(txn, sessionId); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + } + + @Override + public void declineIntroduction(final SessionId sessionId) + throws DbException, FormatException { + + Transaction txn = db.startTransaction(false); + try { + introduceeManager.declineIntroduction(txn, sessionId); + txn.setComplete(); + } finally { + db.endTransaction(txn); + } + } + + @Override + public Collection getIntroductionMessages( + ContactId contactId) throws DbException { + + Collection list = + new ArrayList(); + + Map metadata; + Collection statuses; + Transaction txn = db.startTransaction(true); + try { + // get messages and their status + GroupId g = + getIntroductionGroup(db.getContact(txn, contactId)).getId(); + metadata = clientHelper.getMessageMetadataAsDictionary(txn, g); + statuses = db.getMessageStatus(txn, contactId, g); + + // turn messages into classes for the UI + Map sessionStates = + new HashMap(); + for (MessageStatus s : statuses) { + MessageId messageId = s.getMessageId(); + BdfDictionary msg = metadata.get(messageId); + if (msg == null) continue; + + try { + long type = msg.getLong(TYPE); + if (type == TYPE_ACK || type == TYPE_ABORT) continue; + + // get session state + SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID)); + BdfDictionary state = sessionStates.get(sessionId); + if (state == null) { + state = getSessionState(txn, sessionId.getBytes()); + } + sessionStates.put(sessionId, state); + + boolean local; + long time = msg.getLong(MESSAGE_TIME); + boolean accepted = msg.getBoolean(ACCEPT, false); + boolean read = msg.getBoolean(READ, false); + AuthorId authorId; + String name; + if (type == TYPE_RESPONSE) { + if (state.getLong(ROLE) == ROLE_INTRODUCER) { + if (!concernsThisContact(contactId, messageId, state)) { + // this response is not from contactId + continue; + } + local = false; + authorId = + getAuthorIdForIntroducer(contactId, state); + name = getNameForIntroducer(contactId, state); + } else { + if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE), + messageId.getBytes())) { + // this response is not ours, don't include it + continue; + } + local = true; + authorId = new AuthorId( + state.getRaw(REMOTE_AUTHOR_ID)); + name = state.getString(NAME); + } + IntroductionResponse ir = new IntroductionResponse( + sessionId, messageId, time, local, s.isSent(), + s.isSeen(), read, authorId, name, accepted); + list.add(ir); + } else if (type == TYPE_REQUEST) { + String message; + boolean answered, exists; + if (state.getLong(ROLE) == ROLE_INTRODUCER) { + local = true; + authorId = + getAuthorIdForIntroducer(contactId, state); + name = getNameForIntroducer(contactId, state); + message = msg.getOptionalString(MSG); + answered = false; + exists = false; + } else { + local = false; + authorId = new AuthorId( + state.getRaw(REMOTE_AUTHOR_ID)); + name = state.getString(NAME); + message = state.getOptionalString(MSG); + answered = state.getBoolean(ANSWERED); + exists = state.getBoolean(EXISTS); + } + IntroductionRequest ir = new IntroductionRequest( + sessionId, messageId, time, local, s.isSent(), + s.isSeen(), read, authorId, name, accepted, + message, answered, exists); + list.add(ir); + } + } catch (FormatException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + } + } + txn.setComplete(); + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + return list; + } + + private String getNameForIntroducer(ContactId contactId, + BdfDictionary state) throws FormatException { + + if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) + return state.getString(CONTACT_2); + if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue()) + return state.getString(CONTACT_1); + throw new RuntimeException("Contact not part of this introduction session"); + } + + private AuthorId getAuthorIdForIntroducer(ContactId contactId, + BdfDictionary state) throws FormatException { + + if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) + return new AuthorId(state.getRaw(AUTHOR_ID_2)); + if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue()) + return new AuthorId(state.getRaw(AUTHOR_ID_1)); + throw new RuntimeException("Contact not part of this introduction session"); + } + + private boolean concernsThisContact(ContactId contactId, MessageId messageId, + BdfDictionary state) throws FormatException { + + if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) { + return Arrays.equals(state.getRaw(RESPONSE_1, new byte[0]), + messageId.getBytes()); + } else { + return Arrays.equals(state.getRaw(RESPONSE_2, new byte[0]), + messageId.getBytes()); + } + } + + @Override + public void setReadFlag(MessageId m, boolean read) throws DbException { + try { + BdfDictionary meta = BdfDictionary.of(new BdfEntry(READ, read)); + clientHelper.mergeMessageMetadata(m, meta); + } catch (FormatException e) { + throw new RuntimeException(e); + } + } + + public BdfDictionary getSessionState(Transaction txn, byte[] sessionId) + throws DbException, FormatException { + + try { + return clientHelper.getMessageMetadataAsDictionary(txn, + new MessageId(sessionId)); + } catch (NoSuchMessageException e) { + Map map = clientHelper + .getMessageMetadataAsDictionary(txn, + localGroup.getId()); + for (Map.Entry m : map.entrySet()) { + if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) { + return m.getValue(); + } + } + if (LOG.isLoggable(WARNING)) { + LOG.warning( + "No session state found for this message with session ID " + + Arrays.hashCode(sessionId)); + } + throw new FormatException(); + } + } + + public Group getIntroductionGroup(Contact c) { + return privateGroupFactory.createPrivateGroup(CLIENT_ID, c); + } + + public Group getLocalGroup() { + return localGroup; + } + + public void sendMessage(Transaction txn, BdfDictionary message) + throws DbException, FormatException { + + BdfList bdfList = MessageEncoder.encodeMessage(message); + byte[] body = clientHelper.toByteArray(bdfList); + GroupId groupId = new GroupId(message.getRaw(GROUP_ID)); + Group group = db.getGroup(txn, groupId); + long timestamp = System.currentTimeMillis(); + + message.put(MESSAGE_TIME, timestamp); + Metadata metadata = metadataEncoder.encode(message); + + messageQueueManager + .sendMessage(txn, group, timestamp, body, metadata); + } + + private void deleteMessage(Transaction txn, MessageId messageId) + throws DbException { + + db.deleteMessage(txn, messageId); + db.deleteMessageMetadata(txn, messageId); + } + +} diff --git a/briar-core/src/org/briarproject/introduction/IntroductionModule.java b/briar-core/src/org/briarproject/introduction/IntroductionModule.java new file mode 100644 index 000000000..686a34eda --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroductionModule.java @@ -0,0 +1,56 @@ +package org.briarproject.introduction; + +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.MessageQueueManager; +import org.briarproject.api.contact.ContactManager; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.introduction.IntroductionManager; +import org.briarproject.api.system.Clock; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; + +@Module +public class IntroductionModule { + + public static class EagerSingletons { + @Inject IntroductionManager introductionManager; + @Inject IntroductionValidator introductionValidator; + } + + @Provides + @Singleton + IntroductionValidator getValidator(MessageQueueManager messageQueueManager, + IntroductionManager introductionManager, + MetadataEncoder metadataEncoder, ClientHelper clientHelper, + Clock clock) { + + IntroductionValidator introductionValidator = new IntroductionValidator( + clientHelper, metadataEncoder, clock); + + messageQueueManager.registerMessageValidator( + introductionManager.getClientId(), + introductionValidator); + + return introductionValidator; + } + + @Provides + @Singleton + IntroductionManager getIntroductionManager( + ContactManager contactManager, + MessageQueueManager messageQueueManager, + IntroductionManagerImpl introductionManager) { + + contactManager.registerAddContactHook(introductionManager); + contactManager.registerRemoveContactHook(introductionManager); + messageQueueManager + .registerIncomingMessageHook(introductionManager.getClientId(), + introductionManager); + + return introductionManager; + } +} diff --git a/briar-core/src/org/briarproject/introduction/IntroductionValidator.java b/briar-core/src/org/briarproject/introduction/IntroductionValidator.java new file mode 100644 index 000000000..addf58ea4 --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/IntroductionValidator.java @@ -0,0 +1,173 @@ +package org.briarproject.introduction; + +import org.briarproject.api.DeviceId; +import org.briarproject.api.FormatException; +import org.briarproject.api.TransportId; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.data.MetadataEncoder; +import org.briarproject.api.introduction.SessionId; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.Message; +import org.briarproject.api.system.Clock; +import org.briarproject.clients.BdfMessageValidator; + +import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME; +import static org.briarproject.api.introduction.IntroductionConstants.MSG; +import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; +import static org.briarproject.api.introduction.IntroductionConstants.TIME; +import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; +import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; +import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; + +class IntroductionValidator extends BdfMessageValidator { + + IntroductionValidator(ClientHelper clientHelper, + MetadataEncoder metadataEncoder, Clock clock) { + super(clientHelper, metadataEncoder, clock); + } + + @Override + protected BdfDictionary validateMessage(Message m, Group g, BdfList body) + throws FormatException { + + BdfDictionary d; + long type = body.getLong(0); + byte[] id = body.getRaw(1); + checkLength(id, SessionId.LENGTH); + + if (type == TYPE_REQUEST) { + d = validateRequest(body); + } else if (type == TYPE_RESPONSE) { + d = validateResponse(body); + } else if (type == TYPE_ACK) { + d = validateAck(body); + } else if (type == TYPE_ABORT) { + d = validateAbort(body); + } else { + throw new FormatException(); + } + + d.put(TYPE, type); + d.put(SESSION_ID, id); + d.put(MESSAGE_ID, m.getId()); + d.put(MESSAGE_TIME, m.getTimestamp()); + return d; + } + + private BdfDictionary validateRequest(BdfList message) + throws FormatException { + + checkSize(message, 4, 5); + + // parse contact name + String name = message.getString(2); + checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH); + + // parse contact's public key + byte[] key = message.getRaw(3); + checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH); + + // parse (optional) message + String msg = null; + if (message.size() == 5) { + msg = message.getString(4); + checkLength(msg, 0, MAX_MESSAGE_BODY_LENGTH); + } + + // Return the metadata + BdfDictionary d = new BdfDictionary(); + d.put(NAME, name); + d.put(PUBLIC_KEY, key); + if (msg != null) { + d.put(MSG, msg); + } + return d; + } + + private BdfDictionary validateResponse(BdfList message) + throws FormatException { + + checkSize(message, 3, 7); + + // parse accept/decline + boolean accept = message.getBoolean(2); + + long time = 0; + byte[] pubkey = null; + byte[] deviceId = null; + BdfDictionary tp = new BdfDictionary(); + if (accept) { + checkSize(message, 7); + + // parse timestamp + time = message.getLong(3); + + // parse ephemeral public key + pubkey = message.getRaw(4); + checkLength(pubkey, 0, MAX_PUBLIC_KEY_LENGTH); + + // parse device ID + deviceId = message.getRaw(5); + checkLength(deviceId, DeviceId.LENGTH); + + // parse transport properties + tp = message.getDictionary(6); + if (tp.size() < 1) throw new FormatException(); + for (String tId : tp.keySet()) { + checkLength(tId, 1, TransportId.MAX_TRANSPORT_ID_LENGTH); + BdfDictionary tProps = tp.getDictionary(tId); + for (String propId : tProps.keySet()) { + checkLength(propId, 0, MAX_PROPERTY_LENGTH); + String prop = tProps.getString(propId); + checkLength(prop, 0, MAX_PROPERTY_LENGTH); + } + } + } else { + checkSize(message, 3); + } + + // Return the metadata + BdfDictionary d = new BdfDictionary(); + d.put(ACCEPT, accept); + if (accept) { + d.put(TIME, time); + d.put(E_PUBLIC_KEY, pubkey); + d.put(DEVICE_ID, deviceId); + d.put(TRANSPORT, tp); + } + return d; + } + + private BdfDictionary validateAck(BdfList message) + throws FormatException { + + checkSize(message, 2); + + // Return the metadata + return new BdfDictionary(); + } + + private BdfDictionary validateAbort(BdfList message) + throws FormatException { + + checkSize(message, 2); + + // Return the metadata + return new BdfDictionary(); + } +} diff --git a/briar-core/src/org/briarproject/introduction/MessageEncoder.java b/briar-core/src/org/briarproject/introduction/MessageEncoder.java new file mode 100644 index 000000000..87bf4d5e6 --- /dev/null +++ b/briar-core/src/org/briarproject/introduction/MessageEncoder.java @@ -0,0 +1,74 @@ +package org.briarproject.introduction; + +import org.briarproject.api.FormatException; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; + +import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT; +import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID; +import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.MSG; +import static org.briarproject.api.introduction.IntroductionConstants.NAME; +import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY; +import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID; +import static org.briarproject.api.introduction.IntroductionConstants.TIME; +import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST; +import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE; + +public class MessageEncoder { + + public static BdfList encodeMessage(BdfDictionary d) throws FormatException { + + BdfList body; + long type = d.getLong(TYPE); + if (type == TYPE_REQUEST) { + body = encodeRequest(d); + } else if (type == TYPE_RESPONSE) { + body = encodeResponse(d); + } else if (type == TYPE_ACK) { + body = encodeAck(d); + } else if (type == TYPE_ABORT) { + body = encodeAbort(d); + } else { + throw new FormatException(); + } + return body; + } + + private static BdfList encodeRequest(BdfDictionary d) throws FormatException { + BdfList list = BdfList.of(TYPE_REQUEST, d.getRaw(SESSION_ID), + d.getString(NAME), d.getRaw(PUBLIC_KEY)); + + if (d.containsKey(MSG)) { + list.add(d.getString(MSG)); + } + return list; + } + + private static BdfList encodeResponse(BdfDictionary d) throws FormatException { + BdfList list = BdfList.of(TYPE_RESPONSE, d.getRaw(SESSION_ID), + d.getBoolean(ACCEPT)); + + if (d.getBoolean(ACCEPT)) { + list.add(d.getLong(TIME)); + list.add(d.getRaw(E_PUBLIC_KEY)); + list.add(d.getRaw(DEVICE_ID)); + list.add(d.getDictionary(TRANSPORT)); + } + // TODO Sign the response, see #256 + return list; + } + + private static BdfList encodeAck(BdfDictionary d) throws FormatException { + return BdfList.of(TYPE_ACK, d.getRaw(SESSION_ID)); + } + + private static BdfList encodeAbort(BdfDictionary d) throws FormatException { + return BdfList.of(TYPE_ABORT, d.getRaw(SESSION_ID)); + } + +} From 54f320465f13d854a769708f59e83250e2874eae Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 29 Feb 2016 13:15:11 -0300 Subject: [PATCH 6/6] Add information about whether Contact is active to ContactAddedEvent --- .../src/org/briarproject/api/event/ContactAddedEvent.java | 8 +++++++- .../src/org/briarproject/db/DatabaseComponentImpl.java | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java b/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java index b8d29bf27..0b2e5017c 100644 --- a/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java +++ b/briar-api/src/org/briarproject/api/event/ContactAddedEvent.java @@ -6,12 +6,18 @@ import org.briarproject.api.contact.ContactId; public class ContactAddedEvent extends Event { private final ContactId contactId; + private final boolean active; - public ContactAddedEvent(ContactId contactId) { + public ContactAddedEvent(ContactId contactId, boolean active) { this.contactId = contactId; + this.active = active; } public ContactId getContactId() { return contactId; } + + public boolean isActive() { + return active; + } } diff --git a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java index 150d0a43a..d3e48748c 100644 --- a/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java +++ b/briar-core/src/org/briarproject/db/DatabaseComponentImpl.java @@ -161,7 +161,7 @@ class DatabaseComponentImpl implements DatabaseComponent { if (db.containsContact(txn, remote.getId(), local)) throw new ContactExistsException(); ContactId c = db.addContact(txn, remote, local, active); - transaction.attach(new ContactAddedEvent(c)); + transaction.attach(new ContactAddedEvent(c, active)); if (active) transaction.attach(new ContactStatusChangedEvent(c, true)); return c; }