diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoConstants.java b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoConstants.java index 3b12c5cf7..b9f772d2b 100644 --- a/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoConstants.java +++ b/bramble-api/src/main/java/org/briarproject/bramble/api/crypto/CryptoConstants.java @@ -16,4 +16,10 @@ public interface CryptoConstants { * The maximum length of a signature in bytes. */ int MAX_SIGNATURE_BYTES = 64; + + /** + * The length of a MAC in bytes. + */ + int MAC_BYTES = SecretKey.LENGTH; + } diff --git a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java index dd5874f6e..f63bcb0a6 100644 --- a/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java +++ b/bramble-core/src/main/java/org/briarproject/bramble/db/JdbcDatabase.java @@ -2898,6 +2898,8 @@ abstract class JdbcDatabase implements Database { String sql = "UPDATE outgoingKeys SET active = true" + " WHERE transportId = ? AND keySetId = ?"; ps = txn.prepareStatement(sql); + ps.setString(1, t.getString()); + ps.setInt(2, k.getInt()); int affected = ps.executeUpdate(); if (affected < 0 || affected > 1) throw new DbStateException(); ps.close(); diff --git a/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java b/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java index 799722344..66a4a3b56 100644 --- a/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java +++ b/bramble-core/src/test/java/org/briarproject/bramble/test/ValidatorTestCase.java @@ -1,17 +1,20 @@ package org.briarproject.bramble.test; import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.system.Clock; +import org.jmock.Expectations; +import static org.briarproject.bramble.test.TestUtils.getAuthor; import static org.briarproject.bramble.test.TestUtils.getClientId; import static org.briarproject.bramble.test.TestUtils.getGroup; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; -import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.test.TestUtils.getMessage; public abstract class ValidatorTestCase extends BrambleMockTestCase { @@ -24,10 +27,23 @@ public abstract class ValidatorTestCase extends BrambleMockTestCase { protected final Group group = getGroup(getClientId()); protected final GroupId groupId = group.getId(); protected final byte[] descriptor = group.getDescriptor(); - protected final MessageId messageId = new MessageId(getRandomId()); - protected final long timestamp = 1234567890 * 1000L; - protected final byte[] raw = getRandomBytes(123); - protected final Message message = - new Message(messageId, groupId, timestamp, raw); + protected final Message message = getMessage(groupId); + protected final MessageId messageId = message.getId(); + protected final long timestamp = message.getTimestamp(); + protected final byte[] raw = message.getRaw(); + protected final Author author = getAuthor(); + protected final BdfList authorList = BdfList.of( + author.getFormatVersion(), + author.getName(), + author.getPublicKey() + ); -} + protected void expectParseAuthor(BdfList authorList, Author author) + throws Exception { + context.checking(new Expectations() {{ + oneOf(clientHelper).parseAndValidateAuthor(authorList); + will(returnValue(author)); + }}); + } + +} \ No newline at end of file diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java index 585b0fed9..2af7fb8e5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/ConversationActivity.java @@ -865,7 +865,8 @@ introductionOnboardingSeen(); "Unknown Request Type"); } loadMessages(); - } catch (DbException | FormatException e) { + } catch (DbException e) { + // TODO use more generic error message introductionResponseError(); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); @@ -898,11 +899,14 @@ introductionOnboardingSeen(); @DatabaseExecutor private void respondToIntroductionRequest(SessionId sessionId, - boolean accept, long time) throws DbException, FormatException { - if (accept) { - introductionManager.acceptIntroduction(contactId, sessionId, time); - } else { - introductionManager.declineIntroduction(contactId, sessionId, time); + boolean accept, long time) throws DbException { + try { + introductionManager + .respondToIntroduction(contactId, sessionId, time, accept); + } catch (ProtocolStateException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + introductionResponseError(); } } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java index 3a76421b3..7a9652a61 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java @@ -2,6 +2,7 @@ package org.briarproject.briar.android.introduction; import android.content.Context; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v7.app.ActionBar; import android.view.LayoutInflater; import android.view.MenuItem; @@ -11,7 +12,6 @@ import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; -import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactManager; @@ -33,9 +33,10 @@ import im.delight.android.identicons.IdenticonDrawable; import static android.app.Activity.RESULT_OK; import static android.view.View.GONE; +import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_SHORT; import static java.util.logging.Level.WARNING; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH; +import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH; public class IntroductionMessageFragment extends BaseFragment implements TextInputListener { @@ -125,14 +126,15 @@ public class IntroductionMessageFragment extends BaseFragment new ContactId(contactId1)); Contact c2 = contactManager.getContact( new ContactId(contactId2)); - setUpViews(c1, c2); + boolean possible = introductionManager.canIntroduce(c1, c2); + setUpViews(c1, c2, possible); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); } }); } - private void setUpViews(Contact c1, Contact c2) { + private void setUpViews(Contact c1, Contact c2, boolean possible) { introductionActivity.runOnUiThreadUnlessDestroyed(() -> { contact1 = c1; contact2 = c2; @@ -147,13 +149,22 @@ public class IntroductionMessageFragment extends BaseFragment ui.contactName1.setText(c1.getAuthor().getName()); ui.contactName2.setText(c2.getAuthor().getName()); - // set button action - ui.message.setListener(IntroductionMessageFragment.this); - - // hide progress bar and show views + // hide progress bar ui.progressBar.setVisibility(GONE); - ui.message.setSendButtonEnabled(true); - ui.message.showSoftKeyboard(); + + if (possible) { + // set button action + ui.message.setListener(IntroductionMessageFragment.this); + + // show views + ui.notPossible.setVisibility(GONE); + ui.message.setVisibility(VISIBLE); + ui.message.setSendButtonEnabled(true); + ui.message.showSoftKeyboard(); + } else { + ui.notPossible.setVisibility(VISIBLE); + ui.message.setVisibility(GONE); + } }); } @@ -175,7 +186,8 @@ public class IntroductionMessageFragment extends BaseFragment ui.message.setSendButtonEnabled(false); String msg = ui.message.getText().toString(); - msg = StringUtils.truncateUtf8(msg, MAX_INTRODUCTION_MESSAGE_LENGTH); + if (msg.equals("")) msg = null; + else msg = StringUtils.truncateUtf8(msg, MAX_REQUEST_MESSAGE_LENGTH); makeIntroduction(contact1, contact2, msg); // don't wait for the introduction to be made before finishing activity @@ -184,13 +196,14 @@ public class IntroductionMessageFragment extends BaseFragment introductionActivity.supportFinishAfterTransition(); } - private void makeIntroduction(Contact c1, Contact c2, String msg) { + private void makeIntroduction(Contact c1, Contact c2, + @Nullable String msg) { introductionActivity.runOnDbThread(() -> { // actually make the introduction try { long timestamp = System.currentTimeMillis(); introductionManager.makeIntroduction(c1, c2, msg, timestamp); - } catch (DbException | FormatException e) { + } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); introductionError(); } @@ -208,6 +221,7 @@ public class IntroductionMessageFragment extends BaseFragment private final ProgressBar progressBar; private final CircleImageView avatar1, avatar2; private final TextView contactName1, contactName2; + private final TextView notPossible; private final TextInputView message; private ViewHolder(View v) { @@ -216,6 +230,7 @@ public class IntroductionMessageFragment extends BaseFragment avatar2 = v.findViewById(R.id.avatarContact2); contactName1 = v.findViewById(R.id.nameContact1); contactName2 = v.findViewById(R.id.nameContact2); + notPossible = v.findViewById(R.id.introductionNotPossibleView); message = v.findViewById(R.id.introductionMessageView); } } diff --git a/briar-android/src/main/res/layout/introduction_message.xml b/briar-android/src/main/res/layout/introduction_message.xml index e78e1c639..0cdc34ba8 100644 --- a/briar-android/src/main/res/layout/introduction_message.xml +++ b/briar-android/src/main/res/layout/introduction_message.xml @@ -94,13 +94,25 @@ android:layout_gravity="center" tools:visibility="gone"/> + + + app:maxLines="5" + tools:visibility="visible"/> \ No newline at end of file diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index d4b40f5bb..38b04cd53 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -144,6 +144,7 @@ Introduce your contacts You can introduce your contacts to each other, so they don\'t need to meet in person to connect on Briar. Select Contact + You already have one introduction in progress with these contacts. Please allow for this to finish first. If you or your contacts are rarely online, this can take some time. Introduce Contacts Add a message (optional) Make Introduction diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageQueueManager.java b/briar-api/src/main/java/org/briarproject/briar/api/client/MessageQueueManager.java deleted file mode 100644 index 75418bf7c..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/client/MessageQueueManager.java +++ /dev/null @@ -1,72 +0,0 @@ -package org.briarproject.briar.api.client; - -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.ClientId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.InvalidMessageException; -import org.briarproject.bramble.api.sync.MessageContext; - -@Deprecated -@NotNullByDefault -public interface MessageQueueManager { - - /** - * The key used for storing the queue's state in the group metadata. - */ - String QUEUE_STATE_KEY = "queueState"; - - /** - * Sends a message using the given queue. - */ - QueueMessage sendMessage(Transaction txn, Group queue, long timestamp, - byte[] body, Metadata meta) throws DbException; - - /** - * Sets the message validator for the given client. - */ - void registerMessageValidator(ClientId c, QueueMessageValidator v); - - /** - * Sets the incoming message hook for the given client. The hook will be - * called once for each incoming message that passes validation. Messages - * are passed to the hook in order. - */ - void registerIncomingMessageHook(ClientId c, IncomingQueueMessageHook hook); - - @Deprecated - interface QueueMessageValidator { - - /** - * Validates the given message and returns its metadata and - * dependencies. - */ - MessageContext validateMessage(QueueMessage q, Group g) - throws InvalidMessageException; - } - - @Deprecated - interface IncomingQueueMessageHook { - - /** - * Called once for each incoming message that passes validation. - * Messages are passed to the hook in order. - * - * @throws DbException Should only be used for real database errors. - * If this is thrown, delivery will be attempted again at next startup, - * whereas if an InvalidMessageException is thrown, - * the message will be permanently invalidated. - * @throws InvalidMessageException for any non-database error - * that occurs while handling remotely created data. - * This includes errors that occur while handling locally created data - * in a context controlled by remotely created data - * (for example, parsing the metadata of a dependency - * of an incoming message). - * Never rethrow DbException as InvalidMessageException! - */ - void incomingMessage(Transaction txn, QueueMessage q, Metadata meta) - throws DbException, InvalidMessageException; - } -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/ProtocolEngine.java b/briar-api/src/main/java/org/briarproject/briar/api/client/ProtocolEngine.java deleted file mode 100644 index 281d9af86..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/client/ProtocolEngine.java +++ /dev/null @@ -1,50 +0,0 @@ -package org.briarproject.briar.api.client; - -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import java.util.List; - -@Deprecated -@NotNullByDefault -public interface ProtocolEngine { - - StateUpdate onLocalAction(S localState, A action); - - StateUpdate onMessageReceived(S localState, M received); - - StateUpdate onMessageDelivered(S localState, M delivered); - - class StateUpdate { - public final boolean deleteMessage; - public final boolean deleteState; - public final S localState; - public final List toSend; - public final List toBroadcast; - - /** - * This class represents an update of the local protocol state. - * It only shows how the state should be updated, - * but does not carry out the updates on its own. - * - * @param deleteMessage whether to delete the message that triggered - * the state update. This will be ignored for - * {@link ProtocolEngine#onLocalAction}. - * @param deleteState whether to delete the localState {@link S} - * @param localState the new local state - * @param toSend a list of messages to be sent as part of the - * state update - * @param toBroadcast a list of events to broadcast as result of the - * state update - */ - public StateUpdate(boolean deleteMessage, boolean deleteState, - S localState, List toSend, List toBroadcast) { - - this.deleteMessage = deleteMessage; - this.deleteState = deleteState; - this.localState = localState; - this.toSend = toSend; - this.toBroadcast = toBroadcast; - } - } -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/QueueMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/client/QueueMessage.java deleted file mode 100644 index c41b6da2b..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/client/QueueMessage.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.briarproject.briar.api.client; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; - -import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; - -@Deprecated -@NotNullByDefault -public class QueueMessage extends Message { - - public static final int QUEUE_MESSAGE_HEADER_LENGTH = - MESSAGE_HEADER_LENGTH + 8; - public static final int MAX_QUEUE_MESSAGE_BODY_LENGTH = - MAX_MESSAGE_BODY_LENGTH - 8; - - private final long queuePosition; - - public QueueMessage(MessageId id, GroupId groupId, long timestamp, - long queuePosition, byte[] raw) { - super(id, groupId, timestamp, raw); - this.queuePosition = queuePosition; - } - - public long getQueuePosition() { - return queuePosition; - } -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/client/QueueMessageFactory.java b/briar-api/src/main/java/org/briarproject/briar/api/client/QueueMessageFactory.java deleted file mode 100644 index ac458a8a8..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/client/QueueMessageFactory.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.briarproject.briar.api.client; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; - -@Deprecated -@NotNullByDefault -public interface QueueMessageFactory { - - QueueMessage createMessage(GroupId groupId, long timestamp, - long queuePosition, byte[] body); - - QueueMessage createMessage(MessageId id, byte[] raw); -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeAction.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeAction.java deleted file mode 100644 index 68881b9dc..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeAction.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.briarproject.briar.api.introduction; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import javax.annotation.Nullable; - -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; - -@NotNullByDefault -public enum IntroduceeAction { - - LOCAL_ACCEPT, - LOCAL_DECLINE, - LOCAL_ABORT, - REMOTE_REQUEST, - REMOTE_ACCEPT, - REMOTE_DECLINE, - REMOTE_ABORT, - ACK; - - @Nullable - 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; - } - - @Nullable - public static IntroduceeAction getRemote(int type) { - return getRemote(type, true); - } - - @Nullable - 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_ACK) return ACK; - if (type == TYPE_ABORT) return LOCAL_ABORT; - return null; - } - - @Nullable - public static IntroduceeAction getLocal(int type) { - return getLocal(type, true); - } - -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeProtocolState.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeProtocolState.java deleted file mode 100644 index e696181a0..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroduceeProtocolState.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.briarproject.briar.api.introduction; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import javax.annotation.concurrent.Immutable; - -import static org.briarproject.briar.api.introduction.IntroduceeAction.ACK; -import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_ACCEPT; -import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_DECLINE; -import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_ACCEPT; -import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_DECLINE; -import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_REQUEST; - -@Immutable -@NotNullByDefault -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/main/java/org/briarproject/briar/api/introduction/IntroducerAction.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerAction.java deleted file mode 100644 index 7123c7eb2..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerAction.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.briarproject.briar.api.introduction; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import javax.annotation.Nullable; - -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; - -@NotNullByDefault -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; - - @Nullable - public static IntroducerAction getLocal(int type) { - if (type == TYPE_REQUEST) return LOCAL_REQUEST; - if (type == TYPE_ABORT) return LOCAL_ABORT; - return null; - } - - @Nullable - 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; - } - - @Nullable - public static IntroducerAction getRemote(int type, boolean one) { - return getRemote(type, one, true); - } - -} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerProtocolState.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerProtocolState.java deleted file mode 100644 index b3d89864e..000000000 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroducerProtocolState.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.briarproject.briar.api.introduction; - -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; - -import javax.annotation.concurrent.Immutable; - -import static org.briarproject.briar.api.introduction.IntroducerAction.ACK_1; -import static org.briarproject.briar.api.introduction.IntroducerAction.ACK_2; -import static org.briarproject.briar.api.introduction.IntroducerAction.LOCAL_REQUEST; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ABORT; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_1; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_2; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_1; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_2; - -@Immutable -@NotNullByDefault -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) { - @Override - 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) { - @Override - public IntroducerProtocolState next(IntroducerAction a) { - if (a == REMOTE_ABORT) return ERROR; - return FINISHED; - } - }; - - 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/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java index 72e6030b1..87681d525 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionConstants.java @@ -4,126 +4,29 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_L 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 TRANSPORT = "transport"; - String MESSAGE_ID = "messageId"; - String MESSAGE_TIME = "timestamp"; - String MAC = "mac"; - String SIGNATURE = "signature"; - - /* Validation Constants */ - - /** - * The length of the message authentication code in bytes. - */ - int MAC_LENGTH = 32; - /** * The maximum length of the introducer's optional message to the * introducees in UTF-8 bytes. */ - int MAX_INTRODUCTION_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; + int MAX_REQUEST_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024; - /* 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 LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID"; - /* Introduction Request Action */ - String PUBLIC_KEY1 = "publicKey1"; - String PUBLIC_KEY2 = "publicKey2"; + String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY"; - /* 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 REMOTE_AUTHOR_IS_US = "remoteAuthorIsUs"; - String ANSWERED = "answered"; - String NONCE = "nonce"; - String MAC_KEY = "macKey"; - String OUR_TRANSPORT = "ourTransport"; - String OUR_MAC = "ourMac"; - String OUR_SIGNATURE = "ourSignature"; - - String TASK = "task"; - int TASK_ADD_CONTACT = 0; - int TASK_ACTIVATE_CONTACT = 1; - int TASK_ABORT = 2; - - /** - * Label for deriving the shared secret. - */ - String SHARED_SECRET_LABEL = - "org.briarproject.briar.introduction/SHARED_SECRET"; - - /** - * Label for deriving Alice's key binding nonce from the shared secret. - */ - String ALICE_NONCE_LABEL = - "org.briarproject.briar.introduction/ALICE_NONCE"; - - /** - * Label for deriving Bob's key binding nonce from the shared secret. - */ - String BOB_NONCE_LABEL = - "org.briarproject.briar.introduction/BOB_NONCE"; - - /** - * Label for deriving Alice's MAC key from the shared secret. - */ - String ALICE_MAC_KEY_LABEL = + String LABEL_ALICE_MAC_KEY = "org.briarproject.briar.introduction/ALICE_MAC_KEY"; - /** - * Label for deriving Bob's MAC key from the shared secret. - */ - String BOB_MAC_KEY_LABEL = + String LABEL_BOB_MAC_KEY = "org.briarproject.briar.introduction/BOB_MAC_KEY"; - /** - * Label for signing the introduction response. - */ - String SIGNING_LABEL = - "org.briarproject.briar.introduction/RESPONSE_SIGNATURE"; + String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC"; + + String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN"; + + String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE"; + + String LABEL_ACTIVATE_MAC = + "org.briarproject.briar.introduction/ACTIVATE_MAC"; - /** - * Label for MACing the introduction response. - */ - String MAC_LABEL = "org.briarproject.briar.introduction/RESPONSE_MAC"; } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java index ce9d06727..8711193f1 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionManager.java @@ -1,6 +1,5 @@ package org.briarproject.briar.api.introduction; -import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.db.DbException; @@ -24,25 +23,21 @@ public interface IntroductionManager extends ConversationClient { /** * The current version of the introduction client. */ - int CLIENT_VERSION = 0; + int CLIENT_VERSION = 1; + + boolean canIntroduce(Contact c1, Contact c2) throws DbException; /** * Sends two initial introduction messages. */ void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, - long timestamp) throws DbException, FormatException; + long timestamp) throws DbException; /** - * Accepts an introduction. + * Responds to an introduction. */ - void acceptIntroduction(ContactId contactId, SessionId sessionId, - long timestamp) throws DbException, FormatException; - - /** - * Declines an introduction. - */ - void declineIntroduction(ContactId contactId, SessionId sessionId, - long timestamp) throws DbException, FormatException; + void respondToIntroduction(ContactId contactId, SessionId sessionId, + long timestamp, boolean accept) throws DbException; /** * Returns all introduction messages for the given contact. diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java index 861469a3a..009487fa2 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionMessage.java @@ -8,7 +8,7 @@ import org.briarproject.briar.api.client.SessionId; import javax.annotation.concurrent.Immutable; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER; +import static org.briarproject.briar.api.introduction.Role.INTRODUCER; @Immutable @NotNullByDefault @@ -16,10 +16,10 @@ public class IntroductionMessage extends BaseMessageHeader { private final SessionId sessionId; private final MessageId messageId; - private final int role; + private final Role role; IntroductionMessage(SessionId sessionId, MessageId messageId, - GroupId groupId, int role, long time, boolean local, boolean sent, + GroupId groupId, Role role, long time, boolean local, boolean sent, boolean seen, boolean read) { super(messageId, groupId, time, local, sent, seen, read); @@ -37,7 +37,7 @@ public class IntroductionMessage extends BaseMessageHeader { } public boolean isIntroducer() { - return role == ROLE_INTRODUCER; + return role == INTRODUCER; } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java index 428528260..b2a804bd8 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionRequest.java @@ -1,6 +1,5 @@ package org.briarproject.briar.api.introduction; -import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; @@ -15,21 +14,19 @@ public class IntroductionRequest extends IntroductionResponse { @Nullable private final String message; - private final boolean answered, exists, introducesOtherIdentity; + private final boolean answered, exists; public IntroductionRequest(SessionId sessionId, MessageId messageId, - GroupId groupId, int role, long time, boolean local, boolean sent, - boolean seen, boolean read, AuthorId authorId, String name, - boolean accepted, @Nullable String message, boolean answered, - boolean exists, boolean introducesOtherIdentity) { + GroupId groupId, Role role, long time, boolean local, boolean sent, + boolean seen, boolean read, String name, boolean accepted, + @Nullable String message, boolean answered, boolean exists) { super(sessionId, messageId, groupId, role, time, local, sent, seen, - read, authorId, name, accepted); + read, name, accepted); this.message = message; this.answered = answered; this.exists = exists; - this.introducesOtherIdentity = introducesOtherIdentity; } @Nullable @@ -44,8 +41,4 @@ public class IntroductionRequest extends IntroductionResponse { public boolean contactExists() { return exists; } - - public boolean doesIntroduceOtherIdentity() { - return introducesOtherIdentity; - } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java index 22df6eba8..816135d43 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/IntroductionResponse.java @@ -1,6 +1,5 @@ package org.briarproject.briar.api.introduction; -import org.briarproject.bramble.api.identity.AuthorId; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.MessageId; @@ -12,19 +11,15 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class IntroductionResponse extends IntroductionMessage { - private final AuthorId remoteAuthorId; private final String name; private final boolean accepted; public IntroductionResponse(SessionId sessionId, MessageId messageId, - GroupId groupId, int role, long time, boolean local, boolean sent, - boolean seen, boolean read, AuthorId remoteAuthorId, String name, - boolean accepted) { - + GroupId groupId, Role role, long time, boolean local, boolean sent, + boolean seen, boolean read, String name, boolean accepted) { super(sessionId, messageId, groupId, role, time, local, sent, seen, read); - this.remoteAuthorId = remoteAuthorId; this.name = name; this.accepted = accepted; } @@ -37,7 +32,4 @@ public class IntroductionResponse extends IntroductionMessage { return accepted; } - public AuthorId getRemoteAuthorId() { - return remoteAuthorId; - } } diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/Role.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/Role.java new file mode 100644 index 000000000..38f0bd44c --- /dev/null +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/Role.java @@ -0,0 +1,29 @@ +package org.briarproject.briar.api.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +public enum Role { + + INTRODUCER(0), INTRODUCEE(1); + + private final int value; + + Role(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Role fromValue(int value) throws FormatException { + for (Role r : values()) if (r.value == value) return r; + throw new FormatException(); + } + +} diff --git a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java index 585527591..113ba400e 100644 --- a/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java +++ b/briar-api/src/main/java/org/briarproject/briar/api/introduction/event/IntroductionAbortedEvent.java @@ -1,6 +1,5 @@ package org.briarproject.briar.api.introduction.event; -import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.briar.api.client.SessionId; @@ -11,19 +10,14 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class IntroductionAbortedEvent extends Event { - private final ContactId contactId; private final SessionId sessionId; - public IntroductionAbortedEvent(ContactId contactId, SessionId sessionId) { - this.contactId = contactId; + public IntroductionAbortedEvent(SessionId sessionId) { this.sessionId = sessionId; } - public ContactId getContactId() { - return contactId; - } - public SessionId getSessionId() { return sessionId; } + } diff --git a/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java b/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java index 19d8142c1..edc62948d 100644 --- a/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java +++ b/briar-core/src/main/java/org/briarproject/briar/client/BdfIncomingMessageHook.java @@ -13,18 +13,12 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.InvalidMessageException; import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook; -import org.briarproject.briar.api.client.MessageQueueManager.IncomingQueueMessageHook; -import org.briarproject.briar.api.client.QueueMessage; import javax.annotation.concurrent.Immutable; -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH; - @Immutable @NotNullByDefault -public abstract class BdfIncomingMessageHook implements IncomingMessageHook, - IncomingQueueMessageHook { +public abstract class BdfIncomingMessageHook implements IncomingMessageHook { protected final DatabaseComponent db; protected final ClientHelper clientHelper; @@ -40,6 +34,7 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook, /** * Called once for each incoming message that passes validation. * + * @return whether or not this message should be shared * @throws DbException Should only be used for real database errors. * If this is thrown, delivery will be attempted again at next startup, * whereas if a FormatException is thrown, the message will be permanently @@ -60,29 +55,12 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook, public boolean incomingMessage(Transaction txn, Message m, Metadata meta) throws DbException, InvalidMessageException { try { - return incomingMessage(txn, m, meta, MESSAGE_HEADER_LENGTH); + BdfList body = clientHelper.toList(m); + BdfDictionary metaDictionary = metadataParser.parse(meta); + return incomingMessage(txn, m, body, metaDictionary); } catch (FormatException e) { throw new InvalidMessageException(e); } } - @Override - public void incomingMessage(Transaction txn, QueueMessage q, Metadata meta) - throws DbException, InvalidMessageException { - try { - incomingMessage(txn, q, meta, QUEUE_MESSAGE_HEADER_LENGTH); - } catch (FormatException e) { - throw new InvalidMessageException(e); - } - } - - private boolean incomingMessage(Transaction txn, Message m, Metadata meta, - int headerLength) throws DbException, FormatException { - byte[] raw = m.getRaw(); - BdfList body = clientHelper.toList(raw, headerLength, - raw.length - headerLength); - BdfDictionary metaDictionary = metadataParser.parse(meta); - return incomingMessage(txn, m, body, metaDictionary); - } - } diff --git a/briar-core/src/main/java/org/briarproject/briar/client/BdfQueueMessageValidator.java b/briar-core/src/main/java/org/briarproject/briar/client/BdfQueueMessageValidator.java deleted file mode 100644 index 48fd665a1..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/client/BdfQueueMessageValidator.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.briarproject.briar.client; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.BdfMessageContext; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataEncoder; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.InvalidMessageException; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageContext; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.MessageQueueManager.QueueMessageValidator; -import org.briarproject.briar.api.client.QueueMessage; - -import java.util.logging.Logger; - -import javax.annotation.concurrent.Immutable; - -import static org.briarproject.bramble.api.transport.TransportConstants.MAX_CLOCK_DIFFERENCE; -import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH; - -@Deprecated -@Immutable -@NotNullByDefault -public abstract class BdfQueueMessageValidator - implements QueueMessageValidator { - - protected static final Logger LOG = - Logger.getLogger(BdfQueueMessageValidator.class.getName()); - - protected final ClientHelper clientHelper; - protected final MetadataEncoder metadataEncoder; - protected final Clock clock; - - protected BdfQueueMessageValidator(ClientHelper clientHelper, - MetadataEncoder metadataEncoder, Clock clock) { - this.clientHelper = clientHelper; - this.metadataEncoder = metadataEncoder; - this.clock = clock; - } - - protected abstract BdfMessageContext validateMessage(Message m, Group g, - BdfList body) throws InvalidMessageException, FormatException; - - @Override - public MessageContext validateMessage(QueueMessage q, Group g) - throws InvalidMessageException { - // Reject the message if it's too far in the future - long now = clock.currentTimeMillis(); - if (q.getTimestamp() - now > MAX_CLOCK_DIFFERENCE) { - throw new InvalidMessageException( - "Timestamp is too far in the future"); - } - byte[] raw = q.getRaw(); - if (raw.length <= QUEUE_MESSAGE_HEADER_LENGTH) { - throw new InvalidMessageException("Message is too short"); - } - try { - BdfList body = clientHelper.toList(raw, QUEUE_MESSAGE_HEADER_LENGTH, - raw.length - QUEUE_MESSAGE_HEADER_LENGTH); - BdfMessageContext result = validateMessage(q, g, body); - Metadata meta = metadataEncoder.encode(result.getDictionary()); - return new MessageContext(meta, result.getDependencies()); - } catch (FormatException e) { - throw new InvalidMessageException(e); - } - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/client/BriarClientModule.java b/briar-core/src/main/java/org/briarproject/briar/client/BriarClientModule.java index 46ee505df..9eeb6b959 100644 --- a/briar-core/src/main/java/org/briarproject/briar/client/BriarClientModule.java +++ b/briar-core/src/main/java/org/briarproject/briar/client/BriarClientModule.java @@ -1,14 +1,6 @@ package org.briarproject.briar.client; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.sync.MessageFactory; -import org.briarproject.bramble.api.sync.ValidationManager; -import org.briarproject.briar.api.client.MessageQueueManager; import org.briarproject.briar.api.client.MessageTracker; -import org.briarproject.briar.api.client.QueueMessageFactory; - -import javax.inject.Singleton; import dagger.Module; import dagger.Provides; @@ -16,21 +8,6 @@ import dagger.Provides; @Module public class BriarClientModule { - @Provides - @Singleton - MessageQueueManager provideMessageQueueManager(DatabaseComponent db, - ClientHelper clientHelper, QueueMessageFactory queueMessageFactory, - ValidationManager validationManager) { - return new MessageQueueManagerImpl(db, clientHelper, - queueMessageFactory, validationManager); - } - - @Provides - QueueMessageFactory provideQueueMessageFactory( - MessageFactory messageFactory) { - return new QueueMessageFactoryImpl(messageFactory); - } - @Provides MessageTracker provideMessageTracker(MessageTrackerImpl messageTracker) { return messageTracker; diff --git a/briar-core/src/main/java/org/briarproject/briar/client/MessageQueueManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/MessageQueueManagerImpl.java deleted file mode 100644 index 47c91bbc2..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/client/MessageQueueManagerImpl.java +++ /dev/null @@ -1,259 +0,0 @@ -package org.briarproject.briar.client; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.ClientId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.InvalidMessageException; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageContext; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.sync.ValidationManager; -import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook; -import org.briarproject.bramble.api.sync.ValidationManager.MessageValidator; -import org.briarproject.bramble.util.ByteUtils; -import org.briarproject.briar.api.client.MessageQueueManager; -import org.briarproject.briar.api.client.QueueMessage; -import org.briarproject.briar.api.client.QueueMessageFactory; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; -import javax.inject.Inject; - -import static java.util.logging.Level.INFO; -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH; - -@Immutable -@NotNullByDefault -class MessageQueueManagerImpl implements MessageQueueManager { - - private static final String OUTGOING_POSITION_KEY = "nextOut"; - private static final String INCOMING_POSITION_KEY = "nextIn"; - private static final String PENDING_MESSAGES_KEY = "pending"; - - private static final Logger LOG = - Logger.getLogger(MessageQueueManagerImpl.class.getName()); - - private final DatabaseComponent db; - private final ClientHelper clientHelper; - private final QueueMessageFactory queueMessageFactory; - private final ValidationManager validationManager; - - @Inject - MessageQueueManagerImpl(DatabaseComponent db, ClientHelper clientHelper, - QueueMessageFactory queueMessageFactory, - ValidationManager validationManager) { - this.db = db; - this.clientHelper = clientHelper; - this.queueMessageFactory = queueMessageFactory; - this.validationManager = validationManager; - } - - @Override - public QueueMessage sendMessage(Transaction txn, Group queue, - long timestamp, byte[] body, Metadata meta) throws DbException { - QueueState queueState = loadQueueState(txn, queue.getId()); - long queuePosition = queueState.outgoingPosition; - queueState.outgoingPosition++; - if (LOG.isLoggable(INFO)) - LOG.info("Sending message with position " + queuePosition); - saveQueueState(txn, queue.getId(), queueState); - QueueMessage q = queueMessageFactory.createMessage(queue.getId(), - timestamp, queuePosition, body); - db.addLocalMessage(txn, q, meta, true); - return q; - } - - @Override - public void registerMessageValidator(ClientId c, QueueMessageValidator v) { - validationManager.registerMessageValidator(c, - new DelegatingMessageValidator(v)); - } - - @Override - public void registerIncomingMessageHook(ClientId c, - IncomingQueueMessageHook hook) { - validationManager.registerIncomingMessageHook(c, - new DelegatingIncomingMessageHook(hook)); - } - - private QueueState loadQueueState(Transaction txn, GroupId g) - throws DbException { - try { - TreeMap pending = new TreeMap<>(); - Metadata groupMeta = db.getGroupMetadata(txn, g); - byte[] raw = groupMeta.get(QUEUE_STATE_KEY); - if (raw == null) return new QueueState(0, 0, pending); - BdfDictionary d = clientHelper.toDictionary(raw, 0, raw.length); - long outgoingPosition = d.getLong(OUTGOING_POSITION_KEY); - long incomingPosition = d.getLong(INCOMING_POSITION_KEY); - BdfList pendingList = d.getList(PENDING_MESSAGES_KEY); - for (int i = 0; i < pendingList.size(); i++) { - BdfList item = pendingList.getList(i); - if (item.size() != 2) throw new FormatException(); - pending.put(item.getLong(0), new MessageId(item.getRaw(1))); - } - return new QueueState(outgoingPosition, incomingPosition, pending); - } catch (FormatException e) { - throw new DbException(e); - } - } - - private void saveQueueState(Transaction txn, GroupId g, - QueueState queueState) throws DbException { - try { - BdfDictionary d = new BdfDictionary(); - d.put(OUTGOING_POSITION_KEY, queueState.outgoingPosition); - d.put(INCOMING_POSITION_KEY, queueState.incomingPosition); - BdfList pendingList = new BdfList(); - for (Entry e : queueState.pending.entrySet()) - pendingList.add(BdfList.of(e.getKey(), e.getValue())); - d.put(PENDING_MESSAGES_KEY, pendingList); - Metadata groupMeta = new Metadata(); - groupMeta.put(QUEUE_STATE_KEY, clientHelper.toByteArray(d)); - db.mergeGroupMetadata(txn, g, groupMeta); - } catch (FormatException e) { - throw new RuntimeException(e); - } - } - - private static class QueueState { - - private long outgoingPosition, incomingPosition; - private final TreeMap pending; - - private QueueState(long outgoingPosition, long incomingPosition, - TreeMap pending) { - this.outgoingPosition = outgoingPosition; - this.incomingPosition = incomingPosition; - this.pending = pending; - } - - @Nullable - MessageId popIncomingMessageId() { - Iterator> it = pending.entrySet().iterator(); - if (!it.hasNext()) { - LOG.info("No pending messages"); - return null; - } - Entry e = it.next(); - if (!e.getKey().equals(incomingPosition)) { - if (LOG.isLoggable(INFO)) { - LOG.info("First pending message is " + e.getKey() + ", " - + " expecting " + incomingPosition); - } - return null; - } - if (LOG.isLoggable(INFO)) - LOG.info("Removing pending message " + e.getKey()); - it.remove(); - incomingPosition++; - return e.getValue(); - } - } - - @NotNullByDefault - private static class DelegatingMessageValidator - implements MessageValidator { - - private final QueueMessageValidator delegate; - - private DelegatingMessageValidator(QueueMessageValidator delegate) { - this.delegate = delegate; - } - - @Override - public MessageContext validateMessage(Message m, Group g) - throws InvalidMessageException { - byte[] raw = m.getRaw(); - if (raw.length < QUEUE_MESSAGE_HEADER_LENGTH) - throw new InvalidMessageException(); - long queuePosition = ByteUtils.readUint64(raw, - MESSAGE_HEADER_LENGTH); - if (queuePosition < 0) throw new InvalidMessageException(); - QueueMessage q = new QueueMessage(m.getId(), m.getGroupId(), - m.getTimestamp(), queuePosition, raw); - return delegate.validateMessage(q, g); - } - } - - @NotNullByDefault - private class DelegatingIncomingMessageHook implements IncomingMessageHook { - - private final IncomingQueueMessageHook delegate; - - private DelegatingIncomingMessageHook( - IncomingQueueMessageHook delegate) { - this.delegate = delegate; - } - - @Override - public boolean incomingMessage(Transaction txn, Message m, - Metadata meta) throws DbException, InvalidMessageException { - long queuePosition = ByteUtils.readUint64(m.getRaw(), - MESSAGE_HEADER_LENGTH); - QueueState queueState = loadQueueState(txn, m.getGroupId()); - if (LOG.isLoggable(INFO)) { - LOG.info("Received message with position " - + queuePosition + ", expecting " - + queueState.incomingPosition); - } - if (queuePosition < queueState.incomingPosition) { - // A message with this queue position has already been seen - LOG.warning("Deleting message with duplicate position"); - db.deleteMessage(txn, m.getId()); - db.deleteMessageMetadata(txn, m.getId()); - } else if (queuePosition > queueState.incomingPosition) { - // The message is out of order, add it to the pending list - LOG.info("Message is out of order, adding to pending list"); - queueState.pending.put(queuePosition, m.getId()); - saveQueueState(txn, m.getGroupId(), queueState); - } else { - // The message is in order - LOG.info("Message is in order, delivering"); - QueueMessage q = new QueueMessage(m.getId(), m.getGroupId(), - m.getTimestamp(), queuePosition, m.getRaw()); - queueState.incomingPosition++; - // Collect any consecutive messages - List consecutive = new ArrayList<>(); - MessageId next; - while ((next = queueState.popIncomingMessageId()) != null) - consecutive.add(next); - // Save the queue state before passing control to the delegate - saveQueueState(txn, m.getGroupId(), queueState); - // Deliver the messages to the delegate - delegate.incomingMessage(txn, q, meta); - for (MessageId id : consecutive) { - byte[] raw = db.getRawMessage(txn, id); - if (raw == null) throw new DbException(); - meta = db.getMessageMetadata(txn, id); - q = queueMessageFactory.createMessage(id, raw); - if (LOG.isLoggable(INFO)) { - LOG.info("Delivering pending message with position " - + q.getQueuePosition()); - } - delegate.incomingMessage(txn, q, meta); - } - } - // message queues are only useful for groups with two members - // so messages don't need to be shared - return false; - } - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/client/QueueMessageFactoryImpl.java b/briar-core/src/main/java/org/briarproject/briar/client/QueueMessageFactoryImpl.java deleted file mode 100644 index 480b7670b..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/client/QueueMessageFactoryImpl.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.briarproject.briar.client; - -import org.briarproject.bramble.api.UniqueId; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageFactory; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.util.ByteUtils; -import org.briarproject.briar.api.client.QueueMessage; -import org.briarproject.briar.api.client.QueueMessageFactory; - -import javax.annotation.concurrent.Immutable; -import javax.inject.Inject; - -import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_LENGTH; -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -import static org.briarproject.bramble.util.ByteUtils.INT_64_BYTES; -import static org.briarproject.briar.api.client.QueueMessage.MAX_QUEUE_MESSAGE_BODY_LENGTH; -import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH; - -@Immutable -@NotNullByDefault -class QueueMessageFactoryImpl implements QueueMessageFactory { - - private final MessageFactory messageFactory; - - @Inject - QueueMessageFactoryImpl(MessageFactory messageFactory) { - this.messageFactory = messageFactory; - } - - @Override - public QueueMessage createMessage(GroupId groupId, long timestamp, - long queuePosition, byte[] body) { - if (body.length > MAX_QUEUE_MESSAGE_BODY_LENGTH) - throw new IllegalArgumentException(); - byte[] messageBody = new byte[INT_64_BYTES + body.length]; - ByteUtils.writeUint64(queuePosition, messageBody, 0); - System.arraycopy(body, 0, messageBody, INT_64_BYTES, body.length); - Message m = messageFactory.createMessage(groupId, timestamp, - messageBody); - return new QueueMessage(m.getId(), groupId, timestamp, queuePosition, - m.getRaw()); - } - - @Override - public QueueMessage createMessage(MessageId id, byte[] raw) { - if (raw.length < QUEUE_MESSAGE_HEADER_LENGTH) - throw new IllegalArgumentException(); - if (raw.length > MAX_MESSAGE_LENGTH) - throw new IllegalArgumentException(); - byte[] groupId = new byte[UniqueId.LENGTH]; - System.arraycopy(raw, 0, groupId, 0, UniqueId.LENGTH); - long timestamp = ByteUtils.readUint64(raw, UniqueId.LENGTH); - long queuePosition = ByteUtils.readUint64(raw, MESSAGE_HEADER_LENGTH); - return new QueueMessage(id, new GroupId(groupId), timestamp, - queuePosition, raw); - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java new file mode 100644 index 000000000..e9a2d1233 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbortMessage.java @@ -0,0 +1,27 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class AbortMessage extends AbstractIntroductionMessage { + + private final SessionId sessionId; + + protected AbortMessage(MessageId messageId, GroupId groupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId) { + super(messageId, groupId, timestamp, previousMessageId); + this.sessionId = sessionId; + } + + public SessionId getSessionId() { + return sessionId; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractIntroductionMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractIntroductionMessage.java new file mode 100644 index 000000000..240b5ddec --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractIntroductionMessage.java @@ -0,0 +1,45 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +abstract class AbstractIntroductionMessage { + + private final MessageId messageId; + private final GroupId groupId; + private final long timestamp; + @Nullable + private final MessageId previousMessageId; + + AbstractIntroductionMessage(MessageId messageId, GroupId groupId, + long timestamp, @Nullable MessageId previousMessageId) { + this.messageId = messageId; + this.groupId = groupId; + this.timestamp = timestamp; + this.previousMessageId = previousMessageId; + } + + MessageId getMessageId() { + return messageId; + } + + GroupId getGroupId() { + return groupId; + } + + long getTimestamp() { + return timestamp; + } + + @Nullable + MessageId getPreviousMessageId() { + return previousMessageId; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java new file mode 100644 index 000000000..c9002bb0e --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AbstractProtocolEngine.java @@ -0,0 +1,191 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.IntroductionResponse; +import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.briar.introduction.MessageType.ABORT; +import static org.briarproject.briar.introduction.MessageType.ACCEPT; +import static org.briarproject.briar.introduction.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction.MessageType.AUTH; +import static org.briarproject.briar.introduction.MessageType.DECLINE; +import static org.briarproject.briar.introduction.MessageType.REQUEST; + +@Immutable +@NotNullByDefault +abstract class AbstractProtocolEngine + implements ProtocolEngine { + + protected final DatabaseComponent db; + protected final ClientHelper clientHelper; + protected final ContactManager contactManager; + protected final ContactGroupFactory contactGroupFactory; + protected final MessageTracker messageTracker; + protected final IdentityManager identityManager; + protected final MessageParser messageParser; + protected final MessageEncoder messageEncoder; + protected final Clock clock; + + AbstractProtocolEngine( + DatabaseComponent db, + ClientHelper clientHelper, + ContactManager contactManager, + ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, + IdentityManager identityManager, + MessageParser messageParser, + MessageEncoder messageEncoder, + Clock clock) { + this.db = db; + this.clientHelper = clientHelper; + this.contactManager = contactManager; + this.contactGroupFactory = contactGroupFactory; + this.messageTracker = messageTracker; + this.identityManager = identityManager; + this.messageParser = messageParser; + this.messageEncoder = messageEncoder; + this.clock = clock; + } + + Message sendRequestMessage(Transaction txn, PeerSession s, + long timestamp, Author author, @Nullable String message) + throws DbException { + Message m = messageEncoder + .encodeRequestMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), author, message); + sendMessage(txn, REQUEST, s.getSessionId(), m, true); + return m; + } + + Message sendAcceptMessage(Transaction txn, PeerSession s, long timestamp, + byte[] ephemeralPublicKey, long acceptTimestamp, + Map transportProperties, + boolean visible) + throws DbException { + Message m = messageEncoder + .encodeAcceptMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId(), + ephemeralPublicKey, acceptTimestamp, + transportProperties); + sendMessage(txn, ACCEPT, s.getSessionId(), m, visible); + return m; + } + + Message sendDeclineMessage(Transaction txn, PeerSession s, long timestamp, + boolean visible) throws DbException { + Message m = messageEncoder + .encodeDeclineMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId()); + sendMessage(txn, DECLINE, s.getSessionId(), m, visible); + return m; + } + + Message sendAuthMessage(Transaction txn, PeerSession s, long timestamp, + byte[] mac, byte[] signature) throws DbException { + Message m = messageEncoder + .encodeAuthMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId(), mac, + signature); + sendMessage(txn, AUTH, s.getSessionId(), m, false); + return m; + } + + Message sendActivateMessage(Transaction txn, PeerSession s, long timestamp, + byte[] mac) throws DbException { + Message m = messageEncoder + .encodeActivateMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId(), mac); + sendMessage(txn, ACTIVATE, s.getSessionId(), m, false); + return m; + } + + Message sendAbortMessage(Transaction txn, PeerSession s, long timestamp) + throws DbException { + Message m = messageEncoder + .encodeAbortMessage(s.getContactGroupId(), timestamp, + s.getLastLocalMessageId(), s.getSessionId()); + sendMessage(txn, ABORT, s.getSessionId(), m, false); + return m; + } + + private void sendMessage(Transaction txn, MessageType type, + SessionId sessionId, Message m, boolean visibleInConversation) + throws DbException { + BdfDictionary meta = messageEncoder + .encodeMetadata(type, sessionId, m.getTimestamp(), true, true, + visibleInConversation); + try { + clientHelper.addLocalMessage(txn, m, meta, true); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + void broadcastIntroductionResponseReceivedEvent(Transaction txn, + Session s, AuthorId sender, AbstractIntroductionMessage m) + throws DbException { + AuthorId localAuthorId = identityManager.getLocalAuthor(txn).getId(); + Contact c = contactManager.getContact(txn, sender, localAuthorId); + IntroductionResponse response = + new IntroductionResponse(s.getSessionId(), m.getMessageId(), + m.getGroupId(), s.getRole(), m.getTimestamp(), false, + false, false, false, c.getAuthor().getName(), + m instanceof AcceptMessage); + IntroductionResponseReceivedEvent e = + new IntroductionResponseReceivedEvent(c.getId(), response); + txn.attach(e); + } + + void markMessageVisibleInUi(Transaction txn, MessageId m) + throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setVisibleInUi(meta, true); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + boolean isInvalidDependency(@Nullable MessageId lastRemoteMessageId, + @Nullable MessageId dependency) { + if (dependency == null) return lastRemoteMessageId != null; + return lastRemoteMessageId == null || + !dependency.equals(lastRemoteMessageId); + } + + long getLocalTimestamp(long localTimestamp, long requestTimestamp) { + return Math.max( + clock.currentTimeMillis(), + Math.max( + localTimestamp, + requestTimestamp + ) + 1 + ); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java new file mode 100644 index 000000000..6cadae73b --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AcceptMessage.java @@ -0,0 +1,53 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class AcceptMessage extends AbstractIntroductionMessage { + + private final SessionId sessionId; + private final byte[] ephemeralPublicKey; + private final long acceptTimestamp; + private final Map transportProperties; + + protected AcceptMessage(MessageId messageId, GroupId groupId, + long timestamp, @Nullable MessageId previousMessageId, + SessionId sessionId, + byte[] ephemeralPublicKey, + long acceptTimestamp, + Map transportProperties) { + super(messageId, groupId, timestamp, previousMessageId); + this.sessionId = sessionId; + this.ephemeralPublicKey = ephemeralPublicKey; + this.acceptTimestamp = acceptTimestamp; + this.transportProperties = transportProperties; + } + + public SessionId getSessionId() { + return sessionId; + } + + public byte[] getEphemeralPublicKey() { + return ephemeralPublicKey; + } + + public long getAcceptTimestamp() { + return acceptTimestamp; + } + + public Map getTransportProperties() { + return transportProperties; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java new file mode 100644 index 000000000..5f767737d --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/ActivateMessage.java @@ -0,0 +1,33 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class ActivateMessage extends AbstractIntroductionMessage { + + private final SessionId sessionId; + private final byte[] mac; + + protected ActivateMessage(MessageId messageId, GroupId groupId, + long timestamp, MessageId previousMessageId, SessionId sessionId, + byte[] mac) { + super(messageId, groupId, timestamp, previousMessageId); + this.sessionId = sessionId; + this.mac = mac; + } + + public SessionId getSessionId() { + return sessionId; + } + + public byte[] getMac() { + return mac; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java new file mode 100644 index 000000000..1de1a4eb5 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/AuthMessage.java @@ -0,0 +1,38 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class AuthMessage extends AbstractIntroductionMessage { + + private final SessionId sessionId; + private final byte[] mac, signature; + + protected AuthMessage(MessageId messageId, GroupId groupId, + long timestamp, MessageId previousMessageId, SessionId sessionId, + byte[] mac, byte[] signature) { + super(messageId, groupId, timestamp, previousMessageId); + this.sessionId = sessionId; + this.mac = mac; + this.signature = signature; + } + + public SessionId getSessionId() { + return sessionId; + } + + public byte[] getMac() { + return mac; + } + + public byte[] getSignature() { + return signature; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java new file mode 100644 index 000000000..27386b905 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/DeclineMessage.java @@ -0,0 +1,28 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class DeclineMessage extends AbstractIntroductionMessage { + + private final SessionId sessionId; + + protected DeclineMessage(MessageId messageId, GroupId groupId, + long timestamp, @Nullable MessageId previousMessageId, + SessionId sessionId) { + super(messageId, groupId, timestamp, previousMessageId); + this.sessionId = sessionId; + } + + public SessionId getSessionId() { + return sessionId; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeEngine.java deleted file mode 100644 index 2d5bb95c3..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeEngine.java +++ /dev/null @@ -1,372 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.api.client.ProtocolEngine; -import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.api.introduction.IntroduceeAction; -import org.briarproject.briar.api.introduction.IntroduceeProtocolState; -import org.briarproject.briar.api.introduction.IntroductionRequest; -import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent; -import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; - -import javax.annotation.concurrent.Immutable; - -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static org.briarproject.briar.api.introduction.IntroduceeAction.ACK; -import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_ABORT; -import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_ACCEPT; -import static org.briarproject.briar.api.introduction.IntroduceeAction.LOCAL_DECLINE; -import static org.briarproject.briar.api.introduction.IntroduceeAction.REMOTE_ABORT; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_ACK; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REMOTE_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_RESPONSES; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.ERROR; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.FINISHED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ADD_CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; - -@Immutable -@NotNullByDefault -class IntroduceeEngine - implements ProtocolEngine { - - private static final Logger LOG = - Logger.getLogger(IntroduceeEngine.class.getName()); - - @Override - public StateUpdate onLocalAction( - BdfDictionary localState, BdfDictionary localAction) { - - try { - IntroduceeProtocolState currentState = - getState(localState.getLong(STATE)); - int type = localAction.getLong(TYPE).intValue(); - IntroduceeAction action; - if (localState.containsKey(ACCEPT)) action = IntroduceeAction - .getLocal(type, localState.getBoolean(ACCEPT)); - else action = IntroduceeAction.getLocal(type); - IntroduceeProtocolState nextState = currentState.next(action); - - if (action == LOCAL_ABORT && currentState != ERROR) { - return abortSession(currentState, localState); - } - - if (nextState == ERROR) { - if (LOG.isLoggable(WARNING)) { - LOG.warning("Error: Invalid action in state " + - currentState.name()); - } - if (currentState == ERROR) return noUpdate(localState); - else return abortSession(currentState, localState); - } - - List messages = new ArrayList<>(1); - if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) { - localState.put(STATE, nextState.getValue()); - localState.put(ANSWERED, true); - // 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(TRANSPORT, localAction.getDictionary(TRANSPORT)); - } - msg.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME)); - messages.add(msg); - logAction(currentState, localState, msg); - - if (nextState == AWAIT_ACK) { - localState.put(TASK, TASK_ADD_CONTACT); - } - } else if (action == ACK) { - // just send ACK, don't update local state again - BdfDictionary ack = getAckMessage(localState); - messages.add(ack); - } else { - throw new IllegalArgumentException(); - } - List events = Collections.emptyList(); - return new StateUpdate<>(false, false, - localState, messages, events); - } catch (FormatException e) { - throw new IllegalArgumentException(e); - } - } - - @Override - public StateUpdate onMessageReceived( - BdfDictionary localState, BdfDictionary msg) { - - try { - IntroduceeProtocolState currentState = - getState(localState.getLong(STATE)); - int type = msg.getLong(TYPE).intValue(); - IntroduceeAction action = IntroduceeAction.getRemote(type); - IntroduceeProtocolState nextState = currentState.next(action); - - logMessageReceived(currentState, nextState, localState, type, msg); - - if (nextState == ERROR) { - if (currentState != ERROR && action != REMOTE_ABORT) { - return abortSession(currentState, localState); - } else { - return noUpdate(localState); - } - } - - // update local session state with next protocol state - localState.put(STATE, nextState.getValue()); - List messages; - List events; - // we received the introduction request - if (currentState == AWAIT_REQUEST) { - // remember the session ID used by the introducer - localState.put(SESSION_ID, msg.getRaw(SESSION_ID)); - - addRequestData(localState, msg); - messages = Collections.emptyList(); - events = Collections.singletonList(getEvent(localState, msg)); - } - // we had the request and now one response came in _OR_ - // we had sent our response already and now received the other one - else if (currentState == AWAIT_RESPONSES || - currentState == AWAIT_REMOTE_RESPONSE) { - // update next state based on message content - action = IntroduceeAction - .getRemote(type, msg.getBoolean(ACCEPT)); - nextState = currentState.next(action); - localState.put(STATE, nextState.getValue()); - - addResponseData(localState, msg); - if (nextState == AWAIT_ACK) { - localState.put(TASK, TASK_ADD_CONTACT); - } - messages = Collections.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); - addAckData(localState, msg); - messages = Collections.emptyList(); - events = Collections.emptyList(); - } - // we are done (probably declined response), ignore & delete message - else if (currentState == FINISHED) { - return new StateUpdate<>(true, false, localState, - Collections.emptyList(), - Collections.emptyList()); - } - // this should not happen - else { - throw new IllegalArgumentException(); - } - return new StateUpdate<>(false, false, - localState, messages, events); - } catch (FormatException e) { - throw new IllegalArgumentException(e); - } - } - - private void addRequestData(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - localState.put(NAME, msg.getString(NAME)); - localState.put(PUBLIC_KEY, msg.getRaw(PUBLIC_KEY)); - if (msg.containsKey(MSG)) { - localState.put(MSG, msg.getString(MSG)); - } - } - - private void addResponseData(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - if (localState.containsKey(ACCEPT)) { - localState.put(ACCEPT, - localState.getBoolean(ACCEPT) && msg.getBoolean(ACCEPT)); - } else { - localState.put(ACCEPT, msg.getBoolean(ACCEPT)); - } - localState.put(NOT_OUR_RESPONSE, msg.getRaw(MESSAGE_ID)); - - if (msg.getBoolean(ACCEPT)) { - localState.put(TIME, msg.getLong(TIME)); - localState.put(E_PUBLIC_KEY, msg.getRaw(E_PUBLIC_KEY)); - localState.put(TRANSPORT, msg.getDictionary(TRANSPORT)); - } - } - - private void addAckData(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - localState.put(MAC, msg.getRaw(MAC)); - localState.put(SIGNATURE, msg.getRaw(SIGNATURE)); - } - - 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)); - m.put(MAC, localState.getRaw(OUR_MAC)); - m.put(SIGNATURE, localState.getRaw(OUR_SIGNATURE)); - 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()); - LOG.info("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; - - 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()); - LOG.info("Moving on to state " + nextState.name()); - } - - @Override - public StateUpdate onMessageDelivered( - BdfDictionary localState, BdfDictionary delivered) { - try { - return noUpdate(localState); - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - return null; - } - } - - private IntroduceeProtocolState getState(Long state) { - return IntroduceeProtocolState.fromValue(state.intValue()); - } - - private Event getEvent(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - ContactId contactId = - new ContactId(localState.getLong(CONTACT_ID_1).intValue()); - AuthorId authorId = new AuthorId(localState.getRaw(REMOTE_AUTHOR_ID)); - - SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); - MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); - GroupId groupId = new GroupId(msg.getRaw(GROUP_ID)); - long time = msg.getLong(MESSAGE_TIME); - String name = msg.getString(NAME); - String message = msg.getOptionalString(MSG); - boolean exists = localState.getBoolean(EXISTS); - boolean introducesOtherIdentity = - localState.getBoolean(REMOTE_AUTHOR_IS_US); - - IntroductionRequest ir = new IntroductionRequest(sessionId, messageId, - groupId, ROLE_INTRODUCEE, time, false, false, false, false, - authorId, name, false, message, false, exists, - introducesOtherIdentity); - return new IntroductionRequestReceivedEvent(contactId, ir); - } - - private StateUpdate abortSession( - IntroduceeProtocolState currentState, BdfDictionary localState) - throws FormatException { - - if (LOG.isLoggable(WARNING)) - LOG.warning("Aborting protocol session in state " + - currentState.name()); - - localState.put(STATE, ERROR.getValue()); - localState.put(TASK, TASK_ABORT); - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_ABORT); - msg.put(GROUP_ID, localState.getRaw(GROUP_ID)); - msg.put(SESSION_ID, localState.getRaw(SESSION_ID)); - List messages = Collections.singletonList(msg); - - // send abort event - ContactId contactId = - new ContactId(localState.getLong(CONTACT_ID_1).intValue()); - SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); - Event event = new IntroductionAbortedEvent(contactId, sessionId); - List events = Collections.singletonList(event); - - return new StateUpdate<>(false, false, localState, messages, events); - } - - private StateUpdate noUpdate( - BdfDictionary localState) throws FormatException { - - return new StateUpdate<>(false, false, localState, - Collections.emptyList(), - Collections.emptyList()); - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java deleted file mode 100644 index 4666ac92e..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeManager.java +++ /dev/null @@ -1,569 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.Bytes; -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.contact.ContactManager; -import org.briarproject.bramble.api.crypto.CryptoComponent; -import org.briarproject.bramble.api.crypto.KeyPair; -import org.briarproject.bramble.api.crypto.KeyParser; -import org.briarproject.bramble.api.crypto.PrivateKey; -import org.briarproject.bramble.api.crypto.PublicKey; -import org.briarproject.bramble.api.crypto.SecretKey; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.AuthorFactory; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.identity.LocalAuthor; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.properties.TransportProperties; -import org.briarproject.bramble.api.properties.TransportPropertyManager; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent; - -import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; -import javax.inject.Inject; - -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ADDED_CONTACT_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_MAC_KEY_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_NONCE_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.BOB_MAC_KEY_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.BOB_NONCE_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.OUR_TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SHARED_SECRET_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TASK_ADD_CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION; - -@Immutable -@NotNullByDefault -class IntroduceeManager { - - private static final Logger LOG = - Logger.getLogger(IntroduceeManager.class.getName()); - - private final MessageSender messageSender; - private final DatabaseComponent db; - private final ClientHelper clientHelper; - private final Clock clock; - private final CryptoComponent cryptoComponent; - private final TransportPropertyManager transportPropertyManager; - private final AuthorFactory authorFactory; - private final ContactManager contactManager; - private final IdentityManager identityManager; - private final IntroductionGroupFactory introductionGroupFactory; - - @Inject - IntroduceeManager(MessageSender messageSender, DatabaseComponent db, - ClientHelper clientHelper, Clock clock, - CryptoComponent cryptoComponent, - TransportPropertyManager transportPropertyManager, - AuthorFactory authorFactory, ContactManager contactManager, - IdentityManager identityManager, - IntroductionGroupFactory introductionGroupFactory) { - - this.messageSender = messageSender; - this.db = db; - this.clientHelper = clientHelper; - this.clock = clock; - this.cryptoComponent = cryptoComponent; - this.transportPropertyManager = transportPropertyManager; - this.authorFactory = authorFactory; - this.contactManager = contactManager; - this.identityManager = identityManager; - this.introductionGroupFactory = introductionGroupFactory; - } - - 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( - introductionGroupFactory.createLocalGroup().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, storageId); - d.put(ANSWERED, false); - - // check if the contact we are introduced to does already exist - // TODO: Exchange author format version - 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); - - // check if someone is trying to introduce us to ourselves - if (remoteAuthorId.equals(introducer.getLocalAuthorId())) { - LOG.warning("Received Introduction Request to Ourselves"); - throw new FormatException(); - } - - // check if remote author is actually one of our other identities - boolean introducesOtherIdentity = - db.containsLocalAuthor(txn, remoteAuthorId); - d.put(REMOTE_AUTHOR_IS_US, introducesOtherIdentity); - - // save local state to database - clientHelper.addLocalMessage(txn, localMsg, d, false); - - return d; - } - - public void incomingMessage(Transaction txn, BdfDictionary state, - BdfDictionary message) throws DbException, FormatException { - - IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, message, - engine.onMessageReceived(state, message)); - } - - void acceptIntroduction(Transaction txn, BdfDictionary state, - long timestamp) throws DbException, FormatException { - - // get data to connect and derive a shared secret later - long now = clock.currentTimeMillis(); - KeyPair keyPair = cryptoComponent.generateAgreementKeyPair(); - byte[] publicKey = keyPair.getPublic().getEncoded(); - byte[] privateKey = keyPair.getPrivate().getEncoded(); - Map transportProperties = - transportPropertyManager.getLocalProperties(txn); - BdfDictionary tp = encodeTransportProperties(transportProperties); - - // 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); - state.put(OUR_TRANSPORT, tp); - - // define action - BdfDictionary localAction = new BdfDictionary(); - localAction.put(TYPE, TYPE_RESPONSE); - localAction.put(TRANSPORT, tp); - localAction.put(MESSAGE_TIME, timestamp); - - // start engine and process its state update - IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, null, engine.onLocalAction(state, localAction)); - } - - void declineIntroduction(Transaction txn, BdfDictionary state, - long timestamp) throws DbException, FormatException { - - // update session state - state.put(ACCEPT, false); - - // define action - BdfDictionary localAction = new BdfDictionary(); - localAction.put(TYPE, TYPE_RESPONSE); - localAction.put(MESSAGE_TIME, timestamp); - - // start engine and process its state update - IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, null, - engine.onLocalAction(state, localAction)); - } - - private void processStateUpdate(Transaction txn, - @Nullable BdfDictionary msg, - IntroduceeEngine.StateUpdate result) - throws DbException, FormatException { - - // perform actions based on new local state - BdfDictionary followUpAction = 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) { - messageSender.sendMessage(txn, d); - } - - // broadcast events - for (Event event : result.toBroadcast) { - txn.attach(event); - } - - // delete message - if (result.deleteMessage && msg != null) { - MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); - if (LOG.isLoggable(INFO)) { - LOG.info("Deleting message with id " + messageId.hashCode()); - } - db.deleteMessage(txn, messageId); - db.deleteMessageMetadata(txn, messageId); - } - - // process follow up action at the end if available - if (followUpAction != null) { - IntroduceeEngine engine = new IntroduceeEngine(); - processStateUpdate(txn, null, - engine.onLocalAction(result.localState, followUpAction)); - } - } - - @Nullable - private BdfDictionary performTasks(Transaction txn, - BdfDictionary localState) throws FormatException, DbException { - - if (!localState.containsKey(TASK) || localState.get(TASK) == NULL_VALUE) - return null; - - // remember task and remove it from localState - long task = localState.getLong(TASK); - localState.put(TASK, 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 null; - } - - // figure out who takes which role by comparing public keys - byte[] ourPublicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY); - byte[] theirPublicKeyBytes = localState.getRaw(E_PUBLIC_KEY); - int comp = Bytes.COMPARATOR.compare(new Bytes(ourPublicKeyBytes), - new Bytes(theirPublicKeyBytes)); - boolean alice = comp < 0; - - // get our local author - LocalAuthor author = identityManager.getLocalAuthor(txn); - - SecretKey secretKey; - byte[] ourPrivateKeyBytes = localState.getRaw(OUR_PRIVATE_KEY); - try { - // derive secret master key - secretKey = deriveSecretKey(ourPublicKeyBytes, - ourPrivateKeyBytes, alice, theirPublicKeyBytes); - // derive MAC keys and nonces, sign our nonce and calculate MAC - deriveMacKeysAndNonces(localState, author, secretKey, alice); - } catch (GeneralSecurityException e) { - // we can not continue without the signature - throw new DbException(e); - } - - LOG.info("Adding contact in inactive state"); - - // 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 as inactive - // TODO: Exchange author format version - Author remoteAuthor = authorFactory - .createAuthor(localState.getString(NAME), - localState.getRaw(PUBLIC_KEY)); - ContactId contactId = contactManager - .addContact(txn, remoteAuthor, author.getId(), secretKey, - timestamp, alice, false, 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 - Map transportProperties = - parseTransportProperties(localState); - transportPropertyManager.addRemoteProperties(txn, contactId, - 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, NULL_VALUE); - - // define next action: Send ACK - BdfDictionary localAction = new BdfDictionary(); - localAction.put(TYPE, TYPE_ACK); - - // return follow up action to start engine - // and process its state update again - return localAction; - } - - // we sent and received an ACK, so activate contact - if (task == TASK_ACTIVATE_CONTACT) { - if (!localState.getBoolean(EXISTS) && - localState.containsKey(ADDED_CONTACT_ID)) { - try { - LOG.info("Verifying Signature..."); - verifySignature(localState); - LOG.info("Verifying MAC..."); - verifyMac(localState); - } catch (GeneralSecurityException e) { - throw new DbException(e); - } - - LOG.info("Activating Contact..."); - - ContactId contactId = new ContactId( - localState.getLong(ADDED_CONTACT_ID).intValue()); - - // activate and show contact in contact list - contactManager.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(txn, contactId); - } - } - return null; - } - - private SecretKey deriveSecretKey(byte[] ourPublicKeyBytes, - byte[] ourPrivateKeyBytes, boolean alice, - byte[] theirPublicKeyBytes) throws GeneralSecurityException { - // parse the local ephemeral key pair - KeyParser keyParser = cryptoComponent.getAgreementKeyParser(); - PublicKey ourPublicKey; - PrivateKey ourPrivateKey; - try { - ourPublicKey = keyParser.parsePublicKey(ourPublicKeyBytes); - ourPrivateKey = keyParser.parsePrivateKey(ourPrivateKeyBytes); - } catch (GeneralSecurityException e) { - if (LOG.isLoggable(WARNING)) { - LOG.log(WARNING, e.toString(), e); - } - throw new RuntimeException("Our own ephemeral key is invalid"); - } - KeyPair ourKeyPair = new KeyPair(ourPublicKey, ourPrivateKey); - PublicKey theirPublicKey = - keyParser.parsePublicKey(theirPublicKeyBytes); - - // The shared secret is derived from the local ephemeral key pair - // and the remote ephemeral public key - byte[][] inputs = { - new byte[] {CLIENT_VERSION}, - alice ? ourPublicKeyBytes : theirPublicKeyBytes, - alice ? theirPublicKeyBytes : ourPublicKeyBytes - }; - return cryptoComponent.deriveSharedSecret(SHARED_SECRET_LABEL, - theirPublicKey, ourKeyPair, inputs); - } - - /** - * Derives nonces, signs our nonce and calculates MAC - *

- * Derives two nonces and two MAC keys from the shared secret key. - * The other introducee's nonce and MAC key are added to the localState. - *

- * Our nonce is signed with the local author's long-term private key. - * The signature is added to the localState. - *

- * Calculates a MAC and stores it in the localState. - */ - private void deriveMacKeysAndNonces(BdfDictionary localState, - LocalAuthor author, SecretKey secretKey, boolean alice) - throws FormatException, GeneralSecurityException { - // Derive two nonces and two MAC keys from the shared secret key - String ourNonceLabel = alice ? ALICE_NONCE_LABEL : BOB_NONCE_LABEL; - String theirNonceLabel = alice ? BOB_NONCE_LABEL : ALICE_NONCE_LABEL; - byte[] ourNonce = cryptoComponent.mac(ourNonceLabel, secretKey); - byte[] theirNonce = cryptoComponent.mac(theirNonceLabel, secretKey); - String ourKeyLabel = alice ? ALICE_MAC_KEY_LABEL : BOB_MAC_KEY_LABEL; - String theirKeyLabel = alice ? BOB_MAC_KEY_LABEL : ALICE_MAC_KEY_LABEL; - SecretKey ourMacKey = cryptoComponent.deriveKey(ourKeyLabel, secretKey); - SecretKey theirMacKey = - cryptoComponent.deriveKey(theirKeyLabel, secretKey); - - // Save the other nonce and MAC key for the verification - localState.put(NONCE, theirNonce); - localState.put(MAC_KEY, theirMacKey.getBytes()); - - // Sign our nonce with our long-term identity public key - byte[] sig = cryptoComponent.sign(SIGNING_LABEL, ourNonce, - author.getPrivateKey()); - - // Calculate a MAC over identity public key, ephemeral public key, - // transport properties and timestamp. - byte[] publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY); - BdfDictionary tp = localState.getDictionary(OUR_TRANSPORT); - long ourTime = localState.getLong(OUR_TIME); - BdfList toMacList = BdfList.of(author.getPublicKey(), - publicKeyBytes, tp, ourTime); - byte[] toMac = clientHelper.toByteArray(toMacList); - byte[] mac = cryptoComponent.mac(MAC_LABEL, ourMacKey, toMac); - - // Add MAC and signature to localState, so it can be included in ACK - localState.put(OUR_MAC, mac); - localState.put(OUR_SIGNATURE, sig); - } - - void verifySignature(BdfDictionary localState) - throws FormatException, GeneralSecurityException { - byte[] nonce = localState.getRaw(NONCE); - byte[] sig = localState.getRaw(SIGNATURE); - byte[] key = localState.getRaw(PUBLIC_KEY); - - // Verify the signature - if (!cryptoComponent.verifySignature(sig, SIGNING_LABEL, nonce, key)) { - LOG.warning("Invalid nonce signature in ACK"); - throw new GeneralSecurityException(); - } - } - - void verifyMac(BdfDictionary localState) - throws FormatException, GeneralSecurityException { - // get MAC and MAC key from session state - byte[] mac = localState.getRaw(MAC); - byte[] macKeyBytes = localState.getRaw(MAC_KEY); - SecretKey macKey = new SecretKey(macKeyBytes); - - // get MAC data and calculate a new MAC with stored key - byte[] pubKey = localState.getRaw(PUBLIC_KEY); - byte[] ePubKey = localState.getRaw(E_PUBLIC_KEY); - BdfDictionary tp = localState.getDictionary(TRANSPORT); - long timestamp = localState.getLong(TIME); - BdfList toMacList = BdfList.of(pubKey, ePubKey, tp, timestamp); - byte[] toMac = clientHelper.toByteArray(toMacList); - byte[] calculatedMac = cryptoComponent.mac(MAC_LABEL, macKey, toMac); - if (!Arrays.equals(mac, calculatedMac)) { - LOG.warning("Received ACK with invalid MAC"); - throw new GeneralSecurityException(); - } - } - - public void abort(Transaction txn, BdfDictionary state) { - IntroduceeEngine engine = new IntroduceeEngine(); - BdfDictionary localAction = new BdfDictionary(); - localAction.put(TYPE, TYPE_ABORT); - try { - processStateUpdate(txn, null, - engine.onLocalAction(state, localAction)); - } catch (DbException | IOException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } - } - - private BdfDictionary encodeTransportProperties( - Map map) { - - BdfDictionary d = new BdfDictionary(); - for (Map.Entry e : map.entrySet()) { - d.put(e.getKey().getString(), e.getValue()); - } - return d; - } - - private Map parseTransportProperties( - BdfDictionary d) throws FormatException { - - Map tpMap = new HashMap<>(); - BdfDictionary tpMapDict = d.getDictionary(TRANSPORT); - for (String key : tpMapDict.keySet()) { - TransportId transportId = new TransportId(key); - TransportProperties transportProperties = new TransportProperties(); - BdfDictionary tpDict = tpMapDict.getDictionary(key); - for (String tkey : tpDict.keySet()) { - transportProperties.put(tkey, tpDict.getString(tkey)); - } - tpMap.put(transportId, transportProperties); - } - return tpMap; - } - -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java new file mode 100644 index 000000000..c0cf65a07 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeProtocolEngine.java @@ -0,0 +1,567 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.Contact; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.db.ContactExistsException; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.properties.TransportPropertyManager; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.api.transport.KeyManager; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.ProtocolStateException; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.IntroductionRequest; +import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent; +import org.briarproject.briar.api.introduction.event.IntroductionRequestReceivedEvent; +import org.briarproject.briar.api.introduction.event.IntroductionSucceededEvent; + +import java.security.GeneralSecurityException; +import java.util.Map; +import java.util.logging.Logger; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; +import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_AUTH; +import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_RESPONSES; +import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_ACCEPTED; +import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED; +import static org.briarproject.briar.introduction.IntroduceeState.REMOTE_ACCEPTED; +import static org.briarproject.briar.introduction.IntroduceeState.START; + +@Immutable +@NotNullByDefault +class IntroduceeProtocolEngine + extends AbstractProtocolEngine { + + private final static Logger LOG = + Logger.getLogger(IntroduceeProtocolEngine.class.getSimpleName()); + + private final IntroductionCrypto crypto; + private final KeyManager keyManager; + private final TransportPropertyManager transportPropertyManager; + + @Inject + IntroduceeProtocolEngine( + DatabaseComponent db, + ClientHelper clientHelper, + ContactManager contactManager, + ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, + IdentityManager identityManager, + MessageParser messageParser, + MessageEncoder messageEncoder, + Clock clock, + IntroductionCrypto crypto, + KeyManager keyManager, + TransportPropertyManager transportPropertyManager) { + super(db, clientHelper, contactManager, contactGroupFactory, + messageTracker, identityManager, messageParser, messageEncoder, + clock); + this.crypto = crypto; + this.keyManager = keyManager; + this.transportPropertyManager = transportPropertyManager; + } + + @Override + public IntroduceeSession onRequestAction(Transaction txn, + IntroduceeSession session, @Nullable String message, + long timestamp) { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public IntroduceeSession onAcceptAction(Transaction txn, + IntroduceeSession session, long timestamp) throws DbException { + switch (session.getState()) { + case AWAIT_RESPONSES: + case REMOTE_ACCEPTED: + return onLocalAccept(txn, session, timestamp); + case START: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onDeclineAction(Transaction txn, + IntroduceeSession session, long timestamp) throws DbException { + switch (session.getState()) { + case AWAIT_RESPONSES: + case REMOTE_ACCEPTED: + return onLocalDecline(txn, session, timestamp); + case START: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onRequestMessage(Transaction txn, + IntroduceeSession session, RequestMessage m) throws DbException { + switch (session.getState()) { + case START: + return onRemoteRequest(txn, session, m); + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onAcceptMessage(Transaction txn, + IntroduceeSession session, AcceptMessage m) throws DbException { + switch (session.getState()) { + case LOCAL_DECLINED: + return onRemoteResponseWhenDeclined(txn, session, m); + case AWAIT_RESPONSES: + case LOCAL_ACCEPTED: + return onRemoteAccept(txn, session, m); + case START: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onDeclineMessage(Transaction txn, + IntroduceeSession session, DeclineMessage m) throws DbException { + switch (session.getState()) { + case LOCAL_DECLINED: + return onRemoteResponseWhenDeclined(txn, session, m); + case AWAIT_RESPONSES: + case LOCAL_ACCEPTED: + return onRemoteDecline(txn, session, m); + case START: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onAuthMessage(Transaction txn, + IntroduceeSession session, AuthMessage m) throws DbException { + switch (session.getState()) { + case AWAIT_AUTH: + return onRemoteAuth(txn, session, m); + case START: + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case REMOTE_ACCEPTED: + case AWAIT_ACTIVATE: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onActivateMessage(Transaction txn, + IntroduceeSession session, ActivateMessage m) throws DbException { + switch (session.getState()) { + case AWAIT_ACTIVATE: + return onRemoteActivate(txn, session, m); + case START: + case AWAIT_RESPONSES: + case LOCAL_DECLINED: + case LOCAL_ACCEPTED: + case REMOTE_ACCEPTED: + case AWAIT_AUTH: + return abort(txn, session); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroduceeSession onAbortMessage(Transaction txn, + IntroduceeSession session, AbortMessage m) throws DbException { + return onRemoteAbort(txn, session, m); + } + + private IntroduceeSession onRemoteRequest(Transaction txn, + IntroduceeSession s, RequestMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Mark the request visible in the UI and available to answer + markMessageVisibleInUi(txn, m.getMessageId()); + markRequestAvailableToAnswer(txn, m.getMessageId(), true); + + // Add SessionId to message metadata + addSessionId(txn, m.getMessageId(), s.getSessionId()); + + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Broadcast IntroductionRequestReceivedEvent + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + Contact c = contactManager.getContact(txn, s.getIntroducer().getId(), + localAuthor.getId()); + boolean contactExists = contactManager + .contactExists(txn, m.getAuthor().getId(), localAuthor.getId()); + IntroductionRequest request = + new IntroductionRequest(s.getSessionId(), m.getMessageId(), + m.getGroupId(), INTRODUCEE, m.getTimestamp(), false, + false, false, false, m.getAuthor().getName(), false, + m.getMessage(), false, contactExists); + IntroductionRequestReceivedEvent e = + new IntroductionRequestReceivedEvent(c.getId(), request); + txn.attach(e); + + // Move to the AWAIT_RESPONSES state + return IntroduceeSession.addRemoteRequest(s, AWAIT_RESPONSES, m); + } + + private IntroduceeSession onLocalAccept(Transaction txn, + IntroduceeSession s, long timestamp) throws DbException { + // Mark the request message unavailable to answer + markRequestsUnavailableToAnswer(txn, s); + + // Create ephemeral key pair and get local transport properties + KeyPair keyPair = crypto.generateKeyPair(); + byte[] publicKey = keyPair.getPublic().getEncoded(); + byte[] privateKey = keyPair.getPrivate().getEncoded(); + Map transportProperties = + transportPropertyManager.getLocalProperties(txn); + + // Send a ACCEPT message + long localTimestamp = + Math.max(timestamp + 1, getLocalTimestamp(s)); + Message sent = sendAcceptMessage(txn, s, localTimestamp, publicKey, + localTimestamp, transportProperties, true); + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + + // Determine the next state + IntroduceeState state = + s.getState() == AWAIT_RESPONSES ? LOCAL_ACCEPTED : AWAIT_AUTH; + IntroduceeSession sNew = IntroduceeSession + .addLocalAccept(s, state, sent, publicKey, privateKey, + localTimestamp, transportProperties); + + if (state == AWAIT_AUTH) { + // Move to the AWAIT_AUTH state + return onLocalAuth(txn, sNew); + } + // Move to the LOCAL_ACCEPTED state + return sNew; + } + + private IntroduceeSession onLocalDecline(Transaction txn, + IntroduceeSession s, long timestamp) throws DbException { + // Mark the request message unavailable to answer + markRequestsUnavailableToAnswer(txn, s); + + // Send a DECLINE message + long localTimestamp = Math.max(timestamp + 1, getLocalTimestamp(s)); + Message sent = sendDeclineMessage(txn, s, localTimestamp, true); + + // Track the message + messageTracker.trackOutgoingMessage(txn, sent); + + // Move to the START or LOCAL_DECLINED state, if still awaiting response + IntroduceeState state = + s.getState() == REMOTE_ACCEPTED ? START : LOCAL_DECLINED; + return IntroduceeSession + .clear(s, state, sent.getId(), sent.getTimestamp(), + s.getLastRemoteMessageId()); + } + + private IntroduceeSession onRemoteAccept(Transaction txn, + IntroduceeSession s, AcceptMessage m) + throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Determine next state + IntroduceeState state = + s.getState() == AWAIT_RESPONSES ? REMOTE_ACCEPTED : AWAIT_AUTH; + + if (state == AWAIT_AUTH) { + // Move to the AWAIT_AUTH state and send own auth message + return onLocalAuth(txn, + IntroduceeSession.addRemoteAccept(s, AWAIT_AUTH, m)); + } + // Move to the REMOTE_ACCEPTED state + return IntroduceeSession.addRemoteAccept(s, state, m); + } + + private IntroduceeSession onRemoteDecline(Transaction txn, + IntroduceeSession s, DeclineMessage m) throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Broadcast IntroductionResponseReceivedEvent + broadcastIntroductionResponseReceivedEvent(txn, s, + s.getIntroducer().getId(), m); + + // Move back to START state + return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(), + s.getLocalTimestamp(), m.getMessageId()); + } + + private IntroduceeSession onRemoteResponseWhenDeclined(Transaction txn, + IntroduceeSession s, AbstractIntroductionMessage m) + throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Move to START state + return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(), + s.getLocalTimestamp(), m.getMessageId()); + } + + private IntroduceeSession onLocalAuth(Transaction txn, IntroduceeSession s) + throws DbException { + byte[] mac; + byte[] signature; + SecretKey masterKey, aliceMacKey, bobMacKey; + try { + masterKey = crypto.deriveMasterKey(s); + aliceMacKey = crypto.deriveMacKey(masterKey, true); + bobMacKey = crypto.deriveMacKey(masterKey, false); + SecretKey ourMacKey = s.getLocal().alice ? aliceMacKey : bobMacKey; + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + mac = crypto.authMac(ourMacKey, s, localAuthor.getId()); + signature = crypto.sign(ourMacKey, localAuthor.getPrivateKey()); + } catch (GeneralSecurityException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + return abort(txn, s); + } + if (s.getState() != AWAIT_AUTH) throw new AssertionError(); + Message sent = sendAuthMessage(txn, s, getLocalTimestamp(s), mac, + signature); + return IntroduceeSession.addLocalAuth(s, AWAIT_AUTH, sent, masterKey, + aliceMacKey, bobMacKey); + } + + private IntroduceeSession onRemoteAuth(Transaction txn, + IntroduceeSession s, AuthMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + try { + crypto.verifyAuthMac(m.getMac(), s, localAuthor.getId()); + crypto.verifySignature(m.getSignature(), s); + } catch (GeneralSecurityException e) { + return abort(txn, s); + } + long timestamp = Math.min(s.getLocal().acceptTimestamp, + s.getRemote().acceptTimestamp); + if (timestamp == -1) throw new AssertionError(); + + Map keys = null; + try { + contactManager + .addContact(txn, s.getRemote().author, localAuthor.getId(), + false, true); + + // Only add transport properties and keys when the contact was added + // This will be changed once we have a way to reset state for peers + // that were contacts already at some point in the past. + Contact c = contactManager + .getContact(txn, s.getRemote().author.getId(), + localAuthor.getId()); + + // bind the keys to the new contact + //noinspection ConstantConditions + keys = keyManager + .addUnboundKeys(txn, new SecretKey(s.getMasterKey()), + timestamp, s.getRemote().alice); + keyManager.bindKeys(txn, c.getId(), keys); + + // add signed transport properties for the contact + //noinspection ConstantConditions + transportPropertyManager.addRemoteProperties(txn, c.getId(), + s.getRemote().transportProperties); + + // Broadcast IntroductionSucceededEvent, because contact got added + IntroductionSucceededEvent e = new IntroductionSucceededEvent(c); + txn.attach(e); + } catch (ContactExistsException e) { + // Ignore this, because the other introducee might have deleted us. + // So we still want updated transport properties + // and new transport keys. + } + + // send ACTIVATE message with a MAC + byte[] mac = crypto.activateMac(s); + Message sent = sendActivateMessage(txn, s, getLocalTimestamp(s), mac); + + // Move to AWAIT_ACTIVATE state and clear key material from session + return IntroduceeSession.awaitActivate(s, m, sent, keys); + } + + private IntroduceeSession onRemoteActivate(Transaction txn, + IntroduceeSession s, ActivateMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + + // Validate MAC + try { + crypto.verifyActivateMac(m.getMac(), s); + } catch (GeneralSecurityException e) { + return abort(txn, s); + } + + // We might not have added transport keys + // if the contact existed when the remote AUTH was received. + if (s.getTransportKeys() != null) { + // Activate transport keys + keyManager.activateKeys(txn, s.getTransportKeys()); + } + + // Move back to START state + return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(), + s.getLocalTimestamp(), m.getMessageId()); + } + + private IntroduceeSession onRemoteAbort(Transaction txn, + IntroduceeSession s, AbortMessage m) + throws DbException { + // Mark the request message unavailable to answer + markRequestsUnavailableToAnswer(txn, s); + + // Broadcast abort event for testing + txn.attach(new IntroductionAbortedEvent(s.getSessionId())); + + // Reset the session back to initial state + return IntroduceeSession.clear(s, START, s.getLastLocalMessageId(), + s.getLocalTimestamp(), m.getMessageId()); + } + + private IntroduceeSession abort(Transaction txn, IntroduceeSession s) + throws DbException { + // Mark the request message unavailable to answer + markRequestsUnavailableToAnswer(txn, s); + + // Send an ABORT message + Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s)); + + // Broadcast abort event for testing + txn.attach(new IntroductionAbortedEvent(s.getSessionId())); + + // Reset the session back to initial state + return IntroduceeSession + .clear(s, START, sent.getId(), sent.getTimestamp(), + s.getLastRemoteMessageId()); + } + + private boolean isInvalidDependency(IntroduceeSession s, + @Nullable MessageId dependency) { + return isInvalidDependency(s.getLastRemoteMessageId(), dependency); + } + + private long getLocalTimestamp(IntroduceeSession s) { + return getLocalTimestamp(s.getLocalTimestamp(), + s.getRequestTimestamp()); + } + + private void addSessionId(Transaction txn, MessageId m, SessionId sessionId) + throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.addSessionId(meta, sessionId); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + private void markRequestsUnavailableToAnswer(Transaction txn, + IntroduceeSession s) throws DbException { + BdfDictionary query = messageParser + .getRequestsAvailableToAnswerQuery(s.getSessionId()); + try { + Map results = + clientHelper.getMessageMetadataAsDictionary(txn, + s.getContactGroupId(), query); + for (MessageId m : results.keySet()) + markRequestAvailableToAnswer(txn, m, false); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + private void markRequestAvailableToAnswer(Transaction txn, MessageId m, + boolean available) throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setAvailableToAnswer(meta, available); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java new file mode 100644 index 000000000..b952b3dc5 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeSession.java @@ -0,0 +1,255 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.Role; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.briar.introduction.IntroduceeState.AWAIT_ACTIVATE; +import static org.briarproject.briar.introduction.IntroduceeState.START; +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; + +@Immutable +@NotNullByDefault +class IntroduceeSession extends Session + implements PeerSession { + + private final GroupId contactGroupId; + private final Author introducer; + private final Local local; + private final Remote remote; + @Nullable + private final byte[] masterKey; + @Nullable + private final Map transportKeys; + + IntroduceeSession(SessionId sessionId, IntroduceeState state, + long requestTimestamp, GroupId contactGroupId, Author introducer, + Local local, Remote remote, @Nullable byte[] masterKey, + @Nullable Map transportKeys) { + super(sessionId, state, requestTimestamp); + this.contactGroupId = contactGroupId; + this.introducer = introducer; + this.local = local; + this.remote = remote; + this.masterKey = masterKey; + this.transportKeys = transportKeys; + } + + static IntroduceeSession getInitial(GroupId contactGroupId, + SessionId sessionId, Author introducer, boolean localIsAlice, + Author remoteAuthor) { + Local local = + new Local(localIsAlice, null, -1, null, null, null, -1, null); + Remote remote = + new Remote(!localIsAlice, remoteAuthor, null, null, null, -1, + null); + return new IntroduceeSession(sessionId, START, -1, contactGroupId, + introducer, local, remote, null, null); + } + + static IntroduceeSession addRemoteRequest(IntroduceeSession s, + IntroduceeState state, RequestMessage m) { + Remote remote = new Remote(s.remote, m.getMessageId()); + return new IntroduceeSession(s.getSessionId(), state, m.getTimestamp(), + s.contactGroupId, s.introducer, s.local, remote, s.masterKey, + s.transportKeys); + } + + static IntroduceeSession addLocalAccept(IntroduceeSession s, + IntroduceeState state, Message acceptMessage, + byte[] ephemeralPublicKey, byte[] ephemeralPrivateKey, + long acceptTimestamp, + Map transportProperties) { + Local local = new Local(s.local.alice, acceptMessage.getId(), + acceptMessage.getTimestamp(), ephemeralPublicKey, + ephemeralPrivateKey, transportProperties, acceptTimestamp, + null); + return new IntroduceeSession(s.getSessionId(), state, + s.getRequestTimestamp(), s.contactGroupId, s.introducer, local, + s.remote, s.masterKey, s.transportKeys); + } + + static IntroduceeSession addRemoteAccept(IntroduceeSession s, + IntroduceeState state, AcceptMessage m) { + Remote remote = + new Remote(s.remote.alice, s.remote.author, m.getMessageId(), + m.getEphemeralPublicKey(), m.getTransportProperties(), + m.getAcceptTimestamp(), s.remote.macKey); + return new IntroduceeSession(s.getSessionId(), state, + s.getRequestTimestamp(), s.contactGroupId, s.introducer, + s.local, remote, s.masterKey, s.transportKeys); + } + + static IntroduceeSession addLocalAuth(IntroduceeSession s, + IntroduceeState state, Message m, SecretKey masterKey, + SecretKey aliceMacKey, SecretKey bobMacKey) { + // add mac key and sent message + Local local = new Local(s.local.alice, m.getId(), m.getTimestamp(), + s.local.ephemeralPublicKey, s.local.ephemeralPrivateKey, + s.local.transportProperties, s.local.acceptTimestamp, + s.local.alice ? aliceMacKey.getBytes() : bobMacKey.getBytes()); + // just add the mac key + Remote remote = new Remote(s.remote.alice, s.remote.author, + s.remote.lastMessageId, s.remote.ephemeralPublicKey, + s.remote.transportProperties, s.remote.acceptTimestamp, + s.remote.alice ? aliceMacKey.getBytes() : bobMacKey.getBytes()); + // add master key + return new IntroduceeSession(s.getSessionId(), state, + s.getRequestTimestamp(), s.contactGroupId, s.introducer, local, + remote, masterKey.getBytes(), s.transportKeys); + } + + static IntroduceeSession awaitActivate(IntroduceeSession s, AuthMessage m, + Message sent, @Nullable Map transportKeys) { + Local local = new Local(s.local, sent.getId(), sent.getTimestamp()); + Remote remote = new Remote(s.remote, m.getMessageId()); + return new IntroduceeSession(s.getSessionId(), AWAIT_ACTIVATE, + s.getRequestTimestamp(), s.contactGroupId, s.introducer, local, + remote, null, transportKeys); + } + + static IntroduceeSession clear(IntroduceeSession s, IntroduceeState state, + @Nullable MessageId lastLocalMessageId, long localTimestamp, + @Nullable MessageId lastRemoteMessageId) { + Local local = + new Local(s.local.alice, lastLocalMessageId, localTimestamp, + null, null, null, -1, null); + Remote remote = + new Remote(s.remote.alice, s.remote.author, lastRemoteMessageId, + null, null, -1, null); + return new IntroduceeSession(s.getSessionId(), state, + s.getRequestTimestamp(), s.contactGroupId, s.introducer, local, + remote, null, null); + } + + @Override + Role getRole() { + return INTRODUCEE; + } + + @Override + public GroupId getContactGroupId() { + return contactGroupId; + } + + @Override + public long getLocalTimestamp() { + return local.lastMessageTimestamp; + } + + @Nullable + @Override + public MessageId getLastLocalMessageId() { + return local.lastMessageId; + } + + @Nullable + @Override + public MessageId getLastRemoteMessageId() { + return remote.lastMessageId; + } + + Author getIntroducer() { + return introducer; + } + + public Local getLocal() { + return local; + } + + public Remote getRemote() { + return remote; + } + + @Nullable + byte[] getMasterKey() { + return masterKey; + } + + @Nullable + Map getTransportKeys() { + return transportKeys; + } + + abstract static class Common { + final boolean alice; + @Nullable + final MessageId lastMessageId; + @Nullable + final byte[] ephemeralPublicKey; + @Nullable + final Map transportProperties; + final long acceptTimestamp; + @Nullable + final byte[] macKey; + + private Common(boolean alice, @Nullable MessageId lastMessageId, + @Nullable byte[] ephemeralPublicKey, @Nullable + Map transportProperties, + long acceptTimestamp, @Nullable byte[] macKey) { + this.alice = alice; + this.lastMessageId = lastMessageId; + this.ephemeralPublicKey = ephemeralPublicKey; + this.transportProperties = transportProperties; + this.acceptTimestamp = acceptTimestamp; + this.macKey = macKey; + } + } + + static class Local extends Common { + final long lastMessageTimestamp; + @Nullable + final byte[] ephemeralPrivateKey; + + Local(boolean alice, @Nullable MessageId lastMessageId, + long lastMessageTimestamp, @Nullable byte[] ephemeralPublicKey, + @Nullable byte[] ephemeralPrivateKey, @Nullable + Map transportProperties, + long acceptTimestamp, @Nullable byte[] macKey) { + super(alice, lastMessageId, ephemeralPublicKey, transportProperties, + acceptTimestamp, macKey); + this.lastMessageTimestamp = lastMessageTimestamp; + this.ephemeralPrivateKey = ephemeralPrivateKey; + } + + private Local(Local s, @Nullable MessageId lastMessageId, + long lastMessageTimestamp) { + this(s.alice, lastMessageId, lastMessageTimestamp, + s.ephemeralPublicKey, s.ephemeralPrivateKey, + s.transportProperties, s.acceptTimestamp, s.macKey); + } + } + + static class Remote extends Common { + final Author author; + + Remote(boolean alice, Author author, + @Nullable MessageId lastMessageId, + @Nullable byte[] ephemeralPublicKey, @Nullable + Map transportProperties, + long acceptTimestamp, @Nullable byte[] macKey) { + super(alice, lastMessageId, ephemeralPublicKey, transportProperties, + acceptTimestamp, macKey); + this.author = author; + } + + private Remote(Remote s, @Nullable MessageId lastMessageId) { + this(s.alice, s.author, lastMessageId, s.ephemeralPublicKey, + s.transportProperties, s.acceptTimestamp, s.macKey); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java new file mode 100644 index 000000000..7a54abde8 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroduceeState.java @@ -0,0 +1,36 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum IntroduceeState implements State { + + START(0), + AWAIT_RESPONSES(1), + LOCAL_DECLINED(2), + LOCAL_ACCEPTED(3), + REMOTE_ACCEPTED(4), + AWAIT_AUTH(5), + AWAIT_ACTIVATE(6); + + private final int value; + + IntroduceeState(int value) { + this.value = value; + } + + @Override + public int getValue() { + return value; + } + + static IntroduceeState fromValue(int value) throws FormatException { + for (IntroduceeState s : values()) if (s.value == value) return s; + throw new FormatException(); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerEngine.java deleted file mode 100644 index df364b34f..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerEngine.java +++ /dev/null @@ -1,370 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.briar.api.client.ProtocolEngine; -import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.api.introduction.IntroducerAction; -import org.briarproject.briar.api.introduction.IntroducerProtocolState; -import org.briarproject.briar.api.introduction.IntroductionResponse; -import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent; -import org.briarproject.briar.api.introduction.event.IntroductionResponseReceivedEvent; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.logging.Logger; - -import javax.annotation.concurrent.Immutable; - -import static java.util.logging.Level.INFO; -import static java.util.logging.Level.WARNING; -import static org.briarproject.briar.api.introduction.IntroducerAction.LOCAL_ABORT; -import static org.briarproject.briar.api.introduction.IntroducerAction.LOCAL_REQUEST; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_1; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_ACCEPT_2; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_1; -import static org.briarproject.briar.api.introduction.IntroducerAction.REMOTE_DECLINE_2; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_ACKS; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_ACK_1; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_ACK_2; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_1; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_2; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.ERROR; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.FINISHED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; - -@Immutable -@NotNullByDefault -class IntroducerEngine - implements ProtocolEngine { - - private static final Logger LOG = - Logger.getLogger(IntroducerEngine.class.getName()); - - @Override - public StateUpdate onLocalAction( - BdfDictionary localState, BdfDictionary localAction) { - - try { - IntroducerProtocolState currentState = - getState(localState.getLong(STATE)); - int type = localAction.getLong(TYPE).intValue(); - IntroducerAction action = IntroducerAction.getLocal(type); - IntroducerProtocolState nextState = currentState.next(action); - - if (action == LOCAL_ABORT && currentState != ERROR) { - return abortSession(currentState, localState); - } - - if (nextState == ERROR) { - if (LOG.isLoggable(WARNING)) { - LOG.warning("Error: Invalid action in state " + - currentState.name()); - } - return noUpdate(localState); - } - - localState.put(STATE, nextState.getValue()); - if (action == LOCAL_REQUEST) { - // create the introduction requests for both contacts - List messages = new ArrayList<>(2); - BdfDictionary msg1 = new BdfDictionary(); - msg1.put(TYPE, TYPE_REQUEST); - msg1.put(SESSION_ID, localState.getRaw(SESSION_ID)); - msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); - msg1.put(NAME, localState.getString(CONTACT_2)); - msg1.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY2)); - if (localAction.containsKey(MSG)) { - msg1.put(MSG, localAction.getString(MSG)); - } - msg1.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME)); - messages.add(msg1); - logLocalAction(currentState, localState); - 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)); - } - msg2.put(MESSAGE_TIME, localAction.getLong(MESSAGE_TIME)); - messages.add(msg2); - logLocalAction(currentState, localState); - - List events = Collections.emptyList(); - return new StateUpdate<>(false, false, - localState, messages, events); - } else { - throw new IllegalArgumentException("Unknown Local Action"); - } - } catch (FormatException e) { - throw new IllegalArgumentException(e); - } - } - - @Override - public StateUpdate onMessageReceived( - BdfDictionary localState, BdfDictionary msg) { - - try { - IntroducerProtocolState currentState = - getState(localState.getLong(STATE)); - int type = msg.getLong(TYPE).intValue(); - boolean one = isContact1(localState, msg); - IntroducerAction action = IntroducerAction.getRemote(type, one); - IntroducerProtocolState nextState = currentState.next(action); - - logMessageReceived(currentState, nextState, localState, type, msg); - - if (nextState == ERROR) { - if (currentState != ERROR) { - return abortSession(currentState, localState); - } else { - return noUpdate(localState); - } - } - - List messages; - List events; - - // we have sent our requests and just got the 1st or 2nd response - if (currentState == AWAIT_RESPONSES || - currentState == AWAIT_RESPONSE_1 || - currentState == AWAIT_RESPONSE_2) { - // update next state based on message content - action = IntroducerAction - .getRemote(type, one, msg.getBoolean(ACCEPT)); - nextState = currentState.next(action); - localState.put(STATE, nextState.getValue()); - if (one) localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID)); - else localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID)); - - messages = forwardMessage(localState, msg); - events = Collections.singletonList(getEvent(localState, msg)); - } - // we have forwarded both responses and now received the 1st or 2nd ACK - else if (currentState == AWAIT_ACKS || - currentState == AWAIT_ACK_1 || - currentState == AWAIT_ACK_2) { - localState.put(STATE, nextState.getValue()); - messages = forwardMessage(localState, msg); - events = Collections.emptyList(); - } - // we probably received a response while already being FINISHED - else if (currentState == FINISHED) { - // if it was a response store it to be found later - if (action == REMOTE_ACCEPT_1 || action == REMOTE_DECLINE_1) { - localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID)); - messages = Collections.emptyList(); - events = Collections - .singletonList(getEvent(localState, msg)); - } else if (action == REMOTE_ACCEPT_2 || - action == REMOTE_DECLINE_2) { - localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID)); - messages = Collections.emptyList(); - events = Collections - .singletonList(getEvent(localState, msg)); - } else return noUpdate(localState); - } else { - throw new IllegalArgumentException("Bad state"); - } - return new StateUpdate<>(false, false, - localState, messages, events); - } catch (FormatException e) { - throw new IllegalArgumentException(e); - } - } - - private void logLocalAction(IntroducerProtocolState state, - BdfDictionary localState) { - - if (!LOG.isLoggable(INFO)) return; - try { - LOG.info("Sending introduction request in state " + state.name()); - LOG.info("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; - - 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()); - LOG.info("Moving on to state " + nextState.name()); - } - - private List forwardMessage(BdfDictionary localState, - BdfDictionary message) throws FormatException { - - // clone the message here, because we still need the original - BdfDictionary msg = (BdfDictionary) message.clone(); - if (isContact1(localState, msg)) { - msg.put(GROUP_ID, localState.getRaw(GROUP_ID_2)); - } else { - msg.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); - } - - return Collections.singletonList(msg); - } - - @Override - public StateUpdate onMessageDelivered( - BdfDictionary localState, BdfDictionary delivered) { - try { - return noUpdate(localState); - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - return null; - } - } - - private IntroducerProtocolState getState(Long state) { - return IntroducerProtocolState.fromValue(state.intValue()); - } - - private Event getEvent(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - ContactId contactId = - new ContactId(localState.getLong(CONTACT_ID_1).intValue()); - AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1)); - 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)); - } - - SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); - MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); - GroupId groupId = new GroupId(msg.getRaw(GROUP_ID)); - long time = msg.getLong(MESSAGE_TIME); - String name = getOtherContact(localState, msg); - boolean accept = msg.getBoolean(ACCEPT); - - IntroductionResponse ir = - new IntroductionResponse(sessionId, messageId, groupId, - ROLE_INTRODUCER, 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 getOtherContact(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - String to = localState.getString(CONTACT_2); - if (Arrays - .equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) { - to = localState.getString(CONTACT_1); - } - return to; - } - - private StateUpdate abortSession( - IntroducerProtocolState currentState, BdfDictionary localState) - throws FormatException { - - if (LOG.isLoggable(WARNING)) - LOG.warning("Aborting protocol session in state " + - currentState.name()); - - localState.put(STATE, ERROR.getValue()); - List messages = new ArrayList<>(2); - BdfDictionary msg1 = new BdfDictionary(); - msg1.put(TYPE, TYPE_ABORT); - msg1.put(SESSION_ID, localState.getRaw(SESSION_ID)); - msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); - messages.add(msg1); - BdfDictionary msg2 = new BdfDictionary(); - msg2.put(TYPE, TYPE_ABORT); - msg2.put(SESSION_ID, localState.getRaw(SESSION_ID)); - msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2)); - messages.add(msg2); - - // send one abort event per contact - List events = new ArrayList<>(2); - SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); - ContactId contactId1 = - new ContactId(localState.getLong(CONTACT_ID_1).intValue()); - ContactId contactId2 = - new ContactId(localState.getLong(CONTACT_ID_2).intValue()); - Event event1 = new IntroductionAbortedEvent(contactId1, sessionId); - events.add(event1); - Event event2 = new IntroductionAbortedEvent(contactId2, sessionId); - events.add(event2); - - return new StateUpdate<>(false, false, localState, messages, events); - } - - private StateUpdate noUpdate( - BdfDictionary localState) throws FormatException { - - return new StateUpdate<>(false, false, localState, - Collections.emptyList(), - Collections.emptyList()); - } -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerManager.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerManager.java deleted file mode 100644 index b24109396..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerManager.java +++ /dev/null @@ -1,181 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.Bytes; -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.crypto.CryptoComponent; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.event.Event; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.util.StringUtils; - -import java.io.IOException; -import java.util.logging.Logger; - -import javax.annotation.Nullable; -import javax.annotation.concurrent.Immutable; -import javax.inject.Inject; - -import static java.util.logging.Level.WARNING; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; - -@Immutable -@NotNullByDefault -class IntroducerManager { - - private static final Logger LOG = - Logger.getLogger(IntroducerManager.class.getName()); - - private final MessageSender messageSender; - private final ClientHelper clientHelper; - private final Clock clock; - private final CryptoComponent cryptoComponent; - private final IntroductionGroupFactory introductionGroupFactory; - - @Inject - IntroducerManager(MessageSender messageSender, ClientHelper clientHelper, - Clock clock, CryptoComponent cryptoComponent, - IntroductionGroupFactory introductionGroupFactory) { - - this.messageSender = messageSender; - this.clientHelper = clientHelper; - this.clock = clock; - this.cryptoComponent = cryptoComponent; - this.introductionGroupFactory = introductionGroupFactory; - } - - 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( - introductionGroupFactory.createLocalGroup().getId(), now, - BdfList.of(salt)); - MessageId sessionId = m.getId(); - - Group g1 = introductionGroupFactory.createIntroductionGroup(c1); - Group g2 = introductionGroupFactory.createIntroductionGroup(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, d, false); - - return d; - } - - void makeIntroduction(Transaction txn, Contact c1, Contact c2, - @Nullable String msg, long timestamp) - 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)) { - int msgLength = StringUtils.toUtf8(msg).length; - if (msgLength > MAX_INTRODUCTION_MESSAGE_LENGTH) - throw new IllegalArgumentException(); - localAction.put(MSG, msg); - } - localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey()); - localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey()); - localAction.put(MESSAGE_TIME, timestamp); - - // start engine and process its state update - IntroducerEngine engine = new IntroducerEngine(); - processStateUpdate(txn, - engine.onLocalAction(localState, localAction)); - } - - public void incomingMessage(Transaction txn, BdfDictionary state, - BdfDictionary message) throws DbException, FormatException { - - IntroducerEngine engine = new IntroducerEngine(); - processStateUpdate(txn, - engine.onMessageReceived(state, message)); - } - - private void processStateUpdate(Transaction txn, - IntroducerEngine.StateUpdate - result) throws DbException, FormatException { - - // save new local state - MessageId storageId = - new MessageId(result.localState.getRaw(STORAGE_ID)); - clientHelper.mergeMessageMetadata(txn, storageId, result.localState); - - // send messages - for (BdfDictionary d : result.toSend) { - messageSender.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 | IOException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } - } - -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java new file mode 100644 index 000000000..ed19fe651 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerProtocolEngine.java @@ -0,0 +1,513 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; +import org.briarproject.bramble.api.contact.ContactManager; +import org.briarproject.bramble.api.db.DatabaseComponent; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.briar.api.client.MessageTracker; +import org.briarproject.briar.api.client.ProtocolStateException; +import org.briarproject.briar.api.introduction.event.IntroductionAbortedEvent; +import org.briarproject.briar.introduction.IntroducerSession.Introducee; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATES; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATE_A; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_ACTIVATE_B; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTHS; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTH_A; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTH_B; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSES; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_A; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_RESPONSE_B; +import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.START; + +@Immutable +@NotNullByDefault +class IntroducerProtocolEngine + extends AbstractProtocolEngine { + + @Inject + IntroducerProtocolEngine( + DatabaseComponent db, + ClientHelper clientHelper, + ContactManager contactManager, + ContactGroupFactory contactGroupFactory, + MessageTracker messageTracker, + IdentityManager identityManager, + MessageParser messageParser, + MessageEncoder messageEncoder, + Clock clock) { + super(db, clientHelper, contactManager, contactGroupFactory, + messageTracker, identityManager, messageParser, messageEncoder, + clock); + } + + @Override + public IntroducerSession onRequestAction(Transaction txn, + IntroducerSession s, @Nullable String message, long timestamp) + throws DbException { + switch (s.getState()) { + case START: + return onLocalRequest(txn, s, message, timestamp); + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + case A_DECLINED: + case B_DECLINED: + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onAcceptAction(Transaction txn, + IntroducerSession s, long timestamp) { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public IntroducerSession onDeclineAction(Transaction txn, + IntroducerSession s, long timestamp) { + throw new UnsupportedOperationException(); // Invalid in this role + } + + IntroducerSession onAbortAction(Transaction txn, IntroducerSession s) + throws DbException { + return abort(txn, s); + } + + @Override + public IntroducerSession onRequestMessage(Transaction txn, + IntroducerSession s, RequestMessage m) throws DbException { + return abort(txn, s); // Invalid in this role + } + + @Override + public IntroducerSession onAcceptMessage(Transaction txn, + IntroducerSession s, AcceptMessage m) throws DbException { + switch (s.getState()) { + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + return onRemoteAccept(txn, s, m); + case A_DECLINED: + case B_DECLINED: + return onRemoteResponseWhenDeclined(txn, s, m); + case START: + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onDeclineMessage(Transaction txn, + IntroducerSession s, DeclineMessage m) throws DbException { + switch (s.getState()) { + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + return onRemoteDecline(txn, s, m); + case A_DECLINED: + case B_DECLINED: + return onRemoteResponseWhenDeclined(txn, s, m); + case START: + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onAuthMessage(Transaction txn, IntroducerSession s, + AuthMessage m) throws DbException { + switch (s.getState()) { + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + return onRemoteAuth(txn, s, m); + case START: + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + case A_DECLINED: + case B_DECLINED: + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onActivateMessage(Transaction txn, + IntroducerSession s, ActivateMessage m) throws DbException { + switch (s.getState()) { + case AWAIT_ACTIVATES: + case AWAIT_ACTIVATE_A: + case AWAIT_ACTIVATE_B: + return onRemoteActivate(txn, s, m); + case START: + case AWAIT_RESPONSES: + case AWAIT_RESPONSE_A: + case AWAIT_RESPONSE_B: + case A_DECLINED: + case B_DECLINED: + case AWAIT_AUTHS: + case AWAIT_AUTH_A: + case AWAIT_AUTH_B: + return abort(txn, s); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public IntroducerSession onAbortMessage(Transaction txn, + IntroducerSession s, AbortMessage m) throws DbException { + return onRemoteAbort(txn, s, m); + } + + private IntroducerSession onLocalRequest(Transaction txn, + IntroducerSession s, + @Nullable String message, long timestamp) throws DbException { + // Send REQUEST messages + long maxIntroduceeTimestamp = + Math.max(getLocalTimestamp(s, s.getIntroduceeA()), + getLocalTimestamp(s, s.getIntroduceeB())); + long localTimestamp = Math.max(timestamp, maxIntroduceeTimestamp); + Message sentA = sendRequestMessage(txn, s.getIntroduceeA(), + localTimestamp, s.getIntroduceeB().author, message + ); + Message sentB = sendRequestMessage(txn, s.getIntroduceeB(), + localTimestamp, s.getIntroduceeA().author, message + ); + // Track the messages + messageTracker.trackOutgoingMessage(txn, sentA); + messageTracker.trackOutgoingMessage(txn, sentB); + // Move to the AWAIT_RESPONSES state + Introducee introduceeA = new Introducee(s.getIntroduceeA(), sentA); + Introducee introduceeB = new Introducee(s.getIntroduceeB(), sentB); + return new IntroducerSession(s.getSessionId(), AWAIT_RESPONSES, + localTimestamp, introduceeA, introduceeB); + } + + private IntroducerSession onRemoteAccept(Transaction txn, + IntroducerSession s, AcceptMessage m) throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + // The message must be expected in the current state + boolean senderIsAlice = senderIsAlice(s, m); + if (s.getState() != AWAIT_RESPONSES) { + if (senderIsAlice && s.getState() != AWAIT_RESPONSE_A) + return abort(txn, s); + else if (!senderIsAlice && s.getState() != AWAIT_RESPONSE_B) + return abort(txn, s); + } + + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Forward ACCEPT message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = + sendAcceptMessage(txn, i, timestamp, m.getEphemeralPublicKey(), + m.getAcceptTimestamp(), m.getTransportProperties(), + false); + + // Create the next state + IntroducerState state = AWAIT_AUTHS; + Introducee introduceeA, introduceeB; + if (senderIsAlice) { + if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B; + introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); + introduceeB = new Introducee(s.getIntroduceeB(), sent); + } else { + if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A; + introduceeA = new Introducee(s.getIntroduceeA(), sent); + introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + } + + // Broadcast IntroductionResponseReceivedEvent + Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + + // Move to the next state + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private boolean senderIsAlice(IntroducerSession s, + AbstractIntroductionMessage m) { + return m.getGroupId().equals(s.getIntroduceeA().groupId); + } + + private IntroducerSession onRemoteDecline(Transaction txn, + IntroducerSession s, DeclineMessage m) throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + // The message must be expected in the current state + boolean senderIsAlice = senderIsAlice(s, m); + if (s.getState() != AWAIT_RESPONSES) { + if (senderIsAlice && s.getState() != AWAIT_RESPONSE_A) + return abort(txn, s); + else if (!senderIsAlice && s.getState() != AWAIT_RESPONSE_B) + return abort(txn, s); + } + + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + // Forward DECLINE message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendDeclineMessage(txn, i, timestamp, false); + + // Create the next state + IntroducerState state = START; + Introducee introduceeA, introduceeB; + if (senderIsAlice) { + if (s.getState() == AWAIT_RESPONSES) state = A_DECLINED; + introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); + introduceeB = new Introducee(s.getIntroduceeB(), sent); + } else { + if (s.getState() == AWAIT_RESPONSES) state = B_DECLINED; + introduceeA = new Introducee(s.getIntroduceeA(), sent); + introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + } + + // Broadcast IntroductionResponseReceivedEvent + Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private IntroducerSession onRemoteResponseWhenDeclined(Transaction txn, + IntroducerSession s, AbstractIntroductionMessage m) + throws DbException { + // The timestamp must be higher than the last request message + if (m.getTimestamp() <= s.getRequestTimestamp()) + return abort(txn, s); + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + // The message must be expected in the current state + boolean senderIsAlice = senderIsAlice(s, m); + if (senderIsAlice && s.getState() != B_DECLINED) + return abort(txn, s); + else if (!senderIsAlice && s.getState() != A_DECLINED) + return abort(txn, s); + + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getMessageId()); + // Track the incoming message + messageTracker + .trackMessage(txn, m.getGroupId(), m.getTimestamp(), false); + + Introducee introduceeA, introduceeB; + if (senderIsAlice) { + introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); + introduceeB = s.getIntroduceeB(); + } else { + introduceeA = s.getIntroduceeA(); + introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + } + + // Broadcast IntroductionResponseReceivedEvent + Author sender = senderIsAlice ? introduceeA.author : introduceeB.author; + broadcastIntroductionResponseReceivedEvent(txn, s, sender.getId(), m); + + return new IntroducerSession(s.getSessionId(), START, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private IntroducerSession onRemoteAuth(Transaction txn, + IntroducerSession s, AuthMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + // The message must be expected in the current state + boolean senderIsAlice = senderIsAlice(s, m); + if (s.getState() != AWAIT_AUTHS) { + if (senderIsAlice && s.getState() != AWAIT_AUTH_A) + return abort(txn, s); + else if (!senderIsAlice && s.getState() != AWAIT_AUTH_B) + return abort(txn, s); + } + + // Forward AUTH message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendAuthMessage(txn, i, timestamp, m.getMac(), + m.getSignature()); + + // Move to the next state + IntroducerState state = AWAIT_ACTIVATES; + Introducee introduceeA, introduceeB; + if (senderIsAlice) { + if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_B; + introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); + introduceeB = new Introducee(s.getIntroduceeB(), sent); + } else { + if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_A; + introduceeA = new Introducee(s.getIntroduceeA(), sent); + introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + } + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private IntroducerSession onRemoteActivate(Transaction txn, + IntroducerSession s, ActivateMessage m) throws DbException { + // The dependency, if any, must be the last remote message + if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId())) + return abort(txn, s); + // The message must be expected in the current state + boolean senderIsAlice = senderIsAlice(s, m); + if (s.getState() != AWAIT_ACTIVATES) { + if (senderIsAlice && s.getState() != AWAIT_ACTIVATE_A) + return abort(txn, s); + else if (!senderIsAlice && s.getState() != AWAIT_ACTIVATE_B) + return abort(txn, s); + } + + // Forward ACTIVATE message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendActivateMessage(txn, i, timestamp, m.getMac()); + + // Move to the next state + IntroducerState state = START; + Introducee introduceeA, introduceeB; + if (senderIsAlice) { + if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_B; + introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); + introduceeB = new Introducee(s.getIntroduceeB(), sent); + } else { + if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_A; + introduceeA = new Introducee(s.getIntroduceeA(), sent); + introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + } + return new IntroducerSession(s.getSessionId(), state, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private IntroducerSession onRemoteAbort(Transaction txn, + IntroducerSession s, AbortMessage m) throws DbException { + // Forward ABORT message + Introducee i = getOtherIntroducee(s, m.getGroupId()); + long timestamp = getLocalTimestamp(s, i); + Message sent = sendAbortMessage(txn, i, timestamp); + + // Broadcast abort event for testing + txn.attach(new IntroductionAbortedEvent(s.getSessionId())); + + // Reset the session back to initial state + Introducee introduceeA, introduceeB; + if (i.equals(s.getIntroduceeA())) { + introduceeA = new Introducee(s.getIntroduceeA(), sent); + introduceeB = new Introducee(s.getIntroduceeB(), m.getMessageId()); + } else if (i.equals(s.getIntroduceeB())) { + introduceeA = new Introducee(s.getIntroduceeA(), m.getMessageId()); + introduceeB = new Introducee(s.getIntroduceeB(), sent); + } else throw new AssertionError(); + return new IntroducerSession(s.getSessionId(), START, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private IntroducerSession abort(Transaction txn, + IntroducerSession s) throws DbException { + // Broadcast abort event for testing + txn.attach(new IntroductionAbortedEvent(s.getSessionId())); + + // Send an ABORT message to both introducees + long timestampA = getLocalTimestamp(s, s.getIntroduceeA()); + Message sentA = sendAbortMessage(txn, s.getIntroduceeA(), timestampA); + long timestampB = getLocalTimestamp(s, s.getIntroduceeB()); + Message sentB = sendAbortMessage(txn, s.getIntroduceeB(), timestampB); + // Reset the session back to initial state + Introducee introduceeA = new Introducee(s.getIntroduceeA(), sentA); + Introducee introduceeB = new Introducee(s.getIntroduceeB(), sentB); + return new IntroducerSession(s.getSessionId(), START, + s.getRequestTimestamp(), introduceeA, introduceeB); + } + + private Introducee getIntroducee(IntroducerSession s, GroupId g) { + if (s.getIntroduceeA().groupId.equals(g)) return s.getIntroduceeA(); + else if (s.getIntroduceeB().groupId.equals(g)) + return s.getIntroduceeB(); + else throw new AssertionError(); + } + + private Introducee getOtherIntroducee(IntroducerSession s, GroupId g) { + if (s.getIntroduceeA().groupId.equals(g)) return s.getIntroduceeB(); + else if (s.getIntroduceeB().groupId.equals(g)) + return s.getIntroduceeA(); + else throw new AssertionError(); + } + + private boolean isInvalidDependency(IntroducerSession session, + GroupId contactGroupId, @Nullable MessageId dependency) { + MessageId expected = + getIntroducee(session, contactGroupId).lastRemoteMessageId; + return isInvalidDependency(expected, dependency); + } + + private long getLocalTimestamp(IntroducerSession s, PeerSession p) { + return getLocalTimestamp(p.getLocalTimestamp(), + s.getRequestTimestamp()); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java new file mode 100644 index 000000000..c26eb26d9 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerSession.java @@ -0,0 +1,115 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.Role; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.briar.api.introduction.Role.INTRODUCER; + +@Immutable +@NotNullByDefault +class IntroducerSession extends Session { + + private final Introducee introduceeA, introduceeB; + + IntroducerSession(SessionId sessionId, IntroducerState state, + long requestTimestamp, Introducee introduceeA, + Introducee introduceeB) { + super(sessionId, state, requestTimestamp); + this.introduceeA = introduceeA; + this.introduceeB = introduceeB; + } + + IntroducerSession(SessionId sessionId, GroupId groupIdA, Author authorA, + GroupId groupIdB, Author authorB) { + this(sessionId, IntroducerState.START, -1, + new Introducee(sessionId, groupIdA, authorA), + new Introducee(sessionId, groupIdB, authorB)); + } + + @Override + Role getRole() { + return INTRODUCER; + } + + Introducee getIntroduceeA() { + return introduceeA; + } + + Introducee getIntroduceeB() { + return introduceeB; + } + + @Immutable + @NotNullByDefault + static class Introducee implements PeerSession { + final SessionId sessionId; + final GroupId groupId; + final Author author; + final long localTimestamp; + @Nullable + final MessageId lastLocalMessageId, lastRemoteMessageId; + + Introducee(SessionId sessionId, GroupId groupId, Author author, + long localTimestamp, + @Nullable MessageId lastLocalMessageId, + @Nullable MessageId lastRemoteMessageId) { + this.sessionId = sessionId; + this.groupId = groupId; + this.localTimestamp = localTimestamp; + this.author = author; + this.lastLocalMessageId = lastLocalMessageId; + this.lastRemoteMessageId = lastRemoteMessageId; + } + + Introducee(Introducee i, Message sent) { + this(i.sessionId, i.groupId, i.author, sent.getTimestamp(), + sent.getId(), i.lastRemoteMessageId); + } + + Introducee(Introducee i, MessageId remoteMessageId) { + this(i.sessionId, i.groupId, i.author, i.localTimestamp, + i.lastLocalMessageId, remoteMessageId); + } + + private Introducee(SessionId sessionId, GroupId groupId, + Author author) { + this(sessionId, groupId, author, -1, null, null); + } + + public SessionId getSessionId() { + return sessionId; + } + + @Override + public GroupId getContactGroupId() { + return groupId; + } + + @Override + public long getLocalTimestamp() { + return localTimestamp; + } + + @Nullable + @Override + public MessageId getLastLocalMessageId() { + return lastLocalMessageId; + } + + @Nullable + @Override + public MessageId getLastRemoteMessageId() { + return lastRemoteMessageId; + } + + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java new file mode 100644 index 000000000..6514eca16 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroducerState.java @@ -0,0 +1,37 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum IntroducerState implements State { + + START(0), + AWAIT_RESPONSES(1), + AWAIT_RESPONSE_A(2), AWAIT_RESPONSE_B(3), + A_DECLINED(4), B_DECLINED(5), + AWAIT_AUTHS(6), + AWAIT_AUTH_A(7), AWAIT_AUTH_B(8), + AWAIT_ACTIVATES(9), + AWAIT_ACTIVATE_A(10), AWAIT_ACTIVATE_B(11); + + private final int value; + + IntroducerState(int value) { + this.value = value; + } + + @Override + public int getValue() { + return value; + } + + static IntroducerState fromValue(int value) throws FormatException { + for (IntroducerState s : values()) if (s.value == value) return s; + throw new FormatException(); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java new file mode 100644 index 000000000..522759a55 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionConstants.java @@ -0,0 +1,48 @@ +package org.briarproject.briar.introduction; + +interface IntroductionConstants { + + // Group metadata keys + String GROUP_KEY_CONTACT_ID = "contactId"; + + // Message metadata keys + String MSG_KEY_MESSAGE_TYPE = "messageType"; + String MSG_KEY_SESSION_ID = "sessionId"; + String MSG_KEY_TIMESTAMP = "timestamp"; + String MSG_KEY_LOCAL = "local"; + String MSG_KEY_VISIBLE_IN_UI = "visibleInUi"; + String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer"; + + // Session Keys + String SESSION_KEY_SESSION_ID = "sessionId"; + String SESSION_KEY_ROLE = "role"; + String SESSION_KEY_STATE = "state"; + String SESSION_KEY_REQUEST_TIMESTAMP = "requestTimestamp"; + String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp"; + String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId"; + String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId"; + + // Session Keys Introducer + String SESSION_KEY_INTRODUCEE_A = "introduceeA"; + String SESSION_KEY_INTRODUCEE_B = "introduceeB"; + String SESSION_KEY_GROUP_ID = "groupId"; + String SESSION_KEY_AUTHOR = "author"; + + // Session Keys Introducee + String SESSION_KEY_INTRODUCER = "introducer"; + String SESSION_KEY_LOCAL = "local"; + String SESSION_KEY_REMOTE = "remote"; + + String SESSION_KEY_MASTER_KEY = "masterKey"; + String SESSION_KEY_TRANSPORT_KEYS = "transportKeys"; + + String SESSION_KEY_ALICE = "alice"; + String SESSION_KEY_EPHEMERAL_PUBLIC_KEY = "ephemeralPublicKey"; + String SESSION_KEY_EPHEMERAL_PRIVATE_KEY = "ephemeralPrivateKey"; + String SESSION_KEY_TRANSPORT_PROPERTIES = "transportProperties"; + String SESSION_KEY_ACCEPT_TIMESTAMP = "acceptTimestamp"; + String SESSION_KEY_MAC_KEY = "macKey"; + + String SESSION_KEY_REMOTE_AUTHOR = "remoteAuthor"; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java new file mode 100644 index 000000000..37f7aa10f --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCrypto.java @@ -0,0 +1,100 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.briar.api.client.SessionId; + +import java.security.GeneralSecurityException; + +interface IntroductionCrypto { + + /** + * Returns the {@link SessionId} based on the introducer + * and the two introducees. + */ + SessionId getSessionId(Author introducer, Author local, Author remote); + + /** + * Returns true if the local author is alice + * + * Alice is the Author whose unique ID has the lower ID, + * comparing the IDs as byte strings. + */ + boolean isAlice(AuthorId local, AuthorId remote); + + /** + * Generates an agreement key pair. + */ + KeyPair generateKeyPair(); + + /** + * Derives a session master key for Alice or Bob. + * + * @return The secret master key + */ + SecretKey deriveMasterKey(IntroduceeSession s) + throws GeneralSecurityException; + + /** + * Derives a MAC key from the session's master key for Alice or Bob. + * + * @param masterKey The key returned by {@link #deriveMasterKey(IntroduceeSession)} + * @param alice true for Alice's MAC key, false for Bob's + * @return The MAC key + */ + SecretKey deriveMacKey(SecretKey masterKey, boolean alice); + + /** + * Generates a MAC that covers both introducee's ephemeral public keys, + * transport properties, Author IDs and timestamps of the accept message. + */ + byte[] authMac(SecretKey macKey, IntroduceeSession s, + AuthorId localAuthorId); + + /** + * Verifies a received MAC + * + * @param mac The MAC to verify + * as returned by {@link #deriveMasterKey(IntroduceeSession)} + * @throws GeneralSecurityException if the verification fails + */ + void verifyAuthMac(byte[] mac, IntroduceeSession s, AuthorId localAuthorId) + throws GeneralSecurityException; + + /** + * Signs a nonce derived from the macKey + * with the local introducee's identity private key. + * + * @param macKey The corresponding MAC key for the signer's role + * @param privateKey The identity private key + * (from {@link LocalAuthor#getPrivateKey()}) + * @return The signature as a byte array + */ + byte[] sign(SecretKey macKey, byte[] privateKey) + throws GeneralSecurityException; + + /** + * Verifies the signature on a nonce derived from the MAC key. + * + * @throws GeneralSecurityException if the signature is invalid + */ + void verifySignature(byte[] signature, IntroduceeSession s) + throws GeneralSecurityException; + + /** + * Generates a MAC using the local MAC key. + */ + byte[] activateMac(IntroduceeSession s); + + /** + * Verifies a MAC from an ACTIVATE message. + * + * @throws GeneralSecurityException if the verification fails + */ + void verifyActivateMac(byte[] mac, IntroduceeSession s) + throws GeneralSecurityException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java new file mode 100644 index 000000000..f9d53323b --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionCryptoImpl.java @@ -0,0 +1,239 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.KeyParser; +import org.briarproject.bramble.api.crypto.PrivateKey; +import org.briarproject.bramble.api.crypto.PublicKey; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.introduction.IntroduceeSession.Common; +import org.briarproject.briar.introduction.IntroduceeSession.Remote; + +import java.security.GeneralSecurityException; + +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_ACTIVATE_MAC; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_ALICE_MAC_KEY; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_MAC; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_NONCE; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_AUTH_SIGN; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_BOB_MAC_KEY; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_MASTER_KEY; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_SESSION_ID; +import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION; +import static org.briarproject.briar.introduction.IntroduceeSession.Local; + +@Immutable +@NotNullByDefault +class IntroductionCryptoImpl implements IntroductionCrypto { + + private final CryptoComponent crypto; + private final ClientHelper clientHelper; + + @Inject + IntroductionCryptoImpl( + CryptoComponent crypto, + ClientHelper clientHelper) { + this.crypto = crypto; + this.clientHelper = clientHelper; + } + + @Override + public SessionId getSessionId(Author introducer, Author local, + Author remote) { + boolean isAlice = isAlice(local.getId(), remote.getId()); + byte[] hash = crypto.hash( + LABEL_SESSION_ID, + introducer.getId().getBytes(), + isAlice ? local.getId().getBytes() : remote.getId().getBytes(), + isAlice ? remote.getId().getBytes() : local.getId().getBytes() + ); + return new SessionId(hash); + } + + @Override + public KeyPair generateKeyPair() { + return crypto.generateAgreementKeyPair(); + } + + @Override + public boolean isAlice(AuthorId local, AuthorId remote) { + return local.compareTo(remote) < 0; + } + + @Override + @SuppressWarnings("ConstantConditions") + public SecretKey deriveMasterKey(IntroduceeSession s) + throws GeneralSecurityException { + return deriveMasterKey( + s.getLocal().ephemeralPublicKey, + s.getLocal().ephemeralPrivateKey, + s.getRemote().ephemeralPublicKey, + s.getLocal().alice + ); + } + + SecretKey deriveMasterKey(byte[] publicKey, byte[] privateKey, + byte[] remotePublicKey, boolean alice) + throws GeneralSecurityException { + KeyParser kp = crypto.getAgreementKeyParser(); + PublicKey remoteEphemeralPublicKey = kp.parsePublicKey(remotePublicKey); + PublicKey ephemeralPublicKey = kp.parsePublicKey(publicKey); + PrivateKey ephemeralPrivateKey = kp.parsePrivateKey(privateKey); + KeyPair keyPair = new KeyPair(ephemeralPublicKey, ephemeralPrivateKey); + return crypto.deriveSharedSecret( + LABEL_MASTER_KEY, + remoteEphemeralPublicKey, + keyPair, + new byte[] {CLIENT_VERSION}, + alice ? publicKey : remotePublicKey, + alice ? remotePublicKey : publicKey + ); + } + + @Override + public SecretKey deriveMacKey(SecretKey masterKey, boolean alice) { + return crypto.deriveKey( + alice ? LABEL_ALICE_MAC_KEY : LABEL_BOB_MAC_KEY, + masterKey + ); + } + + @Override + @SuppressWarnings("ConstantConditions") + public byte[] authMac(SecretKey macKey, IntroduceeSession s, + AuthorId localAuthorId) { + // the macKey is not yet available in the session at this point + return authMac(macKey, s.getIntroducer().getId(), localAuthorId, + s.getLocal(), s.getRemote()); + } + + byte[] authMac(SecretKey macKey, AuthorId introducerId, + AuthorId localAuthorId, Local local, Remote remote) { + byte[] inputs = getAuthMacInputs(introducerId, localAuthorId, local, + remote.author.getId(), remote); + return crypto.mac( + LABEL_AUTH_MAC, + macKey, + inputs + ); + } + + @Override + @SuppressWarnings("ConstantConditions") + public void verifyAuthMac(byte[] mac, IntroduceeSession s, + AuthorId localAuthorId) throws GeneralSecurityException { + verifyAuthMac(mac, new SecretKey(s.getRemote().macKey), + s.getIntroducer().getId(), localAuthorId, s.getLocal(), + s.getRemote().author.getId(), s.getRemote()); + } + + void verifyAuthMac(byte[] mac, SecretKey macKey, AuthorId introducerId, + AuthorId localAuthorId, Common local, AuthorId remoteAuthorId, + Common remote) throws GeneralSecurityException { + // switch input for verification + byte[] inputs = getAuthMacInputs(introducerId, remoteAuthorId, remote, + localAuthorId, local); + if (!crypto.verifyMac(mac, LABEL_AUTH_MAC, macKey, inputs)) { + throw new GeneralSecurityException(); + } + } + + @SuppressWarnings("ConstantConditions") + private byte[] getAuthMacInputs(AuthorId introducerId, + AuthorId localAuthorId, Common local, AuthorId remoteAuthorId, + Common remote) { + BdfList localInfo = BdfList.of( + localAuthorId, + local.acceptTimestamp, + local.ephemeralPublicKey, + clientHelper.toDictionary(local.transportProperties) + ); + BdfList remoteInfo = BdfList.of( + remoteAuthorId, + remote.acceptTimestamp, + remote.ephemeralPublicKey, + clientHelper.toDictionary(remote.transportProperties) + ); + BdfList macList = BdfList.of( + introducerId, + localInfo, + remoteInfo + ); + try { + return clientHelper.toByteArray(macList); + } catch (FormatException e) { + throw new AssertionError(); + } + } + + @Override + public byte[] sign(SecretKey macKey, byte[] privateKey) + throws GeneralSecurityException { + return crypto.sign( + LABEL_AUTH_SIGN, + getNonce(macKey), + privateKey + ); + } + + @Override + @SuppressWarnings("ConstantConditions") + public void verifySignature(byte[] signature, IntroduceeSession s) + throws GeneralSecurityException { + SecretKey macKey = new SecretKey(s.getRemote().macKey); + verifySignature(macKey, s.getRemote().author.getPublicKey(), signature); + } + + void verifySignature(SecretKey macKey, byte[] publicKey, + byte[] signature) throws GeneralSecurityException { + byte[] nonce = getNonce(macKey); + if (!crypto.verifySignature(signature, LABEL_AUTH_SIGN, nonce, + publicKey)) { + throw new GeneralSecurityException(); + } + } + + private byte[] getNonce(SecretKey macKey) { + return crypto.mac(LABEL_AUTH_NONCE, macKey); + } + + @Override + public byte[] activateMac(IntroduceeSession s) { + if (s.getLocal().macKey == null) + throw new AssertionError("Local MAC key is null"); + return activateMac(new SecretKey(s.getLocal().macKey)); + } + + byte[] activateMac(SecretKey macKey) { + return crypto.mac( + LABEL_ACTIVATE_MAC, + macKey + ); + } + + @Override + public void verifyActivateMac(byte[] mac, IntroduceeSession s) + throws GeneralSecurityException { + if (s.getRemote().macKey == null) + throw new AssertionError("Remote MAC key is null"); + verifyActivateMac(mac, new SecretKey(s.getRemote().macKey)); + } + + void verifyActivateMac(byte[] mac, SecretKey macKey) + throws GeneralSecurityException { + if (!crypto.verifyMac(mac, LABEL_ACTIVATE_MAC, macKey)) { + throw new GeneralSecurityException(); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionGroupFactory.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionGroupFactory.java deleted file mode 100644 index 050d2b9f4..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionGroupFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.client.ContactGroupFactory; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.sync.Group; - -import javax.inject.Inject; - -import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; -import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION; - -class IntroductionGroupFactory { - - private final ContactGroupFactory contactGroupFactory; - private final Group localGroup; - - @Inject - IntroductionGroupFactory(ContactGroupFactory contactGroupFactory) { - this.contactGroupFactory = contactGroupFactory; - localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID, - CLIENT_VERSION); - } - - Group createIntroductionGroup(Contact c) { - return contactGroupFactory.createContactGroup(CLIENT_ID, - CLIENT_VERSION, c); - } - - Group createLocalGroup() { - return localGroup; - } - -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java index 9ccf06b67..efb857a0f 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionManagerImpl.java @@ -2,19 +2,21 @@ package org.briarproject.briar.introduction; import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.ContactGroupFactory; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; +import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager.ContactHook; import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.data.MetadataParser; import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.NoSuchContactException; -import org.briarproject.bramble.api.db.NoSuchMessageException; +import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.identity.AuthorId; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.IdentityManager; +import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.Client; import org.briarproject.bramble.api.sync.Group; @@ -24,412 +26,394 @@ import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageStatus; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.api.introduction.IntroducerProtocolState; import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionMessage; import org.briarproject.briar.api.introduction.IntroductionRequest; import org.briarproject.briar.api.introduction.IntroductionResponse; +import org.briarproject.briar.api.introduction.Role; import org.briarproject.briar.client.ConversationClientImpl; +import org.briarproject.briar.introduction.IntroducerSession.Introducee; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; -import java.util.logging.Logger; +import java.util.Map.Entry; import javax.annotation.Nullable; import javax.annotation.concurrent.Immutable; import javax.inject.Inject; -import static java.util.logging.Level.WARNING; import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.FINISHED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US; -import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; -import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; +import static org.briarproject.briar.api.introduction.Role.INTRODUCER; +import static org.briarproject.briar.introduction.IntroducerState.START; +import static org.briarproject.briar.introduction.IntroductionConstants.GROUP_KEY_CONTACT_ID; +import static org.briarproject.briar.introduction.MessageType.ABORT; +import static org.briarproject.briar.introduction.MessageType.ACCEPT; +import static org.briarproject.briar.introduction.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction.MessageType.AUTH; +import static org.briarproject.briar.introduction.MessageType.DECLINE; +import static org.briarproject.briar.introduction.MessageType.REQUEST; @Immutable @NotNullByDefault class IntroductionManagerImpl extends ConversationClientImpl implements IntroductionManager, Client, ContactHook { - private static final Logger LOG = - Logger.getLogger(IntroductionManagerImpl.class.getName()); + private final ContactGroupFactory contactGroupFactory; + private final ContactManager contactManager; + private final MessageParser messageParser; + private final SessionEncoder sessionEncoder; + private final SessionParser sessionParser; + private final IntroducerProtocolEngine introducerEngine; + private final IntroduceeProtocolEngine introduceeEngine; + private final IntroductionCrypto crypto; + private final IdentityManager identityManager; - private final IntroducerManager introducerManager; - private final IntroduceeManager introduceeManager; - private final IntroductionGroupFactory introductionGroupFactory; + private final Group localGroup; @Inject - IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper, - MetadataParser metadataParser, MessageTracker messageTracker, - IntroducerManager introducerManager, - IntroduceeManager introduceeManager, - IntroductionGroupFactory introductionGroupFactory) { - + IntroductionManagerImpl( + DatabaseComponent db, + ClientHelper clientHelper, + MetadataParser metadataParser, + MessageTracker messageTracker, + ContactGroupFactory contactGroupFactory, + ContactManager contactManager, + MessageParser messageParser, + SessionEncoder sessionEncoder, + SessionParser sessionParser, + IntroducerProtocolEngine introducerEngine, + IntroduceeProtocolEngine introduceeEngine, + IntroductionCrypto crypto, + IdentityManager identityManager) { super(db, clientHelper, metadataParser, messageTracker); - this.introducerManager = introducerManager; - this.introduceeManager = introduceeManager; - this.introductionGroupFactory = introductionGroupFactory; + this.contactGroupFactory = contactGroupFactory; + this.contactManager = contactManager; + this.messageParser = messageParser; + this.sessionEncoder = sessionEncoder; + this.sessionParser = sessionParser; + this.introducerEngine = introducerEngine; + this.introduceeEngine = introduceeEngine; + this.crypto = crypto; + this.identityManager = identityManager; + this.localGroup = + contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION); } @Override public void createLocalState(Transaction txn) throws DbException { - Group localGroup = introductionGroupFactory.createLocalGroup(); + // Create a local group to store protocol sessions if (db.containsGroup(txn, localGroup.getId())) return; db.addGroup(txn, localGroup); - // Ensure we've set things up for any pre-existing contacts + // Set up groups for communication with any pre-existing contacts for (Contact c : db.getContacts(txn)) addingContact(txn, c); } @Override + // TODO adapt to use upcoming ClientVersioning client public void addingContact(Transaction txn, Contact c) throws DbException { + // Create a group to share with the contact + Group g = getContactGroup(c); + // Store the group and share it with the contact + db.addGroup(txn, g); + db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); + // Attach the contact ID to the group + BdfDictionary meta = new BdfDictionary(); + meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt()); try { - // Create an introduction group for sending introduction messages - Group g = getContactGroup(c); - // Return if we've already set things up for this contact - if (db.containsGroup(txn, g.getId())) return; - // Store the group and share it with the contact - db.addGroup(txn, g); - db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED); - // Attach the contact ID to the group - BdfDictionary gm = new BdfDictionary(); - gm.put(CONTACT, c.getId().getInt()); - clientHelper.mergeGroupMetadata(txn, g.getId(), gm); + clientHelper.mergeGroupMetadata(txn, g.getId(), meta); } catch (FormatException e) { - throw new RuntimeException(e); + throw new AssertionError(e); } } @Override public void removingContact(Transaction txn, Contact c) throws DbException { - GroupId gId = introductionGroupFactory.createLocalGroup().getId(); + removeSessionWithIntroducer(txn, c); + abortOrRemoveSessionWithIntroducee(txn, c); - // search for session states where c introduced us - BdfDictionary query = BdfDictionary.of( - new BdfEntry(ROLE, ROLE_INTRODUCEE), - new BdfEntry(CONTACT_ID_1, c.getId().getInt()) - ); - try { - Map map = clientHelper - .getMessageMetadataAsDictionary(txn, gId, query); - for (Map.Entry entry : map.entrySet()) { - // delete states if introducee removes introducer - deleteMessage(txn, entry.getKey()); - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - } - - // check for open sessions with c and abort those, - // so the other introducee knows - query = BdfDictionary.of( - new BdfEntry(ROLE, ROLE_INTRODUCER) - ); - try { - Map map = clientHelper - .getMessageMetadataAsDictionary(txn, gId, query); - for (Map.Entry entry : map.entrySet()) { - BdfDictionary d = entry.getValue(); - ContactId c1 = new ContactId(d.getLong(CONTACT_ID_1).intValue()); - ContactId c2 = new ContactId(d.getLong(CONTACT_ID_2).intValue()); - - if (c1.equals(c.getId()) || c2.equals(c.getId())) { - IntroducerProtocolState state = IntroducerProtocolState - .fromValue(d.getLong(STATE).intValue()); - // abort protocol if still ongoing - if (IntroducerProtocolState.isOngoing(state)) { - introducerManager.abort(txn, d); - } - // also delete state if both contacts have been deleted - if (c1.equals(c.getId())) { - try { - db.getContact(txn, c2); - } catch (NoSuchContactException e) { - deleteMessage(txn, entry.getKey()); - } - } else if (c2.equals(c.getId())) { - try { - db.getContact(txn, c1); - } catch (NoSuchContactException e) { - deleteMessage(txn, entry.getKey()); - } - } - } - } - } 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 + // Remove the contact group (all messages will be removed with it) db.removeGroup(txn, getContactGroup(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 boolean incomingMessage(Transaction txn, Message m, BdfList body, - BdfDictionary message) throws DbException, FormatException { - - // Get message data and type - GroupId groupId = m.getGroupId(); - long type = message.getLong(TYPE, -1L); - - // we are an introducee, need to initialize new state - if (type == TYPE_REQUEST) { - boolean stateExists = true; - try { - getSessionState(txn, groupId, message.getRaw(SESSION_ID), false); - } catch (FormatException e) { - stateExists = false; - } - if (stateExists) throw new FormatException(); - BdfDictionary state = - introduceeManager.initialize(txn, groupId, message); - try { - introduceeManager.incomingMessage(txn, state, message); - messageTracker.trackIncomingMessage(txn, m); - } catch (DbException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - introduceeManager.abort(txn, state); - } catch (FormatException e) { - // FIXME necessary? - 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 = - getSessionState(txn, groupId, message.getRaw(SESSION_ID)); - - long role = state.getLong(ROLE, -1L); - try { - if (role == ROLE_INTRODUCER) { - introducerManager.incomingMessage(txn, state, message); - if (type == TYPE_RESPONSE) - messageTracker.trackIncomingMessage(txn, m); - } else if (role == ROLE_INTRODUCEE) { - introduceeManager.incomingMessage(txn, state, message); - if (type == TYPE_RESPONSE && !message.getBoolean(ACCEPT)) - messageTracker.trackIncomingMessage(txn, m); - } else { - if (LOG.isLoggable(WARNING)) - LOG.warning("Unknown role '" + role + "'"); - throw new DbException(); - } - } catch (DbException | FormatException 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..."); - } - } - return false; + public Group getContactGroup(Contact c) { + return contactGroupFactory + .createContactGroup(CLIENT_ID, CLIENT_VERSION, c); } @Override - public Group getContactGroup(Contact contact) { - return introductionGroupFactory.createIntroductionGroup(contact); + protected boolean incomingMessage(Transaction txn, Message m, BdfList body, + BdfDictionary bdfMeta) throws DbException, FormatException { + // Parse the metadata + MessageMetadata meta = messageParser.parseMetadata(bdfMeta); + // Look up the session, if there is one + SessionId sessionId = meta.getSessionId(); + IntroduceeSession newIntroduceeSession = null; + if (sessionId == null) { + if (meta.getMessageType() != REQUEST) throw new AssertionError(); + newIntroduceeSession = createNewIntroduceeSession(txn, m, body); + sessionId = newIntroduceeSession.getSessionId(); + } + StoredSession ss = getSession(txn, sessionId); + // Handle the message + Session session; + MessageId storageId; + if (ss == null) { + if (meta.getMessageType() != REQUEST) throw new FormatException(); + if (newIntroduceeSession == null) throw new AssertionError(); + storageId = createStorageId(txn); + session = handleMessage(txn, m, body, meta.getMessageType(), + newIntroduceeSession, introduceeEngine); + } else { + storageId = ss.storageId; + Role role = sessionParser.getRole(ss.bdfSession); + if (role == INTRODUCER) { + session = handleMessage(txn, m, body, meta.getMessageType(), + sessionParser.parseIntroducerSession(ss.bdfSession), + introducerEngine); + } else if (role == INTRODUCEE) { + session = handleMessage(txn, m, body, meta.getMessageType(), + sessionParser.parseIntroduceeSession(m.getGroupId(), + ss.bdfSession), introduceeEngine); + } else throw new AssertionError(); + } + // Store the updated session + storeSession(txn, storageId, session); + return false; + } + + private IntroduceeSession createNewIntroduceeSession(Transaction txn, + Message m, BdfList body) throws DbException, FormatException { + ContactId introducerId = getContactId(txn, m.getGroupId()); + Author introducer = db.getContact(txn, introducerId).getAuthor(); + Author local = identityManager.getLocalAuthor(txn); + Author remote = messageParser.parseRequestMessage(m, body).getAuthor(); + if (local.equals(remote)) throw new FormatException(); + SessionId sessionId = crypto.getSessionId(introducer, local, remote); + boolean alice = crypto.isAlice(local.getId(), remote.getId()); + return IntroduceeSession + .getInitial(m.getGroupId(), sessionId, introducer, alice, + remote); + } + + private S handleMessage(Transaction txn, Message m, + BdfList body, MessageType type, S session, ProtocolEngine engine) + throws DbException, FormatException { + if (type == REQUEST) { + RequestMessage request = messageParser.parseRequestMessage(m, body); + return engine.onRequestMessage(txn, session, request); + } else if (type == ACCEPT) { + AcceptMessage accept = messageParser.parseAcceptMessage(m, body); + return engine.onAcceptMessage(txn, session, accept); + } else if (type == DECLINE) { + DeclineMessage decline = messageParser.parseDeclineMessage(m, body); + return engine.onDeclineMessage(txn, session, decline); + } else if (type == AUTH) { + AuthMessage auth = messageParser.parseAuthMessage(m, body); + return engine.onAuthMessage(txn, session, auth); + } else if (type == ACTIVATE) { + ActivateMessage activate = + messageParser.parseActivateMessage(m, body); + return engine.onActivateMessage(txn, session, activate); + } else if (type == ABORT) { + AbortMessage abort = messageParser.parseAbortMessage(m, body); + return engine.onAbortMessage(txn, session, abort); + } else { + throw new AssertionError(); + } + } + + @Nullable + private StoredSession getSession(Transaction txn, + @Nullable SessionId sessionId) throws DbException, FormatException { + if (sessionId == null) return null; + BdfDictionary query = sessionParser.getSessionQuery(sessionId); + Map results = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), query); + if (results.size() > 1) throw new DbException(); + if (results.isEmpty()) return null; + return new StoredSession(results.keySet().iterator().next(), + results.values().iterator().next()); + } + + private ContactId getContactId(Transaction txn, GroupId contactGroupId) + throws DbException, FormatException { + BdfDictionary meta = + clientHelper.getGroupMetadataAsDictionary(txn, contactGroupId); + return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue()); + } + + private MessageId createStorageId(Transaction txn) throws DbException { + Message m = clientHelper + .createMessageForStoringMetadata(localGroup.getId()); + db.addLocalMessage(txn, m, new Metadata(), false); + return m.getId(); + } + + private void storeSession(Transaction txn, MessageId storageId, + Session session) throws DbException { + BdfDictionary d; + if (session.getRole() == INTRODUCER) { + d = sessionEncoder + .encodeIntroducerSession((IntroducerSession) session); + } else if (session.getRole() == INTRODUCEE) { + d = sessionEncoder + .encodeIntroduceeSession((IntroduceeSession) session); + } else { + throw new AssertionError(); + } + try { + clientHelper.mergeMessageMetadata(txn, storageId, d); + } catch (FormatException e) { + throw new AssertionError(); + } + } + + @Override + public boolean canIntroduce(Contact c1, Contact c2) throws DbException { + Transaction txn = db.startTransaction(true); + try { + boolean can = canIntroduce(txn, c1, c2); + db.commitTransaction(txn); + return can; + } catch (FormatException e) { + throw new DbException(e); + } finally { + db.endTransaction(txn); + } + } + + private boolean canIntroduce(Transaction txn, Contact c1, Contact c2) + throws DbException, FormatException { + // Look up the session, if there is one + Author introducer = identityManager.getLocalAuthor(txn); + SessionId sessionId = + crypto.getSessionId(introducer, c1.getAuthor(), + c2.getAuthor()); + StoredSession ss = getSession(txn, sessionId); + if (ss == null) return true; + IntroducerSession session = + sessionParser.parseIntroducerSession(ss.bdfSession); + return session.getState() == START; } @Override public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, - long timestamp) throws DbException, FormatException { - + long timestamp) throws DbException { Transaction txn = db.startTransaction(false); try { - introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp); - Group g1 = getContactGroup(c1); - Group g2 = getContactGroup(c2); - messageTracker.trackMessage(txn, g1.getId(), timestamp, true); - messageTracker.trackMessage(txn, g2.getId(), timestamp, true); + // Look up the session, if there is one + Author introducer = identityManager.getLocalAuthor(txn); + SessionId sessionId = + crypto.getSessionId(introducer, c1.getAuthor(), + c2.getAuthor()); + StoredSession ss = getSession(txn, sessionId); + // Create or parse the session + IntroducerSession session; + MessageId storageId; + if (ss == null) { + // This is the first request - create a new session + GroupId groupId1 = getContactGroup(c1).getId(); + GroupId groupId2 = getContactGroup(c2).getId(); + boolean alice = crypto.isAlice(c1.getAuthor().getId(), + c2.getAuthor().getId()); + // use fixed deterministic roles for the introducees + session = new IntroducerSession(sessionId, + alice ? groupId1 : groupId2, + alice ? c1.getAuthor() : c2.getAuthor(), + alice ? groupId2 : groupId1, + alice ? c2.getAuthor() : c1.getAuthor() + ); + storageId = createStorageId(txn); + } else { + // An earlier request exists, so we already have a session + session = sessionParser.parseIntroducerSession(ss.bdfSession); + storageId = ss.storageId; + } + // Handle the request action + session = introducerEngine + .onRequestAction(txn, session, msg, timestamp); + // Store the updated session + storeSession(txn, storageId, session); db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); } finally { db.endTransaction(txn); } } @Override - public void acceptIntroduction(ContactId contactId, SessionId sessionId, - long timestamp) throws DbException, FormatException { - + public void respondToIntroduction(ContactId contactId, SessionId sessionId, + long timestamp, boolean accept) throws DbException { Transaction txn = db.startTransaction(false); try { - Contact c = db.getContact(txn, contactId); - Group g = getContactGroup(c); - BdfDictionary state = - getSessionState(txn, g.getId(), sessionId.getBytes()); - - introduceeManager.acceptIntroduction(txn, state, timestamp); - messageTracker.trackMessage(txn, g.getId(), timestamp, true); + // Look up the session + StoredSession ss = getSession(txn, sessionId); + if (ss == null) { + // Actions from the UI may be based on stale information. + // The contact might just have been deleted, for example. + // Throwing a DbException here aborts gracefully. + throw new DbException(); + } + // Parse the session + Contact contact = db.getContact(txn, contactId); + GroupId contactGroupId = getContactGroup(contact).getId(); + IntroduceeSession session = sessionParser + .parseIntroduceeSession(contactGroupId, ss.bdfSession); + // Handle the join or leave action + if (accept) { + session = introduceeEngine + .onAcceptAction(txn, session, timestamp); + } else { + session = introduceeEngine + .onDeclineAction(txn, session, timestamp); + } + // Store the updated session + storeSession(txn, ss.storageId, session); db.commitTransaction(txn); + } catch (FormatException e) { + throw new DbException(e); } finally { db.endTransaction(txn); } } @Override - public void declineIntroduction(ContactId contactId, SessionId sessionId, - long timestamp) throws DbException, FormatException { - - Transaction txn = db.startTransaction(false); - try { - Contact c = db.getContact(txn, contactId); - Group g = getContactGroup(c); - BdfDictionary state = - getSessionState(txn, g.getId(), sessionId.getBytes()); - - introduceeManager.declineIntroduction(txn, state, timestamp); - messageTracker.trackMessage(txn, g.getId(), timestamp, true); - db.commitTransaction(txn); - } finally { - db.endTransaction(txn); - } - } - - @Override - public Collection getIntroductionMessages( - ContactId contactId) throws DbException { - - Collection list = new ArrayList<>(); - - Map metadata; - Collection statuses; + public Collection getIntroductionMessages(ContactId c) + throws DbException { + List messages; Transaction txn = db.startTransaction(true); try { - // get messages and their status - GroupId g = getContactGroup(db.getContact(txn, contactId)).getId(); - metadata = clientHelper.getMessageMetadataAsDictionary(txn, g); - statuses = db.getMessageStatus(txn, contactId, g); - - // turn messages into classes for the UI - 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 = - getSessionState(txn, g, sessionId.getBytes()); - - int role = state.getLong(ROLE).intValue(); - boolean local; - long time = msg.getLong(MESSAGE_TIME); - boolean accepted = msg.getBoolean(ACCEPT, false); - boolean read = msg.getBoolean(MSG_KEY_READ, false); - AuthorId authorId; - String name; - if (type == TYPE_RESPONSE) { - if (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, - // check if it was a decline - if (!accepted) { - local = false; - } else { - // don't include positive responses - continue; - } - } else { - local = true; - } - authorId = new AuthorId( - state.getRaw(REMOTE_AUTHOR_ID)); - name = state.getString(NAME); - } - IntroductionResponse ir = new IntroductionResponse( - sessionId, messageId, g, role, time, local, - s.isSent(), s.isSeen(), read, authorId, name, - accepted); - list.add(ir); - } else if (type == TYPE_REQUEST) { - String message; - boolean answered, exists, introducesOtherIdentity; - if (role == ROLE_INTRODUCER) { - local = true; - authorId = - getAuthorIdForIntroducer(contactId, state); - name = getNameForIntroducer(contactId, state); - message = msg.getOptionalString(MSG); - answered = false; - exists = false; - introducesOtherIdentity = false; - } else { - local = false; - authorId = new AuthorId( - state.getRaw(REMOTE_AUTHOR_ID)); - name = state.getString(NAME); - message = state.getOptionalString(MSG); - boolean finished = state.getLong(STATE) == - FINISHED.getValue(); - answered = finished || state.getBoolean(ANSWERED); - exists = state.getBoolean(EXISTS); - introducesOtherIdentity = - state.getBoolean(REMOTE_AUTHOR_IS_US); - } - IntroductionRequest ir = new IntroductionRequest( - sessionId, messageId, g, role, time, local, - s.isSent(), s.isSeen(), read, authorId, name, - accepted, message, answered, exists, - introducesOtherIdentity); - list.add(ir); - } - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) - LOG.log(WARNING, e.toString(), e); + Contact contact = db.getContact(txn, c); + GroupId contactGroupId = getContactGroup(contact).getId(); + BdfDictionary query = messageParser.getMessagesVisibleInUiQuery(); + Map results = clientHelper + .getMessageMetadataAsDictionary(txn, contactGroupId, query); + messages = new ArrayList<>(results.size()); + for (Entry e : results.entrySet()) { + MessageId m = e.getKey(); + MessageMetadata meta = + messageParser.parseMetadata(e.getValue()); + MessageStatus status = db.getMessageStatus(txn, c, m); + StoredSession ss = getSession(txn, meta.getSessionId()); + if (ss == null) throw new AssertionError(); + MessageType type = meta.getMessageType(); + if (type == REQUEST) { + messages.add( + parseInvitationRequest(txn, contactGroupId, m, + meta, status, ss.bdfSession)); + } else if (type == ACCEPT) { + messages.add( + parseInvitationResponse(contactGroupId, m, meta, + status, ss.bdfSession, true)); + } else if (type == DECLINE) { + messages.add( + parseInvitationResponse(contactGroupId, m, meta, + status, ss.bdfSession, false)); } } db.commitTransaction(txn); @@ -438,88 +422,140 @@ class IntroductionManagerImpl extends ConversationClientImpl } finally { db.endTransaction(txn); } - return list; + return messages; } - 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()); - } - } - - private BdfDictionary getSessionState(Transaction txn, GroupId groupId, - byte[] sessionId, boolean warn) + private IntroductionRequest parseInvitationRequest(Transaction txn, + GroupId contactGroupId, MessageId m, MessageMetadata meta, + MessageStatus status, BdfDictionary bdfSession) throws DbException, FormatException { + Role role = sessionParser.getRole(bdfSession); + SessionId sessionId; + Author author; + if (role == INTRODUCER) { + IntroducerSession session = + sessionParser.parseIntroducerSession(bdfSession); + sessionId = session.getSessionId(); + if (contactGroupId.equals(session.getIntroduceeA().groupId)) { + author = session.getIntroduceeB().author; + } else { + author = session.getIntroduceeA().author; + } + } else if (role == INTRODUCEE) { + IntroduceeSession session = sessionParser + .parseIntroduceeSession(contactGroupId, bdfSession); + sessionId = session.getSessionId(); + author = session.getRemote().author; + } else throw new AssertionError(); + Message msg = clientHelper.getMessage(txn, m); + if (msg == null) throw new AssertionError(); + BdfList body = clientHelper.toList(msg); + RequestMessage rm = messageParser.parseRequestMessage(msg, body); + String message = rm.getMessage(); + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + boolean contactExists = contactManager + .contactExists(txn, rm.getAuthor().getId(), + localAuthor.getId()); + return new IntroductionRequest(sessionId, m, contactGroupId, + role, meta.getTimestamp(), meta.isLocal(), + status.isSent(), status.isSeen(), meta.isRead(), + author.getName(), false, message, !meta.isAvailableToAnswer(), + contactExists); + } + + private IntroductionResponse parseInvitationResponse(GroupId contactGroupId, + MessageId m, MessageMetadata meta, MessageStatus status, + BdfDictionary bdfSession, boolean accept) throws FormatException { + Role role = sessionParser.getRole(bdfSession); + SessionId sessionId; + Author author; + if (role == INTRODUCER) { + IntroducerSession session = + sessionParser.parseIntroducerSession(bdfSession); + sessionId = session.getSessionId(); + if (contactGroupId.equals(session.getIntroduceeA().groupId)) { + author = session.getIntroduceeB().author; + } else { + author = session.getIntroduceeA().author; + } + } else if (role == INTRODUCEE) { + IntroduceeSession session = sessionParser + .parseIntroduceeSession(contactGroupId, bdfSession); + sessionId = session.getSessionId(); + author = session.getRemote().author; + } else throw new AssertionError(); + return new IntroductionResponse(sessionId, m, contactGroupId, + role, meta.getTimestamp(), meta.isLocal(), status.isSent(), + status.isSeen(), meta.isRead(), author.getName(), accept); + } + + private void removeSessionWithIntroducer(Transaction txn, + Contact introducer) throws DbException { + BdfDictionary query = sessionEncoder + .getIntroduceeSessionsByIntroducerQuery(introducer.getAuthor()); + Map sessions; try { - // See if we can find the state directly for the introducer - BdfDictionary state = clientHelper - .getMessageMetadataAsDictionary(txn, - new MessageId(sessionId)); - GroupId g1 = new GroupId(state.getRaw(GROUP_ID_1)); - GroupId g2 = new GroupId(state.getRaw(GROUP_ID_2)); - if (!g1.equals(groupId) && !g2.equals(groupId)) { - throw new NoSuchMessageException(); - } - return state; - } catch (NoSuchMessageException e) { - // State not found directly, so iterate over all states - // to find state for introducee - Map map = clientHelper - .getMessageMetadataAsDictionary(txn, - introductionGroupFactory.createLocalGroup().getId()); - for (Map.Entry m : map.entrySet()) { - if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) { - BdfDictionary state = m.getValue(); - GroupId g = new GroupId(state.getRaw(GROUP_ID)); - if (g.equals(groupId)) return state; - } - } - if (warn && LOG.isLoggable(WARNING)) - LOG.warning("No session state found"); - throw new FormatException(); + sessions = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); + } catch (FormatException e) { + throw new DbException(e); + } + for (MessageId id : sessions.keySet()) { + db.removeMessage(txn, id); } } - private BdfDictionary getSessionState(Transaction txn, GroupId groupId, - byte[] sessionId) throws DbException, FormatException { - - return getSessionState(txn, groupId, sessionId, true); + private void abortOrRemoveSessionWithIntroducee(Transaction txn, + Contact c) throws DbException { + BdfDictionary query = sessionEncoder.getIntroducerSessionsQuery(); + Map sessions; + try { + sessions = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); + } catch (FormatException e) { + throw new DbException(); + } + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + for (Entry session : sessions.entrySet()) { + IntroducerSession s; + try { + s = sessionParser.parseIntroducerSession(session.getValue()); + } catch (FormatException e) { + throw new DbException(); + } + if (s.getIntroduceeA().author.equals(c.getAuthor())) { + abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(), + s.getIntroduceeB(), localAuthor); + } else if (s.getIntroduceeB().author.equals(c.getAuthor())) { + abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(), + s.getIntroduceeA(), localAuthor); + } + } } - private void deleteMessage(Transaction txn, MessageId messageId) - throws DbException { + private void abortOrRemoveSessionWithIntroducee(Transaction txn, + IntroducerSession s, MessageId storageId, Introducee i, + LocalAuthor localAuthor) throws DbException { + if (db.containsContact(txn, i.author.getId(), localAuthor.getId())) { + IntroducerSession session = introducerEngine.onAbortAction(txn, s); + storeSession(txn, storageId, session); + } else { + db.removeMessage(txn, storageId); + } + } - db.deleteMessage(txn, messageId); - db.deleteMessageMetadata(txn, messageId); + private static class StoredSession { + + private final MessageId storageId; + private final BdfDictionary bdfSession; + + private StoredSession(MessageId storageId, BdfDictionary bdfSession) { + this.storageId = storageId; + this.bdfSession = bdfSession; + } } } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java index e0122faa9..24c649c5f 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionModule.java @@ -4,8 +4,8 @@ import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.lifecycle.LifecycleManager; +import org.briarproject.bramble.api.sync.ValidationManager; import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.MessageQueueManager; import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.messaging.ConversationManager; @@ -21,22 +21,22 @@ import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT public class IntroductionModule { public static class EagerSingletons { - @Inject - IntroductionManager introductionManager; @Inject IntroductionValidator introductionValidator; + @Inject + IntroductionManager introductionManager; } @Provides @Singleton - IntroductionValidator provideValidator( - MessageQueueManager messageQueueManager, - MetadataEncoder metadataEncoder, ClientHelper clientHelper, - Clock clock) { + IntroductionValidator provideValidator(ValidationManager validationManager, + MessageEncoder messageEncoder, MetadataEncoder metadataEncoder, + ClientHelper clientHelper, Clock clock) { - IntroductionValidator introductionValidator = new IntroductionValidator( - clientHelper, metadataEncoder, clock); - messageQueueManager.registerMessageValidator(CLIENT_ID, + IntroductionValidator introductionValidator = + new IntroductionValidator(messageEncoder, clientHelper, + metadataEncoder, clock); + validationManager.registerMessageValidator(CLIENT_ID, introductionValidator); return introductionValidator; @@ -46,16 +46,42 @@ public class IntroductionModule { @Singleton IntroductionManager provideIntroductionManager( LifecycleManager lifecycleManager, ContactManager contactManager, - MessageQueueManager messageQueueManager, + ValidationManager validationManager, ConversationManager conversationManager, IntroductionManagerImpl introductionManager) { - lifecycleManager.registerClient(introductionManager); contactManager.registerContactHook(introductionManager); - messageQueueManager.registerIncomingMessageHook(CLIENT_ID, + validationManager.registerIncomingMessageHook(CLIENT_ID, introductionManager); conversationManager.registerConversationClient(introductionManager); return introductionManager; } + + @Provides + MessageParser provideMessageParser(MessageParserImpl messageParser) { + return messageParser; + } + + @Provides + MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) { + return messageEncoder; + } + + @Provides + SessionParser provideSessionParser(SessionParserImpl sessionParser) { + return sessionParser; + } + + @Provides + SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) { + return sessionEncoder; + } + + @Provides + IntroductionCrypto provideIntroductionCrypto( + IntroductionCryptoImpl introductionCrypto) { + return introductionCrypto; + } + } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java index 9fc526e49..929c8bddf 100644 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/IntroductionValidator.java @@ -1,7 +1,9 @@ package org.briarproject.briar.introduction; import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.UniqueId; import org.briarproject.bramble.api.client.BdfMessageContext; +import org.briarproject.bramble.api.client.BdfMessageValidator; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfList; @@ -9,183 +11,190 @@ import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.system.Clock; import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.client.BdfQueueMessageValidator; + +import java.util.Collections; import javax.annotation.concurrent.Immutable; -import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH; -import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT; -import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; import static org.briarproject.bramble.util.ValidationUtils.checkLength; import static org.briarproject.bramble.util.ValidationUtils.checkSize; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; +import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH; +import static org.briarproject.briar.introduction.MessageType.ACCEPT; +import static org.briarproject.briar.introduction.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction.MessageType.AUTH; + @Immutable @NotNullByDefault -class IntroductionValidator extends BdfQueueMessageValidator { +class IntroductionValidator extends BdfMessageValidator { - IntroductionValidator(ClientHelper clientHelper, - MetadataEncoder metadataEncoder, Clock clock) { + private final MessageEncoder messageEncoder; + + IntroductionValidator(MessageEncoder messageEncoder, + ClientHelper clientHelper, MetadataEncoder metadataEncoder, + Clock clock) { super(clientHelper, metadataEncoder, clock); + this.messageEncoder = messageEncoder; } @Override protected BdfMessageContext validateMessage(Message m, Group g, BdfList body) throws FormatException { + MessageType type = MessageType.fromValue(body.getLong(0).intValue()); - BdfDictionary d; - long type = body.getLong(0); - byte[] id = body.getRaw(1); - checkLength(id, SessionId.LENGTH); + switch (type) { + case REQUEST: + return validateRequestMessage(m, body); + case ACCEPT: + return validateAcceptMessage(m, body); + case AUTH: + return validateAuthMessage(m, body); + case ACTIVATE: + return validateActivateMessage(m, body); + case DECLINE: + case ABORT: + return validateOtherMessage(type, m, body); + default: + throw new FormatException(); + } + } - 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); + private BdfMessageContext validateRequestMessage(Message m, BdfList body) + throws FormatException { + checkSize(body, 4); + + byte[] previousMessageId = body.getOptionalRaw(1); + checkLength(previousMessageId, UniqueId.LENGTH); + + BdfList authorList = body.getList(2); + clientHelper.parseAndValidateAuthor(authorList); + + String msg = body.getOptionalString(3); + checkLength(msg, 1, MAX_REQUEST_MESSAGE_LENGTH); + + BdfDictionary meta = + messageEncoder.encodeRequestMetadata(m.getTimestamp()); + if (previousMessageId == null) { + return new BdfMessageContext(meta); } else { - throw new FormatException(); + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); } - - d.put(TYPE, type); - d.put(SESSION_ID, id); - d.put(GROUP_ID, m.getGroupId()); - d.put(MESSAGE_ID, m.getId()); - d.put(MESSAGE_TIME, m.getTimestamp()); - return new BdfMessageContext(d); } - private BdfDictionary validateRequest(BdfList message) + private BdfMessageContext validateAcceptMessage(Message m, BdfList body) throws FormatException { + checkSize(body, 6); - checkSize(message, 4, 5); + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); - // TODO: Exchange author format version + byte[] previousMessageId = body.getOptionalRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); - // parse contact name - String name = message.getString(2); - checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH); + byte[] ephemeralPublicKey = body.getRaw(3); + checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH); - // parse contact's public key - byte[] key = message.getRaw(3); - checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH); + long timestamp = body.getLong(4); + if (timestamp < 0) throw new FormatException(); - // parse (optional) message - String msg = null; - if (message.size() == 5) { - msg = message.getString(4); - checkLength(msg, 0, MAX_INTRODUCTION_MESSAGE_LENGTH); - } + BdfDictionary transportProperties = body.getDictionary(5); + if (transportProperties.size() < 1) throw new FormatException(); + clientHelper + .parseAndValidateTransportPropertiesMap(transportProperties); - // 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, 6); - - // parse accept/decline - boolean accept = message.getBoolean(2); - - long time = 0; - byte[] pubkey = null; - BdfDictionary tp = new BdfDictionary(); - if (accept) { - checkSize(message, 6); - - // parse timestamp - time = message.getLong(3); - - // parse ephemeral public key - pubkey = message.getRaw(4); - checkLength(pubkey, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES); - - // parse transport properties - tp = message.getDictionary(5); - if (tp.size() < 1) throw new FormatException(); - for (String tId : tp.keySet()) { - checkLength(tId, 1, MAX_TRANSPORT_ID_LENGTH); - BdfDictionary tProps = tp.getDictionary(tId); - checkSize(tProps, 0, MAX_PROPERTIES_PER_TRANSPORT); - for (String propId : tProps.keySet()) { - checkLength(propId, 0, MAX_PROPERTY_LENGTH); - String prop = tProps.getString(propId); - checkLength(prop, 0, MAX_PROPERTY_LENGTH); - } - } + SessionId sessionId = new SessionId(sessionIdBytes); + BdfDictionary meta = messageEncoder + .encodeMetadata(ACCEPT, sessionId, m.getTimestamp(), false, + false, false); + if (previousMessageId == null) { + return new BdfMessageContext(meta); } else { - checkSize(message, 3); + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); } - - // 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(TRANSPORT, tp); - } - return d; } - private BdfDictionary validateAck(BdfList message) throws FormatException { - checkSize(message, 4); - - byte[] mac = message.getRaw(2); - checkLength(mac, 1, MAC_LENGTH); - - byte[] sig = message.getRaw(3); - checkLength(sig, 1, MAX_SIGNATURE_LENGTH); - - // Return the metadata - BdfDictionary d = new BdfDictionary(); - d.put(MAC, mac); - d.put(SIGNATURE, sig); - return d; - } - - private BdfDictionary validateAbort(BdfList message) + private BdfMessageContext validateAuthMessage(Message m, BdfList body) throws FormatException { + checkSize(body, 5); - checkSize(message, 2); + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); - // Return the metadata - return new BdfDictionary(); + byte[] previousMessageId = body.getRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); + + byte[] mac = body.getRaw(3); + checkLength(mac, MAC_BYTES); + + byte[] signature = body.getRaw(4); + checkLength(signature, 1, MAX_SIGNATURE_BYTES); + + SessionId sessionId = new SessionId(sessionIdBytes); + BdfDictionary meta = messageEncoder + .encodeMetadata(AUTH, sessionId, m.getTimestamp(), false, false, + false); + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); } + + private BdfMessageContext validateActivateMessage(Message m, BdfList body) + throws FormatException { + checkSize(body, 4); + + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); + + byte[] previousMessageId = body.getRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); + + byte[] mac = body.getOptionalRaw(3); + checkLength(mac, MAC_BYTES); + + SessionId sessionId = new SessionId(sessionIdBytes); + BdfDictionary meta = messageEncoder + .encodeMetadata(ACTIVATE, sessionId, m.getTimestamp(), false, + false, false); + if (previousMessageId == null) { + return new BdfMessageContext(meta); + } else { + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); + } + } + + private BdfMessageContext validateOtherMessage(MessageType type, + Message m, BdfList body) throws FormatException { + checkSize(body, 3); + + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); + + byte[] previousMessageId = body.getOptionalRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); + + SessionId sessionId = new SessionId(sessionIdBytes); + BdfDictionary meta = messageEncoder + .encodeMetadata(type, sessionId, m.getTimestamp(), false, false, + false); + if (previousMessageId == null) { + return new BdfMessageContext(meta); + } else { + MessageId dependency = new MessageId(previousMessageId); + return new BdfMessageContext(meta, + Collections.singletonList(dependency)); + } + } + } diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java new file mode 100644 index 000000000..1327b54a9 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoder.java @@ -0,0 +1,55 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import java.util.Map; + +import javax.annotation.Nullable; + +@NotNullByDefault +interface MessageEncoder { + + BdfDictionary encodeRequestMetadata(long timestamp); + + BdfDictionary encodeMetadata(MessageType type, + @Nullable SessionId sessionId, long timestamp, boolean local, + boolean read, boolean visible); + + void addSessionId(BdfDictionary meta, SessionId sessionId); + + void setVisibleInUi(BdfDictionary meta, boolean visible); + + void setAvailableToAnswer(BdfDictionary meta, boolean available); + + Message encodeRequestMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, Author author, + @Nullable String message); + + Message encodeAcceptMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId, + byte[] ephemeralPublicKey, long acceptTimestamp, + Map transportProperties); + + Message encodeDeclineMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId); + + Message encodeAuthMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId, + byte[] mac, byte[] signature); + + Message encodeActivateMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId, + byte[] mac); + + Message encodeAbortMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId); + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java new file mode 100644 index 000000000..fb3d66f38 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageEncoderImpl.java @@ -0,0 +1,183 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageFactory; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.inject.Inject; + +import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI; +import static org.briarproject.briar.introduction.MessageType.ABORT; +import static org.briarproject.briar.introduction.MessageType.ACCEPT; +import static org.briarproject.briar.introduction.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction.MessageType.AUTH; +import static org.briarproject.briar.introduction.MessageType.DECLINE; +import static org.briarproject.briar.introduction.MessageType.REQUEST; + +@NotNullByDefault +class MessageEncoderImpl implements MessageEncoder { + + private final ClientHelper clientHelper; + private final MessageFactory messageFactory; + + @Inject + MessageEncoderImpl(ClientHelper clientHelper, + MessageFactory messageFactory) { + this.clientHelper = clientHelper; + this.messageFactory = messageFactory; + } + + @Override + public BdfDictionary encodeRequestMetadata(long timestamp) { + BdfDictionary meta = + encodeMetadata(REQUEST, null, timestamp, false, false, false); + meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, false); + return meta; + } + + @Override + public BdfDictionary encodeMetadata(MessageType type, + @Nullable SessionId sessionId, long timestamp, boolean local, + boolean read, boolean visible) { + BdfDictionary meta = new BdfDictionary(); + meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue()); + if (sessionId != null) + meta.put(MSG_KEY_SESSION_ID, sessionId); + else if (type != REQUEST) + throw new IllegalArgumentException(); + meta.put(MSG_KEY_TIMESTAMP, timestamp); + meta.put(MSG_KEY_LOCAL, local); + meta.put(MSG_KEY_READ, read); + meta.put(MSG_KEY_VISIBLE_IN_UI, visible); + return meta; + } + + @Override + public void addSessionId(BdfDictionary meta, SessionId sessionId) { + meta.put(MSG_KEY_SESSION_ID, sessionId); + } + + @Override + public void setVisibleInUi(BdfDictionary meta, boolean visible) { + meta.put(MSG_KEY_VISIBLE_IN_UI, visible); + } + + @Override + public void setAvailableToAnswer(BdfDictionary meta, boolean available) { + meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available); + } + + @Override + public Message encodeRequestMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, Author author, + @Nullable String message) { + if (message != null && message.equals("")) { + throw new IllegalArgumentException(); + } + BdfList body = BdfList.of( + REQUEST.getValue(), + previousMessageId, + clientHelper.toList(author), + message + ); + return createMessage(contactGroupId, timestamp, body); + } + + @Override + public Message encodeAcceptMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId, + byte[] ephemeralPublicKey, long acceptTimestamp, + Map transportProperties) { + BdfList body = BdfList.of( + ACCEPT.getValue(), + sessionId, + previousMessageId, + ephemeralPublicKey, + acceptTimestamp, + clientHelper.toDictionary(transportProperties) + ); + return createMessage(contactGroupId, timestamp, body); + } + + @Override + public Message encodeDeclineMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId) { + return encodeMessage(DECLINE, contactGroupId, sessionId, timestamp, + previousMessageId); + } + + @Override + public Message encodeAuthMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId, + byte[] mac, byte[] signature) { + BdfList body = BdfList.of( + AUTH.getValue(), + sessionId, + previousMessageId, + mac, + signature + ); + return createMessage(contactGroupId, timestamp, body); + } + + @Override + public Message encodeActivateMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId, + byte[] mac) { + BdfList body = BdfList.of( + ACTIVATE.getValue(), + sessionId, + previousMessageId, + mac + ); + return createMessage(contactGroupId, timestamp, body); + } + + @Override + public Message encodeAbortMessage(GroupId contactGroupId, long timestamp, + @Nullable MessageId previousMessageId, SessionId sessionId) { + return encodeMessage(ABORT, contactGroupId, sessionId, timestamp, + previousMessageId); + } + + private Message encodeMessage(MessageType type, GroupId contactGroupId, + SessionId sessionId, long timestamp, + @Nullable MessageId previousMessageId) { + BdfList body = BdfList.of( + type.getValue(), + sessionId, + previousMessageId + ); + return createMessage(contactGroupId, timestamp, body); + } + + private Message createMessage(GroupId contactGroupId, long timestamp, + BdfList body) { + try { + return messageFactory.createMessage(contactGroupId, timestamp, + clientHelper.toByteArray(body)); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java new file mode 100644 index 000000000..102d72bfc --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageMetadata.java @@ -0,0 +1,60 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.api.client.SessionId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class MessageMetadata { + + private final MessageType type; + @Nullable + private final SessionId sessionId; + private final long timestamp; + private final boolean local, read, visible, available; + + MessageMetadata(MessageType type, @Nullable SessionId sessionId, + long timestamp, boolean local, boolean read, boolean visible, + boolean available) { + this.type = type; + this.sessionId = sessionId; + this.timestamp = timestamp; + this.local = local; + this.read = read; + this.visible = visible; + this.available = available; + } + + MessageType getMessageType() { + return type; + } + + @Nullable + public SessionId getSessionId() { + return sessionId; + } + + long getTimestamp() { + return timestamp; + } + + boolean isLocal() { + return local; + } + + boolean isRead() { + return read; + } + + boolean isVisibleInConversation() { + return visible; + } + + boolean isAvailableToAnswer() { + return available; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java new file mode 100644 index 000000000..503dd4cc6 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParser.java @@ -0,0 +1,37 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.briar.api.client.SessionId; + +@NotNullByDefault +interface MessageParser { + + BdfDictionary getMessagesVisibleInUiQuery(); + + BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId); + + MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException; + + RequestMessage parseRequestMessage(Message m, BdfList body) + throws FormatException; + + AcceptMessage parseAcceptMessage(Message m, BdfList body) + throws FormatException; + + DeclineMessage parseDeclineMessage(Message m, BdfList body) + throws FormatException; + + AuthMessage parseAuthMessage(Message m, BdfList body) + throws FormatException; + + ActivateMessage parseActivateMessage(Message m, BdfList body) + throws FormatException; + + AbortMessage parseAbortMessage(Message m, BdfList body) + throws FormatException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java new file mode 100644 index 000000000..69ddd242d --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageParserImpl.java @@ -0,0 +1,143 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import java.util.Map; + +import javax.inject.Inject; + +import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_AVAILABLE_TO_ANSWER; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_LOCAL; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_SESSION_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_VISIBLE_IN_UI; +import static org.briarproject.briar.introduction.MessageType.REQUEST; + +@NotNullByDefault +class MessageParserImpl implements MessageParser { + + private final ClientHelper clientHelper; + + @Inject + MessageParserImpl(ClientHelper clientHelper) { + this.clientHelper = clientHelper; + } + + @Override + public BdfDictionary getMessagesVisibleInUiQuery() { + return BdfDictionary.of(new BdfEntry(MSG_KEY_VISIBLE_IN_UI, true)); + } + + @Override + public BdfDictionary getRequestsAvailableToAnswerQuery(SessionId sessionId) { + return BdfDictionary.of( + new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true), + new BdfEntry(MSG_KEY_MESSAGE_TYPE, REQUEST.getValue()), + new BdfEntry(MSG_KEY_SESSION_ID, sessionId) + ); + } + + @Override + public MessageMetadata parseMetadata(BdfDictionary d) + throws FormatException { + MessageType type = MessageType + .fromValue(d.getLong(MSG_KEY_MESSAGE_TYPE).intValue()); + byte[] sessionIdBytes = d.getOptionalRaw(MSG_KEY_SESSION_ID); + SessionId sessionId = + sessionIdBytes == null ? null : new SessionId(sessionIdBytes); + long timestamp = d.getLong(MSG_KEY_TIMESTAMP); + boolean local = d.getBoolean(MSG_KEY_LOCAL); + boolean read = d.getBoolean(MSG_KEY_READ); + boolean visible = d.getBoolean(MSG_KEY_VISIBLE_IN_UI); + boolean available = d.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false); + return new MessageMetadata(type, sessionId, timestamp, local, read, + visible, available); + } + + @Override + public RequestMessage parseRequestMessage(Message m, BdfList body) + throws FormatException { + byte[] previousMsgBytes = body.getOptionalRaw(1); + MessageId previousMessageId = (previousMsgBytes == null ? null : + new MessageId(previousMsgBytes)); + Author author = clientHelper.parseAndValidateAuthor(body.getList(2)); + String message = body.getOptionalString(3); + return new RequestMessage(m.getId(), m.getGroupId(), + m.getTimestamp(), previousMessageId, author, message); + } + + @Override + public AcceptMessage parseAcceptMessage(Message m, BdfList body) + throws FormatException { + SessionId sessionId = new SessionId(body.getRaw(1)); + byte[] previousMsgBytes = body.getOptionalRaw(2); + MessageId previousMessageId = (previousMsgBytes == null ? null : + new MessageId(previousMsgBytes)); + byte[] ephemeralPublicKey = body.getRaw(3); + long acceptTimestamp = body.getLong(4); + Map transportProperties = clientHelper + .parseAndValidateTransportPropertiesMap(body.getDictionary(5)); + return new AcceptMessage(m.getId(), m.getGroupId(), m.getTimestamp(), + previousMessageId, sessionId, ephemeralPublicKey, + acceptTimestamp, transportProperties); + } + + @Override + public DeclineMessage parseDeclineMessage(Message m, BdfList body) + throws FormatException { + SessionId sessionId = new SessionId(body.getRaw(1)); + byte[] previousMsgBytes = body.getOptionalRaw(2); + MessageId previousMessageId = (previousMsgBytes == null ? null : + new MessageId(previousMsgBytes)); + return new DeclineMessage(m.getId(), m.getGroupId(), m.getTimestamp(), + previousMessageId, sessionId); + } + + @Override + public AuthMessage parseAuthMessage(Message m, BdfList body) + throws FormatException { + SessionId sessionId = new SessionId(body.getRaw(1)); + byte[] previousMsgBytes = body.getRaw(2); + MessageId previousMessageId = new MessageId(previousMsgBytes); + byte[] mac = body.getRaw(3); + byte[] signature = body.getRaw(4); + return new AuthMessage(m.getId(), m.getGroupId(), m.getTimestamp(), + previousMessageId, sessionId, mac, signature); + } + + @Override + public ActivateMessage parseActivateMessage(Message m, BdfList body) + throws FormatException { + SessionId sessionId = new SessionId(body.getRaw(1)); + byte[] previousMsgBytes = body.getRaw(2); + MessageId previousMessageId = new MessageId(previousMsgBytes); + byte[] mac = body.getRaw(3); + return new ActivateMessage(m.getId(), m.getGroupId(), m.getTimestamp(), + previousMessageId, sessionId, mac); + } + + @Override + public AbortMessage parseAbortMessage(Message m, BdfList body) + throws FormatException { + SessionId sessionId = new SessionId(body.getRaw(1)); + byte[] previousMsgBytes = body.getOptionalRaw(2); + MessageId previousMessageId = (previousMsgBytes == null ? null : + new MessageId(previousMsgBytes)); + return new AbortMessage(m.getId(), m.getGroupId(), m.getTimestamp(), + previousMessageId, sessionId); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageSender.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageSender.java deleted file mode 100644 index 7848aaf3d..000000000 --- a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageSender.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataEncoder; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.nullsafety.NotNullByDefault; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.MessageQueueManager; - -import javax.annotation.concurrent.Immutable; -import javax.inject.Inject; - -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; - -@Immutable -@NotNullByDefault -class MessageSender { - - private final DatabaseComponent db; - private final ClientHelper clientHelper; - private final Clock clock; - private final MetadataEncoder metadataEncoder; - private final MessageQueueManager messageQueueManager; - - @Inject - MessageSender(DatabaseComponent db, ClientHelper clientHelper, Clock clock, - MetadataEncoder metadataEncoder, - MessageQueueManager messageQueueManager) { - - this.db = db; - this.clientHelper = clientHelper; - this.clock = clock; - this.metadataEncoder = metadataEncoder; - this.messageQueueManager = messageQueueManager; - } - - void sendMessage(Transaction txn, BdfDictionary message) - throws DbException, FormatException { - - BdfList bdfList = encodeMessage(message); - byte[] body = clientHelper.toByteArray(bdfList); - GroupId groupId = new GroupId(message.getRaw(GROUP_ID)); - Group group = db.getGroup(txn, groupId); - long timestamp = clock.currentTimeMillis(); - - message.put(MESSAGE_TIME, timestamp); - Metadata metadata = metadataEncoder.encode(message); - - messageQueueManager.sendMessage(txn, group, timestamp, body, metadata); - } - - private 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 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 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.getDictionary(TRANSPORT)); - } - return list; - } - - private BdfList encodeAck(BdfDictionary d) throws FormatException { - return BdfList.of(TYPE_ACK, d.getRaw(SESSION_ID), d.getRaw(MAC), - d.getRaw(SIGNATURE)); - } - - private BdfList encodeAbort(BdfDictionary d) throws FormatException { - return BdfList.of(TYPE_ABORT, d.getRaw(SESSION_ID)); - } - -} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/MessageType.java b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageType.java new file mode 100644 index 000000000..67365399b --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/MessageType.java @@ -0,0 +1,29 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +enum MessageType { + + REQUEST(0), ACCEPT(1), DECLINE(2), AUTH(3), ACTIVATE(4), ABORT(5); + + private final int value; + + MessageType(int value) { + this.value = value; + } + + int getValue() { + return value; + } + + static MessageType fromValue(int value) throws FormatException { + for (MessageType m : values()) if (m.value == value) return m; + throw new FormatException(); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java b/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java new file mode 100644 index 000000000..3c453d2fa --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/PeerSession.java @@ -0,0 +1,25 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.briar.api.client.SessionId; + +import javax.annotation.Nullable; + +@NotNullByDefault +interface PeerSession { + + SessionId getSessionId(); + + GroupId getContactGroupId(); + + long getLocalTimestamp(); + + @Nullable + MessageId getLastLocalMessageId(); + + @Nullable + MessageId getLastRemoteMessageId(); + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java b/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java new file mode 100644 index 000000000..e3766c91a --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/ProtocolEngine.java @@ -0,0 +1,40 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.db.Transaction; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +import javax.annotation.Nullable; + +@NotNullByDefault +interface ProtocolEngine { + + S onRequestAction(Transaction txn, S session, @Nullable String message, + long timestamp) throws DbException; + + S onAcceptAction(Transaction txn, S session, long timestamp) + throws DbException; + + S onDeclineAction(Transaction txn, S session, long timestamp) + throws DbException; + + S onRequestMessage(Transaction txn, S session, RequestMessage m) + throws DbException, FormatException; + + S onAcceptMessage(Transaction txn, S session, AcceptMessage m) + throws DbException, FormatException; + + S onDeclineMessage(Transaction txn, S session, DeclineMessage m) + throws DbException, FormatException; + + S onAuthMessage(Transaction txn, S session, AuthMessage m) + throws DbException, FormatException; + + S onActivateMessage(Transaction txn, S session, ActivateMessage m) + throws DbException, FormatException; + + S onAbortMessage(Transaction txn, S session, AbortMessage m) + throws DbException, FormatException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/RequestMessage.java b/briar-core/src/main/java/org/briarproject/briar/introduction/RequestMessage.java new file mode 100644 index 000000000..743e87af8 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/RequestMessage.java @@ -0,0 +1,36 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +class RequestMessage extends AbstractIntroductionMessage { + + private final Author author; + @Nullable + private final String message; + + protected RequestMessage(MessageId messageId, GroupId groupId, + long timestamp, @Nullable MessageId previousMessageId, + Author author, @Nullable String message) { + super(messageId, groupId, timestamp, previousMessageId); + this.author = author; + this.message = message; + } + + public Author getAuthor() { + return author; + } + + @Nullable + public String getMessage() { + return message; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java b/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java new file mode 100644 index 000000000..086dfb1a2 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/Session.java @@ -0,0 +1,37 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.Role; + +import javax.annotation.concurrent.Immutable; + +@Immutable +@NotNullByDefault +abstract class Session { + + private final SessionId sessionId; + private final S state; + private final long requestTimestamp; + + Session(SessionId sessionId, S state, long requestTimestamp) { + this.sessionId = sessionId; + this.state = state; + this.requestTimestamp = requestTimestamp; + } + + abstract Role getRole(); + + public SessionId getSessionId() { + return sessionId; + } + + S getState() { + return state; + } + + long getRequestTimestamp() { + return requestTimestamp; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoder.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoder.java new file mode 100644 index 000000000..70cfff1bb --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoder.java @@ -0,0 +1,18 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; + +@NotNullByDefault +interface SessionEncoder { + + BdfDictionary getIntroduceeSessionsByIntroducerQuery(Author introducer); + + BdfDictionary getIntroducerSessionsQuery(); + + BdfDictionary encodeIntroducerSession(IntroducerSession s); + + BdfDictionary encodeIntroduceeSession(IntroduceeSession s); + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java new file mode 100644 index 000000000..9023f0d94 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionEncoderImpl.java @@ -0,0 +1,159 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.briar.introduction.IntroduceeSession.Common; +import org.briarproject.briar.introduction.IntroduceeSession.Local; +import org.briarproject.briar.introduction.IntroduceeSession.Remote; +import org.briarproject.briar.introduction.IntroducerSession.Introducee; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.bramble.api.data.BdfDictionary.NULL_VALUE; +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; +import static org.briarproject.briar.api.introduction.Role.INTRODUCER; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ALICE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LOCAL; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_MAC_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_MASTER_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ROLE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_STATE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES; + +@Immutable +@NotNullByDefault +class SessionEncoderImpl implements SessionEncoder { + + private final ClientHelper clientHelper; + + @Inject + SessionEncoderImpl(ClientHelper clientHelper) { + this.clientHelper = clientHelper; + } + + @Override + public BdfDictionary getIntroduceeSessionsByIntroducerQuery( + Author introducer) { + return BdfDictionary.of( + new BdfEntry(SESSION_KEY_ROLE, INTRODUCEE.getValue()), + new BdfEntry(SESSION_KEY_INTRODUCER, + clientHelper.toList(introducer)) + ); + } + + @Override + public BdfDictionary getIntroducerSessionsQuery() { + return BdfDictionary.of( + new BdfEntry(SESSION_KEY_ROLE, INTRODUCER.getValue()) + ); + } + + @Override + public BdfDictionary encodeIntroducerSession(IntroducerSession s) { + BdfDictionary d = encodeSession(s); + d.put(SESSION_KEY_INTRODUCEE_A, encodeIntroducee(s.getIntroduceeA())); + d.put(SESSION_KEY_INTRODUCEE_B, encodeIntroducee(s.getIntroduceeB())); + return d; + } + + private BdfDictionary encodeIntroducee(Introducee i) { + BdfDictionary d = new BdfDictionary(); + putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, i.lastLocalMessageId); + putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID, + i.lastRemoteMessageId); + d.put(SESSION_KEY_LOCAL_TIMESTAMP, i.localTimestamp); + d.put(SESSION_KEY_GROUP_ID, i.groupId); + d.put(SESSION_KEY_AUTHOR, clientHelper.toList(i.author)); + return d; + } + + @Override + public BdfDictionary encodeIntroduceeSession(IntroduceeSession s) { + BdfDictionary d = encodeSession(s); + d.put(SESSION_KEY_INTRODUCER, clientHelper.toList(s.getIntroducer())); + d.put(SESSION_KEY_LOCAL, encodeLocal(s.getLocal())); + d.put(SESSION_KEY_REMOTE, encodeRemote(s.getRemote())); + putNullable(d, SESSION_KEY_MASTER_KEY, s.getMasterKey()); + putNullable(d, SESSION_KEY_TRANSPORT_KEYS, + encodeTransportKeys(s.getTransportKeys())); + return d; + } + + private BdfDictionary encodeCommon(Common s) { + BdfDictionary d = new BdfDictionary(); + d.put(SESSION_KEY_ALICE, s.alice); + putNullable(d, SESSION_KEY_EPHEMERAL_PUBLIC_KEY, s.ephemeralPublicKey); + putNullable(d, SESSION_KEY_TRANSPORT_PROPERTIES, + s.transportProperties == null ? null : + clientHelper.toDictionary(s.transportProperties)); + d.put(SESSION_KEY_ACCEPT_TIMESTAMP, s.acceptTimestamp); + putNullable(d, SESSION_KEY_MAC_KEY, s.macKey); + return d; + } + + private BdfDictionary encodeLocal(Local s) { + BdfDictionary d = encodeCommon(s); + d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.lastMessageTimestamp); + putNullable(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID, s.lastMessageId); + putNullable(d, SESSION_KEY_EPHEMERAL_PRIVATE_KEY, + s.ephemeralPrivateKey); + return d; + } + + private BdfDictionary encodeRemote(Remote s) { + BdfDictionary d = encodeCommon(s); + d.put(SESSION_KEY_REMOTE_AUTHOR, clientHelper.toList(s.author)); + putNullable(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID, s.lastMessageId); + return d; + } + + private BdfDictionary encodeSession(Session s) { + BdfDictionary d = new BdfDictionary(); + d.put(SESSION_KEY_SESSION_ID, s.getSessionId()); + d.put(SESSION_KEY_ROLE, s.getRole().getValue()); + d.put(SESSION_KEY_STATE, s.getState().getValue()); + d.put(SESSION_KEY_REQUEST_TIMESTAMP, s.getRequestTimestamp()); + return d; + } + + @Nullable + private BdfDictionary encodeTransportKeys( + @Nullable Map keys) { + if (keys == null) return null; + BdfDictionary d = new BdfDictionary(); + for (Map.Entry e : keys.entrySet()) { + d.put(e.getKey().getString(), e.getValue().getInt()); + } + return d; + } + + private void putNullable(BdfDictionary d, String key, @Nullable Object o) { + d.put(key, o == null ? NULL_VALUE : o); + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java new file mode 100644 index 000000000..c58cac3d9 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParser.java @@ -0,0 +1,23 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.Role; + +@NotNullByDefault +interface SessionParser { + + BdfDictionary getSessionQuery(SessionId s); + + Role getRole(BdfDictionary d) throws FormatException; + + IntroducerSession parseIntroducerSession(BdfDictionary d) + throws FormatException; + + IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId, + BdfDictionary d) throws FormatException; + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java new file mode 100644 index 000000000..52c12e547 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/SessionParserImpl.java @@ -0,0 +1,199 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.BdfEntry; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.nullsafety.NotNullByDefault; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.api.introduction.Role; +import org.briarproject.briar.introduction.IntroduceeSession.Local; +import org.briarproject.briar.introduction.IntroduceeSession.Remote; +import org.briarproject.briar.introduction.IntroducerSession.Introducee; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; +import javax.inject.Inject; + +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; +import static org.briarproject.briar.api.introduction.Role.INTRODUCER; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ACCEPT_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ALICE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PRIVATE_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_EPHEMERAL_PUBLIC_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_GROUP_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCER; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LOCAL; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LOCAL_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_MAC_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_MASTER_KEY; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REMOTE_AUTHOR; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_REQUEST_TIMESTAMP; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ROLE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_STATE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_KEYS; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_TRANSPORT_PROPERTIES; + +@Immutable +@NotNullByDefault +class SessionParserImpl implements SessionParser { + + private final ClientHelper clientHelper; + + @Inject + SessionParserImpl(ClientHelper clientHelper) { + this.clientHelper = clientHelper; + } + + @Override + public BdfDictionary getSessionQuery(SessionId s) { + return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s)); + } + + @Override + public Role getRole(BdfDictionary d) throws FormatException { + return Role.fromValue(d.getLong(SESSION_KEY_ROLE).intValue()); + } + + @Override + public IntroducerSession parseIntroducerSession(BdfDictionary d) + throws FormatException { + if (getRole(d) != INTRODUCER) throw new IllegalArgumentException(); + SessionId sessionId = getSessionId(d); + IntroducerState state = IntroducerState.fromValue(getState(d)); + long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP); + Introducee introduceeA = parseIntroducee(sessionId, + d.getDictionary(SESSION_KEY_INTRODUCEE_A)); + Introducee introduceeB = parseIntroducee(sessionId, + d.getDictionary(SESSION_KEY_INTRODUCEE_B)); + return new IntroducerSession(sessionId, state, requestTimestamp, + introduceeA, introduceeB); + } + + private Introducee parseIntroducee(SessionId sessionId, BdfDictionary d) + throws FormatException { + MessageId lastLocalMessageId = + getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID); + MessageId lastRemoteMessageId = + getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID); + long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP); + GroupId groupId = getGroupId(d, SESSION_KEY_GROUP_ID); + Author author = getAuthor(d, SESSION_KEY_AUTHOR); + return new Introducee(sessionId, groupId, author, localTimestamp, + lastLocalMessageId, lastRemoteMessageId); + } + + @Override + public IntroduceeSession parseIntroduceeSession(GroupId introducerGroupId, + BdfDictionary d) throws FormatException { + if (getRole(d) != INTRODUCEE) throw new IllegalArgumentException(); + SessionId sessionId = getSessionId(d); + IntroduceeState state = IntroduceeState.fromValue(getState(d)); + long requestTimestamp = d.getLong(SESSION_KEY_REQUEST_TIMESTAMP); + Author introducer = getAuthor(d, SESSION_KEY_INTRODUCER); + Local local = parseLocal(d.getDictionary(SESSION_KEY_LOCAL)); + Remote remote = parseRemote(d.getDictionary(SESSION_KEY_REMOTE)); + byte[] masterKey = d.getOptionalRaw(SESSION_KEY_MASTER_KEY); + Map transportKeys = parseTransportKeys( + d.getOptionalDictionary(SESSION_KEY_TRANSPORT_KEYS)); + return new IntroduceeSession(sessionId, state, requestTimestamp, + introducerGroupId, introducer, local, remote, + masterKey, transportKeys); + } + + private Local parseLocal(BdfDictionary d) throws FormatException { + boolean alice = d.getBoolean(SESSION_KEY_ALICE); + MessageId lastLocalMessageId = + getMessageId(d, SESSION_KEY_LAST_LOCAL_MESSAGE_ID); + long localTimestamp = d.getLong(SESSION_KEY_LOCAL_TIMESTAMP); + byte[] ephemeralPublicKey = + d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY); + BdfDictionary tpDict = + d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES); + byte[] ephemeralPrivateKey = + d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PRIVATE_KEY); + Map transportProperties = + tpDict == null ? null : clientHelper + .parseAndValidateTransportPropertiesMap(tpDict); + long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP); + byte[] macKey = d.getOptionalRaw(SESSION_KEY_MAC_KEY); + return new Local(alice, lastLocalMessageId, localTimestamp, + ephemeralPublicKey, ephemeralPrivateKey, transportProperties, + acceptTimestamp, macKey); + } + + private Remote parseRemote(BdfDictionary d) throws FormatException { + boolean alice = d.getBoolean(SESSION_KEY_ALICE); + Author remoteAuthor = getAuthor(d, SESSION_KEY_REMOTE_AUTHOR); + MessageId lastRemoteMessageId = + getMessageId(d, SESSION_KEY_LAST_REMOTE_MESSAGE_ID); + byte[] ephemeralPublicKey = + d.getOptionalRaw(SESSION_KEY_EPHEMERAL_PUBLIC_KEY); + BdfDictionary tpDict = + d.getOptionalDictionary(SESSION_KEY_TRANSPORT_PROPERTIES); + Map transportProperties = + tpDict == null ? null : clientHelper + .parseAndValidateTransportPropertiesMap(tpDict); + long acceptTimestamp = d.getLong(SESSION_KEY_ACCEPT_TIMESTAMP); + byte[] macKey = d.getOptionalRaw(SESSION_KEY_MAC_KEY); + return new Remote(alice, remoteAuthor, lastRemoteMessageId, + ephemeralPublicKey, transportProperties, acceptTimestamp, + macKey); + } + + private int getState(BdfDictionary d) throws FormatException { + return d.getLong(SESSION_KEY_STATE).intValue(); + } + + private SessionId getSessionId(BdfDictionary d) throws FormatException { + byte[] b = d.getRaw(SESSION_KEY_SESSION_ID); + return new SessionId(b); + } + + @Nullable + private MessageId getMessageId(BdfDictionary d, String key) + throws FormatException { + byte[] b = d.getOptionalRaw(key); + return b == null ? null : new MessageId(b); + } + + private GroupId getGroupId(BdfDictionary d, String key) + throws FormatException { + return new GroupId(d.getRaw(key)); + } + + private Author getAuthor(BdfDictionary d, String key) + throws FormatException { + return clientHelper.parseAndValidateAuthor(d.getList(key)); + } + + @Nullable + private Map parseTransportKeys( + @Nullable BdfDictionary d) throws FormatException { + if (d == null) return null; + Map map = new HashMap<>(d.size()); + for (String key : d.keySet()) { + map.put(new TransportId(key), + new KeySetId(d.getLong(key).intValue()) + ); + } + return map; + } + +} diff --git a/briar-core/src/main/java/org/briarproject/briar/introduction/State.java b/briar-core/src/main/java/org/briarproject/briar/introduction/State.java new file mode 100644 index 000000000..3063f9bd8 --- /dev/null +++ b/briar-core/src/main/java/org/briarproject/briar/introduction/State.java @@ -0,0 +1,7 @@ +package org.briarproject.briar.introduction; + +interface State { + + int getValue(); + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/client/MessageQueueManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/client/MessageQueueManagerImplTest.java deleted file mode 100644 index 0a11b0fea..000000000 --- a/briar-core/src/test/java/org/briarproject/briar/client/MessageQueueManagerImplTest.java +++ /dev/null @@ -1,566 +0,0 @@ -package org.briarproject.briar.client; - -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.sync.ClientId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.InvalidMessageException; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageContext; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.sync.ValidationManager; -import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook; -import org.briarproject.bramble.api.sync.ValidationManager.MessageValidator; -import org.briarproject.bramble.test.CaptureArgumentAction; -import org.briarproject.bramble.test.TestUtils; -import org.briarproject.bramble.util.ByteUtils; -import org.briarproject.briar.api.client.MessageQueueManager.IncomingQueueMessageHook; -import org.briarproject.briar.api.client.MessageQueueManager.QueueMessageValidator; -import org.briarproject.briar.api.client.QueueMessage; -import org.briarproject.briar.api.client.QueueMessageFactory; -import org.briarproject.briar.test.BriarTestCase; -import org.hamcrest.Description; -import org.jmock.Expectations; -import org.jmock.Mockery; -import org.jmock.api.Action; -import org.jmock.api.Invocation; -import org.junit.Test; - -import java.util.concurrent.atomic.AtomicReference; - -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -import static org.briarproject.bramble.test.TestUtils.getClientId; -import static org.briarproject.bramble.test.TestUtils.getGroup; -import static org.briarproject.briar.api.client.MessageQueueManager.QUEUE_STATE_KEY; -import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; - -public class MessageQueueManagerImplTest extends BriarTestCase { - - private final ClientId clientId = getClientId(); - private final Group group = getGroup(clientId); - private final GroupId groupId = group.getId(); - private final long timestamp = System.currentTimeMillis(); - - @Test - public void testSendingMessages() throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - - Transaction txn = new Transaction(null, false); - byte[] body = new byte[123]; - Metadata groupMetadata = new Metadata(); - Metadata messageMetadata = new Metadata(); - Metadata groupMetadata1 = new Metadata(); - byte[] queueState = new byte[123]; - groupMetadata1.put(QUEUE_STATE_KEY, queueState); - - context.checking(new Expectations() {{ - // First message: queue state does not exist - oneOf(db).getGroupMetadata(txn, groupId); - will(returnValue(groupMetadata)); - oneOf(clientHelper).toByteArray(with(any(BdfDictionary.class))); - will(new EncodeQueueStateAction(1L, 0L, new BdfList())); - oneOf(db).mergeGroupMetadata(with(txn), with(groupId), - with(any(Metadata.class))); - oneOf(queueMessageFactory).createMessage(groupId, timestamp, 0L, - body); - will(new CreateMessageAction()); - oneOf(db).addLocalMessage(with(txn), with(any(QueueMessage.class)), - with(messageMetadata), with(true)); - // Second message: queue state exists - oneOf(db).getGroupMetadata(txn, groupId); - will(returnValue(groupMetadata1)); - oneOf(clientHelper).toDictionary(queueState, 0, queueState.length); - will(new DecodeQueueStateAction(1L, 0L, new BdfList())); - oneOf(clientHelper).toByteArray(with(any(BdfDictionary.class))); - will(new EncodeQueueStateAction(2L, 0L, new BdfList())); - oneOf(db).mergeGroupMetadata(with(txn), with(groupId), - with(any(Metadata.class))); - oneOf(queueMessageFactory).createMessage(groupId, timestamp, 1L, - body); - will(new CreateMessageAction()); - oneOf(db).addLocalMessage(with(txn), with(any(QueueMessage.class)), - with(messageMetadata), with(true)); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // First message - QueueMessage q = mqm.sendMessage(txn, group, timestamp, body, - messageMetadata); - assertEquals(groupId, q.getGroupId()); - assertEquals(timestamp, q.getTimestamp()); - assertEquals(0L, q.getQueuePosition()); - assertEquals(QUEUE_MESSAGE_HEADER_LENGTH + body.length, q.getLength()); - - // Second message - QueueMessage q1 = mqm.sendMessage(txn, group, timestamp, body, - messageMetadata); - assertEquals(groupId, q1.getGroupId()); - assertEquals(timestamp, q1.getTimestamp()); - assertEquals(1L, q1.getQueuePosition()); - assertEquals(QUEUE_MESSAGE_HEADER_LENGTH + body.length, q1.getLength()); - - context.assertIsSatisfied(); - } - - @Test - public void testValidatorRejectsShortMessage() throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - - AtomicReference captured = new AtomicReference<>(); - QueueMessageValidator queueMessageValidator = - context.mock(QueueMessageValidator.class); - // The message is too short to be a valid queue message - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH - 1]; - Message message = new Message(messageId, groupId, timestamp, raw); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerMessageValidator(with(clientId), - with(any(MessageValidator.class))); - will(new CaptureArgumentAction<>(captured, - MessageValidator.class, 1)); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating message validator - mqm.registerMessageValidator(clientId, queueMessageValidator); - MessageValidator delegate = captured.get(); - assertNotNull(delegate); - // The message should be invalid - try { - delegate.validateMessage(message, group); - fail(); - } catch (InvalidMessageException expected) { - // Expected - } - - context.assertIsSatisfied(); - } - - @Test - public void testValidatorRejectsNegativeQueuePosition() throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - - AtomicReference captured = new AtomicReference<>(); - QueueMessageValidator queueMessageValidator = - context.mock(QueueMessageValidator.class); - // The message has a negative queue position - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - for (int i = 0; i < 8; i++) - raw[MESSAGE_HEADER_LENGTH + i] = (byte) 0xFF; - Message message = new Message(messageId, groupId, timestamp, raw); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerMessageValidator(with(clientId), - with(any(MessageValidator.class))); - will(new CaptureArgumentAction<>(captured, - MessageValidator.class, 1)); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating message validator - mqm.registerMessageValidator(clientId, queueMessageValidator); - MessageValidator delegate = captured.get(); - assertNotNull(delegate); - // The message should be invalid - try { - delegate.validateMessage(message, group); - fail(); - } catch (InvalidMessageException expected) { - // Expected - } - - context.assertIsSatisfied(); - } - - @Test - public void testValidatorDelegatesValidMessage() throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - - AtomicReference captured = new AtomicReference<>(); - QueueMessageValidator queueMessageValidator = - context.mock(QueueMessageValidator.class); - Metadata metadata = new Metadata(); - MessageContext messageContext = - new MessageContext(metadata); - // The message is valid, with a queue position of zero - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - Message message = new Message(messageId, groupId, timestamp, raw); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerMessageValidator(with(clientId), - with(any(MessageValidator.class))); - will(new CaptureArgumentAction<>(captured, - MessageValidator.class, 1)); - // The message should be delegated - oneOf(queueMessageValidator).validateMessage( - with(any(QueueMessage.class)), with(group)); - will(returnValue(messageContext)); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating message validator - mqm.registerMessageValidator(clientId, queueMessageValidator); - MessageValidator delegate = captured.get(); - assertNotNull(delegate); - // The message should be valid and the metadata should be returned - assertSame(messageContext, delegate.validateMessage(message, group)); - assertSame(metadata, messageContext.getMetadata()); - - context.assertIsSatisfied(); - } - - @Test - public void testIncomingMessageHookDeletesDuplicateMessage() - throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - AtomicReference captured = new AtomicReference<>(); - IncomingQueueMessageHook incomingQueueMessageHook = - context.mock(IncomingQueueMessageHook.class); - - Transaction txn = new Transaction(null, false); - Metadata groupMetadata = new Metadata(); - byte[] queueState = new byte[123]; - groupMetadata.put(QUEUE_STATE_KEY, queueState); - // The message has queue position 0 - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - Message message = new Message(messageId, groupId, timestamp, raw); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerIncomingMessageHook(with(clientId), - with(any(IncomingMessageHook.class))); - will(new CaptureArgumentAction<>(captured, - IncomingMessageHook.class, 1)); - oneOf(db).getGroupMetadata(txn, groupId); - will(returnValue(groupMetadata)); - // Queue position 1 is expected - oneOf(clientHelper).toDictionary(queueState, 0, queueState.length); - will(new DecodeQueueStateAction(0L, 1L, new BdfList())); - // The message and its metadata should be deleted - oneOf(db).deleteMessage(txn, messageId); - oneOf(db).deleteMessageMetadata(txn, messageId); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating incoming message hook - mqm.registerIncomingMessageHook(clientId, incomingQueueMessageHook); - IncomingMessageHook delegate = captured.get(); - assertNotNull(delegate); - // Pass the message to the hook - delegate.incomingMessage(txn, message, new Metadata()); - - context.assertIsSatisfied(); - } - - @Test - public void testIncomingMessageHookAddsOutOfOrderMessageToPendingList() - throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - AtomicReference captured = new AtomicReference<>(); - IncomingQueueMessageHook incomingQueueMessageHook = - context.mock(IncomingQueueMessageHook.class); - - Transaction txn = new Transaction(null, false); - Metadata groupMetadata = new Metadata(); - byte[] queueState = new byte[123]; - groupMetadata.put(QUEUE_STATE_KEY, queueState); - // The message has queue position 1 - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - ByteUtils.writeUint64(1L, raw, MESSAGE_HEADER_LENGTH); - Message message = new Message(messageId, groupId, timestamp, raw); - BdfList pending = BdfList.of(BdfList.of(1L, messageId)); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerIncomingMessageHook(with(clientId), - with(any(IncomingMessageHook.class))); - will(new CaptureArgumentAction<>(captured, - IncomingMessageHook.class, 1)); - oneOf(db).getGroupMetadata(txn, groupId); - will(returnValue(groupMetadata)); - // Queue position 0 is expected - oneOf(clientHelper).toDictionary(queueState, 0, queueState.length); - will(new DecodeQueueStateAction(0L, 0L, new BdfList())); - // The message should be added to the pending list - oneOf(clientHelper).toByteArray(with(any(BdfDictionary.class))); - will(new EncodeQueueStateAction(0L, 0L, pending)); - oneOf(db).mergeGroupMetadata(with(txn), with(groupId), - with(any(Metadata.class))); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating incoming message hook - mqm.registerIncomingMessageHook(clientId, incomingQueueMessageHook); - IncomingMessageHook delegate = captured.get(); - assertNotNull(delegate); - // Pass the message to the hook - delegate.incomingMessage(txn, message, new Metadata()); - - context.assertIsSatisfied(); - } - - @Test - public void testIncomingMessageHookDelegatesInOrderMessage() - throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - AtomicReference captured = new AtomicReference<>(); - IncomingQueueMessageHook incomingQueueMessageHook = - context.mock(IncomingQueueMessageHook.class); - - Transaction txn = new Transaction(null, false); - Metadata groupMetadata = new Metadata(); - byte[] queueState = new byte[123]; - groupMetadata.put(QUEUE_STATE_KEY, queueState); - // The message has queue position 0 - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - Message message = new Message(messageId, groupId, timestamp, raw); - Metadata messageMetadata = new Metadata(); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerIncomingMessageHook(with(clientId), - with(any(IncomingMessageHook.class))); - will(new CaptureArgumentAction<>(captured, - IncomingMessageHook.class, 1)); - oneOf(db).getGroupMetadata(txn, groupId); - will(returnValue(groupMetadata)); - // Queue position 0 is expected - oneOf(clientHelper).toDictionary(queueState, 0, queueState.length); - will(new DecodeQueueStateAction(0L, 0L, new BdfList())); - // Queue position 1 should be expected next - oneOf(clientHelper).toByteArray(with(any(BdfDictionary.class))); - will(new EncodeQueueStateAction(0L, 1L, new BdfList())); - oneOf(db).mergeGroupMetadata(with(txn), with(groupId), - with(any(Metadata.class))); - // The message should be delegated - oneOf(incomingQueueMessageHook).incomingMessage(with(txn), - with(any(QueueMessage.class)), with(messageMetadata)); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating incoming message hook - mqm.registerIncomingMessageHook(clientId, incomingQueueMessageHook); - IncomingMessageHook delegate = captured.get(); - assertNotNull(delegate); - // Pass the message to the hook - delegate.incomingMessage(txn, message, messageMetadata); - - context.assertIsSatisfied(); - } - - @Test - public void testIncomingMessageHookRetrievesPendingMessage() - throws Exception { - Mockery context = new Mockery(); - DatabaseComponent db = context.mock(DatabaseComponent.class); - ClientHelper clientHelper = context.mock(ClientHelper.class); - QueueMessageFactory queueMessageFactory = - context.mock(QueueMessageFactory.class); - ValidationManager validationManager = - context.mock(ValidationManager.class); - AtomicReference captured = new AtomicReference<>(); - IncomingQueueMessageHook incomingQueueMessageHook = - context.mock(IncomingQueueMessageHook.class); - - Transaction txn = new Transaction(null, false); - Metadata groupMetadata = new Metadata(); - byte[] queueState = new byte[123]; - groupMetadata.put(QUEUE_STATE_KEY, queueState); - // The message has queue position 0 - MessageId messageId = new MessageId(TestUtils.getRandomId()); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - Message message = new Message(messageId, groupId, timestamp, raw); - Metadata messageMetadata = new Metadata(); - // Queue position 1 is pending - MessageId messageId1 = new MessageId(TestUtils.getRandomId()); - byte[] raw1 = new byte[QUEUE_MESSAGE_HEADER_LENGTH]; - QueueMessage message1 = new QueueMessage(messageId1, groupId, - timestamp, 1L, raw1); - Metadata messageMetadata1 = new Metadata(); - BdfList pending = BdfList.of(BdfList.of(1L, messageId1)); - - context.checking(new Expectations() {{ - oneOf(validationManager).registerIncomingMessageHook(with(clientId), - with(any(IncomingMessageHook.class))); - will(new CaptureArgumentAction<>(captured, - IncomingMessageHook.class, 1)); - oneOf(db).getGroupMetadata(txn, groupId); - will(returnValue(groupMetadata)); - // Queue position 0 is expected, position 1 is pending - oneOf(clientHelper).toDictionary(queueState, 0, queueState.length); - will(new DecodeQueueStateAction(0L, 0L, pending)); - // Queue position 2 should be expected next - oneOf(clientHelper).toByteArray(with(any(BdfDictionary.class))); - will(new EncodeQueueStateAction(0L, 2L, new BdfList())); - oneOf(db).mergeGroupMetadata(with(txn), with(groupId), - with(any(Metadata.class))); - // The new message should be delegated - oneOf(incomingQueueMessageHook).incomingMessage(with(txn), - with(any(QueueMessage.class)), with(messageMetadata)); - // The pending message should be retrieved - oneOf(db).getRawMessage(txn, messageId1); - will(returnValue(raw1)); - oneOf(db).getMessageMetadata(txn, messageId1); - will(returnValue(messageMetadata1)); - oneOf(queueMessageFactory).createMessage(messageId1, raw1); - will(returnValue(message1)); - // The pending message should be delegated - oneOf(incomingQueueMessageHook).incomingMessage(txn, message1, - messageMetadata1); - }}); - - MessageQueueManagerImpl mqm = new MessageQueueManagerImpl(db, - clientHelper, queueMessageFactory, validationManager); - - // Capture the delegating incoming message hook - mqm.registerIncomingMessageHook(clientId, incomingQueueMessageHook); - IncomingMessageHook delegate = captured.get(); - assertNotNull(delegate); - // Pass the message to the hook - delegate.incomingMessage(txn, message, messageMetadata); - - context.assertIsSatisfied(); - } - - private class EncodeQueueStateAction implements Action { - - private final long outgoingPosition, incomingPosition; - private final BdfList pending; - - private EncodeQueueStateAction(long outgoingPosition, - long incomingPosition, BdfList pending) { - this.outgoingPosition = outgoingPosition; - this.incomingPosition = incomingPosition; - this.pending = pending; - } - - @Override - public Object invoke(Invocation invocation) throws Throwable { - BdfDictionary d = (BdfDictionary) invocation.getParameter(0); - assertEquals(outgoingPosition, d.getLong("nextOut").longValue()); - assertEquals(incomingPosition, d.getLong("nextIn").longValue()); - assertEquals(pending, d.getList("pending")); - return new byte[123]; - } - - @Override - public void describeTo(Description description) { - description.appendText("encodes a queue state"); - } - } - - private class DecodeQueueStateAction implements Action { - - private final long outgoingPosition, incomingPosition; - private final BdfList pending; - - private DecodeQueueStateAction(long outgoingPosition, - long incomingPosition, BdfList pending) { - this.outgoingPosition = outgoingPosition; - this.incomingPosition = incomingPosition; - this.pending = pending; - } - - @Override - public Object invoke(Invocation invocation) throws Throwable { - BdfDictionary d = new BdfDictionary(); - d.put("nextOut", outgoingPosition); - d.put("nextIn", incomingPosition); - d.put("pending", pending); - return d; - } - - @Override - public void describeTo(Description description) { - description.appendText("decodes a queue state"); - } - } - - private class CreateMessageAction implements Action { - - @Override - public Object invoke(Invocation invocation) throws Throwable { - GroupId groupId = (GroupId) invocation.getParameter(0); - long timestamp = (Long) invocation.getParameter(1); - long queuePosition = (Long) invocation.getParameter(2); - byte[] body = (byte[]) invocation.getParameter(3); - byte[] raw = new byte[QUEUE_MESSAGE_HEADER_LENGTH + body.length]; - MessageId id = new MessageId(TestUtils.getRandomId()); - return new QueueMessage(id, groupId, timestamp, queuePosition, raw); - } - - @Override - public void describeTo(Description description) { - description.appendText("creates a message"); - } - } - -} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java deleted file mode 100644 index 5f8391d9b..000000000 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroduceeManagerTest.java +++ /dev/null @@ -1,424 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.Bytes; -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.contact.ContactManager; -import org.briarproject.bramble.api.crypto.CryptoComponent; -import org.briarproject.bramble.api.crypto.SecretKey; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfEntry; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.AuthorFactory; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.identity.IdentityManager; -import org.briarproject.bramble.api.properties.TransportPropertyManager; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.GroupId; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.api.introduction.IntroduceeProtocolState; -import org.briarproject.briar.test.BriarTestCase; -import org.jmock.Expectations; -import org.jmock.Mockery; -import org.jmock.lib.legacy.ClassImposteriser; -import org.junit.Test; - -import java.security.GeneralSecurityException; -import java.security.SecureRandom; - -import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -import static org.briarproject.bramble.test.TestUtils.getAuthor; -import static org.briarproject.bramble.test.TestUtils.getGroup; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; -import static org.briarproject.bramble.test.TestUtils.getRandomId; -import static org.briarproject.bramble.test.TestUtils.getSecretKey; -import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ADDED_CONTACT_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; -import static org.hamcrest.Matchers.array; -import static org.hamcrest.Matchers.samePropertyValuesAs; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; - -public class IntroduceeManagerTest extends BriarTestCase { - - private final Mockery context; - private final IntroduceeManager introduceeManager; - private final DatabaseComponent db; - private final CryptoComponent cryptoComponent; - private final ClientHelper clientHelper; - private final IntroductionGroupFactory introductionGroupFactory; - private final AuthorFactory authorFactory; - private final ContactManager contactManager; - private final Clock clock; - private final Contact introducer; - private final Contact introducee1; - private final Contact introducee2; - private final Group localGroup1; - private final Group introductionGroup1; - private final Transaction txn; - private final long time = 42L; - private final Message localStateMessage; - private final SessionId sessionId; - private final Message message1; - - public IntroduceeManagerTest() { - context = new Mockery(); - context.setImposteriser(ClassImposteriser.INSTANCE); - MessageSender messageSender = context.mock(MessageSender.class); - db = context.mock(DatabaseComponent.class); - cryptoComponent = context.mock(CryptoComponent.class); - clientHelper = context.mock(ClientHelper.class); - clock = context.mock(Clock.class); - introductionGroupFactory = - context.mock(IntroductionGroupFactory.class); - TransportPropertyManager transportPropertyManager = - context.mock(TransportPropertyManager.class); - authorFactory = context.mock(AuthorFactory.class); - contactManager = context.mock(ContactManager.class); - IdentityManager identityManager = context.mock(IdentityManager.class); - - introduceeManager = new IntroduceeManager(messageSender, db, - clientHelper, clock, cryptoComponent, transportPropertyManager, - authorFactory, contactManager, identityManager, - introductionGroupFactory); - - Author author0 = getAuthor(); - AuthorId localAuthorId = new AuthorId(getRandomId()); - ContactId contactId0 = new ContactId(234); - introducer = - new Contact(contactId0, author0, localAuthorId, true, true); - - Author author1 = getAuthor(); - AuthorId localAuthorId1 = new AuthorId(getRandomId()); - ContactId contactId1 = new ContactId(234); - introducee1 = - new Contact(contactId1, author1, localAuthorId1, true, true); - - Author author2 = getAuthor(); - ContactId contactId2 = new ContactId(235); - introducee2 = - new Contact(contactId2, author2, localAuthorId, true, true); - - localGroup1 = getGroup(CLIENT_ID); - introductionGroup1 = getGroup(CLIENT_ID); - - sessionId = new SessionId(getRandomId()); - localStateMessage = new Message( - new MessageId(getRandomId()), - localGroup1.getId(), - time, - getRandomBytes(MESSAGE_HEADER_LENGTH + 1) - ); - message1 = new Message( - new MessageId(getRandomId()), - introductionGroup1.getId(), - time, - getRandomBytes(MESSAGE_HEADER_LENGTH + 1) - ); - - txn = new Transaction(null, false); - } - - @Test - public void testIncomingRequestMessage() - throws DbException, FormatException { - - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_REQUEST); - msg.put(GROUP_ID, introductionGroup1.getId()); - msg.put(SESSION_ID, sessionId); - msg.put(MESSAGE_ID, message1.getId()); - msg.put(MESSAGE_TIME, time); - msg.put(NAME, introducee2.getAuthor().getName()); - msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); - - BdfDictionary state = - initializeSessionState(txn, introductionGroup1.getId(), msg); - - context.checking(new Expectations() {{ - oneOf(clientHelper).mergeMessageMetadata(txn, - localStateMessage.getId(), state); - }}); - - introduceeManager.incomingMessage(txn, state, msg); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - - @Test - public void testIncomingResponseMessage() - throws DbException, FormatException { - - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_RESPONSE); - msg.put(GROUP_ID, introductionGroup1.getId()); - msg.put(SESSION_ID, sessionId); - msg.put(MESSAGE_ID, message1.getId()); - msg.put(MESSAGE_TIME, time); - msg.put(NAME, introducee2.getAuthor().getName()); - msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); - - BdfDictionary state = - initializeSessionState(txn, introductionGroup1.getId(), msg); - state.put(STATE, IntroduceeProtocolState.AWAIT_RESPONSES.ordinal()); - - // turn request message into a response - msg.put(ACCEPT, true); - msg.put(TIME, time); - msg.put(E_PUBLIC_KEY, getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES)); - msg.put(TRANSPORT, new BdfDictionary()); - - context.checking(new Expectations() {{ - oneOf(clientHelper).mergeMessageMetadata(txn, - localStateMessage.getId(), state); - }}); - - introduceeManager.incomingMessage(txn, state, msg); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - - @Test - public void testDetectReplacedEphemeralPublicKey() - throws DbException, FormatException, GeneralSecurityException { - - // TODO MR !237 should use its new default initialization method here - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_RESPONSE); - msg.put(GROUP_ID, introductionGroup1.getId()); - msg.put(SESSION_ID, sessionId); - msg.put(MESSAGE_ID, message1.getId()); - msg.put(MESSAGE_TIME, time); - msg.put(NAME, introducee2.getAuthor().getName()); - msg.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); - BdfDictionary state = - initializeSessionState(txn, introductionGroup1.getId(), msg); - - // prepare state for incoming ACK - state.put(STATE, IntroduceeProtocolState.AWAIT_ACK.ordinal()); - state.put(ADDED_CONTACT_ID, 2); - byte[] nonce = getRandomBytes(42); - state.put(NONCE, nonce); - state.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); - - // create incoming ACK message - byte[] mac = getRandomBytes(MAC_LENGTH); - byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH); - BdfDictionary ack = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_ACK), - new BdfEntry(SESSION_ID, sessionId), - new BdfEntry(GROUP_ID, introductionGroup1.getId()), - new BdfEntry(MAC, mac), - new BdfEntry(SIGNATURE, sig) - ); - - context.checking(new Expectations() {{ - oneOf(cryptoComponent).verifySignature(sig, SIGNING_LABEL, nonce, - introducee2.getAuthor().getPublicKey()); - will(returnValue(false)); - }}); - - try { - introduceeManager.incomingMessage(txn, state, ack); - fail(); - } catch (DbException e) { - // expected - assertTrue(e.getCause() instanceof GeneralSecurityException); - } - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - - @Test - public void testSignatureVerification() - throws FormatException, DbException, GeneralSecurityException { - - byte[] publicKeyBytes = introducee2.getAuthor().getPublicKey(); - byte[] nonce = getRandomBytes(MAC_LENGTH); - byte[] sig = getRandomBytes(MAC_LENGTH); - - BdfDictionary state = new BdfDictionary(); - state.put(PUBLIC_KEY, publicKeyBytes); - state.put(NONCE, nonce); - state.put(SIGNATURE, sig); - - context.checking(new Expectations() {{ - oneOf(cryptoComponent).verifySignature(sig, SIGNING_LABEL, nonce, - publicKeyBytes); - will(returnValue(true)); - }}); - introduceeManager.verifySignature(state); - context.assertIsSatisfied(); - } - - @Test - public void testMacVerification() - throws FormatException, DbException, GeneralSecurityException { - - byte[] publicKeyBytes = introducee2.getAuthor().getPublicKey(); - BdfDictionary tp = BdfDictionary.of(new BdfEntry("fake", "fake")); - byte[] ePublicKeyBytes = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES); - byte[] mac = getRandomBytes(MAC_LENGTH); - SecretKey macKey = getSecretKey(); - - // move state to where it would be after an ACK arrived - BdfDictionary state = new BdfDictionary(); - state.put(PUBLIC_KEY, publicKeyBytes); - state.put(TRANSPORT, tp); - state.put(TIME, time); - state.put(E_PUBLIC_KEY, ePublicKeyBytes); - state.put(MAC, mac); - state.put(MAC_KEY, macKey.getBytes()); - - byte[] signBytes = getRandomBytes(42); - context.checking(new Expectations() {{ - oneOf(clientHelper).toByteArray( - BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time)); - will(returnValue(signBytes)); - //noinspection unchecked - oneOf(cryptoComponent).mac(with(MAC_LABEL), - with(samePropertyValuesAs(macKey)), - with(array(equal(signBytes)))); - will(returnValue(mac)); - }}); - introduceeManager.verifyMac(state); - context.assertIsSatisfied(); - - // now produce wrong MAC - context.checking(new Expectations() {{ - oneOf(clientHelper).toByteArray( - BdfList.of(publicKeyBytes, ePublicKeyBytes, tp, time)); - will(returnValue(signBytes)); - //noinspection unchecked - oneOf(cryptoComponent).mac(with(MAC_LABEL), - with(samePropertyValuesAs(macKey)), - with(array(equal(signBytes)))); - will(returnValue(getRandomBytes(MAC_LENGTH))); - }}); - try { - introduceeManager.verifyMac(state); - fail(); - } catch (GeneralSecurityException e) { - // expected - } - context.assertIsSatisfied(); - } - - private BdfDictionary initializeSessionState(Transaction txn, - GroupId groupId, BdfDictionary msg) - throws DbException, FormatException { - - SecureRandom secureRandom = context.mock(SecureRandom.class); - Bytes salt = new Bytes(new byte[64]); - BdfDictionary groupMetadata = BdfDictionary.of( - new BdfEntry(CONTACT, introducee1.getId().getInt()) - ); - boolean contactExists = false; - BdfDictionary state = new BdfDictionary(); - state.put(STORAGE_ID, localStateMessage.getId()); - state.put(STATE, AWAIT_REQUEST.getValue()); - state.put(ROLE, ROLE_INTRODUCEE); - state.put(GROUP_ID, groupId); - state.put(INTRODUCER, introducer.getAuthor().getName()); - state.put(CONTACT_ID_1, introducer.getId().getInt()); - state.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes()); - state.put(NOT_OUR_RESPONSE, localStateMessage.getId()); - state.put(ANSWERED, false); - state.put(EXISTS, contactExists); - state.put(REMOTE_AUTHOR_ID, introducee2.getAuthor().getId()); - state.put(REMOTE_AUTHOR_IS_US, false); - - context.checking(new Expectations() {{ - oneOf(clock).currentTimeMillis(); - will(returnValue(time)); - oneOf(cryptoComponent).getSecureRandom(); - will(returnValue(secureRandom)); - oneOf(secureRandom).nextBytes(salt.getBytes()); - oneOf(introductionGroupFactory).createLocalGroup(); - will(returnValue(localGroup1)); - oneOf(clientHelper) - .createMessage(localGroup1.getId(), time, BdfList.of(salt)); - will(returnValue(localStateMessage)); - - // who is making the introduction? who is the introducer? - oneOf(clientHelper).getGroupMetadataAsDictionary(txn, - groupId); - will(returnValue(groupMetadata)); - oneOf(db).getContact(txn, introducer.getId()); - will(returnValue(introducer)); - - // create remote author to check if contact exists - oneOf(authorFactory).createAuthor(introducee2.getAuthor().getName(), - introducee2.getAuthor().getPublicKey()); - will(returnValue(introducee2.getAuthor())); - oneOf(contactManager) - .contactExists(txn, introducee2.getAuthor().getId(), - introducer.getLocalAuthorId()); - will(returnValue(contactExists)); - - // checks if remote author is one of our identities - oneOf(db).containsLocalAuthor(txn, introducee2.getAuthor().getId()); - will(returnValue(false)); - - // store session state - oneOf(clientHelper) - .addLocalMessage(txn, localStateMessage, state, false); - }}); - - BdfDictionary result = introduceeManager.initialize(txn, groupId, msg); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - return result; - } - -} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroducerManagerTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroducerManagerTest.java deleted file mode 100644 index 558b26ec9..000000000 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroducerManagerTest.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.Bytes; -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.crypto.CryptoComponent; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.test.BriarTestCase; -import org.jmock.Expectations; -import org.jmock.Mockery; -import org.jmock.lib.legacy.ClassImposteriser; -import org.junit.Test; - -import java.security.SecureRandom; - -import static org.briarproject.bramble.test.TestUtils.getAuthor; -import static org.briarproject.bramble.test.TestUtils.getGroup; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; -import static org.briarproject.bramble.test.TestUtils.getRandomId; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES; -import static org.briarproject.briar.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.STORAGE_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; -import static org.junit.Assert.assertFalse; - -public class IntroducerManagerTest extends BriarTestCase { - - private final Mockery context; - private final IntroducerManager introducerManager; - private final CryptoComponent cryptoComponent; - private final ClientHelper clientHelper; - private final IntroductionGroupFactory introductionGroupFactory; - private final MessageSender messageSender; - private final Clock clock; - private final Contact introducee1; - private final Contact introducee2; - private final Group localGroup0; - private final Group introductionGroup1; - private final Group introductionGroup2; - - public IntroducerManagerTest() { - context = new Mockery(); - context.setImposteriser(ClassImposteriser.INSTANCE); - messageSender = context.mock(MessageSender.class); - cryptoComponent = context.mock(CryptoComponent.class); - clientHelper = context.mock(ClientHelper.class); - clock = context.mock(Clock.class); - introductionGroupFactory = - context.mock(IntroductionGroupFactory.class); - - introducerManager = - new IntroducerManager(messageSender, clientHelper, clock, - cryptoComponent, introductionGroupFactory); - - Author author1 = getAuthor(); - AuthorId localAuthorId1 = new AuthorId(getRandomId()); - ContactId contactId1 = new ContactId(234); - introducee1 = - new Contact(contactId1, author1, localAuthorId1, true, true); - - Author author2 = getAuthor(); - AuthorId localAuthorId2 = new AuthorId(getRandomId()); - ContactId contactId2 = new ContactId(235); - introducee2 = - new Contact(contactId2, author2, localAuthorId2, true, true); - - localGroup0 = getGroup(CLIENT_ID); - introductionGroup1 = getGroup(CLIENT_ID); - introductionGroup2 = getGroup(CLIENT_ID); - - context.assertIsSatisfied(); - } - - @Test - public void testMakeIntroduction() throws DbException, FormatException { - Transaction txn = new Transaction(null, false); - long time = 42L; - context.setImposteriser(ClassImposteriser.INSTANCE); - SecureRandom secureRandom = context.mock(SecureRandom.class); - Bytes salt = new Bytes(new byte[64]); - Message msg = new Message(new MessageId(getRandomId()), - localGroup0.getId(), time, getRandomBytes(64)); - BdfDictionary state = new BdfDictionary(); - state.put(SESSION_ID, msg.getId()); - state.put(STORAGE_ID, msg.getId()); - state.put(STATE, PREPARE_REQUESTS.getValue()); - state.put(ROLE, ROLE_INTRODUCER); - state.put(GROUP_ID_1, introductionGroup1.getId()); - state.put(GROUP_ID_2, introductionGroup2.getId()); - state.put(CONTACT_1, introducee1.getAuthor().getName()); - state.put(CONTACT_2, introducee2.getAuthor().getName()); - state.put(CONTACT_ID_1, introducee1.getId().getInt()); - state.put(CONTACT_ID_2, introducee2.getId().getInt()); - state.put(AUTHOR_ID_1, introducee1.getAuthor().getId()); - state.put(AUTHOR_ID_2, introducee2.getAuthor().getId()); - BdfDictionary state2 = (BdfDictionary) state.clone(); - state2.put(STATE, AWAIT_RESPONSES.getValue()); - - BdfDictionary msg1 = new BdfDictionary(); - msg1.put(TYPE, TYPE_REQUEST); - msg1.put(SESSION_ID, state.getRaw(SESSION_ID)); - msg1.put(GROUP_ID, state.getRaw(GROUP_ID_1)); - msg1.put(NAME, state.getString(CONTACT_2)); - msg1.put(PUBLIC_KEY, introducee2.getAuthor().getPublicKey()); - BdfDictionary msg1send = (BdfDictionary) msg1.clone(); - msg1send.put(MESSAGE_TIME, time); - - BdfDictionary msg2 = new BdfDictionary(); - msg2.put(TYPE, TYPE_REQUEST); - msg2.put(SESSION_ID, state.getRaw(SESSION_ID)); - msg2.put(GROUP_ID, state.getRaw(GROUP_ID_2)); - msg2.put(NAME, state.getString(CONTACT_1)); - msg2.put(PUBLIC_KEY, introducee1.getAuthor().getPublicKey()); - BdfDictionary msg2send = (BdfDictionary) msg2.clone(); - msg2send.put(MESSAGE_TIME, time); - - context.checking(new Expectations() {{ - // initialize and store session state - oneOf(clock).currentTimeMillis(); - will(returnValue(time)); - oneOf(cryptoComponent).getSecureRandom(); - will(returnValue(secureRandom)); - oneOf(secureRandom).nextBytes(salt.getBytes()); - oneOf(introductionGroupFactory).createLocalGroup(); - will(returnValue(localGroup0)); - oneOf(clientHelper).createMessage(localGroup0.getId(), time, - BdfList.of(salt)); - will(returnValue(msg)); - oneOf(introductionGroupFactory) - .createIntroductionGroup(introducee1); - will(returnValue(introductionGroup1)); - oneOf(introductionGroupFactory) - .createIntroductionGroup(introducee2); - will(returnValue(introductionGroup2)); - oneOf(clientHelper).addLocalMessage(txn, msg, state, false); - - // send message - oneOf(clientHelper).mergeMessageMetadata(txn, msg.getId(), state2); - oneOf(messageSender).sendMessage(txn, msg1send); - oneOf(messageSender).sendMessage(txn, msg2send); - }}); - - introducerManager - .makeIntroduction(txn, introducee1, introducee2, null, time); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - -} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java new file mode 100644 index 000000000..81c2ae01c --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoIntegrationTest.java @@ -0,0 +1,161 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorFactory; +import org.briarproject.bramble.api.identity.LocalAuthor; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.briar.api.client.SessionId; +import org.junit.Test; + +import java.util.Map; + +import javax.inject.Inject; + +import static org.briarproject.bramble.test.TestUtils.getSecretKey; +import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap; +import static org.briarproject.briar.introduction.IntroduceeSession.Local; +import static org.briarproject.briar.introduction.IntroduceeSession.Remote; +import static org.briarproject.briar.test.BriarTestUtils.getRealAuthor; +import static org.briarproject.briar.test.BriarTestUtils.getRealLocalAuthor; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class IntroductionCryptoIntegrationTest extends BrambleTestCase { + + @Inject + ClientHelper clientHelper; + @Inject + AuthorFactory authorFactory; + @Inject + CryptoComponent cryptoComponent; + + private final IntroductionCryptoImpl crypto; + + private final Author introducer; + private final LocalAuthor alice, bob; + private final long aliceAcceptTimestamp = 42L; + private final long bobAcceptTimestamp = 1337L; + private final SecretKey masterKey = getSecretKey(); + private final KeyPair aliceEphemeral, bobEphemeral; + private final Map aliceTransport = + getTransportPropertiesMap(3); + private final Map bobTransport = + getTransportPropertiesMap(3); + + public IntroductionCryptoIntegrationTest() { + IntroductionIntegrationTestComponent component = + DaggerIntroductionIntegrationTestComponent.builder().build(); + component.inject(this); + crypto = new IntroductionCryptoImpl(cryptoComponent, clientHelper); + + introducer = getRealAuthor(authorFactory); + LocalAuthor introducee1 = + getRealLocalAuthor(cryptoComponent, authorFactory); + LocalAuthor introducee2 = + getRealLocalAuthor(cryptoComponent, authorFactory); + boolean isAlice = + crypto.isAlice(introducee1.getId(), introducee2.getId()); + alice = isAlice ? introducee1 : introducee2; + bob = isAlice ? introducee2 : introducee1; + aliceEphemeral = crypto.generateKeyPair(); + bobEphemeral = crypto.generateKeyPair(); + } + + @Test + public void testGetSessionId() { + SessionId s1 = crypto.getSessionId(introducer, alice, bob); + SessionId s2 = crypto.getSessionId(introducer, bob, alice); + assertEquals(s1, s2); + + SessionId s3 = crypto.getSessionId(alice, bob, introducer); + assertNotEquals(s1, s3); + } + + @Test + public void testIsAlice() { + assertTrue(crypto.isAlice(alice.getId(), bob.getId())); + assertFalse(crypto.isAlice(bob.getId(), alice.getId())); + } + + @Test + public void testDeriveMasterKey() throws Exception { + SecretKey aliceMasterKey = + crypto.deriveMasterKey(aliceEphemeral.getPublic().getEncoded(), + aliceEphemeral.getPrivate().getEncoded(), + bobEphemeral.getPublic().getEncoded(), true); + SecretKey bobMasterKey = + crypto.deriveMasterKey(bobEphemeral.getPublic().getEncoded(), + bobEphemeral.getPrivate().getEncoded(), + aliceEphemeral.getPublic().getEncoded(), false); + assertArrayEquals(aliceMasterKey.getBytes(), bobMasterKey.getBytes()); + } + + @Test + public void testAliceAuthMac() throws Exception { + SecretKey aliceMacKey = crypto.deriveMacKey(masterKey, true); + Local local = new Local(true, null, -1, + aliceEphemeral.getPublic().getEncoded(), + aliceEphemeral.getPrivate().getEncoded(), aliceTransport, + aliceAcceptTimestamp, aliceMacKey.getBytes()); + Remote remote = new Remote(false, bob, null, + bobEphemeral.getPublic().getEncoded(), bobTransport, + bobAcceptTimestamp, null); + byte[] aliceMac = + crypto.authMac(aliceMacKey, introducer.getId(), alice.getId(), + local, remote); + + // verify from Bob's perspective + crypto.verifyAuthMac(aliceMac, aliceMacKey, introducer.getId(), + bob.getId(), remote, alice.getId(), local); + } + + @Test + public void testBobAuthMac() throws Exception { + SecretKey bobMacKey = crypto.deriveMacKey(masterKey, false); + Local local = new Local(false, null, -1, + bobEphemeral.getPublic().getEncoded(), + bobEphemeral.getPrivate().getEncoded(), bobTransport, + bobAcceptTimestamp, bobMacKey.getBytes()); + Remote remote = new Remote(true, alice, null, + aliceEphemeral.getPublic().getEncoded(), aliceTransport, + aliceAcceptTimestamp, null); + byte[] bobMac = + crypto.authMac(bobMacKey, introducer.getId(), bob.getId(), + local, remote); + + // verify from Alice's perspective + crypto.verifyAuthMac(bobMac, bobMacKey, introducer.getId(), + alice.getId(), remote, bob.getId(), local); + } + + @Test + public void testSign() throws Exception { + SecretKey macKey = crypto.deriveMacKey(masterKey, true); + byte[] signature = crypto.sign(macKey, alice.getPrivateKey()); + crypto.verifySignature(macKey, alice.getPublicKey(), signature); + } + + @Test + public void testAliceActivateMac() throws Exception { + SecretKey aliceMacKey = crypto.deriveMacKey(masterKey, true); + byte[] aliceMac = crypto.activateMac(aliceMacKey); + crypto.verifyActivateMac(aliceMac, aliceMacKey); + } + + @Test + public void testBobActivateMac() throws Exception { + SecretKey bobMacKey = crypto.deriveMacKey(masterKey, false); + byte[] bobMac = crypto.activateMac(bobMacKey); + crypto.verifyActivateMac(bobMac, bobMacKey); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java new file mode 100644 index 000000000..f966aa2cb --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionCryptoTest.java @@ -0,0 +1,45 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.briarproject.briar.api.client.SessionId; +import org.jmock.Expectations; +import org.junit.Test; + +import static org.briarproject.bramble.test.TestUtils.getAuthor; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.briar.api.introduction.IntroductionConstants.LABEL_SESSION_ID; +import static org.junit.Assert.assertEquals; + +public class IntroductionCryptoTest extends BrambleMockTestCase { + + private final CryptoComponent cryptoComponent = + context.mock(CryptoComponent.class); + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + + private final IntroductionCrypto crypto = + new IntroductionCryptoImpl(cryptoComponent, clientHelper); + + private final Author introducer = getAuthor(); + private final Author alice = getAuthor(), bob = getAuthor(); + private final byte[] hash = getRandomId(); + + @Test + public void testGetSessionId() { + boolean isAlice = crypto.isAlice(alice.getId(), bob.getId()); + context.checking(new Expectations() {{ + oneOf(cryptoComponent).hash( + LABEL_SESSION_ID, + introducer.getId().getBytes(), + isAlice ? alice.getId().getBytes() : bob.getId().getBytes(), + isAlice ? bob.getId().getBytes() : alice.getId().getBytes() + ); + will(returnValue(hash)); + }}); + SessionId sessionId = crypto.getSessionId(introducer, alice, bob); + assertEquals(new SessionId(hash), sessionId); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java index 9b485e5c4..df0d46b88 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTest.java @@ -6,25 +6,24 @@ import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.crypto.KeyPair; -import org.briarproject.bramble.api.crypto.SecretKey; import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; import org.briarproject.bramble.api.db.Transaction; import org.briarproject.bramble.api.event.Event; import org.briarproject.bramble.api.event.EventListener; +import org.briarproject.bramble.api.identity.Author; import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault; import org.briarproject.bramble.api.properties.TransportProperties; import org.briarproject.bramble.api.properties.TransportPropertyManager; import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.test.TestDatabaseModule; +import org.briarproject.briar.api.client.ProtocolStateException; import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.introduction.IntroductionManager; import org.briarproject.briar.api.introduction.IntroductionMessage; @@ -38,56 +37,43 @@ import org.junit.Before; import org.junit.Test; import java.io.IOException; -import java.security.GeneralSecurityException; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.List; import java.util.Map; -import java.util.Map.Entry; import java.util.concurrent.TimeoutException; import java.util.logging.Logger; -import javax.inject.Inject; - import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; import static org.briarproject.bramble.test.TestPluginConfigModule.TRANSPORT_ID; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getTransportId; -import static org.briarproject.bramble.util.StringUtils.getRandomString; -import static org.briarproject.briar.api.client.MessageQueueManager.QUEUE_STATE_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_MAC_KEY_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ALICE_NONCE_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NONCE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SHARED_SECRET_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNING_LABEL; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; +import static org.briarproject.bramble.test.TestUtils.getTransportProperties; +import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap; +import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_VERSION; +import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.A_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.B_DECLINED; +import static org.briarproject.briar.introduction.IntroducerState.START; +import static org.briarproject.briar.introduction.IntroductionConstants.MSG_KEY_MESSAGE_TYPE; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_AUTHOR; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_A; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_INTRODUCEE_B; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_SESSION_ID; +import static org.briarproject.briar.introduction.MessageType.ACCEPT; +import static org.briarproject.briar.introduction.MessageType.AUTH; +import static org.briarproject.briar.introduction.MessageType.DECLINE; import static org.briarproject.briar.test.BriarTestUtils.assertGroupCount; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; public class IntroductionIntegrationTest extends BriarIntegrationTest { - @Inject - IntroductionGroupFactory introductionGroupFactory; - // objects accessed from background threads need to be volatile private volatile IntroductionManager introductionManager0; private volatile IntroductionManager introductionManager1; @@ -102,7 +88,7 @@ public class IntroductionIntegrationTest Logger.getLogger(IntroductionIntegrationTest.class.getName()); interface StateVisitor { - boolean visit(BdfDictionary response); + AcceptMessage visit(AcceptMessage response); } @Before @@ -151,50 +137,61 @@ public class IntroductionIntegrationTest .makeIntroduction(introducee1, introducee2, "Hi!", time); // check that messages are tracked properly - Group g1 = introductionGroupFactory - .createIntroductionGroup(introducee1); - Group g2 = introductionGroupFactory - .createIntroductionGroup(introducee2); - assertGroupCount(messageTracker0, g1.getId(), 1, 0, time); - assertGroupCount(messageTracker0, g2.getId(), 1, 0, time); + Group g1 = introductionManager0.getContactGroup(introducee1); + Group g2 = introductionManager0.getContactGroup(introducee2); + assertGroupCount(messageTracker0, g1.getId(), 1, 0); + assertGroupCount(messageTracker0, g2.getId(), 1, 0); - // sync first request message + // sync first REQUEST message sync0To1(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); assertGroupCount(messageTracker1, g1.getId(), 2, 1); - // sync second request message + // sync second REQUEST message sync0To2(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener2.requestReceived); assertGroupCount(messageTracker2, g2.getId(), 2, 1); - // sync first response + // sync first ACCEPT message sync1To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response1Received); assertGroupCount(messageTracker0, g1.getId(), 2, 1); - // sync second response + // sync second ACCEPT message sync2To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response2Received); assertGroupCount(messageTracker0, g2.getId(), 2, 1); - // sync forwarded responses to introducees + // sync forwarded ACCEPT messages to introducees sync0To1(1, true); sync0To2(1, true); - assertGroupCount(messageTracker1, g1.getId(), 2, 1); - assertGroupCount(messageTracker2, g2.getId(), 2, 1); - // sync first ACK and its forward + // sync first AUTH and its forward sync1To0(1, true); sync0To2(1, true); - // sync second ACK and its forward - sync2To0(1, true); - sync0To1(1, true); + // assert that introducee2 did add the transport keys + IntroduceeSession session2 = getIntroduceeSession(c2); + assertNotNull(session2.getTransportKeys()); + assertFalse(session2.getTransportKeys().isEmpty()); + + // sync second AUTH and its forward as well as the following ACTIVATE + sync2To0(2, true); + sync0To1(2, true); + + // assert that introducee1 really purged the key material + IntroduceeSession session1 = getIntroduceeSession(c1); + assertNull(session1.getMasterKey()); + assertNull(session1.getLocal().ephemeralPrivateKey); + assertNull(session1.getTransportKeys()); + + // sync second ACTIVATE and its forward + sync1To0(1, true); + sync0To2(1, true); // wait for introduction to succeed eventWaiter.await(TIMEOUT, 2); @@ -245,16 +242,32 @@ public class IntroductionIntegrationTest assertTrue(listener1.requestReceived); assertTrue(listener2.requestReceived); + // assert that introducee is in correct state + IntroduceeSession introduceeSession = getIntroduceeSession(c1); + assertEquals(LOCAL_DECLINED, introduceeSession.getState()); + // sync first response sync1To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response1Received); + // assert that introducer is in correct state + boolean alice = c0.getIntroductionCrypto() + .isAlice(introducee1.getAuthor().getId(), + introducee2.getAuthor().getId()); + IntroducerSession introducerSession = getIntroducerSession(); + assertEquals(alice ? A_DECLINED : B_DECLINED, + introducerSession.getState()); + // sync second response sync2To0(1, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response2Received); + // assert that introducer now moved to START state + introducerSession = getIntroducerSession(); + assertEquals(START, introducerSession.getState()); + // sync first forwarded response sync0To2(1, true); @@ -269,10 +282,8 @@ public class IntroductionIntegrationTest assertFalse(contactManager2 .contactExists(author1.getId(), author2.getId())); - Group g1 = introductionGroupFactory - .createIntroductionGroup(introducee1); - Group g2 = introductionGroupFactory - .createIntroductionGroup(introducee2); + Group g1 = introductionManager0.getContactGroup(introducee1); + Group g2 = introductionManager0.getContactGroup(introducee2); assertEquals(2, introductionManager0.getIntroductionMessages(contactId1From0) .size()); @@ -290,6 +301,10 @@ public class IntroductionIntegrationTest introductionManager2.getIntroductionMessages(contactId0From2) .size()); assertGroupCount(messageTracker2, g2.getId(), 3, 2); + + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); } @Test @@ -342,6 +357,9 @@ public class IntroductionIntegrationTest assertEquals(2, introductionManager2.getIntroductionMessages(contactId0From2) .size()); + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); } @Test @@ -393,6 +411,9 @@ public class IntroductionIntegrationTest // since introducee2 was already in FINISHED state when // introducee1's response arrived, she ignores and deletes it assertDefaultUiMessages(); + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); } @Test @@ -425,13 +446,16 @@ public class IntroductionIntegrationTest // answer request manually introductionManager2 - .acceptIntroduction(contactId0From2, listener2.sessionId, time); + .respondToIntroduction(contactId0From2, listener2.sessionId, time, + true); // sync second response and ACK and make sure there is no abort sync2To0(2, true); eventWaiter.await(TIMEOUT, 1); assertTrue(listener0.response2Received); assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); } @Test @@ -452,61 +476,290 @@ public class IntroductionIntegrationTest // make really sure we don't have that request assertTrue(introductionManager1.getIntroductionMessages(contactId0From1) .isEmpty()); + + // The message was invalid, so no abort message was sent + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); + } + + @Test(expected = ProtocolStateException.class) + public void testDoubleIntroduction() throws Exception { + // we can make an introduction + assertTrue(introductionManager0 + .canIntroduce(contact1From0, contact2From0)); + + // make the introduction + long time = clock.currentTimeMillis(); + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); + + // no more introduction allowed while the existing one is in progress + assertFalse(introductionManager0 + .canIntroduce(contact1From0, contact2From0)); + + // try it anyway and fail + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); } @Test - public void testSessionIdReuse() throws Exception { + public void testIntroductionToExistingContact() throws Exception { + // let contact1 and contact2 add each other already + addContacts1And2(); + assertNotNull(contactId2From1); + assertNotNull(contactId1From2); + + // both will still accept the introduction addListeners(true, true); - // make introduction + // make the introduction long time = clock.currentTimeMillis(); introductionManager0 - .makeIntroduction(contact1From0, contact2From0, "Hi!", time); + .makeIntroduction(contact1From0, contact2From0, null, time); - // sync first request message + // sync REQUEST messages sync0To1(1, true); - eventWaiter.await(TIMEOUT, 1); - assertTrue(listener1.requestReceived); + sync0To2(1, true); - // get SessionId - List list = new ArrayList<>( - introductionManager1.getIntroductionMessages(contactId0From1)); - assertEquals(2, list.size()); - assertTrue(list.get(0) instanceof IntroductionRequest); - IntroductionRequest msg = (IntroductionRequest) list.get(0); - SessionId sessionId = msg.getSessionId(); + // assert that introducees get notified about the existing contact + IntroductionRequest ir1 = + getIntroductionRequest(introductionManager1, contactId0From1); + assertTrue(ir1.contactExists()); + IntroductionRequest ir2 = + getIntroductionRequest(introductionManager2, contactId0From2); + assertTrue(ir2.contactExists()); - // get contact group - Group group = - introductionGroupFactory.createIntroductionGroup(contact1From0); + // sync ACCEPT messages back to introducer + sync1To0(1, true); + sync2To0(1, true); - // create new message with same SessionId - BdfDictionary d = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_REQUEST), - new BdfEntry(SESSION_ID, sessionId), - new BdfEntry(GROUP_ID, group.getId()), - new BdfEntry(NAME, getRandomString(42)), - new BdfEntry(PUBLIC_KEY, getRandomBytes(MAX_PUBLIC_KEY_LENGTH)) - ); + // sync forwarded ACCEPT messages to introducees + sync0To1(1, true); + sync0To2(1, true); - // reset request received state - listener1.requestReceived = false; + // sync first AUTH and its forward + sync1To0(1, true); + sync0To2(1, true); - // add the message to the queue - MessageSender sender0 = c0.getMessageSender(); - Transaction txn = db0.startTransaction(false); - try { - sender0.sendMessage(txn, d); - db0.commitTransaction(txn); - } finally { - db0.endTransaction(txn); - } + // sync second AUTH and its forward as well as the following ACTIVATE + sync2To0(2, true); + sync0To1(2, true); - // actually send message - sync0To1(1, false); + // sync second ACTIVATE and its forward + sync1To0(1, true); + sync0To2(1, true); - // make sure it does not arrive - assertFalse(listener1.requestReceived); + // assert that no session was aborted and no success event was broadcast + assertFalse(listener1.succeeded); + assertFalse(listener2.succeeded); + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); + } + + @Test + public void testIntroductionToRemovedContact() throws Exception { + // let contact1 and contact2 add each other + addContacts1And2(); + assertNotNull(contactId2From1); + assertNotNull(contactId1From2); + + // only introducee1 removes introducee2 + contactManager1.removeContact(contactId2From1); + + // both will accept the introduction + addListeners(true, true); + + // make the introduction + long time = clock.currentTimeMillis(); + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); + + // sync REQUEST messages + sync0To1(1, true); + sync0To2(1, true); + + // sync ACCEPT messages back to introducer + sync1To0(1, true); + sync2To0(1, true); + + // sync forwarded ACCEPT messages to introducees + sync0To1(1, true); + sync0To2(1, true); + + // sync first AUTH and its forward + sync1To0(1, true); + sync0To2(1, true); + + // sync second AUTH and its forward as well as the following ACTIVATE + sync2To0(2, true); + sync0To1(2, true); + + // sync second ACTIVATE and its forward + sync1To0(1, true); + sync0To2(1, true); + + // Introduction only succeeded for introducee1 + assertTrue(listener1.succeeded); + assertFalse(listener2.succeeded); + + // assert that no session was aborted + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertFalse(listener2.aborted); + } + + /** + * One introducee illegally sends two ACCEPT messages in a row. + * The introducer should notice this and ABORT the session. + */ + @Test + public void testDoubleAccept() throws Exception { + addListeners(true, true); + + // make the introduction + long time = clock.currentTimeMillis(); + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); + + // sync REQUEST to introducee1 + sync0To1(1, true); + + // save ACCEPT from introducee1 + AcceptMessage m = (AcceptMessage) getMessageFor(c1.getClientHelper(), + contact0From1, ACCEPT); + + // sync ACCEPT back to introducer + sync1To0(1, true); + + // fake a second ACCEPT message from introducee1 + Message msg = c1.getMessageEncoder() + .encodeAcceptMessage(m.getGroupId(), m.getTimestamp() + 1, + m.getMessageId(), m.getSessionId(), + m.getEphemeralPublicKey(), m.getAcceptTimestamp(), + m.getTransportProperties()); + c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true); + + // sync fake ACCEPT back to introducer + sync1To0(1, true); + + assertTrue(listener0.aborted); + } + + /** + * One introducee sends an ACCEPT and then another DECLINE message. + * The introducer should notice this and ABORT the session. + */ + @Test + public void testAcceptAndDecline() throws Exception { + addListeners(true, true); + + // make the introduction + long time = clock.currentTimeMillis(); + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); + + // sync REQUEST to introducee1 + sync0To1(1, true); + + // save ACCEPT from introducee1 + AcceptMessage m = (AcceptMessage) getMessageFor(c1.getClientHelper(), + contact0From1, ACCEPT); + + // sync ACCEPT back to introducer + sync1To0(1, true); + + // fake a second DECLINE message also from introducee1 + Message msg = c1.getMessageEncoder() + .encodeDeclineMessage(m.getGroupId(), m.getTimestamp() + 1, + m.getMessageId(), m.getSessionId()); + c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true); + + // sync fake DECLINE back to introducer + sync1To0(1, true); + + assertTrue(listener0.aborted); + } + + /** + * One introducee sends an DECLINE and then another DECLINE message. + * The introducer should notice this and ABORT the session. + */ + @Test + public void testDoubleDecline() throws Exception { + addListeners(false, true); + + // make the introduction + long time = clock.currentTimeMillis(); + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); + + // sync REQUEST to introducee1 + sync0To1(1, true); + + // save DECLINE from introducee1 + DeclineMessage m = (DeclineMessage) getMessageFor(c1.getClientHelper(), + contact0From1, DECLINE); + + // sync DECLINE back to introducer + sync1To0(1, true); + + // fake a second DECLINE message also from introducee1 + Message msg = c1.getMessageEncoder() + .encodeDeclineMessage(m.getGroupId(), m.getTimestamp() + 1, + m.getMessageId(), m.getSessionId()); + c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true); + + // sync fake DECLINE back to introducer + sync1To0(1, true); + + assertTrue(listener0.aborted); + } + + /** + * One introducee sends two AUTH messages. + * The introducer should notice this and ABORT the session. + */ + @Test + public void testDoubleAuth() throws Exception { + addListeners(true, true); + + // make the introduction + long time = clock.currentTimeMillis(); + introductionManager0 + .makeIntroduction(contact1From0, contact2From0, null, time); + + // sync REQUEST messages + sync0To1(1, true); + sync0To2(1, true); + + // sync ACCEPT messages + sync1To0(1, true); + sync2To0(1, true); + + // sync forwarded ACCEPT messages to introducees + sync0To1(1, true); + sync0To2(1, true); + + // save AUTH from introducee1 + AuthMessage m = (AuthMessage) getMessageFor(c1.getClientHelper(), + contact0From1, AUTH); + + // sync first AUTH message + sync1To0(1, true); + + // fake a second AUTH message also from introducee1 + Message msg = c1.getMessageEncoder() + .encodeAuthMessage(m.getGroupId(), m.getTimestamp() + 1, + m.getMessageId(), m.getSessionId(), m.getMac(), + m.getSignature()); + c1.getClientHelper().addLocalMessage(msg, new BdfDictionary(), true); + + // sync second AUTH message + sync1To0(1, true); + + assertTrue(listener0.aborted); } @Test @@ -523,34 +776,19 @@ public class IntroductionIntegrationTest eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); - // get database and local group for introducee - Group group1 = introductionGroupFactory.createLocalGroup(); + // get local group for introducee1 + Group group1 = getLocalGroup(); - // get local session state messages - Map map; - Transaction txn = db1.startTransaction(false); - try { - map = db1.getMessageMetadata(txn, group1.getId()); - db1.commitTransaction(txn); - } finally { - db1.endTransaction(txn); - } // check that we have one session state - assertEquals(1, map.size()); + assertEquals(1, c1.getClientHelper() + .getMessageMetadataAsDictionary(group1.getId()).size()); // introducee1 removes introducer contactManager1.removeContact(contactId0From1); - // get local session state messages again - txn = db1.startTransaction(false); - try { - map = db1.getMessageMetadata(txn, group1.getId()); - db1.commitTransaction(txn); - } finally { - db1.endTransaction(txn); - } // make sure local state got deleted - assertEquals(0, map.size()); + assertEquals(0, c1.getClientHelper() + .getMessageMetadataAsDictionary(group1.getId()).size()); } @Test @@ -567,48 +805,35 @@ public class IntroductionIntegrationTest eventWaiter.await(TIMEOUT, 1); assertTrue(listener1.requestReceived); - // get database and local group for introducee - Group group1 = introductionGroupFactory.createLocalGroup(); + // get local group for introducer + Group group0 = getLocalGroup(); - // get local session state messages - Map map; - Transaction txn = db0.startTransaction(false); - try { - map = db0.getMessageMetadata(txn, group1.getId()); - db0.commitTransaction(txn); - } finally { - db0.endTransaction(txn); - } // check that we have one session state - assertEquals(1, map.size()); + assertEquals(1, c0.getClientHelper() + .getMessageMetadataAsDictionary(group0.getId()).size()); // introducer removes introducee1 contactManager0.removeContact(contactId1From0); - // get local session state messages again - txn = db0.startTransaction(false); - try { - map = db0.getMessageMetadata(txn, group1.getId()); - db0.commitTransaction(txn); - } finally { - db0.endTransaction(txn); - } // make sure local state is still there - assertEquals(1, map.size()); + assertEquals(1, c0.getClientHelper() + .getMessageMetadataAsDictionary(group0.getId()).size()); + + // ensure introducer has aborted the session + assertTrue(listener0.aborted); + + // sync REQUEST and ABORT message + sync0To2(2, true); + + // ensure introducee2 has aborted the session as well + assertTrue(listener2.aborted); // introducer removes other introducee contactManager0.removeContact(contactId2From0); - // get local session state messages again - txn = db0.startTransaction(false); - try { - map = db0.getMessageMetadata(txn, group1.getId()); - db0.commitTransaction(txn); - } finally { - db0.endTransaction(txn); - } // make sure local state is gone now - assertEquals(0, map.size()); + assertEquals(0, c0.getClientHelper() + .getMessageMetadataAsDictionary(group0.getId()).size()); } private void testModifiedResponse(StateVisitor visitor) @@ -630,26 +855,35 @@ public class IntroductionIntegrationTest eventWaiter.await(TIMEOUT, 1); // get response to be forwarded - ClientHelper ch = c0.getClientHelper(); // need 0's ClientHelper here - Entry resp = - getMessageFor(ch, contact2From0, TYPE_RESPONSE); - MessageId responseId = resp.getKey(); - BdfDictionary response = resp.getValue(); - - // adapt outgoing message queue to removed message - Group g2 = introductionGroupFactory - .createIntroductionGroup(contact2From0); - decreaseOutgoingMessageCounter(ch, g2.getId()); + AcceptMessage message = + (AcceptMessage) getMessageFor(c0.getClientHelper(), + contact2From0, ACCEPT); // allow visitor to modify response - boolean earlyAbort = visitor.visit(response); + AcceptMessage m = visitor.visit(message); // replace original response with modified one - MessageSender sender0 = c0.getMessageSender(); Transaction txn = db0.startTransaction(false); try { - db0.deleteMessage(txn, responseId); - sender0.sendMessage(txn, response); + db0.removeMessage(txn, message.getMessageId()); + Message msg = c0.getMessageEncoder() + .encodeAcceptMessage(m.getGroupId(), m.getTimestamp(), + m.getPreviousMessageId(), m.getSessionId(), + m.getEphemeralPublicKey(), m.getAcceptTimestamp(), + m.getTransportProperties()); + c0.getClientHelper() + .addLocalMessage(txn, msg, new BdfDictionary(), true); + Group group0 = getLocalGroup(); + BdfDictionary query = BdfDictionary.of( + new BdfEntry(SESSION_KEY_SESSION_ID, m.getSessionId()) + ); + Map.Entry session = c0.getClientHelper() + .getMessageMetadataAsDictionary(txn, group0.getId(), query) + .entrySet().iterator().next(); + replacePreviousLocalMessageId(contact2From0.getAuthor(), + session.getValue(), msg.getId()); + c0.getClientHelper().mergeMessageMetadata(txn, session.getKey(), + session.getValue()); db0.commitTransaction(txn); } finally { db0.endTransaction(txn); @@ -663,21 +897,14 @@ public class IntroductionIntegrationTest sync0To1(1, true); sync0To2(1, true); - // sync first ACK and forward it + // sync first AUTH and forward it sync1To0(1, true); sync0To2(1, true); // introducee2 should have detected the fake now - // and deleted introducee1 again - Collection contacts2; - txn = db2.startTransaction(true); - try { - contacts2 = db2.getContacts(txn); - db2.commitTransaction(txn); - } finally { - db2.endTransaction(txn); - } - assertEquals(1, contacts2.size()); + assertFalse(listener0.aborted); + assertFalse(listener1.aborted); + assertTrue(listener2.aborted); // sync introducee2's ack and following abort sync2To0(2, true); @@ -687,144 +914,44 @@ public class IntroductionIntegrationTest // sync abort messages to introducees sync0To1(2, true); - sync0To2(1, true); - if (earlyAbort) { - assertTrue(listener1.aborted); - assertTrue(listener2.aborted); - } else { - assertTrue(listener2.aborted); - // when aborted late, introducee1 keeps the contact, - // so introducer can not make contacts disappear by aborting - Collection contacts1; - txn = db1.startTransaction(true); - try { - contacts1 = db1.getContacts(txn); - db1.commitTransaction(txn); - } finally { - db1.endTransaction(txn); - } - assertEquals(2, contacts1.size()); - } + // ensure everybody got the abort now + assertTrue(listener0.aborted); + assertTrue(listener1.aborted); + assertTrue(listener2.aborted); } @Test public void testModifiedTransportProperties() throws Exception { - testModifiedResponse(response -> { - BdfDictionary tp = response.getDictionary(TRANSPORT, null); - tp.put("fakeId", BdfDictionary.of(new BdfEntry("fake", "fake"))); - response.put(TRANSPORT, tp); - return false; - }); + testModifiedResponse( + m -> new AcceptMessage(m.getMessageId(), m.getGroupId(), + m.getTimestamp(), m.getPreviousMessageId(), + m.getSessionId(), m.getEphemeralPublicKey(), + m.getAcceptTimestamp(), + getTransportPropertiesMap(2)) + ); } @Test public void testModifiedTimestamp() throws Exception { - testModifiedResponse(response -> { - long timestamp = response.getLong(TIME, 0L); - response.put(TIME, timestamp + 1); - return false; - }); + testModifiedResponse( + m -> new AcceptMessage(m.getMessageId(), m.getGroupId(), + m.getTimestamp(), m.getPreviousMessageId(), + m.getSessionId(), m.getEphemeralPublicKey(), + clock.currentTimeMillis(), + m.getTransportProperties()) + ); } @Test public void testModifiedEphemeralPublicKey() throws Exception { - testModifiedResponse(response -> { - KeyPair keyPair = crypto.generateAgreementKeyPair(); - response.put(E_PUBLIC_KEY, keyPair.getPublic().getEncoded()); - return true; - }); - } - - @Test - public void testModifiedEphemeralPublicKeyWithFakeMac() - throws Exception { - // initialize a real introducee manager - MessageSender messageSender = c2.getMessageSender(); - TransportPropertyManager tpManager = c2.getTransportPropertyManager(); - IntroduceeManager manager2 = - new IntroduceeManager(messageSender, db2, clientHelper, clock, - crypto, tpManager, authorFactory, contactManager2, - identityManager2, introductionGroupFactory); - - // create keys - KeyPair keyPair1 = crypto.generateSignatureKeyPair(); - KeyPair eKeyPair1 = crypto.generateAgreementKeyPair(); - KeyPair eKeyPair2 = crypto.generateAgreementKeyPair(); - - // Nonce 1 - byte[][] inputs = { - new byte[] {CLIENT_VERSION}, - eKeyPair1.getPublic().getEncoded(), - eKeyPair2.getPublic().getEncoded() - }; - SecretKey sharedSecret = crypto.deriveSharedSecret(SHARED_SECRET_LABEL, - eKeyPair2.getPublic(), eKeyPair1, inputs); - byte[] nonce1 = crypto.mac(ALICE_NONCE_LABEL, sharedSecret); - - // Signature 1 - byte[] sig1 = crypto.sign(SIGNING_LABEL, nonce1, - keyPair1.getPrivate().getEncoded()); - - // MAC 1 - SecretKey macKey1 = crypto.deriveKey(ALICE_MAC_KEY_LABEL, sharedSecret); - BdfDictionary tp1 = BdfDictionary.of(new BdfEntry("fake", "fake")); - long time1 = clock.currentTimeMillis(); - BdfList toMacList = BdfList.of(keyPair1.getPublic().getEncoded(), - eKeyPair1.getPublic().getEncoded(), tp1, time1); - byte[] toMac = clientHelper.toByteArray(toMacList); - byte[] mac1 = crypto.mac(MAC_LABEL, macKey1, toMac); - - // create only relevant part of state for introducee2 - BdfDictionary state = new BdfDictionary(); - state.put(PUBLIC_KEY, keyPair1.getPublic().getEncoded()); - state.put(TRANSPORT, tp1); - state.put(TIME, time1); - state.put(E_PUBLIC_KEY, eKeyPair1.getPublic().getEncoded()); - state.put(MAC, mac1); - state.put(MAC_KEY, macKey1.getBytes()); - state.put(NONCE, nonce1); - state.put(SIGNATURE, sig1); - - // MAC and signature verification should pass - manager2.verifyMac(state); - manager2.verifySignature(state); - - // replace ephemeral key pair and recalculate matching keys and nonce - KeyPair eKeyPair1f = crypto.generateAgreementKeyPair(); - byte[][] fakeInputs = { - new byte[] {CLIENT_VERSION}, - eKeyPair1f.getPublic().getEncoded(), - eKeyPair2.getPublic().getEncoded() - }; - sharedSecret = crypto.deriveSharedSecret(SHARED_SECRET_LABEL, - eKeyPair2.getPublic(), eKeyPair1f, fakeInputs); - nonce1 = crypto.mac(ALICE_NONCE_LABEL, sharedSecret); - - // recalculate MAC - macKey1 = crypto.deriveKey(ALICE_MAC_KEY_LABEL, sharedSecret); - toMacList = BdfList.of(keyPair1.getPublic().getEncoded(), - eKeyPair1f.getPublic().getEncoded(), tp1, time1); - toMac = clientHelper.toByteArray(toMacList); - mac1 = crypto.mac(MAC_LABEL, macKey1, toMac); - - // update state with faked information - state.put(E_PUBLIC_KEY, eKeyPair1f.getPublic().getEncoded()); - state.put(MAC, mac1); - state.put(MAC_KEY, macKey1.getBytes()); - state.put(NONCE, nonce1); - - // MAC verification should still pass - manager2.verifyMac(state); - - // Signature can not be verified, because we don't have private - // long-term key to fake it - try { - manager2.verifySignature(state); - fail(); - } catch (GeneralSecurityException e) { - // expected - } + testModifiedResponse( + m -> new AcceptMessage(m.getMessageId(), m.getGroupId(), + m.getTimestamp(), m.getPreviousMessageId(), + m.getSessionId(), + getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + m.getAcceptTimestamp(), m.getTransportProperties()) + ); } private void addTransportProperties() @@ -832,17 +959,15 @@ public class IntroductionIntegrationTest TransportPropertyManager tpm0 = c0.getTransportPropertyManager(); TransportPropertyManager tpm1 = c1.getTransportPropertyManager(); TransportPropertyManager tpm2 = c2.getTransportPropertyManager(); - TransportProperties tp = new TransportProperties( - Collections.singletonMap("key", "value")); - tpm0.mergeLocalProperties(TRANSPORT_ID, tp); + tpm0.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2)); sync0To1(1, true); sync0To2(1, true); - tpm1.mergeLocalProperties(TRANSPORT_ID, tp); + tpm1.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2)); sync1To0(1, true); - tpm2.mergeLocalProperties(TRANSPORT_ID, tp); + tpm2.mergeLocalProperties(TRANSPORT_ID, getTransportProperties(2)); sync2To0(1, true); } @@ -915,27 +1040,15 @@ public class IntroductionIntegrationTest long time = clock.currentTimeMillis(); try { if (introducee == 1 && answerRequests) { - if (accept) { - introductionManager1 - .acceptIntroduction(contactId, sessionId, - time); - } else { - introductionManager1 - .declineIntroduction(contactId, sessionId, - time); - } + introductionManager1 + .respondToIntroduction(contactId, sessionId, + time, accept); } else if (introducee == 2 && answerRequests) { - if (accept) { - introductionManager2 - .acceptIntroduction(contactId, sessionId, - time); - } else { - introductionManager2 - .declineIntroduction(contactId, sessionId, - time); - } + introductionManager2 + .respondToIntroduction(contactId, sessionId, + time, accept); } - } catch (DbException | FormatException exception) { + } catch (DbException exception) { eventWaiter.rethrow(exception); } finally { eventWaiter.resume(); @@ -945,7 +1058,6 @@ public class IntroductionIntegrationTest Contact contact = ((IntroductionSucceededEvent) e).getContact(); eventWaiter .assertFalse(contact.getId().equals(contactId0From1)); - eventWaiter.assertTrue(contact.isActive()); eventWaiter.resume(); } else if (e instanceof IntroductionAbortedEvent) { aborted = true; @@ -981,30 +1093,87 @@ public class IntroductionIntegrationTest } - private void decreaseOutgoingMessageCounter(ClientHelper ch, GroupId g) - throws FormatException, DbException { - BdfDictionary gD = ch.getGroupMetadataAsDictionary(g); - LOG.warning(gD.toString()); - BdfDictionary queue = gD.getDictionary(QUEUE_STATE_KEY); - queue.put("nextOut", queue.getLong("nextOut") - 1); - gD.put(QUEUE_STATE_KEY, queue); - ch.mergeGroupMetadata(g, gD); + private void replacePreviousLocalMessageId(Author author, + BdfDictionary d, MessageId id) throws FormatException { + BdfDictionary i1 = d.getDictionary(SESSION_KEY_INTRODUCEE_A); + BdfDictionary i2 = d.getDictionary(SESSION_KEY_INTRODUCEE_B); + Author a1 = clientHelper + .parseAndValidateAuthor(i1.getList(SESSION_KEY_AUTHOR)); + Author a2 = clientHelper + .parseAndValidateAuthor(i2.getList(SESSION_KEY_AUTHOR)); + + if (a1.equals(author)) { + i1.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id); + d.put(SESSION_KEY_INTRODUCEE_A, i1); + } else if (a2.equals(author)) { + i2.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, id); + d.put(SESSION_KEY_INTRODUCEE_B, i2); + } else { + throw new AssertionError(); + } } - private Entry getMessageFor(ClientHelper ch, - Contact contact, long type) throws FormatException, DbException { - Entry response = null; - Group g = introductionGroupFactory - .createIntroductionGroup(contact); + private AbstractIntroductionMessage getMessageFor(ClientHelper ch, + Contact contact, MessageType type) + throws FormatException, DbException { + Group g = introductionManager0.getContactGroup(contact); + BdfDictionary query = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue()) + ); Map map = - ch.getMessageMetadataAsDictionary(g.getId()); - for (Entry entry : map.entrySet()) { - if (entry.getValue().getLong(TYPE) == type) { - response = entry; + ch.getMessageMetadataAsDictionary(g.getId(), query); + assertEquals(1, map.size()); + MessageId id = map.entrySet().iterator().next().getKey(); + Message m = ch.getMessage(id); + BdfList body = ch.getMessageAsList(id); + if (type == ACCEPT) { + //noinspection ConstantConditions + return c0.getMessageParser().parseAcceptMessage(m, body); + } else if (type == DECLINE) { + //noinspection ConstantConditions + return c0.getMessageParser().parseDeclineMessage(m, body); + } else if (type == AUTH) { + //noinspection ConstantConditions + return c0.getMessageParser().parseAuthMessage(m, body); + } else throw new AssertionError("Not implemented"); + } + + private IntroductionRequest getIntroductionRequest( + IntroductionManager manager, ContactId contactId) + throws DbException { + for (IntroductionMessage im : manager + .getIntroductionMessages(contactId)) { + if (im instanceof IntroductionRequest) { + return (IntroductionRequest) im; } } - assertTrue(response != null); - return response; + throw new AssertionError("No IntroductionRequest found"); + } + + private IntroducerSession getIntroducerSession() + throws DbException, FormatException { + Map dicts = c0.getClientHelper() + .getMessageMetadataAsDictionary(getLocalGroup().getId()); + assertEquals(1, dicts.size()); + BdfDictionary d = dicts.values().iterator().next(); + return c0.getSessionParser().parseIntroducerSession(d); + } + + private IntroduceeSession getIntroduceeSession( + IntroductionIntegrationTestComponent c) + throws DbException, FormatException { + Map dicts = c.getClientHelper() + .getMessageMetadataAsDictionary(getLocalGroup().getId()); + assertEquals(1, dicts.size()); + BdfDictionary d = dicts.values().iterator().next(); + Group introducerGroup = + introductionManager2.getContactGroup(contact0From2); + return c.getSessionParser() + .parseIntroduceeSession(introducerGroup.getId(), d); + } + + private Group getLocalGroup() { + return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION); } } diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java index bc0ea6241..3a90d7d14 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionIntegrationTestComponent.java @@ -59,6 +59,13 @@ interface IntroductionIntegrationTestComponent void inject(IntroductionIntegrationTest init); - MessageSender getMessageSender(); + void inject(MessageEncoderParserIntegrationTest init); + void inject(SessionEncoderParserIntegrationTest init); + void inject(IntroductionCryptoIntegrationTest init); + + MessageEncoder getMessageEncoder(); + MessageParser getMessageParser(); + SessionParser getSessionParser(); + IntroductionCrypto getIntroductionCrypto(); } diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionManagerImplTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionManagerImplTest.java deleted file mode 100644 index e44591020..000000000 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionManagerImplTest.java +++ /dev/null @@ -1,291 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.contact.Contact; -import org.briarproject.bramble.api.contact.ContactId; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfEntry; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataParser; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.identity.AuthorId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.Message; -import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.sync.MessageStatus; -import org.briarproject.briar.api.client.MessageTracker; -import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.test.BriarTestCase; -import org.jmock.Expectations; -import org.jmock.Mockery; -import org.jmock.lib.legacy.ClassImposteriser; -import org.junit.Test; - -import java.util.Collection; -import java.util.Collections; -import java.util.Map; - -import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH; -import static org.briarproject.bramble.test.TestUtils.getAuthor; -import static org.briarproject.bramble.test.TestUtils.getGroup; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; -import static org.briarproject.bramble.test.TestUtils.getRandomId; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; -import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; -import static org.junit.Assert.assertFalse; - -public class IntroductionManagerImplTest extends BriarTestCase { - - private final Mockery context; - private final IntroductionManagerImpl introductionManager; - private final IntroducerManager introducerManager; - private final IntroduceeManager introduceeManager; - private final DatabaseComponent db; - private final ClientHelper clientHelper; - private final MessageTracker messageTracker; - private final IntroductionGroupFactory introductionGroupFactory; - private final SessionId sessionId = new SessionId(getRandomId()); - private final MessageId storageId = new MessageId(sessionId.getBytes()); - private final long time = 42L; - private final Contact introducee1; - private final Contact introducee2; - private final Group introductionGroup1; - private final Group introductionGroup2; - private final Message message1; - private Transaction txn; - - public IntroductionManagerImplTest() { - Author author1 = getAuthor(); - AuthorId localAuthorId1 = new AuthorId(getRandomId()); - ContactId contactId1 = new ContactId(234); - introducee1 = - new Contact(contactId1, author1, localAuthorId1, true, true); - - Author author2 = getAuthor(); - AuthorId localAuthorId2 = new AuthorId(getRandomId()); - ContactId contactId2 = new ContactId(235); - introducee2 = - new Contact(contactId2, author2, localAuthorId2, true, true); - - introductionGroup1 = getGroup(CLIENT_ID); - introductionGroup2 = getGroup(CLIENT_ID); - - message1 = new Message( - new MessageId(getRandomId()), - introductionGroup1.getId(), - time, - getRandomBytes(MESSAGE_HEADER_LENGTH + 1) - ); - - // mock ALL THE THINGS!!! - context = new Mockery(); - context.setImposteriser(ClassImposteriser.INSTANCE); - introducerManager = context.mock(IntroducerManager.class); - introduceeManager = context.mock(IntroduceeManager.class); - db = context.mock(DatabaseComponent.class); - clientHelper = context.mock(ClientHelper.class); - MetadataParser metadataParser = context.mock(MetadataParser.class); - messageTracker = context.mock(MessageTracker.class); - introductionGroupFactory = context.mock(IntroductionGroupFactory.class); - - introductionManager = new IntroductionManagerImpl(db, clientHelper, - metadataParser, messageTracker, introducerManager, - introduceeManager, introductionGroupFactory); - } - - @Test - public void testMakeIntroduction() throws DbException, FormatException { - txn = new Transaction(null, false); - - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); - oneOf(introducerManager) - .makeIntroduction(txn, introducee1, introducee2, null, - time); - // get both introduction groups - oneOf(introductionGroupFactory) - .createIntroductionGroup(introducee1); - will(returnValue(introductionGroup1)); - oneOf(introductionGroupFactory) - .createIntroductionGroup(introducee2); - will(returnValue(introductionGroup2)); - // track message for group 1 - oneOf(messageTracker).trackMessage(txn, - introductionGroup1.getId(), time, true); - // track message for group 2 - oneOf(messageTracker).trackMessage(txn, - introductionGroup2.getId(), time, true); - oneOf(db).commitTransaction(txn); - oneOf(db).endTransaction(txn); - }}); - - introductionManager - .makeIntroduction(introducee1, introducee2, null, time); - - context.assertIsSatisfied(); - } - - @Test - public void testAcceptIntroduction() throws DbException, FormatException { - BdfDictionary state = BdfDictionary.of( - new BdfEntry(GROUP_ID_1, introductionGroup1.getId()), - new BdfEntry(GROUP_ID_2, introductionGroup2.getId()) - ); - txn = new Transaction(null, false); - - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); - oneOf(db).getContact(txn, introducee1.getId()); - will(returnValue(introducee1)); - oneOf(introductionGroupFactory).createIntroductionGroup(introducee1); - will(returnValue(introductionGroup1)); - oneOf(clientHelper).getMessageMetadataAsDictionary(txn, storageId); - will(returnValue(state)); - oneOf(introduceeManager).acceptIntroduction(txn, state, time); - // track message - oneOf(messageTracker).trackMessage(txn, - introductionGroup1.getId(), time, true); - oneOf(db).commitTransaction(txn); - oneOf(db).endTransaction(txn); - }}); - - introductionManager - .acceptIntroduction(introducee1.getId(), sessionId, time); - - context.assertIsSatisfied(); - } - - @Test - public void testDeclineIntroduction() throws DbException, FormatException { - BdfDictionary state = BdfDictionary.of( - new BdfEntry(GROUP_ID_1, introductionGroup1.getId()), - new BdfEntry(GROUP_ID_2, introductionGroup2.getId()) - ); - txn = new Transaction(null, false); - - context.checking(new Expectations() {{ - oneOf(db).startTransaction(false); - will(returnValue(txn)); - oneOf(db).getContact(txn, introducee1.getId()); - will(returnValue(introducee1)); - oneOf(introductionGroupFactory).createIntroductionGroup(introducee1); - will(returnValue(introductionGroup1)); - oneOf(clientHelper).getMessageMetadataAsDictionary(txn, storageId); - will(returnValue(state)); - oneOf(introduceeManager).declineIntroduction(txn, state, time); - // track message - oneOf(messageTracker).trackMessage(txn, - introductionGroup1.getId(), time, true); - oneOf(db).commitTransaction(txn); - oneOf(db).endTransaction(txn); - }}); - - introductionManager - .declineIntroduction(introducee1.getId(), sessionId, time); - - context.assertIsSatisfied(); - } - - @Test - public void testGetIntroductionMessages() - throws DbException, FormatException { - - Map metadata = Collections.emptyMap(); - Collection statuses = Collections.emptyList(); - txn = new Transaction(null, false); - - context.checking(new Expectations() {{ - oneOf(db).startTransaction(true); - will(returnValue(txn)); - oneOf(db).getContact(txn, introducee1.getId()); - will(returnValue(introducee1)); - oneOf(introductionGroupFactory).createIntroductionGroup(introducee1); - will(returnValue(introductionGroup1)); - oneOf(clientHelper).getMessageMetadataAsDictionary(txn, - introductionGroup1.getId()); - will(returnValue(metadata)); - oneOf(db).getMessageStatus(txn, introducee1.getId(), - introductionGroup1.getId()); - will(returnValue(statuses)); - oneOf(db).commitTransaction(txn); - oneOf(db).endTransaction(txn); - }}); - - introductionManager.getIntroductionMessages(introducee1.getId()); - - context.assertIsSatisfied(); - } - - @Test - public void testIncomingRequestMessage() - throws DbException, FormatException { - - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_REQUEST); - - BdfDictionary state = new BdfDictionary(); - txn = new Transaction(null, false); - - context.checking(new Expectations() {{ - oneOf(introduceeManager) - .initialize(txn, introductionGroup1.getId(), msg); - will(returnValue(state)); - oneOf(introduceeManager) - .incomingMessage(txn, state, msg); - // track message - oneOf(messageTracker).trackIncomingMessage(txn, message1); - }}); - - introductionManager - .incomingMessage(txn, message1, new BdfList(), msg); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - - @Test - public void testIncomingResponseMessage() - throws DbException, FormatException { - - BdfDictionary msg = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_RESPONSE), - new BdfEntry(SESSION_ID, sessionId) - ); - - BdfDictionary state = new BdfDictionary(); - state.put(ROLE, ROLE_INTRODUCER); - state.put(GROUP_ID_1, introductionGroup1.getId()); - state.put(GROUP_ID_2, introductionGroup2.getId()); - - txn = new Transaction(null, false); - - context.checking(new Expectations() {{ - oneOf(clientHelper).getMessageMetadataAsDictionary(txn, storageId); - will(returnValue(state)); - oneOf(introducerManager).incomingMessage(txn, state, msg); - // track message - oneOf(messageTracker).trackIncomingMessage(txn, message1); - }}); - - introductionManager - .incomingMessage(txn, message1, new BdfList(), msg); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - - -} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java index a2d481547..4f52aa4de 100644 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/IntroductionValidatorTest.java @@ -1,361 +1,463 @@ package org.briarproject.briar.introduction; import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.client.BdfMessageContext; import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataEncoder; -import org.briarproject.bramble.api.identity.Author; -import org.briarproject.bramble.api.plugin.TransportId; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.MessageId; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.bramble.system.SystemClock; +import org.briarproject.bramble.test.ValidatorTestCase; import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.test.BriarTestCase; -import org.jmock.Mockery; +import org.jmock.Expectations; import org.junit.Test; -import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; +import javax.annotation.Nullable; + +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES; import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH; -import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH; -import static org.briarproject.bramble.test.TestUtils.getAuthor; -import static org.briarproject.bramble.test.TestUtils.getClientId; -import static org.briarproject.bramble.test.TestUtils.getGroup; import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.util.StringUtils.getRandomString; -import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; -import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE; -import static org.junit.Assert.assertArrayEquals; +import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH; +import static org.briarproject.briar.introduction.MessageType.ABORT; +import static org.briarproject.briar.introduction.MessageType.ACCEPT; +import static org.briarproject.briar.introduction.MessageType.ACTIVATE; +import static org.briarproject.briar.introduction.MessageType.AUTH; +import static org.briarproject.briar.introduction.MessageType.DECLINE; +import static org.briarproject.briar.introduction.MessageType.REQUEST; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -public class IntroductionValidatorTest extends BriarTestCase { +public class IntroductionValidatorTest extends ValidatorTestCase { - private final Mockery context = new Mockery(); - private final Group group; - private final Message message; - private final IntroductionValidator validator; - private final Clock clock = new SystemClock(); + private final MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + private final IntroductionValidator validator = + new IntroductionValidator(messageEncoder, clientHelper, + metadataEncoder, clock); - public IntroductionValidatorTest() { - group = getGroup(getClientId()); - MessageId messageId = new MessageId(getRandomId()); - long timestamp = System.currentTimeMillis(); - byte[] raw = getRandomBytes(123); - message = new Message(messageId, group.getId(), timestamp, raw); - - - ClientHelper clientHelper = context.mock(ClientHelper.class); - MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class); - validator = new IntroductionValidator(clientHelper, metadataEncoder, - clock); - context.assertIsSatisfied(); - } + private final SessionId sessionId = new SessionId(getRandomId()); + private final MessageId previousMsgId = new MessageId(getRandomId()); + private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH); + private final BdfDictionary meta = new BdfDictionary(); + private final long acceptTimestamp = 42; + private final BdfDictionary transportProperties = BdfDictionary.of( + new BdfEntry("transportId", new BdfDictionary()) + ); + private final byte[] mac = getRandomBytes(MAC_BYTES); + private final byte[] signature = getRandomBytes(MAX_SIGNATURE_BYTES); // - // Introduction Requests + // Introduction REQUEST // @Test - public void testValidateProperIntroductionRequest() throws Exception { - byte[] sessionId = getRandomId(); - String name = getRandomString(MAX_AUTHOR_NAME_LENGTH); - byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH); - String text = getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH); + public void testAcceptsRequest() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), previousMsgId.getBytes(), + authorList, text); - BdfList body = BdfList.of(TYPE_REQUEST, sessionId, - name, publicKey, text); + expectParseAuthor(authorList, author); + expectEncodeRequestMetadata(); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); - BdfDictionary result = - validator.validateMessage(message, group, body).getDictionary(); - - assertEquals(Long.valueOf(TYPE_REQUEST), result.getLong(TYPE)); - assertEquals(sessionId, result.getRaw(SESSION_ID)); - assertEquals(name, result.getString(NAME)); - assertEquals(publicKey, result.getRaw(PUBLIC_KEY)); - assertEquals(text, result.getString(MSG)); - context.assertIsSatisfied(); - } - - @Test(expected = FormatException.class) - public void testValidateIntroductionRequestWithNoName() throws Exception { - BdfDictionary msg = getValidIntroductionRequest(); - - // no NAME is message - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getRaw(PUBLIC_KEY)); - if (msg.containsKey(MSG)) body.add(msg.getString(MSG)); - - validator.validateMessage(message, group, body); - } - - @Test(expected = FormatException.class) - public void testValidateIntroductionRequestWithLongName() throws Exception { - // too long NAME in message - BdfDictionary msg = getValidIntroductionRequest(); - msg.put(NAME, msg.get(NAME) + "x"); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getString(NAME), msg.getRaw(PUBLIC_KEY)); - if (msg.containsKey(MSG)) body.add(msg.getString(MSG)); - - validator.validateMessage(message, group, body); - } - - @Test(expected = FormatException.class) - public void testValidateIntroductionRequestWithWrongType() - throws Exception { - // wrong message type - BdfDictionary msg = getValidIntroductionRequest(); - msg.put(TYPE, 324234); - - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getString(NAME), msg.getRaw(PUBLIC_KEY)); - if (msg.containsKey(MSG)) body.add(msg.getString(MSG)); - validator.validateMessage(message, group, body); - } - - private BdfDictionary getValidIntroductionRequest() throws Exception { - byte[] sessionId = getRandomId(); - Author author = getAuthor(); - String text = getRandomString(MAX_MESSAGE_BODY_LENGTH); - - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_REQUEST); - msg.put(SESSION_ID, sessionId); - msg.put(NAME, author.getName()); - msg.put(PUBLIC_KEY, author.getPublicKey()); - msg.put(MSG, text); - - return msg; - } - - // - // Introduction Responses - // - - @Test - public void testValidateIntroductionAcceptResponse() throws Exception { - byte[] groupId = getRandomId(); - byte[] sessionId = getRandomId(); - long time = clock.currentTimeMillis(); - byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES); - String transportId = - getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH); - BdfDictionary tProps = BdfDictionary.of( - new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH), - getRandomString(MAX_PROPERTY_LENGTH)) - ); - BdfDictionary tp = BdfDictionary.of( - new BdfEntry(transportId, tProps) - ); - - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_RESPONSE); - msg.put(GROUP_ID, groupId); - msg.put(SESSION_ID, sessionId); - msg.put(ACCEPT, true); - msg.put(TIME, time); - msg.put(E_PUBLIC_KEY, publicKey); - msg.put(TRANSPORT, tp); - - BdfList body = BdfList.of(TYPE_RESPONSE, msg.getRaw(SESSION_ID), - msg.getBoolean(ACCEPT), msg.getLong(TIME), - msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT)); - - BdfDictionary result = - validator.validateMessage(message, group, body).getDictionary(); - - assertEquals(Long.valueOf(TYPE_RESPONSE), result.getLong(TYPE)); - assertEquals(sessionId, result.getRaw(SESSION_ID)); - assertEquals(true, result.getBoolean(ACCEPT)); - assertEquals(publicKey, result.getRaw(E_PUBLIC_KEY)); - assertEquals(tp, result.getDictionary(TRANSPORT)); - context.assertIsSatisfied(); + assertExpectedContext(messageContext, previousMsgId); } @Test - public void testValidateIntroductionDeclineResponse() throws Exception { - BdfDictionary msg = getValidIntroductionResponse(false); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getBoolean(ACCEPT)); + public void testAcceptsRequestWithPreviousMsgIdNull() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, text); - BdfDictionary result = validator.validateMessage(message, group, body) - .getDictionary(); + expectParseAuthor(authorList, author); + expectEncodeRequestMetadata(); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); - assertFalse(result.getBoolean(ACCEPT)); - context.assertIsSatisfied(); + assertExpectedContext(messageContext, null); + } + + @Test + public void testAcceptsRequestWithMessageNull() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, null); + + expectParseAuthor(authorList, author); + expectEncodeRequestMetadata(); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, null); } @Test(expected = FormatException.class) - public void testValidateIntroductionResponseWithoutAccept() - throws Exception { - BdfDictionary msg = getValidIntroductionResponse(false); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID)); - + public void testRejectsTooShortBodyForRequest() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), null, authorList); validator.validateMessage(message, group, body); } @Test(expected = FormatException.class) - public void testValidateIntroductionResponseWithBrokenTp() - throws Exception { - BdfDictionary msg = getValidIntroductionResponse(true); - BdfDictionary tp = msg.getDictionary(TRANSPORT); - tp.put( - getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH), "X"); - msg.put(TRANSPORT, tp); - - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getBoolean(ACCEPT), msg.getLong(TIME), - msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT)); - + public void testRejectsTooLongBodyForRequest() throws Exception { + BdfList body = + BdfList.of(REQUEST.getValue(), null, authorList, text, null); validator.validateMessage(message, group, body); } @Test(expected = FormatException.class) - public void testValidateIntroductionResponseWithoutPublicKey() - throws Exception { - BdfDictionary msg = getValidIntroductionResponse(true); - - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getBoolean(ACCEPT), msg.getLong(TIME), - msg.getDictionary(TRANSPORT)); - + public void testRejectsRawMessageForRequest() throws Exception { + BdfList body = + BdfList.of(REQUEST.getValue(), null, authorList, getRandomId()); + expectParseAuthor(authorList, author); validator.validateMessage(message, group, body); } - private BdfDictionary getValidIntroductionResponse(boolean accept) + @Test(expected = FormatException.class) + public void testRejectsStringMessageIdForRequest() throws Exception { + BdfList body = + BdfList.of(REQUEST.getValue(), "NoMessageId", authorList, null); + validator.validateMessage(message, group, body); + } + + // + // Introduction ACCEPT + // + + @Test + public void testAcceptsAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + acceptTimestamp, transportProperties); + context.checking(new Expectations() {{ + oneOf(clientHelper).parseAndValidateTransportPropertiesMap( + transportProperties); + }}); + expectEncodeMetadata(ACCEPT); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, previousMsgId); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), + getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongBodyForAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + acceptTimestamp, transportProperties, null); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidSessionIdForAccept() throws Exception { + BdfList body = + BdfList.of(ACCEPT.getValue(), null, previousMsgId.getBytes(), + getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp, + transportProperties); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidPreviousMsgIdForAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), 1, + getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp, + transportProperties); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongPublicKeyForAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), + getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp, + transportProperties); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsNegativeTimestampForAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH), + -1, transportProperties); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsEmptyTransportPropertiesForAccept() throws Exception { + BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), + getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp, + new BdfDictionary()); + validator.validateMessage(message, group, body); + } - byte[] groupId = getRandomId(); - byte[] sessionId = getRandomId(); - long time = clock.currentTimeMillis(); - byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES); - String transportId = - getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH); - BdfDictionary tProps = BdfDictionary.of( - new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH), - getRandomString(MAX_PROPERTY_LENGTH)) - ); - BdfDictionary tp = BdfDictionary.of( - new BdfEntry(transportId, tProps) - ); + // + // Introduction DECLINE + // - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_RESPONSE); - msg.put(GROUP_ID, groupId); - msg.put(SESSION_ID, sessionId); - msg.put(ACCEPT, accept); - if (accept) { - msg.put(TIME, time); - msg.put(E_PUBLIC_KEY, publicKey); - msg.put(TRANSPORT, tp); + @Test + public void testAcceptsDecline() throws Exception { + BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes()); + + expectEncodeMetadata(DECLINE); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, previousMsgId); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForDecline() throws Exception { + BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes()); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongBodyForDecline() throws Exception { + BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), null); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidSessionIdForDecline() throws Exception { + BdfList body = + BdfList.of(DECLINE.getValue(), null, previousMsgId.getBytes()); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidPreviousMsgIdForDecline() throws Exception { + BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(), 1); + validator.validateMessage(message, group, body); + } + + // + // Introduction AUTH + // + + @Test + public void testAcceptsAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, signature); + + expectEncodeMetadata(AUTH); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, previousMsgId); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongBodyForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, signature, null); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidPreviousMsgIdForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + 1, getRandomBytes(MAC_BYTES), + signature); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsPreviousMsgIdNullForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), null, + getRandomBytes(MAC_BYTES), signature); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortMacForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1), + signature); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongMacForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), + getRandomBytes(MAC_BYTES + 1), signature); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidMacForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), null, signature); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortSignatureForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, getRandomBytes(0)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongSignatureForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, + getRandomBytes(MAX_SIGNATURE_BYTES + 1)); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidSignatureForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, null); + validator.validateMessage(message, group, body); + } + + // + // Introduction ACTIVATE + // + + @Test + public void testAcceptsActivate() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac); + + expectEncodeMetadata(ACTIVATE); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, previousMsgId); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForActivate() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes()); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongBodyForActivate() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, null); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidSessionIdForActivate() throws Exception { + BdfList body = + BdfList.of(ACTIVATE.getValue(), null, previousMsgId.getBytes(), + mac); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidPreviousMsgIdForActivate() throws Exception { + BdfList body = + BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), 1, mac); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsPreviousMsgIdNullForActivate() throws Exception { + BdfList body = + BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), null, + mac); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidMacForActivate() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1)); + validator.validateMessage(message, group, body); + } + + // + // Introduction ABORT + // + + @Test + public void testAcceptsAbort() throws Exception { + BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes()); + + expectEncodeMetadata(ABORT); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, previousMsgId); + } + + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForAbort() throws Exception { + BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes()); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsTooLongBodyForAbort() throws Exception { + BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), null); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidSessionIdForAbort() throws Exception { + BdfList body = + BdfList.of(ABORT.getValue(), null, previousMsgId.getBytes()); + validator.validateMessage(message, group, body); + } + + @Test(expected = FormatException.class) + public void testRejectsInvalidPreviousMsgIdForAbort() throws Exception { + BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), 1); + validator.validateMessage(message, group, body); + } + + // + // Introduction Helper Methods + // + + private void expectEncodeRequestMetadata() { + context.checking(new Expectations() {{ + oneOf(messageEncoder).encodeRequestMetadata(message.getTimestamp()); + will(returnValue(meta)); + }}); + } + + private void expectEncodeMetadata(MessageType type) { + context.checking(new Expectations() {{ + oneOf(messageEncoder) + .encodeMetadata(type, sessionId, message.getTimestamp(), + false, false, false); + will(returnValue(meta)); + }}); + } + + private void assertExpectedContext(BdfMessageContext c, + @Nullable MessageId dependency) { + assertEquals(meta, c.getDictionary()); + if (dependency == null) { + assertEquals(0, c.getDependencies().size()); + } else { + assertEquals(dependency, c.getDependencies().iterator().next()); } - - return msg; - } - - // - // Introduction ACK - // - - @Test - public void testValidateProperIntroductionAck() throws Exception { - byte[] sessionId = getRandomId(); - byte[] mac = getRandomBytes(MAC_LENGTH); - byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH); - BdfList body = BdfList.of(TYPE_ACK, sessionId, mac, sig); - - BdfDictionary result = - validator.validateMessage(message, group, body).getDictionary(); - - assertEquals(Long.valueOf(TYPE_ACK), result.getLong(TYPE)); - assertArrayEquals(sessionId, result.getRaw(SESSION_ID)); - assertArrayEquals(mac, result.getRaw(MAC)); - assertArrayEquals(sig, result.getRaw(SIGNATURE)); - context.assertIsSatisfied(); - } - - @Test(expected = FormatException.class) - public void testValidateTooLongIntroductionAck() throws Exception { - BdfDictionary msg = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_ACK), - new BdfEntry(SESSION_ID, getRandomId()), - new BdfEntry("garbage", getRandomString(255)) - ); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getString("garbage")); - - validator.validateMessage(message, group, body); - } - - @Test(expected = FormatException.class) - public void testValidateIntroductionAckWithLongSessionId() - throws Exception { - BdfDictionary msg = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_ACK), - new BdfEntry(SESSION_ID, new byte[SessionId.LENGTH + 1]) - ); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID)); - - validator.validateMessage(message, group, body); - } - - // - // Introduction Abort - // - - @Test - public void testValidateProperIntroductionAbort() throws Exception { - byte[] sessionId = getRandomId(); - - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_ABORT); - msg.put(SESSION_ID, sessionId); - - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID)); - - BdfDictionary result = - validator.validateMessage(message, group, body).getDictionary(); - - assertEquals(Long.valueOf(TYPE_ABORT), result.getLong(TYPE)); - assertEquals(sessionId, result.getRaw(SESSION_ID)); - context.assertIsSatisfied(); - } - - @Test(expected = FormatException.class) - public void testValidateTooLongIntroductionAbort() throws Exception { - BdfDictionary msg = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_ABORT), - new BdfEntry(SESSION_ID, getRandomId()), - new BdfEntry("garbage", getRandomString(255)) - ); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getString("garbage")); - - validator.validateMessage(message, group, body); } } diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java new file mode 100644 index 000000000..7b15b6ab4 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderParserIntegrationTest.java @@ -0,0 +1,246 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.data.MetadataEncoder; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorFactory; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.Group; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageFactory; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.system.Clock; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.briar.api.client.SessionId; +import org.junit.Test; + +import java.util.Map; + +import javax.inject.Inject; + +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES; +import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES; +import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap; +import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH; +import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT_ID; +import static org.briarproject.briar.introduction.MessageType.ABORT; +import static org.briarproject.briar.introduction.MessageType.REQUEST; +import static org.briarproject.briar.test.BriarTestUtils.getRealAuthor; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class MessageEncoderParserIntegrationTest extends BrambleTestCase { + + @Inject + ClientHelper clientHelper; + @Inject + MessageFactory messageFactory; + @Inject + MetadataEncoder metadataEncoder; + @Inject + AuthorFactory authorFactory; + @Inject + Clock clock; + + private final MessageEncoder messageEncoder; + private final MessageParser messageParser; + private final IntroductionValidator validator; + + private final GroupId groupId = new GroupId(getRandomId()); + private final Group group = new Group(groupId, CLIENT_ID, getRandomId()); + private final long timestamp = 42L; + private final SessionId sessionId = new SessionId(getRandomId()); + private final MessageId previousMsgId = new MessageId(getRandomId()); + private final Author author; + private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH); + private final byte[] ephemeralPublicKey = + getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final byte[] mac = getRandomBytes(MAC_BYTES); + private final byte[] signature = getRandomBytes(MAX_SIGNATURE_BYTES); + + public MessageEncoderParserIntegrationTest() { + IntroductionIntegrationTestComponent component = + DaggerIntroductionIntegrationTestComponent.builder().build(); + component.inject(this); + + messageEncoder = new MessageEncoderImpl(clientHelper, messageFactory); + messageParser = new MessageParserImpl(clientHelper); + validator = new IntroductionValidator(messageEncoder, clientHelper, + metadataEncoder, clock); + author = getRealAuthor(authorFactory); + } + + @Test + public void testRequestMessageMetadata() throws FormatException { + BdfDictionary d = messageEncoder + .encodeRequestMetadata(timestamp); + MessageMetadata meta = messageParser.parseMetadata(d); + + assertEquals(REQUEST, meta.getMessageType()); + assertNull(meta.getSessionId()); + assertEquals(timestamp, meta.getTimestamp()); + assertFalse(meta.isLocal()); + assertFalse(meta.isRead()); + assertFalse(meta.isVisibleInConversation()); + assertFalse(meta.isAvailableToAnswer()); + } + + @Test + public void testMessageMetadata() throws FormatException { + BdfDictionary d = messageEncoder + .encodeMetadata(ABORT, sessionId, timestamp, false, true, + false); + MessageMetadata meta = messageParser.parseMetadata(d); + + assertEquals(ABORT, meta.getMessageType()); + assertEquals(sessionId, meta.getSessionId()); + assertEquals(timestamp, meta.getTimestamp()); + assertFalse(meta.isLocal()); + assertTrue(meta.isRead()); + assertFalse(meta.isVisibleInConversation()); + assertFalse(meta.isAvailableToAnswer()); + } + + @Test + public void testRequestMessage() throws FormatException { + Message m = messageEncoder + .encodeRequestMessage(groupId, timestamp, previousMsgId, author, + text); + validator.validateMessage(m, group, clientHelper.toList(m)); + RequestMessage rm = + messageParser.parseRequestMessage(m, clientHelper.toList(m)); + + assertEquals(m.getId(), rm.getMessageId()); + assertEquals(m.getGroupId(), rm.getGroupId()); + assertEquals(m.getTimestamp(), rm.getTimestamp()); + assertEquals(previousMsgId, rm.getPreviousMessageId()); + assertEquals(author, rm.getAuthor()); + assertEquals(text, rm.getMessage()); + } + + @Test + public void testRequestMessageWithPreviousMsgNull() throws FormatException { + Message m = messageEncoder + .encodeRequestMessage(groupId, timestamp, null, author, text); + validator.validateMessage(m, group, clientHelper.toList(m)); + RequestMessage rm = + messageParser.parseRequestMessage(m, clientHelper.toList(m)); + + assertNull(rm.getPreviousMessageId()); + } + + @Test + public void testRequestMessageWithMsgNull() throws FormatException { + Message m = messageEncoder + .encodeRequestMessage(groupId, timestamp, previousMsgId, author, + null); + validator.validateMessage(m, group, clientHelper.toList(m)); + RequestMessage rm = + messageParser.parseRequestMessage(m, clientHelper.toList(m)); + + assertNull(rm.getMessage()); + } + + @Test + public void testAcceptMessage() throws Exception { + Map transportProperties = + getTransportPropertiesMap(2); + + long acceptTimestamp = 1337L; + Message m = messageEncoder + .encodeAcceptMessage(groupId, timestamp, previousMsgId, + sessionId, ephemeralPublicKey, acceptTimestamp, + transportProperties); + validator.validateMessage(m, group, clientHelper.toList(m)); + AcceptMessage am = + messageParser.parseAcceptMessage(m, clientHelper.toList(m)); + + assertEquals(m.getId(), am.getMessageId()); + assertEquals(m.getGroupId(), am.getGroupId()); + assertEquals(m.getTimestamp(), am.getTimestamp()); + assertEquals(previousMsgId, am.getPreviousMessageId()); + assertEquals(sessionId, am.getSessionId()); + assertArrayEquals(ephemeralPublicKey, am.getEphemeralPublicKey()); + assertEquals(acceptTimestamp, am.getAcceptTimestamp()); + assertEquals(transportProperties, am.getTransportProperties()); + } + + @Test + public void testDeclineMessage() throws Exception { + Message m = messageEncoder + .encodeDeclineMessage(groupId, timestamp, previousMsgId, + sessionId); + validator.validateMessage(m, group, clientHelper.toList(m)); + DeclineMessage dm = + messageParser.parseDeclineMessage(m, clientHelper.toList(m)); + + assertEquals(m.getId(), dm.getMessageId()); + assertEquals(m.getGroupId(), dm.getGroupId()); + assertEquals(m.getTimestamp(), dm.getTimestamp()); + assertEquals(previousMsgId, dm.getPreviousMessageId()); + assertEquals(sessionId, dm.getSessionId()); + } + + @Test + public void testAuthMessage() throws Exception { + Message m = messageEncoder + .encodeAuthMessage(groupId, timestamp, previousMsgId, + sessionId, mac, signature); + validator.validateMessage(m, group, clientHelper.toList(m)); + AuthMessage am = + messageParser.parseAuthMessage(m, clientHelper.toList(m)); + + assertEquals(m.getId(), am.getMessageId()); + assertEquals(m.getGroupId(), am.getGroupId()); + assertEquals(m.getTimestamp(), am.getTimestamp()); + assertEquals(previousMsgId, am.getPreviousMessageId()); + assertEquals(sessionId, am.getSessionId()); + assertArrayEquals(mac, am.getMac()); + assertArrayEquals(signature, am.getSignature()); + } + + @Test + public void testActivateMessage() throws Exception { + Message m = messageEncoder + .encodeActivateMessage(groupId, timestamp, previousMsgId, + sessionId, mac); + validator.validateMessage(m, group, clientHelper.toList(m)); + ActivateMessage am = + messageParser.parseActivateMessage(m, clientHelper.toList(m)); + + assertEquals(m.getId(), am.getMessageId()); + assertEquals(m.getGroupId(), am.getGroupId()); + assertEquals(m.getTimestamp(), am.getTimestamp()); + assertEquals(previousMsgId, am.getPreviousMessageId()); + assertEquals(sessionId, am.getSessionId()); + assertArrayEquals(mac, am.getMac()); + } + + @Test + public void testAbortMessage() throws Exception { + Message m = messageEncoder + .encodeAbortMessage(groupId, timestamp, previousMsgId, + sessionId); + validator.validateMessage(m, group, clientHelper.toList(m)); + AbortMessage am = + messageParser.parseAbortMessage(m, clientHelper.toList(m)); + + assertEquals(m.getId(), am.getMessageId()); + assertEquals(m.getGroupId(), am.getGroupId()); + assertEquals(m.getTimestamp(), am.getTimestamp()); + assertEquals(previousMsgId, am.getPreviousMessageId()); + assertEquals(sessionId, am.getSessionId()); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderTest.java new file mode 100644 index 000000000..56fe04bdb --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageEncoderTest.java @@ -0,0 +1,59 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.data.BdfList; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.Message; +import org.briarproject.bramble.api.sync.MessageFactory; +import org.briarproject.bramble.test.BrambleMockTestCase; +import org.jmock.Expectations; +import org.junit.Test; + +import static org.briarproject.bramble.test.TestUtils.getAuthor; +import static org.briarproject.bramble.test.TestUtils.getMessage; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.util.StringUtils.getRandomString; +import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH; +import static org.briarproject.briar.introduction.MessageType.REQUEST; + +public class MessageEncoderTest extends BrambleMockTestCase { + + private final ClientHelper clientHelper = context.mock(ClientHelper.class); + private final MessageFactory messageFactory = + context.mock(MessageFactory.class); + private final MessageEncoder messageEncoder = + new MessageEncoderImpl(clientHelper, messageFactory); + + private final GroupId groupId = new GroupId(getRandomId()); + private final Message message = getMessage(groupId); + private final long timestamp = message.getTimestamp(); + private final byte[] body = message.getRaw(); + private final Author author = getAuthor(); + private final BdfList authorList = new BdfList(); + private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH); + + @Test + public void testEncodeRequestMessage() throws FormatException { + context.checking(new Expectations() {{ + oneOf(clientHelper).toList(author); + will(returnValue(authorList)); + }}); + expectCreateMessage( + BdfList.of(REQUEST.getValue(), null, authorList, text)); + + messageEncoder + .encodeRequestMessage(groupId, timestamp, null, author, text); + } + + private void expectCreateMessage(BdfList bodyList) throws FormatException { + context.checking(new Expectations() {{ + oneOf(clientHelper).toByteArray(bodyList); + will(returnValue(body)); + oneOf(messageFactory).createMessage(groupId, timestamp, body); + will(returnValue(message)); + }}); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageSenderTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/MessageSenderTest.java deleted file mode 100644 index 1f922d706..000000000 --- a/briar-core/src/test/java/org/briarproject/briar/introduction/MessageSenderTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.briarproject.briar.introduction; - -import org.briarproject.bramble.api.FormatException; -import org.briarproject.bramble.api.client.ClientHelper; -import org.briarproject.bramble.api.data.BdfDictionary; -import org.briarproject.bramble.api.data.BdfEntry; -import org.briarproject.bramble.api.data.BdfList; -import org.briarproject.bramble.api.data.MetadataEncoder; -import org.briarproject.bramble.api.db.DatabaseComponent; -import org.briarproject.bramble.api.db.DbException; -import org.briarproject.bramble.api.db.Metadata; -import org.briarproject.bramble.api.db.Transaction; -import org.briarproject.bramble.api.sync.Group; -import org.briarproject.bramble.api.system.Clock; -import org.briarproject.briar.api.client.MessageQueueManager; -import org.briarproject.briar.api.client.SessionId; -import org.briarproject.briar.test.BriarTestCase; -import org.jmock.Expectations; -import org.jmock.Mockery; -import org.junit.Test; - -import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; -import static org.briarproject.bramble.test.TestUtils.getClientId; -import static org.briarproject.bramble.test.TestUtils.getGroup; -import static org.briarproject.bramble.test.TestUtils.getRandomBytes; -import static org.briarproject.bramble.test.TestUtils.getRandomId; -import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID; -import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE; -import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK; -import static org.junit.Assert.assertFalse; - -public class MessageSenderTest extends BriarTestCase { - - private final Mockery context; - private final MessageSender messageSender; - private final DatabaseComponent db; - private final ClientHelper clientHelper; - private final MetadataEncoder metadataEncoder; - private final MessageQueueManager messageQueueManager; - private final Clock clock; - - public MessageSenderTest() { - context = new Mockery(); - db = context.mock(DatabaseComponent.class); - clientHelper = context.mock(ClientHelper.class); - metadataEncoder = - context.mock(MetadataEncoder.class); - messageQueueManager = - context.mock(MessageQueueManager.class); - clock = context.mock(Clock.class); - - messageSender = new MessageSender(db, clientHelper, clock, - metadataEncoder, messageQueueManager); - } - - @Test - public void testSendMessage() throws DbException, FormatException { - Transaction txn = new Transaction(null, false); - Group privateGroup = getGroup(getClientId()); - SessionId sessionId = new SessionId(getRandomId()); - byte[] mac = getRandomBytes(42); - byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH); - long time = 42L; - BdfDictionary msg = BdfDictionary.of( - new BdfEntry(TYPE, TYPE_ACK), - new BdfEntry(GROUP_ID, privateGroup.getId()), - new BdfEntry(SESSION_ID, sessionId), - new BdfEntry(MAC, mac), - new BdfEntry(SIGNATURE, sig) - ); - BdfList bodyList = - BdfList.of(TYPE_ACK, sessionId.getBytes(), mac, sig); - byte[] body = getRandomBytes(8); - Metadata metadata = new Metadata(); - - context.checking(new Expectations() {{ - oneOf(clientHelper).toByteArray(bodyList); - will(returnValue(body)); - oneOf(db).getGroup(txn, privateGroup.getId()); - will(returnValue(privateGroup)); - oneOf(metadataEncoder).encode(msg); - will(returnValue(metadata)); - oneOf(clock).currentTimeMillis(); - will(returnValue(time)); - oneOf(messageQueueManager) - .sendMessage(txn, privateGroup, time, body, metadata); - }}); - - messageSender.sendMessage(txn, msg); - - context.assertIsSatisfied(); - assertFalse(txn.isCommitted()); - } - -} diff --git a/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java new file mode 100644 index 000000000..9ddb00632 --- /dev/null +++ b/briar-core/src/test/java/org/briarproject/briar/introduction/SessionEncoderParserIntegrationTest.java @@ -0,0 +1,328 @@ +package org.briarproject.briar.introduction; + +import org.briarproject.bramble.api.FormatException; +import org.briarproject.bramble.api.client.ClientHelper; +import org.briarproject.bramble.api.crypto.SecretKey; +import org.briarproject.bramble.api.data.BdfDictionary; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorFactory; +import org.briarproject.bramble.api.plugin.TransportId; +import org.briarproject.bramble.api.properties.TransportProperties; +import org.briarproject.bramble.api.sync.GroupId; +import org.briarproject.bramble.api.sync.MessageId; +import org.briarproject.bramble.api.transport.KeySetId; +import org.briarproject.bramble.test.BrambleTestCase; +import org.briarproject.briar.api.client.SessionId; +import org.briarproject.briar.introduction.IntroducerSession.Introducee; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import javax.inject.Inject; + +import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.test.TestUtils.getRandomId; +import static org.briarproject.bramble.test.TestUtils.getTransportId; +import static org.briarproject.bramble.test.TestUtils.getTransportPropertiesMap; +import static org.briarproject.briar.api.introduction.Role.INTRODUCEE; +import static org.briarproject.briar.api.introduction.Role.INTRODUCER; +import static org.briarproject.briar.introduction.IntroduceeSession.Local; +import static org.briarproject.briar.introduction.IntroduceeSession.Remote; +import static org.briarproject.briar.introduction.IntroduceeState.LOCAL_ACCEPTED; +import static org.briarproject.briar.introduction.IntroducerState.AWAIT_AUTHS; +import static org.briarproject.briar.introduction.IntroductionConstants.SESSION_KEY_ROLE; +import static org.briarproject.briar.test.BriarTestUtils.getRealAuthor; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +public class SessionEncoderParserIntegrationTest extends BrambleTestCase { + + @Inject + ClientHelper clientHelper; + @Inject + AuthorFactory authorFactory; + + private final SessionEncoder sessionEncoder; + private final SessionParser sessionParser; + + private final GroupId groupId1 = new GroupId(getRandomId()); + private final GroupId groupId2 = new GroupId(getRandomId()); + private final SessionId sessionId = new SessionId(getRandomId()); + private final long requestTimestamp = 42; + private final long localTimestamp = 1337; + private final long localTimestamp2 = 1338; + private final long acceptTimestamp = 123456; + private final long remoteAcceptTimestamp = 1234567; + private final MessageId lastLocalMessageId = new MessageId(getRandomId()); + private final MessageId lastLocalMessageId2 = new MessageId(getRandomId()); + private final MessageId lastRemoteMessageId = new MessageId(getRandomId()); + private final MessageId lastRemoteMessageId2 = new MessageId(getRandomId()); + private final Author author1; + private final Author author2; + private final byte[] ephemeralPublicKey = + getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final byte[] ephemeralPrivateKey = + getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final byte[] masterKey = getRandomBytes(SecretKey.LENGTH); + private final byte[] remoteEphemeralPublicKey = + getRandomBytes(MAX_PUBLIC_KEY_LENGTH); + private final Map transportProperties = + getTransportPropertiesMap(3); + private final Map + remoteTransportProperties = getTransportPropertiesMap(3); + private final Map transportKeys = new HashMap<>(); + private final byte[] localMacKey = getRandomBytes(SecretKey.LENGTH); + private final byte[] remoteMacKey = getRandomBytes(SecretKey.LENGTH); + + public SessionEncoderParserIntegrationTest() { + IntroductionIntegrationTestComponent component = + DaggerIntroductionIntegrationTestComponent.builder().build(); + component.inject(this); + + sessionEncoder = new SessionEncoderImpl(clientHelper); + sessionParser = new SessionParserImpl(clientHelper); + author1 = getRealAuthor(authorFactory); + author2 = getRealAuthor(authorFactory); + transportKeys.put(getTransportId(), new KeySetId(1)); + transportKeys.put(getTransportId(), new KeySetId(2)); + transportKeys.put(getTransportId(), new KeySetId(3)); + } + + @Test + public void testIntroducerSession() throws FormatException { + IntroducerSession s1 = getIntroducerSession(); + + BdfDictionary d = sessionEncoder.encodeIntroducerSession(s1); + IntroducerSession s2 = sessionParser.parseIntroducerSession(d); + + assertEquals(INTRODUCER, s1.getRole()); + assertEquals(s1.getRole(), s2.getRole()); + assertEquals(sessionId, s1.getSessionId()); + assertEquals(s1.getSessionId(), s2.getSessionId()); + assertEquals(AWAIT_AUTHS, s1.getState()); + assertEquals(s1.getState(), s2.getState()); + assertIntroduceeEquals(s1.getIntroduceeA(), s2.getIntroduceeA()); + assertIntroduceeEquals(s1.getIntroduceeB(), s2.getIntroduceeB()); + } + + @Test + public void testIntroducerSessionWithNulls() throws FormatException { + Introducee introducee1 = + new Introducee(sessionId, groupId1, author1, localTimestamp, + null, null); + Introducee introducee2 = + new Introducee(sessionId, groupId2, author2, localTimestamp2, + null, null); + IntroducerSession s1 = new IntroducerSession(sessionId, + AWAIT_AUTHS, requestTimestamp, introducee1, + introducee2); + + BdfDictionary d = sessionEncoder.encodeIntroducerSession(s1); + IntroducerSession s2 = sessionParser.parseIntroducerSession(d); + + assertNull(s1.getIntroduceeA().lastLocalMessageId); + assertEquals(s1.getIntroduceeA().lastLocalMessageId, + s2.getIntroduceeA().lastLocalMessageId); + assertNull(s1.getIntroduceeA().lastRemoteMessageId); + assertEquals(s1.getIntroduceeA().lastRemoteMessageId, + s2.getIntroduceeA().lastRemoteMessageId); + + assertNull(s1.getIntroduceeB().lastLocalMessageId); + assertEquals(s1.getIntroduceeB().lastLocalMessageId, + s2.getIntroduceeB().lastLocalMessageId); + assertNull(s1.getIntroduceeB().lastRemoteMessageId); + assertEquals(s1.getIntroduceeB().lastRemoteMessageId, + s2.getIntroduceeB().lastRemoteMessageId); + } + + @Test(expected = FormatException.class) + public void testIntroducerSessionUnknownRole() throws FormatException { + IntroducerSession s = getIntroducerSession(); + BdfDictionary d = sessionEncoder.encodeIntroducerSession(s); + d.put(SESSION_KEY_ROLE, 1337); + sessionParser.parseIntroducerSession(d); + } + + @Test(expected = IllegalArgumentException.class) + public void testIntroducerSessionWrongRole() throws FormatException { + IntroducerSession s = getIntroducerSession(); + BdfDictionary d = sessionEncoder.encodeIntroducerSession(s); + d.put(SESSION_KEY_ROLE, INTRODUCEE.getValue()); + sessionParser.parseIntroducerSession(d); + } + + @Test + public void testIntroduceeSession() throws FormatException { + IntroduceeSession s1 = getIntroduceeSession(); + BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s1); + IntroduceeSession s2 = + sessionParser.parseIntroduceeSession(groupId1, d); + + assertEquals(LOCAL_ACCEPTED, s1.getState()); + assertEquals(s1.getState(), s2.getState()); + assertEquals(INTRODUCEE, s1.getRole()); + assertEquals(s1.getRole(), s2.getRole()); + assertEquals(sessionId, s1.getSessionId()); + assertEquals(s1.getSessionId(), s2.getSessionId()); + assertEquals(groupId1, s1.getContactGroupId()); + assertEquals(s1.getContactGroupId(), s2.getContactGroupId()); + assertEquals(author1, s1.getIntroducer()); + assertEquals(s1.getIntroducer(), s2.getIntroducer()); + assertArrayEquals(masterKey, s1.getMasterKey()); + assertArrayEquals(s1.getMasterKey(), s2.getMasterKey()); + assertEquals(transportKeys, s1.getTransportKeys()); + assertEquals(s1.getTransportKeys(), s2.getTransportKeys()); + assertEquals(localTimestamp, s1.getLocalTimestamp()); + assertEquals(s1.getLocalTimestamp(), s2.getLocalTimestamp()); + assertEquals(lastLocalMessageId, s1.getLastLocalMessageId()); + assertEquals(s1.getLastLocalMessageId(), s2.getLastLocalMessageId()); + assertEquals(lastRemoteMessageId, s1.getLastRemoteMessageId()); + assertEquals(s1.getLastRemoteMessageId(), s2.getLastRemoteMessageId()); + + // check local + assertTrue(s1.getLocal().alice); + assertEquals(s1.getLocal().alice, s2.getLocal().alice); + assertEquals(lastLocalMessageId, s1.getLocal().lastMessageId); + assertEquals(s1.getLocal().lastMessageId, s2.getLocal().lastMessageId); + assertEquals(localTimestamp, s1.getLocal().lastMessageTimestamp); + assertEquals(s1.getLocal().lastMessageTimestamp, + s2.getLocal().lastMessageTimestamp); + assertArrayEquals(ephemeralPublicKey, s1.getLocal().ephemeralPublicKey); + assertArrayEquals(s1.getLocal().ephemeralPublicKey, + s2.getLocal().ephemeralPublicKey); + assertArrayEquals(ephemeralPrivateKey, + s1.getLocal().ephemeralPrivateKey); + assertArrayEquals(s1.getLocal().ephemeralPrivateKey, + s2.getLocal().ephemeralPrivateKey); + assertEquals(transportProperties, s1.getLocal().transportProperties); + assertEquals(s1.getLocal().transportProperties, + s2.getLocal().transportProperties); + assertEquals(acceptTimestamp, s1.getLocal().acceptTimestamp); + assertEquals(s1.getLocal().acceptTimestamp, + s2.getLocal().acceptTimestamp); + assertArrayEquals(localMacKey, s1.getLocal().macKey); + assertArrayEquals(s1.getLocal().macKey, s2.getLocal().macKey); + + // check remote + assertFalse(s1.getRemote().alice); + assertEquals(s1.getRemote().alice, s2.getRemote().alice); + assertEquals(author2, s1.getRemote().author); + assertEquals(s1.getRemote().author, s2.getRemote().author); + assertEquals(lastRemoteMessageId, s1.getRemote().lastMessageId); + assertEquals(s1.getRemote().lastMessageId, + s2.getRemote().lastMessageId); + assertArrayEquals(remoteEphemeralPublicKey, + s1.getRemote().ephemeralPublicKey); + assertArrayEquals(s1.getRemote().ephemeralPublicKey, + s2.getRemote().ephemeralPublicKey); + assertEquals(remoteTransportProperties, + s1.getRemote().transportProperties); + assertEquals(s1.getRemote().transportProperties, + s2.getRemote().transportProperties); + assertEquals(remoteAcceptTimestamp, s1.getRemote().acceptTimestamp); + assertEquals(s1.getRemote().acceptTimestamp, + s2.getRemote().acceptTimestamp); + assertArrayEquals(remoteMacKey, s1.getRemote().macKey); + assertArrayEquals(s1.getRemote().macKey, s2.getRemote().macKey); + } + + @Test + public void testIntroduceeSessionWithNulls() throws FormatException { + IntroduceeSession s1 = IntroduceeSession + .getInitial(groupId1, sessionId, author1, false, author2); + + BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s1); + IntroduceeSession s2 = + sessionParser.parseIntroduceeSession(groupId1, d); + + assertNull(s1.getLastLocalMessageId()); + assertEquals(s1.getLastLocalMessageId(), s2.getLastLocalMessageId()); + assertNull(s1.getLastRemoteMessageId()); + assertEquals(s1.getLastRemoteMessageId(), s2.getLastRemoteMessageId()); + assertNull(s1.getMasterKey()); + assertEquals(s1.getMasterKey(), s2.getMasterKey()); + assertNull(s1.getTransportKeys()); + assertEquals(s1.getTransportKeys(), s2.getTransportKeys()); + + // check local + assertNull(s1.getLocal().lastMessageId); + assertEquals(s1.getLocal().lastMessageId, s2.getLocal().lastMessageId); + assertNull(s1.getLocal().ephemeralPublicKey); + assertEquals(s1.getLocal().ephemeralPublicKey, + s2.getLocal().ephemeralPublicKey); + assertNull(s1.getLocal().ephemeralPrivateKey); + assertEquals(s1.getLocal().ephemeralPrivateKey, + s2.getLocal().ephemeralPrivateKey); + assertNull(s1.getLocal().transportProperties); + assertEquals(s1.getLocal().transportProperties, + s2.getLocal().transportProperties); + assertNull(s1.getLocal().macKey); + assertEquals(s1.getLocal().macKey, s2.getLocal().macKey); + + // check remote + assertNull(s1.getRemote().lastMessageId); + assertEquals(s1.getRemote().lastMessageId, + s2.getRemote().lastMessageId); + assertNull(s1.getRemote().ephemeralPublicKey); + assertEquals(s1.getRemote().ephemeralPublicKey, + s2.getRemote().ephemeralPublicKey); + assertNull(s1.getRemote().transportProperties); + assertEquals(s1.getRemote().transportProperties, + s2.getRemote().transportProperties); + assertNull(s1.getRemote().macKey); + assertEquals(s1.getRemote().macKey, s2.getRemote().macKey); + } + + @Test(expected = FormatException.class) + public void testIntroduceeSessionUnknownRole() throws FormatException { + IntroduceeSession s = getIntroduceeSession(); + BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s); + d.put(SESSION_KEY_ROLE, 1337); + sessionParser.parseIntroduceeSession(groupId1, d); + } + + @Test(expected = IllegalArgumentException.class) + public void testIntroduceeSessionWrongRole() throws FormatException { + IntroduceeSession s = getIntroduceeSession(); + BdfDictionary d = sessionEncoder.encodeIntroduceeSession(s); + d.put(SESSION_KEY_ROLE, INTRODUCER.getValue()); + sessionParser.parseIntroduceeSession(groupId1, d); + } + + private IntroducerSession getIntroducerSession() { + Introducee introducee1 = + new Introducee(sessionId, groupId1, author1, localTimestamp, + lastLocalMessageId, lastRemoteMessageId); + Introducee introducee2 = + new Introducee(sessionId, groupId2, author2, localTimestamp2, + lastLocalMessageId2, lastRemoteMessageId2); + return new IntroducerSession(sessionId, AWAIT_AUTHS, + requestTimestamp, introducee1, introducee2); + } + + private IntroduceeSession getIntroduceeSession() { + Local local = new Local(true, lastLocalMessageId, localTimestamp, + ephemeralPublicKey, ephemeralPrivateKey, transportProperties, + acceptTimestamp, localMacKey); + Remote remote = new Remote(false, author2, lastRemoteMessageId, + remoteEphemeralPublicKey, remoteTransportProperties, + remoteAcceptTimestamp, remoteMacKey); + return new IntroduceeSession(sessionId, LOCAL_ACCEPTED, + requestTimestamp, groupId1, author1, local, remote, + masterKey, transportKeys); + } + + private void assertIntroduceeEquals(Introducee i1, Introducee i2) { + assertEquals(i1.author, i2.author); + assertEquals(i1.groupId, i2.groupId); + assertEquals(i1.localTimestamp, i2.localTimestamp); + assertEquals(i1.lastLocalMessageId, i2.lastLocalMessageId); + assertEquals(i1.lastRemoteMessageId, i2.lastRemoteMessageId); + } + +} diff --git a/briar-core/src/test/java/org/briarproject/briar/privategroup/GroupMessageValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/privategroup/GroupMessageValidatorTest.java index 196ce09ca..3148da439 100644 --- a/briar-core/src/test/java/org/briarproject/briar/privategroup/GroupMessageValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/privategroup/GroupMessageValidatorTest.java @@ -368,14 +368,6 @@ public class GroupMessageValidatorTest extends ValidatorTestCase { .getBoolean(KEY_INITIAL_JOIN_MSG)); } - private void expectParseAuthor(BdfList authorList, Author author) - throws Exception { - context.checking(new Expectations() {{ - oneOf(clientHelper).parseAndValidateAuthor(authorList); - will(returnValue(author)); - }}); - } - private void expectRejectAuthor(BdfList authorList) throws Exception { context.checking(new Expectations() {{ oneOf(clientHelper).parseAndValidateAuthor(authorList); diff --git a/briar-core/src/test/java/org/briarproject/briar/sharing/SharingValidatorTest.java b/briar-core/src/test/java/org/briarproject/briar/sharing/SharingValidatorTest.java index 157c4ce79..f001fb680 100644 --- a/briar-core/src/test/java/org/briarproject/briar/sharing/SharingValidatorTest.java +++ b/briar-core/src/test/java/org/briarproject/briar/sharing/SharingValidatorTest.java @@ -150,7 +150,7 @@ public abstract class SharingValidatorTest extends ValidatorTestCase { } void assertExpectedContext(BdfMessageContext messageContext, - @Nullable MessageId previousMsgId) throws FormatException { + @Nullable MessageId previousMsgId) { Collection dependencies = messageContext.getDependencies(); if (previousMsgId == null) { assertTrue(dependencies.isEmpty()); diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java index 1b2344d3a..ecaf60d1b 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarIntegrationTestComponent.java @@ -36,7 +36,10 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager import org.briarproject.briar.blog.BlogModule; import org.briarproject.briar.client.BriarClientModule; import org.briarproject.briar.forum.ForumModule; +import org.briarproject.briar.introduction.IntroductionCryptoIntegrationTest; import org.briarproject.briar.introduction.IntroductionModule; +import org.briarproject.briar.introduction.MessageEncoderParserIntegrationTest; +import org.briarproject.briar.introduction.SessionEncoderParserIntegrationTest; import org.briarproject.briar.messaging.MessagingModule; import org.briarproject.briar.privategroup.PrivateGroupModule; import org.briarproject.briar.privategroup.invitation.GroupInvitationModule; diff --git a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java index d29fc0b54..9f71087eb 100644 --- a/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java +++ b/briar-core/src/test/java/org/briarproject/briar/test/BriarTestUtils.java @@ -1,10 +1,18 @@ package org.briarproject.briar.test; +import org.briarproject.bramble.api.crypto.CryptoComponent; +import org.briarproject.bramble.api.crypto.KeyPair; import org.briarproject.bramble.api.db.DbException; +import org.briarproject.bramble.api.identity.Author; +import org.briarproject.bramble.api.identity.AuthorFactory; +import org.briarproject.bramble.api.identity.LocalAuthor; import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker.GroupCount; +import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH; +import static org.briarproject.bramble.test.TestUtils.getRandomBytes; +import static org.briarproject.bramble.util.StringUtils.getRandomString; import static org.junit.Assert.assertEquals; public class BriarTestUtils { @@ -25,4 +33,17 @@ public class BriarTestUtils { assertEquals(unreadCount, c1.getUnreadCount()); } + public static Author getRealAuthor(AuthorFactory authorFactory) { + return authorFactory.createAuthor(getRandomString(5), + getRandomBytes(MAX_PUBLIC_KEY_LENGTH)); + } + + public static LocalAuthor getRealLocalAuthor( + CryptoComponent cryptoComponent, AuthorFactory authorFactory) { + KeyPair keyPair = cryptoComponent.generateSignatureKeyPair(); + return authorFactory.createLocalAuthor(getRandomString(5), + keyPair.getPublic().getEncoded(), + keyPair.getPrivate().getEncoded()); + } + }