mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-13 03:09:04 +01:00
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:
@@ -16,4 +16,10 @@ public interface CryptoConstants {
|
||||
* The maximum length of a signature in bytes.
|
||||
*/
|
||||
int MAX_SIGNATURE_BYTES = 64;
|
||||
|
||||
/**
|
||||
* The length of a MAC in bytes.
|
||||
*/
|
||||
int MAC_BYTES = SecretKey.LENGTH;
|
||||
|
||||
}
|
||||
|
||||
@@ -2898,6 +2898,8 @@ abstract class JdbcDatabase implements Database<Connection> {
|
||||
String sql = "UPDATE outgoingKeys SET active = true"
|
||||
+ " WHERE transportId = ? AND keySetId = ?";
|
||||
ps = txn.prepareStatement(sql);
|
||||
ps.setString(1, t.getString());
|
||||
ps.setInt(2, k.getInt());
|
||||
int affected = ps.executeUpdate();
|
||||
if (affected < 0 || affected > 1) throw new DbStateException();
|
||||
ps.close();
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
package org.briarproject.bramble.test;
|
||||
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.jmock.Expectations;
|
||||
|
||||
import static org.briarproject.bramble.test.TestUtils.getAuthor;
|
||||
import static org.briarproject.bramble.test.TestUtils.getClientId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getMessage;
|
||||
|
||||
public abstract class ValidatorTestCase extends BrambleMockTestCase {
|
||||
|
||||
@@ -24,10 +27,23 @@ public abstract class ValidatorTestCase extends BrambleMockTestCase {
|
||||
protected final Group group = getGroup(getClientId());
|
||||
protected final GroupId groupId = group.getId();
|
||||
protected final byte[] descriptor = group.getDescriptor();
|
||||
protected final MessageId messageId = new MessageId(getRandomId());
|
||||
protected final long timestamp = 1234567890 * 1000L;
|
||||
protected final byte[] raw = getRandomBytes(123);
|
||||
protected final Message message =
|
||||
new Message(messageId, groupId, timestamp, raw);
|
||||
protected final Message message = getMessage(groupId);
|
||||
protected final MessageId messageId = message.getId();
|
||||
protected final long timestamp = message.getTimestamp();
|
||||
protected final byte[] raw = message.getRaw();
|
||||
protected final Author author = getAuthor();
|
||||
protected final BdfList authorList = BdfList.of(
|
||||
author.getFormatVersion(),
|
||||
author.getName(),
|
||||
author.getPublicKey()
|
||||
);
|
||||
|
||||
}
|
||||
protected void expectParseAuthor(BdfList authorList, Author author)
|
||||
throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).parseAndValidateAuthor(authorList);
|
||||
will(returnValue(author));
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -865,7 +865,8 @@ introductionOnboardingSeen();
|
||||
"Unknown Request Type");
|
||||
}
|
||||
loadMessages();
|
||||
} catch (DbException | FormatException e) {
|
||||
} catch (DbException e) {
|
||||
// TODO use more generic error message
|
||||
introductionResponseError();
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
@@ -898,11 +899,14 @@ introductionOnboardingSeen();
|
||||
|
||||
@DatabaseExecutor
|
||||
private void respondToIntroductionRequest(SessionId sessionId,
|
||||
boolean accept, long time) throws DbException, FormatException {
|
||||
if (accept) {
|
||||
introductionManager.acceptIntroduction(contactId, sessionId, time);
|
||||
} else {
|
||||
introductionManager.declineIntroduction(contactId, sessionId, time);
|
||||
boolean accept, long time) throws DbException {
|
||||
try {
|
||||
introductionManager
|
||||
.respondToIntroduction(contactId, sessionId, time, accept);
|
||||
} catch (ProtocolStateException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
introductionResponseError();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.briar.android.introduction;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
@@ -11,7 +12,6 @@ import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
@@ -33,9 +33,10 @@ import im.delight.android.identicons.IdenticonDrawable;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
|
||||
|
||||
public class IntroductionMessageFragment extends BaseFragment
|
||||
implements TextInputListener {
|
||||
@@ -125,14 +126,15 @@ public class IntroductionMessageFragment extends BaseFragment
|
||||
new ContactId(contactId1));
|
||||
Contact c2 = contactManager.getContact(
|
||||
new ContactId(contactId2));
|
||||
setUpViews(c1, c2);
|
||||
boolean possible = introductionManager.canIntroduce(c1, c2);
|
||||
setUpViews(c1, c2, possible);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setUpViews(Contact c1, Contact c2) {
|
||||
private void setUpViews(Contact c1, Contact c2, boolean possible) {
|
||||
introductionActivity.runOnUiThreadUnlessDestroyed(() -> {
|
||||
contact1 = c1;
|
||||
contact2 = c2;
|
||||
@@ -147,13 +149,22 @@ public class IntroductionMessageFragment extends BaseFragment
|
||||
ui.contactName1.setText(c1.getAuthor().getName());
|
||||
ui.contactName2.setText(c2.getAuthor().getName());
|
||||
|
||||
// set button action
|
||||
ui.message.setListener(IntroductionMessageFragment.this);
|
||||
|
||||
// hide progress bar and show views
|
||||
// hide progress bar
|
||||
ui.progressBar.setVisibility(GONE);
|
||||
ui.message.setSendButtonEnabled(true);
|
||||
ui.message.showSoftKeyboard();
|
||||
|
||||
if (possible) {
|
||||
// set button action
|
||||
ui.message.setListener(IntroductionMessageFragment.this);
|
||||
|
||||
// show views
|
||||
ui.notPossible.setVisibility(GONE);
|
||||
ui.message.setVisibility(VISIBLE);
|
||||
ui.message.setSendButtonEnabled(true);
|
||||
ui.message.showSoftKeyboard();
|
||||
} else {
|
||||
ui.notPossible.setVisibility(VISIBLE);
|
||||
ui.message.setVisibility(GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -175,7 +186,8 @@ public class IntroductionMessageFragment extends BaseFragment
|
||||
ui.message.setSendButtonEnabled(false);
|
||||
|
||||
String msg = ui.message.getText().toString();
|
||||
msg = StringUtils.truncateUtf8(msg, MAX_INTRODUCTION_MESSAGE_LENGTH);
|
||||
if (msg.equals("")) msg = null;
|
||||
else msg = StringUtils.truncateUtf8(msg, MAX_REQUEST_MESSAGE_LENGTH);
|
||||
makeIntroduction(contact1, contact2, msg);
|
||||
|
||||
// don't wait for the introduction to be made before finishing activity
|
||||
@@ -184,13 +196,14 @@ public class IntroductionMessageFragment extends BaseFragment
|
||||
introductionActivity.supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
private void makeIntroduction(Contact c1, Contact c2, String msg) {
|
||||
private void makeIntroduction(Contact c1, Contact c2,
|
||||
@Nullable String msg) {
|
||||
introductionActivity.runOnDbThread(() -> {
|
||||
// actually make the introduction
|
||||
try {
|
||||
long timestamp = System.currentTimeMillis();
|
||||
introductionManager.makeIntroduction(c1, c2, msg, timestamp);
|
||||
} catch (DbException | FormatException e) {
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
introductionError();
|
||||
}
|
||||
@@ -208,6 +221,7 @@ public class IntroductionMessageFragment extends BaseFragment
|
||||
private final ProgressBar progressBar;
|
||||
private final CircleImageView avatar1, avatar2;
|
||||
private final TextView contactName1, contactName2;
|
||||
private final TextView notPossible;
|
||||
private final TextInputView message;
|
||||
|
||||
private ViewHolder(View v) {
|
||||
@@ -216,6 +230,7 @@ public class IntroductionMessageFragment extends BaseFragment
|
||||
avatar2 = v.findViewById(R.id.avatarContact2);
|
||||
contactName1 = v.findViewById(R.id.nameContact1);
|
||||
contactName2 = v.findViewById(R.id.nameContact2);
|
||||
notPossible = v.findViewById(R.id.introductionNotPossibleView);
|
||||
message = v.findViewById(R.id.introductionMessageView);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,13 +94,25 @@
|
||||
android:layout_gravity="center"
|
||||
tools:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/introductionNotPossibleView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="@dimen/margin_activity_horizontal"
|
||||
android:text="@string/introduction_not_possible"
|
||||
android:textSize="@dimen/text_size_large"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<org.briarproject.briar.android.view.LargeTextInputView
|
||||
android:id="@+id/introductionMessageView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:buttonText="@string/introduction_button"
|
||||
app:hint="@string/introduction_message_hint"
|
||||
app:maxLines="5"/>
|
||||
app:maxLines="5"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
@@ -144,6 +144,7 @@
|
||||
<string name="introduction_onboarding_title">Introduce your contacts</string>
|
||||
<string name="introduction_onboarding_text">You can introduce your contacts to each other, so they don\'t need to meet in person to connect on Briar.</string>
|
||||
<string name="introduction_activity_title">Select Contact</string>
|
||||
<string name="introduction_not_possible">You already have one introduction in progress with these contacts. Please allow for this to finish first. If you or your contacts are rarely online, this can take some time.</string>
|
||||
<string name="introduction_message_title">Introduce Contacts</string>
|
||||
<string name="introduction_message_hint">Add a message (optional)</string>
|
||||
<string name="introduction_button">Make Introduction</string>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4,126 +4,29 @@ import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_L
|
||||
|
||||
public interface IntroductionConstants {
|
||||
|
||||
/* Protocol roles */
|
||||
int ROLE_INTRODUCER = 0;
|
||||
int ROLE_INTRODUCEE = 1;
|
||||
|
||||
/* Message types */
|
||||
int TYPE_REQUEST = 1;
|
||||
int TYPE_RESPONSE = 2;
|
||||
int TYPE_ACK = 3;
|
||||
int TYPE_ABORT = 4;
|
||||
|
||||
/* Message Constants */
|
||||
String TYPE = "type";
|
||||
String GROUP_ID = "groupId";
|
||||
String SESSION_ID = "sessionId";
|
||||
String CONTACT = "contactId";
|
||||
String NAME = "name";
|
||||
String PUBLIC_KEY = "publicKey";
|
||||
String E_PUBLIC_KEY = "ephemeralPublicKey";
|
||||
String MSG = "msg";
|
||||
String ACCEPT = "accept";
|
||||
String TIME = "time";
|
||||
String TRANSPORT = "transport";
|
||||
String MESSAGE_ID = "messageId";
|
||||
String MESSAGE_TIME = "timestamp";
|
||||
String MAC = "mac";
|
||||
String SIGNATURE = "signature";
|
||||
|
||||
/* Validation Constants */
|
||||
|
||||
/**
|
||||
* The length of the message authentication code in bytes.
|
||||
*/
|
||||
int MAC_LENGTH = 32;
|
||||
|
||||
/**
|
||||
* The maximum length of the introducer's optional message to the
|
||||
* introducees in UTF-8 bytes.
|
||||
*/
|
||||
int MAX_INTRODUCTION_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
|
||||
int MAX_REQUEST_MESSAGE_LENGTH = MAX_MESSAGE_BODY_LENGTH - 1024;
|
||||
|
||||
/* Introducer Local State Metadata */
|
||||
String STATE = "state";
|
||||
String ROLE = "role";
|
||||
String GROUP_ID_1 = "groupId1";
|
||||
String GROUP_ID_2 = "groupId2";
|
||||
String CONTACT_1 = "contact1";
|
||||
String CONTACT_2 = "contact2";
|
||||
String AUTHOR_ID_1 = "authorId1";
|
||||
String AUTHOR_ID_2 = "authorId2";
|
||||
String CONTACT_ID_1 = "contactId1";
|
||||
String CONTACT_ID_2 = "contactId2";
|
||||
String RESPONSE_1 = "response1";
|
||||
String RESPONSE_2 = "response2";
|
||||
String LABEL_SESSION_ID = "org.briarproject.briar.introduction/SESSION_ID";
|
||||
|
||||
/* Introduction Request Action */
|
||||
String PUBLIC_KEY1 = "publicKey1";
|
||||
String PUBLIC_KEY2 = "publicKey2";
|
||||
String LABEL_MASTER_KEY = "org.briarproject.briar.introduction/MASTER_KEY";
|
||||
|
||||
/* Introducee Local State Metadata (without those already defined) */
|
||||
String STORAGE_ID = "storageId";
|
||||
String INTRODUCER = "introducer";
|
||||
String LOCAL_AUTHOR_ID = "localAuthorId";
|
||||
String REMOTE_AUTHOR_ID = "remoteAuthorId";
|
||||
String OUR_PUBLIC_KEY = "ourEphemeralPublicKey";
|
||||
String OUR_PRIVATE_KEY = "ourEphemeralPrivateKey";
|
||||
String OUR_TIME = "ourTime";
|
||||
String ADDED_CONTACT_ID = "addedContactId";
|
||||
String NOT_OUR_RESPONSE = "notOurResponse";
|
||||
String EXISTS = "contactExists";
|
||||
String REMOTE_AUTHOR_IS_US = "remoteAuthorIsUs";
|
||||
String ANSWERED = "answered";
|
||||
String NONCE = "nonce";
|
||||
String MAC_KEY = "macKey";
|
||||
String OUR_TRANSPORT = "ourTransport";
|
||||
String OUR_MAC = "ourMac";
|
||||
String OUR_SIGNATURE = "ourSignature";
|
||||
|
||||
String TASK = "task";
|
||||
int TASK_ADD_CONTACT = 0;
|
||||
int TASK_ACTIVATE_CONTACT = 1;
|
||||
int TASK_ABORT = 2;
|
||||
|
||||
/**
|
||||
* Label for deriving the shared secret.
|
||||
*/
|
||||
String SHARED_SECRET_LABEL =
|
||||
"org.briarproject.briar.introduction/SHARED_SECRET";
|
||||
|
||||
/**
|
||||
* Label for deriving Alice's key binding nonce from the shared secret.
|
||||
*/
|
||||
String ALICE_NONCE_LABEL =
|
||||
"org.briarproject.briar.introduction/ALICE_NONCE";
|
||||
|
||||
/**
|
||||
* Label for deriving Bob's key binding nonce from the shared secret.
|
||||
*/
|
||||
String BOB_NONCE_LABEL =
|
||||
"org.briarproject.briar.introduction/BOB_NONCE";
|
||||
|
||||
/**
|
||||
* Label for deriving Alice's MAC key from the shared secret.
|
||||
*/
|
||||
String ALICE_MAC_KEY_LABEL =
|
||||
String LABEL_ALICE_MAC_KEY =
|
||||
"org.briarproject.briar.introduction/ALICE_MAC_KEY";
|
||||
|
||||
/**
|
||||
* Label for deriving Bob's MAC key from the shared secret.
|
||||
*/
|
||||
String BOB_MAC_KEY_LABEL =
|
||||
String LABEL_BOB_MAC_KEY =
|
||||
"org.briarproject.briar.introduction/BOB_MAC_KEY";
|
||||
|
||||
/**
|
||||
* Label for signing the introduction response.
|
||||
*/
|
||||
String SIGNING_LABEL =
|
||||
"org.briarproject.briar.introduction/RESPONSE_SIGNATURE";
|
||||
String LABEL_AUTH_MAC = "org.briarproject.briar.introduction/AUTH_MAC";
|
||||
|
||||
String LABEL_AUTH_SIGN = "org.briarproject.briar.introduction/AUTH_SIGN";
|
||||
|
||||
String LABEL_AUTH_NONCE = "org.briarproject.briar.introduction/AUTH_NONCE";
|
||||
|
||||
String LABEL_ACTIVATE_MAC =
|
||||
"org.briarproject.briar.introduction/ACTIVATE_MAC";
|
||||
|
||||
/**
|
||||
* Label for MACing the introduction response.
|
||||
*/
|
||||
String MAC_LABEL = "org.briarproject.briar.introduction/RESPONSE_MAC";
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.api.introduction;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
@@ -24,25 +23,21 @@ public interface IntroductionManager extends ConversationClient {
|
||||
/**
|
||||
* The current version of the introduction client.
|
||||
*/
|
||||
int CLIENT_VERSION = 0;
|
||||
int CLIENT_VERSION = 1;
|
||||
|
||||
boolean canIntroduce(Contact c1, Contact c2) throws DbException;
|
||||
|
||||
/**
|
||||
* Sends two initial introduction messages.
|
||||
*/
|
||||
void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
|
||||
long timestamp) throws DbException, FormatException;
|
||||
long timestamp) throws DbException;
|
||||
|
||||
/**
|
||||
* Accepts an introduction.
|
||||
* Responds to an introduction.
|
||||
*/
|
||||
void acceptIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException, FormatException;
|
||||
|
||||
/**
|
||||
* Declines an introduction.
|
||||
*/
|
||||
void declineIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException, FormatException;
|
||||
void respondToIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp, boolean accept) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all introduction messages for the given contact.
|
||||
|
||||
@@ -8,7 +8,7 @@ import org.briarproject.briar.api.client.SessionId;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
|
||||
import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
@@ -16,10 +16,10 @@ public class IntroductionMessage extends BaseMessageHeader {
|
||||
|
||||
private final SessionId sessionId;
|
||||
private final MessageId messageId;
|
||||
private final int role;
|
||||
private final Role role;
|
||||
|
||||
IntroductionMessage(SessionId sessionId, MessageId messageId,
|
||||
GroupId groupId, int role, long time, boolean local, boolean sent,
|
||||
GroupId groupId, Role role, long time, boolean local, boolean sent,
|
||||
boolean seen, boolean read) {
|
||||
|
||||
super(messageId, groupId, time, local, sent, seen, read);
|
||||
@@ -37,7 +37,7 @@ public class IntroductionMessage extends BaseMessageHeader {
|
||||
}
|
||||
|
||||
public boolean isIntroducer() {
|
||||
return role == ROLE_INTRODUCER;
|
||||
return role == INTRODUCER;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.api.introduction;
|
||||
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
@@ -15,21 +14,19 @@ public class IntroductionRequest extends IntroductionResponse {
|
||||
|
||||
@Nullable
|
||||
private final String message;
|
||||
private final boolean answered, exists, introducesOtherIdentity;
|
||||
private final boolean answered, exists;
|
||||
|
||||
public IntroductionRequest(SessionId sessionId, MessageId messageId,
|
||||
GroupId groupId, int role, long time, boolean local, boolean sent,
|
||||
boolean seen, boolean read, AuthorId authorId, String name,
|
||||
boolean accepted, @Nullable String message, boolean answered,
|
||||
boolean exists, boolean introducesOtherIdentity) {
|
||||
GroupId groupId, Role role, long time, boolean local, boolean sent,
|
||||
boolean seen, boolean read, String name, boolean accepted,
|
||||
@Nullable String message, boolean answered, boolean exists) {
|
||||
|
||||
super(sessionId, messageId, groupId, role, time, local, sent, seen,
|
||||
read, authorId, name, accepted);
|
||||
read, name, accepted);
|
||||
|
||||
this.message = message;
|
||||
this.answered = answered;
|
||||
this.exists = exists;
|
||||
this.introducesOtherIdentity = introducesOtherIdentity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -44,8 +41,4 @@ public class IntroductionRequest extends IntroductionResponse {
|
||||
public boolean contactExists() {
|
||||
return exists;
|
||||
}
|
||||
|
||||
public boolean doesIntroduceOtherIdentity() {
|
||||
return introducesOtherIdentity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.api.introduction;
|
||||
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
@@ -12,19 +11,15 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class IntroductionResponse extends IntroductionMessage {
|
||||
|
||||
private final AuthorId remoteAuthorId;
|
||||
private final String name;
|
||||
private final boolean accepted;
|
||||
|
||||
public IntroductionResponse(SessionId sessionId, MessageId messageId,
|
||||
GroupId groupId, int role, long time, boolean local, boolean sent,
|
||||
boolean seen, boolean read, AuthorId remoteAuthorId, String name,
|
||||
boolean accepted) {
|
||||
|
||||
GroupId groupId, Role role, long time, boolean local, boolean sent,
|
||||
boolean seen, boolean read, String name, boolean accepted) {
|
||||
super(sessionId, messageId, groupId, role, time, local, sent, seen,
|
||||
read);
|
||||
|
||||
this.remoteAuthorId = remoteAuthorId;
|
||||
this.name = name;
|
||||
this.accepted = accepted;
|
||||
}
|
||||
@@ -37,7 +32,4 @@ public class IntroductionResponse extends IntroductionMessage {
|
||||
return accepted;
|
||||
}
|
||||
|
||||
public AuthorId getRemoteAuthorId() {
|
||||
return remoteAuthorId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package org.briarproject.briar.api.introduction.event;
|
||||
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.event.Event;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
@@ -11,19 +10,14 @@ import javax.annotation.concurrent.Immutable;
|
||||
@NotNullByDefault
|
||||
public class IntroductionAbortedEvent extends Event {
|
||||
|
||||
private final ContactId contactId;
|
||||
private final SessionId sessionId;
|
||||
|
||||
public IntroductionAbortedEvent(ContactId contactId, SessionId sessionId) {
|
||||
this.contactId = contactId;
|
||||
public IntroductionAbortedEvent(SessionId sessionId) {
|
||||
this.sessionId = sessionId;
|
||||
}
|
||||
|
||||
public ContactId getContactId() {
|
||||
return contactId;
|
||||
}
|
||||
|
||||
public SessionId getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,18 +13,12 @@ import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.InvalidMessageException;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.ValidationManager.IncomingMessageHook;
|
||||
import org.briarproject.briar.api.client.MessageQueueManager.IncomingQueueMessageHook;
|
||||
import org.briarproject.briar.api.client.QueueMessage;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MESSAGE_HEADER_LENGTH;
|
||||
import static org.briarproject.briar.api.client.QueueMessage.QUEUE_MESSAGE_HEADER_LENGTH;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
|
||||
IncomingQueueMessageHook {
|
||||
public abstract class BdfIncomingMessageHook implements IncomingMessageHook {
|
||||
|
||||
protected final DatabaseComponent db;
|
||||
protected final ClientHelper clientHelper;
|
||||
@@ -40,6 +34,7 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
|
||||
/**
|
||||
* Called once for each incoming message that passes validation.
|
||||
*
|
||||
* @return whether or not this message should be shared
|
||||
* @throws DbException Should only be used for real database errors.
|
||||
* If this is thrown, delivery will be attempted again at next startup,
|
||||
* whereas if a FormatException is thrown, the message will be permanently
|
||||
@@ -60,29 +55,12 @@ public abstract class BdfIncomingMessageHook implements IncomingMessageHook,
|
||||
public boolean incomingMessage(Transaction txn, Message m, Metadata meta)
|
||||
throws DbException, InvalidMessageException {
|
||||
try {
|
||||
return incomingMessage(txn, m, meta, MESSAGE_HEADER_LENGTH);
|
||||
BdfList body = clientHelper.toList(m);
|
||||
BdfDictionary metaDictionary = metadataParser.parse(meta);
|
||||
return incomingMessage(txn, m, body, metaDictionary);
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void incomingMessage(Transaction txn, QueueMessage q, Metadata meta)
|
||||
throws DbException, InvalidMessageException {
|
||||
try {
|
||||
incomingMessage(txn, q, meta, QUEUE_MESSAGE_HEADER_LENGTH);
|
||||
} catch (FormatException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean incomingMessage(Transaction txn, Message m, Metadata meta,
|
||||
int headerLength) throws DbException, FormatException {
|
||||
byte[] raw = m.getRaw();
|
||||
BdfList body = clientHelper.toList(raw, headerLength,
|
||||
raw.length - headerLength);
|
||||
BdfDictionary metaDictionary = metadataParser.parse(meta);
|
||||
return incomingMessage(txn, m, body, metaDictionary);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,6 @@
|
||||
package org.briarproject.briar.client;
|
||||
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.sync.MessageFactory;
|
||||
import org.briarproject.bramble.api.sync.ValidationManager;
|
||||
import org.briarproject.briar.api.client.MessageQueueManager;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.QueueMessageFactory;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
@@ -16,21 +8,6 @@ import dagger.Provides;
|
||||
@Module
|
||||
public class BriarClientModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
MessageQueueManager provideMessageQueueManager(DatabaseComponent db,
|
||||
ClientHelper clientHelper, QueueMessageFactory queueMessageFactory,
|
||||
ValidationManager validationManager) {
|
||||
return new MessageQueueManagerImpl(db, clientHelper,
|
||||
queueMessageFactory, validationManager);
|
||||
}
|
||||
|
||||
@Provides
|
||||
QueueMessageFactory provideQueueMessageFactory(
|
||||
MessageFactory messageFactory) {
|
||||
return new QueueMessageFactoryImpl(messageFactory);
|
||||
}
|
||||
|
||||
@Provides
|
||||
MessageTracker provideMessageTracker(MessageTrackerImpl messageTracker) {
|
||||
return messageTracker;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,19 +2,21 @@ package org.briarproject.briar.introduction;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.client.ContactGroupFactory;
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.contact.ContactManager.ContactHook;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfEntry;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataParser;
|
||||
import org.briarproject.bramble.api.db.DatabaseComponent;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.db.NoSuchContactException;
|
||||
import org.briarproject.bramble.api.db.NoSuchMessageException;
|
||||
import org.briarproject.bramble.api.db.Metadata;
|
||||
import org.briarproject.bramble.api.db.Transaction;
|
||||
import org.briarproject.bramble.api.identity.AuthorId;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.IdentityManager;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Client;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
@@ -24,412 +26,394 @@ import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.sync.MessageStatus;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
import org.briarproject.briar.api.introduction.IntroducerProtocolState;
|
||||
import org.briarproject.briar.api.introduction.IntroductionManager;
|
||||
import org.briarproject.briar.api.introduction.IntroductionMessage;
|
||||
import org.briarproject.briar.api.introduction.IntroductionRequest;
|
||||
import org.briarproject.briar.api.introduction.IntroductionResponse;
|
||||
import org.briarproject.briar.api.introduction.Role;
|
||||
import org.briarproject.briar.client.ConversationClientImpl;
|
||||
import org.briarproject.briar.introduction.IntroducerSession.Introducee;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.briar.api.introduction.IntroduceeProtocolState.FINISHED;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ANSWERED;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_1;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.AUTHOR_ID_2;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_1;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_2;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_1;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.CONTACT_ID_2;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.EXISTS;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_1;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID_2;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.REMOTE_AUTHOR_IS_US;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_1;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.RESPONSE_2;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.STATE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
import static org.briarproject.briar.client.MessageTrackerConstants.MSG_KEY_READ;
|
||||
import static org.briarproject.briar.api.introduction.Role.INTRODUCEE;
|
||||
import static org.briarproject.briar.api.introduction.Role.INTRODUCER;
|
||||
import static org.briarproject.briar.introduction.IntroducerState.START;
|
||||
import static org.briarproject.briar.introduction.IntroductionConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.briar.introduction.MessageType.ABORT;
|
||||
import static org.briarproject.briar.introduction.MessageType.ACCEPT;
|
||||
import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
|
||||
import static org.briarproject.briar.introduction.MessageType.AUTH;
|
||||
import static org.briarproject.briar.introduction.MessageType.DECLINE;
|
||||
import static org.briarproject.briar.introduction.MessageType.REQUEST;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class IntroductionManagerImpl extends ConversationClientImpl
|
||||
implements IntroductionManager, Client, ContactHook {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(IntroductionManagerImpl.class.getName());
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final ContactManager contactManager;
|
||||
private final MessageParser messageParser;
|
||||
private final SessionEncoder sessionEncoder;
|
||||
private final SessionParser sessionParser;
|
||||
private final IntroducerProtocolEngine introducerEngine;
|
||||
private final IntroduceeProtocolEngine introduceeEngine;
|
||||
private final IntroductionCrypto crypto;
|
||||
private final IdentityManager identityManager;
|
||||
|
||||
private final IntroducerManager introducerManager;
|
||||
private final IntroduceeManager introduceeManager;
|
||||
private final IntroductionGroupFactory introductionGroupFactory;
|
||||
private final Group localGroup;
|
||||
|
||||
@Inject
|
||||
IntroductionManagerImpl(DatabaseComponent db, ClientHelper clientHelper,
|
||||
MetadataParser metadataParser, MessageTracker messageTracker,
|
||||
IntroducerManager introducerManager,
|
||||
IntroduceeManager introduceeManager,
|
||||
IntroductionGroupFactory introductionGroupFactory) {
|
||||
|
||||
IntroductionManagerImpl(
|
||||
DatabaseComponent db,
|
||||
ClientHelper clientHelper,
|
||||
MetadataParser metadataParser,
|
||||
MessageTracker messageTracker,
|
||||
ContactGroupFactory contactGroupFactory,
|
||||
ContactManager contactManager,
|
||||
MessageParser messageParser,
|
||||
SessionEncoder sessionEncoder,
|
||||
SessionParser sessionParser,
|
||||
IntroducerProtocolEngine introducerEngine,
|
||||
IntroduceeProtocolEngine introduceeEngine,
|
||||
IntroductionCrypto crypto,
|
||||
IdentityManager identityManager) {
|
||||
super(db, clientHelper, metadataParser, messageTracker);
|
||||
this.introducerManager = introducerManager;
|
||||
this.introduceeManager = introduceeManager;
|
||||
this.introductionGroupFactory = introductionGroupFactory;
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.contactManager = contactManager;
|
||||
this.messageParser = messageParser;
|
||||
this.sessionEncoder = sessionEncoder;
|
||||
this.sessionParser = sessionParser;
|
||||
this.introducerEngine = introducerEngine;
|
||||
this.introduceeEngine = introduceeEngine;
|
||||
this.crypto = crypto;
|
||||
this.identityManager = identityManager;
|
||||
this.localGroup =
|
||||
contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLocalState(Transaction txn) throws DbException {
|
||||
Group localGroup = introductionGroupFactory.createLocalGroup();
|
||||
// Create a local group to store protocol sessions
|
||||
if (db.containsGroup(txn, localGroup.getId())) return;
|
||||
db.addGroup(txn, localGroup);
|
||||
// Ensure we've set things up for any pre-existing contacts
|
||||
// Set up groups for communication with any pre-existing contacts
|
||||
for (Contact c : db.getContacts(txn)) addingContact(txn, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
// TODO adapt to use upcoming ClientVersioning client
|
||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||
// Create a group to share with the contact
|
||||
Group g = getContactGroup(c);
|
||||
// Store the group and share it with the contact
|
||||
db.addGroup(txn, g);
|
||||
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||
try {
|
||||
// Create an introduction group for sending introduction messages
|
||||
Group g = getContactGroup(c);
|
||||
// Return if we've already set things up for this contact
|
||||
if (db.containsGroup(txn, g.getId())) return;
|
||||
// Store the group and share it with the contact
|
||||
db.addGroup(txn, g);
|
||||
db.setGroupVisibility(txn, c.getId(), g.getId(), SHARED);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary gm = new BdfDictionary();
|
||||
gm.put(CONTACT, c.getId().getInt());
|
||||
clientHelper.mergeGroupMetadata(txn, g.getId(), gm);
|
||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
GroupId gId = introductionGroupFactory.createLocalGroup().getId();
|
||||
removeSessionWithIntroducer(txn, c);
|
||||
abortOrRemoveSessionWithIntroducee(txn, c);
|
||||
|
||||
// search for session states where c introduced us
|
||||
BdfDictionary query = BdfDictionary.of(
|
||||
new BdfEntry(ROLE, ROLE_INTRODUCEE),
|
||||
new BdfEntry(CONTACT_ID_1, c.getId().getInt())
|
||||
);
|
||||
try {
|
||||
Map<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
|
||||
// Remove the contact group (all messages will be removed with it)
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when a new message arrived and is being validated.
|
||||
* It is the central method where we determine which role we play
|
||||
* in the introduction protocol and which engine we need to start.
|
||||
*/
|
||||
@Override
|
||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||
BdfDictionary message) throws DbException, FormatException {
|
||||
|
||||
// Get message data and type
|
||||
GroupId groupId = m.getGroupId();
|
||||
long type = message.getLong(TYPE, -1L);
|
||||
|
||||
// we are an introducee, need to initialize new state
|
||||
if (type == TYPE_REQUEST) {
|
||||
boolean stateExists = true;
|
||||
try {
|
||||
getSessionState(txn, groupId, message.getRaw(SESSION_ID), false);
|
||||
} catch (FormatException e) {
|
||||
stateExists = false;
|
||||
}
|
||||
if (stateExists) throw new FormatException();
|
||||
BdfDictionary state =
|
||||
introduceeManager.initialize(txn, groupId, message);
|
||||
try {
|
||||
introduceeManager.incomingMessage(txn, state, message);
|
||||
messageTracker.trackIncomingMessage(txn, m);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
introduceeManager.abort(txn, state);
|
||||
} catch (FormatException e) {
|
||||
// FIXME necessary?
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
introduceeManager.abort(txn, state);
|
||||
}
|
||||
}
|
||||
// our role can be anything
|
||||
else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
|
||||
BdfDictionary state =
|
||||
getSessionState(txn, groupId, message.getRaw(SESSION_ID));
|
||||
|
||||
long role = state.getLong(ROLE, -1L);
|
||||
try {
|
||||
if (role == ROLE_INTRODUCER) {
|
||||
introducerManager.incomingMessage(txn, state, message);
|
||||
if (type == TYPE_RESPONSE)
|
||||
messageTracker.trackIncomingMessage(txn, m);
|
||||
} else if (role == ROLE_INTRODUCEE) {
|
||||
introduceeManager.incomingMessage(txn, state, message);
|
||||
if (type == TYPE_RESPONSE && !message.getBoolean(ACCEPT))
|
||||
messageTracker.trackIncomingMessage(txn, m);
|
||||
} else {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.warning("Unknown role '" + role + "'");
|
||||
throw new DbException();
|
||||
}
|
||||
} catch (DbException | FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
|
||||
else introduceeManager.abort(txn, state);
|
||||
}
|
||||
} else {
|
||||
// the message has been validated, so this should not happen
|
||||
if(LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Unknown message type '" + type + "', deleting...");
|
||||
}
|
||||
}
|
||||
return false;
|
||||
public Group getContactGroup(Contact c) {
|
||||
return contactGroupFactory
|
||||
.createContactGroup(CLIENT_ID, CLIENT_VERSION, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Group getContactGroup(Contact contact) {
|
||||
return introductionGroupFactory.createIntroductionGroup(contact);
|
||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||
BdfDictionary bdfMeta) throws DbException, FormatException {
|
||||
// Parse the metadata
|
||||
MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
|
||||
// Look up the session, if there is one
|
||||
SessionId sessionId = meta.getSessionId();
|
||||
IntroduceeSession newIntroduceeSession = null;
|
||||
if (sessionId == null) {
|
||||
if (meta.getMessageType() != REQUEST) throw new AssertionError();
|
||||
newIntroduceeSession = createNewIntroduceeSession(txn, m, body);
|
||||
sessionId = newIntroduceeSession.getSessionId();
|
||||
}
|
||||
StoredSession ss = getSession(txn, sessionId);
|
||||
// Handle the message
|
||||
Session session;
|
||||
MessageId storageId;
|
||||
if (ss == null) {
|
||||
if (meta.getMessageType() != REQUEST) throw new FormatException();
|
||||
if (newIntroduceeSession == null) throw new AssertionError();
|
||||
storageId = createStorageId(txn);
|
||||
session = handleMessage(txn, m, body, meta.getMessageType(),
|
||||
newIntroduceeSession, introduceeEngine);
|
||||
} else {
|
||||
storageId = ss.storageId;
|
||||
Role role = sessionParser.getRole(ss.bdfSession);
|
||||
if (role == INTRODUCER) {
|
||||
session = handleMessage(txn, m, body, meta.getMessageType(),
|
||||
sessionParser.parseIntroducerSession(ss.bdfSession),
|
||||
introducerEngine);
|
||||
} else if (role == INTRODUCEE) {
|
||||
session = handleMessage(txn, m, body, meta.getMessageType(),
|
||||
sessionParser.parseIntroduceeSession(m.getGroupId(),
|
||||
ss.bdfSession), introduceeEngine);
|
||||
} else throw new AssertionError();
|
||||
}
|
||||
// Store the updated session
|
||||
storeSession(txn, storageId, session);
|
||||
return false;
|
||||
}
|
||||
|
||||
private IntroduceeSession createNewIntroduceeSession(Transaction txn,
|
||||
Message m, BdfList body) throws DbException, FormatException {
|
||||
ContactId introducerId = getContactId(txn, m.getGroupId());
|
||||
Author introducer = db.getContact(txn, introducerId).getAuthor();
|
||||
Author local = identityManager.getLocalAuthor(txn);
|
||||
Author remote = messageParser.parseRequestMessage(m, body).getAuthor();
|
||||
if (local.equals(remote)) throw new FormatException();
|
||||
SessionId sessionId = crypto.getSessionId(introducer, local, remote);
|
||||
boolean alice = crypto.isAlice(local.getId(), remote.getId());
|
||||
return IntroduceeSession
|
||||
.getInitial(m.getGroupId(), sessionId, introducer, alice,
|
||||
remote);
|
||||
}
|
||||
|
||||
private <S 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
|
||||
public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
|
||||
long timestamp) throws DbException, FormatException {
|
||||
|
||||
long timestamp) throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
introducerManager.makeIntroduction(txn, c1, c2, msg, timestamp);
|
||||
Group g1 = getContactGroup(c1);
|
||||
Group g2 = getContactGroup(c2);
|
||||
messageTracker.trackMessage(txn, g1.getId(), timestamp, true);
|
||||
messageTracker.trackMessage(txn, g2.getId(), timestamp, true);
|
||||
// Look up the session, if there is one
|
||||
Author introducer = identityManager.getLocalAuthor(txn);
|
||||
SessionId sessionId =
|
||||
crypto.getSessionId(introducer, c1.getAuthor(),
|
||||
c2.getAuthor());
|
||||
StoredSession ss = getSession(txn, sessionId);
|
||||
// Create or parse the session
|
||||
IntroducerSession session;
|
||||
MessageId storageId;
|
||||
if (ss == null) {
|
||||
// This is the first request - create a new session
|
||||
GroupId groupId1 = getContactGroup(c1).getId();
|
||||
GroupId groupId2 = getContactGroup(c2).getId();
|
||||
boolean alice = crypto.isAlice(c1.getAuthor().getId(),
|
||||
c2.getAuthor().getId());
|
||||
// use fixed deterministic roles for the introducees
|
||||
session = new IntroducerSession(sessionId,
|
||||
alice ? groupId1 : groupId2,
|
||||
alice ? c1.getAuthor() : c2.getAuthor(),
|
||||
alice ? groupId2 : groupId1,
|
||||
alice ? c2.getAuthor() : c1.getAuthor()
|
||||
);
|
||||
storageId = createStorageId(txn);
|
||||
} else {
|
||||
// An earlier request exists, so we already have a session
|
||||
session = sessionParser.parseIntroducerSession(ss.bdfSession);
|
||||
storageId = ss.storageId;
|
||||
}
|
||||
// Handle the request action
|
||||
session = introducerEngine
|
||||
.onRequestAction(txn, session, msg, timestamp);
|
||||
// Store the updated session
|
||||
storeSession(txn, storageId, session);
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException, FormatException {
|
||||
|
||||
public void respondToIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp, boolean accept) throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
Contact c = db.getContact(txn, contactId);
|
||||
Group g = getContactGroup(c);
|
||||
BdfDictionary state =
|
||||
getSessionState(txn, g.getId(), sessionId.getBytes());
|
||||
|
||||
introduceeManager.acceptIntroduction(txn, state, timestamp);
|
||||
messageTracker.trackMessage(txn, g.getId(), timestamp, true);
|
||||
// Look up the session
|
||||
StoredSession ss = getSession(txn, sessionId);
|
||||
if (ss == null) {
|
||||
// Actions from the UI may be based on stale information.
|
||||
// The contact might just have been deleted, for example.
|
||||
// Throwing a DbException here aborts gracefully.
|
||||
throw new DbException();
|
||||
}
|
||||
// Parse the session
|
||||
Contact contact = db.getContact(txn, contactId);
|
||||
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||
IntroduceeSession session = sessionParser
|
||||
.parseIntroduceeSession(contactGroupId, ss.bdfSession);
|
||||
// Handle the join or leave action
|
||||
if (accept) {
|
||||
session = introduceeEngine
|
||||
.onAcceptAction(txn, session, timestamp);
|
||||
} else {
|
||||
session = introduceeEngine
|
||||
.onDeclineAction(txn, session, timestamp);
|
||||
}
|
||||
// Store the updated session
|
||||
storeSession(txn, ss.storageId, session);
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void declineIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException, FormatException {
|
||||
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
Contact c = db.getContact(txn, contactId);
|
||||
Group g = getContactGroup(c);
|
||||
BdfDictionary state =
|
||||
getSessionState(txn, g.getId(), sessionId.getBytes());
|
||||
|
||||
introduceeManager.declineIntroduction(txn, state, timestamp);
|
||||
messageTracker.trackMessage(txn, g.getId(), timestamp, true);
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IntroductionMessage> getIntroductionMessages(
|
||||
ContactId contactId) throws DbException {
|
||||
|
||||
Collection<IntroductionMessage> list = new ArrayList<>();
|
||||
|
||||
Map<MessageId, BdfDictionary> metadata;
|
||||
Collection<MessageStatus> statuses;
|
||||
public Collection<IntroductionMessage> getIntroductionMessages(ContactId c)
|
||||
throws DbException {
|
||||
List<IntroductionMessage> messages;
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
// get messages and their status
|
||||
GroupId g = getContactGroup(db.getContact(txn, contactId)).getId();
|
||||
metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
statuses = db.getMessageStatus(txn, contactId, g);
|
||||
|
||||
// turn messages into classes for the UI
|
||||
for (MessageStatus s : statuses) {
|
||||
MessageId messageId = s.getMessageId();
|
||||
BdfDictionary msg = metadata.get(messageId);
|
||||
if (msg == null) continue;
|
||||
|
||||
try {
|
||||
long type = msg.getLong(TYPE);
|
||||
if (type == TYPE_ACK || type == TYPE_ABORT) continue;
|
||||
|
||||
// get session state
|
||||
SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
|
||||
BdfDictionary state =
|
||||
getSessionState(txn, g, sessionId.getBytes());
|
||||
|
||||
int role = state.getLong(ROLE).intValue();
|
||||
boolean local;
|
||||
long time = msg.getLong(MESSAGE_TIME);
|
||||
boolean accepted = msg.getBoolean(ACCEPT, false);
|
||||
boolean read = msg.getBoolean(MSG_KEY_READ, false);
|
||||
AuthorId authorId;
|
||||
String name;
|
||||
if (type == TYPE_RESPONSE) {
|
||||
if (role == ROLE_INTRODUCER) {
|
||||
if (!concernsThisContact(contactId, messageId, state)) {
|
||||
// this response is not from contactId
|
||||
continue;
|
||||
}
|
||||
local = false;
|
||||
authorId =
|
||||
getAuthorIdForIntroducer(contactId, state);
|
||||
name = getNameForIntroducer(contactId, state);
|
||||
} else {
|
||||
if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE),
|
||||
messageId.getBytes())) {
|
||||
// this response is not ours,
|
||||
// check if it was a decline
|
||||
if (!accepted) {
|
||||
local = false;
|
||||
} else {
|
||||
// don't include positive responses
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
local = true;
|
||||
}
|
||||
authorId = new AuthorId(
|
||||
state.getRaw(REMOTE_AUTHOR_ID));
|
||||
name = state.getString(NAME);
|
||||
}
|
||||
IntroductionResponse ir = new IntroductionResponse(
|
||||
sessionId, messageId, g, role, time, local,
|
||||
s.isSent(), s.isSeen(), read, authorId, name,
|
||||
accepted);
|
||||
list.add(ir);
|
||||
} else if (type == TYPE_REQUEST) {
|
||||
String message;
|
||||
boolean answered, exists, introducesOtherIdentity;
|
||||
if (role == ROLE_INTRODUCER) {
|
||||
local = true;
|
||||
authorId =
|
||||
getAuthorIdForIntroducer(contactId, state);
|
||||
name = getNameForIntroducer(contactId, state);
|
||||
message = msg.getOptionalString(MSG);
|
||||
answered = false;
|
||||
exists = false;
|
||||
introducesOtherIdentity = false;
|
||||
} else {
|
||||
local = false;
|
||||
authorId = new AuthorId(
|
||||
state.getRaw(REMOTE_AUTHOR_ID));
|
||||
name = state.getString(NAME);
|
||||
message = state.getOptionalString(MSG);
|
||||
boolean finished = state.getLong(STATE) ==
|
||||
FINISHED.getValue();
|
||||
answered = finished || state.getBoolean(ANSWERED);
|
||||
exists = state.getBoolean(EXISTS);
|
||||
introducesOtherIdentity =
|
||||
state.getBoolean(REMOTE_AUTHOR_IS_US);
|
||||
}
|
||||
IntroductionRequest ir = new IntroductionRequest(
|
||||
sessionId, messageId, g, role, time, local,
|
||||
s.isSent(), s.isSeen(), read, authorId, name,
|
||||
accepted, message, answered, exists,
|
||||
introducesOtherIdentity);
|
||||
list.add(ir);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
Contact contact = db.getContact(txn, c);
|
||||
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||
BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
|
||||
Map<MessageId, BdfDictionary> results = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, contactGroupId, query);
|
||||
messages = new ArrayList<>(results.size());
|
||||
for (Entry<MessageId, BdfDictionary> e : results.entrySet()) {
|
||||
MessageId m = e.getKey();
|
||||
MessageMetadata meta =
|
||||
messageParser.parseMetadata(e.getValue());
|
||||
MessageStatus status = db.getMessageStatus(txn, c, m);
|
||||
StoredSession ss = getSession(txn, meta.getSessionId());
|
||||
if (ss == null) throw new AssertionError();
|
||||
MessageType type = meta.getMessageType();
|
||||
if (type == REQUEST) {
|
||||
messages.add(
|
||||
parseInvitationRequest(txn, contactGroupId, m,
|
||||
meta, status, ss.bdfSession));
|
||||
} else if (type == ACCEPT) {
|
||||
messages.add(
|
||||
parseInvitationResponse(contactGroupId, m, meta,
|
||||
status, ss.bdfSession, true));
|
||||
} else if (type == DECLINE) {
|
||||
messages.add(
|
||||
parseInvitationResponse(contactGroupId, m, meta,
|
||||
status, ss.bdfSession, false));
|
||||
}
|
||||
}
|
||||
db.commitTransaction(txn);
|
||||
@@ -438,88 +422,140 @@ class IntroductionManagerImpl extends ConversationClientImpl
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return list;
|
||||
return messages;
|
||||
}
|
||||
|
||||
private String getNameForIntroducer(ContactId contactId,
|
||||
BdfDictionary state) throws FormatException {
|
||||
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
|
||||
return state.getString(CONTACT_2);
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
|
||||
return state.getString(CONTACT_1);
|
||||
throw new RuntimeException(
|
||||
"Contact not part of this introduction session");
|
||||
}
|
||||
|
||||
private AuthorId getAuthorIdForIntroducer(ContactId contactId,
|
||||
BdfDictionary state) throws FormatException {
|
||||
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
|
||||
return new AuthorId(state.getRaw(AUTHOR_ID_2));
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
|
||||
return new AuthorId(state.getRaw(AUTHOR_ID_1));
|
||||
throw new RuntimeException(
|
||||
"Contact not part of this introduction session");
|
||||
}
|
||||
|
||||
private boolean concernsThisContact(ContactId contactId, MessageId messageId,
|
||||
BdfDictionary state) throws FormatException {
|
||||
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) {
|
||||
return Arrays.equals(state.getRaw(RESPONSE_1, new byte[0]),
|
||||
messageId.getBytes());
|
||||
} else {
|
||||
return Arrays.equals(state.getRaw(RESPONSE_2, new byte[0]),
|
||||
messageId.getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
private BdfDictionary getSessionState(Transaction txn, GroupId groupId,
|
||||
byte[] sessionId, boolean warn)
|
||||
private IntroductionRequest parseInvitationRequest(Transaction txn,
|
||||
GroupId contactGroupId, MessageId m, MessageMetadata meta,
|
||||
MessageStatus status, BdfDictionary bdfSession)
|
||||
throws DbException, FormatException {
|
||||
Role role = sessionParser.getRole(bdfSession);
|
||||
SessionId sessionId;
|
||||
Author author;
|
||||
if (role == INTRODUCER) {
|
||||
IntroducerSession session =
|
||||
sessionParser.parseIntroducerSession(bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
if (contactGroupId.equals(session.getIntroduceeA().groupId)) {
|
||||
author = session.getIntroduceeB().author;
|
||||
} else {
|
||||
author = session.getIntroduceeA().author;
|
||||
}
|
||||
} else if (role == INTRODUCEE) {
|
||||
IntroduceeSession session = sessionParser
|
||||
.parseIntroduceeSession(contactGroupId, bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
author = session.getRemote().author;
|
||||
} else throw new AssertionError();
|
||||
Message msg = clientHelper.getMessage(txn, m);
|
||||
if (msg == null) throw new AssertionError();
|
||||
BdfList body = clientHelper.toList(msg);
|
||||
RequestMessage rm = messageParser.parseRequestMessage(msg, body);
|
||||
String message = rm.getMessage();
|
||||
LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
|
||||
boolean contactExists = contactManager
|
||||
.contactExists(txn, rm.getAuthor().getId(),
|
||||
localAuthor.getId());
|
||||
|
||||
return new IntroductionRequest(sessionId, m, contactGroupId,
|
||||
role, meta.getTimestamp(), meta.isLocal(),
|
||||
status.isSent(), status.isSeen(), meta.isRead(),
|
||||
author.getName(), false, message, !meta.isAvailableToAnswer(),
|
||||
contactExists);
|
||||
}
|
||||
|
||||
private IntroductionResponse parseInvitationResponse(GroupId contactGroupId,
|
||||
MessageId m, MessageMetadata meta, MessageStatus status,
|
||||
BdfDictionary bdfSession, boolean accept) throws FormatException {
|
||||
Role role = sessionParser.getRole(bdfSession);
|
||||
SessionId sessionId;
|
||||
Author author;
|
||||
if (role == INTRODUCER) {
|
||||
IntroducerSession session =
|
||||
sessionParser.parseIntroducerSession(bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
if (contactGroupId.equals(session.getIntroduceeA().groupId)) {
|
||||
author = session.getIntroduceeB().author;
|
||||
} else {
|
||||
author = session.getIntroduceeA().author;
|
||||
}
|
||||
} else if (role == INTRODUCEE) {
|
||||
IntroduceeSession session = sessionParser
|
||||
.parseIntroduceeSession(contactGroupId, bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
author = session.getRemote().author;
|
||||
} else throw new AssertionError();
|
||||
return new IntroductionResponse(sessionId, m, contactGroupId,
|
||||
role, meta.getTimestamp(), meta.isLocal(), status.isSent(),
|
||||
status.isSeen(), meta.isRead(), author.getName(), accept);
|
||||
}
|
||||
|
||||
private void removeSessionWithIntroducer(Transaction txn,
|
||||
Contact introducer) throws DbException {
|
||||
BdfDictionary query = sessionEncoder
|
||||
.getIntroduceeSessionsByIntroducerQuery(introducer.getAuthor());
|
||||
Map<MessageId, BdfDictionary> sessions;
|
||||
try {
|
||||
// See if we can find the state directly for the introducer
|
||||
BdfDictionary state = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn,
|
||||
new MessageId(sessionId));
|
||||
GroupId g1 = new GroupId(state.getRaw(GROUP_ID_1));
|
||||
GroupId g2 = new GroupId(state.getRaw(GROUP_ID_2));
|
||||
if (!g1.equals(groupId) && !g2.equals(groupId)) {
|
||||
throw new NoSuchMessageException();
|
||||
}
|
||||
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();
|
||||
sessions = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, localGroup.getId(),
|
||||
query);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
for (MessageId id : sessions.keySet()) {
|
||||
db.removeMessage(txn, id);
|
||||
}
|
||||
}
|
||||
|
||||
private BdfDictionary getSessionState(Transaction txn, GroupId groupId,
|
||||
byte[] sessionId) throws DbException, FormatException {
|
||||
|
||||
return getSessionState(txn, groupId, sessionId, true);
|
||||
private void abortOrRemoveSessionWithIntroducee(Transaction txn,
|
||||
Contact c) throws DbException {
|
||||
BdfDictionary query = sessionEncoder.getIntroducerSessionsQuery();
|
||||
Map<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)
|
||||
throws DbException {
|
||||
private void abortOrRemoveSessionWithIntroducee(Transaction txn,
|
||||
IntroducerSession s, MessageId storageId, Introducee i,
|
||||
LocalAuthor localAuthor) throws DbException {
|
||||
if (db.containsContact(txn, i.author.getId(), localAuthor.getId())) {
|
||||
IntroducerSession session = introducerEngine.onAbortAction(txn, s);
|
||||
storeSession(txn, storageId, session);
|
||||
} else {
|
||||
db.removeMessage(txn, storageId);
|
||||
}
|
||||
}
|
||||
|
||||
db.deleteMessage(txn, messageId);
|
||||
db.deleteMessageMetadata(txn, messageId);
|
||||
private static class StoredSession {
|
||||
|
||||
private final MessageId storageId;
|
||||
private final BdfDictionary bdfSession;
|
||||
|
||||
private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
|
||||
this.storageId = storageId;
|
||||
this.bdfSession = bdfSession;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.contact.ContactManager;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.bramble.api.sync.ValidationManager;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.api.client.MessageQueueManager;
|
||||
import org.briarproject.briar.api.introduction.IntroductionManager;
|
||||
import org.briarproject.briar.api.messaging.ConversationManager;
|
||||
|
||||
@@ -21,22 +21,22 @@ import static org.briarproject.briar.api.introduction.IntroductionManager.CLIENT
|
||||
public class IntroductionModule {
|
||||
|
||||
public static class EagerSingletons {
|
||||
@Inject
|
||||
IntroductionManager introductionManager;
|
||||
@Inject
|
||||
IntroductionValidator introductionValidator;
|
||||
@Inject
|
||||
IntroductionManager introductionManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
IntroductionValidator provideValidator(
|
||||
MessageQueueManager messageQueueManager,
|
||||
MetadataEncoder metadataEncoder, ClientHelper clientHelper,
|
||||
Clock clock) {
|
||||
IntroductionValidator provideValidator(ValidationManager validationManager,
|
||||
MessageEncoder messageEncoder, MetadataEncoder metadataEncoder,
|
||||
ClientHelper clientHelper, Clock clock) {
|
||||
|
||||
IntroductionValidator introductionValidator = new IntroductionValidator(
|
||||
clientHelper, metadataEncoder, clock);
|
||||
messageQueueManager.registerMessageValidator(CLIENT_ID,
|
||||
IntroductionValidator introductionValidator =
|
||||
new IntroductionValidator(messageEncoder, clientHelper,
|
||||
metadataEncoder, clock);
|
||||
validationManager.registerMessageValidator(CLIENT_ID,
|
||||
introductionValidator);
|
||||
|
||||
return introductionValidator;
|
||||
@@ -46,16 +46,42 @@ public class IntroductionModule {
|
||||
@Singleton
|
||||
IntroductionManager provideIntroductionManager(
|
||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||
MessageQueueManager messageQueueManager,
|
||||
ValidationManager validationManager,
|
||||
ConversationManager conversationManager,
|
||||
IntroductionManagerImpl introductionManager) {
|
||||
|
||||
lifecycleManager.registerClient(introductionManager);
|
||||
contactManager.registerContactHook(introductionManager);
|
||||
messageQueueManager.registerIncomingMessageHook(CLIENT_ID,
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID,
|
||||
introductionManager);
|
||||
conversationManager.registerConversationClient(introductionManager);
|
||||
|
||||
return introductionManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MessageParser provideMessageParser(MessageParserImpl messageParser) {
|
||||
return messageParser;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
|
||||
return messageEncoder;
|
||||
}
|
||||
|
||||
@Provides
|
||||
SessionParser provideSessionParser(SessionParserImpl sessionParser) {
|
||||
return sessionParser;
|
||||
}
|
||||
|
||||
@Provides
|
||||
SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) {
|
||||
return sessionEncoder;
|
||||
}
|
||||
|
||||
@Provides
|
||||
IntroductionCrypto provideIntroductionCrypto(
|
||||
IntroductionCryptoImpl introductionCrypto) {
|
||||
return introductionCrypto;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.briarproject.briar.introduction;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.UniqueId;
|
||||
import org.briarproject.bramble.api.client.BdfMessageContext;
|
||||
import org.briarproject.bramble.api.client.BdfMessageValidator;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
@@ -9,183 +11,190 @@ import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
import org.briarproject.briar.client.BdfQueueMessageValidator;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
|
||||
import static org.briarproject.bramble.api.plugin.TransportId.MAX_TRANSPORT_ID_LENGTH;
|
||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTIES_PER_TRANSPORT;
|
||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkLength;
|
||||
import static org.briarproject.bramble.util.ValidationUtils.checkSize;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MESSAGE_TIME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
|
||||
import static org.briarproject.briar.introduction.MessageType.ACCEPT;
|
||||
import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
|
||||
import static org.briarproject.briar.introduction.MessageType.AUTH;
|
||||
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class IntroductionValidator extends BdfQueueMessageValidator {
|
||||
class IntroductionValidator extends BdfMessageValidator {
|
||||
|
||||
IntroductionValidator(ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
private final MessageEncoder messageEncoder;
|
||||
|
||||
IntroductionValidator(MessageEncoder messageEncoder,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
this.messageEncoder = messageEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||
BdfList body) throws FormatException {
|
||||
MessageType type = MessageType.fromValue(body.getLong(0).intValue());
|
||||
|
||||
BdfDictionary d;
|
||||
long type = body.getLong(0);
|
||||
byte[] id = body.getRaw(1);
|
||||
checkLength(id, SessionId.LENGTH);
|
||||
switch (type) {
|
||||
case REQUEST:
|
||||
return validateRequestMessage(m, body);
|
||||
case ACCEPT:
|
||||
return validateAcceptMessage(m, body);
|
||||
case AUTH:
|
||||
return validateAuthMessage(m, body);
|
||||
case ACTIVATE:
|
||||
return validateActivateMessage(m, body);
|
||||
case DECLINE:
|
||||
case ABORT:
|
||||
return validateOtherMessage(type, m, body);
|
||||
default:
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
|
||||
if (type == TYPE_REQUEST) {
|
||||
d = validateRequest(body);
|
||||
} else if (type == TYPE_RESPONSE) {
|
||||
d = validateResponse(body);
|
||||
} else if (type == TYPE_ACK) {
|
||||
d = validateAck(body);
|
||||
} else if (type == TYPE_ABORT) {
|
||||
d = validateAbort(body);
|
||||
private BdfMessageContext validateRequestMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 4);
|
||||
|
||||
byte[] previousMessageId = body.getOptionalRaw(1);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
|
||||
BdfList authorList = body.getList(2);
|
||||
clientHelper.parseAndValidateAuthor(authorList);
|
||||
|
||||
String msg = body.getOptionalString(3);
|
||||
checkLength(msg, 1, MAX_REQUEST_MESSAGE_LENGTH);
|
||||
|
||||
BdfDictionary meta =
|
||||
messageEncoder.encodeRequestMetadata(m.getTimestamp());
|
||||
if (previousMessageId == null) {
|
||||
return new BdfMessageContext(meta);
|
||||
} else {
|
||||
throw new FormatException();
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
|
||||
d.put(TYPE, type);
|
||||
d.put(SESSION_ID, id);
|
||||
d.put(GROUP_ID, m.getGroupId());
|
||||
d.put(MESSAGE_ID, m.getId());
|
||||
d.put(MESSAGE_TIME, m.getTimestamp());
|
||||
return new BdfMessageContext(d);
|
||||
}
|
||||
|
||||
private BdfDictionary validateRequest(BdfList message)
|
||||
private BdfMessageContext validateAcceptMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 6);
|
||||
|
||||
checkSize(message, 4, 5);
|
||||
byte[] sessionIdBytes = body.getRaw(1);
|
||||
checkLength(sessionIdBytes, UniqueId.LENGTH);
|
||||
|
||||
// TODO: Exchange author format version
|
||||
byte[] previousMessageId = body.getOptionalRaw(2);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
|
||||
// parse contact name
|
||||
String name = message.getString(2);
|
||||
checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
|
||||
byte[] ephemeralPublicKey = body.getRaw(3);
|
||||
checkLength(ephemeralPublicKey, 0, MAX_PUBLIC_KEY_LENGTH);
|
||||
|
||||
// parse contact's public key
|
||||
byte[] key = message.getRaw(3);
|
||||
checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH);
|
||||
long timestamp = body.getLong(4);
|
||||
if (timestamp < 0) throw new FormatException();
|
||||
|
||||
// parse (optional) message
|
||||
String msg = null;
|
||||
if (message.size() == 5) {
|
||||
msg = message.getString(4);
|
||||
checkLength(msg, 0, MAX_INTRODUCTION_MESSAGE_LENGTH);
|
||||
}
|
||||
BdfDictionary transportProperties = body.getDictionary(5);
|
||||
if (transportProperties.size() < 1) throw new FormatException();
|
||||
clientHelper
|
||||
.parseAndValidateTransportPropertiesMap(transportProperties);
|
||||
|
||||
// Return the metadata
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(NAME, name);
|
||||
d.put(PUBLIC_KEY, key);
|
||||
if (msg != null) {
|
||||
d.put(MSG, msg);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private BdfDictionary validateResponse(BdfList message)
|
||||
throws FormatException {
|
||||
|
||||
checkSize(message, 3, 6);
|
||||
|
||||
// parse accept/decline
|
||||
boolean accept = message.getBoolean(2);
|
||||
|
||||
long time = 0;
|
||||
byte[] pubkey = null;
|
||||
BdfDictionary tp = new BdfDictionary();
|
||||
if (accept) {
|
||||
checkSize(message, 6);
|
||||
|
||||
// parse timestamp
|
||||
time = message.getLong(3);
|
||||
|
||||
// parse ephemeral public key
|
||||
pubkey = message.getRaw(4);
|
||||
checkLength(pubkey, 1, MAX_AGREEMENT_PUBLIC_KEY_BYTES);
|
||||
|
||||
// parse transport properties
|
||||
tp = message.getDictionary(5);
|
||||
if (tp.size() < 1) throw new FormatException();
|
||||
for (String tId : tp.keySet()) {
|
||||
checkLength(tId, 1, MAX_TRANSPORT_ID_LENGTH);
|
||||
BdfDictionary tProps = tp.getDictionary(tId);
|
||||
checkSize(tProps, 0, MAX_PROPERTIES_PER_TRANSPORT);
|
||||
for (String propId : tProps.keySet()) {
|
||||
checkLength(propId, 0, MAX_PROPERTY_LENGTH);
|
||||
String prop = tProps.getString(propId);
|
||||
checkLength(prop, 0, MAX_PROPERTY_LENGTH);
|
||||
}
|
||||
}
|
||||
SessionId sessionId = new SessionId(sessionIdBytes);
|
||||
BdfDictionary meta = messageEncoder
|
||||
.encodeMetadata(ACCEPT, sessionId, m.getTimestamp(), false,
|
||||
false, false);
|
||||
if (previousMessageId == null) {
|
||||
return new BdfMessageContext(meta);
|
||||
} else {
|
||||
checkSize(message, 3);
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
|
||||
// Return the metadata
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(ACCEPT, accept);
|
||||
if (accept) {
|
||||
d.put(TIME, time);
|
||||
d.put(E_PUBLIC_KEY, pubkey);
|
||||
d.put(TRANSPORT, tp);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private BdfDictionary validateAck(BdfList message) throws FormatException {
|
||||
checkSize(message, 4);
|
||||
|
||||
byte[] mac = message.getRaw(2);
|
||||
checkLength(mac, 1, MAC_LENGTH);
|
||||
|
||||
byte[] sig = message.getRaw(3);
|
||||
checkLength(sig, 1, MAX_SIGNATURE_LENGTH);
|
||||
|
||||
// Return the metadata
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(MAC, mac);
|
||||
d.put(SIGNATURE, sig);
|
||||
return d;
|
||||
}
|
||||
|
||||
private BdfDictionary validateAbort(BdfList message)
|
||||
private BdfMessageContext validateAuthMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 5);
|
||||
|
||||
checkSize(message, 2);
|
||||
byte[] sessionIdBytes = body.getRaw(1);
|
||||
checkLength(sessionIdBytes, UniqueId.LENGTH);
|
||||
|
||||
// Return the metadata
|
||||
return new BdfDictionary();
|
||||
byte[] previousMessageId = body.getRaw(2);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
|
||||
byte[] mac = body.getRaw(3);
|
||||
checkLength(mac, MAC_BYTES);
|
||||
|
||||
byte[] signature = body.getRaw(4);
|
||||
checkLength(signature, 1, MAX_SIGNATURE_BYTES);
|
||||
|
||||
SessionId sessionId = new SessionId(sessionIdBytes);
|
||||
BdfDictionary meta = messageEncoder
|
||||
.encodeMetadata(AUTH, sessionId, m.getTimestamp(), false, false,
|
||||
false);
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
|
||||
private BdfMessageContext validateActivateMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 4);
|
||||
|
||||
byte[] sessionIdBytes = body.getRaw(1);
|
||||
checkLength(sessionIdBytes, UniqueId.LENGTH);
|
||||
|
||||
byte[] previousMessageId = body.getRaw(2);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
|
||||
byte[] mac = body.getOptionalRaw(3);
|
||||
checkLength(mac, MAC_BYTES);
|
||||
|
||||
SessionId sessionId = new SessionId(sessionIdBytes);
|
||||
BdfDictionary meta = messageEncoder
|
||||
.encodeMetadata(ACTIVATE, sessionId, m.getTimestamp(), false,
|
||||
false, false);
|
||||
if (previousMessageId == null) {
|
||||
return new BdfMessageContext(meta);
|
||||
} else {
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
private BdfMessageContext validateOtherMessage(MessageType type,
|
||||
Message m, BdfList body) throws FormatException {
|
||||
checkSize(body, 3);
|
||||
|
||||
byte[] sessionIdBytes = body.getRaw(1);
|
||||
checkLength(sessionIdBytes, UniqueId.LENGTH);
|
||||
|
||||
byte[] previousMessageId = body.getOptionalRaw(2);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
|
||||
SessionId sessionId = new SessionId(sessionIdBytes);
|
||||
BdfDictionary meta = messageEncoder
|
||||
.encodeMetadata(type, sessionId, m.getTimestamp(), false, false,
|
||||
false);
|
||||
if (previousMessageId == null) {
|
||||
return new BdfMessageContext(meta);
|
||||
} else {
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.briarproject.briar.introduction;
|
||||
|
||||
interface State {
|
||||
|
||||
int getValue();
|
||||
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -59,6 +59,13 @@ interface IntroductionIntegrationTestComponent
|
||||
|
||||
void inject(IntroductionIntegrationTest init);
|
||||
|
||||
MessageSender getMessageSender();
|
||||
void inject(MessageEncoderParserIntegrationTest init);
|
||||
void inject(SessionEncoderParserIntegrationTest init);
|
||||
void inject(IntroductionCryptoIntegrationTest init);
|
||||
|
||||
MessageEncoder getMessageEncoder();
|
||||
MessageParser getMessageParser();
|
||||
SessionParser getSessionParser();
|
||||
IntroductionCrypto getIntroductionCrypto();
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,361 +1,463 @@
|
||||
package org.briarproject.briar.introduction;
|
||||
|
||||
import org.briarproject.bramble.api.FormatException;
|
||||
import org.briarproject.bramble.api.client.ClientHelper;
|
||||
import org.briarproject.bramble.api.client.BdfMessageContext;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
import org.briarproject.bramble.api.data.BdfEntry;
|
||||
import org.briarproject.bramble.api.data.BdfList;
|
||||
import org.briarproject.bramble.api.data.MetadataEncoder;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.plugin.TransportId;
|
||||
import org.briarproject.bramble.api.sync.Group;
|
||||
import org.briarproject.bramble.api.sync.Message;
|
||||
import org.briarproject.bramble.api.sync.MessageId;
|
||||
import org.briarproject.bramble.api.system.Clock;
|
||||
import org.briarproject.bramble.system.SystemClock;
|
||||
import org.briarproject.bramble.test.ValidatorTestCase;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
import org.briarproject.briar.test.BriarTestCase;
|
||||
import org.jmock.Mockery;
|
||||
import org.jmock.Expectations;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_AGREEMENT_PUBLIC_KEY_BYTES;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAC_BYTES;
|
||||
import static org.briarproject.bramble.api.crypto.CryptoConstants.MAX_SIGNATURE_BYTES;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
|
||||
import static org.briarproject.bramble.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
|
||||
import static org.briarproject.bramble.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
|
||||
import static org.briarproject.bramble.test.TestUtils.getAuthor;
|
||||
import static org.briarproject.bramble.test.TestUtils.getClientId;
|
||||
import static org.briarproject.bramble.test.TestUtils.getGroup;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomId;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAC_LENGTH;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_MESSAGE_LENGTH;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.SIGNATURE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TIME;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TRANSPORT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_REQUEST_MESSAGE_LENGTH;
|
||||
import static org.briarproject.briar.introduction.MessageType.ABORT;
|
||||
import static org.briarproject.briar.introduction.MessageType.ACCEPT;
|
||||
import static org.briarproject.briar.introduction.MessageType.ACTIVATE;
|
||||
import static org.briarproject.briar.introduction.MessageType.AUTH;
|
||||
import static org.briarproject.briar.introduction.MessageType.DECLINE;
|
||||
import static org.briarproject.briar.introduction.MessageType.REQUEST;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class IntroductionValidatorTest extends BriarTestCase {
|
||||
public class IntroductionValidatorTest extends ValidatorTestCase {
|
||||
|
||||
private final Mockery context = new Mockery();
|
||||
private final Group group;
|
||||
private final Message message;
|
||||
private final IntroductionValidator validator;
|
||||
private final Clock clock = new SystemClock();
|
||||
private final MessageEncoder messageEncoder =
|
||||
context.mock(MessageEncoder.class);
|
||||
private final IntroductionValidator validator =
|
||||
new IntroductionValidator(messageEncoder, clientHelper,
|
||||
metadataEncoder, clock);
|
||||
|
||||
public IntroductionValidatorTest() {
|
||||
group = getGroup(getClientId());
|
||||
MessageId messageId = new MessageId(getRandomId());
|
||||
long timestamp = System.currentTimeMillis();
|
||||
byte[] raw = getRandomBytes(123);
|
||||
message = new Message(messageId, group.getId(), timestamp, raw);
|
||||
|
||||
|
||||
ClientHelper clientHelper = context.mock(ClientHelper.class);
|
||||
MetadataEncoder metadataEncoder = context.mock(MetadataEncoder.class);
|
||||
validator = new IntroductionValidator(clientHelper, metadataEncoder,
|
||||
clock);
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
private final SessionId sessionId = new SessionId(getRandomId());
|
||||
private final MessageId previousMsgId = new MessageId(getRandomId());
|
||||
private final String text = getRandomString(MAX_REQUEST_MESSAGE_LENGTH);
|
||||
private final BdfDictionary meta = new BdfDictionary();
|
||||
private final long acceptTimestamp = 42;
|
||||
private final BdfDictionary transportProperties = BdfDictionary.of(
|
||||
new BdfEntry("transportId", new BdfDictionary())
|
||||
);
|
||||
private final byte[] mac = getRandomBytes(MAC_BYTES);
|
||||
private final byte[] signature = getRandomBytes(MAX_SIGNATURE_BYTES);
|
||||
|
||||
//
|
||||
// Introduction Requests
|
||||
// Introduction REQUEST
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testValidateProperIntroductionRequest() throws Exception {
|
||||
byte[] sessionId = getRandomId();
|
||||
String name = getRandomString(MAX_AUTHOR_NAME_LENGTH);
|
||||
byte[] publicKey = getRandomBytes(MAX_PUBLIC_KEY_LENGTH);
|
||||
String text = getRandomString(MAX_INTRODUCTION_MESSAGE_LENGTH);
|
||||
public void testAcceptsRequest() throws Exception {
|
||||
BdfList body = BdfList.of(REQUEST.getValue(), previousMsgId.getBytes(),
|
||||
authorList, text);
|
||||
|
||||
BdfList body = BdfList.of(TYPE_REQUEST, sessionId,
|
||||
name, publicKey, text);
|
||||
expectParseAuthor(authorList, author);
|
||||
expectEncodeRequestMetadata();
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
BdfDictionary result =
|
||||
validator.validateMessage(message, group, body).getDictionary();
|
||||
|
||||
assertEquals(Long.valueOf(TYPE_REQUEST), result.getLong(TYPE));
|
||||
assertEquals(sessionId, result.getRaw(SESSION_ID));
|
||||
assertEquals(name, result.getString(NAME));
|
||||
assertEquals(publicKey, result.getRaw(PUBLIC_KEY));
|
||||
assertEquals(text, result.getString(MSG));
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionRequestWithNoName() throws Exception {
|
||||
BdfDictionary msg = getValidIntroductionRequest();
|
||||
|
||||
// no NAME is message
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getRaw(PUBLIC_KEY));
|
||||
if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
|
||||
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionRequestWithLongName() throws Exception {
|
||||
// too long NAME in message
|
||||
BdfDictionary msg = getValidIntroductionRequest();
|
||||
msg.put(NAME, msg.get(NAME) + "x");
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
|
||||
if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
|
||||
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionRequestWithWrongType()
|
||||
throws Exception {
|
||||
// wrong message type
|
||||
BdfDictionary msg = getValidIntroductionRequest();
|
||||
msg.put(TYPE, 324234);
|
||||
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getString(NAME), msg.getRaw(PUBLIC_KEY));
|
||||
if (msg.containsKey(MSG)) body.add(msg.getString(MSG));
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
private BdfDictionary getValidIntroductionRequest() throws Exception {
|
||||
byte[] sessionId = getRandomId();
|
||||
Author author = getAuthor();
|
||||
String text = getRandomString(MAX_MESSAGE_BODY_LENGTH);
|
||||
|
||||
BdfDictionary msg = new BdfDictionary();
|
||||
msg.put(TYPE, TYPE_REQUEST);
|
||||
msg.put(SESSION_ID, sessionId);
|
||||
msg.put(NAME, author.getName());
|
||||
msg.put(PUBLIC_KEY, author.getPublicKey());
|
||||
msg.put(MSG, text);
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction Responses
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testValidateIntroductionAcceptResponse() throws Exception {
|
||||
byte[] groupId = getRandomId();
|
||||
byte[] sessionId = getRandomId();
|
||||
long time = clock.currentTimeMillis();
|
||||
byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES);
|
||||
String transportId =
|
||||
getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
|
||||
BdfDictionary tProps = BdfDictionary.of(
|
||||
new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH),
|
||||
getRandomString(MAX_PROPERTY_LENGTH))
|
||||
);
|
||||
BdfDictionary tp = BdfDictionary.of(
|
||||
new BdfEntry(transportId, tProps)
|
||||
);
|
||||
|
||||
BdfDictionary msg = new BdfDictionary();
|
||||
msg.put(TYPE, TYPE_RESPONSE);
|
||||
msg.put(GROUP_ID, groupId);
|
||||
msg.put(SESSION_ID, sessionId);
|
||||
msg.put(ACCEPT, true);
|
||||
msg.put(TIME, time);
|
||||
msg.put(E_PUBLIC_KEY, publicKey);
|
||||
msg.put(TRANSPORT, tp);
|
||||
|
||||
BdfList body = BdfList.of(TYPE_RESPONSE, msg.getRaw(SESSION_ID),
|
||||
msg.getBoolean(ACCEPT), msg.getLong(TIME),
|
||||
msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));
|
||||
|
||||
BdfDictionary result =
|
||||
validator.validateMessage(message, group, body).getDictionary();
|
||||
|
||||
assertEquals(Long.valueOf(TYPE_RESPONSE), result.getLong(TYPE));
|
||||
assertEquals(sessionId, result.getRaw(SESSION_ID));
|
||||
assertEquals(true, result.getBoolean(ACCEPT));
|
||||
assertEquals(publicKey, result.getRaw(E_PUBLIC_KEY));
|
||||
assertEquals(tp, result.getDictionary(TRANSPORT));
|
||||
context.assertIsSatisfied();
|
||||
assertExpectedContext(messageContext, previousMsgId);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateIntroductionDeclineResponse() throws Exception {
|
||||
BdfDictionary msg = getValidIntroductionResponse(false);
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getBoolean(ACCEPT));
|
||||
public void testAcceptsRequestWithPreviousMsgIdNull() throws Exception {
|
||||
BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, text);
|
||||
|
||||
BdfDictionary result = validator.validateMessage(message, group, body)
|
||||
.getDictionary();
|
||||
expectParseAuthor(authorList, author);
|
||||
expectEncodeRequestMetadata();
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertFalse(result.getBoolean(ACCEPT));
|
||||
context.assertIsSatisfied();
|
||||
assertExpectedContext(messageContext, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAcceptsRequestWithMessageNull() throws Exception {
|
||||
BdfList body = BdfList.of(REQUEST.getValue(), null, authorList, null);
|
||||
|
||||
expectParseAuthor(authorList, author);
|
||||
expectEncodeRequestMetadata();
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertExpectedContext(messageContext, null);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionResponseWithoutAccept()
|
||||
throws Exception {
|
||||
BdfDictionary msg = getValidIntroductionResponse(false);
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
|
||||
|
||||
public void testRejectsTooShortBodyForRequest() throws Exception {
|
||||
BdfList body = BdfList.of(REQUEST.getValue(), null, authorList);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionResponseWithBrokenTp()
|
||||
throws Exception {
|
||||
BdfDictionary msg = getValidIntroductionResponse(true);
|
||||
BdfDictionary tp = msg.getDictionary(TRANSPORT);
|
||||
tp.put(
|
||||
getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH), "X");
|
||||
msg.put(TRANSPORT, tp);
|
||||
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getBoolean(ACCEPT), msg.getLong(TIME),
|
||||
msg.getRaw(E_PUBLIC_KEY), msg.getDictionary(TRANSPORT));
|
||||
|
||||
public void testRejectsTooLongBodyForRequest() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(REQUEST.getValue(), null, authorList, text, null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionResponseWithoutPublicKey()
|
||||
throws Exception {
|
||||
BdfDictionary msg = getValidIntroductionResponse(true);
|
||||
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getBoolean(ACCEPT), msg.getLong(TIME),
|
||||
msg.getDictionary(TRANSPORT));
|
||||
|
||||
public void testRejectsRawMessageForRequest() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(REQUEST.getValue(), null, authorList, getRandomId());
|
||||
expectParseAuthor(authorList, author);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
private BdfDictionary getValidIntroductionResponse(boolean accept)
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsStringMessageIdForRequest() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(REQUEST.getValue(), "NoMessageId", authorList, null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction ACCEPT
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testAcceptsAccept() throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
|
||||
acceptTimestamp, transportProperties);
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).parseAndValidateTransportPropertiesMap(
|
||||
transportProperties);
|
||||
}});
|
||||
expectEncodeMetadata(ACCEPT);
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertExpectedContext(messageContext, previousMsgId);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortBodyForAccept() throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(),
|
||||
getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongBodyForAccept() throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
|
||||
acceptTimestamp, transportProperties, null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidSessionIdForAccept() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(ACCEPT.getValue(), null, previousMsgId.getBytes(),
|
||||
getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
|
||||
transportProperties);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidPreviousMsgIdForAccept() throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(), 1,
|
||||
getRandomBytes(MAX_PUBLIC_KEY_LENGTH), acceptTimestamp,
|
||||
transportProperties);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongPublicKeyForAccept() throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(),
|
||||
getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
|
||||
transportProperties);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsNegativeTimestampForAccept() throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), getRandomBytes(MAX_PUBLIC_KEY_LENGTH),
|
||||
-1, transportProperties);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsEmptyTransportPropertiesForAccept()
|
||||
throws Exception {
|
||||
BdfList body = BdfList.of(ACCEPT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(),
|
||||
getRandomBytes(MAX_PUBLIC_KEY_LENGTH + 1), acceptTimestamp,
|
||||
new BdfDictionary());
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
byte[] groupId = getRandomId();
|
||||
byte[] sessionId = getRandomId();
|
||||
long time = clock.currentTimeMillis();
|
||||
byte[] publicKey = getRandomBytes(MAX_AGREEMENT_PUBLIC_KEY_BYTES);
|
||||
String transportId =
|
||||
getRandomString(TransportId.MAX_TRANSPORT_ID_LENGTH);
|
||||
BdfDictionary tProps = BdfDictionary.of(
|
||||
new BdfEntry(getRandomString(MAX_PROPERTY_LENGTH),
|
||||
getRandomString(MAX_PROPERTY_LENGTH))
|
||||
);
|
||||
BdfDictionary tp = BdfDictionary.of(
|
||||
new BdfEntry(transportId, tProps)
|
||||
);
|
||||
//
|
||||
// Introduction DECLINE
|
||||
//
|
||||
|
||||
BdfDictionary msg = new BdfDictionary();
|
||||
msg.put(TYPE, TYPE_RESPONSE);
|
||||
msg.put(GROUP_ID, groupId);
|
||||
msg.put(SESSION_ID, sessionId);
|
||||
msg.put(ACCEPT, accept);
|
||||
if (accept) {
|
||||
msg.put(TIME, time);
|
||||
msg.put(E_PUBLIC_KEY, publicKey);
|
||||
msg.put(TRANSPORT, tp);
|
||||
@Test
|
||||
public void testAcceptsDecline() throws Exception {
|
||||
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes());
|
||||
|
||||
expectEncodeMetadata(DECLINE);
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertExpectedContext(messageContext, previousMsgId);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortBodyForDecline() throws Exception {
|
||||
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes());
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongBodyForDecline() throws Exception {
|
||||
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidSessionIdForDecline() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(DECLINE.getValue(), null, previousMsgId.getBytes());
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidPreviousMsgIdForDecline() throws Exception {
|
||||
BdfList body = BdfList.of(DECLINE.getValue(), sessionId.getBytes(), 1);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction AUTH
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testAcceptsAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac, signature);
|
||||
|
||||
expectEncodeMetadata(AUTH);
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertExpectedContext(messageContext, previousMsgId);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortBodyForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongBodyForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac, signature, null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidPreviousMsgIdForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
1, getRandomBytes(MAC_BYTES),
|
||||
signature);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsPreviousMsgIdNullForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(), null,
|
||||
getRandomBytes(MAC_BYTES), signature);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortMacForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1),
|
||||
signature);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongMacForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(),
|
||||
getRandomBytes(MAC_BYTES + 1), signature);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidMacForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), null, signature);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortSignatureForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac, getRandomBytes(0));
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongSignatureForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac,
|
||||
getRandomBytes(MAX_SIGNATURE_BYTES + 1));
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidSignatureForAuth() throws Exception {
|
||||
BdfList body = BdfList.of(AUTH.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac, null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction ACTIVATE
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testAcceptsActivate() throws Exception {
|
||||
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac);
|
||||
|
||||
expectEncodeMetadata(ACTIVATE);
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertExpectedContext(messageContext, previousMsgId);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortBodyForActivate() throws Exception {
|
||||
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes());
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongBodyForActivate() throws Exception {
|
||||
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), mac, null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidSessionIdForActivate() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(ACTIVATE.getValue(), null, previousMsgId.getBytes(),
|
||||
mac);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidPreviousMsgIdForActivate() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), 1, mac);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsPreviousMsgIdNullForActivate() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(), null,
|
||||
mac);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidMacForActivate() throws Exception {
|
||||
BdfList body = BdfList.of(ACTIVATE.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), getRandomBytes(MAC_BYTES - 1));
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction ABORT
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testAcceptsAbort() throws Exception {
|
||||
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes());
|
||||
|
||||
expectEncodeMetadata(ABORT);
|
||||
BdfMessageContext messageContext =
|
||||
validator.validateMessage(message, group, body);
|
||||
|
||||
assertExpectedContext(messageContext, previousMsgId);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooShortBodyForAbort() throws Exception {
|
||||
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes());
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsTooLongBodyForAbort() throws Exception {
|
||||
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(),
|
||||
previousMsgId.getBytes(), null);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidSessionIdForAbort() throws Exception {
|
||||
BdfList body =
|
||||
BdfList.of(ABORT.getValue(), null, previousMsgId.getBytes());
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testRejectsInvalidPreviousMsgIdForAbort() throws Exception {
|
||||
BdfList body = BdfList.of(ABORT.getValue(), sessionId.getBytes(), 1);
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction Helper Methods
|
||||
//
|
||||
|
||||
private void expectEncodeRequestMetadata() {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(messageEncoder).encodeRequestMetadata(message.getTimestamp());
|
||||
will(returnValue(meta));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectEncodeMetadata(MessageType type) {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(messageEncoder)
|
||||
.encodeMetadata(type, sessionId, message.getTimestamp(),
|
||||
false, false, false);
|
||||
will(returnValue(meta));
|
||||
}});
|
||||
}
|
||||
|
||||
private void assertExpectedContext(BdfMessageContext c,
|
||||
@Nullable MessageId dependency) {
|
||||
assertEquals(meta, c.getDictionary());
|
||||
if (dependency == null) {
|
||||
assertEquals(0, c.getDependencies().size());
|
||||
} else {
|
||||
assertEquals(dependency, c.getDependencies().iterator().next());
|
||||
}
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction ACK
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testValidateProperIntroductionAck() throws Exception {
|
||||
byte[] sessionId = getRandomId();
|
||||
byte[] mac = getRandomBytes(MAC_LENGTH);
|
||||
byte[] sig = getRandomBytes(MAX_SIGNATURE_LENGTH);
|
||||
BdfList body = BdfList.of(TYPE_ACK, sessionId, mac, sig);
|
||||
|
||||
BdfDictionary result =
|
||||
validator.validateMessage(message, group, body).getDictionary();
|
||||
|
||||
assertEquals(Long.valueOf(TYPE_ACK), result.getLong(TYPE));
|
||||
assertArrayEquals(sessionId, result.getRaw(SESSION_ID));
|
||||
assertArrayEquals(mac, result.getRaw(MAC));
|
||||
assertArrayEquals(sig, result.getRaw(SIGNATURE));
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateTooLongIntroductionAck() throws Exception {
|
||||
BdfDictionary msg = BdfDictionary.of(
|
||||
new BdfEntry(TYPE, TYPE_ACK),
|
||||
new BdfEntry(SESSION_ID, getRandomId()),
|
||||
new BdfEntry("garbage", getRandomString(255))
|
||||
);
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getString("garbage"));
|
||||
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateIntroductionAckWithLongSessionId()
|
||||
throws Exception {
|
||||
BdfDictionary msg = BdfDictionary.of(
|
||||
new BdfEntry(TYPE, TYPE_ACK),
|
||||
new BdfEntry(SESSION_ID, new byte[SessionId.LENGTH + 1])
|
||||
);
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
|
||||
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
//
|
||||
// Introduction Abort
|
||||
//
|
||||
|
||||
@Test
|
||||
public void testValidateProperIntroductionAbort() throws Exception {
|
||||
byte[] sessionId = getRandomId();
|
||||
|
||||
BdfDictionary msg = new BdfDictionary();
|
||||
msg.put(TYPE, TYPE_ABORT);
|
||||
msg.put(SESSION_ID, sessionId);
|
||||
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID));
|
||||
|
||||
BdfDictionary result =
|
||||
validator.validateMessage(message, group, body).getDictionary();
|
||||
|
||||
assertEquals(Long.valueOf(TYPE_ABORT), result.getLong(TYPE));
|
||||
assertEquals(sessionId, result.getRaw(SESSION_ID));
|
||||
context.assertIsSatisfied();
|
||||
}
|
||||
|
||||
@Test(expected = FormatException.class)
|
||||
public void testValidateTooLongIntroductionAbort() throws Exception {
|
||||
BdfDictionary msg = BdfDictionary.of(
|
||||
new BdfEntry(TYPE, TYPE_ABORT),
|
||||
new BdfEntry(SESSION_ID, getRandomId()),
|
||||
new BdfEntry("garbage", getRandomString(255))
|
||||
);
|
||||
BdfList body = BdfList.of(msg.getLong(TYPE), msg.getRaw(SESSION_ID),
|
||||
msg.getString("garbage"));
|
||||
|
||||
validator.validateMessage(message, group, body);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -368,14 +368,6 @@ public class GroupMessageValidatorTest extends ValidatorTestCase {
|
||||
.getBoolean(KEY_INITIAL_JOIN_MSG));
|
||||
}
|
||||
|
||||
private void expectParseAuthor(BdfList authorList, Author author)
|
||||
throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).parseAndValidateAuthor(authorList);
|
||||
will(returnValue(author));
|
||||
}});
|
||||
}
|
||||
|
||||
private void expectRejectAuthor(BdfList authorList) throws Exception {
|
||||
context.checking(new Expectations() {{
|
||||
oneOf(clientHelper).parseAndValidateAuthor(authorList);
|
||||
|
||||
@@ -150,7 +150,7 @@ public abstract class SharingValidatorTest extends ValidatorTestCase {
|
||||
}
|
||||
|
||||
void assertExpectedContext(BdfMessageContext messageContext,
|
||||
@Nullable MessageId previousMsgId) throws FormatException {
|
||||
@Nullable MessageId previousMsgId) {
|
||||
Collection<MessageId> dependencies = messageContext.getDependencies();
|
||||
if (previousMsgId == null) {
|
||||
assertTrue(dependencies.isEmpty());
|
||||
|
||||
@@ -36,7 +36,10 @@ import org.briarproject.briar.api.privategroup.invitation.GroupInvitationManager
|
||||
import org.briarproject.briar.blog.BlogModule;
|
||||
import org.briarproject.briar.client.BriarClientModule;
|
||||
import org.briarproject.briar.forum.ForumModule;
|
||||
import org.briarproject.briar.introduction.IntroductionCryptoIntegrationTest;
|
||||
import org.briarproject.briar.introduction.IntroductionModule;
|
||||
import org.briarproject.briar.introduction.MessageEncoderParserIntegrationTest;
|
||||
import org.briarproject.briar.introduction.SessionEncoderParserIntegrationTest;
|
||||
import org.briarproject.briar.messaging.MessagingModule;
|
||||
import org.briarproject.briar.privategroup.PrivateGroupModule;
|
||||
import org.briarproject.briar.privategroup.invitation.GroupInvitationModule;
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
package org.briarproject.briar.test;
|
||||
|
||||
import org.briarproject.bramble.api.crypto.CryptoComponent;
|
||||
import org.briarproject.bramble.api.crypto.KeyPair;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.identity.Author;
|
||||
import org.briarproject.bramble.api.identity.AuthorFactory;
|
||||
import org.briarproject.bramble.api.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.sync.GroupId;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.MessageTracker.GroupCount;
|
||||
|
||||
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.bramble.test.TestUtils.getRandomBytes;
|
||||
import static org.briarproject.bramble.util.StringUtils.getRandomString;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
public class BriarTestUtils {
|
||||
@@ -25,4 +33,17 @@ public class BriarTestUtils {
|
||||
assertEquals(unreadCount, c1.getUnreadCount());
|
||||
}
|
||||
|
||||
public static Author getRealAuthor(AuthorFactory authorFactory) {
|
||||
return authorFactory.createAuthor(getRandomString(5),
|
||||
getRandomBytes(MAX_PUBLIC_KEY_LENGTH));
|
||||
}
|
||||
|
||||
public static LocalAuthor getRealLocalAuthor(
|
||||
CryptoComponent cryptoComponent, AuthorFactory authorFactory) {
|
||||
KeyPair keyPair = cryptoComponent.generateSignatureKeyPair();
|
||||
return authorFactory.createLocalAuthor(getRandomString(5),
|
||||
keyPair.getPublic().getEncoded(),
|
||||
keyPair.getPrivate().getEncoded());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user