mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
IntroductionManager and Protocol Engines
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.briar.api.introduction2;
|
||||
|
||||
import org.briarproject.bramble.api.contact.Contact;
|
||||
import org.briarproject.bramble.api.contact.ContactId;
|
||||
import org.briarproject.bramble.api.db.DbException;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.ClientId;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
import org.briarproject.briar.api.messaging.ConversationManager.ConversationClient;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface IntroductionManager extends ConversationClient {
|
||||
|
||||
/**
|
||||
* The unique ID of the introduction client.
|
||||
*/
|
||||
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.introduction");
|
||||
|
||||
/**
|
||||
* The current version of the introduction client.
|
||||
*/
|
||||
int CLIENT_VERSION = 1;
|
||||
|
||||
/**
|
||||
* Sends two initial introduction messages.
|
||||
*/
|
||||
void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
|
||||
long timestamp) throws DbException;
|
||||
|
||||
/**
|
||||
* Accepts an introduction.
|
||||
*/
|
||||
void acceptIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException;
|
||||
|
||||
/**
|
||||
* Declines an introduction.
|
||||
*/
|
||||
void declineIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all introduction messages for the given contact.
|
||||
*/
|
||||
Collection<IntroductionMessage> getIntroductionMessages(ContactId contactId)
|
||||
throws DbException;
|
||||
|
||||
}
|
||||
@@ -40,6 +40,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
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package org.briarproject.briar.introduction2;
|
||||
|
||||
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.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.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.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.MessageTracker;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_ID;
|
||||
import static org.briarproject.briar.api.introduction2.IntroductionManager.CLIENT_VERSION;
|
||||
import static org.briarproject.briar.introduction2.MessageType.ABORT;
|
||||
import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
|
||||
import static org.briarproject.briar.introduction2.MessageType.ACTIVATE;
|
||||
import static org.briarproject.briar.introduction2.MessageType.AUTH;
|
||||
import static org.briarproject.briar.introduction2.MessageType.DECLINE;
|
||||
import static org.briarproject.briar.introduction2.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)
|
||||
throws DbException {
|
||||
Message m = messageEncoder
|
||||
.encodeActivateMessage(s.getContactGroupId(), timestamp,
|
||||
s.getLastLocalMessageId(), s.getSessionId());
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
|
||||
void markRequestUnavailableToAnswer(Transaction txn, MessageId m)
|
||||
throws DbException {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
messageEncoder.setAvailableToAnswer(meta, false);
|
||||
try {
|
||||
clientHelper.mergeMessageMetadata(txn, m, meta);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
Map<MessageId, BdfDictionary> getSessions(Transaction txn,
|
||||
BdfDictionary query) throws DbException, FormatException {
|
||||
return clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, getLocalGroup().getId(),
|
||||
query);
|
||||
}
|
||||
|
||||
private Group getLocalGroup() {
|
||||
return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
|
||||
}
|
||||
|
||||
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,510 @@
|
||||
package org.briarproject.briar.introduction2;
|
||||
|
||||
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.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.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.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.introduction2.IntroductionRequest;
|
||||
import org.briarproject.briar.api.introduction2.IntroductionResponse;
|
||||
import org.briarproject.briar.api.introduction2.event.IntroductionRequestReceivedEvent;
|
||||
import org.briarproject.briar.api.introduction2.event.IntroductionResponseReceivedEvent;
|
||||
import org.briarproject.briar.api.introduction2.event.IntroductionSucceededEvent;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
|
||||
import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_AUTH;
|
||||
import static org.briarproject.briar.introduction2.IntroduceeState.AWAIT_RESPONSES;
|
||||
import static org.briarproject.briar.introduction2.IntroduceeState.LOCAL_ACCEPTED;
|
||||
import static org.briarproject.briar.introduction2.IntroduceeState.REMOTE_ACCEPTED;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class IntroduceeProtocolEngine
|
||||
extends AbstractProtocolEngine<IntroduceeSession> {
|
||||
|
||||
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, FormatException {
|
||||
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, FormatException {
|
||||
switch (session.getState()) {
|
||||
case AWAIT_RESPONSES:
|
||||
case LOCAL_ACCEPTED:
|
||||
return onRemoteAccept(txn, session, m);
|
||||
case START:
|
||||
case LOCAL_DECLINED:
|
||||
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, FormatException {
|
||||
switch (session.getState()) {
|
||||
case START:
|
||||
return session; // Ignore in the START state
|
||||
case AWAIT_RESPONSES:
|
||||
case LOCAL_DECLINED:
|
||||
case LOCAL_ACCEPTED:
|
||||
return onRemoteDecline(txn, session, m);
|
||||
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, FormatException {
|
||||
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, FormatException {
|
||||
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, FormatException {
|
||||
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
|
||||
markMessageVisibleInUi(txn, m.getMessageId());
|
||||
|
||||
// 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
|
||||
Contact c = contactManager.getContact(txn, s.getIntroducer().getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
boolean contactExists = false; // TODO
|
||||
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
|
||||
MessageId requestId = s.getLastRemoteMessageId();
|
||||
if (requestId == null) throw new IllegalStateException();
|
||||
markRequestUnavailableToAnswer(txn, requestId);
|
||||
|
||||
// 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, 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
|
||||
MessageId requestId = s.getLastRemoteMessageId();
|
||||
if (requestId == null) throw new IllegalStateException();
|
||||
markRequestUnavailableToAnswer(txn, requestId);
|
||||
|
||||
// Send a DECLINE message
|
||||
long localTimestamp = Math.max(timestamp, getLocalTimestamp(s));
|
||||
Message sent = sendDeclineMessage(txn, s, localTimestamp, true);
|
||||
// Track the message
|
||||
messageTracker.trackOutgoingMessage(txn, sent);
|
||||
|
||||
// Move to the START state
|
||||
return IntroduceeSession.clear(s, sent.getId(), sent.getTimestamp(),
|
||||
s.getLastRemoteMessageId());
|
||||
}
|
||||
|
||||
private IntroduceeSession onRemoteAccept(Transaction txn,
|
||||
IntroduceeSession s, AcceptMessage m)
|
||||
throws DbException, FormatException {
|
||||
// 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);
|
||||
|
||||
// Broadcast IntroductionResponseReceivedEvent
|
||||
Contact c = contactManager.getContact(s.getIntroducer().getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
IntroductionResponse request =
|
||||
new IntroductionResponse(s.getSessionId(), m.getMessageId(),
|
||||
m.getGroupId(), INTRODUCEE, m.getTimestamp(), false,
|
||||
false, false, false, s.getRemoteAuthor().getName(),
|
||||
true);
|
||||
IntroductionResponseReceivedEvent e =
|
||||
new IntroductionResponseReceivedEvent(c.getId(), request);
|
||||
txn.attach(e);
|
||||
|
||||
// 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);
|
||||
|
||||
// Move back to START state
|
||||
return IntroduceeSession
|
||||
.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
|
||||
m.getMessageId());
|
||||
}
|
||||
|
||||
private IntroduceeSession onLocalAuth(Transaction txn, IntroduceeSession s)
|
||||
throws DbException {
|
||||
boolean alice = isAlice(txn, s);
|
||||
byte[] mac;
|
||||
byte[] signature;
|
||||
SecretKey masterKey;
|
||||
try {
|
||||
masterKey = crypto.deriveMasterKey(s, alice);
|
||||
SecretKey macKey = crypto.deriveMacKey(masterKey, alice);
|
||||
LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
|
||||
mac = crypto.mac(macKey, s, localAuthor.getId(), alice);
|
||||
signature = crypto.sign(macKey, localAuthor.getPrivateKey());
|
||||
} catch (GeneralSecurityException e) {
|
||||
// TODO
|
||||
return abort(txn, s);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
if (s.getState() != AWAIT_AUTH) throw new AssertionError();
|
||||
Message sent = sendAuthMessage(txn, s, getLocalTimestamp(s), mac,
|
||||
signature);
|
||||
return IntroduceeSession.addLocalAuth(s, AWAIT_AUTH, masterKey, sent);
|
||||
}
|
||||
|
||||
private IntroduceeSession onRemoteAuth(Transaction txn,
|
||||
IntroduceeSession s, AuthMessage m)
|
||||
throws DbException, FormatException {
|
||||
// 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.verifyMac(m.getMac(), s, localAuthor.getId());
|
||||
crypto.verifySignature(m.getSignature(), s, localAuthor.getId());
|
||||
} catch (GeneralSecurityException e) {
|
||||
return abort(txn, s);
|
||||
}
|
||||
|
||||
try {
|
||||
ContactId c = contactManager
|
||||
.addContact(txn, s.getRemoteAuthor(), localAuthor.getId(),
|
||||
false, false);
|
||||
//noinspection ConstantConditions
|
||||
transportPropertyManager.addRemoteProperties(txn, c,
|
||||
s.getRemoteTransportProperties());
|
||||
} catch (ContactExistsException e) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
long timestamp =
|
||||
Math.min(s.getAcceptTimestamp(), s.getRemoteAcceptTimestamp());
|
||||
if (timestamp == -1) throw new AssertionError();
|
||||
|
||||
//noinspection ConstantConditions
|
||||
Map<TransportId, KeySetId> keys = keyManager
|
||||
.addUnboundKeys(txn, new SecretKey(s.getMasterKey()), timestamp,
|
||||
isAlice(txn, s));
|
||||
|
||||
Message sent = sendActivateMessage(txn, s, getLocalTimestamp(s));
|
||||
|
||||
// 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);
|
||||
|
||||
Contact c = contactManager.getContact(txn, s.getRemoteAuthor().getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
keyManager.bindKeys(txn, c.getId(), s.getTransportKeys());
|
||||
keyManager.activateKeys(txn, s.getTransportKeys());
|
||||
|
||||
// TODO remove when concept of inactive contacts is removed
|
||||
contactManager.setContactActive(txn, c.getId(), true);
|
||||
|
||||
// Broadcast IntroductionSucceededEvent
|
||||
IntroductionSucceededEvent e = new IntroductionSucceededEvent(c);
|
||||
txn.attach(e);
|
||||
|
||||
// Move back to START state
|
||||
return IntroduceeSession
|
||||
.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
|
||||
m.getMessageId());
|
||||
}
|
||||
|
||||
private IntroduceeSession onRemoteAbort(Transaction txn,
|
||||
IntroduceeSession s, AbortMessage m)
|
||||
throws DbException {
|
||||
// Mark the request message unavailable to answer
|
||||
MessageId requestId = s.getLastRemoteMessageId();
|
||||
if (requestId == null) throw new IllegalStateException();
|
||||
markRequestUnavailableToAnswer(txn, requestId);
|
||||
|
||||
// Reset the session back to initial state
|
||||
return IntroduceeSession
|
||||
.clear(s, s.getLastLocalMessageId(), s.getLocalTimestamp(),
|
||||
m.getMessageId());
|
||||
}
|
||||
|
||||
private IntroduceeSession abort(Transaction txn, IntroduceeSession s)
|
||||
throws DbException {
|
||||
// Mark the request message unavailable to answer
|
||||
MessageId requestId = s.getLastRemoteMessageId();
|
||||
if (requestId == null) throw new IllegalStateException();
|
||||
markRequestUnavailableToAnswer(txn, requestId);
|
||||
|
||||
// Send an ABORT message
|
||||
Message sent = sendAbortMessage(txn, s, getLocalTimestamp(s));
|
||||
|
||||
// Reset the session back to initial state
|
||||
return IntroduceeSession.clear(s, 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 boolean isAlice(Transaction txn, IntroduceeSession s)
|
||||
throws DbException {
|
||||
Author localAuthor = identityManager.getLocalAuthor(txn);
|
||||
return crypto.isAlice(localAuthor.getId(), s.getRemoteAuthor().getId());
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,462 @@
|
||||
package org.briarproject.briar.introduction2;
|
||||
|
||||
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.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.introduction2.IntroductionResponse;
|
||||
import org.briarproject.briar.api.introduction2.event.IntroductionResponseReceivedEvent;
|
||||
import org.briarproject.briar.introduction2.IntroducerSession.Introducee;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATES;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATE_A;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_ACTIVATE_B;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTHS;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTH_A;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_AUTH_B;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSES;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSE_A;
|
||||
import static org.briarproject.briar.introduction2.IntroducerState.AWAIT_RESPONSE_B;
|
||||
import static org.briarproject.briar.introduction2.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 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
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntroducerSession onRequestMessage(Transaction txn,
|
||||
IntroducerSession s, RequestMessage m)
|
||||
throws DbException, FormatException {
|
||||
return abort(txn, s); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public IntroducerSession onAcceptMessage(Transaction txn,
|
||||
IntroducerSession s, AcceptMessage m)
|
||||
throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case AWAIT_RESPONSES:
|
||||
case AWAIT_RESPONSE_A:
|
||||
case AWAIT_RESPONSE_B:
|
||||
return onRemoteAccept(txn, s, m);
|
||||
case START:
|
||||
// TODO check and update lastRemoteMsgId?
|
||||
return s; // Ignored in this state
|
||||
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, FormatException {
|
||||
switch (s.getState()) {
|
||||
case AWAIT_RESPONSES:
|
||||
case AWAIT_RESPONSE_A:
|
||||
case AWAIT_RESPONSE_B:
|
||||
return onRemoteDecline(txn, s, m);
|
||||
case START:
|
||||
// TODO check and update lastRemoteMsgId?
|
||||
return s; // Ignored in this state
|
||||
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, FormatException {
|
||||
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 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, FormatException {
|
||||
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 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, FormatException {
|
||||
return onRemoteAbort(txn, s, m);
|
||||
}
|
||||
|
||||
private IntroducerSession onLocalRequest(Transaction txn,
|
||||
IntroducerSession s,
|
||||
@Nullable String message, long timestamp) throws DbException {
|
||||
// Send REQUEST messages
|
||||
long localTimestamp =
|
||||
Math.max(timestamp, getLocalTimestamp(s, s.getIntroducee1()));
|
||||
Message sent1 = sendRequestMessage(txn, s.getIntroducee1(),
|
||||
localTimestamp, s.getIntroducee2().author, message
|
||||
);
|
||||
Message sent2 = sendRequestMessage(txn, s.getIntroducee2(),
|
||||
localTimestamp, s.getIntroducee1().author, message
|
||||
);
|
||||
// Track the messages
|
||||
messageTracker.trackOutgoingMessage(txn, sent1);
|
||||
messageTracker.trackOutgoingMessage(txn, sent2);
|
||||
// Move to the AWAIT_RESPONSES state
|
||||
Introducee introducee1 = new Introducee(s.getIntroducee1(), sent1);
|
||||
Introducee introducee2 = new Introducee(s.getIntroducee2(), sent2);
|
||||
return new IntroducerSession(s.getSessionId(), AWAIT_RESPONSES,
|
||||
localTimestamp, introducee1, introducee2);
|
||||
}
|
||||
|
||||
private IntroducerSession onRemoteAccept(Transaction txn,
|
||||
IntroducerSession s, AcceptMessage m)
|
||||
throws DbException, FormatException {
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
|
||||
// Move to the next state
|
||||
IntroducerState state = AWAIT_AUTHS;
|
||||
Introducee introducee1, introducee2;
|
||||
Contact c;
|
||||
if (i.equals(s.getIntroducee1())) {
|
||||
if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_A;
|
||||
introducee1 = new Introducee(s.getIntroducee1(), sent);
|
||||
introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
|
||||
c = contactManager.getContact(s.getIntroducee2().author.getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
} else if (i.equals(s.getIntroducee2())) {
|
||||
if (s.getState() == AWAIT_RESPONSES) state = AWAIT_RESPONSE_B;
|
||||
introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
|
||||
introducee2 = new Introducee(s.getIntroducee2(), sent);
|
||||
c = contactManager.getContact(s.getIntroducee1().author.getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
} else throw new AssertionError();
|
||||
|
||||
// Broadcast IntroductionResponseReceivedEvent
|
||||
IntroductionResponse request =
|
||||
new IntroductionResponse(s.getSessionId(), m.getMessageId(),
|
||||
m.getGroupId(), INTRODUCER, m.getTimestamp(), false,
|
||||
false, false, false, c.getAuthor().getName(), true);
|
||||
IntroductionResponseReceivedEvent e =
|
||||
new IntroductionResponseReceivedEvent(c.getId(), request);
|
||||
txn.attach(e);
|
||||
|
||||
return new IntroducerSession(s.getSessionId(), state,
|
||||
s.getRequestTimestamp(), introducee1, introducee2);
|
||||
}
|
||||
|
||||
private IntroducerSession onRemoteDecline(Transaction txn,
|
||||
IntroducerSession s, DeclineMessage m)
|
||||
throws DbException, FormatException {
|
||||
// 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);
|
||||
|
||||
// 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);
|
||||
// Track the message
|
||||
messageTracker.trackOutgoingMessage(txn, sent);
|
||||
|
||||
// Move to the START state
|
||||
Introducee introducee1, introducee2;
|
||||
Contact c;
|
||||
if (i.equals(s.getIntroducee1())) {
|
||||
introducee1 = new Introducee(s.getIntroducee1(), sent);
|
||||
introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
|
||||
c = contactManager.getContact(s.getIntroducee2().author.getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
} else if (i.equals(s.getIntroducee2())) {
|
||||
introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
|
||||
introducee2 = new Introducee(s.getIntroducee2(), sent);
|
||||
c = contactManager.getContact(s.getIntroducee2().author.getId(),
|
||||
identityManager.getLocalAuthor(txn).getId());
|
||||
} else throw new AssertionError();
|
||||
|
||||
// Broadcast IntroductionResponseReceivedEvent
|
||||
IntroductionResponse request =
|
||||
new IntroductionResponse(s.getSessionId(), m.getMessageId(),
|
||||
m.getGroupId(), INTRODUCER, m.getTimestamp(), false,
|
||||
false, false, false, c.getAuthor().getName(), false);
|
||||
IntroductionResponseReceivedEvent e =
|
||||
new IntroductionResponseReceivedEvent(c.getId(), request);
|
||||
txn.attach(e);
|
||||
|
||||
return new IntroducerSession(s.getSessionId(), START,
|
||||
s.getRequestTimestamp(), introducee1, introducee2);
|
||||
}
|
||||
|
||||
private IntroducerSession onRemoteAuth(Transaction txn,
|
||||
IntroducerSession s, AuthMessage m)
|
||||
throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
|
||||
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 introducee1, introducee2;
|
||||
if (i.equals(s.getIntroducee1())) {
|
||||
if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_A;
|
||||
introducee1 = new Introducee(s.getIntroducee1(), sent);
|
||||
introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
|
||||
} else if (i.equals(s.getIntroducee2())) {
|
||||
if (s.getState() == AWAIT_AUTHS) state = AWAIT_AUTH_B;
|
||||
introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
|
||||
introducee2 = new Introducee(s.getIntroducee2(), sent);
|
||||
} else throw new AssertionError();
|
||||
return new IntroducerSession(s.getSessionId(), state,
|
||||
s.getRequestTimestamp(), introducee1, introducee2);
|
||||
}
|
||||
|
||||
private IntroducerSession onRemoteActivate(Transaction txn,
|
||||
IntroducerSession s, ActivateMessage m)
|
||||
throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (isInvalidDependency(s, m.getGroupId(), m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
|
||||
// Forward AUTH message
|
||||
Introducee i = getOtherIntroducee(s, m.getGroupId());
|
||||
long timestamp = getLocalTimestamp(s, i);
|
||||
Message sent = sendActivateMessage(txn, i, timestamp);
|
||||
|
||||
// Move to the next state
|
||||
IntroducerState state = START;
|
||||
Introducee introducee1, introducee2;
|
||||
if (i.equals(s.getIntroducee1())) {
|
||||
if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_A;
|
||||
introducee1 = new Introducee(s.getIntroducee1(), sent);
|
||||
introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
|
||||
} else if (i.equals(s.getIntroducee2())) {
|
||||
if (s.getState() == AWAIT_ACTIVATES) state = AWAIT_ACTIVATE_B;
|
||||
introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
|
||||
introducee2 = new Introducee(s.getIntroducee2(), sent);
|
||||
} else throw new AssertionError();
|
||||
return new IntroducerSession(s.getSessionId(), state,
|
||||
s.getRequestTimestamp(), introducee1, introducee2);
|
||||
}
|
||||
|
||||
private IntroducerSession onRemoteAbort(Transaction txn,
|
||||
IntroducerSession s, AbortMessage m)
|
||||
throws DbException, FormatException {
|
||||
// Mark any REQUEST messages in the session unavailable to answer
|
||||
markRequestsUnavailableToAnswer(txn, s);
|
||||
|
||||
// Forward ABORT message
|
||||
Introducee i = getOtherIntroducee(s, m.getGroupId());
|
||||
long timestamp = getLocalTimestamp(s, i);
|
||||
Message sent = sendAbortMessage(txn, i, timestamp);
|
||||
|
||||
// Reset the session back to initial state
|
||||
Introducee introducee1, introducee2;
|
||||
if (i.equals(s.getIntroducee1())) {
|
||||
introducee1 = new Introducee(s.getIntroducee1(), sent);
|
||||
introducee2 = new Introducee(s.getIntroducee2(), m.getMessageId());
|
||||
} else if (i.equals(s.getIntroducee2())) {
|
||||
introducee1 = new Introducee(s.getIntroducee1(), m.getMessageId());
|
||||
introducee2 = new Introducee(s.getIntroducee2(), sent);
|
||||
} else throw new AssertionError();
|
||||
return new IntroducerSession(s.getSessionId(), START,
|
||||
s.getRequestTimestamp(), introducee1, introducee2);
|
||||
}
|
||||
|
||||
private IntroducerSession abort(Transaction txn,
|
||||
IntroducerSession s) throws DbException, FormatException {
|
||||
// Mark any REQUEST messages in the session unavailable to answer
|
||||
markRequestsUnavailableToAnswer(txn, s);
|
||||
// Send an ABORT message to both introducees
|
||||
long timestamp1 = getLocalTimestamp(s, s.getIntroducee1());
|
||||
Message sent1 = sendAbortMessage(txn, s.getIntroducee1(), timestamp1);
|
||||
long timestamp2 = getLocalTimestamp(s, s.getIntroducee2());
|
||||
Message sent2 = sendAbortMessage(txn, s.getIntroducee2(), timestamp2);
|
||||
// Reset the session back to initial state
|
||||
Introducee introducee1 = new Introducee(s.getIntroducee1(), sent1);
|
||||
Introducee introducee2 = new Introducee(s.getIntroducee2(), sent2);
|
||||
return new IntroducerSession(s.getSessionId(), START,
|
||||
s.getRequestTimestamp(), introducee1, introducee2);
|
||||
}
|
||||
|
||||
private void markRequestsUnavailableToAnswer(Transaction txn, Session s)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary query = messageParser
|
||||
.getInvitesAvailableToAnswerQuery(s.getSessionId());
|
||||
Map<MessageId, BdfDictionary> results = getSessions(txn, query);
|
||||
for (MessageId m : results.keySet())
|
||||
markRequestUnavailableToAnswer(txn, m);
|
||||
}
|
||||
|
||||
private Introducee getIntroducee(IntroducerSession s, GroupId g) {
|
||||
if (s.getIntroducee1().groupId.equals(g)) return s.getIntroducee1();
|
||||
else if (s.getIntroducee2().groupId.equals(g))
|
||||
return s.getIntroducee2();
|
||||
else throw new AssertionError();
|
||||
}
|
||||
|
||||
private Introducee getOtherIntroducee(IntroducerSession s, GroupId g) {
|
||||
if (s.getIntroducee1().groupId.equals(g)) return s.getIntroducee2();
|
||||
else if (s.getIntroducee2().groupId.equals(g))
|
||||
return s.getIntroducee1();
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package org.briarproject.briar.introduction2;
|
||||
|
||||
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";
|
||||
|
||||
@@ -0,0 +1,459 @@
|
||||
package org.briarproject.briar.introduction2;
|
||||
|
||||
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.ContactHook;
|
||||
import org.briarproject.bramble.api.data.BdfDictionary;
|
||||
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.Metadata;
|
||||
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.identity.LocalAuthor;
|
||||
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.bramble.api.sync.Client;
|
||||
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.sync.MessageStatus;
|
||||
import org.briarproject.briar.api.client.MessageTracker;
|
||||
import org.briarproject.briar.api.client.SessionId;
|
||||
import org.briarproject.briar.api.introduction2.IntroductionManager;
|
||||
import org.briarproject.briar.api.introduction2.IntroductionMessage;
|
||||
import org.briarproject.briar.api.introduction2.IntroductionRequest;
|
||||
import org.briarproject.briar.api.introduction2.IntroductionResponse;
|
||||
import org.briarproject.briar.api.introduction2.Role;
|
||||
import org.briarproject.briar.client.ConversationClientImpl;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.bramble.api.sync.Group.Visibility.SHARED;
|
||||
import static org.briarproject.briar.api.introduction2.Role.INTRODUCEE;
|
||||
import static org.briarproject.briar.api.introduction2.Role.INTRODUCER;
|
||||
import static org.briarproject.briar.introduction2.IntroductionConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.briar.introduction2.MessageType.ABORT;
|
||||
import static org.briarproject.briar.introduction2.MessageType.ACCEPT;
|
||||
import static org.briarproject.briar.introduction2.MessageType.ACTIVATE;
|
||||
import static org.briarproject.briar.introduction2.MessageType.AUTH;
|
||||
import static org.briarproject.briar.introduction2.MessageType.DECLINE;
|
||||
import static org.briarproject.briar.introduction2.MessageType.REQUEST;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class IntroductionManagerImpl extends ConversationClientImpl
|
||||
implements IntroductionManager, Client, ContactHook {
|
||||
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
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;
|
||||
|
||||
@Inject
|
||||
IntroductionManagerImpl(
|
||||
DatabaseComponent db,
|
||||
ClientHelper clientHelper,
|
||||
MetadataParser metadataParser,
|
||||
MessageTracker messageTracker,
|
||||
ContactGroupFactory contactGroupFactory,
|
||||
MessageParser messageParser,
|
||||
SessionEncoder sessionEncoder,
|
||||
SessionParser sessionParser,
|
||||
IntroducerProtocolEngine introducerEngine,
|
||||
IntroduceeProtocolEngine introduceeEngine,
|
||||
IntroductionCrypto crypto,
|
||||
IdentityManager identityManager) {
|
||||
super(db, clientHelper, metadataParser, messageTracker);
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.messageParser = messageParser;
|
||||
this.sessionEncoder = sessionEncoder;
|
||||
this.sessionParser = sessionParser;
|
||||
this.introducerEngine = introducerEngine;
|
||||
this.introduceeEngine = introduceeEngine;
|
||||
this.crypto = crypto;
|
||||
this.identityManager = identityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createLocalState(Transaction txn) throws DbException {
|
||||
// Create a local group to store protocol sessions
|
||||
Group localGroup = getLocalGroup();
|
||||
if (db.containsGroup(txn, localGroup.getId())) return;
|
||||
db.addGroup(txn, localGroup);
|
||||
// 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);
|
||||
// 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 meta = new BdfDictionary();
|
||||
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||
try {
|
||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
// Remove the contact group (all messages will be removed with it)
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
// TODO abort other sessions the contact is involved in
|
||||
}
|
||||
|
||||
@Override
|
||||
public Group getContactGroup(Contact c) {
|
||||
return contactGroupFactory
|
||||
.createContactGroup(CLIENT_ID, CLIENT_VERSION, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||
BdfDictionary bdfMeta) throws DbException, FormatException {
|
||||
// Parse the metadata
|
||||
MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
|
||||
// Look up the session, if there is one
|
||||
SessionId sessionId = meta.getSessionId();
|
||||
IntroduceeSession newIntroduceeSession = null;
|
||||
if (sessionId == null) {
|
||||
if (meta.getMessageType() != REQUEST) throw new AssertionError();
|
||||
newIntroduceeSession = createNewIntroduceeSession(txn, m, body);
|
||||
sessionId = newIntroduceeSession.getSessionId();
|
||||
}
|
||||
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 alice = identityManager.getLocalAuthor(txn);
|
||||
Author bob = messageParser.parseRequestMessage(m, body).getAuthor();
|
||||
SessionId sessionId = crypto.getSessionId(introducer, alice, bob);
|
||||
return IntroduceeSession
|
||||
.getInitial(m.getGroupId(), sessionId, introducer, bob);
|
||||
}
|
||||
|
||||
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, getLocalGroup().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(getLocalGroup().getId());
|
||||
db.addLocalMessage(txn, m, new Metadata(), false);
|
||||
return m.getId();
|
||||
}
|
||||
|
||||
private void storeSession(Transaction txn, MessageId storageId,
|
||||
Session session) throws DbException, FormatException {
|
||||
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();
|
||||
}
|
||||
clientHelper.mergeMessageMetadata(txn, storageId, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeIntroduction(Contact c1, Contact c2, @Nullable String msg,
|
||||
long timestamp) throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
// 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();
|
||||
session = new IntroducerSession(sessionId, groupId1,
|
||||
c1.getAuthor(), groupId2, c2.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 {
|
||||
respondToRequest(contactId, sessionId, timestamp, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void declineIntroduction(ContactId contactId, SessionId sessionId,
|
||||
long timestamp) throws DbException {
|
||||
respondToRequest(contactId, sessionId, timestamp, false);
|
||||
}
|
||||
|
||||
private void respondToRequest(ContactId contactId, SessionId sessionId,
|
||||
long timestamp, boolean accept) throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
// Look up the session
|
||||
StoredSession ss = getSession(txn, sessionId);
|
||||
if (ss == null) throw new IllegalArgumentException();
|
||||
// Parse the session
|
||||
Contact contact = db.getContact(txn, contactId);
|
||||
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||
IntroduceeSession session = sessionParser
|
||||
.parseIntroduceeSession(contactGroupId, ss.bdfSession);
|
||||
// Handle the join or leave action
|
||||
if (accept) {
|
||||
session = introduceeEngine
|
||||
.onAcceptAction(txn, session, timestamp);
|
||||
} else {
|
||||
session = introduceeEngine
|
||||
.onDeclineAction(txn, session, timestamp);
|
||||
}
|
||||
// Store the updated session
|
||||
storeSession(txn, ss.storageId, session);
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IntroductionMessage> getIntroductionMessages(ContactId c)
|
||||
throws DbException {
|
||||
List<IntroductionMessage> messages;
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
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 (Map.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(txn, contactGroupId, m,
|
||||
meta, status, ss.bdfSession, true));
|
||||
} else if (type == DECLINE) {
|
||||
messages.add(
|
||||
parseInvitationResponse(txn, contactGroupId, m,
|
||||
meta, status, ss.bdfSession, false));
|
||||
}
|
||||
}
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
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();
|
||||
LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
|
||||
if (localAuthor.equals(session.getIntroducee1().author)) {
|
||||
author = session.getIntroducee2().author;
|
||||
} else {
|
||||
author = session.getIntroducee1().author;
|
||||
}
|
||||
} else if (role == INTRODUCEE) {
|
||||
IntroduceeSession session = sessionParser
|
||||
.parseIntroduceeSession(contactGroupId, bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
author = session.getRemoteAuthor();
|
||||
} else throw new AssertionError();
|
||||
String message = ""; // TODO
|
||||
boolean contactExists = false; // TODO
|
||||
|
||||
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(Transaction txn,
|
||||
GroupId contactGroupId, MessageId m, MessageMetadata meta,
|
||||
MessageStatus status, BdfDictionary bdfSession, boolean accept)
|
||||
throws FormatException, DbException {
|
||||
Role role = sessionParser.getRole(bdfSession);
|
||||
SessionId sessionId;
|
||||
Author author;
|
||||
if (role == INTRODUCER) {
|
||||
IntroducerSession session =
|
||||
sessionParser.parseIntroducerSession(bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
LocalAuthor localAuthor = identityManager.getLocalAuthor(txn);
|
||||
if (localAuthor.equals(session.getIntroducee1().author)) {
|
||||
author = session.getIntroducee2().author;
|
||||
} else {
|
||||
author = session.getIntroducee1().author;
|
||||
}
|
||||
} else if (role == INTRODUCEE) {
|
||||
IntroduceeSession session = sessionParser
|
||||
.parseIntroduceeSession(contactGroupId, bdfSession);
|
||||
sessionId = session.getSessionId();
|
||||
author = session.getRemoteAuthor();
|
||||
} 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 Group getLocalGroup() {
|
||||
return contactGroupFactory.createLocalGroup(CLIENT_ID, CLIENT_VERSION);
|
||||
}
|
||||
|
||||
private static class StoredSession {
|
||||
|
||||
private final MessageId storageId;
|
||||
private final BdfDictionary bdfSession;
|
||||
|
||||
private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
|
||||
this.storageId = storageId;
|
||||
this.bdfSession = bdfSession;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.briarproject.briar.introduction2;
|
||||
|
||||
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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user