Merge branch '474-introduction-client' into 'master'

New Introduction Protocol

Closes #308, #377, #474, and #613

See merge request akwizgran/briar!758
This commit is contained in:
Torsten Grote
2018-04-27 14:43:01 +00:00
84 changed files with 5952 additions and 5518 deletions

View File

@@ -16,4 +16,10 @@ public interface CryptoConstants {
* The maximum length of a signature in bytes. * The maximum length of a signature in bytes.
*/ */
int MAX_SIGNATURE_BYTES = 64; int MAX_SIGNATURE_BYTES = 64;
/**
* The length of a MAC in bytes.
*/
int MAC_BYTES = SecretKey.LENGTH;
} }

View File

@@ -2898,6 +2898,8 @@ abstract class JdbcDatabase implements Database<Connection> {
String sql = "UPDATE outgoingKeys SET active = true" String sql = "UPDATE outgoingKeys SET active = true"
+ " WHERE transportId = ? AND keySetId = ?"; + " WHERE transportId = ? AND keySetId = ?";
ps = txn.prepareStatement(sql); ps = txn.prepareStatement(sql);
ps.setString(1, t.getString());
ps.setInt(2, k.getInt());
int affected = ps.executeUpdate(); int affected = ps.executeUpdate();
if (affected < 0 || affected > 1) throw new DbStateException(); if (affected < 0 || affected > 1) throw new DbStateException();
ps.close(); ps.close();

View File

@@ -1,17 +1,20 @@
package org.briarproject.bramble.test; package org.briarproject.bramble.test;
import org.briarproject.bramble.api.client.ClientHelper; 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.data.MetadataEncoder;
import org.briarproject.bramble.api.identity.Author;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.system.Clock; 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.getClientId;
import static org.briarproject.bramble.test.TestUtils.getGroup; import static org.briarproject.bramble.test.TestUtils.getGroup;
import static org.briarproject.bramble.test.TestUtils.getRandomBytes; import static org.briarproject.bramble.test.TestUtils.getMessage;
import static org.briarproject.bramble.test.TestUtils.getRandomId;
public abstract class ValidatorTestCase extends BrambleMockTestCase { public abstract class ValidatorTestCase extends BrambleMockTestCase {
@@ -24,10 +27,23 @@ public abstract class ValidatorTestCase extends BrambleMockTestCase {
protected final Group group = getGroup(getClientId()); protected final Group group = getGroup(getClientId());
protected final GroupId groupId = group.getId(); protected final GroupId groupId = group.getId();
protected final byte[] descriptor = group.getDescriptor(); protected final byte[] descriptor = group.getDescriptor();
protected final MessageId messageId = new MessageId(getRandomId()); protected final Message message = getMessage(groupId);
protected final long timestamp = 1234567890 * 1000L; protected final MessageId messageId = message.getId();
protected final byte[] raw = getRandomBytes(123); protected final long timestamp = message.getTimestamp();
protected final Message message = protected final byte[] raw = message.getRaw();
new Message(messageId, groupId, timestamp, raw); 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));
}});
}
}

View File

@@ -865,7 +865,8 @@ introductionOnboardingSeen();
"Unknown Request Type"); "Unknown Request Type");
} }
loadMessages(); loadMessages();
} catch (DbException | FormatException e) { } catch (DbException e) {
// TODO use more generic error message
introductionResponseError(); introductionResponseError();
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e); LOG.log(WARNING, e.toString(), e);
@@ -898,11 +899,14 @@ introductionOnboardingSeen();
@DatabaseExecutor @DatabaseExecutor
private void respondToIntroductionRequest(SessionId sessionId, private void respondToIntroductionRequest(SessionId sessionId,
boolean accept, long time) throws DbException, FormatException { boolean accept, long time) throws DbException {
if (accept) { try {
introductionManager.acceptIntroduction(contactId, sessionId, time); introductionManager
} else { .respondToIntroduction(contactId, sessionId, time, accept);
introductionManager.declineIntroduction(contactId, sessionId, time); } catch (ProtocolStateException e) {
if (LOG.isLoggable(WARNING))
LOG.log(WARNING, e.toString(), e);
introductionResponseError();
} }
} }

View File

