mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-14 11:49:04 +01:00
Add an IntroductionManager and Validator
This Introduction BSP Client uses its own group to communicate with existing contacts. It uses four types of messages to facilitate introductions: the introduction, the response, the ack and the abort. The protocol logic is encapsulated in two protocol engines, one for the introducer and one for the introducee. The introduction client keeps the local state for each engine, hands messages over to the engines and processes the result and state changes they return.
This commit is contained in:
@@ -4,6 +4,7 @@ import org.briarproject.contact.ContactModule;
|
||||
import org.briarproject.crypto.CryptoModule;
|
||||
import org.briarproject.db.DatabaseModule;
|
||||
import org.briarproject.forum.ForumModule;
|
||||
import org.briarproject.introduction.IntroductionModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
import org.briarproject.plugins.PluginsModule;
|
||||
@@ -16,6 +17,7 @@ public interface CoreEagerSingletons {
|
||||
void inject(CryptoModule.EagerSingletons init);
|
||||
void inject(DatabaseModule.EagerSingletons init);
|
||||
void inject(ForumModule.EagerSingletons init);
|
||||
void inject(IntroductionModule.EagerSingletons init);
|
||||
void inject(LifecycleModule.EagerSingletons init);
|
||||
void inject(MessagingModule.EagerSingletons init);
|
||||
void inject(PluginsModule.EagerSingletons init);
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.briarproject.db.DatabaseModule;
|
||||
import org.briarproject.event.EventModule;
|
||||
import org.briarproject.forum.ForumModule;
|
||||
import org.briarproject.identity.IdentityModule;
|
||||
import org.briarproject.introduction.IntroductionModule;
|
||||
import org.briarproject.invitation.InvitationModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
@@ -27,7 +28,7 @@ import dagger.Module;
|
||||
IdentityModule.class, EventModule.class, DataModule.class,
|
||||
ContactModule.class, PropertiesModule.class, TransportModule.class,
|
||||
SyncModule.class, SettingsModule.class, ClientsModule.class,
|
||||
SystemModule.class, PluginsModule.class})
|
||||
SystemModule.class, PluginsModule.class, IntroductionModule.class})
|
||||
public class CoreModule {
|
||||
|
||||
public static void initEagerSingletons(CoreEagerSingletons c) {
|
||||
@@ -41,5 +42,6 @@ public class CoreModule {
|
||||
c.inject(new PropertiesModule.EagerSingletons());
|
||||
c.inject(new SyncModule.EagerSingletons());
|
||||
c.inject(new TransportModule.EagerSingletons());
|
||||
c.inject(new IntroductionModule.EagerSingletons());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.ProtocolEngine;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.IntroductionRequestReceivedEvent;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.introduction.IntroduceeAction;
|
||||
import org.briarproject.api.introduction.IntroduceeProtocolState;
|
||||
import org.briarproject.api.introduction.IntroductionRequest;
|
||||
import org.briarproject.api.introduction.SessionId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroduceeAction.LOCAL_DECLINE;
|
||||
import static org.briarproject.api.introduction.IntroduceeAction.REMOTE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_ACK;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REMOTE_RESPONSE;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_RESPONSES;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.ERROR;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.FINISHED;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK_ADD_CONTACT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
|
||||
public class IntroduceeEngine
|
||||
implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(IntroduceeEngine.class.getName());
|
||||
|
||||
@Override
|
||||
public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
|
||||
BdfDictionary localState, BdfDictionary localAction) {
|
||||
|
||||
try {
|
||||
IntroduceeProtocolState currentState =
|
||||
getState(localState.getLong(STATE));
|
||||
int type = localAction.getLong(TYPE).intValue();
|
||||
IntroduceeAction action;
|
||||
if (localState.containsKey(ACCEPT)) action = IntroduceeAction
|
||||
.getLocal(type, localState.getBoolean(ACCEPT));
|
||||
else action = IntroduceeAction.getLocal(type);
|
||||
IntroduceeProtocolState nextState = currentState.next(action);
|
||||
|
||||
if (action == LOCAL_ABORT && currentState != ERROR) {
|
||||
return abortSession(currentState, localState);
|
||||
}
|
||||
|
||||
if (nextState == ERROR) {
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Error: Invalid action in state " +
|
||||
currentState.name());
|
||||
}
|
||||
if (currentState == ERROR) return noUpdate(localState);
|
||||
else abortSession(currentState, localState);
|
||||
}
|
||||
|
||||
if (action == LOCAL_ACCEPT || action == LOCAL_DECLINE) {
|
||||
localState.put(STATE, nextState.getValue());
|
||||
localState.put(ANSWERED, true);
|
||||
List<BdfDictionary> messages = new ArrayList<BdfDictionary>(1);
|
||||
// create the introduction response message
|
||||
BdfDictionary msg = new BdfDictionary();
|
||||
msg.put(TYPE, TYPE_RESPONSE);
|
||||
msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
|
||||
msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
msg.put(ACCEPT, localState.getBoolean(ACCEPT));
|
||||
if (localState.getBoolean(ACCEPT)) {
|
||||
msg.put(TIME, localState.getLong(OUR_TIME));
|
||||
msg.put(E_PUBLIC_KEY, localState.getRaw(OUR_PUBLIC_KEY));
|
||||
msg.put(DEVICE_ID, localAction.getRaw(DEVICE_ID));
|
||||
msg.put(TRANSPORT, localAction.getDictionary(TRANSPORT));
|
||||
}
|
||||
messages.add(msg);
|
||||
logAction(currentState, localState, msg);
|
||||
|
||||
if (nextState == AWAIT_ACK) {
|
||||
localState.put(TASK, TASK_ADD_CONTACT);
|
||||
// also send ACK, because we already have the other response
|
||||
BdfDictionary ack = getAckMessage(localState);
|
||||
messages.add(ack);
|
||||
}
|
||||
List<Event> events = Collections.emptyList();
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false,
|
||||
false,
|
||||
localState, messages, events);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
|
||||
BdfDictionary localState, BdfDictionary msg) {
|
||||
|
||||
try {
|
||||
IntroduceeProtocolState currentState =
|
||||
getState(localState.getLong(STATE));
|
||||
int type = msg.getLong(TYPE).intValue();
|
||||
IntroduceeAction action = IntroduceeAction.getRemote(type);
|
||||
IntroduceeProtocolState nextState = currentState.next(action);
|
||||
|
||||
logMessageReceived(currentState, nextState, localState, type, msg);
|
||||
|
||||
if (nextState == ERROR) {
|
||||
if (currentState != ERROR && action != REMOTE_ABORT) {
|
||||
return abortSession(currentState, localState);
|
||||
} else {
|
||||
return noUpdate(localState);
|
||||
}
|
||||
}
|
||||
|
||||
// update local session state with next protocol state
|
||||
localState.put(STATE, nextState.getValue());
|
||||
List<BdfDictionary> messages;
|
||||
List<Event> events;
|
||||
// we received the introduction request
|
||||
if (currentState == AWAIT_REQUEST) {
|
||||
// remember the session ID used by the introducer
|
||||
localState.put(SESSION_ID, msg.getRaw(SESSION_ID));
|
||||
|
||||
addRequestData(localState, msg);
|
||||
messages = Collections.emptyList();
|
||||
events = Collections.singletonList(getEvent(localState, msg));
|
||||
}
|
||||
// we had the request and now one response came in _OR_
|
||||
// we had sent our response already and now received the other one
|
||||
else if (currentState == AWAIT_RESPONSES ||
|
||||
currentState == AWAIT_REMOTE_RESPONSE) {
|
||||
// update next state based on message content
|
||||
action = IntroduceeAction
|
||||
.getRemote(type, msg.getBoolean(ACCEPT));
|
||||
nextState = currentState.next(action);
|
||||
localState.put(STATE, nextState.getValue());
|
||||
|
||||
addResponseData(localState, msg);
|
||||
if (nextState == AWAIT_ACK) {
|
||||
localState.put(TASK, TASK_ADD_CONTACT);
|
||||
messages = Collections
|
||||
.singletonList(getAckMessage(localState));
|
||||
} else {
|
||||
messages = Collections.emptyList();
|
||||
}
|
||||
events = Collections.emptyList();
|
||||
}
|
||||
// we already sent our ACK and now received the other one
|
||||
else if (currentState == AWAIT_ACK) {
|
||||
localState.put(TASK, TASK_ACTIVATE_CONTACT);
|
||||
messages = Collections.emptyList();
|
||||
events = Collections.emptyList();
|
||||
}
|
||||
// we are done (probably declined response) and ignore this message
|
||||
else if (currentState == FINISHED) {
|
||||
return noUpdate(localState);
|
||||
}
|
||||
// this should not happen
|
||||
else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, messages, events);
|
||||
} catch (FormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void addRequestData(BdfDictionary localState, BdfDictionary msg)
|
||||
throws FormatException {
|
||||
|
||||
localState.put(NAME, msg.getString(NAME));
|
||||
localState.put(PUBLIC_KEY, msg.getRaw(PUBLIC_KEY));
|
||||
if (msg.containsKey(MSG)) {
|
||||
localState.put(MSG, msg.getString(MSG));
|
||||
}
|
||||
}
|
||||
|
||||
private void addResponseData(BdfDictionary localState, BdfDictionary msg)
|
||||
throws FormatException {
|
||||
|
||||
if (localState.containsKey(ACCEPT)) {
|
||||
localState.put(ACCEPT,
|
||||
localState.getBoolean(ACCEPT) && msg.getBoolean(ACCEPT));
|
||||
} else {
|
||||
localState.put(ACCEPT, msg.getBoolean(ACCEPT));
|
||||
}
|
||||
localState.put(NOT_OUR_RESPONSE, msg.getRaw(MESSAGE_ID));
|
||||
|
||||
if (msg.getBoolean(ACCEPT)) {
|
||||
localState.put(TIME, msg.getLong(TIME));
|
||||
localState.put(E_PUBLIC_KEY, msg.getRaw(E_PUBLIC_KEY));
|
||||
localState.put(DEVICE_ID, msg.getRaw(DEVICE_ID));
|
||||
localState.put(TRANSPORT, msg.getDictionary(TRANSPORT));
|
||||
}
|
||||
}
|
||||
|
||||
private BdfDictionary getAckMessage(BdfDictionary localState)
|
||||
throws FormatException {
|
||||
|
||||
BdfDictionary m = new BdfDictionary();
|
||||
m.put(TYPE, TYPE_ACK);
|
||||
m.put(GROUP_ID, localState.getRaw(GROUP_ID));
|
||||
m.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Sending ACK " + " to " +
|
||||
localState.getString(INTRODUCER) + " for " +
|
||||
localState.getString(NAME) + " with session ID " +
|
||||
Arrays.hashCode(m.getRaw(SESSION_ID)) + " in group " +
|
||||
Arrays.hashCode(m.getRaw(GROUP_ID)));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private void logAction(IntroduceeProtocolState state,
|
||||
BdfDictionary localState, BdfDictionary msg) {
|
||||
|
||||
if (!LOG.isLoggable(INFO)) return;
|
||||
|
||||
try {
|
||||
LOG.info("Sending " +
|
||||
(localState.getBoolean(ACCEPT) ? "accept " : "decline ") +
|
||||
"response in state " + state.name() +
|
||||
" to " + localState.getString(INTRODUCER) +
|
||||
" for " + localState.getString(NAME) + " with session ID " +
|
||||
Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
|
||||
Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
|
||||
"Moving on to state " +
|
||||
getState(localState.getLong(STATE)).name()
|
||||
);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void logMessageReceived(IntroduceeProtocolState currentState,
|
||||
IntroduceeProtocolState nextState, BdfDictionary localState,
|
||||
int type, BdfDictionary msg) {
|
||||
|
||||
if (!LOG.isLoggable(INFO)) return;
|
||||
|
||||
try {
|
||||
String t = "unknown";
|
||||
if (type == TYPE_REQUEST) t = "Introduction";
|
||||
else if (type == TYPE_RESPONSE) t = "Response";
|
||||
else if (type == TYPE_ACK) t = "ACK";
|
||||
else if (type == TYPE_ABORT) t = "Abort";
|
||||
|
||||
LOG.info("Received " + t + " in state " + currentState.name() +
|
||||
" from " + localState.getString(INTRODUCER) +
|
||||
(localState.containsKey(NAME) ?
|
||||
" related to " + localState.getString(NAME) : "") +
|
||||
" with session ID " +
|
||||
Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
|
||||
Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
|
||||
"Moving on to state " + nextState.name()
|
||||
);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
|
||||
BdfDictionary localState, BdfDictionary delivered) {
|
||||
try {
|
||||
return noUpdate(localState);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private IntroduceeProtocolState getState(Long state) {
|
||||
return IntroduceeProtocolState.fromValue(state.intValue());
|
||||
}
|
||||
|
||||
private Event getEvent(BdfDictionary localState, BdfDictionary msg)
|
||||
throws FormatException {
|
||||
|
||||
ContactId contactId =
|
||||
new ContactId(localState.getLong(CONTACT_ID_1).intValue());
|
||||
AuthorId authorId = new AuthorId(localState.getRaw(REMOTE_AUTHOR_ID));
|
||||
|
||||
SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
|
||||
MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
|
||||
long time = msg.getLong(MESSAGE_TIME);
|
||||
String name = msg.getString(NAME);
|
||||
String message = msg.getOptionalString(MSG);
|
||||
boolean exists = localState.getBoolean(EXISTS);
|
||||
|
||||
IntroductionRequest ir = new IntroductionRequest(sessionId, messageId,
|
||||
time, false, false, false, false, authorId, name, false,
|
||||
message, false, exists);
|
||||
return new IntroductionRequestReceivedEvent(contactId, ir);
|
||||
}
|
||||
|
||||
private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
|
||||
IntroduceeProtocolState currentState, BdfDictionary localState)
|
||||
throws FormatException {
|
||||
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Aborting protocol session " +
|
||||
Arrays.hashCode(localState.getRaw(SESSION_ID)) +
|
||||
" in state " + currentState.name());
|
||||
}
|
||||
|
||||
localState.put(STATE, ERROR.getValue());
|
||||
localState.put(TASK, TASK_ABORT);
|
||||
BdfDictionary msg = new BdfDictionary();
|
||||
msg.put(TYPE, TYPE_ABORT);
|
||||
msg.put(GROUP_ID, localState.getRaw(GROUP_ID));
|
||||
msg.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
List<BdfDictionary> messages = Collections.singletonList(msg);
|
||||
// TODO inform about protocol abort via new Event?
|
||||
List<Event> events = Collections.emptyList();
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, messages, events);
|
||||
}
|
||||
|
||||
private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
|
||||
BdfDictionary localState) throws FormatException {
|
||||
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, new ArrayList<BdfDictionary>(0),
|
||||
new ArrayList<Event>(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
|
||||
import org.briarproject.api.Bytes;
|
||||
import org.briarproject.api.DeviceId;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.crypto.KeyPair;
|
||||
import org.briarproject.api.crypto.KeyParser;
|
||||
import org.briarproject.api.crypto.PrivateKey;
|
||||
import org.briarproject.api.crypto.PublicKey;
|
||||
import org.briarproject.api.crypto.SecretKey;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.IntroductionSucceededEvent;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
import org.briarproject.api.introduction.SessionId;
|
||||
import org.briarproject.api.properties.TransportProperties;
|
||||
import org.briarproject.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.introduction.IntroduceeProtocolState.AWAIT_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ADDED_CONTACT_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.INTRODUCER;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.LOCAL_AUTHOR_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.OUR_PRIVATE_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.OUR_PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.OUR_TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK_ACTIVATE_CONTACT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TASK_ADD_CONTACT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
|
||||
class IntroduceeManager {
|
||||
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(IntroduceeManager.class.getName());
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final IntroductionManager introductionManager;
|
||||
private final ClientHelper clientHelper;
|
||||
private final Clock clock;
|
||||
private final CryptoComponent cryptoComponent;
|
||||
private final TransportPropertyManager transportPropertyManager;
|
||||
private final AuthorFactory authorFactory;
|
||||
private final ContactManager contactManager;
|
||||
|
||||
IntroduceeManager(DatabaseComponent db,
|
||||
IntroductionManager introductionManager, ClientHelper clientHelper,
|
||||
Clock clock, CryptoComponent cryptoComponent,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
AuthorFactory authorFactory, ContactManager contactManager) {
|
||||
|
||||
this.db = db;
|
||||
this.introductionManager = introductionManager;
|
||||
this.clientHelper = clientHelper;
|
||||
this.clock = clock;
|
||||
this.cryptoComponent = cryptoComponent;
|
||||
this.transportPropertyManager = transportPropertyManager;
|
||||
this.authorFactory = authorFactory;
|
||||
this.contactManager = contactManager;
|
||||
}
|
||||
|
||||
public BdfDictionary initialize(Transaction txn, GroupId groupId,
|
||||
BdfDictionary message) throws DbException, FormatException {
|
||||
|
||||
// create local message to keep engine state
|
||||
long now = clock.currentTimeMillis();
|
||||
Bytes salt = new Bytes(new byte[64]);
|
||||
cryptoComponent.getSecureRandom().nextBytes(salt.getBytes());
|
||||
|
||||
Message localMsg = clientHelper
|
||||
.createMessage(introductionManager.getLocalGroup().getId(), now,
|
||||
BdfList.of(salt));
|
||||
MessageId storageId = localMsg.getId();
|
||||
|
||||
// find out who is introducing us
|
||||
BdfDictionary gd =
|
||||
clientHelper.getGroupMetadataAsDictionary(txn, groupId);
|
||||
ContactId introducerId =
|
||||
new ContactId(gd.getLong(CONTACT).intValue());
|
||||
Contact introducer = db.getContact(txn, introducerId);
|
||||
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(STORAGE_ID, storageId);
|
||||
d.put(STATE, AWAIT_REQUEST.getValue());
|
||||
d.put(ROLE, ROLE_INTRODUCEE);
|
||||
d.put(GROUP_ID, groupId);
|
||||
d.put(INTRODUCER, introducer.getAuthor().getName());
|
||||
d.put(CONTACT_ID_1, introducer.getId().getInt());
|
||||
d.put(LOCAL_AUTHOR_ID, introducer.getLocalAuthorId().getBytes());
|
||||
d.put(NOT_OUR_RESPONSE, new byte[0]);
|
||||
d.put(ANSWERED, false);
|
||||
|
||||
// check if the contact we are introduced to does already exist
|
||||
AuthorId remoteAuthorId = authorFactory
|
||||
.createAuthor(message.getString(NAME),
|
||||
message.getRaw(PUBLIC_KEY)).getId();
|
||||
boolean exists = contactManager.contactExists(txn, remoteAuthorId,
|
||||
introducer.getLocalAuthorId());
|
||||
d.put(EXISTS, exists);
|
||||
d.put(REMOTE_AUTHOR_ID, remoteAuthorId);
|
||||
|
||||
// save local state to database
|
||||
clientHelper.addLocalMessage(txn, localMsg,
|
||||
introductionManager.getClientId(), d, false);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
public void incomingMessage(Transaction txn, BdfDictionary state,
|
||||
BdfDictionary message) throws DbException, FormatException {
|
||||
|
||||
IntroduceeEngine engine = new IntroduceeEngine();
|
||||
processStateUpdate(txn, engine.onMessageReceived(state, message));
|
||||
}
|
||||
|
||||
public void acceptIntroduction(Transaction txn,
|
||||
final SessionId sessionId) throws DbException, FormatException {
|
||||
|
||||
BdfDictionary state =
|
||||
introductionManager.getSessionState(txn, sessionId.getBytes());
|
||||
|
||||
// get data to connect and derive a shared secret later
|
||||
long now = clock.currentTimeMillis();
|
||||
byte[] deviceId = db.getDeviceId(txn).getBytes();
|
||||
KeyPair keyPair = cryptoComponent.generateAgreementKeyPair();
|
||||
byte[] publicKey = keyPair.getPublic().getEncoded();
|
||||
byte[] privateKey = keyPair.getPrivate().getEncoded();
|
||||
Map<TransportId, TransportProperties> transportProperties =
|
||||
transportPropertyManager.getLocalProperties();
|
||||
|
||||
// update session state for later
|
||||
state.put(ACCEPT, true);
|
||||
state.put(OUR_TIME, now);
|
||||
state.put(OUR_PUBLIC_KEY, publicKey);
|
||||
state.put(OUR_PRIVATE_KEY, privateKey);
|
||||
|
||||
// define action
|
||||
BdfDictionary localAction = new BdfDictionary();
|
||||
localAction.put(TYPE, TYPE_RESPONSE);
|
||||
localAction.put(DEVICE_ID, deviceId);
|
||||
localAction.put(TRANSPORT,
|
||||
encodeTransportProperties(transportProperties));
|
||||
|
||||
// start engine and process its state update
|
||||
IntroduceeEngine engine = new IntroduceeEngine();
|
||||
processStateUpdate(txn,
|
||||
engine.onLocalAction(state, localAction));
|
||||
}
|
||||
|
||||
public void declineIntroduction(Transaction txn, final SessionId sessionId)
|
||||
throws DbException, FormatException {
|
||||
|
||||
BdfDictionary state =
|
||||
introductionManager.getSessionState(txn, sessionId.getBytes());
|
||||
|
||||
// update session state
|
||||
state.put(ACCEPT, false);
|
||||
|
||||
// define action
|
||||
BdfDictionary localAction = new BdfDictionary();
|
||||
localAction.put(TYPE, TYPE_RESPONSE);
|
||||
|
||||
// start engine and process its state update
|
||||
IntroduceeEngine engine = new IntroduceeEngine();
|
||||
processStateUpdate(txn,
|
||||
engine.onLocalAction(state, localAction));
|
||||
}
|
||||
|
||||
private void processStateUpdate(Transaction txn,
|
||||
IntroduceeEngine.StateUpdate<BdfDictionary, BdfDictionary>
|
||||
result) throws DbException, FormatException {
|
||||
|
||||
// perform actions based on new local state
|
||||
performTasks(txn, result.localState);
|
||||
|
||||
// save new local state
|
||||
MessageId storageId =
|
||||
new MessageId(result.localState.getRaw(STORAGE_ID));
|
||||
clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
|
||||
|
||||
// send messages
|
||||
for (BdfDictionary d : result.toSend) {
|
||||
introductionManager.sendMessage(txn, d);
|
||||
}
|
||||
|
||||
// broadcast events
|
||||
for (Event event : result.toBroadcast) {
|
||||
txn.attach(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void performTasks(Transaction txn, BdfDictionary localState)
|
||||
throws FormatException, DbException {
|
||||
|
||||
if (!localState.containsKey(TASK)) return;
|
||||
|
||||
// remember task and remove it from localState
|
||||
long task = localState.getLong(TASK);
|
||||
localState.put(TASK, BdfDictionary.NULL_VALUE);
|
||||
|
||||
|
||||
|
||||
if (task == TASK_ADD_CONTACT) {
|
||||
if (localState.getBoolean(EXISTS)) {
|
||||
// we have this contact already, so do not perform actions
|
||||
LOG.info("We have this contact already, do not add");
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.info("Adding contact in inactive state");
|
||||
|
||||
// get all keys
|
||||
KeyParser keyParser = cryptoComponent.getAgreementKeyParser();
|
||||
byte[] publicKeyBytes;
|
||||
PublicKey publicKey;
|
||||
PrivateKey privateKey;
|
||||
try {
|
||||
publicKeyBytes = localState.getRaw(OUR_PUBLIC_KEY);
|
||||
publicKey = keyParser
|
||||
.parsePublicKey(publicKeyBytes);
|
||||
privateKey = keyParser.parsePrivateKey(
|
||||
localState.getRaw(OUR_PRIVATE_KEY));
|
||||
} catch (GeneralSecurityException e) {
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
// we can not continue without the keys
|
||||
throw new RuntimeException("Our own ephemeral key is invalid");
|
||||
}
|
||||
KeyPair keyPair = new KeyPair(publicKey, privateKey);
|
||||
byte[] theirEphemeralKey = localState.getRaw(E_PUBLIC_KEY);
|
||||
|
||||
// figure out who takes which role by comparing public keys
|
||||
int comp = Bytes.COMPARATOR.compare(new Bytes(publicKeyBytes),
|
||||
new Bytes(theirEphemeralKey));
|
||||
boolean alice = comp < 0;
|
||||
|
||||
// The master secret is derived from the local ephemeral key pair
|
||||
// and the remote ephemeral public key
|
||||
SecretKey secretKey;
|
||||
try {
|
||||
secretKey = cryptoComponent
|
||||
.deriveMasterSecret(theirEphemeralKey, keyPair, alice);
|
||||
} catch (GeneralSecurityException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
// we can not continue without the shared secret
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
// The agreed timestamp is the minimum of the peers' timestamps
|
||||
long ourTime = localState.getLong(OUR_TIME);
|
||||
long theirTime = localState.getLong(TIME);
|
||||
long timestamp = Math.min(ourTime, theirTime);
|
||||
|
||||
// Add the contact to the database
|
||||
AuthorId localAuthorId =
|
||||
new AuthorId(localState.getRaw(LOCAL_AUTHOR_ID));
|
||||
Author remoteAuthor = authorFactory
|
||||
.createAuthor(localState.getString(NAME),
|
||||
localState.getRaw(PUBLIC_KEY));
|
||||
ContactId contactId = contactManager
|
||||
.addContact(txn, remoteAuthor, localAuthorId, secretKey,
|
||||
timestamp, alice, false);
|
||||
|
||||
// Update local state with ContactId, so we know what to activate
|
||||
localState.put(ADDED_CONTACT_ID, contactId.getInt());
|
||||
|
||||
// let the transport manager know how to connect to the contact
|
||||
DeviceId deviceId = new DeviceId(localState.getRaw(DEVICE_ID));
|
||||
Map<TransportId, TransportProperties> transportProperties =
|
||||
parseTransportProperties(localState);
|
||||
transportPropertyManager.addRemoteProperties(txn, contactId,
|
||||
deviceId, transportProperties);
|
||||
|
||||
// delete the ephemeral private key by overwriting with NULL value
|
||||
// this ensures future ephemeral keys can not be recovered when
|
||||
// this device should gets compromised
|
||||
localState.put(OUR_PRIVATE_KEY, BdfDictionary.NULL_VALUE);
|
||||
}
|
||||
|
||||
// we sent and received an ACK, so activate contact
|
||||
if (task == TASK_ACTIVATE_CONTACT) {
|
||||
if (!localState.getBoolean(EXISTS) &&
|
||||
localState.containsKey(ADDED_CONTACT_ID)) {
|
||||
|
||||
LOG.info("Activating Contact...");
|
||||
|
||||
ContactId contactId = new ContactId(
|
||||
localState.getLong(ADDED_CONTACT_ID).intValue());
|
||||
|
||||
// activate and show contact in contact list
|
||||
db.setContactActive(txn, contactId, true);
|
||||
|
||||
// broadcast event informing of successful introduction
|
||||
Contact contact = db.getContact(txn, contactId);
|
||||
Event event = new IntroductionSucceededEvent(contact);
|
||||
txn.attach(event);
|
||||
} else {
|
||||
LOG.info(
|
||||
"We must have had this contact already, not activating...");
|
||||
}
|
||||
}
|
||||
|
||||
// we need to abort the protocol, clean up what has been done
|
||||
if (task == TASK_ABORT) {
|
||||
if (localState.containsKey(ADDED_CONTACT_ID)) {
|
||||
LOG.info("Deleting added contact due to abort...");
|
||||
ContactId contactId = new ContactId(
|
||||
localState.getLong(ADDED_CONTACT_ID).intValue());
|
||||
contactManager.removeContact(contactId);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void abort(Transaction txn, BdfDictionary state) {
|
||||
|
||||
IntroduceeEngine engine = new IntroduceeEngine();
|
||||
BdfDictionary localAction = new BdfDictionary();
|
||||
localAction.put(TYPE, TYPE_ABORT);
|
||||
try {
|
||||
processStateUpdate(txn,
|
||||
engine.onLocalAction(state, localAction));
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private BdfDictionary encodeTransportProperties(
|
||||
Map<TransportId, TransportProperties> map) {
|
||||
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
for (Map.Entry<TransportId, TransportProperties> e : map.entrySet()) {
|
||||
d.put(e.getKey().getString(), e.getValue());
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private Map<TransportId, TransportProperties> parseTransportProperties(
|
||||
BdfDictionary d) throws FormatException {
|
||||
|
||||
Map<TransportId, TransportProperties> tpMap =
|
||||
new HashMap<TransportId, TransportProperties>();
|
||||
BdfDictionary tpMapDict = d.getDictionary(TRANSPORT);
|
||||
for (String key : tpMapDict.keySet()) {
|
||||
TransportId transportId = new TransportId(key);
|
||||
TransportProperties transportProperties = new TransportProperties();
|
||||
BdfDictionary tpDict = tpMapDict.getDictionary(key);
|
||||
for (String tkey : tpDict.keySet()) {
|
||||
transportProperties.put(tkey, tpDict.getString(tkey));
|
||||
}
|
||||
tpMap.put(transportId, transportProperties);
|
||||
}
|
||||
return tpMap;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.ProtocolEngine;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.IntroductionResponseReceivedEvent;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.introduction.IntroducerAction;
|
||||
import org.briarproject.api.introduction.IntroducerProtocolState;
|
||||
import org.briarproject.api.introduction.IntroductionResponse;
|
||||
import org.briarproject.api.introduction.SessionId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import static java.util.logging.Level.INFO;
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.introduction.IntroducerAction.LOCAL_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroducerAction.LOCAL_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_1;
|
||||
import static org.briarproject.api.introduction.IntroducerAction.REMOTE_ACCEPT_2;
|
||||
import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_1;
|
||||
import static org.briarproject.api.introduction.IntroducerAction.REMOTE_DECLINE_2;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACKS;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACK_1;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_ACK_2;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSES;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_1;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.AWAIT_RESPONSE_2;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.ERROR;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.FINISHED;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
|
||||
public class IntroducerEngine
|
||||
implements ProtocolEngine<BdfDictionary, BdfDictionary, BdfDictionary> {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(IntroducerEngine.class.getName());
|
||||
|
||||
@Override
|
||||
public StateUpdate<BdfDictionary, BdfDictionary> onLocalAction(
|
||||
BdfDictionary localState, BdfDictionary localAction) {
|
||||
|
||||
try {
|
||||
IntroducerProtocolState currentState =
|
||||
getState(localState.getLong(STATE));
|
||||
int type = localAction.getLong(TYPE).intValue();
|
||||
IntroducerAction action = IntroducerAction.getLocal(type);
|
||||
IntroducerProtocolState nextState = currentState.next(action);
|
||||
|
||||
if (action == LOCAL_ABORT && currentState != ERROR) {
|
||||
return abortSession(currentState, localState);
|
||||
}
|
||||
|
||||
if (nextState == ERROR) {
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Error: Invalid action in state " +
|
||||
currentState.name());
|
||||
}
|
||||
return noUpdate(localState);
|
||||
}
|
||||
|
||||
localState.put(STATE, nextState.getValue());
|
||||
if (action == LOCAL_REQUEST) {
|
||||
// create the introduction requests for both contacts
|
||||
List<BdfDictionary> messages = new ArrayList<BdfDictionary>(2);
|
||||
BdfDictionary msg1 = new BdfDictionary();
|
||||
msg1.put(TYPE, TYPE_REQUEST);
|
||||
msg1.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
|
||||
msg1.put(NAME, localState.getString(CONTACT_2));
|
||||
msg1.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY2));
|
||||
if (localAction.containsKey(MSG)) {
|
||||
msg1.put(MSG, localAction.getString(MSG));
|
||||
}
|
||||
messages.add(msg1);
|
||||
logLocalAction(currentState, localState, msg1);
|
||||
BdfDictionary msg2 = new BdfDictionary();
|
||||
msg2.put(TYPE, TYPE_REQUEST);
|
||||
msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
|
||||
msg2.put(NAME, localState.getString(CONTACT_1));
|
||||
msg2.put(PUBLIC_KEY, localAction.getRaw(PUBLIC_KEY1));
|
||||
if (localAction.containsKey(MSG)) {
|
||||
msg2.put(MSG, localAction.getString(MSG));
|
||||
}
|
||||
messages.add(msg2);
|
||||
logLocalAction(currentState, localState, msg2);
|
||||
|
||||
List<Event> events = Collections.emptyList();
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, messages, events);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown Local Action");
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateUpdate<BdfDictionary, BdfDictionary> onMessageReceived(
|
||||
BdfDictionary localState, BdfDictionary msg) {
|
||||
|
||||
try {
|
||||
IntroducerProtocolState currentState =
|
||||
getState(localState.getLong(STATE));
|
||||
int type = msg.getLong(TYPE).intValue();
|
||||
boolean one = isContact1(localState, msg);
|
||||
IntroducerAction action = IntroducerAction.getRemote(type, one);
|
||||
IntroducerProtocolState nextState = currentState.next(action);
|
||||
|
||||
logMessageReceived(currentState, nextState, localState, type, msg);
|
||||
|
||||
if (nextState == ERROR) {
|
||||
if (currentState != ERROR) {
|
||||
return abortSession(currentState, localState);
|
||||
} else {
|
||||
return noUpdate(localState);
|
||||
}
|
||||
}
|
||||
|
||||
List<BdfDictionary> messages;
|
||||
List<Event> events;
|
||||
|
||||
// we have sent our requests and just got the 1st or 2nd response
|
||||
if (currentState == AWAIT_RESPONSES ||
|
||||
currentState == AWAIT_RESPONSE_1 ||
|
||||
currentState == AWAIT_RESPONSE_2) {
|
||||
// update next state based on message content
|
||||
action = IntroducerAction
|
||||
.getRemote(type, one, msg.getBoolean(ACCEPT));
|
||||
nextState = currentState.next(action);
|
||||
localState.put(STATE, nextState.getValue());
|
||||
if (one) localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID));
|
||||
else localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID));
|
||||
|
||||
messages = forwardMessage(localState, msg);
|
||||
events = Collections.singletonList(getEvent(localState, msg));
|
||||
}
|
||||
// we have forwarded both responses and now received the 1st or 2nd ACK
|
||||
else if (currentState == AWAIT_ACKS ||
|
||||
currentState == AWAIT_ACK_1 ||
|
||||
currentState == AWAIT_ACK_2) {
|
||||
localState.put(STATE, nextState.getValue());
|
||||
messages = forwardMessage(localState, msg);
|
||||
events = Collections.emptyList();
|
||||
}
|
||||
// we probably received a response while already being FINISHED
|
||||
else if (currentState == FINISHED) {
|
||||
// if it was a response store it to be found later
|
||||
if (action == REMOTE_ACCEPT_1 || action == REMOTE_DECLINE_1) {
|
||||
localState.put(RESPONSE_1, msg.getRaw(MESSAGE_ID));
|
||||
messages = Collections.emptyList();
|
||||
events = Collections.singletonList(getEvent(localState, msg));
|
||||
} else if (action == REMOTE_ACCEPT_2 ||
|
||||
action == REMOTE_DECLINE_2) {
|
||||
localState.put(RESPONSE_2, msg.getRaw(MESSAGE_ID));
|
||||
messages = Collections.emptyList();
|
||||
events = Collections.singletonList(getEvent(localState, msg));
|
||||
} else return noUpdate(localState);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Bad state");
|
||||
}
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, messages, events);
|
||||
} catch (FormatException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void logLocalAction(IntroducerProtocolState state,
|
||||
BdfDictionary localState, BdfDictionary msg) {
|
||||
|
||||
if (!LOG.isLoggable(INFO)) return;
|
||||
|
||||
try {
|
||||
String to = getMessagePartner(localState, msg);
|
||||
LOG.info("Sending introduction request in state " + state.name() +
|
||||
" to " + to + " with session ID " +
|
||||
Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
|
||||
Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
|
||||
"Moving on to state " +
|
||||
getState(localState.getLong(STATE)).name()
|
||||
);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void logMessageReceived(IntroducerProtocolState currentState,
|
||||
IntroducerProtocolState nextState,
|
||||
BdfDictionary localState, int type, BdfDictionary msg) {
|
||||
if (!LOG.isLoggable(INFO)) return;
|
||||
|
||||
try {
|
||||
String t = "unknown";
|
||||
if (type == TYPE_REQUEST) t = "Introduction";
|
||||
else if (type == TYPE_RESPONSE) t = "Response";
|
||||
else if (type == TYPE_ACK) t = "ACK";
|
||||
else if (type == TYPE_ABORT) t = "Abort";
|
||||
|
||||
String from = getMessagePartner(localState, msg);
|
||||
String to = getOtherContact(localState, msg);
|
||||
|
||||
LOG.info("Received " + t + " in state " + currentState.name() + " from " +
|
||||
from + " to " + to + " with session ID " +
|
||||
Arrays.hashCode(msg.getRaw(SESSION_ID)) + " in group " +
|
||||
Arrays.hashCode(msg.getRaw(GROUP_ID)) + ". " +
|
||||
"Moving on to state " + nextState.name()
|
||||
);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private List<BdfDictionary> forwardMessage(BdfDictionary localState,
|
||||
BdfDictionary message) throws FormatException {
|
||||
|
||||
// clone the message here, because we still need the original
|
||||
BdfDictionary msg = (BdfDictionary) message.clone();
|
||||
if (isContact1(localState, msg)) {
|
||||
msg.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
|
||||
} else {
|
||||
msg.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
|
||||
}
|
||||
|
||||
if (LOG.isLoggable(INFO)) {
|
||||
LOG.info("Forwarding message to group " +
|
||||
Arrays.hashCode(msg.getRaw(GROUP_ID)));
|
||||
}
|
||||
|
||||
return Collections.singletonList(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StateUpdate<BdfDictionary, BdfDictionary> onMessageDelivered(
|
||||
BdfDictionary localState, BdfDictionary delivered) {
|
||||
try {
|
||||
return noUpdate(localState);
|
||||
}
|
||||
catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private IntroducerProtocolState getState(Long state) {
|
||||
return IntroducerProtocolState.fromValue(state.intValue());
|
||||
}
|
||||
|
||||
private Event getEvent(BdfDictionary localState, BdfDictionary msg)
|
||||
throws FormatException {
|
||||
|
||||
ContactId contactId =
|
||||
new ContactId(localState.getLong(CONTACT_ID_1).intValue());
|
||||
AuthorId authorId = new AuthorId(localState.getRaw(AUTHOR_ID_1, new byte[32])); // TODO remove byte[]
|
||||
if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
|
||||
contactId =
|
||||
new ContactId(localState.getLong(CONTACT_ID_2).intValue());
|
||||
authorId = new AuthorId(localState.getRaw(AUTHOR_ID_2, new byte[32])); // TODO remove byte[]
|
||||
}
|
||||
|
||||
SessionId sessionId = new SessionId(localState.getRaw(SESSION_ID));
|
||||
MessageId messageId = new MessageId(msg.getRaw(MESSAGE_ID));
|
||||
long time = msg.getLong(MESSAGE_TIME);
|
||||
String name = getOtherContact(localState, msg);
|
||||
boolean accept = msg.getBoolean(ACCEPT);
|
||||
|
||||
IntroductionResponse ir =
|
||||
new IntroductionResponse(sessionId, messageId, time, false,
|
||||
false, false, false, authorId, name, accept);
|
||||
return new IntroductionResponseReceivedEvent(contactId, ir);
|
||||
}
|
||||
|
||||
private boolean isContact1(BdfDictionary localState, BdfDictionary msg)
|
||||
throws FormatException {
|
||||
|
||||
byte[] group = msg.getRaw(GROUP_ID);
|
||||
byte[] group1 = localState.getRaw(GROUP_ID_1);
|
||||
byte[] group2 = localState.getRaw(GROUP_ID_2);
|
||||
|
||||
if (Arrays.equals(group, group1)) {
|
||||
return true;
|
||||
} else if (Arrays.equals(group, group2)) {
|
||||
return false;
|
||||
} else {
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
|
||||
private String getMessagePartner(BdfDictionary localState,
|
||||
BdfDictionary msg) throws FormatException {
|
||||
|
||||
String from = localState.getString(CONTACT_1);
|
||||
if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
|
||||
from = localState.getString(CONTACT_2);
|
||||
}
|
||||
return from;
|
||||
}
|
||||
|
||||
private String getOtherContact(BdfDictionary localState, BdfDictionary msg)
|
||||
throws FormatException {
|
||||
|
||||
String to = localState.getString(CONTACT_2);
|
||||
if (Arrays.equals(msg.getRaw(GROUP_ID), localState.getRaw(GROUP_ID_2))) {
|
||||
to = localState.getString(CONTACT_1);
|
||||
}
|
||||
return to;
|
||||
}
|
||||
|
||||
private StateUpdate<BdfDictionary, BdfDictionary> abortSession(
|
||||
IntroducerProtocolState currentState, BdfDictionary localState)
|
||||
throws FormatException {
|
||||
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Aborting protocol session " +
|
||||
Arrays.hashCode(localState.getRaw(SESSION_ID)) +
|
||||
" in state " + currentState.name());
|
||||
}
|
||||
|
||||
localState.put(STATE, ERROR.getValue());
|
||||
List<BdfDictionary> messages = new ArrayList<BdfDictionary>(2);
|
||||
BdfDictionary msg1 = new BdfDictionary();
|
||||
msg1.put(TYPE, TYPE_ABORT);
|
||||
msg1.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
msg1.put(GROUP_ID, localState.getRaw(GROUP_ID_1));
|
||||
messages.add(msg1);
|
||||
BdfDictionary msg2 = new BdfDictionary();
|
||||
msg2.put(TYPE, TYPE_ABORT);
|
||||
msg2.put(SESSION_ID, localState.getRaw(SESSION_ID));
|
||||
msg2.put(GROUP_ID, localState.getRaw(GROUP_ID_2));
|
||||
messages.add(msg2);
|
||||
// TODO inform about protocol abort via new Event?
|
||||
List<Event> events = Collections.emptyList();
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, messages, events);
|
||||
}
|
||||
|
||||
private StateUpdate<BdfDictionary, BdfDictionary> noUpdate(
|
||||
BdfDictionary localState) throws FormatException {
|
||||
|
||||
return new StateUpdate<BdfDictionary, BdfDictionary>(false, false,
|
||||
localState, new ArrayList<BdfDictionary>(0),
|
||||
new ArrayList<Event>(0));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.Bytes;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.introduction.IntroducerProtocolState.PREPARE_REQUESTS;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STORAGE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
|
||||
class IntroducerManager {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(IntroducerManager.class.getName());
|
||||
|
||||
private final IntroductionManager introductionManager;
|
||||
private final ClientHelper clientHelper;
|
||||
private final Clock clock;
|
||||
private final CryptoComponent cryptoComponent;
|
||||
|
||||
IntroducerManager(IntroductionManager introductionManager,
|
||||
ClientHelper clientHelper, Clock clock,
|
||||
CryptoComponent cryptoComponent) {
|
||||
|
||||
this.introductionManager = introductionManager;
|
||||
this.clientHelper = clientHelper;
|
||||
this.clock = clock;
|
||||
this.cryptoComponent = cryptoComponent;
|
||||
}
|
||||
|
||||
public BdfDictionary initialize(Transaction txn, Contact c1, Contact c2)
|
||||
throws FormatException, DbException {
|
||||
|
||||
// create local message to keep engine state
|
||||
long now = clock.currentTimeMillis();
|
||||
Bytes salt = new Bytes(new byte[64]);
|
||||
cryptoComponent.getSecureRandom().nextBytes(salt.getBytes());
|
||||
|
||||
Message m = clientHelper
|
||||
.createMessage(introductionManager.getLocalGroup().getId(), now,
|
||||
BdfList.of(salt));
|
||||
MessageId sessionId = m.getId();
|
||||
|
||||
Group g1 = introductionManager.getIntroductionGroup(c1);
|
||||
Group g2 = introductionManager.getIntroductionGroup(c2);
|
||||
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(SESSION_ID, sessionId);
|
||||
d.put(STORAGE_ID, sessionId);
|
||||
d.put(STATE, PREPARE_REQUESTS.getValue());
|
||||
d.put(ROLE, ROLE_INTRODUCER);
|
||||
d.put(GROUP_ID_1, g1.getId());
|
||||
d.put(GROUP_ID_2, g2.getId());
|
||||
d.put(CONTACT_1, c1.getAuthor().getName());
|
||||
d.put(CONTACT_2, c2.getAuthor().getName());
|
||||
d.put(CONTACT_ID_1, c1.getId().getInt());
|
||||
d.put(CONTACT_ID_2, c2.getId().getInt());
|
||||
d.put(AUTHOR_ID_1, c1.getAuthor().getId());
|
||||
d.put(AUTHOR_ID_2, c2.getAuthor().getId());
|
||||
|
||||
// save local state to database
|
||||
clientHelper.addLocalMessage(txn, m, introductionManager.getClientId(), d, false);
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
public void makeIntroduction(Transaction txn, Contact c1, Contact c2,
|
||||
String msg) throws DbException, FormatException {
|
||||
|
||||
// TODO check for existing session with those contacts?
|
||||
// deny new introduction under which conditions?
|
||||
|
||||
// initialize engine state
|
||||
BdfDictionary localState = initialize(txn, c1, c2);
|
||||
|
||||
// define action
|
||||
BdfDictionary localAction = new BdfDictionary();
|
||||
localAction.put(TYPE, TYPE_REQUEST);
|
||||
if (!StringUtils.isNullOrEmpty(msg)) {
|
||||
localAction.put(MSG, msg);
|
||||
}
|
||||
localAction.put(PUBLIC_KEY1, c1.getAuthor().getPublicKey());
|
||||
localAction.put(PUBLIC_KEY2, c2.getAuthor().getPublicKey());
|
||||
|
||||
// start engine and process its state update
|
||||
IntroducerEngine engine = new IntroducerEngine();
|
||||
processStateUpdate(txn,
|
||||
engine.onLocalAction(localState, localAction));
|
||||
}
|
||||
|
||||
public void incomingMessage(Transaction txn, BdfDictionary state,
|
||||
BdfDictionary message) throws DbException, FormatException {
|
||||
|
||||
IntroducerEngine engine = new IntroducerEngine();
|
||||
processStateUpdate(txn,
|
||||
engine.onMessageReceived(state, message));
|
||||
}
|
||||
|
||||
private void processStateUpdate(Transaction txn,
|
||||
IntroducerEngine.StateUpdate<BdfDictionary, BdfDictionary>
|
||||
result) throws DbException, FormatException {
|
||||
|
||||
// save new local state
|
||||
MessageId storageId = new MessageId(result.localState.getRaw(STORAGE_ID));
|
||||
clientHelper.mergeMessageMetadata(txn, storageId, result.localState);
|
||||
|
||||
// send messages
|
||||
for (BdfDictionary d : result.toSend) {
|
||||
introductionManager.sendMessage(txn, d);
|
||||
}
|
||||
|
||||
// broadcast events
|
||||
for (Event event : result.toBroadcast) {
|
||||
txn.attach(event);
|
||||
}
|
||||
}
|
||||
|
||||
public void abort(Transaction txn, BdfDictionary state) {
|
||||
|
||||
IntroducerEngine engine = new IntroducerEngine();
|
||||
BdfDictionary localAction = new BdfDictionary();
|
||||
localAction.put(TYPE, TYPE_ABORT);
|
||||
try {
|
||||
processStateUpdate(txn,
|
||||
engine.onLocalAction(state, localAction));
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,509 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.MessageQueueManager;
|
||||
import org.briarproject.api.clients.PrivateGroupFactory;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.contact.ContactManager.AddContactHook;
|
||||
import org.briarproject.api.contact.ContactManager.RemoveContactHook;
|
||||
import org.briarproject.api.crypto.CryptoComponent;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.data.MetadataParser;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Metadata;
|
||||
import org.briarproject.api.db.NoSuchMessageException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.introduction.IntroducerProtocolState;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
import org.briarproject.api.introduction.IntroductionMessage;
|
||||
import org.briarproject.api.introduction.IntroductionRequest;
|
||||
import org.briarproject.api.introduction.IntroductionResponse;
|
||||
import org.briarproject.api.introduction.SessionId;
|
||||
import org.briarproject.api.properties.TransportPropertyManager;
|
||||
import org.briarproject.api.sync.ClientId;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.GroupFactory;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.sync.MessageStatus;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.clients.BdfIncomingMessageHook;
|
||||
import org.briarproject.util.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ANSWERED;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.AUTHOR_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.CONTACT_ID_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.EXISTS;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.GROUP_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NOT_OUR_RESPONSE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.READ;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.REMOTE_AUTHOR_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_1;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.RESPONSE_2;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCEE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ROLE_INTRODUCER;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.STATE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
|
||||
class IntroductionManagerImpl extends BdfIncomingMessageHook
|
||||
implements IntroductionManager, AddContactHook, RemoveContactHook {
|
||||
|
||||
static final ClientId CLIENT_ID = new ClientId(StringUtils.fromHexString(
|
||||
"23b1897c198a90ae75b976ac023d0f32"
|
||||
+ "80ca67b12f2346b2c23a34f34e2434c3"));
|
||||
|
||||
private static final byte[] LOCAL_GROUP_DESCRIPTOR = new byte[0];
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(IntroductionManagerImpl.class.getName());
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final MessageQueueManager messageQueueManager;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
private final MetadataEncoder metadataEncoder;
|
||||
private final IntroducerManager introducerManager;
|
||||
private final IntroduceeManager introduceeManager;
|
||||
private final Group localGroup;
|
||||
|
||||
@Inject
|
||||
IntroductionManagerImpl(DatabaseComponent db,
|
||||
MessageQueueManager messageQueueManager,
|
||||
ClientHelper clientHelper, GroupFactory groupFactory,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
MetadataEncoder metadataEncoder, MetadataParser metadataParser,
|
||||
CryptoComponent cryptoComponent,
|
||||
TransportPropertyManager transportPropertyManager,
|
||||
AuthorFactory authorFactory, ContactManager contactManager,
|
||||
Clock clock) {
|
||||
|
||||
super(clientHelper, metadataParser, clock);
|
||||
this.db = db;
|
||||
this.messageQueueManager = messageQueueManager;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.metadataEncoder = metadataEncoder;
|
||||
this.introducerManager =
|
||||
new IntroducerManager(this, clientHelper, clock,
|
||||
cryptoComponent);
|
||||
this.introduceeManager =
|
||||
new IntroduceeManager(db, this, clientHelper, clock,
|
||||
cryptoComponent, transportPropertyManager,
|
||||
authorFactory, contactManager);
|
||||
localGroup =
|
||||
groupFactory.createGroup(CLIENT_ID, LOCAL_GROUP_DESCRIPTOR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientId getClientId() {
|
||||
return CLIENT_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||
try {
|
||||
// create an introduction group for sending introduction messages
|
||||
Group g = getIntroductionGroup(c);
|
||||
db.addGroup(txn, g);
|
||||
db.setVisibleToContact(txn, c.getId(), g.getId(), true);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary gm = new BdfDictionary();
|
||||
gm.put(CONTACT, c.getId().getInt());
|
||||
clientHelper.mergeGroupMetadata(txn, g.getId(), gm);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
// check for open sessions with that contact and abort those
|
||||
Long id = (long) c.getId().getInt();
|
||||
try {
|
||||
Map<MessageId, BdfDictionary> map = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, localGroup.getId());
|
||||
for (Map.Entry<MessageId, BdfDictionary> entry : map.entrySet()) {
|
||||
BdfDictionary d = entry.getValue();
|
||||
long role = d.getLong(ROLE, -1L);
|
||||
if (role != ROLE_INTRODUCER) continue;
|
||||
if (d.getLong(CONTACT_ID_1).equals(id) ||
|
||||
d.getLong(CONTACT_ID_2).equals(id)) {
|
||||
|
||||
IntroducerProtocolState state = IntroducerProtocolState
|
||||
.fromValue(d.getLong(STATE).intValue());
|
||||
if (IntroducerProtocolState.isOngoing(state)) {
|
||||
introducerManager.abort(txn, d);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
|
||||
// remove the group (all messages will be removed with it)
|
||||
// this contact won't get our abort message, but the other will
|
||||
db.removeGroup(txn, getIntroductionGroup(c));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called when a new message arrived and is being validated.
|
||||
* It is the central method where we determine which role we play
|
||||
* in the introduction protocol and which engine we need to start.
|
||||
*/
|
||||
@Override
|
||||
protected void incomingMessage(Transaction txn, Message m, BdfList body,
|
||||
BdfDictionary message) throws DbException {
|
||||
|
||||
// add local group for engine states to make sure it exists
|
||||
db.addGroup(txn, localGroup);
|
||||
|
||||
// Get message data and type
|
||||
GroupId groupId = m.getGroupId();
|
||||
message.put(GROUP_ID, groupId);
|
||||
long type = message.getLong(TYPE, -1L);
|
||||
|
||||
// we are an introducee, need to initialize new state
|
||||
if (type == TYPE_REQUEST) {
|
||||
BdfDictionary state;
|
||||
try {
|
||||
state = introduceeManager.initialize(txn, groupId, message);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Could not initialize introducee state");
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
introduceeManager.incomingMessage(txn, state, message);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
introduceeManager.abort(txn, state);
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
introduceeManager.abort(txn, state);
|
||||
}
|
||||
}
|
||||
// our role can be anything
|
||||
else if (type == TYPE_RESPONSE || type == TYPE_ACK || type == TYPE_ABORT) {
|
||||
BdfDictionary state;
|
||||
try {
|
||||
state = getSessionState(txn,
|
||||
message.getRaw(SESSION_ID, new byte[0]));
|
||||
} catch (FormatException e) {
|
||||
LOG.warning("Could not find state for message, deleting...");
|
||||
deleteMessage(txn, m.getId());
|
||||
return;
|
||||
}
|
||||
|
||||
long role = state.getLong(ROLE, -1L);
|
||||
try {
|
||||
if (role == ROLE_INTRODUCER) {
|
||||
introducerManager.incomingMessage(txn, state, message);
|
||||
} else if (role == ROLE_INTRODUCEE) {
|
||||
introduceeManager.incomingMessage(txn, state, message);
|
||||
} else {
|
||||
if(LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Unknown role '" + role +
|
||||
"'. Deleting message...");
|
||||
deleteMessage(txn, m.getId());
|
||||
}
|
||||
}
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
|
||||
else introduceeManager.abort(txn, state);
|
||||
} catch (IOException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
if (role == ROLE_INTRODUCER) introducerManager.abort(txn, state);
|
||||
else introduceeManager.abort(txn, state);
|
||||
}
|
||||
} else {
|
||||
// the message has been validated, so this should not happen
|
||||
if(LOG.isLoggable(WARNING)) {
|
||||
LOG.warning("Unknown message type '" + type + "', deleting...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void makeIntroduction(Contact c1, Contact c2, String msg)
|
||||
throws DbException, FormatException {
|
||||
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
// add local group for session states to make sure it exists
|
||||
db.addGroup(txn, getLocalGroup());
|
||||
introducerManager.makeIntroduction(txn, c1, c2, msg);
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptIntroduction(final SessionId sessionId)
|
||||
throws DbException, FormatException {
|
||||
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
introduceeManager.acceptIntroduction(txn, sessionId);
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void declineIntroduction(final SessionId sessionId)
|
||||
throws DbException, FormatException {
|
||||
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
introduceeManager.declineIntroduction(txn, sessionId);
|
||||
txn.setComplete();
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<IntroductionMessage> getIntroductionMessages(
|
||||
ContactId contactId) throws DbException {
|
||||
|
||||
Collection<IntroductionMessage> list =
|
||||
new ArrayList<IntroductionMessage>();
|
||||
|
||||
Map<MessageId, BdfDictionary> metadata;
|
||||
Collection<MessageStatus> statuses;
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
// get messages and their status
|
||||
GroupId g =
|
||||
getIntroductionGroup(db.getContact(txn, contactId)).getId();
|
||||
metadata = clientHelper.getMessageMetadataAsDictionary(txn, g);
|
||||
statuses = db.getMessageStatus(txn, contactId, g);
|
||||
|
||||
// turn messages into classes for the UI
|
||||
Map<SessionId, BdfDictionary> sessionStates =
|
||||
new HashMap<SessionId, BdfDictionary>();
|
||||
for (MessageStatus s : statuses) {
|
||||
MessageId messageId = s.getMessageId();
|
||||
BdfDictionary msg = metadata.get(messageId);
|
||||
if (msg == null) continue;
|
||||
|
||||
try {
|
||||
long type = msg.getLong(TYPE);
|
||||
if (type == TYPE_ACK || type == TYPE_ABORT) continue;
|
||||
|
||||
// get session state
|
||||
SessionId sessionId = new SessionId(msg.getRaw(SESSION_ID));
|
||||
BdfDictionary state = sessionStates.get(sessionId);
|
||||
if (state == null) {
|
||||
state = getSessionState(txn, sessionId.getBytes());
|
||||
}
|
||||
sessionStates.put(sessionId, state);
|
||||
|
||||
boolean local;
|
||||
long time = msg.getLong(MESSAGE_TIME);
|
||||
boolean accepted = msg.getBoolean(ACCEPT, false);
|
||||
boolean read = msg.getBoolean(READ, false);
|
||||
AuthorId authorId;
|
||||
String name;
|
||||
if (type == TYPE_RESPONSE) {
|
||||
if (state.getLong(ROLE) == ROLE_INTRODUCER) {
|
||||
if (!concernsThisContact(contactId, messageId, state)) {
|
||||
// this response is not from contactId
|
||||
continue;
|
||||
}
|
||||
local = false;
|
||||
authorId =
|
||||
getAuthorIdForIntroducer(contactId, state);
|
||||
name = getNameForIntroducer(contactId, state);
|
||||
} else {
|
||||
if (Arrays.equals(state.getRaw(NOT_OUR_RESPONSE),
|
||||
messageId.getBytes())) {
|
||||
// this response is not ours, don't include it
|
||||
continue;
|
||||
}
|
||||
local = true;
|
||||
authorId = new AuthorId(
|
||||
state.getRaw(REMOTE_AUTHOR_ID));
|
||||
name = state.getString(NAME);
|
||||
}
|
||||
IntroductionResponse ir = new IntroductionResponse(
|
||||
sessionId, messageId, time, local, s.isSent(),
|
||||
s.isSeen(), read, authorId, name, accepted);
|
||||
list.add(ir);
|
||||
} else if (type == TYPE_REQUEST) {
|
||||
String message;
|
||||
boolean answered, exists;
|
||||
if (state.getLong(ROLE) == ROLE_INTRODUCER) {
|
||||
local = true;
|
||||
authorId =
|
||||
getAuthorIdForIntroducer(contactId, state);
|
||||
name = getNameForIntroducer(contactId, state);
|
||||
message = msg.getOptionalString(MSG);
|
||||
answered = false;
|
||||
exists = false;
|
||||
} else {
|
||||
local = false;
|
||||
authorId = new AuthorId(
|
||||
state.getRaw(REMOTE_AUTHOR_ID));
|
||||
name = state.getString(NAME);
|
||||
message = state.getOptionalString(MSG);
|
||||
answered = state.getBoolean(ANSWERED);
|
||||
exists = state.getBoolean(EXISTS);
|
||||
}
|
||||
IntroductionRequest ir = new IntroductionRequest(
|
||||
sessionId, messageId, time, local, s.isSent(),
|
||||
s.isSeen(), read, authorId, name, accepted,
|
||||
message, answered, exists);
|
||||
list.add(ir);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e);
|
||||
}
|
||||
}
|
||||
txn.setComplete();
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private String getNameForIntroducer(ContactId contactId,
|
||||
BdfDictionary state) throws FormatException {
|
||||
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
|
||||
return state.getString(CONTACT_2);
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
|
||||
return state.getString(CONTACT_1);
|
||||
throw new RuntimeException("Contact not part of this introduction session");
|
||||
}
|
||||
|
||||
private AuthorId getAuthorIdForIntroducer(ContactId contactId,
|
||||
BdfDictionary state) throws FormatException {
|
||||
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue())
|
||||
return new AuthorId(state.getRaw(AUTHOR_ID_2));
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_2).intValue())
|
||||
return new AuthorId(state.getRaw(AUTHOR_ID_1));
|
||||
throw new RuntimeException("Contact not part of this introduction session");
|
||||
}
|
||||
|
||||
private boolean concernsThisContact(ContactId contactId, MessageId messageId,
|
||||
BdfDictionary state) throws FormatException {
|
||||
|
||||
if (contactId.getInt() == state.getLong(CONTACT_ID_1).intValue()) {
|
||||
return Arrays.equals(state.getRaw(RESPONSE_1, new byte[0]),
|
||||
messageId.getBytes());
|
||||
} else {
|
||||
return Arrays.equals(state.getRaw(RESPONSE_2, new byte[0]),
|
||||
messageId.getBytes());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadFlag(MessageId m, boolean read) throws DbException {
|
||||
try {
|
||||
BdfDictionary meta = BdfDictionary.of(new BdfEntry(READ, read));
|
||||
clientHelper.mergeMessageMetadata(m, meta);
|
||||
} catch (FormatException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public BdfDictionary getSessionState(Transaction txn, byte[] sessionId)
|
||||
throws DbException, FormatException {
|
||||
|
||||
try {
|
||||
return clientHelper.getMessageMetadataAsDictionary(txn,
|
||||
new MessageId(sessionId));
|
||||
} catch (NoSuchMessageException e) {
|
||||
Map<MessageId, BdfDictionary> map = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn,
|
||||
localGroup.getId());
|
||||
for (Map.Entry<MessageId, BdfDictionary> m : map.entrySet()) {
|
||||
if (Arrays.equals(m.getValue().getRaw(SESSION_ID), sessionId)) {
|
||||
return m.getValue();
|
||||
}
|
||||
}
|
||||
if (LOG.isLoggable(WARNING)) {
|
||||
LOG.warning(
|
||||
"No session state found for this message with session ID " +
|
||||
Arrays.hashCode(sessionId));
|
||||
}
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
|
||||
public Group getIntroductionGroup(Contact c) {
|
||||
return privateGroupFactory.createPrivateGroup(CLIENT_ID, c);
|
||||
}
|
||||
|
||||
public Group getLocalGroup() {
|
||||
return localGroup;
|
||||
}
|
||||
|
||||
public void sendMessage(Transaction txn, BdfDictionary message)
|
||||
throws DbException, FormatException {
|
||||
|
||||
BdfList bdfList = MessageEncoder.encodeMessage(message);
|
||||
byte[] body = clientHelper.toByteArray(bdfList);
|
||||
GroupId groupId = new GroupId(message.getRaw(GROUP_ID));
|
||||
Group group = db.getGroup(txn, groupId);
|
||||
long timestamp = System.currentTimeMillis();
|
||||
|
||||
message.put(MESSAGE_TIME, timestamp);
|
||||
Metadata metadata = metadataEncoder.encode(message);
|
||||
|
||||
messageQueueManager
|
||||
.sendMessage(txn, group, timestamp, body, metadata);
|
||||
}
|
||||
|
||||
private void deleteMessage(Transaction txn, MessageId messageId)
|
||||
throws DbException {
|
||||
|
||||
db.deleteMessage(txn, messageId);
|
||||
db.deleteMessageMetadata(txn, messageId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.MessageQueueManager;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
@Module
|
||||
public class IntroductionModule {
|
||||
|
||||
public static class EagerSingletons {
|
||||
@Inject IntroductionManager introductionManager;
|
||||
@Inject IntroductionValidator introductionValidator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
IntroductionValidator getValidator(MessageQueueManager messageQueueManager,
|
||||
IntroductionManager introductionManager,
|
||||
MetadataEncoder metadataEncoder, ClientHelper clientHelper,
|
||||
Clock clock) {
|
||||
|
||||
IntroductionValidator introductionValidator = new IntroductionValidator(
|
||||
clientHelper, metadataEncoder, clock);
|
||||
|
||||
messageQueueManager.registerMessageValidator(
|
||||
introductionManager.getClientId(),
|
||||
introductionValidator);
|
||||
|
||||
return introductionValidator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
IntroductionManager getIntroductionManager(
|
||||
ContactManager contactManager,
|
||||
MessageQueueManager messageQueueManager,
|
||||
IntroductionManagerImpl introductionManager) {
|
||||
|
||||
contactManager.registerAddContactHook(introductionManager);
|
||||
contactManager.registerRemoveContactHook(introductionManager);
|
||||
messageQueueManager
|
||||
.registerIncomingMessageHook(introductionManager.getClientId(),
|
||||
introductionManager);
|
||||
|
||||
return introductionManager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.DeviceId;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.TransportId;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.introduction.SessionId;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.clients.BdfMessageValidator;
|
||||
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
|
||||
import static org.briarproject.api.identity.AuthorConstants.MAX_PUBLIC_KEY_LENGTH;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MESSAGE_TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
import static org.briarproject.api.properties.TransportPropertyConstants.MAX_PROPERTY_LENGTH;
|
||||
import static org.briarproject.api.sync.SyncConstants.MAX_MESSAGE_BODY_LENGTH;
|
||||
|
||||
class IntroductionValidator extends BdfMessageValidator {
|
||||
|
||||
IntroductionValidator(ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BdfDictionary validateMessage(Message m, Group g, BdfList body)
|
||||
throws FormatException {
|
||||
|
||||
BdfDictionary d;
|
||||
long type = body.getLong(0);
|
||||
byte[] id = body.getRaw(1);
|
||||
checkLength(id, SessionId.LENGTH);
|
||||
|
||||
if (type == TYPE_REQUEST) {
|
||||
d = validateRequest(body);
|
||||
} else if (type == TYPE_RESPONSE) {
|
||||
d = validateResponse(body);
|
||||
} else if (type == TYPE_ACK) {
|
||||
d = validateAck(body);
|
||||
} else if (type == TYPE_ABORT) {
|
||||
d = validateAbort(body);
|
||||
} else {
|
||||
throw new FormatException();
|
||||
}
|
||||
|
||||
d.put(TYPE, type);
|
||||
d.put(SESSION_ID, id);
|
||||
d.put(MESSAGE_ID, m.getId());
|
||||
d.put(MESSAGE_TIME, m.getTimestamp());
|
||||
return d;
|
||||
}
|
||||
|
||||
private BdfDictionary validateRequest(BdfList message)
|
||||
throws FormatException {
|
||||
|
||||
checkSize(message, 4, 5);
|
||||
|
||||
// parse contact name
|
||||
String name = message.getString(2);
|
||||
checkLength(name, 1, MAX_AUTHOR_NAME_LENGTH);
|
||||
|
||||
// parse contact's public key
|
||||
byte[] key = message.getRaw(3);
|
||||
checkLength(key, 0, MAX_PUBLIC_KEY_LENGTH);
|
||||
|
||||
// parse (optional) message
|
||||
String msg = null;
|
||||
if (message.size() == 5) {
|
||||
msg = message.getString(4);
|
||||
checkLength(msg, 0, MAX_MESSAGE_BODY_LENGTH);
|
||||
}
|
||||
|
||||
// Return the metadata
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(NAME, name);
|
||||
d.put(PUBLIC_KEY, key);
|
||||
if (msg != null) {
|
||||
d.put(MSG, msg);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private BdfDictionary validateResponse(BdfList message)
|
||||
throws FormatException {
|
||||
|
||||
checkSize(message, 3, 7);
|
||||
|
||||
// parse accept/decline
|
||||
boolean accept = message.getBoolean(2);
|
||||
|
||||
long time = 0;
|
||||
byte[] pubkey = null;
|
||||
byte[] deviceId = null;
|
||||
BdfDictionary tp = new BdfDictionary();
|
||||
if (accept) {
|
||||
checkSize(message, 7);
|
||||
|
||||
// parse timestamp
|
||||
time = message.getLong(3);
|
||||
|
||||
// parse ephemeral public key
|
||||
pubkey = message.getRaw(4);
|
||||
checkLength(pubkey, 0, MAX_PUBLIC_KEY_LENGTH);
|
||||
|
||||
// parse device ID
|
||||
deviceId = message.getRaw(5);
|
||||
checkLength(deviceId, DeviceId.LENGTH);
|
||||
|
||||
// parse transport properties
|
||||
tp = message.getDictionary(6);
|
||||
if (tp.size() < 1) throw new FormatException();
|
||||
for (String tId : tp.keySet()) {
|
||||
checkLength(tId, 1, TransportId.MAX_TRANSPORT_ID_LENGTH);
|
||||
BdfDictionary tProps = tp.getDictionary(tId);
|
||||
for (String propId : tProps.keySet()) {
|
||||
checkLength(propId, 0, MAX_PROPERTY_LENGTH);
|
||||
String prop = tProps.getString(propId);
|
||||
checkLength(prop, 0, MAX_PROPERTY_LENGTH);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
checkSize(message, 3);
|
||||
}
|
||||
|
||||
// Return the metadata
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(ACCEPT, accept);
|
||||
if (accept) {
|
||||
d.put(TIME, time);
|
||||
d.put(E_PUBLIC_KEY, pubkey);
|
||||
d.put(DEVICE_ID, deviceId);
|
||||
d.put(TRANSPORT, tp);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private BdfDictionary validateAck(BdfList message)
|
||||
throws FormatException {
|
||||
|
||||
checkSize(message, 2);
|
||||
|
||||
// Return the metadata
|
||||
return new BdfDictionary();
|
||||
}
|
||||
|
||||
private BdfDictionary validateAbort(BdfList message)
|
||||
throws FormatException {
|
||||
|
||||
checkSize(message, 2);
|
||||
|
||||
// Return the metadata
|
||||
return new BdfDictionary();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.briarproject.introduction;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.ACCEPT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.DEVICE_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.E_PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.MSG;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.NAME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.PUBLIC_KEY;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.SESSION_ID;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TIME;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TRANSPORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ABORT;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_ACK;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_REQUEST;
|
||||
import static org.briarproject.api.introduction.IntroductionConstants.TYPE_RESPONSE;
|
||||
|
||||
public class MessageEncoder {
|
||||
|
||||
public static BdfList encodeMessage(BdfDictionary d) throws FormatException {
|
||||
|
||||
BdfList body;
|
||||
long type = d.getLong(TYPE);
|
||||
if (type == TYPE_REQUEST) {
|
||||
body = encodeRequest(d);
|
||||
} else if (type == TYPE_RESPONSE) {
|
||||
body = encodeResponse(d);
|
||||
} else if (type == TYPE_ACK) {
|
||||
body = encodeAck(d);
|
||||
} else if (type == TYPE_ABORT) {
|
||||
body = encodeAbort(d);
|
||||
} else {
|
||||
throw new FormatException();
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
private static BdfList encodeRequest(BdfDictionary d) throws FormatException {
|
||||
BdfList list = BdfList.of(TYPE_REQUEST, d.getRaw(SESSION_ID),
|
||||
d.getString(NAME), d.getRaw(PUBLIC_KEY));
|
||||
|
||||
if (d.containsKey(MSG)) {
|
||||
list.add(d.getString(MSG));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static BdfList encodeResponse(BdfDictionary d) throws FormatException {
|
||||
BdfList list = BdfList.of(TYPE_RESPONSE, d.getRaw(SESSION_ID),
|
||||
d.getBoolean(ACCEPT));
|
||||
|
||||
if (d.getBoolean(ACCEPT)) {
|
||||
list.add(d.getLong(TIME));
|
||||
list.add(d.getRaw(E_PUBLIC_KEY));
|
||||
list.add(d.getRaw(DEVICE_ID));
|
||||
list.add(d.getDictionary(TRANSPORT));
|
||||
}
|
||||
// TODO Sign the response, see #256
|
||||
return list;
|
||||
}
|
||||
|
||||
private static BdfList encodeAck(BdfDictionary d) throws FormatException {
|
||||
return BdfList.of(TYPE_ACK, d.getRaw(SESSION_ID));
|
||||
}
|
||||
|
||||
private static BdfList encodeAbort(BdfDictionary d) throws FormatException {
|
||||
return BdfList.of(TYPE_ABORT, d.getRaw(SESSION_ID));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user