From f44cb5ff946a53540e13daa66287e5cb395ea765 Mon Sep 17 00:00:00 2001 From: Torsten Grote <t@grobox.de> Date: Thu, 21 Jan 2016 10:10:40 -0200 Subject: [PATCH] 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 0000000000..65d9fe52e1 --- /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<A, S, M> { + StateUpdate<S, M> onLocalAction(S localState, A action); + + StateUpdate<S, M> onMessageReceived(S localState, M received); + + StateUpdate<S, M> onMessageDelivered(S localState, M delivered); + + class StateUpdate<S, M> { + public final boolean deleteMessages; + public final boolean deleteState; + public final S localState; + public final List<M> toSend; + public final List<Event> toBroadcast; + + public StateUpdate(boolean deleteMessages, boolean deleteState, + S localState, List<M> toSend, List<Event> 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 f18bd1dc3b..a40a2b08c4 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 0000000000..58473e08c8 --- /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 0000000000..2938c40db6 --- /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 0000000000..dfbf3a3193 --- /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 c53a836361..26216a8730 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 0000000000..db50467f08 --- /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 0000000000..8b46a0c1f6 --- /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 0000000000..d20433876a --- /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 0000000000..d132e0d68a --- /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 0000000000..64bc309830 --- /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 0000000000..882ccbf112 --- /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<IntroductionMessage> 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 0000000000..6ac98793f2 --- /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 0000000000..facd7151fb --- /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 0000000000..e73065bcc0 --- /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 0000000000..d68bf9e030 --- /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 00d46b1da7..275b938a6f 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 11d6c87b0a..e51b5a3c51 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 0000000000..7be3184086 --- /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<BdfDictionary, BdfDictionary, BdfDictionary> { + + private static final Logger LOG = + Logger.getLogger(IntroduceeEngine.class.getName()); + + @Override + public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = new ArrayList<BdfDictionary>(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<Event> events = Collections.emptyList(); + return new StateUpdate<BdfDictionary, BdfDictionary>(false, + false, + localState, messages, events); + } else { + throw new IllegalArgumentException(); + } + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages; + List<Event> 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<BdfDictionary, BdfDictionary>(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<BdfDictionary, BdfDictionary> 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<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = Collections.singletonList(msg); + // TODO inform about protocol abort via new Event? + List<Event> events = Collections.emptyList(); + return new StateUpdate<BdfDictionary, BdfDictionary>(false, false, + localState, messages, events); + } + + private StateUpdate<BdfDictionary, BdfDictionary> noUpdate( + BdfDictionary localState) throws FormatException { + + return new StateUpdate<BdfDictionary, BdfDictionary>(false, false, + localState, new ArrayList<BdfDictionary>(0), + new ArrayList<Event>(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 0000000000..2049fba3ff --- /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<TransportId, TransportProperties> 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<BdfDictionary, BdfDictionary> + 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<TransportId, TransportProperties> 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<TransportId, TransportProperties> map) { + + BdfDictionary d = new BdfDictionary(); + for (Map.Entry<TransportId, TransportProperties> e : map.entrySet()) { + d.put(e.getKey().getString(), e.getValue()); + } + return d; + } + + private Map<TransportId, TransportProperties> parseTransportProperties( + BdfDictionary d) throws FormatException { + + Map<TransportId, TransportProperties> tpMap = + new HashMap<TransportId, TransportProperties>(); + 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 0000000000..fb88c28df6 --- /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<BdfDictionary, BdfDictionary, BdfDictionary> { + + private static final Logger LOG = + Logger.getLogger(IntroducerEngine.class.getName()); + + @Override + public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = new ArrayList<BdfDictionary>(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<Event> events = Collections.emptyList(); + return new StateUpdate<BdfDictionary, BdfDictionary>(false, false, + localState, messages, events); + } else { + throw new IllegalArgumentException("Unknown Local Action"); + } + } catch (FormatException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public StateUpdate<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages; + List<Event> 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<BdfDictionary, BdfDictionary>(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<BdfDictionary> 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<BdfDictionary, BdfDictionary> 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<BdfDictionary, BdfDictionary> 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<BdfDictionary> messages = new ArrayList<BdfDictionary>(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<Event> events = Collections.emptyList(); + return new StateUpdate<BdfDictionary, BdfDictionary>(false, false, + localState, messages, events); + } + + private StateUpdate<BdfDictionary, BdfDictionary> noUpdate( + BdfDictionary localState) throws FormatException { + + return new StateUpdate<BdfDictionary, BdfDictionary>(false, false, + localState, new ArrayList<BdfDictionary>(0), + new ArrayList<Event>(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 0000000000..1b35fc26c0 --- /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<BdfDictionary, BdfDictionary> + 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 0000000000..a01c62b101 --- /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<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId()); + for (Map.Entry<MessageId, BdfDictionary> 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<IntroductionMessage> getIntroductionMessages( + ContactId contactId) throws DbException { + + Collection<IntroductionMessage> list = + new ArrayList<IntroductionMessage>(); + + Map<MessageId, BdfDictionary> metadata; + Collection<MessageStatus> 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<SessionId, BdfDictionary> sessionStates = + new HashMap<SessionId, BdfDictionary>(); + 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<MessageId, BdfDictionary> map = clientHelper + .getMessageMetadataAsDictionary(txn, + localGroup.getId()); + for (Map.Entry<MessageId, BdfDictionary> 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 0000000000..686a34edad --- /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 0000000000..addf58ea49 --- /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 0000000000..87bf4d5e6a --- /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)); + } + +} -- GitLab