@@ -2,6 +2,7 @@ package org.briarproject.briar.android.introduction;
import android.content.Context; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBar;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.MenuItem; import android.view.MenuItem;
@@ -11,7 +12,6 @@ import android.widget.ProgressBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.contact.Contact; import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager; 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.app.Activity.RESULT_OK;
import static android.view.View.GONE; import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT; import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.WARNING; 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 public class IntroductionMessageFragment extends BaseFragment
implements TextInputListener { implements TextInputListener {
@@ -125,14 +126,15 @@ public class IntroductionMessageFragment extends BaseFragment
new ContactId(contactId1)); new ContactId(contactId1));
Contact c2 = contactManager.getContact( Contact c2 = contactManager.getContact(
new ContactId(contactId2)); new ContactId(contactId2));
setUpViews(c1, c2); boolean possible = introductionManager.canIntroduce(c1, c2);
setUpViews(c1, c2, possible);
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), 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(() -> { introductionActivity.runOnUiThreadUnlessDestroyed(() -> {
contact1 = c1; contact1 = c1;
contact2 = c2; contact2 = c2;
@@ -147,13 +149,22 @@ public class IntroductionMessageFragment extends BaseFragment
ui.contactName1.setText(c1.getAuthor().getName()); ui.contactName1.setText(c1.getAuthor().getName());
ui.contactName2.setText(c2.getAuthor().getName()); ui.contactName2.setText(c2.getAuthor().getName());
// set button action // hide progress bar
ui.message.setListener(IntroductionMessageFragment.this);
// hide progress bar and show views
ui.progressBar.setVisibility(GONE); 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); ui.message.setSendButtonEnabled(false);
String msg = ui.message.getText().toString(); 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); makeIntroduction(contact1, contact2, msg);
// don't wait for the introduction to be made before finishing activity // don't wait for the introduction to be made before finishing activity
@@ -184,13 +196,14 @@ public class IntroductionMessageFragment extends BaseFragment
introductionActivity.supportFinishAfterTransition(); introductionActivity.supportFinishAfterTransition();
} }
private void makeIntroduction(Contact c1, Contact c2, String msg) { private void makeIntroduction(Contact c1, Contact c2,
@Nullable String msg) {
introductionActivity.runOnDbThread(() -> { introductionActivity.runOnDbThread(() -> {
// actually make the introduction // actually make the introduction
try { try {
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
introductionManager.makeIntroduction(c1, c2, msg, timestamp); introductionManager.makeIntroduction(c1, c2, msg, timestamp);
} catch (DbException | FormatException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
introductionError(); introductionError();
} }
@@ -208,6 +221,7 @@ public class IntroductionMessageFragment extends BaseFragment
private final ProgressBar progressBar; private final ProgressBar progressBar;
private final CircleImageView avatar1, avatar2; private final CircleImageView avatar1, avatar2;
private final TextView contactName1, contactName2; private final TextView contactName1, contactName2;
private final TextView notPossible;
private final TextInputView message; private final TextInputView message;
private ViewHolder(View v) { private ViewHolder(View v) {
@@ -216,6 +230,7 @@ public class IntroductionMessageFragment extends BaseFragment
avatar2 = v.findViewById(R.id.avatarContact2); avatar2 = v.findViewById(R.id.avatarContact2);
contactName1 = v.findViewById(R.id.nameContact1); contactName1 = v.findViewById(R.id.nameContact1);
contactName2 = v.findViewById(R.id.nameContact2); contactName2 = v.findViewById(R.id.nameContact2);
notPossible = v.findViewById(R.id.introductionNotPossibleView);
message = v.findViewById(R.id.introductionMessageView); message = v.findViewById(R.id.introductionMessageView);
} }
} }

View File

@@ -94,13 +94,25 @@
android:layout_gravity="center" android:layout_gravity="center"
tools:visibility="gone"/> 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 <org.briarproject.briar.android.view.LargeTextInputView
android:id="@+id/introductionMessageView" android:id="@+id/introductionMessageView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone"
app:buttonText="@string/introduction_button" app:buttonText="@string/introduction_button"
app:hint="@string/introduction_message_hint" app:hint="@string/introduction_message_hint"
app:maxLines="5"/> app:maxLines="5"
tools:visibility="visible"/>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -144,6 +144,7 @@
<string name="introduction_onboarding_title">Introduce your contacts</string> <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_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_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_title">Introduce Contacts</string>
<string name="introduction_message_hint">Add a message (optional)</string> <string name="introduction_message_hint">Add a message (optional)</string>
<string name="introduction_button">Make Introduction</string> <string name="introduction_button">Make Introduction</string>

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -4,126 +4,29 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_L
public interface IntroductionConstants { 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 * The maximum length of the introducer's optional message to the
* introducees in UTF-8 bytes. * 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 LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID";
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";
/* Introduction Request Action */ String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY";
String PUBLIC_KEY1 = "publicKey1";
String PUBLIC_KEY2 = "publicKey2";
/* Introducee Local State Metadata (without those already defined) */ String LABEL_ALICE_MAC_KEY =
String STORAGE_ID = "storageId";
String INTRODUCER = "introducer";
String LOCAL_AUTHOR_ID = "localAuthorId";
String REMOTE_AUTHOR_ID = "remoteAuthorId";
String OUR_PUBLIC_KEY = "ourEphemeralPublicKey";
String OUR_PRIVATE_KEY = "ourEphemeralPrivateKey";
String OUR_TIME = "ourTime";
String ADDED_CONTACT_ID = "addedContactId";
String NOT_OUR_RESPONSE = "notOurResponse";
String EXISTS = "contactExists";
String REMOTE_AUTHOR_IS_US = "remoteAuthorIsUs";
String ANSWERED = "answered";
String NONCE = "nonce";
String MAC_KEY = "macKey";
String OUR_TRANSPORT = "ourTransport";
String OUR_MAC = "ourMac";
String OUR_SIGNATURE = "ourSignature";
String TASK = "task";
int TASK_ADD_CONTACT = 0;
int TASK_ACTIVATE_CONTACT = 1;
int TASK_ABORT = 2;
/**
* Label for deriving the shared secret.
*/
String SHARED_SECRET_LABEL =
"org.briarproject.briar.introduction/SHARED_SECRET";
/**
* Label for deriving Alice's key binding nonce from the shared secret.
*/
String ALICE_NONCE_LABEL =
"org.briarproject.briar.introduction/ALICE_NONCE";
/**
* Label for deriving Bob's key binding nonce from the shared secret.
*/
String BOB_NONCE_LABEL =
"org.briarproject.briar.introduction/BOB_NONCE";
/**
* Label for deriving Alice's MAC key from the shared secret.
*/
String ALICE_MAC_KEY_LABEL =
"org.briarproject.briar.introduction/ALICE_MAC_KEY"; "org.briarproject.briar.introduction/ALICE_MAC_KEY";
/** String LABEL_BOB_MAC_KEY =
* Label for deriving Bob's MAC key from the shared secret.
*/
String BOB_MAC_KEY_LABEL =
"org.briarproject.briar.introduction/BOB_MAC_KEY"; "org.briarproject.briar.introduction/BOB_MAC_KEY";
/** String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC";
* Label for signing the introduction response.
*/ String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN";
String SIGNING_LABEL =
"org.briarproject.briar.introduction/RESPONSE_SIGNATURE"; String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE";
String LABEL_ACTIVATE_MAC =
"org.briarproject.briar.introduction/ACTIVATE_MAC";
/**
* Label for MACing the introduction response.
*/
String MAC_LABEL = "org.briarproject.briar.introduction/RESPONSE_MAC";
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.api.introduction; 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.Contact;
import org.briarproject.bramble.api.contact.ContactId; import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
@@ -24,25 +23,21 @@ public interface IntroductionManager extends ConversationClient {
/** /**
* The current version of the introduction client. * 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. * Sends two initial introduction messages.
*/ */
void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
long timestamp) throws DbException, FormatException; long timestamp) throws DbException;
/** /**
* Accepts an introduction. * Responds to an introduction.
*/ */
void acceptIntroduction(ContactId contactId, SessionId sessionId, void respondToIntroduction(ContactId contactId, SessionId sessionId,
long timestamp) throws DbException, FormatException; long timestamp, boolean accept) throws DbException;
/**
* Declines an introduction.
*/
void declineIntroduction(ContactId contactId, SessionId sessionId,
long timestamp) throws DbException, FormatException;
/** /**
* Returns all introduction messages for the given contact. * Returns all introduction messages for the given contact.

View File

@@ -8,7 +8,7 @@ import org.briarproject.briar.api.client.SessionId;
import javax.annotation.concurrent.Immutable; 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 @Immutable
@NotNullByDefault @NotNullByDefault
@@ -16,10 +16,10 @@ public class IntroductionMessage extends BaseMessageHeader {
private final SessionId sessionId; private final SessionId sessionId;
private final MessageId messageId; private final MessageId messageId;
private final int role; private final Role role;
IntroductionMessage(SessionId sessionId, MessageId messageId, 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) { boolean seen, boolean read) {
super(messageId, groupId, time, local, sent, seen, read); super(messageId, groupId, time, local, sent, seen, read);
@@ -37,7 +37,7 @@ public class IntroductionMessage extends BaseMessageHeader {
} }
public boolean isIntroducer() { public boolean isIntroducer() {
return role == ROLE_INTRODUCER; return role == INTRODUCER;
} }
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.api.introduction; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
@@ -15,21 +14,19 @@ public class IntroductionRequest extends IntroductionResponse {
@Nullable @Nullable
private final String message; private final String message;
private final boolean answered, exists, introducesOtherIdentity; private final boolean answered, exists;
public IntroductionRequest(SessionId sessionId, MessageId messageId, public IntroductionRequest(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, AuthorId authorId, String name, boolean seen, boolean read, String name, boolean accepted,
boolean accepted, @Nullable String message, boolean answered, @Nullable String message, boolean answered, boolean exists) {
boolean exists, boolean introducesOtherIdentity) {
super(sessionId, messageId, groupId, role, time, local, sent, seen, super(sessionId, messageId, groupId, role, time, local, sent, seen,
read, authorId, name, accepted); read, name, accepted);
this.message = message; this.message = message;
this.answered = answered; this.answered = answered;
this.exists = exists; this.exists = exists;
this.introducesOtherIdentity = introducesOtherIdentity;
} }
@Nullable @Nullable
@@ -44,8 +41,4 @@ public class IntroductionRequest extends IntroductionResponse {
public boolean contactExists() { public boolean contactExists() {
return exists; return exists;
} }
public boolean doesIntroduceOtherIdentity() {
return introducesOtherIdentity;
}
} }

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.api.introduction; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId; import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.api.sync.MessageId; import org.briarproject.bramble.api.sync.MessageId;
@@ -12,19 +11,15 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault @NotNullByDefault
public class IntroductionResponse extends IntroductionMessage { public class IntroductionResponse extends IntroductionMessage {
private final AuthorId remoteAuthorId;
private final String name; private final String name;
private final boolean accepted; private final boolean accepted;
public IntroductionResponse(SessionId sessionId, MessageId messageId, public IntroductionResponse(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, AuthorId remoteAuthorId, String name, boolean seen, boolean read, String name, boolean accepted) {
boolean accepted) {
super(sessionId, messageId, groupId, role, time, local, sent, seen, super(sessionId, messageId, groupId, role, time, local, sent, seen,
read); read);
this.remoteAuthorId = remoteAuthorId;
this.name = name; this.name = name;
this.accepted = accepted; this.accepted = accepted;
} }
@@ -37,7 +32,4 @@ public class IntroductionResponse extends IntroductionMessage {
return accepted; return accepted;
} }
public AuthorId getRemoteAuthorId() {
return remoteAuthorId;
}
} }

View File

@@ -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();
}
}

View File

@@ -1,6 +1,5 @@
package org.briarproject.briar.api.introduction.event; 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.event.Event;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault; import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.client.SessionId;
@@ -11,19 +10,14 @@ import javax.annotation.concurrent.Immutable;
@NotNullByDefault @NotNullByDefault
public class IntroductionAbortedEvent extends Event { public class IntroductionAbortedEvent extends Event {
private final ContactId contactId;
private final SessionId sessionId; private final SessionId sessionId;
public IntroductionAbortedEvent(ContactId contactId, SessionId sessionId) { public IntroductionAbortedEvent(SessionId sessionId) {
this.contactId = contactId;
this.sessionId = sessionId; this.sessionId = sessionId;
} }
public ContactId getContactId() {
return contactId;
}
public SessionId getSessionId() { public SessionId getSessionId() {
return sessionId; return sessionId;
} }
} }

View File

@@ -13,18 +13,12 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.InvalidMessageException; import org.briarproject.bramble.api.sync.InvalidMessageException;
import org.briarproject.bramble.api.sync.Message; import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook; 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 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 @Immutable
@NotNullByDefault @NotNullByDefault
public abstract class BdfIncomingMessageHook implements IncomingMessageHook, public abstract class BdfIncomingMessageHook implements IncomingMessageHook {
IncomingQueueMessageHook {
protected final DatabaseComponent db; protected final DatabaseComponent db;
protected final ClientHelper clientHelper; protected final ClientHelper clientHelper;
@@ -40,6 +34,7 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
/** /**
* Called once for each incoming message that passes validation. * 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. * @throws DbException Should only be used for real database errors.
* If this is thrown, delivery will be attempted again at next startup, * If this is thrown, delivery will be attempted again at next startup,
* whereas if a FormatException is thrown, the message will be permanently * 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) public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
throws DbException, InvalidMessageException { throws DbException, InvalidMessageException {
try { 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) { } catch (FormatException e) {
throw new InvalidMessageException(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);
}
} }

View File

@@ -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);
}
}
}

View File

@@ -1,14 +1,6 @@
package org.briarproject.briar.client; 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.MessageTracker;
import org.briarproject.briar.api.client.QueueMessageFactory;
import javax.inject.Singleton;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
@@ -16,21 +8,6 @@ import dagger.Provides;
@Module @Module
public class BriarClientModule { 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 @Provides
MessageTracker provideMessageTracker(MessageTrackerImpl messageTracker) { MessageTracker provideMessageTracker(MessageTrackerImpl messageTracker) {
return messageTracker; return messageTracker;

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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
);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

@@ -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";
}

View File

@@ -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;
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -2,19 +2,21 @@ package org.briarproject.briar.introduction;
import org.briarproject.bramble.api.FormatException; import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.client.ClientHelper; 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.Contact;
import org.briarproject.bramble.api.contact.ContactId; 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.contact.ContactManager.ContactHook;
import org.briarproject.bramble.api.data.BdfDictionary; 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.BdfList;
import org.briarproject.bramble.api.data.MetadataParser; import org.briarproject.bramble.api.data.MetadataParser;
import org.briarproject.bramble.api.db.DatabaseComponent; import org.briarproject.bramble.api.db.DatabaseComponent;
import org.briarproject.bramble.api.db.DbException; import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.db.NoSuchContactException; import org.briarproject.bramble.api.db.Metadata;
import org.briarproject.bramble.api.db.NoSuchMessageException;
import org.briarproject.bramble.api.db.Transaction; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Client; import org.briarproject.bramble.api.sync.Client;
import org.briarproject.bramble.api.sync.Group; 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.bramble.api.sync.MessageStatus;
import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.SessionId; 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.IntroductionManager;
import org.briarproject.briar.api.introduction.IntroductionMessage; import org.briarproject.briar.api.introduction.IntroductionMessage;
import org.briarproject.briar.api.introduction.IntroductionRequest; import org.briarproject.briar.api.introduction.IntroductionRequest;
import org.briarproject.briar.api.introduction.IntroductionResponse; import org.briarproject.briar.api.introduction.IntroductionResponse;
import org.briarproject.briar.api.introduction.Role;
import org.briarproject.briar.client.ConversationClientImpl; import org.briarproject.briar.client.ConversationClientImpl;
import org.briarproject.briar.introduction.IntroducerSession.Introducee;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.logging.Logger; import java.util.Map.Entry;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import javax.inject.Inject; 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.bramble.api.sync.Group.Visibility.SHARED;
import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.FINISHED; import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT; import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED; import static org.briarproject.briar.introduction.IntroducerState.START;
import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1; import static org.briarproject.briar.introduction.IntroductionConstants.GROUP_KEY_CONTACT_ID;
import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2; import static org.briarproject.briar.introduction.MessageType.ABORT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT; import static org.briarproject.briar.introduction.MessageType.ACCEPT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1; import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2; import static org.briarproject.briar.introduction.MessageType.AUTH;
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1; import static org.briarproject.briar.introduction.MessageType.DECLINE;
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2; import static org.briarproject.briar.introduction.MessageType.REQUEST;
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;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class IntroductionManagerImpl extends ConversationClientImpl class IntroductionManagerImpl extends ConversationClientImpl
implements IntroductionManager, Client, ContactHook { implements IntroductionManager, Client, ContactHook {
private static final Logger LOG = private final ContactGroupFactory contactGroupFactory;
Logger.getLogger(IntroductionManagerImpl.class.getName()); 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 Group localGroup;
private final IntroduceeManager introduceeManager;
private final IntroductionGroupFactory introductionGroupFactory;
@Inject @Inject
IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper, IntroductionManagerImpl(
MetadataParser metadataParser, MessageTracker messageTracker, DatabaseComponent db,
IntroducerManager introducerManager, ClientHelper clientHelper,
IntroduceeManager introduceeManager, MetadataParser metadataParser,
IntroductionGroupFactory introductionGroupFactory) { 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); super(db, clientHelper, metadataParser, messageTracker);
this.introducerManager = introducerManager; this.contactGroupFactory = contactGroupFactory;
this.introduceeManager = introduceeManager; this.contactManager = contactManager;
this.introductionGroupFactory = introductionGroupFactory; 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 @Override
public void createLocalState(Transaction txn) throws DbException { 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; if (db.containsGroup(txn, localGroup.getId())) return;
db.addGroup(txn, localGroup); 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); for (Contact c : db.getContacts(txn)) addingContact(txn, c);
} }
@Override @Override
// TODO adapt to use upcoming ClientVersioning client
public void addingContact(Transaction txn, Contact c) throws DbException { 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 { try {
// Create an introduction group for sending introduction messages clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
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);
} catch (FormatException e) { } catch (FormatException e) {
throw new RuntimeException(e); throw new AssertionError(e);
} }
} }
@Override @Override
public void removingContact(Transaction txn, Contact c) throws DbException { public void removingContact(Transaction txn, Contact c) throws DbException {
GroupId gId = introductionGroupFactory.createLocalGroup().getId(); removeSessionWithIntroducer(txn, c);
abortOrRemoveSessionWithIntroducee(txn, c);
// search for session states where c introduced us // Remove the contact group (all messages will be removed with it)
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);
}
// remove the group (all messages will be removed with it)
// this contact won't get our abort message, but the other will
db.removeGroup(txn, getContactGroup(c)); 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 @Override
protected boolean incomingMessage(Transaction txn, Message m, BdfList body, public Group getContactGroup(Contact c) {
BdfDictionary message) throws DbException, FormatException { return contactGroupFactory
.createContactGroup(CLIENT_ID, CLIENT_VERSION, c);
// Get message data and type
GroupId groupId = m.getGroupId();
long type = message.getLong(TYPE, -1L);
// we are an introducee, need to initialize new state
if (type == TYPE_REQUEST) {
boolean stateExists = true;
try {
getSessionState(txn, groupId, message.getRaw(SESSION_ID), false);
} catch (FormatException e) {
stateExists = false;
}
if (stateExists) throw new FormatException();
BdfDictionary state =
introduceeManager.initialize(txn, groupId, message);
try {
introduceeManager.incomingMessage(txn, state, message);
messageTracker.trackIncomingMessage(txn, m);
} catch (DbException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
introduceeManager.abort(txn, state);
} catch (FormatException e) {
// FIXME necessary?
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
introduceeManager.abort(txn, state);
}
}
// our role can be anything
else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
BdfDictionary state =
getSessionState(txn, groupId, message.getRaw(SESSION_ID));
long role = state.getLong(ROLE, -1L);
try {
if (role == ROLE_INTRODUCER) {
introducerManager.incomingMessage(txn, state, message);
if (type == TYPE_RESPONSE)
messageTracker.trackIncomingMessage(txn, m);
} else if (role == ROLE_INTRODUCEE) {
introduceeManager.incomingMessage(txn, state, message);
if (type == TYPE_RESPONSE && !message.getBoolean(ACCEPT))
messageTracker.trackIncomingMessage(txn, m);
} else {
if (LOG.isLoggable(WARNING))
LOG.warning("Unknown role '" + role + "'");
throw new DbException();
}
} catch (DbException | FormatException e) {
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
else introduceeManager.abort(txn, state);
}
} else {
// the message has been validated, so this should not happen
if(LOG.isLoggable(WARNING)) {
LOG.warning("Unknown message type '" + type + "', deleting...");
}
}
return false;
} }
@Override @Override
public Group getContactGroup(Contact contact) { protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
return introductionGroupFactory.createIntroductionGroup(contact); BdfDictionary bdfMeta) throws DbException, FormatException {
// Parse the metadata
MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
// Look up the session, if there is one
SessionId sessionId = meta.getSessionId();
IntroduceeSession newIntroduceeSession = null;
if (sessionId == null) {
if (meta.getMessageType() != REQUEST) throw new AssertionError();
newIntroduceeSession = createNewIntroduceeSession(txn, m, body);
sessionId = newIntroduceeSession.getSessionId();
}
StoredSession ss = getSession(txn, sessionId);
// Handle the message
Session session;
MessageId storageId;
if (ss == null) {
if (meta.getMessageType() != REQUEST) throw new FormatException();
if (newIntroduceeSession == null) throw new AssertionError();
storageId = createStorageId(txn);
session = handleMessage(txn, m, body, meta.getMessageType(),
newIntroduceeSession, introduceeEngine);
} else {
storageId = ss.storageId;
Role role = sessionParser.getRole(ss.bdfSession);
if (role == INTRODUCER) {
session = handleMessage(txn, m, body, meta.getMessageType(),
sessionParser.parseIntroducerSession(ss.bdfSession),
introducerEngine);
} else if (role == INTRODUCEE) {
session = handleMessage(txn, m, body, meta.getMessageType(),
sessionParser.parseIntroduceeSession(m.getGroupId(),
ss.bdfSession), introduceeEngine);
} else throw new AssertionError();
}
// Store the updated session
storeSession(txn, storageId, session);
return false;
}
private IntroduceeSession createNewIntroduceeSession(Transaction txn,
Message m, BdfList body) throws DbException, FormatException {
ContactId introducerId = getContactId(txn, m.getGroupId());
Author introducer = db.getContact(txn, introducerId).getAuthor();
Author local = identityManager.getLocalAuthor(txn);
Author remote = messageParser.parseRequestMessage(m, body).getAuthor();
if (local.equals(remote)) throw new FormatException();
SessionId sessionId = crypto.getSessionId(introducer, local, remote);
boolean alice = crypto.isAlice(local.getId(), remote.getId());
return IntroduceeSession
.getInitial(m.getGroupId(), sessionId, introducer, alice,
remote);
}
private <S 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();
}
}
@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 {
boolean can = canIntroduce(txn, c1, c2);
db.commitTransaction(txn);
return can;
} catch (FormatException e) {
throw new DbException(e);
} finally {
db.endTransaction(txn);
}
}
private boolean canIntroduce(Transaction txn, Contact c1, Contact c2)
throws DbException, FormatException {
// Look up the session, if there is one
Author introducer = identityManager.getLocalAuthor(txn);
SessionId sessionId =
crypto.getSessionId(introducer, c1.getAuthor(),
c2.getAuthor());
StoredSession ss = getSession(txn, sessionId);
if (ss == null) return true;
IntroducerSession session =
sessionParser.parseIntroducerSession(ss.bdfSession);
return session.getState() == START;
} }
@Override @Override
public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg, public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
long timestamp) throws DbException, FormatException { long timestamp) throws DbException {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp); // Look up the session, if there is one
Group g1 = getContactGroup(c1); Author introducer = identityManager.getLocalAuthor(txn);
Group g2 = getContactGroup(c2); SessionId sessionId =
messageTracker.trackMessage(txn, g1.getId(), timestamp, true); crypto.getSessionId(introducer, c1.getAuthor(),
messageTracker.trackMessage(txn, g2.getId(), timestamp, true); 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); db.commitTransaction(txn);
} catch (FormatException e) {
throw new DbException(e);
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
} }
} }
@Override @Override
public void acceptIntroduction(ContactId contactId, SessionId sessionId, public void respondToIntroduction(ContactId contactId, SessionId sessionId,
long timestamp) throws DbException, FormatException { long timestamp, boolean accept) throws DbException {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
Contact c = db.getContact(txn, contactId); // Look up the session
Group g = getContactGroup(c); StoredSession ss = getSession(txn, sessionId);
BdfDictionary state = if (ss == null) {
getSessionState(txn, g.getId(), sessionId.getBytes()); // Actions from the UI may be based on stale information.
// The contact might just have been deleted, for example.
introduceeManager.acceptIntroduction(txn, state, timestamp); // Throwing a DbException here aborts gracefully.
messageTracker.trackMessage(txn, g.getId(), timestamp, true); 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); db.commitTransaction(txn);
} catch (FormatException e) {
throw new DbException(e);
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
} }
} }
@Override @Override
public void declineIntroduction(ContactId contactId, SessionId sessionId, public Collection<IntroductionMessage> getIntroductionMessages(ContactId c)
long timestamp) throws DbException, FormatException { throws DbException {
List<IntroductionMessage> messages;
Transaction txn = db.startTransaction(false);
try {
Contact c = db.getContact(txn, contactId);
Group g = getContactGroup(c);
BdfDictionary state =
getSessionState(txn, g.getId(), sessionId.getBytes());
introduceeManager.declineIntroduction(txn, state, timestamp);
messageTracker.trackMessage(txn, g.getId(), timestamp, true);
db.commitTransaction(txn);
} finally {
db.endTransaction(txn);
}
}
@Override
public Collection<IntroductionMessage> getIntroductionMessages(
ContactId contactId) throws DbException {
Collection<IntroductionMessage> list = new ArrayList<>();
Map<MessageId, BdfDictionary> metadata;
Collection<MessageStatus> statuses;
Transaction txn = db.startTransaction(true); Transaction txn = db.startTransaction(true);
try { try {
// get messages and their status Contact contact = db.getContact(txn, c);
GroupId g = getContactGroup(db.getContact(txn, contactId)).getId(); GroupId contactGroupId = getContactGroup(contact).getId();
metadata = clientHelper.getMessageMetadataAsDictionary(txn, g); BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
statuses = db.getMessageStatus(txn, contactId, g); Map<MessageId, BdfDictionary> results = clientHelper
.getMessageMetadataAsDictionary(txn, contactGroupId, query);
// turn messages into classes for the UI messages = new ArrayList<>(results.size());
for (MessageStatus s : statuses) { for (Entry<MessageId, BdfDictionary> e : results.entrySet()) {
MessageId messageId = s.getMessageId(); MessageId m = e.getKey();
BdfDictionary msg = metadata.get(messageId); MessageMetadata meta =
if (msg == null) continue; messageParser.parseMetadata(e.getValue());
MessageStatus status = db.getMessageStatus(txn, c, m);
try { StoredSession ss = getSession(txn, meta.getSessionId());
long type = msg.getLong(TYPE); if (ss == null) throw new AssertionError();
if (type == TYPE_ACK || type == TYPE_ABORT) continue; MessageType type = meta.getMessageType();
if (type == REQUEST) {
// get session state messages.add(
SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID)); parseInvitationRequest(txn, contactGroupId, m,
BdfDictionary state = meta, status, ss.bdfSession));
getSessionState(txn, g, sessionId.getBytes()); } else if (type == ACCEPT) {
messages.add(
int role = state.getLong(ROLE).intValue(); parseInvitationResponse(contactGroupId, m, meta,
boolean local; status, ss.bdfSession, true));
long time = msg.getLong(MESSAGE_TIME); } else if (type == DECLINE) {
boolean accepted = msg.getBoolean(ACCEPT, false); messages.add(
boolean read = msg.getBoolean(MSG_KEY_READ, false); parseInvitationResponse(contactGroupId, m, meta,
AuthorId authorId; status, ss.bdfSession, false));
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);
} }
} }
db.commitTransaction(txn); db.commitTransaction(txn);
@@ -438,88 +422,140 @@ class IntroductionManagerImpl extends ConversationClientImpl
} finally { } finally {
db.endTransaction(txn); db.endTransaction(txn);
} }
return list; return messages;
} }
private String getNameForIntroducer(ContactId contactId, private IntroductionRequest parseInvitationRequest(Transaction txn,
BdfDictionary state) throws FormatException { GroupId contactGroupId, MessageId m, MessageMetadata meta,
MessageStatus status, BdfDictionary bdfSession)
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
return state.getString(CONTACT_2);
if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
return state.getString(CONTACT_1);
throw new RuntimeException(
"Contact not part of this introduction session");
}
private AuthorId getAuthorIdForIntroducer(ContactId contactId,
BdfDictionary state) throws FormatException {
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
return new AuthorId(state.getRaw(AUTHOR_ID_2));
if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
return new AuthorId(state.getRaw(AUTHOR_ID_1));
throw new RuntimeException(
"Contact not part of this introduction session");
}
private boolean concernsThisContact(ContactId contactId, MessageId messageId,
BdfDictionary state) throws FormatException {
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) {
return Arrays.equals(state.getRaw(RESPONSE_1, new byte[0]),
messageId.getBytes());
} else {
return Arrays.equals(state.getRaw(RESPONSE_2, new byte[0]),
messageId.getBytes());
}
}
private BdfDictionary getSessionState(Transaction txn, GroupId groupId,
byte[] sessionId, boolean warn)
throws DbException, FormatException { throws DbException, FormatException {
Role role = sessionParser.getRole(bdfSession);
SessionId sessionId;
Author author;
if (role == INTRODUCER) {
IntroducerSession session =
sessionParser.parseIntroducerSession(bdfSession);
sessionId = session.getSessionId();
if (contactGroupId.equals(session.getIntroduceeA().groupId)) {
author = session.getIntroduceeB().author;
} else {
author = session.getIntroduceeA().author;
}
} else if (role == INTRODUCEE) {
IntroduceeSession session = sessionParser
.parseIntroduceeSession(contactGroupId, bdfSession);
sessionId = session.getSessionId();
author = session.getRemote().author;
} else throw new AssertionError();
Message msg = clientHelper.getMessage(txn, m);
if (msg == null) throw new AssertionError();
BdfList body = clientHelper.toList(msg);
RequestMessage rm = messageParser.parseRequestMessage(msg, body);
String message = rm.getMessage();
LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
boolean contactExists = contactManager
.contactExists(txn, rm.getAuthor().getId(),
localAuthor.getId());
return new IntroductionRequest(sessionId, m, contactGroupId,
role, meta.getTimestamp(), meta.isLocal(),
status.isSent(), status.isSeen(), meta.isRead(),
author.getName(), false, message, !meta.isAvailableToAnswer(),
contactExists);
}
private IntroductionResponse parseInvitationResponse(GroupId contactGroupId,
MessageId m, MessageMetadata meta, MessageStatus status,
BdfDictionary bdfSession, boolean accept) throws FormatException {
Role role = sessionParser.getRole(bdfSession);
SessionId sessionId;
Author author;
if (role == INTRODUCER) {
IntroducerSession session =
sessionParser.parseIntroducerSession(bdfSession);
sessionId = session.getSessionId();
if (contactGroupId.equals(session.getIntroduceeA().groupId)) {
author = session.getIntroduceeB().author;
} else {
author = session.getIntroduceeA().author;
}
} else if (role == INTRODUCEE) {
IntroduceeSession session = sessionParser
.parseIntroduceeSession(contactGroupId, bdfSession);
sessionId = session.getSessionId();
author = session.getRemote().author;
} else throw new AssertionError();
return new IntroductionResponse(sessionId, m, contactGroupId,
role, meta.getTimestamp(), meta.isLocal(), status.isSent(),
status.isSeen(), meta.isRead(), author.getName(), accept);
}
private void removeSessionWithIntroducer(Transaction txn,
Contact introducer) throws DbException {
BdfDictionary query = sessionEncoder
.getIntroduceeSessionsByIntroducerQuery(introducer.getAuthor());
Map<MessageId, BdfDictionary> sessions;
try { try {
// See if we can find the state directly for the introducer sessions = clientHelper
BdfDictionary state = clientHelper .getMessageMetadataAsDictionary(txn, localGroup.getId(),
.getMessageMetadataAsDictionary(txn, query);
new MessageId(sessionId)); } catch (FormatException e) {
GroupId g1 = new GroupId(state.getRaw(GROUP_ID_1)); throw new DbException(e);
GroupId g2 = new GroupId(state.getRaw(GROUP_ID_2)); }
if (!g1.equals(groupId) && !g2.equals(groupId)) { for (MessageId id : sessions.keySet()) {
throw new NoSuchMessageException(); db.removeMessage(txn, id);
}
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 (warn && LOG.isLoggable(WARNING))
LOG.warning("No session state found");
throw new FormatException();
} }
} }
private BdfDictionary getSessionState(Transaction txn, GroupId groupId, private void abortOrRemoveSessionWithIntroducee(Transaction txn,
byte[] sessionId) throws DbException, FormatException { Contact c) throws DbException {
BdfDictionary query = sessionEncoder.getIntroducerSessionsQuery();
return getSessionState(txn, groupId, sessionId, true); Map<MessageId, BdfDictionary> sessions;
try {
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();
}
if (s.getIntroduceeA().author.equals(c.getAuthor())) {
abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
s.getIntroduceeB(), localAuthor);
} else if (s.getIntroduceeB().author.equals(c.getAuthor())) {
abortOrRemoveSessionWithIntroducee(txn, s, session.getKey(),
s.getIntroduceeA(), localAuthor);
}
}
} }
private void deleteMessage(Transaction txn, MessageId messageId) private void abortOrRemoveSessionWithIntroducee(Transaction txn,
throws DbException { IntroducerSession s, MessageId storageId, Introducee i,
LocalAuthor localAuthor) throws DbException {
if (db.containsContact(txn, i.author.getId(), localAuthor.getId())) {
IntroducerSession session = introducerEngine.onAbortAction(txn, s);
storeSession(txn, storageId, session);
} else {
db.removeMessage(txn, storageId);
}
}
db.deleteMessage(txn, messageId); private static class StoredSession {
db.deleteMessageMetadata(txn, messageId);
private final MessageId storageId;
private final BdfDictionary bdfSession;
private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
this.storageId = storageId;
this.bdfSession = bdfSession;
}
} }
} }

View File

@@ -4,8 +4,8 @@ import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.contact.ContactManager; import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.data.MetadataEncoder; import org.briarproject.bramble.api.data.MetadataEncoder;
import org.briarproject.bramble.api.lifecycle.LifecycleManager; import org.briarproject.bramble.api.lifecycle.LifecycleManager;
import org.briarproject.bramble.api.sync.ValidationManager;
import org.briarproject.bramble.api.system.Clock; 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.introduction.IntroductionManager;
import org.briarproject.briar.api.messaging.ConversationManager; import org.briarproject.briar.api.messaging.ConversationManager;
@@ -21,22 +21,22 @@ import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT
public class IntroductionModule { public class IntroductionModule {
public static class EagerSingletons { public static class EagerSingletons {
@Inject
IntroductionManager introductionManager;
@Inject @Inject
IntroductionValidator introductionValidator; IntroductionValidator introductionValidator;
@Inject
IntroductionManager introductionManager;
} }
@Provides @Provides
@Singleton @Singleton
IntroductionValidator provideValidator( IntroductionValidator provideValidator(ValidationManager validationManager,
MessageQueueManager messageQueueManager, MessageEncoder messageEncoder, MetadataEncoder metadataEncoder,
MetadataEncoder metadataEncoder, ClientHelper clientHelper, ClientHelper clientHelper, Clock clock) {
Clock clock) {
IntroductionValidator introductionValidator = new IntroductionValidator( IntroductionValidator introductionValidator =
clientHelper, metadataEncoder, clock); new IntroductionValidator(messageEncoder, clientHelper,
messageQueueManager.registerMessageValidator(CLIENT_ID, metadataEncoder, clock);
validationManager.registerMessageValidator(CLIENT_ID,
introductionValidator); introductionValidator);
return introductionValidator; return introductionValidator;
@@ -46,16 +46,42 @@ public class IntroductionModule {
@Singleton @Singleton
IntroductionManager provideIntroductionManager( IntroductionManager provideIntroductionManager(
LifecycleManager lifecycleManager, ContactManager contactManager, LifecycleManager lifecycleManager, ContactManager contactManager,
MessageQueueManager messageQueueManager, ValidationManager validationManager,
ConversationManager conversationManager, ConversationManager conversationManager,
IntroductionManagerImpl introductionManager) { IntroductionManagerImpl introductionManager) {
lifecycleManager.registerClient(introductionManager); lifecycleManager.registerClient(introductionManager);
contactManager.registerContactHook(introductionManager); contactManager.registerContactHook(introductionManager);
messageQueueManager.registerIncomingMessageHook(CLIENT_ID, validationManager.registerIncomingMessageHook(CLIENT_ID,
introductionManager); introductionManager);
conversationManager.registerConversationClient(introductionManager); conversationManager.registerConversationClient(introductionManager);
return 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;
}
} }

View File

@@ -1,7 +1,9 @@
package org.briarproject.briar.introduction; package org.briarproject.briar.introduction;
import org.briarproject.bramble.api.FormatException; 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.BdfMessageContext;
import org.briarproject.bramble.api.client.BdfMessageValidator;
import org.briarproject.bramble.api.client.ClientHelper; import org.briarproject.bramble.api.client.ClientHelper;
import org.briarproject.bramble.api.data.BdfDictionary; import org.briarproject.bramble.api.data.BdfDictionary;
import org.briarproject.bramble.api.data.BdfList; 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.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.sync.Group; import org.briarproject.bramble.api.sync.Group;
import org.briarproject.bramble.api.sync.Message; 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.system.Clock;
import org.briarproject.briar.api.client.SessionId; import org.briarproject.briar.api.client.SessionId;
import org.briarproject.briar.client.BdfQueueMessageValidator;
import java.util.Collections;
import javax.annotation.concurrent.Immutable; import javax.annotation.concurrent.Immutable;
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; 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_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.checkLength;
import static org.briarproject.bramble.util.ValidationUtils.checkSize; 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.MAX_REQUEST_MESSAGE_LENGTH;
import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; import static org.briarproject.briar.introduction.MessageType.ACCEPT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; import static org.briarproject.briar.introduction.MessageType.AUTH;
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;
@Immutable @Immutable
@NotNullByDefault @NotNullByDefault
class IntroductionValidator extends BdfQueueMessageValidator { class IntroductionValidator extends BdfMessageValidator {
IntroductionValidator(ClientHelper clientHelper, private final MessageEncoder messageEncoder;
MetadataEncoder metadataEncoder, Clock clock) {
IntroductionValidator(MessageEncoder messageEncoder,
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock) {
super(clientHelper, metadataEncoder, clock); super(clientHelper, metadataEncoder, clock);
this.messageEncoder = messageEncoder;
} }
@Override @Override
protected BdfMessageContext validateMessage(Message m, Group g, protected BdfMessageContext validateMessage(Message m, Group g,
BdfList body) throws FormatException { BdfList body) throws FormatException {
MessageType type = MessageType.fromValue(body.getLong(0).intValue());
BdfDictionary d; switch (type) {
long type = body.getLong(0); case REQUEST:
byte[] id = body.getRaw(1); return validateRequestMessage(m, body);
checkLength(id, SessionId.LENGTH); case ACCEPT:
return validateAcceptMessage(m, body);
case AUTH:
return validateAuthMessage(m, body);
case ACTIVATE:
return validateActivateMessage(m, body);
case DECLINE:
case ABORT:
return validateOtherMessage(type, m, body);
default:
throw new FormatException();
}
}
if (type == TYPE_REQUEST) { private BdfMessageContext validateRequestMessage(Message m, BdfList body)
d = validateRequest(body); throws FormatException {
} else if (type == TYPE_RESPONSE) { checkSize(body, 4);
d = validateResponse(body);
} else if (type == TYPE_ACK) { byte[] previousMessageId = body.getOptionalRaw(1);
d = validateAck(body); checkLength(previousMessageId, UniqueId.LENGTH);
} else if (type == TYPE_ABORT) {
d = validateAbort(body); 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 { } 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 { 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 byte[] ephemeralPublicKey = body.getRaw(3);
String name = message.getString(2); checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
// parse contact's public key long timestamp = body.getLong(4);
byte[] key = message.getRaw(3); if (timestamp < 0) throw new FormatException();
checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH);
// parse (optional) message BdfDictionary transportProperties = body.getDictionary(5);
String msg = null; if (transportProperties.size() < 1) throw new FormatException();
if (message.size() == 5) { clientHelper
msg = message.getString(4); .parseAndValidateTransportPropertiesMap(transportProperties);
checkLength(msg, 0, MAX_INTRODUCTION_MESSAGE_LENGTH);
}
// Return the metadata SessionId sessionId = new SessionId(sessionIdBytes);
BdfDictionary d = new BdfDictionary(); BdfDictionary meta = messageEncoder
d.put(NAME, name); .encodeMetadata(ACCEPT, sessionId, m.getTimestamp(), false,
d.put(PUBLIC_KEY, key); false, false);
if (msg != null) { if (previousMessageId == null) {
d.put(MSG, msg); return new BdfMessageContext(meta);
}
return d;
}
private BdfDictionary validateResponse(BdfList message)
throws FormatException {
checkSize(message, 3, 6);
// parse accept/decline
boolean accept = message.getBoolean(2);
long time = 0;
byte[] pubkey = null;
BdfDictionary tp = new BdfDictionary();
if (accept) {
checkSize(message, 6);
// parse timestamp
time = message.getLong(3);
// parse ephemeral public key
pubkey = message.getRaw(4);
checkLength(pubkey, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES);
// parse transport properties
tp = message.getDictionary(5);
if (tp.size() < 1) throw new FormatException();
for (String tId : tp.keySet()) {
checkLength(tId, 1, MAX_TRANSPORT_ID_LENGTH);
BdfDictionary tProps = tp.getDictionary(tId);
checkSize(tProps, 0, MAX_PROPERTIES_PER_TRANSPORT);
for (String propId : tProps.keySet()) {
checkLength(propId, 0, MAX_PROPERTY_LENGTH);
String prop = tProps.getString(propId);
checkLength(prop, 0, MAX_PROPERTY_LENGTH);
}
}
} else { } else {
checkSize(message, 3); MessageId dependency = new MessageId(previousMessageId);
return new BdfMessageContext(meta,
Collections.singletonList(dependency));
} }
// Return the metadata
BdfDictionary d = new BdfDictionary();
d.put(ACCEPT, accept);
if (accept) {
d.put(TIME, time);
d.put(E_PUBLIC_KEY, pubkey);
d.put(TRANSPORT, tp);
}
return d;
} }
private BdfDictionary validateAck(BdfList message) throws FormatException { private BdfMessageContext validateAuthMessage(Message m, BdfList body)
checkSize(message, 4);
byte[] mac = message.getRaw(2);
checkLength(mac, 1, MAC_LENGTH);
byte[] sig = message.getRaw(3);
checkLength(sig, 1, MAX_SIGNATURE_LENGTH);
// Return the metadata
BdfDictionary d = new BdfDictionary();
d.put(MAC, mac);
d.put(SIGNATURE, sig);
return d;
}
private BdfDictionary validateAbort(BdfList message)
throws FormatException { throws FormatException {
checkSize(body, 5);
checkSize(message, 2); byte[] sessionIdBytes = body.getRaw(1);
checkLength(sessionIdBytes, UniqueId.LENGTH);
// Return the metadata byte[] previousMessageId = body.getRaw(2);
return new BdfDictionary(); checkLength(previousMessageId, UniqueId.LENGTH);
byte[] mac = body.getRaw(3);
checkLength(mac, MAC_BYTES);
byte[] signature = body.getRaw(4);
checkLength(signature, 1, MAX_SIGNATURE_BYTES);
SessionId sessionId = new SessionId(sessionIdBytes);
BdfDictionary meta = messageEncoder
.encodeMetadata(AUTH, sessionId, m.getTimestamp(), false, false,
false);
MessageId dependency = new MessageId(previousMessageId);
return new BdfMessageContext(meta,
Collections.singletonList(dependency));
} }
private BdfMessageContext validateActivateMessage(Message m, BdfList body)
throws FormatException {
checkSize(body, 4);
byte[] sessionIdBytes = body.getRaw(1);
checkLength(sessionIdBytes, UniqueId.LENGTH);
byte[] previousMessageId = body.getRaw(2);
checkLength(previousMessageId, UniqueId.LENGTH);
byte[] mac = body.getOptionalRaw(3);
checkLength(mac, MAC_BYTES);
SessionId sessionId = new SessionId(sessionIdBytes);
BdfDictionary meta = messageEncoder
.encodeMetadata(ACTIVATE, sessionId, m.getTimestamp(), false,
false, false);
if (previousMessageId == null) {
return new BdfMessageContext(meta);
} else {
MessageId dependency = new MessageId(previousMessageId);
return new BdfMessageContext(meta,
Collections.singletonList(dependency));
}
}
private BdfMessageContext validateOtherMessage(MessageType type,
Message m, BdfList body) throws FormatException {
checkSize(body, 3);
byte[] sessionIdBytes = body.getRaw(1);
checkLength(sessionIdBytes, UniqueId.LENGTH);
byte[] previousMessageId = body.getOptionalRaw(2);
checkLength(previousMessageId, UniqueId.LENGTH);
SessionId sessionId = new SessionId(sessionIdBytes);
BdfDictionary meta = messageEncoder
.encodeMetadata(type, sessionId, m.getTimestamp(), false, false,
false);
if (previousMessageId == null) {
return new BdfMessageContext(meta);
} else {
MessageId dependency = new MessageId(previousMessageId);
return new BdfMessageContext(meta,
Collections.singletonList(dependency));
}
}
} }

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
package org.briarproject.briar.introduction;
interface State {
int getValue();
}

View File

@@ -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");
}
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -59,6 +59,13 @@ interface IntroductionIntegrationTestComponent
void inject(IntroductionIntegrationTest init); 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();
} }

View File

@@ -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());
}
}

View File

@@ -1,361 +1,463 @@
package org.briarproject.briar.introduction; package org.briarproject.briar.introduction;
import org.briarproject.bramble.api.FormatException; 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.BdfDictionary;
import org.briarproject.bramble.api.data.BdfEntry; import org.briarproject.bramble.api.data.BdfEntry;
import org.briarproject.bramble.api.data.BdfList; 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.sync.MessageId;
import org.briarproject.bramble.api.system.Clock; import org.briarproject.bramble.test.ValidatorTestCase;
import org.briarproject.bramble.system.SystemClock;
import org.briarproject.briar.api.client.SessionId; 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 org.junit.Test;
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES; import javax.annotation.Nullable;
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_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.getRandomBytes;
import static org.briarproject.bramble.test.TestUtils.getRandomId; import static org.briarproject.bramble.test.TestUtils.getRandomId;
import static org.briarproject.bramble.util.StringUtils.getRandomString; 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.MAX_REQUEST_MESSAGE_LENGTH;
import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY; import static org.briarproject.briar.introduction.MessageType.ABORT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID; import static org.briarproject.briar.introduction.MessageType.ACCEPT;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC; import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH; import static org.briarproject.briar.introduction.MessageType.AUTH;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH; import static org.briarproject.briar.introduction.MessageType.DECLINE;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG; import static org.briarproject.briar.introduction.MessageType.REQUEST;
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.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class IntroductionValidatorTest extends BriarTestCase { public class IntroductionValidatorTest extends ValidatorTestCase {
private final Mockery context = new Mockery(); private final MessageEncoder messageEncoder =
private final Group group; context.mock(MessageEncoder.class);
private final Message message; private final IntroductionValidator validator =
private final IntroductionValidator validator; new IntroductionValidator(messageEncoder, clientHelper,
private final Clock clock = new SystemClock(); metadataEncoder, clock);
public IntroductionValidatorTest() { private final SessionId sessionId = new SessionId(getRandomId());
group = getGroup(getClientId()); private final MessageId previousMsgId = new MessageId(getRandomId());
MessageId messageId = new MessageId(getRandomId()); private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH);
long timestamp = System.currentTimeMillis(); private final BdfDictionary meta = new BdfDictionary();
byte[] raw = getRandomBytes(123); private final long acceptTimestamp = 42;
message = new Message(messageId, group.getId(), timestamp, raw); private final BdfDictionary transportProperties = BdfDictionary.of(
new BdfEntry("transportId", new BdfDictionary())
);
ClientHelper clientHelper = context.mock(ClientHelper.class); private final byte[] mac = getRandomBytes(MAC_BYTES);
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class); private final byte[] signature = getRandomBytes(MAX_SIGNATURE_BYTES);
validator = new IntroductionValidator(clientHelper, metadataEncoder,
clock);
context.assertIsSatisfied();
}
// //
// Introduction Requests // Introduction REQUEST
// //
@Test @Test
public void testValidateProperIntroductionRequest() throws Exception { public void testAcceptsRequest() throws Exception {
byte[] sessionId = getRandomId(); BdfList body = BdfList.of(REQUEST.getValue(), previousMsgId.getBytes(),
String name = getRandomString(MAX_AUTHOR_NAME_LENGTH); authorList, text);
byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
String text = getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH);
BdfList body = BdfList.of(TYPE_REQUEST, sessionId, expectParseAuthor(authorList, author);
name, publicKey, text); expectEncodeRequestMetadata();
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
BdfDictionary result = assertExpectedContext(messageContext, previousMsgId);
validator.validateMessage(message, group, body).getDictionary();
assertEquals(Long.valueOf(TYPE_REQUEST), result.getLong(TYPE));
assertEquals(sessionId, result.getRaw(SESSION_ID));
assertEquals(name, result.getString(NAME));
assertEquals(publicKey, result.getRaw(PUBLIC_KEY));
assertEquals(text, result.getString(MSG));
context.assertIsSatisfied();
}
@Test(expected = FormatException.class)
public void testValidateIntroductionRequestWithNoName() throws Exception {
BdfDictionary msg = getValidIntroductionRequest();
// no NAME is message
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getRaw(PUBLIC_KEY));
if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testValidateIntroductionRequestWithLongName() throws Exception {
// too long NAME in message
BdfDictionary msg = getValidIntroductionRequest();
msg.put(NAME, msg.get(NAME) + "x");
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testValidateIntroductionRequestWithWrongType()
throws Exception {
// wrong message type
BdfDictionary msg = getValidIntroductionRequest();
msg.put(TYPE, 324234);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
validator.validateMessage(message, group, body);
}
private BdfDictionary getValidIntroductionRequest() throws Exception {
byte[] sessionId = getRandomId();
Author author = getAuthor();
String text = getRandomString(MAX_MESSAGE_BODY_LENGTH);
BdfDictionary msg = new BdfDictionary();
msg.put(TYPE, TYPE_REQUEST);
msg.put(SESSION_ID, sessionId);
msg.put(NAME, author.getName());
msg.put(PUBLIC_KEY, author.getPublicKey());
msg.put(MSG, text);
return msg;
}
//
// Introduction Responses
//
@Test
public void testValidateIntroductionAcceptResponse() throws Exception {
byte[] groupId = getRandomId();
byte[] sessionId = getRandomId();
long time = clock.currentTimeMillis();
byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES);
String transportId =
getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
BdfDictionary tProps = BdfDictionary.of(
new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH),
getRandomString(MAX_PROPERTY_LENGTH))
);
BdfDictionary tp = BdfDictionary.of(
new BdfEntry(transportId, tProps)
);
BdfDictionary msg = new BdfDictionary();
msg.put(TYPE, TYPE_RESPONSE);
msg.put(GROUP_ID, groupId);
msg.put(SESSION_ID, sessionId);
msg.put(ACCEPT, true);
msg.put(TIME, time);
msg.put(E_PUBLIC_KEY, publicKey);
msg.put(TRANSPORT, tp);
BdfList body = BdfList.of(TYPE_RESPONSE, msg.getRaw(SESSION_ID),
msg.getBoolean(ACCEPT), msg.getLong(TIME),
msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));
BdfDictionary result =
validator.validateMessage(message, group, body).getDictionary();
assertEquals(Long.valueOf(TYPE_RESPONSE), result.getLong(TYPE));
assertEquals(sessionId, result.getRaw(SESSION_ID));
assertEquals(true, result.getBoolean(ACCEPT));
assertEquals(publicKey, result.getRaw(E_PUBLIC_KEY));
assertEquals(tp, result.getDictionary(TRANSPORT));
context.assertIsSatisfied();
} }
@Test @Test
public void testValidateIntroductionDeclineResponse() throws Exception { public void testAcceptsRequestWithPreviousMsgIdNull() throws Exception {
BdfDictionary msg = getValidIntroductionResponse(false); BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, text);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getBoolean(ACCEPT));
BdfDictionary result = validator.validateMessage(message, group, body) expectParseAuthor(authorList, author);
.getDictionary(); expectEncodeRequestMetadata();
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
assertFalse(result.getBoolean(ACCEPT)); assertExpectedContext(messageContext, null);
context.assertIsSatisfied(); }
@Test
public void testAcceptsRequestWithMessageNull() throws Exception {
BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, null);
expectParseAuthor(authorList, author);
expectEncodeRequestMetadata();
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
assertExpectedContext(messageContext, null);
} }
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateIntroductionResponseWithoutAccept() public void testRejectsTooShortBodyForRequest() throws Exception {
throws Exception { BdfList body = BdfList.of(REQUEST.getValue(), null, authorList);
BdfDictionary msg = getValidIntroductionResponse(false);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
validator.validateMessage(message, group, body); validator.validateMessage(message, group, body);
} }
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateIntroductionResponseWithBrokenTp() public void testRejectsTooLongBodyForRequest() throws Exception {
throws Exception { BdfList body =
BdfDictionary msg = getValidIntroductionResponse(true); BdfList.of(REQUEST.getValue(), null, authorList, text, null);
BdfDictionary tp = msg.getDictionary(TRANSPORT);
tp.put(
getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH), "X");
msg.put(TRANSPORT, tp);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getBoolean(ACCEPT), msg.getLong(TIME),
msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));
validator.validateMessage(message, group, body); validator.validateMessage(message, group, body);
} }
@Test(expected = FormatException.class) @Test(expected = FormatException.class)
public void testValidateIntroductionResponseWithoutPublicKey() public void testRejectsRawMessageForRequest() throws Exception {
throws Exception { BdfList body =
BdfDictionary msg = getValidIntroductionResponse(true); BdfList.of(REQUEST.getValue(), null, authorList, getRandomId());
expectParseAuthor(authorList, author);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getBoolean(ACCEPT), msg.getLong(TIME),
msg.getDictionary(TRANSPORT));
validator.validateMessage(message, group, body); validator.validateMessage(message, group, body);
} }
private BdfDictionary getValidIntroductionResponse(boolean accept) @Test(expected = FormatException.class)
public void testRejectsStringMessageIdForRequest() throws Exception {
BdfList body =
BdfList.of(REQUEST.getValue(), "NoMessageId", authorList, null);
validator.validateMessage(message, group, body);
}
//
// Introduction ACCEPT
//
@Test
public void testAcceptsAccept() throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
acceptTimestamp, transportProperties);
context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateTransportPropertiesMap(
transportProperties);
}});
expectEncodeMetadata(ACCEPT);
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
assertExpectedContext(messageContext, previousMsgId);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortBodyForAccept() throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(),
getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongBodyForAccept() throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
acceptTimestamp, transportProperties, null);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidSessionIdForAccept() throws Exception {
BdfList body =
BdfList.of(ACCEPT.getValue(), null, previousMsgId.getBytes(),
getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
transportProperties);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidPreviousMsgIdForAccept() throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), 1,
getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
transportProperties);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongPublicKeyForAccept() throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(),
getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
transportProperties);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsNegativeTimestampForAccept() throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
-1, transportProperties);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsEmptyTransportPropertiesForAccept()
throws Exception { throws Exception {
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(),
getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
new BdfDictionary());
validator.validateMessage(message, group, body);
}
byte[] groupId = getRandomId(); //
byte[] sessionId = getRandomId(); // Introduction DECLINE
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(); @Test
msg.put(TYPE, TYPE_RESPONSE); public void testAcceptsDecline() throws Exception {
msg.put(GROUP_ID, groupId); BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
msg.put(SESSION_ID, sessionId); previousMsgId.getBytes());
msg.put(ACCEPT, accept);
if (accept) { expectEncodeMetadata(DECLINE);
msg.put(TIME, time); BdfMessageContext messageContext =
msg.put(E_PUBLIC_KEY, publicKey); validator.validateMessage(message, group, body);
msg.put(TRANSPORT, tp);
assertExpectedContext(messageContext, previousMsgId);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortBodyForDecline() throws Exception {
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes());
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongBodyForDecline() throws Exception {
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), null);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidSessionIdForDecline() throws Exception {
BdfList body =
BdfList.of(DECLINE.getValue(), null, previousMsgId.getBytes());
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidPreviousMsgIdForDecline() throws Exception {
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(), 1);
validator.validateMessage(message, group, body);
}
//
// Introduction AUTH
//
@Test
public void testAcceptsAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac, signature);
expectEncodeMetadata(AUTH);
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
assertExpectedContext(messageContext, previousMsgId);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortBodyForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongBodyForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac, signature, null);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidPreviousMsgIdForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
1, getRandomBytes(MAC_BYTES),
signature);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsPreviousMsgIdNullForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), null,
getRandomBytes(MAC_BYTES), signature);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortMacForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1),
signature);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongMacForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(),
getRandomBytes(MAC_BYTES + 1), signature);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidMacForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), null, signature);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortSignatureForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac, getRandomBytes(0));
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongSignatureForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac,
getRandomBytes(MAX_SIGNATURE_BYTES + 1));
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidSignatureForAuth() throws Exception {
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac, null);
validator.validateMessage(message, group, body);
}
//
// Introduction ACTIVATE
//
@Test
public void testAcceptsActivate() throws Exception {
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac);
expectEncodeMetadata(ACTIVATE);
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
assertExpectedContext(messageContext, previousMsgId);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortBodyForActivate() throws Exception {
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
previousMsgId.getBytes());
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongBodyForActivate() throws Exception {
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), mac, null);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidSessionIdForActivate() throws Exception {
BdfList body =
BdfList.of(ACTIVATE.getValue(), null, previousMsgId.getBytes(),
mac);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidPreviousMsgIdForActivate() throws Exception {
BdfList body =
BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), 1, mac);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsPreviousMsgIdNullForActivate() throws Exception {
BdfList body =
BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), null,
mac);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidMacForActivate() throws Exception {
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1));
validator.validateMessage(message, group, body);
}
//
// Introduction ABORT
//
@Test
public void testAcceptsAbort() throws Exception {
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes());
expectEncodeMetadata(ABORT);
BdfMessageContext messageContext =
validator.validateMessage(message, group, body);
assertExpectedContext(messageContext, previousMsgId);
}
@Test(expected = FormatException.class)
public void testRejectsTooShortBodyForAbort() throws Exception {
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes());
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsTooLongBodyForAbort() throws Exception {
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
previousMsgId.getBytes(), null);
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidSessionIdForAbort() throws Exception {
BdfList body =
BdfList.of(ABORT.getValue(), null, previousMsgId.getBytes());
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testRejectsInvalidPreviousMsgIdForAbort() throws Exception {
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), 1);
validator.validateMessage(message, group, body);
}
//
// Introduction Helper Methods
//
private void expectEncodeRequestMetadata() {
context.checking(new Expectations() {{
oneOf(messageEncoder).encodeRequestMetadata(message.getTimestamp());
will(returnValue(meta));
}});
}
private void expectEncodeMetadata(MessageType type) {
context.checking(new Expectations() {{
oneOf(messageEncoder)
.encodeMetadata(type, sessionId, message.getTimestamp(),
false, false, false);
will(returnValue(meta));
}});
}
private void assertExpectedContext(BdfMessageContext c,
@Nullable MessageId dependency) {
assertEquals(meta, c.getDictionary());
if (dependency == null) {
assertEquals(0, c.getDependencies().size());
} else {
assertEquals(dependency, c.getDependencies().iterator().next());
} }
return msg;
}
//
// Introduction ACK
//
@Test
public void testValidateProperIntroductionAck() throws Exception {
byte[] sessionId = getRandomId();
byte[] mac = getRandomBytes(MAC_LENGTH);
byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH);
BdfList body = BdfList.of(TYPE_ACK, sessionId, mac, sig);
BdfDictionary result =
validator.validateMessage(message, group, body).getDictionary();
assertEquals(Long.valueOf(TYPE_ACK), result.getLong(TYPE));
assertArrayEquals(sessionId, result.getRaw(SESSION_ID));
assertArrayEquals(mac, result.getRaw(MAC));
assertArrayEquals(sig, result.getRaw(SIGNATURE));
context.assertIsSatisfied();
}
@Test(expected = FormatException.class)
public void testValidateTooLongIntroductionAck() throws Exception {
BdfDictionary msg = BdfDictionary.of(
new BdfEntry(TYPE, TYPE_ACK),
new BdfEntry(SESSION_ID, getRandomId()),
new BdfEntry("garbage", getRandomString(255))
);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getString("garbage"));
validator.validateMessage(message, group, body);
}
@Test(expected = FormatException.class)
public void testValidateIntroductionAckWithLongSessionId()
throws Exception {
BdfDictionary msg = BdfDictionary.of(
new BdfEntry(TYPE, TYPE_ACK),
new BdfEntry(SESSION_ID, new byte[SessionId.LENGTH + 1])
);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
validator.validateMessage(message, group, body);
}
//
// Introduction Abort
//
@Test
public void testValidateProperIntroductionAbort() throws Exception {
byte[] sessionId = getRandomId();
BdfDictionary msg = new BdfDictionary();
msg.put(TYPE, TYPE_ABORT);
msg.put(SESSION_ID, sessionId);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
BdfDictionary result =
validator.validateMessage(message, group, body).getDictionary();
assertEquals(Long.valueOf(TYPE_ABORT), result.getLong(TYPE));
assertEquals(sessionId, result.getRaw(SESSION_ID));
context.assertIsSatisfied();
}
@Test(expected = FormatException.class)
public void testValidateTooLongIntroductionAbort() throws Exception {
BdfDictionary msg = BdfDictionary.of(
new BdfEntry(TYPE, TYPE_ABORT),
new BdfEntry(SESSION_ID, getRandomId()),
new BdfEntry("garbage", getRandomString(255))
);
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
msg.getString("garbage"));
validator.validateMessage(message, group, body);
} }
} }

View File

@@ -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());
}
}

View File

@@ -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));
}});
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -368,14 +368,6 @@ public class GroupMessageValidatorTest extends ValidatorTestCase {
.getBoolean(KEY_INITIAL_JOIN_MSG)); .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 { private void expectRejectAuthor(BdfList authorList) throws Exception {
context.checking(new Expectations() {{ context.checking(new Expectations() {{
oneOf(clientHelper).parseAndValidateAuthor(authorList); oneOf(clientHelper).parseAndValidateAuthor(authorList);

View File

@@ -150,7 +150,7 @@ public abstract class SharingValidatorTest extends ValidatorTestCase {
} }
void assertExpectedContext(BdfMessageContext messageContext, void assertExpectedContext(BdfMessageContext messageContext,
@Nullable MessageId previousMsgId) throws FormatException { @Nullable MessageId previousMsgId) {
Collection<MessageId> dependencies = messageContext.getDependencies(); Collection<MessageId> dependencies = messageContext.getDependencies();
if (previousMsgId == null) { if (previousMsgId == null) {
assertTrue(dependencies.isEmpty()); assertTrue(dependencies.isEmpty());

View File

@@ -36,7 +36,10 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
import org.briarproject.briar.blog.BlogModule; import org.briarproject.briar.blog.BlogModule;
import org.briarproject.briar.client.BriarClientModule; import org.briarproject.briar.client.BriarClientModule;
import org.briarproject.briar.forum.ForumModule; import org.briarproject.briar.forum.ForumModule;
import org.briarproject.briar.introduction.IntroductionCryptoIntegrationTest;
import org.briarproject.briar.introduction.IntroductionModule; 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.messaging.MessagingModule;
import org.briarproject.briar.privategroup.PrivateGroupModule; import org.briarproject.briar.privategroup.PrivateGroupModule;
import org.briarproject.briar.privategroup.invitation.GroupInvitationModule; import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;

View File

@@ -1,10 +1,18 @@
package org.briarproject.briar.test; 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.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.bramble.api.sync.GroupId;
import org.briarproject.briar.api.client.MessageTracker; import org.briarproject.briar.api.client.MessageTracker;
import org.briarproject.briar.api.client.MessageTracker.GroupCount; 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; import static org.junit.Assert.assertEquals;
public class BriarTestUtils { public class BriarTestUtils {
@@ -25,4 +33,17 @@ public class BriarTestUtils {
assertEquals(unreadCount, c1.getUnreadCount()); 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());
}
} }