IntroductionManager and Protocol Engines

This commit is contained in:
Torsten Grote
2018-04-19 17:06:31 -03:00
parent 61b216f572
commit 1bc29fec06
8 changed files with 1724 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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