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 3b12c5cf75ae93f0b9fa0f84f549c88c39c0a3d1..b9f772d2b03905307fabe5d5dab3401fc6a912e9 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 dd5874f6edcca3400a2922d3f1feb9e2db5f2b2f..f63bcb0a6baf91c708eaaa2dc26ea8fbde2c73c5 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<Connection> { 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 799722344ba234014686b8549cd7c5bc7b6cb417..66a4a3b5630f14dd5b8f6448b320ebcd67894be8 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 585b0fed9be5a448482568b7b21e7e962dfc157b..2af7fb8e54522643319eb4215391879c310b2edf 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 3a76421b375e9c8f9b51ac4fa6976a923cd56582..7a9652a610f22ff4a994655458cdfbf33ff6cf3d 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 e78e1c63980169f266bef00394d1232bdf245bd4..0cdc34ba86a400842a912bc59e6367b32c6095df 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"/> + <TextView + android:id="@+id/introductionNotPossibleView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="@dimen/margin_activity_horizontal" + android:text="@string/introduction_not_possible" + android:textSize="@dimen/text_size_large" + android:visibility="gone" + tools:visibility="visible"/> + <org.briarproject.briar.android.view.LargeTextInputView android:id="@+id/introductionMessageView" android:layout_width="match_parent" android:layout_height="wrap_content" + android:visibility="gone" app:buttonText="@string/introduction_button" app:hint="@string/introduction_message_hint" - app:maxLines="5"/> + app:maxLines="5" + tools:visibility="visible"/> </LinearLayout> </ScrollView> \ 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 d4b40f5bb8636cadbb67b1b4fe315222e0fbe11c..38b04cd534ba1ac46342e839d5f61b8e7d90514b 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -144,6 +144,7 @@ <string name="introduction_onboarding_title">Introduce your contacts</string> <string name="introduction_onboarding_text">You can introduce your contacts to each other, so they don\'t need to meet in person to connect on Briar.</string> <string name="introduction_activity_title">Select Contact</string> + <string name="introduction_not_possible">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.</string> <string name="introduction_message_title">Introduce Contacts</string> <string name="introduction_message_hint">Add a message (optional)</string> <string name="introduction_button">Make Introduction</string> 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 75418bf7c27afd1375a2156d6ac6b1f15388805e..0000000000000000000000000000000000000000 --- 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 281d9af86ff4b2d642a08d0854860cf46a626db3..0000000000000000000000000000000000000000 --- 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<A, S, M> { - - StateUpdate<S, M> onLocalAction(S localState, A action); - - StateUpdate<S, M> onMessageReceived(S localState, M received); - - StateUpdate<S, M> onMessageDelivered(S localState, M delivered); - - class StateUpdate<S, M> { - public final boolean deleteMessage; - public final boolean deleteState; - public final S localState; - public final List<M> toSend; - public final List<Event> 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<M> toSend, List<Event> 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 c41b6da2b665dfc2dbc5b0c939c81f7917c80543..0000000000000000000000000000000000000000 --- 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 ac458a8a89494536aab35ba09fffc68e0a80b182..0000000000000000000000000000000000000000 --- 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 68881b9dc0d57e5993afb38e35943652141629a5..0000000000000000000000000000000000000000 --- 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 e696181a01715261bd2d58b697ce03ff61907d67..0000000000000000000000000000000000000000 --- 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 7123c7eb26f8abf6f9c455307253aaaf04f59c75..0000000000000000000000000000000000000000 --- 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 b3d89864e421b60853111c67e43fdbe9a2c6a6f3..0000000000000000000000000000000000000000 --- 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 72e6030b10b207a179abf99be29e7897af2777cb..87681d525076125d90ed43e7437911d4aea8f97e 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"; + String LABEL_ALICE_MAC_KEY = + "org.briarproject.briar.introduction/ALICE_MAC_KEY"; - /** - * Label for deriving Alice's key binding nonce from the shared secret. - */ - String ALICE_NONCE_LABEL = - "org.briarproject.briar.introduction/ALICE_NONCE"; + String LABEL_BOB_MAC_KEY = + "org.briarproject.briar.introduction/BOB_MAC_KEY"; - /** - * Label for deriving Bob's key binding nonce from the shared secret. - */ - String BOB_NONCE_LABEL = - "org.briarproject.briar.introduction/BOB_NONCE"; + String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC"; - /** - * Label for deriving Alice's MAC key from the shared secret. - */ - String ALICE_MAC_KEY_LABEL = - "org.briarproject.briar.introduction/ALICE_MAC_KEY"; + String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN"; - /** - * Label for deriving Bob's MAC key from the shared secret. - */ - String BOB_MAC_KEY_LABEL = - "org.briarproject.briar.introduction/BOB_MAC_KEY"; + String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE"; - /** - * Label for signing the introduction response. - */ - String SIGNING_LABEL = - "org.briarproject.briar.introduction/RESPONSE_SIGNATURE"; + 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 ce9d067277fabd9aed94a254f3c86f2b47ee3775..8711193f15b9e4696fa0bebb8bb2d8f61d9aeb16 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; - - /** - * Accepts an introduction. - */ - void acceptIntroduction(ContactId contactId, SessionId sessionId, - long timestamp) throws DbException, FormatException; + long timestamp) throws DbException; /** - * Declines an introduction. + * Responds to 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 861469a3a4d0c8eb6b9f34b5c6e5247db98fe995..009487fa2faf852fc5aa843db4d3aba9a09ba24f 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 428528260c6a4f11dbd88983e78f55183f9b1614..b2a804bd88b8de918546c68e15a3327e4605180f 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 22df6eba89e2e44d38fb15cdd94bd75dfacd3cb4..816135d43f59668a4ecfa4baf27e625ba2469462 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 0000000000000000000000000000000000000000..38f0bd44cf85fb804fcff9f49e520474423e4a19 --- /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 5855275918a6001dbabf6211183d5aad40d4b9a5..113ba400efdebb484b0b41b84d70ccc64443f2b3 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 19d8142c1a6c2f8e99d378ffea4a8f769e5d3cc3..edc62948dd5681e84bd72f1834b6158f31833d30 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 48fd665a10f17cd77c2fc446da96ccf5a39d3181..0000000000000000000000000000000000000000 --- 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 46ee505dfa0cf6ad68f119ef8563a024b16f9f42..9eeb6b9597eac924138444067dcddd716384be8c 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 47c91bbc2d7edc24ae4b9945a17faed057d0ada3..0000000000000000000000000000000000000000 --- 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<Long, MessageId> 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<Long, MessageId> 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<Long, MessageId> pending; - - private QueueState(long outgoingPosition, long incomingPosition, - TreeMap<Long, MessageId> pending) { - this.outgoingPosition = outgoingPosition; - this.incomingPosition = incomingPosition; - this.pending = pending; - } - - @Nullable - MessageId popIncomingMessageId() { - Iterator<Entry<Long, MessageId>> it = pending.entrySet().iterator(); - if (!it.hasNext()) { - LOG.info("No pending messages"); - return null; - } - Entry<Long, MessageId> 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<MessageId> 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 480b7670b8a6d7b56d715a0ef144dea483488621..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..e9a2d1233489716140048f80642ca2687c3a33de --- /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 0000000000000000000000000000000000000000..240b5ddecf2e7915041da47b0b7df0e0bc59a7e9 --- /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 0000000000000000000000000000000000000000..c9002bb0e61fb3f8f2989a1fdf308e52afa6858c --- /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<S extends Session> + implements ProtocolEngine<S> { + + 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<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..6cadae73b6257bbe76c67d5904a47dba286838c3 --- /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<TransportId, TransportProperties> transportProperties; + + protected AcceptMessage(MessageId messageId, GroupId groupId, + long timestamp, @Nullable MessageId previousMessageId, + SessionId sessionId, + byte[] ephemeralPublicKey, + long acceptTimestamp, + Map<TransportId, TransportProperties> 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<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..5f767737d90fe68029f80e2e0ab4d87932df13ce --- /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 0000000000000000000000000000000000000000..1de1a4eb5803cbbfef2fb82c6fbc99e88ea09f5d --- /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 0000000000000000000000000000000000000000..27386b90587a7297174ffbbf27e2ef82544b2444 --- /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 2d5bb95c3ad591c57b5aabb0889f48c4803d93ab..0000000000000000000000000000000000000000 --- 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<BdfDictionary, BdfDictionary, BdfDictionary> { - - private static final Logger LOG = - Logger.getLogger(IntroduceeEngine.class.getName()); - - @Override - public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction( - BdfDictionary localState, BdfDictionary localAction) { - - try { - IntroduceeProtocolState currentState = - getState(localState.getLong(STATE)); - int type = localAction.getLong(TYPE).intValue(); - IntroduceeAction action; - if (localState.containsKey(ACCEPT)) action = IntroduceeAction - .getLocal(type, localState.getBoolean(ACCEPT)); - else action = IntroduceeAction.getLocal(type); - IntroduceeProtocolState nextState = currentState.next(action); - - if (action == LOCAL_ABORT && currentState != ERROR) { - return abortSession(currentState, localState); - } - - if (nextState == ERROR) { - if (LOG.isLoggable(WARNING)) { - LOG.warning("Error: Invalid action in state " + - currentState.name()); - } - if (currentState == ERROR) return noUpdate(localState); - else return abortSession(currentState, localState); - } - - List<BdfDictionary> 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<Event> events = Collections.emptyList(); - return new StateUpdate<>(false, false, - localState, messages, events); - } catch (FormatException e) { - throw new IllegalArgumentException(e); - } - } - - @Override - public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived( - BdfDictionary localState, BdfDictionary msg) { - - try { - IntroduceeProtocolState currentState = - getState(localState.getLong(STATE)); - int type = msg.getLong(TYPE).intValue(); - IntroduceeAction action = IntroduceeAction.getRemote(type); - IntroduceeProtocolState nextState = currentState.next(action); - - logMessageReceived(currentState, nextState, localState, type, msg); - - if (nextState == ERROR) { - if (currentState != ERROR && action != REMOTE_ABORT) { - return abortSession(currentState, localState); - } else { - return noUpdate(localState); - } - } - - // update local session state with next protocol state - localState.put(STATE, nextState.getValue()); - List<BdfDictionary> messages; - List<Event> events; - // we received the introduction request - if (currentState == AWAIT_REQUEST) { - // remember the session ID used by the introducer - localState.put(SESSION_ID, msg.getRaw(SESSION_ID)); - - addRequestData(localState, msg); - messages = Collections.emptyList(); - events = Collections.singletonList(getEvent(localState, msg)); - } - // we had the request and now one response came in _OR_ - // we had sent our response already and now received the other one - else if (currentState == AWAIT_RESPONSES || - currentState == AWAIT_REMOTE_RESPONSE) { - // update next state based on message content - action = IntroduceeAction - .getRemote(type, msg.getBoolean(ACCEPT)); - nextState = currentState.next(action); - localState.put(STATE, nextState.getValue()); - - addResponseData(localState, msg); - if (nextState == AWAIT_ACK) { - localState.put(TASK, TASK_ADD_CONTACT); - } - messages = Collections.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.<BdfDictionary>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<BdfDictionary, BdfDictionary> onMessageDelivered( - BdfDictionary localState, BdfDictionary delivered) { - try { - return noUpdate(localState); - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - return null; - } - } - - private IntroduceeProtocolState getState(Long state) { - return IntroduceeProtocolState.fromValue(state.intValue()); - } - - private Event getEvent(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - ContactId contactId = - new ContactId(localState.getLong(CONTACT_ID_1).intValue()); - AuthorId authorId = new AuthorId(localState.getRaw(REMOTE_AUTHOR_ID)); - - SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID)); - MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID)); - 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<BdfDictionary, BdfDictionary> 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<BdfDictionary> 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<Event> events = Collections.singletonList(event); - - return new StateUpdate<>(false, false, localState, messages, events); - } - - private StateUpdate<BdfDictionary, BdfDictionary> noUpdate( - BdfDictionary localState) throws FormatException { - - return new StateUpdate<>(false, false, localState, - Collections.<BdfDictionary>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 4666ac92e68bdf30621f35fa33e3801c221ef23f..0000000000000000000000000000000000000000 --- 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<TransportId, TransportProperties> 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<BdfDictionary, BdfDictionary> 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<TransportId, TransportProperties> 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 - * <p> - * 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. - * <p> - * Our nonce is signed with the local author's long-term private key. - * The signature is added to the localState. - * <p> - * 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<TransportId, TransportProperties> map) { - - BdfDictionary d = new BdfDictionary(); - for (Map.Entry<TransportId, TransportProperties> e : map.entrySet()) { - d.put(e.getKey().getString(), e.getValue()); - } - return d; - } - - private Map<TransportId, TransportProperties> parseTransportProperties( - BdfDictionary d) throws FormatException { - - Map<TransportId, TransportProperties> tpMap = new HashMap<>(); - 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 0000000000000000000000000000000000000000..c0cf65a073da10600635754295ab449a335c1f39 --- /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<IntroduceeSession> { + + 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<TransportId, TransportProperties> 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<TransportId, KeySetId> 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<MessageId, BdfDictionary> 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 0000000000000000000000000000000000000000..b952b3dc52e9c64a5c8900cb6e5c27131e309cc5 --- /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<IntroduceeState> + 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<TransportId, KeySetId> transportKeys; + + IntroduceeSession(SessionId sessionId, IntroduceeState state, + long requestTimestamp, GroupId contactGroupId, Author introducer, + Local local, Remote remote, @Nullable byte[] masterKey, + @Nullable Map<TransportId, KeySetId> 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<TransportId, TransportProperties> 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<TransportId, KeySetId> 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<TransportId, KeySetId> getTransportKeys() { + return transportKeys; + } + + abstract static class Common { + final boolean alice; + @Nullable + final MessageId lastMessageId; + @Nullable + final byte[] ephemeralPublicKey; + @Nullable + final Map<TransportId, TransportProperties> transportProperties; + final long acceptTimestamp; + @Nullable + final byte[] macKey; + + private Common(boolean alice, @Nullable MessageId lastMessageId, + @Nullable byte[] ephemeralPublicKey, @Nullable + Map<TransportId, TransportProperties> 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<TransportId, TransportProperties> 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<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..7a54abde8d0938a4201dd307013264431cc2e3ef --- /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 df364b34ff0f7cdc39e4c607678ae6cdb1e64398..0000000000000000000000000000000000000000 --- 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<BdfDictionary, BdfDictionary, BdfDictionary> { - - private static final Logger LOG = - Logger.getLogger(IntroducerEngine.class.getName()); - - @Override - public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction( - BdfDictionary localState, BdfDictionary localAction) { - - try { - IntroducerProtocolState currentState = - getState(localState.getLong(STATE)); - int type = localAction.getLong(TYPE).intValue(); - IntroducerAction action = IntroducerAction.getLocal(type); - IntroducerProtocolState nextState = currentState.next(action); - - if (action == LOCAL_ABORT && currentState != ERROR) { - return abortSession(currentState, localState); - } - - if (nextState == ERROR) { - if (LOG.isLoggable(WARNING)) { - LOG.warning("Error: Invalid action in state " + - currentState.name()); - } - return noUpdate(localState); - } - - localState.put(STATE, nextState.getValue()); - if (action == LOCAL_REQUEST) { - // create the introduction requests for both contacts - List<BdfDictionary> messages = new ArrayList<>(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<Event> 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<BdfDictionary, BdfDictionary> onMessageReceived( - BdfDictionary localState, BdfDictionary msg) { - - try { - IntroducerProtocolState currentState = - getState(localState.getLong(STATE)); - int type = msg.getLong(TYPE).intValue(); - boolean one = isContact1(localState, msg); - IntroducerAction action = IntroducerAction.getRemote(type, one); - IntroducerProtocolState nextState = currentState.next(action); - - logMessageReceived(currentState, nextState, localState, type, msg); - - if (nextState == ERROR) { - if (currentState != ERROR) { - return abortSession(currentState, localState); - } else { - return noUpdate(localState); - } - } - - List<BdfDictionary> messages; - List<Event> events; - - // we have sent our requests and just got the 1st or 2nd response - if (currentState == AWAIT_RESPONSES || - currentState == AWAIT_RESPONSE_1 || - currentState == AWAIT_RESPONSE_2) { - // update next state based on message content - action = IntroducerAction - .getRemote(type, one, msg.getBoolean(ACCEPT)); - nextState = currentState.next(action); - localState.put(STATE, nextState.getValue()); - if (one) localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID)); - else localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID)); - - messages = forwardMessage(localState, msg); - events = Collections.singletonList(getEvent(localState, msg)); - } - // we have forwarded both responses and now received the 1st or 2nd ACK - else if (currentState == AWAIT_ACKS || - currentState == AWAIT_ACK_1 || - currentState == AWAIT_ACK_2) { - localState.put(STATE, nextState.getValue()); - messages = forwardMessage(localState, msg); - events = Collections.emptyList(); - } - // we probably received a response while already being FINISHED - else if (currentState == FINISHED) { - // if it was a response store it to be found later - if (action == REMOTE_ACCEPT_1 || action == REMOTE_DECLINE_1) { - localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID)); - messages = Collections.emptyList(); - events = Collections - .singletonList(getEvent(localState, msg)); - } else if (action == REMOTE_ACCEPT_2 || - action == REMOTE_DECLINE_2) { - localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID)); - messages = Collections.emptyList(); - events = Collections - .singletonList(getEvent(localState, msg)); - } else return noUpdate(localState); - } else { - throw new IllegalArgumentException("Bad state"); - } - return new StateUpdate<>(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<BdfDictionary> forwardMessage(BdfDictionary localState, - BdfDictionary message) throws FormatException { - - // clone the message here, because we still need the original - BdfDictionary msg = (BdfDictionary) message.clone(); - if (isContact1(localState, msg)) { - msg.put(GROUP_ID, localState.getRaw(GROUP_ID_2)); - } else { - msg.put(GROUP_ID, localState.getRaw(GROUP_ID_1)); - } - - return Collections.singletonList(msg); - } - - @Override - public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered( - BdfDictionary localState, BdfDictionary delivered) { - try { - return noUpdate(localState); - } catch (FormatException e) { - if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); - return null; - } - } - - private IntroducerProtocolState getState(Long state) { - return IntroducerProtocolState.fromValue(state.intValue()); - } - - private Event getEvent(BdfDictionary localState, BdfDictionary msg) - throws FormatException { - - ContactId contactId = - new ContactId(localState.getLong(CONTACT_ID_1).intValue()); - AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1)); - 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<BdfDictionary, BdfDictionary> 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<BdfDictionary> 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<Event> 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<BdfDictionary, BdfDictionary> noUpdate( - BdfDictionary localState) throws FormatException { - - return new StateUpdate<>(false, false, localState, - Collections.<BdfDictionary>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 b24109396c7f634c8668ff9aea64dca16750cca8..0000000000000000000000000000000000000000 --- 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<BdfDictionary, BdfDictionary> - result) throws DbException, FormatException { - - // save new local state - MessageId storageId = - new MessageId(result.localState.getRaw(STORAGE_ID)); - clientHelper.mergeMessageMetadata(txn, storageId, result.localState); - - // send messages - for (BdfDictionary d : result.toSend) { - 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 0000000000000000000000000000000000000000..ed19fe651a5d1d0693426cd97fbaab89c031151e --- /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<IntroducerSession> { + + @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 0000000000000000000000000000000000000000..c26eb26d981922f6d0a76e2ff6a86b3148683b99 --- /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<IntroducerState> { + + 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 0000000000000000000000000000000000000000..6514eca16feb6cfd511f8558d2c26cf8e7096742 --- /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 0000000000000000000000000000000000000000..522759a55bcafa40e32b2cbe0efa691064d19893 --- /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 0000000000000000000000000000000000000000..37f7aa10f63ffb9e25514d3d557139c408dd3433 --- /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 0000000000000000000000000000000000000000..f9d53323b08aeb90b6f81ed46018051ce6418c6e --- /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 050d2b9f4430bcf38f950ea6c3dd941d0ac44cd2..0000000000000000000000000000000000000000 --- 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 9ccf06b6796fdf869500d8e7233042950c9a84ec..efb857a0f57699ae41d9bacd541656085fc80eb7 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(); - - // 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<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, gId, query); - for (Map.Entry<MessageId, BdfDictionary> 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<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, gId, query); - for (Map.Entry<MessageId, BdfDictionary> 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); - } + removeSessionWithIntroducer(txn, c); + abortOrRemoveSessionWithIntroducee(txn, c); - // 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); + public Group getContactGroup(Contact c) { + return contactGroupFactory + .createContactGroup(CLIENT_ID, CLIENT_VERSION, c); + } - // 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); - } + @Override + protected boolean incomingMessage(Transaction txn, Message m, BdfList body, + BdfDictionary bdfMeta) throws DbException, FormatException { + // Parse the metadata + MessageMetadata meta = messageParser.parseMetadata(bdfMeta); + // Look up the session, if there is one + SessionId sessionId = meta.getSessionId(); + IntroduceeSession newIntroduceeSession = null; + if (sessionId == null) { + if (meta.getMessageType() != REQUEST) throw new AssertionError(); + newIntroduceeSession = createNewIntroduceeSession(txn, m, body); + sessionId = newIntroduceeSession.getSessionId(); } - // 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); - } + 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 { - // the message has been validated, so this should not happen - if(LOG.isLoggable(WARNING)) { - LOG.warning("Unknown message type '" + type + "', deleting..."); - } + 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; } - @Override - public Group getContactGroup(Contact contact) { - return introductionGroupFactory.createIntroductionGroup(contact); + 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); } - @Override - public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, - long timestamp) throws DbException, FormatException { + private <S extends Session> S handleMessage(Transaction txn, Message m, + BdfList body, MessageType type, S session, ProtocolEngine<S> 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(); + } + } - Transaction txn = db.startTransaction(false); + @Nullable + private StoredSession getSession(Transaction txn, + @Nullable SessionId sessionId) throws DbException, FormatException { + if (sessionId == null) return null; + BdfDictionary query = sessionParser.getSessionQuery(sessionId); + Map<MessageId, BdfDictionary> 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 { - 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); + boolean can = canIntroduce(txn, c1, c2); db.commitTransaction(txn); + return can; + } catch (FormatException e) { + throw new DbException(e); } finally { db.endTransaction(txn); } } - @Override - public void acceptIntroduction(ContactId contactId, SessionId sessionId, - long timestamp) throws DbException, FormatException { + 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 { 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, 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 declineIntroduction(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.declineIntroduction(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 Collection<IntroductionMessage> getIntroductionMessages( - ContactId contactId) throws DbException { - - Collection<IntroductionMessage> list = new ArrayList<>(); - - Map<MessageId, BdfDictionary> metadata; - Collection<MessageStatus> statuses; + public Collection<IntroductionMessage> getIntroductionMessages(ContactId c) + throws DbException { + List<IntroductionMessage> 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<MessageId, BdfDictionary> results = clientHelper + .getMessageMetadataAsDictionary(txn, contactGroupId, query); + messages = new ArrayList<>(results.size()); + for (Entry<MessageId, BdfDictionary> 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 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 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 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 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 void removeSessionWithIntroducer(Transaction txn, + Contact introducer) throws DbException { + BdfDictionary query = sessionEncoder + .getIntroduceeSessionsByIntroducerQuery(introducer.getAuthor()); + Map<MessageId, BdfDictionary> sessions; + try { + 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, boolean warn) - throws DbException, FormatException { - + private void abortOrRemoveSessionWithIntroducee(Transaction txn, + Contact c) throws DbException { + BdfDictionary query = sessionEncoder.getIntroducerSessionsQuery(); + Map<MessageId, BdfDictionary> 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(); + sessions = clientHelper + .getMessageMetadataAsDictionary(txn, localGroup.getId(), + query); + } catch (FormatException e) { + throw new DbException(); + } + LocalAuthor localAuthor = identityManager.getLocalAuthor(txn); + for (Entry<MessageId, BdfDictionary> session : sessions.entrySet()) { + IntroducerSession s; + try { + s = sessionParser.parseIntroducerSession(session.getValue()); + } catch (FormatException e) { + throw new DbException(); } - return state; - } catch (NoSuchMessageException e) { - // State not found directly, so iterate over all states - // to find state for introducee - Map<MessageId, BdfDictionary> map = clientHelper - .getMessageMetadataAsDictionary(txn, - introductionGroupFactory.createLocalGroup().getId()); - for (Map.Entry<MessageId, BdfDictionary> 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 (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); } - if (warn && LOG.isLoggable(WARNING)) - LOG.warning("No session state found"); - throw new FormatException(); } } - private BdfDictionary getSessionState(Transaction txn, GroupId groupId, - byte[] sessionId) throws DbException, FormatException { - - return getSessionState(txn, groupId, sessionId, true); + 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); + } } - private void deleteMessage(Transaction txn, MessageId messageId) - throws DbException { + private static class StoredSession { + + private final MessageId storageId; + private final BdfDictionary bdfSession; - db.deleteMessage(txn, messageId); - db.deleteMessageMetadata(txn, messageId); + 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 e0122faa9d315af728599a43220b89c4afdec756..24c649c5fd6cbb0e4a1fe7ad838af2ff13ac4d20 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 introductionValidator = new IntroductionValidator( - clientHelper, metadataEncoder, clock); - messageQueueManager.registerMessageValidator(CLIENT_ID, + IntroductionValidator provideValidator(ValidationManager validationManager, + MessageEncoder messageEncoder, MetadataEncoder metadataEncoder, + ClientHelper clientHelper, Clock clock) { + + 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 9fc526e49c24a709cc2a43d9abaea0b5973c944d..929c8bddf91798d3ca284ed9a42308edaa0a5e60 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()); + + 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(); + } + } + + private BdfMessageContext validateRequestMessage(Message m, BdfList body) + throws FormatException { + checkSize(body, 4); - BdfDictionary d; - long type = body.getLong(0); - byte[] id = body.getRaw(1); - checkLength(id, SessionId.LENGTH); - - if (type == TYPE_REQUEST) { - d = validateRequest(body); - } else if (type == TYPE_RESPONSE) { - d = validateResponse(body); - } else if (type == TYPE_ACK) { - d = validateAck(body); - } else if (type == TYPE_ABORT) { - d = validateAbort(body); + 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); + SessionId sessionId = new SessionId(sessionIdBytes); + BdfDictionary meta = messageEncoder + .encodeMetadata(ACCEPT, 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)); } - return d; } - private BdfDictionary validateResponse(BdfList message) + private BdfMessageContext validateAuthMessage(Message m, BdfList body) throws FormatException { + checkSize(body, 5); - 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); - } - } - } else { - checkSize(message, 3); - } + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); - // 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; + 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 BdfDictionary validateAck(BdfList message) throws FormatException { - checkSize(message, 4); + private BdfMessageContext validateActivateMessage(Message m, BdfList body) + throws FormatException { + checkSize(body, 4); + + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); - byte[] mac = message.getRaw(2); - checkLength(mac, 1, MAC_LENGTH); + byte[] previousMessageId = body.getRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); - byte[] sig = message.getRaw(3); - checkLength(sig, 1, MAX_SIGNATURE_LENGTH); + byte[] mac = body.getOptionalRaw(3); + checkLength(mac, MAC_BYTES); - // Return the metadata - BdfDictionary d = new BdfDictionary(); - d.put(MAC, mac); - d.put(SIGNATURE, sig); - return d; + 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 BdfDictionary validateAbort(BdfList message) - throws FormatException { + private BdfMessageContext validateOtherMessage(MessageType type, + Message m, BdfList body) throws FormatException { + checkSize(body, 3); + + byte[] sessionIdBytes = body.getRaw(1); + checkLength(sessionIdBytes, UniqueId.LENGTH); - checkSize(message, 2); + byte[] previousMessageId = body.getOptionalRaw(2); + checkLength(previousMessageId, UniqueId.LENGTH); - // Return the metadata - return new BdfDictionary(); + 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 0000000000000000000000000000000000000000..1327b54a938150a5c14019ae5db11a9dab162934 --- /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<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..fb3d66f38012129401d21453d63b9ab431be13d1 --- /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<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..102d72bfc3fc0634c39a915f41c9efc2cfa1cd7b --- /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 0000000000000000000000000000000000000000..503dd4cc66077228053a147ac1e5dc92b546cfee --- /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 0000000000000000000000000000000000000000..69ddd242d1cab3137d7e312905d16388362a629f --- /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<TransportId, TransportProperties> 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 7848aaf3dcd4fadba978f7cf576e763873c174d5..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..67365399b0bc22a8627bdf6bba31355116be22ab --- /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 0000000000000000000000000000000000000000..3c453d2fa9e8cc311623768c48362f65b197a2ad --- /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 0000000000000000000000000000000000000000..e3766c91a4556b3bde9e40c2371079a229ef4da7 --- /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 extends Session> { + + 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 0000000000000000000000000000000000000000..743e87af872d898f8535b9311d60eb9650622562 --- /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 0000000000000000000000000000000000000000..086dfb1a2975defaf177206dbce5cb65cac7edcb --- /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<S extends State> { + + 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 0000000000000000000000000000000000000000..70cfff1bba1077426bc4e087cf60a6627829ae3b --- /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 0000000000000000000000000000000000000000..9023f0d94d3ebb466b9229038766c7d903dd6bf4 --- /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<TransportId, KeySetId> keys) { + if (keys == null) return null; + BdfDictionary d = new BdfDictionary(); + for (Map.Entry<TransportId, KeySetId> 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 0000000000000000000000000000000000000000..c58cac3d951012cbd1298f2283b97e2747f13681 --- /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 0000000000000000000000000000000000000000..52c12e5477ae040cc77be6193eeed5108d600662 --- /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<TransportId, KeySetId> 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<TransportId, TransportProperties> 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<TransportId, TransportProperties> 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<TransportId, KeySetId> parseTransportKeys( + @Nullable BdfDictionary d) throws FormatException { + if (d == null) return null; + Map<TransportId, KeySetId> 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 0000000000000000000000000000000000000000..3063f9bd83d1761fc6c4b5446a482b00f0329f80 --- /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 0a11b0fea447cbb09a53982ae98db25d70c047ba..0000000000000000000000000000000000000000 --- 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<MessageValidator> 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<MessageValidator> 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<MessageValidator> 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<IncomingMessageHook> 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<IncomingMessageHook> 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<IncomingMessageHook> 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<IncomingMessageHook> 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 5f8391d9b8be793e3c53290c1b6ad3be1d18e3b5..0000000000000000000000000000000000000000 --- 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 558b26ec9b2fdb9452676dc54e4b86ff8bf2418c..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..81c2ae01c3c6a80389b5bc793615006f4a6df1a2 --- /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<TransportId, TransportProperties> aliceTransport = + getTransportPropertiesMap(3); + private final Map<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..f966aa2cb06531c1c3b241bdd112c10d23620f0b --- /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 9b485e5c42b1ce13b9ab9414cb90eb48de20432d..df0d46b8811e60e6f8f58919ef388a590fd4363e 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<IntroductionIntegrationTestComponent> { - @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<IntroductionMessage> 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(); - - // get contact group - Group group = - introductionGroupFactory.createIntroductionGroup(contact1From0); - - // 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)) - ); + // 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()); - // reset request received state - listener1.requestReceived = false; + // sync ACCEPT messages back to introducer + sync1To0(1, true); + sync2To0(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 forwarded ACCEPT messages to introducees + sync0To1(1, true); + sync0To2(1, true); - // actually send message - sync0To1(1, false); + // sync first AUTH and its forward + sync1To0(1, true); + sync0To2(1, true); - // make sure it does not arrive - assertFalse(listener1.requestReceived); + // 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); + + // 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<MessageId, Metadata> 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<MessageId, Metadata> 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<MessageId, BdfDictionary> 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<MessageId, BdfDictionary> 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<Contact> 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<Contact> 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) + 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 AbstractIntroductionMessage getMessageFor(ClientHelper ch, + Contact contact, MessageType type) 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 Entry<MessageId, BdfDictionary> getMessageFor(ClientHelper ch, - Contact contact, long type) throws FormatException, DbException { - Entry<MessageId, BdfDictionary> response = null; - Group g = introductionGroupFactory - .createIntroductionGroup(contact); + Group g = introductionManager0.getContactGroup(contact); + BdfDictionary query = BdfDictionary.of( + new BdfEntry(MSG_KEY_MESSAGE_TYPE, type.getValue()) + ); Map<MessageId, BdfDictionary> map = - ch.getMessageMetadataAsDictionary(g.getId()); - for (Entry<MessageId, BdfDictionary> 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<MessageId, BdfDictionary> 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<MessageId, BdfDictionary> 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 bc0ea62410713a6ae9f4a3f5034c8e10c187111b..3a90d7d148af021f0744cf0a3f67502c324e2f78 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 e445910201364626e62d3d8b74b535d81ebd7d1c..0000000000000000000000000000000000000000 --- 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<MessageId, BdfDictionary> metadata = Collections.emptyMap(); - Collection<MessageStatus> 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 a2d481547c7c75e2e53c490f51ac4299bc10997f..4f52aa4de9f83d9df7cb4e5b735bf3557e82c524 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 MessageEncoder messageEncoder = + context.mock(MessageEncoder.class); + private final IntroductionValidator validator = + new IntroductionValidator(messageEncoder, clientHelper, + metadataEncoder, clock); + + 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); - private final Mockery context = new Mockery(); - private final Group group; - private final Message message; - private final IntroductionValidator validator; - private final Clock clock = new SystemClock(); + // + // Introduction REQUEST + // - 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); + @Test + public void testAcceptsRequest() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), previousMsgId.getBytes(), + authorList, text); + expectParseAuthor(authorList, author); + expectEncodeRequestMetadata(); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); - ClientHelper clientHelper = context.mock(ClientHelper.class); - MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class); - validator = new IntroductionValidator(clientHelper, metadataEncoder, - clock); - context.assertIsSatisfied(); + assertExpectedContext(messageContext, previousMsgId); } - // - // Introduction Requests - // + @Test + public void testAcceptsRequestWithPreviousMsgIdNull() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, text); + + expectParseAuthor(authorList, author); + expectEncodeRequestMetadata(); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); + + assertExpectedContext(messageContext, null); + } @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 testAcceptsRequestWithMessageNull() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, null); - 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(); + assertExpectedContext(messageContext, null); + } - 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 testRejectsTooShortBodyForRequest() throws Exception { + BdfList body = BdfList.of(REQUEST.getValue(), null, authorList); + validator.validateMessage(message, group, body); } @Test(expected = FormatException.class) - public void testValidateIntroductionRequestWithNoName() throws Exception { - BdfDictionary msg = getValidIntroductionRequest(); + public void testRejectsTooLongBodyForRequest() throws Exception { + BdfList body = + BdfList.of(REQUEST.getValue(), null, authorList, text, null); + validator.validateMessage(message, group, body); + } - // 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)); + @Test(expected = FormatException.class) + public void testRejectsRawMessageForRequest() throws Exception { + BdfList body = + BdfList.of(REQUEST.getValue(), null, authorList, getRandomId()); + expectParseAuthor(authorList, author); + validator.validateMessage(message, group, body); + } + @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 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)); + 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 testValidateIntroductionRequestWithWrongType() - throws Exception { - // wrong message type - BdfDictionary msg = getValidIntroductionRequest(); - msg.put(TYPE, 324234); + 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); + } - 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)); + @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); } - private BdfDictionary getValidIntroductionRequest() throws Exception { - byte[] sessionId = getRandomId(); - Author author = getAuthor(); - String text = getRandomString(MAX_MESSAGE_BODY_LENGTH); + @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); + } - 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); + @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); + } - return msg; + @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); } // - // Introduction Responses + // Introduction DECLINE // @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(); + 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 - public void testValidateIntroductionDeclineResponse() throws Exception { - BdfDictionary msg = getValidIntroductionResponse(false); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getBoolean(ACCEPT)); + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForDecline() throws Exception { + BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes()); + validator.validateMessage(message, group, body); + } - BdfDictionary result = validator.validateMessage(message, group, body) - .getDictionary(); + @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); + } - assertFalse(result.getBoolean(ACCEPT)); - context.assertIsSatisfied(); + @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 testValidateIntroductionResponseWithoutAccept() - throws Exception { - BdfDictionary msg = getValidIntroductionResponse(false); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID)); + 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 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); + public void testRejectsTooLongBodyForAuth() throws Exception { + BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac, signature, null); + validator.validateMessage(message, group, body); + } - 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)); + @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 testValidateIntroductionResponseWithoutPublicKey() - throws Exception { - BdfDictionary msg = getValidIntroductionResponse(true); + 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); + } - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID), - msg.getBoolean(ACCEPT), msg.getLong(TIME), - msg.getDictionary(TRANSPORT)); + @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); } - private BdfDictionary getValidIntroductionResponse(boolean accept) - throws Exception { + @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); + } - 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, accept); - if (accept) { - msg.put(TIME, time); - msg.put(E_PUBLIC_KEY, publicKey); - msg.put(TRANSPORT, tp); - } + @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); + } - return msg; + @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 ACK + // Introduction ACTIVATE // @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); + public void testAcceptsActivate() throws Exception { + BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), + previousMsgId.getBytes(), mac); - BdfDictionary result = - validator.validateMessage(message, group, body).getDictionary(); + expectEncodeMetadata(ACTIVATE); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); - 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(); + assertExpectedContext(messageContext, previousMsgId); } @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")); + 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 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)); + 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 + // Introduction ABORT // @Test - public void testValidateProperIntroductionAbort() throws Exception { - byte[] sessionId = getRandomId(); + public void testAcceptsAbort() throws Exception { + BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), + previousMsgId.getBytes()); - BdfDictionary msg = new BdfDictionary(); - msg.put(TYPE, TYPE_ABORT); - msg.put(SESSION_ID, sessionId); + expectEncodeMetadata(ABORT); + BdfMessageContext messageContext = + validator.validateMessage(message, group, body); - BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID)); + assertExpectedContext(messageContext, previousMsgId); + } - BdfDictionary result = - validator.validateMessage(message, group, body).getDictionary(); + @Test(expected = FormatException.class) + public void testRejectsTooShortBodyForAbort() throws Exception { + BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes()); + validator.validateMessage(message, group, body); + } - assertEquals(Long.valueOf(TYPE_ABORT), result.getLong(TYPE)); - assertEquals(sessionId, result.getRaw(SESSION_ID)); - context.assertIsSatisfied(); + @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 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")); + 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()); + } + } + } 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 0000000000000000000000000000000000000000..7b15b6ab43e665f52c99af222f833298a758c161 --- /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<TransportId, TransportProperties> 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 0000000000000000000000000000000000000000..56fe04bdb3219547caa2e01a80b1fbb0ccc94216 --- /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 1f922d70644fef119fa0b506dd25707ffa46b9d4..0000000000000000000000000000000000000000 --- 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 0000000000000000000000000000000000000000..9ddb006323c4f5f28c9c36a59a80c53b9d1015fb --- /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<TransportId, TransportProperties> transportProperties = + getTransportPropertiesMap(3); + private final Map<TransportId, TransportProperties> + remoteTransportProperties = getTransportPropertiesMap(3); + private final Map<TransportId, KeySetId> 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 196ce09ca663ac28a2fe1370bdd96e0de7b67d8a..3148da439fe6f99daf90d57e3ae5a5088a2a2dca 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 157c4ce79d9f89d26cae4fc6e57ae0183522e42d..f001fb68008107c10380a66001fde553cca7e97e 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<MessageId> 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 1b2344d3add2760d94dab0aae8f09babbc052a0d..ecaf60d1b9763e069018b243312948c3906dc80f 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 d29fc0b54167dba4eb80b43c77575fd994925114..9f71087ebd38fc5d2e3ce8883fc9b37c13c089ce 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()); + } + }