mirror of
https://code.briarproject.org/briar/briar.git
synced 2026-02-12 10:49:06 +01:00
Merge branch '709-private-group-invitation-protocol' into 'master'
Private group invitation protocol This branch implements the private group invitation protocol. The implementation is something of an experiment with a new way of writing client protocols. We start with a role enum that lists the roles in the protocol, and a state enum for each role, which lists the states in the role's state machine. Then there's a session class, parameterised by the state class and therefore by the role, which represents the session information held by that role. Then there's an engine interface, parameterised by the session class and therefore by the role, which encapsulates the protocol logic for the role. Most of this stuff can be created pretty mechanically from the state machine diagrams. The engine interface has a method for each type of message and each local action. I started out with one method for all messages and another for all local actions, but that turned out to be a bad design - the information about what kind of message was being handled was lost when the message was passed to the engine, and had to be recovered using an instanceof ladder. Each engine method takes a message or an action and a session, and returns an updated session. A transaction is passed in so the engine can send messages, attach events, and do any other work it needs to do (such as changing the visibility of groups, in the case of this protocol). This removes the need to run tasks outside the engine, so the protocol logic is better encapsulated inside the engine. Parsing and encoding of messages and sessions is separated from protocol logic. MessageParser, MessageEncoder and the validator are the only classes that know how messages and their metadata are formatted, and likewise SessionParser and SessionEncoder are the only classes that know how sessions are formatted. The metadata keys are declared in a package-private interface. It's common knowledge that I never make mistakes, so to keep things interesting I've hidden 114 deliberate mistakes in this code. See how many you can spot! Needs tests before #709 is closed. See merge request !382
This commit is contained in:
@@ -27,7 +27,6 @@ import org.briarproject.api.privategroup.JoinMessageHeader;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
@@ -94,8 +93,6 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
|
||||
PrivateGroupFactory privateGroupFactory;
|
||||
@Inject
|
||||
GroupMessageFactory groupMessageFactory;
|
||||
@Inject
|
||||
GroupInvitationManager groupInvitationManager;
|
||||
|
||||
// objects accessed from background threads need to be volatile
|
||||
private volatile Waiter validationWaiter;
|
||||
|
||||
@@ -17,6 +17,7 @@ import org.briarproject.identity.IdentityModule;
|
||||
import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
import org.briarproject.privategroup.PrivateGroupModule;
|
||||
import org.briarproject.privategroup.invitation.GroupInvitationModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.sharing.SharingModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
@@ -40,6 +41,7 @@ import dagger.Component;
|
||||
EventModule.class,
|
||||
MessagingModule.class,
|
||||
PrivateGroupModule.class,
|
||||
GroupInvitationModule.class,
|
||||
IdentityModule.class,
|
||||
LifecycleModule.class,
|
||||
PropertiesModule.class,
|
||||
|
||||
@@ -20,7 +20,6 @@ import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.feed.FeedManager;
|
||||
import org.briarproject.api.forum.ForumManager;
|
||||
import org.briarproject.api.forum.ForumSharingManager;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.introduction.IntroductionManager;
|
||||
import org.briarproject.api.invitation.InvitationTaskFactory;
|
||||
@@ -37,6 +36,7 @@ import org.briarproject.api.plugins.PluginManager;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.settings.SettingsManager;
|
||||
import org.briarproject.api.system.Clock;
|
||||
@@ -68,8 +68,6 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
||||
|
||||
DatabaseConfig databaseConfig();
|
||||
|
||||
AuthorFactory authFactory();
|
||||
|
||||
ReferenceManager referenceMangager();
|
||||
|
||||
@DatabaseExecutor
|
||||
@@ -99,6 +97,8 @@ public interface AndroidComponent extends CoreEagerSingletons {
|
||||
|
||||
PrivateGroupManager privateGroupManager();
|
||||
|
||||
GroupInvitationFactory groupInvitationFactory();
|
||||
|
||||
GroupInvitationManager groupInvitationManager();
|
||||
|
||||
PrivateGroupFactory privateGroupFactory();
|
||||
|
||||
@@ -898,7 +898,7 @@ public class ConversationActivity extends BriarActivity
|
||||
@DatabaseExecutor
|
||||
private void respondToGroupRequest(SessionId id, boolean accept)
|
||||
throws DbException {
|
||||
groupInvitationManager.respondToInvitation(id, accept);
|
||||
groupInvitationManager.respondToInvitation(contactId, id, accept);
|
||||
}
|
||||
|
||||
private void introductionResponseError() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.briarproject.android.privategroup.creation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.briarproject.R;
|
||||
import org.briarproject.android.controller.handler.UiResultExceptionHandler;
|
||||
@@ -16,7 +15,6 @@ import java.util.Collection;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static android.widget.Toast.LENGTH_SHORT;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
|
||||
|
||||
public abstract class BaseGroupInviteActivity
|
||||
@@ -68,9 +66,6 @@ public abstract class BaseGroupInviteActivity
|
||||
new UiResultExceptionHandler<Void, DbException>(this) {
|
||||
@Override
|
||||
public void onResultUi(Void result) {
|
||||
Toast.makeText(BaseGroupInviteActivity.this,
|
||||
"Inviting members is not yet implemented",
|
||||
LENGTH_SHORT).show();
|
||||
setResult(RESULT_OK);
|
||||
supportFinishAfterTransition();
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ import org.briarproject.android.controller.DbController;
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@NotNullByDefault
|
||||
public interface CreateGroupController extends DbController {
|
||||
|
||||
void createGroup(String name,
|
||||
|
||||
@@ -2,57 +2,75 @@ package org.briarproject.android.privategroup.creation;
|
||||
|
||||
import org.briarproject.android.controller.DbControllerImpl;
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.db.DatabaseExecutor;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.NoSuchContactException;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static java.util.logging.Level.WARNING;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class CreateGroupControllerImpl extends DbControllerImpl
|
||||
implements CreateGroupController {
|
||||
|
||||
private static final Logger LOG =
|
||||
Logger.getLogger(CreateGroupControllerImpl.class.getName());
|
||||
|
||||
private final Executor cryptoExecutor;
|
||||
private final ContactManager contactManager;
|
||||
private final IdentityManager identityManager;
|
||||
private final PrivateGroupFactory groupFactory;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
private final PrivateGroupManager groupManager;
|
||||
private final GroupInvitationFactory groupInvitationFactory;
|
||||
private final GroupInvitationManager groupInvitationManager;
|
||||
private final Clock clock;
|
||||
@CryptoExecutor
|
||||
private final Executor cryptoExecutor;
|
||||
|
||||
@Inject
|
||||
CreateGroupControllerImpl(@DatabaseExecutor Executor dbExecutor,
|
||||
@CryptoExecutor Executor cryptoExecutor,
|
||||
LifecycleManager lifecycleManager, IdentityManager identityManager,
|
||||
PrivateGroupFactory groupFactory,
|
||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||
IdentityManager identityManager, PrivateGroupFactory groupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
PrivateGroupManager groupManager, Clock clock) {
|
||||
PrivateGroupManager groupManager,
|
||||
GroupInvitationFactory groupInvitationFactory,
|
||||
GroupInvitationManager groupInvitationManager, Clock clock) {
|
||||
super(dbExecutor, lifecycleManager);
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
this.contactManager = contactManager;
|
||||
this.identityManager = identityManager;
|
||||
this.groupFactory = groupFactory;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
this.groupManager = groupManager;
|
||||
this.groupInvitationFactory = groupInvitationFactory;
|
||||
this.groupInvitationManager = groupInvitationManager;
|
||||
this.clock = clock;
|
||||
this.cryptoExecutor = cryptoExecutor;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -111,17 +129,89 @@ public class CreateGroupControllerImpl extends DbControllerImpl
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendInvitation(final GroupId groupId,
|
||||
final Collection<ContactId> contacts, final String message,
|
||||
final ResultExceptionHandler<Void, DbException> result) {
|
||||
public void sendInvitation(final GroupId g,
|
||||
final Collection<ContactId> contactIds, final String message,
|
||||
final ResultExceptionHandler<Void, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// TODO actually send invitation
|
||||
//noinspection ConstantConditions
|
||||
result.onResult(null);
|
||||
try {
|
||||
LocalAuthor localAuthor = identityManager.getLocalAuthor();
|
||||
List<Contact> contacts = new ArrayList<>();
|
||||
for (ContactId c : contactIds) {
|
||||
try {
|
||||
contacts.add(contactManager.getContact(c));
|
||||
} catch (NoSuchContactException e) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
signInvitations(g, localAuthor, contacts, message, handler);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void signInvitations(final GroupId g, final LocalAuthor localAuthor,
|
||||
final Collection<Contact> contacts, final String message,
|
||||
final ResultExceptionHandler<Void, DbException> handler) {
|
||||
cryptoExecutor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
long timestamp = clock.currentTimeMillis();
|
||||
List<InvitationContext> contexts = new ArrayList<>();
|
||||
for (Contact c : contacts) {
|
||||
byte[] signature = groupInvitationFactory.signInvitation(c,
|
||||
g, timestamp, localAuthor.getPrivateKey());
|
||||
contexts.add(new InvitationContext(c.getId(), timestamp,
|
||||
signature));
|
||||
}
|
||||
sendInvitations(g, contexts, message, handler);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void sendInvitations(final GroupId g,
|
||||
final Collection<InvitationContext> contexts, final String message,
|
||||
final ResultExceptionHandler<Void, DbException> handler) {
|
||||
runOnDbThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
String msg = message.isEmpty() ? null : message;
|
||||
for (InvitationContext context : contexts) {
|
||||
try {
|
||||
groupInvitationManager.sendInvitation(g,
|
||||
context.contactId, msg, context.timestamp,
|
||||
context.signature);
|
||||
} catch (NoSuchContactException e) {
|
||||
// Continue
|
||||
}
|
||||
}
|
||||
handler.onResult(null);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
handler.onException(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static class InvitationContext {
|
||||
|
||||
private final ContactId contactId;
|
||||
private final long timestamp;
|
||||
private final byte[] signature;
|
||||
|
||||
private InvitationContext(ContactId contactId, long timestamp,
|
||||
byte[] signature) {
|
||||
this.contactId = contactId;
|
||||
this.timestamp = timestamp;
|
||||
this.signature = signature;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,17 @@ import org.briarproject.android.sharing.ContactSelectorFragment;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.db.DatabaseExecutor;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public class GroupInviteActivity extends BaseGroupInviteActivity
|
||||
implements MessageFragmentListener {
|
||||
|
||||
@Inject
|
||||
GroupInvitationManager groupInvitationManager;
|
||||
|
||||
@Override
|
||||
public void injectActivity(ActivityComponent component) {
|
||||
component.inject(this);
|
||||
@@ -42,9 +48,8 @@ public class GroupInviteActivity extends BaseGroupInviteActivity
|
||||
|
||||
@Override
|
||||
@DatabaseExecutor
|
||||
public boolean isDisabled(GroupId groupId, Contact c) throws DbException {
|
||||
// TODO disable contacts that can not be invited
|
||||
return false;
|
||||
public boolean isDisabled(GroupId g, Contact c) throws DbException {
|
||||
return !groupInvitationManager.isInvitationAllowed(c, g);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,12 +2,13 @@ package org.briarproject.android.privategroup.invitation;
|
||||
|
||||
import org.briarproject.android.controller.handler.ResultExceptionHandler;
|
||||
import org.briarproject.android.sharing.InvitationControllerImpl;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.db.DatabaseExecutor;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.event.Event;
|
||||
import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.GroupInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.GroupInvitationRequestReceivedEvent;
|
||||
import org.briarproject.api.event.GroupInvitationResponseReceivedEvent;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
@@ -44,8 +45,11 @@ public class GroupInvitationControllerImpl
|
||||
public void eventOccurred(Event e) {
|
||||
super.eventOccurred(e);
|
||||
|
||||
if (e instanceof GroupInvitationReceivedEvent) {
|
||||
LOG.info("Group invitation received, reloading");
|
||||
if (e instanceof GroupInvitationRequestReceivedEvent) {
|
||||
LOG.info("Group invitation request received, reloading");
|
||||
listener.loadInvitations(false);
|
||||
} else if (e instanceof GroupInvitationResponseReceivedEvent) {
|
||||
LOG.info("Group invitation response received, reloading");
|
||||
listener.loadInvitations(false);
|
||||
}
|
||||
}
|
||||
@@ -70,8 +74,8 @@ public class GroupInvitationControllerImpl
|
||||
public void run() {
|
||||
try {
|
||||
PrivateGroup g = item.getShareable();
|
||||
Contact c = item.getCreator();
|
||||
groupInvitationManager.respondToInvitation(g, c, accept);
|
||||
ContactId c = item.getCreator().getId();
|
||||
groupInvitationManager.respondToInvitation(c, g, accept);
|
||||
} catch (DbException e) {
|
||||
if (LOG.isLoggable(WARNING))
|
||||
LOG.log(WARNING, e.toString(), e);
|
||||
|
||||
@@ -14,7 +14,7 @@ import org.briarproject.api.event.EventBus;
|
||||
import org.briarproject.api.event.EventListener;
|
||||
import org.briarproject.api.event.GroupAddedEvent;
|
||||
import org.briarproject.api.event.GroupDissolvedEvent;
|
||||
import org.briarproject.api.event.GroupInvitationReceivedEvent;
|
||||
import org.briarproject.api.event.GroupInvitationRequestReceivedEvent;
|
||||
import org.briarproject.api.event.GroupMessageAddedEvent;
|
||||
import org.briarproject.api.event.GroupRemovedEvent;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
@@ -92,8 +92,7 @@ public class GroupListControllerImpl extends DbControllerImpl
|
||||
GroupMessageAddedEvent g = (GroupMessageAddedEvent) e;
|
||||
LOG.info("Private group message added");
|
||||
onGroupMessageAdded(g.getHeader());
|
||||
} else if (e instanceof GroupInvitationReceivedEvent) {
|
||||
GroupInvitationReceivedEvent g = (GroupInvitationReceivedEvent) e;
|
||||
} else if (e instanceof GroupInvitationRequestReceivedEvent) {
|
||||
LOG.info("Private group invitation received");
|
||||
onGroupInvitationReceived();
|
||||
} else if (e instanceof GroupAddedEvent) {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
package org.briarproject.api.event;
|
||||
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.forum.ForumInvitationRequest;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
|
||||
|
||||
public class GroupInvitationReceivedEvent extends
|
||||
public class GroupInvitationRequestReceivedEvent extends
|
||||
InvitationRequestReceivedEvent<PrivateGroup> {
|
||||
|
||||
public GroupInvitationReceivedEvent(PrivateGroup group, ContactId contactId,
|
||||
GroupInvitationRequest request) {
|
||||
public GroupInvitationRequestReceivedEvent(PrivateGroup group,
|
||||
ContactId contactId, GroupInvitationRequest request) {
|
||||
super(group, contactId, request);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.briarproject.api.event;
|
||||
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.sharing.InvitationResponse;
|
||||
|
||||
public class GroupInvitationResponseReceivedEvent
|
||||
extends InvitationResponseReceivedEvent {
|
||||
|
||||
public GroupInvitationResponseReceivedEvent(ContactId contactId,
|
||||
InvitationResponse response) {
|
||||
super(contactId, response);
|
||||
}
|
||||
}
|
||||
@@ -14,38 +14,63 @@ import java.util.Collection;
|
||||
@NotNullByDefault
|
||||
public interface PrivateGroupManager extends MessageTracker {
|
||||
|
||||
/** The unique ID of the private group client. */
|
||||
/**
|
||||
* The unique ID of the private group client.
|
||||
*/
|
||||
ClientId CLIENT_ID = new ClientId("org.briarproject.briar.privategroup");
|
||||
|
||||
/**
|
||||
* Adds a new private group and joins it.
|
||||
*
|
||||
* @param group The private group to add
|
||||
* @param joinMsg The creator's own join message
|
||||
* @param joinMsg The new member's join message
|
||||
*/
|
||||
void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
|
||||
throws DbException;
|
||||
|
||||
/** Removes a dissolved private group. */
|
||||
/**
|
||||
* Adds a new private group and joins it.
|
||||
*
|
||||
* @param group The private group to add
|
||||
* @param joinMsg The new member's join message
|
||||
*/
|
||||
void addPrivateGroup(Transaction txn, PrivateGroup group,
|
||||
GroupMessage joinMsg) throws DbException;
|
||||
|
||||
/**
|
||||
* Removes a dissolved private group.
|
||||
*/
|
||||
void removePrivateGroup(GroupId g) throws DbException;
|
||||
|
||||
/** Gets the MessageId of your previous message sent to the group */
|
||||
/**
|
||||
* Gets the MessageId of your previous message sent to the group
|
||||
*/
|
||||
MessageId getPreviousMsgId(GroupId g) throws DbException;
|
||||
|
||||
/** Returns the timestamp of the message with the given ID */
|
||||
/**
|
||||
* Returns the timestamp of the message with the given ID
|
||||
*/
|
||||
// TODO change to getPreviousMessageHeader()
|
||||
long getMessageTimestamp(MessageId id) throws DbException;
|
||||
|
||||
/** Marks the group with GroupId g as resolved */
|
||||
/**
|
||||
* Marks the group with GroupId g as resolved
|
||||
*/
|
||||
void markGroupDissolved(Transaction txn, GroupId g) throws DbException;
|
||||
|
||||
/** Returns true if the private group has been dissolved. */
|
||||
/**
|
||||
* Returns true if the private group has been dissolved.
|
||||
*/
|
||||
boolean isDissolved(GroupId g) throws DbException;
|
||||
|
||||
/** Stores (and sends) a local group message. */
|
||||
/**
|
||||
* Stores (and sends) a local group message.
|
||||
*/
|
||||
GroupMessageHeader addLocalMessage(GroupMessage p) throws DbException;
|
||||
|
||||
/** Returns the private group with the given ID. */
|
||||
/**
|
||||
* Returns the private group with the given ID.
|
||||
*/
|
||||
PrivateGroup getPrivateGroup(GroupId g) throws DbException;
|
||||
|
||||
/**
|
||||
@@ -53,25 +78,35 @@ public interface PrivateGroupManager extends MessageTracker {
|
||||
*/
|
||||
PrivateGroup getPrivateGroup(Transaction txn, GroupId g) throws DbException;
|
||||
|
||||
/** Returns all private groups the user is a member of. */
|
||||
/**
|
||||
* Returns all private groups the user is a member of.
|
||||
*/
|
||||
Collection<PrivateGroup> getPrivateGroups() throws DbException;
|
||||
|
||||
/** Returns the body of the group message with the given ID. */
|
||||
/**
|
||||
* Returns the body of the group message with the given ID.
|
||||
*/
|
||||
String getMessageBody(MessageId m) throws DbException;
|
||||
|
||||
/** Returns the headers of all group messages in the given group. */
|
||||
/**
|
||||
* Returns the headers of all group messages in the given group.
|
||||
*/
|
||||
Collection<GroupMessageHeader> getHeaders(GroupId g) throws DbException;
|
||||
|
||||
/** Returns all members of the group with ID g */
|
||||
/**
|
||||
* Returns all members of the group with ID g
|
||||
*/
|
||||
Collection<GroupMember> getMembers(GroupId g) throws DbException;
|
||||
|
||||
/** Returns true if the given Author a is member of the group with ID g */
|
||||
/**
|
||||
* Returns true if the given Author a is member of the group with ID g
|
||||
*/
|
||||
boolean isMember(Transaction txn, GroupId g, Author a) throws DbException;
|
||||
|
||||
/**
|
||||
* Registers a hook to be called when members are added
|
||||
* or groups are removed.
|
||||
* */
|
||||
*/
|
||||
void registerPrivateGroupHook(PrivateGroupHook hook);
|
||||
|
||||
@NotNullByDefault
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
package org.briarproject.api.privategroup.invitation;
|
||||
|
||||
public interface GroupInvitationConstants {
|
||||
|
||||
// Group Metadata Keys
|
||||
String CONTACT_ID = "contactId";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.briarproject.api.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.crypto.CryptoExecutor;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
public interface GroupInvitationFactory {
|
||||
|
||||
/**
|
||||
* Returns a signature to include when inviting a member to join a private
|
||||
* group. If the member accepts the invitation, the signature will be
|
||||
* included in the member's join message.
|
||||
*/
|
||||
@CryptoExecutor
|
||||
byte[] signInvitation(Contact c, GroupId privateGroupId, long timestamp,
|
||||
byte[] privateKey);
|
||||
|
||||
/**
|
||||
* Returns a token to be signed by the creator when inviting a member to
|
||||
* join a private group. If the member accepts the invitation, the
|
||||
* signature will be included in the member's join message.
|
||||
*/
|
||||
BdfList createInviteToken(AuthorId creatorId, AuthorId memberId,
|
||||
GroupId privateGroupId, long timestamp);
|
||||
}
|
||||
@@ -13,10 +13,8 @@ public class GroupInvitationItem extends InvitationItem<PrivateGroup> {
|
||||
|
||||
private final Contact creator;
|
||||
|
||||
public GroupInvitationItem(PrivateGroup shareable, boolean subscribed,
|
||||
Contact creator) {
|
||||
super(shareable, subscribed);
|
||||
|
||||
public GroupInvitationItem(PrivateGroup privateGroup, Contact creator) {
|
||||
super(privateGroup, false);
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package org.briarproject.api.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.clients.MessageTracker;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.sharing.InvitationMessage;
|
||||
import org.briarproject.api.sync.ClientId;
|
||||
@@ -12,38 +12,51 @@ import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface GroupInvitationManager extends MessageTracker {
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** The unique ID of the private group invitation client. */
|
||||
@NotNullByDefault
|
||||
public interface GroupInvitationManager {
|
||||
|
||||
/**
|
||||
* The unique ID of the private group invitation client.
|
||||
*/
|
||||
ClientId CLIENT_ID =
|
||||
new ClientId("org.briarproject.briar.privategroup.invitation");
|
||||
|
||||
/**
|
||||
* Sends an invitation to share the given forum with the given contact
|
||||
* and sends an optional message along with it.
|
||||
* Sends an invitation to share the given private group with the given
|
||||
* contact, including an optional message.
|
||||
*/
|
||||
void sendInvitation(GroupId groupId, ContactId contactId,
|
||||
String message) throws DbException;
|
||||
void sendInvitation(GroupId g, ContactId c, @Nullable String message,
|
||||
long timestamp, byte[] signature) throws DbException;
|
||||
|
||||
/**
|
||||
* Responds to a pending private group invitation
|
||||
* Responds to a pending private group invitation from the given contact.
|
||||
*/
|
||||
void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
|
||||
void respondToInvitation(ContactId c, PrivateGroup g, boolean accept)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Responds to a pending private group invitation
|
||||
* Responds to a pending private group invitation from the given contact.
|
||||
*/
|
||||
void respondToInvitation(SessionId id, boolean accept) throws DbException;
|
||||
void respondToInvitation(ContactId c, SessionId s, boolean accept)
|
||||
throws DbException;
|
||||
|
||||
/**
|
||||
* Returns all private group invitation messages related to the contact
|
||||
* identified by contactId.
|
||||
* Returns all private group invitation messages related to the given
|
||||
* contact.
|
||||
*/
|
||||
Collection<InvitationMessage> getInvitationMessages(
|
||||
ContactId contactId) throws DbException;
|
||||
Collection<InvitationMessage> getInvitationMessages(ContactId c)
|
||||
throws DbException;
|
||||
|
||||
/** Returns all private groups to which the user has been invited. */
|
||||
/**
|
||||
* Returns all private groups to which the user has been invited.
|
||||
*/
|
||||
Collection<GroupInvitationItem> getInvitations() throws DbException;
|
||||
|
||||
/**
|
||||
* Returns true if the given contact can be invited to the given private
|
||||
* group.
|
||||
*/
|
||||
boolean isInvitationAllowed(Contact c, GroupId g) throws DbException;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import org.briarproject.api.sharing.InvitationRequest;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
@@ -19,8 +19,8 @@ public class GroupInvitationRequest extends InvitationRequest {
|
||||
private final Author creator;
|
||||
|
||||
public GroupInvitationRequest(MessageId id, SessionId sessionId,
|
||||
GroupId groupId, Author creator, ContactId contactId,
|
||||
String groupName, String message, boolean available, long time,
|
||||
GroupId groupId, ContactId contactId, @Nullable String message,
|
||||
String groupName, Author creator, boolean available, long time,
|
||||
boolean local, boolean sent, boolean seen, boolean read) {
|
||||
super(id, sessionId, groupId, contactId, message, available, time,
|
||||
local, sent, seen, read);
|
||||
|
||||
@@ -2,39 +2,21 @@ package org.briarproject.api.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sharing.InvitationResponse;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.annotation.concurrent.ThreadSafe;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
public class GroupInvitationResponse extends InvitationResponse {
|
||||
|
||||
private final String groupName;
|
||||
private final Author creator;
|
||||
|
||||
public GroupInvitationResponse(MessageId id, SessionId sessionId,
|
||||
GroupId groupId, String groupName, Author creator,
|
||||
ContactId contactId, boolean accept, long time, boolean local,
|
||||
boolean sent, boolean seen, boolean read) {
|
||||
GroupId groupId, ContactId contactId, boolean accept, long time,
|
||||
boolean local, boolean sent, boolean seen, boolean read) {
|
||||
super(id, sessionId, groupId, contactId, accept, time, local, sent,
|
||||
seen, read);
|
||||
this.groupName = groupName;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
public Author getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.briarproject;
|
||||
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.blogs.BlogsModule;
|
||||
import org.briarproject.contact.ContactModule;
|
||||
import org.briarproject.crypto.CryptoModule;
|
||||
@@ -14,6 +12,7 @@ import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
import org.briarproject.plugins.PluginsModule;
|
||||
import org.briarproject.privategroup.PrivateGroupModule;
|
||||
import org.briarproject.privategroup.invitation.GroupInvitationModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.sharing.SharingModule;
|
||||
import org.briarproject.sync.SyncModule;
|
||||
@@ -32,6 +31,8 @@ public interface CoreEagerSingletons {
|
||||
|
||||
void inject(ForumModule.EagerSingletons init);
|
||||
|
||||
void inject(GroupInvitationModule.EagerSingletons init);
|
||||
|
||||
void inject(IdentityModule.EagerSingletons init);
|
||||
|
||||
void inject(IntroductionModule.EagerSingletons init);
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.briarproject.lifecycle.LifecycleModule;
|
||||
import org.briarproject.messaging.MessagingModule;
|
||||
import org.briarproject.plugins.PluginsModule;
|
||||
import org.briarproject.privategroup.PrivateGroupModule;
|
||||
import org.briarproject.privategroup.invitation.GroupInvitationModule;
|
||||
import org.briarproject.properties.PropertiesModule;
|
||||
import org.briarproject.reliability.ReliabilityModule;
|
||||
import org.briarproject.reporting.ReportingModule;
|
||||
@@ -40,6 +41,7 @@ import dagger.Module;
|
||||
DatabaseExecutorModule.class,
|
||||
EventModule.class,
|
||||
ForumModule.class,
|
||||
GroupInvitationModule.class,
|
||||
IdentityModule.class,
|
||||
IntroductionModule.class,
|
||||
InvitationModule.class,
|
||||
@@ -67,6 +69,7 @@ public class CoreModule {
|
||||
c.inject(new CryptoModule.EagerSingletons());
|
||||
c.inject(new DatabaseExecutorModule.EagerSingletons());
|
||||
c.inject(new ForumModule.EagerSingletons());
|
||||
c.inject(new GroupInvitationModule.EagerSingletons());
|
||||
c.inject(new IdentityModule.EagerSingletons());
|
||||
c.inject(new LifecycleModule.EagerSingletons());
|
||||
c.inject(new MessagingModule.EagerSingletons());
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.briarproject.privategroup;
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.BdfMessageContext;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.ContactGroupFactory;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
@@ -12,6 +11,7 @@ import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.privategroup.MessageType;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.InvalidMessageException;
|
||||
import org.briarproject.api.sync.Message;
|
||||
@@ -29,7 +29,6 @@ import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH
|
||||
import static org.briarproject.api.privategroup.MessageType.JOIN;
|
||||
import static org.briarproject.api.privategroup.MessageType.POST;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
|
||||
import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_ID;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_NAME;
|
||||
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
|
||||
@@ -41,18 +40,18 @@ import static org.briarproject.privategroup.Constants.KEY_TYPE;
|
||||
|
||||
class GroupMessageValidator extends BdfMessageValidator {
|
||||
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final PrivateGroupFactory groupFactory;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
private final AuthorFactory authorFactory;
|
||||
private final GroupInvitationFactory groupInvitationFactory;
|
||||
|
||||
GroupMessageValidator(ContactGroupFactory contactGroupFactory,
|
||||
PrivateGroupFactory groupFactory,
|
||||
GroupMessageValidator(PrivateGroupFactory privateGroupFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock, AuthorFactory authorFactory) {
|
||||
Clock clock, AuthorFactory authorFactory,
|
||||
GroupInvitationFactory groupInvitationFactory) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.groupFactory = groupFactory;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.authorFactory = authorFactory;
|
||||
this.groupInvitationFactory = groupInvitationFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -96,15 +95,16 @@ class GroupMessageValidator extends BdfMessageValidator {
|
||||
|
||||
// The content is a BDF list with five elements
|
||||
checkSize(body, 5);
|
||||
PrivateGroup pg = groupFactory.parsePrivateGroup(g);
|
||||
PrivateGroup pg = privateGroupFactory.parsePrivateGroup(g);
|
||||
|
||||
// invite is null if the member is the creator of the private group
|
||||
Author creator = pg.getAuthor();
|
||||
BdfList invite = body.getOptionalList(3);
|
||||
if (invite == null) {
|
||||
if (!member.equals(pg.getAuthor()))
|
||||
if (!member.equals(creator))
|
||||
throw new InvalidMessageException();
|
||||
} else {
|
||||
if (member.equals(pg.getAuthor()))
|
||||
if (member.equals(creator))
|
||||
throw new InvalidMessageException();
|
||||
|
||||
// Otherwise invite is a list with two elements
|
||||
@@ -120,21 +120,13 @@ class GroupMessageValidator extends BdfMessageValidator {
|
||||
byte[] creatorSignature = invite.getRaw(1);
|
||||
checkLength(creatorSignature, 1, MAX_SIGNATURE_LENGTH);
|
||||
|
||||
// derive invitation group
|
||||
Group invitationGroup = contactGroupFactory
|
||||
.createContactGroup(CLIENT_ID, pg.getAuthor().getId(),
|
||||
member.getId());
|
||||
|
||||
// signature with the creator's private key
|
||||
// over a list with four elements:
|
||||
// invite_type (int), invite_timestamp (int),
|
||||
// invitation_group_id (raw), and private_group_id (raw)
|
||||
BdfList signed =
|
||||
BdfList.of(0, inviteTimestamp, invitationGroup.getId(),
|
||||
g.getId());
|
||||
// the invite token is signed by the creator of the private group
|
||||
BdfList token = groupInvitationFactory
|
||||
.createInviteToken(creator.getId(), member.getId(),
|
||||
pg.getId(), inviteTimestamp);
|
||||
try {
|
||||
clientHelper.verifySignature(creatorSignature,
|
||||
pg.getAuthor().getPublicKey(), signed);
|
||||
creator.getPublicKey(), token);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new InvalidMessageException(e);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.briarproject.privategroup;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
@@ -86,6 +87,17 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
public void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
|
||||
throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
addPrivateGroup(txn, group, joinMsg);
|
||||
db.commitTransaction(txn);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addPrivateGroup(Transaction txn, PrivateGroup group,
|
||||
GroupMessage joinMsg) throws DbException {
|
||||
try {
|
||||
db.addGroup(txn, group.getGroup());
|
||||
BdfDictionary meta = BdfDictionary.of(
|
||||
@@ -94,11 +106,8 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
);
|
||||
clientHelper.mergeGroupMetadata(txn, group.getId(), meta);
|
||||
joinPrivateGroup(txn, joinMsg);
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,7 +294,9 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
try {
|
||||
// type(0), member_name(1), member_public_key(2), parent_id(3),
|
||||
// previous_message_id(4), content(5), signature(6)
|
||||
return clientHelper.getMessageAsList(m).getString(5);
|
||||
BdfList body = clientHelper.getMessageAsList(m);
|
||||
if (body == null) throw new DbException();
|
||||
return body.getString(5);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
@@ -370,10 +381,10 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
Status status = identityManager.getAuthorStatus(txn, a.getId());
|
||||
boolean shared = false;
|
||||
if (status == VERIFIED || status == UNVERIFIED) {
|
||||
Collection<ContactId> contacts =
|
||||
db.getContacts(txn, a.getId());
|
||||
Collection<Contact> contacts =
|
||||
db.getContactsByAuthorId(txn, a.getId());
|
||||
if (contacts.size() != 1) throw new DbException();
|
||||
ContactId c = contacts.iterator().next();
|
||||
ContactId c = contacts.iterator().next().getId();
|
||||
shared = db.isVisibleToContact(txn, c, g);
|
||||
}
|
||||
members.add(new GroupMember(a, status, shared));
|
||||
@@ -489,6 +500,9 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
|
||||
new BdfEntry(KEY_MEMBER_PUBLIC_KEY, a.getPublicKey())
|
||||
));
|
||||
clientHelper.mergeGroupMetadata(txn, g, meta);
|
||||
for (PrivateGroupHook hook : hooks) {
|
||||
hook.addingMember(txn, g, a);
|
||||
}
|
||||
}
|
||||
|
||||
private Author getAuthor(BdfDictionary meta) throws FormatException {
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
package org.briarproject.privategroup;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.ContactGroupFactory;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.messaging.ConversationManager;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||
import org.briarproject.api.sync.ValidationManager;
|
||||
import org.briarproject.api.system.Clock;
|
||||
import org.briarproject.privategroup.invitation.GroupInvitationManagerImpl;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
@@ -21,6 +16,8 @@ import javax.inject.Singleton;
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.api.privategroup.PrivateGroupManager.CLIENT_ID;
|
||||
|
||||
@Module
|
||||
public class PrivateGroupModule {
|
||||
|
||||
@@ -28,19 +25,15 @@ public class PrivateGroupModule {
|
||||
@Inject
|
||||
GroupMessageValidator groupMessageValidator;
|
||||
@Inject
|
||||
GroupInvitationManager groupInvitationManager;
|
||||
PrivateGroupManager groupManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
PrivateGroupManager provideForumManager(
|
||||
PrivateGroupManager provideGroupManager(
|
||||
PrivateGroupManagerImpl groupManager,
|
||||
ValidationManager validationManager) {
|
||||
|
||||
validationManager
|
||||
.registerIncomingMessageHook(PrivateGroupManager.CLIENT_ID,
|
||||
groupManager);
|
||||
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID, groupManager);
|
||||
return groupManager;
|
||||
}
|
||||
|
||||
@@ -59,38 +52,16 @@ public class PrivateGroupModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
GroupMessageValidator provideGroupMessageValidator(
|
||||
ContactGroupFactory contactGroupFactory,
|
||||
PrivateGroupFactory groupFactory,
|
||||
ValidationManager validationManager, ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock,
|
||||
AuthorFactory authorFactory,
|
||||
GroupInvitationManager groupInvitationManager) {
|
||||
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock, AuthorFactory authorFactory,
|
||||
GroupInvitationFactory groupInvitationFactory,
|
||||
ValidationManager validationManager) {
|
||||
GroupMessageValidator validator = new GroupMessageValidator(
|
||||
contactGroupFactory, groupFactory, clientHelper,
|
||||
metadataEncoder, clock, authorFactory);
|
||||
validationManager.registerMessageValidator(
|
||||
PrivateGroupManager.CLIENT_ID, validator);
|
||||
|
||||
privateGroupFactory, clientHelper, metadataEncoder, clock,
|
||||
authorFactory, groupInvitationFactory);
|
||||
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||
return validator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
GroupInvitationManager provideGroupInvitationManager(
|
||||
LifecycleManager lifecycleManager, ContactManager contactManager,
|
||||
GroupInvitationManagerImpl groupInvitationManager,
|
||||
ConversationManager conversationManager,
|
||||
ValidationManager validationManager) {
|
||||
|
||||
validationManager.registerIncomingMessageHook(
|
||||
GroupInvitationManager.CLIENT_ID, groupInvitationManager);
|
||||
lifecycleManager.registerClient(groupInvitationManager);
|
||||
contactManager.registerAddContactHook(groupInvitationManager);
|
||||
contactManager.registerRemoveContactHook(groupInvitationManager);
|
||||
conversationManager.registerConversationClient(groupInvitationManager);
|
||||
|
||||
return groupInvitationManager;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class AbortMessage extends GroupInvitationMessage {
|
||||
|
||||
AbortMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp) {
|
||||
super(id, contactGroupId, privateGroupId, timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
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.identity.IdentityManager;
|
||||
import org.briarproject.api.identity.LocalAuthor;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessage;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.sync.Group;
|
||||
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.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
abstract class AbstractProtocolEngine<S extends Session>
|
||||
implements ProtocolEngine<S> {
|
||||
|
||||
protected final DatabaseComponent db;
|
||||
protected final ClientHelper clientHelper;
|
||||
protected final PrivateGroupManager privateGroupManager;
|
||||
protected final PrivateGroupFactory privateGroupFactory;
|
||||
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
private final IdentityManager identityManager;
|
||||
private final MessageParser messageParser;
|
||||
private final MessageEncoder messageEncoder;
|
||||
private final Clock clock;
|
||||
|
||||
AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
IdentityManager identityManager, MessageParser messageParser,
|
||||
MessageEncoder messageEncoder, Clock clock) {
|
||||
this.db = db;
|
||||
this.clientHelper = clientHelper;
|
||||
this.privateGroupManager = privateGroupManager;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
this.identityManager = identityManager;
|
||||
this.messageParser = messageParser;
|
||||
this.messageEncoder = messageEncoder;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
ContactId getContactId(Transaction txn, GroupId contactGroupId)
|
||||
throws DbException, FormatException {
|
||||
BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn,
|
||||
contactGroupId);
|
||||
return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue());
|
||||
}
|
||||
|
||||
boolean isSubscribedPrivateGroup(Transaction txn, GroupId g)
|
||||
throws DbException {
|
||||
if (!db.containsGroup(txn, g)) return false;
|
||||
Group group = db.getGroup(txn, g);
|
||||
return group.getClientId().equals(PrivateGroupManager.CLIENT_ID);
|
||||
}
|
||||
|
||||
boolean isValidDependency(S session, @Nullable MessageId dependency) {
|
||||
MessageId expected = session.getLastRemoteMessageId();
|
||||
if (dependency == null) return expected == null;
|
||||
return dependency.equals(expected);
|
||||
}
|
||||
|
||||
void syncPrivateGroupWithContact(Transaction txn, S session, boolean sync)
|
||||
throws DbException, FormatException {
|
||||
ContactId contactId = getContactId(txn, session.getContactGroupId());
|
||||
db.setVisibleToContact(txn, contactId, session.getPrivateGroupId(),
|
||||
sync);
|
||||
}
|
||||
|
||||
Message sendInviteMessage(Transaction txn, S session,
|
||||
@Nullable String message, long timestamp, byte[] signature)
|
||||
throws DbException {
|
||||
Group g = db.getGroup(txn, session.getPrivateGroupId());
|
||||
PrivateGroup privateGroup;
|
||||
try {
|
||||
privateGroup = privateGroupFactory.parsePrivateGroup(g);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group descriptor
|
||||
}
|
||||
Message m = messageEncoder.encodeInviteMessage(
|
||||
session.getContactGroupId(), privateGroup.getId(),
|
||||
timestamp, privateGroup.getName(), privateGroup.getAuthor(),
|
||||
privateGroup.getSalt(), message, signature);
|
||||
sendMessage(txn, m, INVITE, privateGroup.getId(), true);
|
||||
return m;
|
||||
}
|
||||
|
||||
Message sendJoinMessage(Transaction txn, S session, boolean visibleInUi)
|
||||
throws DbException {
|
||||
Message m = messageEncoder.encodeJoinMessage(
|
||||
session.getContactGroupId(), session.getPrivateGroupId(),
|
||||
getLocalTimestamp(session), session.getLastLocalMessageId());
|
||||
sendMessage(txn, m, JOIN, session.getPrivateGroupId(), visibleInUi);
|
||||
return m;
|
||||
}
|
||||
|
||||
Message sendLeaveMessage(Transaction txn, S session, boolean visibleInUi)
|
||||
throws DbException {
|
||||
Message m = messageEncoder.encodeLeaveMessage(
|
||||
session.getContactGroupId(), session.getPrivateGroupId(),
|
||||
getLocalTimestamp(session), session.getLastLocalMessageId());
|
||||
sendMessage(txn, m, LEAVE, session.getPrivateGroupId(), visibleInUi);
|
||||
return m;
|
||||
}
|
||||
|
||||
Message sendAbortMessage(Transaction txn, S session) throws DbException {
|
||||
Message m = messageEncoder.encodeAbortMessage(
|
||||
session.getContactGroupId(), session.getPrivateGroupId(),
|
||||
getLocalTimestamp(session));
|
||||
sendMessage(txn, m, ABORT, session.getPrivateGroupId(), false);
|
||||
return m;
|
||||
}
|
||||
|
||||
void markMessageVisibleInUi(Transaction txn, MessageId m, boolean visible)
|
||||
throws DbException {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
messageEncoder.setVisibleInUi(meta, visible);
|
||||
try {
|
||||
clientHelper.mergeMessageMetadata(txn, m, meta);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
void markMessageAvailableToAnswer(Transaction txn, MessageId m,
|
||||
boolean available) throws DbException {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
messageEncoder.setAvailableToAnswer(meta, available);
|
||||
try {
|
||||
clientHelper.mergeMessageMetadata(txn, m, meta);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
void markInvitesUnavailableToAnswer(Transaction txn, S session)
|
||||
throws DbException, FormatException {
|
||||
GroupId privateGroupId = session.getPrivateGroupId();
|
||||
BdfDictionary query =
|
||||
messageParser.getInvitesAvailableToAnswerQuery(privateGroupId);
|
||||
Map<MessageId, BdfDictionary> results =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn,
|
||||
session.getContactGroupId(), query);
|
||||
for (MessageId m : results.keySet())
|
||||
markMessageAvailableToAnswer(txn, m, false);
|
||||
}
|
||||
|
||||
void subscribeToPrivateGroup(Transaction txn, MessageId inviteId)
|
||||
throws DbException, FormatException {
|
||||
InviteMessage invite = getInviteMessage(txn, inviteId);
|
||||
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||
invite.getGroupName(), invite.getCreator(), invite.getSalt());
|
||||
long timestamp =
|
||||
Math.max(clock.currentTimeMillis(), invite.getTimestamp() + 1);
|
||||
// TODO: Create the join message on the crypto executor
|
||||
LocalAuthor member = identityManager.getLocalAuthor(txn);
|
||||
GroupMessage joinMessage = groupMessageFactory.createJoinMessage(
|
||||
privateGroup.getId(), timestamp, member, invite.getTimestamp(),
|
||||
invite.getSignature());
|
||||
privateGroupManager.addPrivateGroup(txn, privateGroup, joinMessage);
|
||||
}
|
||||
|
||||
long getLocalTimestamp(S session) {
|
||||
return Math.max(clock.currentTimeMillis(),
|
||||
Math.max(session.getLocalTimestamp(),
|
||||
session.getInviteTimestamp()) + 1);
|
||||
}
|
||||
|
||||
private InviteMessage getInviteMessage(Transaction txn, MessageId m)
|
||||
throws DbException, FormatException {
|
||||
Message message = clientHelper.getMessage(txn, m);
|
||||
if (message == null) throw new DbException();
|
||||
BdfList body = clientHelper.toList(message);
|
||||
return messageParser.parseInviteMessage(message, body);
|
||||
}
|
||||
|
||||
private void sendMessage(Transaction txn, Message m, MessageType type,
|
||||
GroupId privateGroupId, boolean visibleInConversation)
|
||||
throws DbException {
|
||||
BdfDictionary meta = messageEncoder.encodeMetadata(type, privateGroupId,
|
||||
m.getTimestamp(), true, true, visibleInConversation, false);
|
||||
try {
|
||||
clientHelper.addLocalMessage(txn, m, meta, true);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.ProtocolStateException;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.event.GroupInvitationResponseReceivedEvent;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationResponse;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.DISSOLVED;
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.ERROR;
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.INVITED;
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.INVITEE_JOINED;
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.INVITEE_LEFT;
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.START;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> {
|
||||
|
||||
CreatorProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
IdentityManager identityManager, MessageParser messageParser,
|
||||
MessageEncoder messageEncoder, Clock clock) {
|
||||
super(db, clientHelper, privateGroupManager, privateGroupFactory,
|
||||
groupMessageFactory, identityManager, messageParser,
|
||||
messageEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onInviteAction(Transaction txn, CreatorSession s,
|
||||
@Nullable String message, long timestamp, byte[] signature)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
return onLocalInvite(txn, s, message, timestamp, signature);
|
||||
case INVITED:
|
||||
case INVITEE_JOINED:
|
||||
case INVITEE_LEFT:
|
||||
case DISSOLVED:
|
||||
case ERROR:
|
||||
throw new ProtocolStateException(); // Invalid in these states
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onJoinAction(Transaction txn, CreatorSession s)
|
||||
throws DbException {
|
||||
throw new UnsupportedOperationException(); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onLeaveAction(Transaction txn, CreatorSession s)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case DISSOLVED:
|
||||
case ERROR:
|
||||
return s; // Ignored in these states
|
||||
case INVITED:
|
||||
case INVITEE_JOINED:
|
||||
case INVITEE_LEFT:
|
||||
return onLocalLeave(txn, s);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onMemberAddedAction(Transaction txn, CreatorSession s)
|
||||
throws DbException {
|
||||
return s; // Ignored in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onInviteMessage(Transaction txn, CreatorSession s,
|
||||
InviteMessage m) throws DbException, FormatException {
|
||||
return abort(txn, s); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onJoinMessage(Transaction txn, CreatorSession s,
|
||||
JoinMessage m) throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case INVITEE_JOINED:
|
||||
case INVITEE_LEFT:
|
||||
return abort(txn, s); // Invalid in these states
|
||||
case INVITED:
|
||||
return onRemoteAccept(txn, s, m);
|
||||
case DISSOLVED:
|
||||
case ERROR:
|
||||
return s; // Ignored in these states
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onLeaveMessage(Transaction txn, CreatorSession s,
|
||||
LeaveMessage m) throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case INVITEE_LEFT:
|
||||
return abort(txn, s); // Invalid in these states
|
||||
case INVITED:
|
||||
return onRemoteDecline(txn, s, m);
|
||||
case INVITEE_JOINED:
|
||||
return onRemoteLeave(txn, s, m);
|
||||
case DISSOLVED:
|
||||
case ERROR:
|
||||
return s; // Ignored in these states
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession onAbortMessage(Transaction txn, CreatorSession s,
|
||||
AbortMessage m) throws DbException, FormatException {
|
||||
return abort(txn, s);
|
||||
}
|
||||
|
||||
private CreatorSession onLocalInvite(Transaction txn, CreatorSession s,
|
||||
@Nullable String message, long timestamp, byte[] signature)
|
||||
throws DbException {
|
||||
// Send an INVITE message
|
||||
Message sent = sendInviteMessage(txn, s, message, timestamp, signature);
|
||||
long localTimestamp = Math.max(timestamp, getLocalTimestamp(s));
|
||||
// Move to the INVITED state
|
||||
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), localTimestamp,
|
||||
timestamp, INVITED);
|
||||
}
|
||||
|
||||
private CreatorSession onLocalLeave(Transaction txn, CreatorSession s)
|
||||
throws DbException {
|
||||
try {
|
||||
// Stop syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group metadata
|
||||
}
|
||||
// Send a LEAVE message
|
||||
Message sent = sendLeaveMessage(txn, s, false);
|
||||
// Move to the DISSOLVED state
|
||||
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
s.getInviteTimestamp(), DISSOLVED);
|
||||
}
|
||||
|
||||
private CreatorSession onRemoteAccept(Transaction txn, CreatorSession s,
|
||||
JoinMessage m) throws DbException, FormatException {
|
||||
// The timestamp must be higher than the last invite message
|
||||
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Mark the response visible in the UI
|
||||
markMessageVisibleInUi(txn, m.getId(), true);
|
||||
// Start syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, true);
|
||||
// Broadcast an event
|
||||
ContactId contactId = getContactId(txn, m.getContactGroupId());
|
||||
txn.attach(new GroupInvitationResponseReceivedEvent(contactId,
|
||||
createInvitationResponse(m, contactId, true)));
|
||||
// Move to the INVITEE_JOINED state
|
||||
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
s.getInviteTimestamp(), INVITEE_JOINED);
|
||||
}
|
||||
|
||||
private CreatorSession onRemoteDecline(Transaction txn, CreatorSession s,
|
||||
LeaveMessage m) throws DbException, FormatException {
|
||||
// The timestamp must be higher than the last invite message
|
||||
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Mark the response visible in the UI
|
||||
markMessageVisibleInUi(txn, m.getId(), true);
|
||||
// Broadcast an event
|
||||
ContactId contactId = getContactId(txn, m.getContactGroupId());
|
||||
txn.attach(new GroupInvitationResponseReceivedEvent(contactId,
|
||||
createInvitationResponse(m, contactId, false)));
|
||||
// Move to the START state
|
||||
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
s.getInviteTimestamp(), START);
|
||||
}
|
||||
|
||||
private CreatorSession onRemoteLeave(Transaction txn, CreatorSession s,
|
||||
LeaveMessage m) throws DbException, FormatException {
|
||||
// The timestamp must be higher than the last invite message
|
||||
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Stop syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
// Move to the INVITEE_LEFT state
|
||||
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
s.getInviteTimestamp(), INVITEE_LEFT);
|
||||
}
|
||||
|
||||
private CreatorSession abort(Transaction txn, CreatorSession s)
|
||||
throws DbException, FormatException {
|
||||
// If the session has already been aborted, do nothing
|
||||
if (s.getState() == ERROR) return s;
|
||||
// If we subscribe, stop syncing the private group with the contact
|
||||
if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
// Send an ABORT message
|
||||
Message sent = sendAbortMessage(txn, s);
|
||||
// Move to the ERROR state
|
||||
return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
s.getInviteTimestamp(), ERROR);
|
||||
}
|
||||
|
||||
private GroupInvitationResponse createInvitationResponse(
|
||||
GroupInvitationMessage m, ContactId c, boolean accept) {
|
||||
SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes());
|
||||
return new GroupInvitationResponse(m.getId(), sessionId,
|
||||
m.getContactGroupId(), c, accept, m.getTimestamp(), false,
|
||||
false, true, false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.START;
|
||||
import static org.briarproject.privategroup.invitation.Role.CREATOR;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class CreatorSession extends Session<CreatorState> {
|
||||
|
||||
private final CreatorState state;
|
||||
|
||||
CreatorSession(GroupId contactGroupId, GroupId privateGroupId,
|
||||
@Nullable MessageId lastLocalMessageId,
|
||||
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||
long inviteTimestamp, CreatorState state) {
|
||||
super(contactGroupId, privateGroupId, lastLocalMessageId,
|
||||
lastRemoteMessageId, localTimestamp, inviteTimestamp);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
CreatorSession(GroupId contactGroupId, GroupId privateGroupId) {
|
||||
this(contactGroupId, privateGroupId, null, null, 0, 0, START);
|
||||
}
|
||||
|
||||
@Override
|
||||
Role getRole() {
|
||||
return CREATOR;
|
||||
}
|
||||
|
||||
@Override
|
||||
CreatorState getState() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
|
||||
enum CreatorState implements State {
|
||||
|
||||
START(0), INVITED(1), INVITEE_JOINED(2), INVITEE_LEFT(3), DISSOLVED(4),
|
||||
ERROR(5);
|
||||
|
||||
private final int value;
|
||||
|
||||
CreatorState(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static CreatorState fromValue(int value) throws FormatException {
|
||||
for (CreatorState s : values()) if (s.value == value) return s;
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
interface GroupInvitationConstants {
|
||||
|
||||
// Group metadata keys
|
||||
String GROUP_KEY_CONTACT_ID = "contactId";
|
||||
|
||||
// Message metadata keys
|
||||
String MSG_KEY_MESSAGE_TYPE = "messageType";
|
||||
String MSG_KEY_PRIVATE_GROUP_ID = "privateGroupId";
|
||||
String MSG_KEY_TIMESTAMP = "timestamp";
|
||||
String MSG_KEY_LOCAL = "local";
|
||||
String MSG_KEY_VISIBLE_IN_UI = "visibleInUi";
|
||||
String MSG_KEY_AVAILABLE_TO_ANSWER = "availableToAnswer";
|
||||
|
||||
// Session keys
|
||||
String SESSION_KEY_SESSION_ID = "sessionId";
|
||||
String SESSION_KEY_PRIVATE_GROUP_ID = "privateGroupId";
|
||||
String SESSION_KEY_LAST_LOCAL_MESSAGE_ID = "lastLocalMessageId";
|
||||
String SESSION_KEY_LAST_REMOTE_MESSAGE_ID = "lastRemoteMessageId";
|
||||
String SESSION_KEY_LOCAL_TIMESTAMP = "localTimestamp";
|
||||
String SESSION_KEY_INVITE_TIMESTAMP = "inviteTimestamp";
|
||||
String SESSION_KEY_ROLE = "role";
|
||||
String SESSION_KEY_STATE = "state";
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.ContactGroupFactory;
|
||||
import org.briarproject.api.contact.Contact;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.identity.AuthorId;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||
import org.briarproject.api.sync.Group;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
|
||||
|
||||
class GroupInvitationFactoryImpl implements GroupInvitationFactory {
|
||||
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final ClientHelper clientHelper;
|
||||
|
||||
@Inject
|
||||
GroupInvitationFactoryImpl(ContactGroupFactory contactGroupFactory,
|
||||
ClientHelper clientHelper) {
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.clientHelper = clientHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] signInvitation(Contact c, GroupId privateGroupId,
|
||||
long timestamp, byte[] privateKey) {
|
||||
AuthorId creatorId = c.getLocalAuthorId();
|
||||
AuthorId memberId = c.getAuthor().getId();
|
||||
BdfList token = createInviteToken(creatorId, memberId, privateGroupId,
|
||||
timestamp);
|
||||
try {
|
||||
return clientHelper.sign(token, privateKey);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfList createInviteToken(AuthorId creatorId, AuthorId memberId,
|
||||
GroupId privateGroupId, long timestamp) {
|
||||
Group contactGroup = contactGroupFactory.createContactGroup(CLIENT_ID,
|
||||
creatorId, memberId);
|
||||
return BdfList.of(
|
||||
0, // TODO: Replace with a namespaced string
|
||||
timestamp,
|
||||
contactGroup.getId(),
|
||||
privateGroupId
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,44 +7,89 @@ import org.briarproject.api.clients.ContactGroupFactory;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
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.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
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.Transaction;
|
||||
import org.briarproject.api.messaging.ConversationManager;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager.PrivateGroupHook;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationItem;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationResponse;
|
||||
import org.briarproject.api.sharing.InvitationMessage;
|
||||
import org.briarproject.api.sync.Group;
|
||||
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.clients.ConversationClientImpl;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.api.privategroup.invitation.GroupInvitationConstants.CONTACT_ID;
|
||||
import static org.briarproject.privategroup.invitation.CreatorState.START;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||
import static org.briarproject.privategroup.invitation.Role.CREATOR;
|
||||
import static org.briarproject.privategroup.invitation.Role.INVITEE;
|
||||
import static org.briarproject.privategroup.invitation.Role.PEER;
|
||||
|
||||
public class GroupInvitationManagerImpl extends ConversationClientImpl
|
||||
implements GroupInvitationManager, Client,
|
||||
ContactManager.AddContactHook, ContactManager.RemoveContactHook,
|
||||
ConversationManager.ConversationClient {
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class GroupInvitationManagerImpl extends ConversationClientImpl
|
||||
implements GroupInvitationManager, Client, AddContactHook,
|
||||
RemoveContactHook, PrivateGroupHook {
|
||||
|
||||
private final ContactGroupFactory contactGroupFactory;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
private final PrivateGroupManager privateGroupManager;
|
||||
private final MessageParser messageParser;
|
||||
private final SessionParser sessionParser;
|
||||
private final SessionEncoder sessionEncoder;
|
||||
private final ProtocolEngine<CreatorSession> creatorEngine;
|
||||
private final ProtocolEngine<InviteeSession> inviteeEngine;
|
||||
private final ProtocolEngine<PeerSession> peerEngine;
|
||||
private final Group localGroup;
|
||||
|
||||
@Inject
|
||||
protected GroupInvitationManagerImpl(DatabaseComponent db,
|
||||
ClientHelper clientHelper, MetadataParser metadataParser,
|
||||
ContactGroupFactory contactGroupFactory) {
|
||||
ContactGroupFactory contactGroupFactory,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
MessageParser messageParser, SessionParser sessionParser,
|
||||
SessionEncoder sessionEncoder,
|
||||
ProtocolEngineFactory engineFactory) {
|
||||
super(db, clientHelper, metadataParser);
|
||||
this.contactGroupFactory = contactGroupFactory;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.privateGroupManager = privateGroupManager;
|
||||
this.messageParser = messageParser;
|
||||
this.sessionParser = sessionParser;
|
||||
this.sessionEncoder = sessionEncoder;
|
||||
creatorEngine = engineFactory.createCreatorEngine();
|
||||
inviteeEngine = engineFactory.createInviteeEngine();
|
||||
peerEngine = engineFactory.createPeerEngine();
|
||||
localGroup = contactGroupFactory.createLocalGroup(CLIENT_ID);
|
||||
}
|
||||
|
||||
@@ -57,26 +102,31 @@ public class GroupInvitationManagerImpl extends ConversationClientImpl
|
||||
|
||||
@Override
|
||||
public void addingContact(Transaction txn, Contact c) throws DbException {
|
||||
// Create a group to share with the contact
|
||||
Group g = getContactGroup(c);
|
||||
// Return if we've already set things up for this contact
|
||||
if (db.containsGroup(txn, g.getId())) return;
|
||||
// Store the group and share it with the contact
|
||||
db.addGroup(txn, g);
|
||||
db.setVisibleToContact(txn, c.getId(), g.getId(), true);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(GROUP_KEY_CONTACT_ID, c.getId().getInt());
|
||||
try {
|
||||
// Create a group to share with the contact
|
||||
Group g = getContactGroup(c);
|
||||
// Return if we've already set things up for this contact
|
||||
if (db.containsGroup(txn, g.getId())) return;
|
||||
// Store the group and share it with the contact
|
||||
db.addGroup(txn, g);
|
||||
db.setVisibleToContact(txn, c.getId(), g.getId(), true);
|
||||
// Attach the contact ID to the group
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(CONTACT_ID, c.getId().getInt());
|
||||
clientHelper.mergeGroupMetadata(txn, g.getId(), meta);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
// If the contact belongs to any private groups, create a peer session
|
||||
for (Group pg : db.getGroups(txn, PrivateGroupManager.CLIENT_ID)) {
|
||||
if (privateGroupManager.isMember(txn, pg.getId(), c.getAuthor()))
|
||||
addingMember(txn, pg.getId(), c);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingContact(Transaction txn, Contact c) throws DbException {
|
||||
// remove the contact group (all messages will be removed with it)
|
||||
// Remove the contact group (all messages will be removed with it)
|
||||
db.removeGroup(txn, getContactGroup(c));
|
||||
}
|
||||
|
||||
@@ -87,43 +137,416 @@ public class GroupInvitationManagerImpl extends ConversationClientImpl
|
||||
|
||||
@Override
|
||||
protected boolean incomingMessage(Transaction txn, Message m, BdfList body,
|
||||
BdfDictionary meta) throws DbException, FormatException {
|
||||
BdfDictionary bdfMeta) throws DbException, FormatException {
|
||||
// Parse the metadata
|
||||
MessageMetadata meta = messageParser.parseMetadata(bdfMeta);
|
||||
// Look up the session, if there is one
|
||||
SessionId sessionId = getSessionId(meta.getPrivateGroupId());
|
||||
StoredSession ss = getSession(txn, m.getGroupId(), sessionId);
|
||||
// Handle the message
|
||||
Session session;
|
||||
MessageId storageId;
|
||||
if (ss == null) {
|
||||
session = handleFirstMessage(txn, m, body, meta);
|
||||
storageId = createStorageId(txn, m.getGroupId());
|
||||
} else {
|
||||
session = handleMessage(txn, m, body, meta, ss.bdfSession);
|
||||
storageId = ss.storageId;
|
||||
}
|
||||
// Store the updated session
|
||||
storeSession(txn, storageId, session);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendInvitation(GroupId groupId, ContactId contactId,
|
||||
String message) throws DbException {
|
||||
|
||||
private SessionId getSessionId(GroupId privateGroupId) {
|
||||
return new SessionId(privateGroupId.getBytes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void respondToInvitation(PrivateGroup g, Contact c, boolean accept)
|
||||
@Nullable
|
||||
private StoredSession getSession(Transaction txn, GroupId contactGroupId,
|
||||
SessionId sessionId) throws DbException, FormatException {
|
||||
BdfDictionary query = sessionParser.getSessionQuery(sessionId);
|
||||
Map<MessageId, BdfDictionary> results = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, contactGroupId, query);
|
||||
if (results.size() > 1) throw new DbException();
|
||||
if (results.isEmpty()) return null;
|
||||
return new StoredSession(results.keySet().iterator().next(),
|
||||
results.values().iterator().next());
|
||||
}
|
||||
|
||||
private Session handleFirstMessage(Transaction txn, Message m, BdfList body,
|
||||
MessageMetadata meta) throws DbException, FormatException {
|
||||
GroupId privateGroupId = meta.getPrivateGroupId();
|
||||
MessageType type = meta.getMessageType();
|
||||
if (type == INVITE) {
|
||||
InviteeSession session =
|
||||
new InviteeSession(m.getGroupId(), privateGroupId);
|
||||
return handleMessage(txn, m, body, type, session, inviteeEngine);
|
||||
} else if (type == JOIN) {
|
||||
PeerSession session =
|
||||
new PeerSession(m.getGroupId(), privateGroupId);
|
||||
return handleMessage(txn, m, body, type, session, peerEngine);
|
||||
} else {
|
||||
throw new FormatException(); // Invalid first message
|
||||
}
|
||||
}
|
||||
|
||||
private Session handleMessage(Transaction txn, Message m, BdfList body,
|
||||
MessageMetadata meta, BdfDictionary bdfSession)
|
||||
throws DbException, FormatException {
|
||||
MessageType type = meta.getMessageType();
|
||||
Role role = sessionParser.getRole(bdfSession);
|
||||
if (role == CREATOR) {
|
||||
CreatorSession session = sessionParser
|
||||
.parseCreatorSession(m.getGroupId(), bdfSession);
|
||||
return handleMessage(txn, m, body, type, session, creatorEngine);
|
||||
} else if (role == INVITEE) {
|
||||
InviteeSession session = sessionParser
|
||||
.parseInviteeSession(m.getGroupId(), bdfSession);
|
||||
return handleMessage(txn, m, body, type, session, inviteeEngine);
|
||||
} else if (role == PEER) {
|
||||
PeerSession session = sessionParser
|
||||
.parsePeerSession(m.getGroupId(), bdfSession);
|
||||
return handleMessage(txn, m, body, type, session, peerEngine);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private <S extends Session> S handleMessage(Transaction txn, Message m,
|
||||
BdfList body, MessageType type, S session, ProtocolEngine<S> engine)
|
||||
throws DbException, FormatException {
|
||||
if (type == INVITE) {
|
||||
InviteMessage invite = messageParser.parseInviteMessage(m, body);
|
||||
return engine.onInviteMessage(txn, session, invite);
|
||||
} else if (type == JOIN) {
|
||||
JoinMessage join = messageParser.parseJoinMessage(m, body);
|
||||
return engine.onJoinMessage(txn, session, join);
|
||||
} else if (type == LEAVE) {
|
||||
LeaveMessage leave = messageParser.parseLeaveMessage(m, body);
|
||||
return engine.onLeaveMessage(txn, session, leave);
|
||||
} else if (type == ABORT) {
|
||||
AbortMessage abort = messageParser.parseAbortMessage(m, body);
|
||||
return engine.onAbortMessage(txn, session, abort);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private MessageId createStorageId(Transaction txn, GroupId g)
|
||||
throws DbException {
|
||||
Message m = clientHelper.createMessageForStoringMetadata(g);
|
||||
db.addLocalMessage(txn, m, new Metadata(), false);
|
||||
return m.getId();
|
||||
}
|
||||
|
||||
private void storeSession(Transaction txn, MessageId storageId,
|
||||
Session session) throws DbException, FormatException {
|
||||
BdfDictionary d = sessionEncoder.encodeSession(session);
|
||||
clientHelper.mergeMessageMetadata(txn, storageId, d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void respondToInvitation(SessionId id, boolean accept)
|
||||
public void sendInvitation(GroupId privateGroupId, ContactId c,
|
||||
@Nullable String message, long timestamp, byte[] signature)
|
||||
throws DbException {
|
||||
|
||||
SessionId sessionId = getSessionId(privateGroupId);
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
// Look up the session, if there is one
|
||||
Contact contact = db.getContact(txn, c);
|
||||
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||
// Create or parse the session
|
||||
CreatorSession session;
|
||||
MessageId storageId;
|
||||
if (ss == null) {
|
||||
// This is the first invite - create a new session
|
||||
session = new CreatorSession(contactGroupId, privateGroupId);
|
||||
storageId = createStorageId(txn, contactGroupId);
|
||||
} else {
|
||||
// An earlier invite was declined, so we already have a session
|
||||
session = sessionParser
|
||||
.parseCreatorSession(contactGroupId, ss.bdfSession);
|
||||
storageId = ss.storageId;
|
||||
}
|
||||
// Handle the invite action
|
||||
session = creatorEngine.onInviteAction(txn, session, message,
|
||||
timestamp, signature);
|
||||
// Store the updated session
|
||||
storeSession(txn, storageId, session);
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<InvitationMessage> getInvitationMessages(
|
||||
ContactId contactId) throws DbException {
|
||||
Collection<InvitationMessage> invitations =
|
||||
new ArrayList<InvitationMessage>();
|
||||
public void respondToInvitation(ContactId c, PrivateGroup g, boolean accept)
|
||||
throws DbException {
|
||||
respondToInvitation(c, getSessionId(g.getId()), accept);
|
||||
}
|
||||
|
||||
return invitations;
|
||||
@Override
|
||||
public void respondToInvitation(ContactId c, SessionId sessionId,
|
||||
boolean accept) throws DbException {
|
||||
Transaction txn = db.startTransaction(false);
|
||||
try {
|
||||
// Look up the session
|
||||
Contact contact = db.getContact(txn, c);
|
||||
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||
if (ss == null) throw new IllegalArgumentException();
|
||||
// Parse the session
|
||||
InviteeSession session = sessionParser
|
||||
.parseInviteeSession(contactGroupId, ss.bdfSession);
|
||||
// Handle the join or leave action
|
||||
if (accept) session = inviteeEngine.onJoinAction(txn, session);
|
||||
else session = inviteeEngine.onLeaveAction(txn, session);
|
||||
// Store the updated session
|
||||
storeSession(txn, ss.storageId, session);
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
private <S extends Session> S handleAction(Transaction txn,
|
||||
LocalAction type, S session, ProtocolEngine<S> engine)
|
||||
throws DbException, FormatException {
|
||||
if (type == LocalAction.INVITE) {
|
||||
throw new IllegalArgumentException();
|
||||
} else if (type == LocalAction.JOIN) {
|
||||
return engine.onJoinAction(txn, session);
|
||||
} else if (type == LocalAction.LEAVE) {
|
||||
return engine.onLeaveAction(txn, session);
|
||||
} else if (type == LocalAction.MEMBER_ADDED) {
|
||||
return engine.onMemberAddedAction(txn, session);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<InvitationMessage> getInvitationMessages(ContactId c)
|
||||
throws DbException {
|
||||
List<InvitationMessage> messages;
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
Contact contact = db.getContact(txn, c);
|
||||
GroupId contactGroupId = getContactGroup(contact).getId();
|
||||
BdfDictionary query = messageParser.getMessagesVisibleInUiQuery();
|
||||
Map<MessageId, BdfDictionary> results = clientHelper
|
||||
.getMessageMetadataAsDictionary(txn, contactGroupId, query);
|
||||
messages = new ArrayList<InvitationMessage>(results.size());
|
||||
for (Entry<MessageId, BdfDictionary> e : results.entrySet()) {
|
||||
MessageId m = e.getKey();
|
||||
MessageMetadata meta =
|
||||
messageParser.parseMetadata(e.getValue());
|
||||
MessageStatus status = db.getMessageStatus(txn, c, m);
|
||||
MessageType type = meta.getMessageType();
|
||||
if (type == INVITE) {
|
||||
messages.add(parseInvitationRequest(txn, c, contactGroupId,
|
||||
m, meta, status));
|
||||
} else if (type == JOIN) {
|
||||
messages.add(parseInvitationResponse(c, contactGroupId, m,
|
||||
meta, status, true));
|
||||
} else if (type == LEAVE) {
|
||||
messages.add(parseInvitationResponse(c, contactGroupId, m,
|
||||
meta, status, false));
|
||||
}
|
||||
}
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
private GroupInvitationRequest parseInvitationRequest(Transaction txn,
|
||||
ContactId c, GroupId contactGroupId, MessageId m,
|
||||
MessageMetadata meta, MessageStatus status)
|
||||
throws DbException, FormatException {
|
||||
SessionId sessionId = getSessionId(meta.getPrivateGroupId());
|
||||
// Look up the invite message to get the details of the private group
|
||||
InviteMessage invite = getInviteMessage(txn, m);
|
||||
return new GroupInvitationRequest(m, sessionId, contactGroupId, c,
|
||||
invite.getMessage(), invite.getGroupName(), invite.getCreator(),
|
||||
meta.isAvailableToAnswer(), meta.getTimestamp(), meta.isLocal(),
|
||||
status.isSent(), status.isSeen(), meta.isRead());
|
||||
}
|
||||
|
||||
private InviteMessage getInviteMessage(Transaction txn, MessageId m)
|
||||
throws DbException, FormatException {
|
||||
Message message = clientHelper.getMessage(txn, m);
|
||||
if (message == null) throw new DbException();
|
||||
BdfList body = clientHelper.toList(message);
|
||||
return messageParser.parseInviteMessage(message, body);
|
||||
}
|
||||
|
||||
private GroupInvitationResponse parseInvitationResponse(ContactId c,
|
||||
GroupId contactGroupId, MessageId m, MessageMetadata meta,
|
||||
MessageStatus status, boolean accept)
|
||||
throws DbException, FormatException {
|
||||
SessionId sessionId = getSessionId(meta.getPrivateGroupId());
|
||||
return new GroupInvitationResponse(m, sessionId, contactGroupId, c,
|
||||
accept, meta.getTimestamp(), meta.isLocal(), status.isSent(),
|
||||
status.isSeen(), meta.isRead());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<GroupInvitationItem> getInvitations() throws DbException {
|
||||
Collection<GroupInvitationItem> invitations =
|
||||
new ArrayList<GroupInvitationItem>();
|
||||
|
||||
return invitations;
|
||||
List<GroupInvitationItem> items = new ArrayList<GroupInvitationItem>();
|
||||
BdfDictionary query = messageParser.getInvitesAvailableToAnswerQuery();
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
// Look up the available invite messages for each contact
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
GroupId contactGroupId = getContactGroup(c).getId();
|
||||
Map<MessageId, BdfDictionary> results =
|
||||
clientHelper.getMessageMetadataAsDictionary(txn,
|
||||
contactGroupId, query);
|
||||
for (MessageId m : results.keySet())
|
||||
items.add(parseGroupInvitationItem(txn, c, m));
|
||||
}
|
||||
db.commitTransaction(txn);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isInvitationAllowed(Contact c, GroupId privateGroupId)
|
||||
throws DbException {
|
||||
GroupId contactGroupId = getContactGroup(c).getId();
|
||||
SessionId sessionId = getSessionId(privateGroupId);
|
||||
Transaction txn = db.startTransaction(true);
|
||||
try {
|
||||
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||
db.commitTransaction(txn);
|
||||
// If there's no session, the contact can be invited
|
||||
if (ss == null) return true;
|
||||
// If there's a session, it should be a creator session
|
||||
if (sessionParser.getRole(ss.bdfSession) != CREATOR)
|
||||
throw new IllegalArgumentException();
|
||||
// If the session's in the start state, the contact can be invited
|
||||
CreatorSession session = sessionParser
|
||||
.parseCreatorSession(contactGroupId, ss.bdfSession);
|
||||
return session.getState() == START;
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
} finally {
|
||||
db.endTransaction(txn);
|
||||
}
|
||||
}
|
||||
|
||||
private GroupInvitationItem parseGroupInvitationItem(Transaction txn,
|
||||
Contact c, MessageId m) throws DbException, FormatException {
|
||||
InviteMessage invite = getInviteMessage(txn, m);
|
||||
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||
invite.getGroupName(), invite.getCreator(), invite.getSalt());
|
||||
return new GroupInvitationItem(privateGroup, c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addingMember(Transaction txn, GroupId privateGroupId, Author a)
|
||||
throws DbException {
|
||||
// If the member is a contact, handle the add member action
|
||||
for (Contact c : db.getContactsByAuthorId(txn, a.getId()))
|
||||
addingMember(txn, privateGroupId, c);
|
||||
}
|
||||
|
||||
private void addingMember(Transaction txn, GroupId privateGroupId,
|
||||
Contact c) throws DbException {
|
||||
try {
|
||||
// Look up the session for the contact, if there is one
|
||||
GroupId contactGroupId = getContactGroup(c).getId();
|
||||
SessionId sessionId = getSessionId(privateGroupId);
|
||||
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||
// Create or parse the session
|
||||
Session session;
|
||||
MessageId storageId;
|
||||
if (ss == null) {
|
||||
// If there's no session the contact must be a peer,
|
||||
// otherwise we would have exchanged invitation messages
|
||||
PeerSession peerSession =
|
||||
new PeerSession(contactGroupId, privateGroupId);
|
||||
// Handle the action
|
||||
session = peerEngine.onMemberAddedAction(txn, peerSession);
|
||||
storageId = createStorageId(txn, contactGroupId);
|
||||
} else {
|
||||
// Handle the action
|
||||
session = handleAction(txn, LocalAction.MEMBER_ADDED,
|
||||
contactGroupId, ss.bdfSession);
|
||||
storageId = ss.storageId;
|
||||
}
|
||||
// Store the updated session
|
||||
storeSession(txn, storageId, session);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removingGroup(Transaction txn, GroupId privateGroupId)
|
||||
throws DbException {
|
||||
SessionId sessionId = getSessionId(privateGroupId);
|
||||
// If we have any sessions in progress, tell the contacts we're leaving
|
||||
try {
|
||||
for (Contact c : db.getContacts(txn)) {
|
||||
// Look up the session for the contact, if there is one
|
||||
GroupId contactGroupId = getContactGroup(c).getId();
|
||||
StoredSession ss = getSession(txn, contactGroupId, sessionId);
|
||||
if (ss == null) continue; // No session for this contact
|
||||
// Handle the action
|
||||
Session session = handleAction(txn, LocalAction.LEAVE,
|
||||
contactGroupId, ss.bdfSession);
|
||||
// Store the updated session
|
||||
storeSession(txn, ss.storageId, session);
|
||||
}
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Session handleAction(Transaction txn, LocalAction a,
|
||||
GroupId contactGroupId, BdfDictionary bdfSession)
|
||||
throws DbException, FormatException {
|
||||
Role role = sessionParser.getRole(bdfSession);
|
||||
if (role == CREATOR) {
|
||||
CreatorSession session = sessionParser
|
||||
.parseCreatorSession(contactGroupId, bdfSession);
|
||||
return handleAction(txn, a, session, creatorEngine);
|
||||
} else if (role == INVITEE) {
|
||||
InviteeSession session = sessionParser
|
||||
.parseInviteeSession(contactGroupId, bdfSession);
|
||||
return handleAction(txn, a, session, inviteeEngine);
|
||||
} else if (role == PEER) {
|
||||
PeerSession session = sessionParser
|
||||
.parsePeerSession(contactGroupId, bdfSession);
|
||||
return handleAction(txn, a, session, peerEngine);
|
||||
} else {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private static class StoredSession {
|
||||
|
||||
private final MessageId storageId;
|
||||
private final BdfDictionary bdfSession;
|
||||
|
||||
private StoredSession(MessageId storageId, BdfDictionary bdfSession) {
|
||||
this.storageId = storageId;
|
||||
this.bdfSession = bdfSession;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
abstract class GroupInvitationMessage {
|
||||
|
||||
private final MessageId id;
|
||||
private final GroupId contactGroupId, privateGroupId;
|
||||
private final long timestamp;
|
||||
|
||||
GroupInvitationMessage(MessageId id, GroupId contactGroupId,
|
||||
GroupId privateGroupId, long timestamp) {
|
||||
this.id = id;
|
||||
this.contactGroupId = contactGroupId;
|
||||
this.privateGroupId = privateGroupId;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
MessageId getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
GroupId getContactGroupId() {
|
||||
return contactGroupId;
|
||||
}
|
||||
|
||||
GroupId getPrivateGroupId() {
|
||||
return privateGroupId;
|
||||
}
|
||||
|
||||
long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.contact.ContactManager;
|
||||
import org.briarproject.api.data.MetadataEncoder;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.lifecycle.LifecycleManager;
|
||||
import org.briarproject.api.messaging.ConversationManager;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationFactory;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
|
||||
import org.briarproject.api.sync.ValidationManager;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import dagger.Module;
|
||||
import dagger.Provides;
|
||||
|
||||
import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID;
|
||||
|
||||
@Module
|
||||
public class GroupInvitationModule {
|
||||
|
||||
public static class EagerSingletons {
|
||||
@Inject
|
||||
GroupInvitationValidator groupInvitationValidator;
|
||||
@Inject
|
||||
GroupInvitationManager groupInvitationManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
GroupInvitationManager provideGroupInvitationManager(
|
||||
GroupInvitationManagerImpl groupInvitationManager,
|
||||
LifecycleManager lifecycleManager,
|
||||
ValidationManager validationManager, ContactManager contactManager,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
ConversationManager conversationManager) {
|
||||
lifecycleManager.registerClient(groupInvitationManager);
|
||||
validationManager.registerIncomingMessageHook(CLIENT_ID,
|
||||
groupInvitationManager);
|
||||
contactManager.registerAddContactHook(groupInvitationManager);
|
||||
contactManager.registerRemoveContactHook(groupInvitationManager);
|
||||
privateGroupManager.registerPrivateGroupHook(groupInvitationManager);
|
||||
conversationManager.registerConversationClient(groupInvitationManager);
|
||||
return groupInvitationManager;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
GroupInvitationValidator provideGroupInvitationValidator(
|
||||
ClientHelper clientHelper, MetadataEncoder metadataEncoder,
|
||||
Clock clock, AuthorFactory authorFactory,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
MessageEncoder messageEncoder,
|
||||
ValidationManager validationManager) {
|
||||
GroupInvitationValidator validator = new GroupInvitationValidator(
|
||||
clientHelper, metadataEncoder, clock, authorFactory,
|
||||
privateGroupFactory, messageEncoder);
|
||||
validationManager.registerMessageValidator(CLIENT_ID, validator);
|
||||
return validator;
|
||||
}
|
||||
|
||||
@Provides
|
||||
GroupInvitationFactory provideGroupInvitationFactory(
|
||||
GroupInvitationFactoryImpl groupInvitationFactory) {
|
||||
return groupInvitationFactory;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MessageParser provideMessageParser(MessageParserImpl messageParser) {
|
||||
return messageParser;
|
||||
}
|
||||
|
||||
@Provides
|
||||
MessageEncoder provideMessageEncoder(MessageEncoderImpl messageEncoder) {
|
||||
return messageEncoder;
|
||||
}
|
||||
|
||||
@Provides
|
||||
SessionParser provideSessionParser(SessionParserImpl sessionParser) {
|
||||
return sessionParser;
|
||||
}
|
||||
|
||||
@Provides
|
||||
SessionEncoder provideSessionEncoder(SessionEncoderImpl sessionEncoder) {
|
||||
return sessionEncoder;
|
||||
}
|
||||
|
||||
@Provides
|
||||
ProtocolEngineFactory provideProtocolEngineFactory(
|
||||
ProtocolEngineFactoryImpl protocolEngineFactory) {
|
||||
return protocolEngineFactory;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.UniqueId;
|
||||
import org.briarproject.api.clients.BdfMessageContext;
|
||||
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.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.sync.Group;
|
||||
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 org.briarproject.clients.BdfMessageValidator;
|
||||
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
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.identity.AuthorConstants.MAX_SIGNATURE_LENGTH;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.GROUP_SALT_LENGTH;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_INVITATION_MSG_LENGTH;
|
||||
import static org.briarproject.api.privategroup.PrivateGroupConstants.MAX_GROUP_NAME_LENGTH;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class GroupInvitationValidator extends BdfMessageValidator {
|
||||
|
||||
private final AuthorFactory authorFactory;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
private final MessageEncoder messageEncoder;
|
||||
|
||||
@Inject
|
||||
GroupInvitationValidator(ClientHelper clientHelper,
|
||||
MetadataEncoder metadataEncoder, Clock clock,
|
||||
AuthorFactory authorFactory,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
MessageEncoder messageEncoder) {
|
||||
super(clientHelper, metadataEncoder, clock);
|
||||
this.authorFactory = authorFactory;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.messageEncoder = messageEncoder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BdfMessageContext validateMessage(Message m, Group g,
|
||||
BdfList body) throws FormatException {
|
||||
MessageType type = MessageType.fromValue(body.getLong(0).intValue());
|
||||
switch (type) {
|
||||
case INVITE:
|
||||
return validateInviteMessage(m, body);
|
||||
case JOIN:
|
||||
return validateJoinMessage(m, body);
|
||||
case LEAVE:
|
||||
return validateLeaveMessage(m, body);
|
||||
case ABORT:
|
||||
return validateAbortMessage(m, body);
|
||||
default:
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
|
||||
private BdfMessageContext validateInviteMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 7);
|
||||
String groupName = body.getString(1);
|
||||
checkLength(groupName, 1, MAX_GROUP_NAME_LENGTH);
|
||||
String creatorName = body.getString(2);
|
||||
checkLength(creatorName, 1, MAX_AUTHOR_NAME_LENGTH);
|
||||
byte[] creatorPublicKey = body.getRaw(3);
|
||||
checkLength(creatorPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
|
||||
byte[] salt = body.getRaw(4);
|
||||
checkLength(salt, GROUP_SALT_LENGTH);
|
||||
String message = body.getOptionalString(5);
|
||||
checkLength(message, 1, MAX_GROUP_INVITATION_MSG_LENGTH);
|
||||
byte[] signature = body.getRaw(6);
|
||||
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
|
||||
// Create the private group
|
||||
Author creator = authorFactory.createAuthor(creatorName,
|
||||
creatorPublicKey);
|
||||
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||
groupName, creator, salt);
|
||||
// Verify the signature
|
||||
BdfList signed = BdfList.of(
|
||||
INVITE.getValue(),
|
||||
m.getTimestamp(),
|
||||
m.getGroupId(),
|
||||
privateGroup.getId()
|
||||
);
|
||||
try {
|
||||
clientHelper.verifySignature(signature, creatorPublicKey, signed);
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new FormatException();
|
||||
}
|
||||
// Create the metadata
|
||||
BdfDictionary meta = messageEncoder.encodeMetadata(INVITE,
|
||||
privateGroup.getId(), m.getTimestamp(), false, false, false,
|
||||
false);
|
||||
return new BdfMessageContext(meta);
|
||||
}
|
||||
|
||||
private BdfMessageContext validateJoinMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 3);
|
||||
byte[] privateGroupId = body.getRaw(1);
|
||||
checkLength(privateGroupId, UniqueId.LENGTH);
|
||||
byte[] previousMessageId = body.getOptionalRaw(2);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
BdfDictionary meta = messageEncoder.encodeMetadata(JOIN,
|
||||
new GroupId(privateGroupId), m.getTimestamp(), false, false,
|
||||
false, false);
|
||||
if (previousMessageId == null) {
|
||||
return new BdfMessageContext(meta);
|
||||
} else {
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
private BdfMessageContext validateLeaveMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 3);
|
||||
byte[] privateGroupId = body.getRaw(1);
|
||||
checkLength(privateGroupId, UniqueId.LENGTH);
|
||||
byte[] previousMessageId = body.getOptionalRaw(2);
|
||||
checkLength(previousMessageId, UniqueId.LENGTH);
|
||||
BdfDictionary meta = messageEncoder.encodeMetadata(LEAVE,
|
||||
new GroupId(privateGroupId), m.getTimestamp(), false, false,
|
||||
false, false);
|
||||
if (previousMessageId == null) {
|
||||
return new BdfMessageContext(meta);
|
||||
} else {
|
||||
MessageId dependency = new MessageId(previousMessageId);
|
||||
return new BdfMessageContext(meta,
|
||||
Collections.singletonList(dependency));
|
||||
}
|
||||
}
|
||||
|
||||
private BdfMessageContext validateAbortMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
checkSize(body, 2);
|
||||
byte[] privateGroupId = body.getRaw(1);
|
||||
checkLength(privateGroupId, UniqueId.LENGTH);
|
||||
BdfDictionary meta = messageEncoder.encodeMetadata(ABORT,
|
||||
new GroupId(privateGroupId), m.getTimestamp(), false, false,
|
||||
false, false);
|
||||
return new BdfMessageContext(meta);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class InviteAction {
|
||||
|
||||
@Nullable
|
||||
private final String message;
|
||||
private final long timestamp;
|
||||
private final byte[] signature;
|
||||
|
||||
InviteAction(@Nullable String message, long timestamp, byte[] signature) {
|
||||
this.message = message;
|
||||
this.timestamp = timestamp;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class InviteMessage extends GroupInvitationMessage {
|
||||
|
||||
private final String groupName;
|
||||
private final Author creator;
|
||||
private final byte[] salt, signature;
|
||||
@Nullable
|
||||
private final String message;
|
||||
|
||||
InviteMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp, String groupName, Author creator, byte[] salt,
|
||||
@Nullable String message, byte[] signature) {
|
||||
super(id, contactGroupId, privateGroupId, timestamp);
|
||||
this.groupName = groupName;
|
||||
this.creator = creator;
|
||||
this.salt = salt;
|
||||
this.message = message;
|
||||
this.signature = signature;
|
||||
}
|
||||
|
||||
String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
Author getCreator() {
|
||||
return creator;
|
||||
}
|
||||
|
||||
byte[] getSalt() {
|
||||
return salt;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
byte[] getSignature() {
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.ProtocolStateException;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.contact.ContactId;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.event.GroupInvitationRequestReceivedEvent;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.privategroup.invitation.GroupInvitationRequest;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.DISSOLVED;
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.ERROR;
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.INVITED;
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.INVITEE_JOINED;
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.INVITEE_LEFT;
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.START;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> {
|
||||
|
||||
InviteeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
IdentityManager identityManager, MessageParser messageParser,
|
||||
MessageEncoder messageEncoder, Clock clock) {
|
||||
super(db, clientHelper, privateGroupManager, privateGroupFactory,
|
||||
groupMessageFactory, identityManager, messageParser,
|
||||
messageEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onInviteAction(Transaction txn, InviteeSession s,
|
||||
@Nullable String message, long timestamp, byte[] signature)
|
||||
throws DbException {
|
||||
throw new UnsupportedOperationException(); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onJoinAction(Transaction txn, InviteeSession s)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case INVITEE_JOINED:
|
||||
case INVITEE_LEFT:
|
||||
case DISSOLVED:
|
||||
case ERROR:
|
||||
throw new ProtocolStateException(); // Invalid in these states
|
||||
case INVITED:
|
||||
return onLocalAccept(txn, s);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onLeaveAction(Transaction txn, InviteeSession s)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case INVITEE_LEFT:
|
||||
case DISSOLVED:
|
||||
case ERROR:
|
||||
return s; // Ignored in these states
|
||||
case INVITED:
|
||||
return onLocalDecline(txn, s);
|
||||
case INVITEE_JOINED:
|
||||
return onLocalLeave(txn, s);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onMemberAddedAction(Transaction txn, InviteeSession s)
|
||||
throws DbException {
|
||||
return s; // Ignored in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onInviteMessage(Transaction txn, InviteeSession s,
|
||||
InviteMessage m) throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
return onRemoteInvite(txn, s, m);
|
||||
case INVITED:
|
||||
case INVITEE_JOINED:
|
||||
case INVITEE_LEFT:
|
||||
case DISSOLVED:
|
||||
return abort(txn, s); // Invalid in these states
|
||||
case ERROR:
|
||||
return s; // Ignored in this state
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onJoinMessage(Transaction txn, InviteeSession s,
|
||||
JoinMessage m) throws DbException, FormatException {
|
||||
return abort(txn, s); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onLeaveMessage(Transaction txn, InviteeSession s,
|
||||
LeaveMessage m) throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case DISSOLVED:
|
||||
return abort(txn, s); // Invalid in these states
|
||||
case INVITED:
|
||||
case INVITEE_JOINED:
|
||||
case INVITEE_LEFT:
|
||||
return onRemoteLeave(txn, s, m);
|
||||
case ERROR:
|
||||
return s; // Ignored in this state
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession onAbortMessage(Transaction txn, InviteeSession s,
|
||||
AbortMessage m) throws DbException, FormatException {
|
||||
return abort(txn, s);
|
||||
}
|
||||
|
||||
private InviteeSession onLocalAccept(Transaction txn, InviteeSession s)
|
||||
throws DbException {
|
||||
// Mark the invite message unavailable to answer
|
||||
MessageId inviteId = s.getLastRemoteMessageId();
|
||||
if (inviteId == null) throw new IllegalStateException();
|
||||
markMessageAvailableToAnswer(txn, inviteId, false);
|
||||
// Send a JOIN message
|
||||
Message sent = sendJoinMessage(txn, s, true);
|
||||
try {
|
||||
// Subscribe to the private group
|
||||
subscribeToPrivateGroup(txn, inviteId);
|
||||
// Start syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, true);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group metadata
|
||||
}
|
||||
// Move to the INVITEE_JOINED state
|
||||
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
s.getInviteTimestamp(), INVITEE_JOINED);
|
||||
}
|
||||
|
||||
private InviteeSession onLocalDecline(Transaction txn, InviteeSession s)
|
||||
throws DbException {
|
||||
// Mark the invite message unavailable to answer
|
||||
MessageId inviteId = s.getLastRemoteMessageId();
|
||||
if (inviteId == null) throw new IllegalStateException();
|
||||
markMessageAvailableToAnswer(txn, inviteId, false);
|
||||
// Send a LEAVE message
|
||||
Message sent = sendLeaveMessage(txn, s, true);
|
||||
// Move to the START state
|
||||
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
s.getInviteTimestamp(), START);
|
||||
}
|
||||
|
||||
private InviteeSession onLocalLeave(Transaction txn, InviteeSession s)
|
||||
throws DbException {
|
||||
// Send a LEAVE message
|
||||
Message sent = sendLeaveMessage(txn, s, false);
|
||||
// Move to the INVITEE_LEFT state
|
||||
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
s.getInviteTimestamp(), INVITEE_LEFT);
|
||||
}
|
||||
|
||||
private InviteeSession onRemoteInvite(Transaction txn, InviteeSession s,
|
||||
InviteMessage m) throws DbException, FormatException {
|
||||
// The timestamp must be higher than the last invite message, if any
|
||||
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||
// Check that the contact is the creator
|
||||
ContactId contactId = getContactId(txn, s.getContactGroupId());
|
||||
Author contact = db.getContact(txn, contactId).getAuthor();
|
||||
if (!contact.getId().equals(m.getCreator().getId()))
|
||||
return abort(txn, s);
|
||||
// Mark the invite message visible in the UI and available to answer
|
||||
markMessageVisibleInUi(txn, m.getId(), true);
|
||||
markMessageAvailableToAnswer(txn, m.getId(), true);
|
||||
// Broadcast an event
|
||||
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||
m.getGroupName(), m.getCreator(), m.getSalt());
|
||||
txn.attach(new GroupInvitationRequestReceivedEvent(privateGroup,
|
||||
contactId, createInvitationRequest(m, contactId)));
|
||||
// Move to the INVITED state
|
||||
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
m.getTimestamp(), INVITED);
|
||||
}
|
||||
|
||||
private InviteeSession onRemoteLeave(Transaction txn, InviteeSession s,
|
||||
LeaveMessage m) throws DbException, FormatException {
|
||||
// The timestamp must be higher than the last invite message, if any
|
||||
if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s);
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
try {
|
||||
// Stop syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group metadata
|
||||
}
|
||||
// Mark the group dissolved
|
||||
privateGroupManager.markGroupDissolved(txn, s.getPrivateGroupId());
|
||||
// Move to the DISSOLVED state
|
||||
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
s.getInviteTimestamp(), DISSOLVED);
|
||||
}
|
||||
|
||||
private InviteeSession abort(Transaction txn, InviteeSession s)
|
||||
throws DbException, FormatException {
|
||||
// If the session has already been aborted, do nothing
|
||||
if (s.getState() == ERROR) return s;
|
||||
// Mark any invite messages in the session unavailable to answer
|
||||
markInvitesUnavailableToAnswer(txn, s);
|
||||
// Stop syncing the private group with the contact, if we subscribe
|
||||
if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
// Send an ABORT message
|
||||
Message sent = sendAbortMessage(txn, s);
|
||||
// Move to the ERROR state
|
||||
return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
s.getInviteTimestamp(), ERROR);
|
||||
}
|
||||
|
||||
private GroupInvitationRequest createInvitationRequest(InviteMessage m,
|
||||
ContactId c) {
|
||||
SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes());
|
||||
return new GroupInvitationRequest(m.getId(), sessionId,
|
||||
m.getContactGroupId(), c, m.getMessage(), m.getGroupName(),
|
||||
m.getCreator(), true, m.getTimestamp(), false, false, true,
|
||||
false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.InviteeState.START;
|
||||
import static org.briarproject.privategroup.invitation.Role.INVITEE;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class InviteeSession extends Session<InviteeState> {
|
||||
|
||||
private final InviteeState state;
|
||||
|
||||
InviteeSession(GroupId contactGroupId, GroupId privateGroupId,
|
||||
@Nullable MessageId lastLocalMessageId,
|
||||
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||
long inviteTimestamp, InviteeState state) {
|
||||
super(contactGroupId, privateGroupId, lastLocalMessageId,
|
||||
lastRemoteMessageId, localTimestamp, inviteTimestamp);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
InviteeSession(GroupId contactGroupId, GroupId privateGroupId) {
|
||||
this(contactGroupId, privateGroupId, null, null, 0, 0, START);
|
||||
}
|
||||
|
||||
@Override
|
||||
Role getRole() {
|
||||
return INVITEE;
|
||||
}
|
||||
|
||||
@Override
|
||||
InviteeState getState() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
|
||||
enum InviteeState implements State {
|
||||
|
||||
START(0), INVITED(1), INVITEE_JOINED(2), INVITEE_LEFT(3), DISSOLVED(4),
|
||||
ERROR(5);
|
||||
|
||||
private final int value;
|
||||
|
||||
InviteeState(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static InviteeState fromValue(int value) throws FormatException {
|
||||
for (InviteeState s : values()) if (s.value == value) return s;
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class JoinMessage extends GroupInvitationMessage {
|
||||
|
||||
@Nullable
|
||||
private final MessageId previousMessageId;
|
||||
|
||||
JoinMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp, @Nullable MessageId previousMessageId) {
|
||||
super(id, contactGroupId, privateGroupId, timestamp);
|
||||
this.previousMessageId = previousMessageId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MessageId getPreviousMessageId() {
|
||||
return previousMessageId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class LeaveMessage extends GroupInvitationMessage {
|
||||
|
||||
@Nullable
|
||||
private final MessageId previousMessageId;
|
||||
|
||||
LeaveMessage(MessageId id, GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp, @Nullable MessageId previousMessageId) {
|
||||
super(id, contactGroupId, privateGroupId, timestamp);
|
||||
this.previousMessageId = previousMessageId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MessageId getPreviousMessageId() {
|
||||
return previousMessageId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
enum LocalAction {
|
||||
|
||||
INVITE, JOIN, LEAVE, MEMBER_ADDED
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
interface MessageEncoder {
|
||||
|
||||
BdfDictionary encodeMetadata(MessageType type, GroupId privateGroupId,
|
||||
long timestamp, boolean local, boolean read, boolean visible,
|
||||
boolean available);
|
||||
|
||||
void setVisibleInUi(BdfDictionary meta, boolean visible);
|
||||
|
||||
void setAvailableToAnswer(BdfDictionary meta, boolean available);
|
||||
|
||||
Message encodeInviteMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp, String groupName, Author creator, byte[] salt,
|
||||
@Nullable String message, byte[] signature);
|
||||
|
||||
Message encodeJoinMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp, @Nullable MessageId previousMessageId);
|
||||
|
||||
Message encodeLeaveMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp, @Nullable MessageId previousMessageId);
|
||||
|
||||
Message encodeAbortMessage(GroupId contactGroupId, GroupId privateGroupId,
|
||||
long timestamp);
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageFactory;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_VISIBLE_IN_UI;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.ABORT;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.JOIN;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.LEAVE;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MessageEncoderImpl implements MessageEncoder {
|
||||
|
||||
private final ClientHelper clientHelper;
|
||||
private final MessageFactory messageFactory;
|
||||
|
||||
@Inject
|
||||
MessageEncoderImpl(ClientHelper clientHelper,
|
||||
MessageFactory messageFactory) {
|
||||
this.clientHelper = clientHelper;
|
||||
this.messageFactory = messageFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfDictionary encodeMetadata(MessageType type,
|
||||
GroupId privateGroupId, long timestamp, boolean local, boolean read,
|
||||
boolean visible, boolean available) {
|
||||
BdfDictionary meta = new BdfDictionary();
|
||||
meta.put(MSG_KEY_MESSAGE_TYPE, type.getValue());
|
||||
meta.put(MSG_KEY_PRIVATE_GROUP_ID, privateGroupId);
|
||||
meta.put(MSG_KEY_TIMESTAMP, timestamp);
|
||||
meta.put(MSG_KEY_LOCAL, local);
|
||||
meta.put(MSG_KEY_READ, read);
|
||||
meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
|
||||
meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
|
||||
return meta;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisibleInUi(BdfDictionary meta, boolean visible) {
|
||||
meta.put(MSG_KEY_VISIBLE_IN_UI, visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAvailableToAnswer(BdfDictionary meta, boolean available) {
|
||||
meta.put(MSG_KEY_AVAILABLE_TO_ANSWER, available);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message encodeInviteMessage(GroupId contactGroupId,
|
||||
GroupId privateGroupId, long timestamp, String groupName,
|
||||
Author creator, byte[] salt, @Nullable String message,
|
||||
byte[] signature) {
|
||||
BdfList body = BdfList.of(
|
||||
INVITE.getValue(),
|
||||
groupName,
|
||||
creator.getName(),
|
||||
creator.getPublicKey(),
|
||||
salt,
|
||||
message,
|
||||
signature
|
||||
);
|
||||
try {
|
||||
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||
clientHelper.toByteArray(body));
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message encodeJoinMessage(GroupId contactGroupId,
|
||||
GroupId privateGroupId, long timestamp,
|
||||
@Nullable MessageId previousMessageId) {
|
||||
BdfList body = BdfList.of(
|
||||
JOIN.getValue(),
|
||||
privateGroupId,
|
||||
previousMessageId
|
||||
);
|
||||
try {
|
||||
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||
clientHelper.toByteArray(body));
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message encodeLeaveMessage(GroupId contactGroupId,
|
||||
GroupId privateGroupId, long timestamp,
|
||||
@Nullable MessageId previousMessageId) {
|
||||
BdfList body = BdfList.of(
|
||||
LEAVE.getValue(),
|
||||
privateGroupId,
|
||||
previousMessageId
|
||||
);
|
||||
try {
|
||||
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||
clientHelper.toByteArray(body));
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Message encodeAbortMessage(GroupId contactGroupId,
|
||||
GroupId privateGroupId, long timestamp) {
|
||||
BdfList body = BdfList.of(
|
||||
ABORT.getValue(),
|
||||
privateGroupId
|
||||
);
|
||||
try {
|
||||
return messageFactory.createMessage(contactGroupId, timestamp,
|
||||
clientHelper.toByteArray(body));
|
||||
} catch (FormatException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
class MessageMetadata {
|
||||
|
||||
private final MessageType type;
|
||||
private final GroupId privateGroupId;
|
||||
private final long timestamp;
|
||||
private final boolean local, read, visible, available;
|
||||
|
||||
MessageMetadata(MessageType type, GroupId privateGroupId,
|
||||
long timestamp, boolean local, boolean read, boolean visible,
|
||||
boolean available) {
|
||||
this.privateGroupId = privateGroupId;
|
||||
this.type = type;
|
||||
this.timestamp = timestamp;
|
||||
this.local = local;
|
||||
this.read = read;
|
||||
this.visible = visible;
|
||||
this.available = available;
|
||||
}
|
||||
|
||||
MessageType getMessageType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
GroupId getPrivateGroupId() {
|
||||
return privateGroupId;
|
||||
}
|
||||
|
||||
long getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
boolean isLocal() {
|
||||
return local;
|
||||
}
|
||||
|
||||
boolean isRead() {
|
||||
return read;
|
||||
}
|
||||
|
||||
boolean isVisibleInConversation() {
|
||||
return visible;
|
||||
}
|
||||
|
||||
boolean isAvailableToAnswer() {
|
||||
return available;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
|
||||
@NotNullByDefault
|
||||
interface MessageParser {
|
||||
|
||||
BdfDictionary getMessagesVisibleInUiQuery();
|
||||
|
||||
BdfDictionary getInvitesAvailableToAnswerQuery();
|
||||
|
||||
BdfDictionary getInvitesAvailableToAnswerQuery(GroupId privateGroupId);
|
||||
|
||||
MessageMetadata parseMetadata(BdfDictionary meta) throws FormatException;
|
||||
|
||||
InviteMessage parseInviteMessage(Message m, BdfList body)
|
||||
throws FormatException;
|
||||
|
||||
JoinMessage parseJoinMessage(Message m, BdfList body)
|
||||
throws FormatException;
|
||||
|
||||
LeaveMessage parseLeaveMessage(Message m, BdfList body)
|
||||
throws FormatException;
|
||||
|
||||
AbortMessage parseAbortMessage(Message m, BdfList body)
|
||||
throws FormatException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
import org.briarproject.api.data.BdfList;
|
||||
import org.briarproject.api.identity.Author;
|
||||
import org.briarproject.api.identity.AuthorFactory;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.PrivateGroup;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.clients.BdfConstants.MSG_KEY_READ;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_AVAILABLE_TO_ANSWER;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_LOCAL;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_MESSAGE_TYPE;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_PRIVATE_GROUP_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.MSG_KEY_VISIBLE_IN_UI;
|
||||
import static org.briarproject.privategroup.invitation.MessageType.INVITE;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class MessageParserImpl implements MessageParser {
|
||||
|
||||
private final AuthorFactory authorFactory;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
|
||||
@Inject
|
||||
MessageParserImpl(AuthorFactory authorFactory,
|
||||
PrivateGroupFactory privateGroupFactory) {
|
||||
this.authorFactory = authorFactory;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfDictionary getMessagesVisibleInUiQuery() {
|
||||
return BdfDictionary.of(new BdfEntry(MSG_KEY_VISIBLE_IN_UI, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfDictionary getInvitesAvailableToAnswerQuery() {
|
||||
return BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
|
||||
new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue())
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfDictionary getInvitesAvailableToAnswerQuery(
|
||||
GroupId privateGroupId) {
|
||||
return BdfDictionary.of(
|
||||
new BdfEntry(MSG_KEY_AVAILABLE_TO_ANSWER, true),
|
||||
new BdfEntry(MSG_KEY_MESSAGE_TYPE, INVITE.getValue()),
|
||||
new BdfEntry(MSG_KEY_PRIVATE_GROUP_ID, privateGroupId)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageMetadata parseMetadata(BdfDictionary meta)
|
||||
throws FormatException {
|
||||
MessageType type = MessageType.fromValue(
|
||||
meta.getLong(MSG_KEY_MESSAGE_TYPE).intValue());
|
||||
GroupId privateGroupId =
|
||||
new GroupId(meta.getRaw(MSG_KEY_PRIVATE_GROUP_ID));
|
||||
long timestamp = meta.getLong(MSG_KEY_TIMESTAMP);
|
||||
boolean local = meta.getBoolean(MSG_KEY_LOCAL);
|
||||
boolean read = meta.getBoolean(MSG_KEY_READ, false);
|
||||
boolean visible = meta.getBoolean(MSG_KEY_VISIBLE_IN_UI, false);
|
||||
boolean available = meta.getBoolean(MSG_KEY_AVAILABLE_TO_ANSWER, false);
|
||||
return new MessageMetadata(type, privateGroupId, timestamp, local, read,
|
||||
visible, available);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteMessage parseInviteMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
String groupName = body.getString(1);
|
||||
String creatorName = body.getString(2);
|
||||
byte[] creatorPublicKey = body.getRaw(3);
|
||||
byte[] salt = body.getRaw(4);
|
||||
String message = body.getOptionalString(5);
|
||||
byte[] signature = body.getRaw(6);
|
||||
Author creator = authorFactory.createAuthor(creatorName,
|
||||
creatorPublicKey);
|
||||
PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup(
|
||||
groupName, creator, salt);
|
||||
return new InviteMessage(m.getId(), m.getGroupId(),
|
||||
privateGroup.getId(), m.getTimestamp(), groupName, creator,
|
||||
salt, message, signature);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JoinMessage parseJoinMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
GroupId privateGroupId = new GroupId(body.getRaw(1));
|
||||
byte[] b = body.getOptionalRaw(2);
|
||||
MessageId previousMessageId = b == null ? null : new MessageId(b);
|
||||
return new JoinMessage(m.getId(), m.getGroupId(), privateGroupId,
|
||||
m.getTimestamp(), previousMessageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public LeaveMessage parseLeaveMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
GroupId privateGroupId = new GroupId(body.getRaw(1));
|
||||
byte[] b = body.getOptionalRaw(2);
|
||||
MessageId previousMessageId = b == null ? null : new MessageId(b);
|
||||
return new LeaveMessage(m.getId(), m.getGroupId(), privateGroupId,
|
||||
m.getTimestamp(), previousMessageId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbortMessage parseAbortMessage(Message m, BdfList body)
|
||||
throws FormatException {
|
||||
GroupId privateGroupId = new GroupId(body.getRaw(1));
|
||||
return new AbortMessage(m.getId(), m.getGroupId(), privateGroupId,
|
||||
m.getTimestamp());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
|
||||
enum MessageType {
|
||||
|
||||
INVITE(0), JOIN(1), LEAVE(2), ABORT(3);
|
||||
|
||||
private final int value;
|
||||
|
||||
MessageType(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static MessageType fromValue(int value) throws FormatException {
|
||||
for (MessageType m : values()) if (m.value == value) return m;
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.clients.ProtocolStateException;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.sync.Message;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.PeerState.AWAIT_MEMBER;
|
||||
import static org.briarproject.privategroup.invitation.PeerState.BOTH_JOINED;
|
||||
import static org.briarproject.privategroup.invitation.PeerState.ERROR;
|
||||
import static org.briarproject.privategroup.invitation.PeerState.LOCAL_JOINED;
|
||||
import static org.briarproject.privategroup.invitation.PeerState.LOCAL_LEFT;
|
||||
import static org.briarproject.privategroup.invitation.PeerState.NEITHER_JOINED;
|
||||
import static org.briarproject.privategroup.invitation.PeerState.START;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> {
|
||||
|
||||
PeerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
IdentityManager identityManager, MessageParser messageParser,
|
||||
MessageEncoder messageEncoder, Clock clock) {
|
||||
super(db, clientHelper, privateGroupManager, privateGroupFactory,
|
||||
groupMessageFactory, identityManager, messageParser,
|
||||
messageEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onInviteAction(Transaction txn, PeerSession s,
|
||||
@Nullable String message, long timestamp, byte[] signature)
|
||||
throws DbException {
|
||||
throw new UnsupportedOperationException(); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onJoinAction(Transaction txn, PeerSession s)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case AWAIT_MEMBER:
|
||||
case LOCAL_JOINED:
|
||||
case BOTH_JOINED:
|
||||
case ERROR:
|
||||
throw new ProtocolStateException(); // Invalid in these states
|
||||
case NEITHER_JOINED:
|
||||
return onLocalJoinFromNeitherJoined(txn, s);
|
||||
case LOCAL_LEFT:
|
||||
return onLocalJoinFromLocalLeft(txn, s);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onLeaveAction(Transaction txn, PeerSession s)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case AWAIT_MEMBER:
|
||||
case NEITHER_JOINED:
|
||||
case LOCAL_LEFT:
|
||||
case ERROR:
|
||||
return s; // Ignored in these states
|
||||
case LOCAL_JOINED:
|
||||
return onLocalLeaveFromLocalJoined(txn, s);
|
||||
case BOTH_JOINED:
|
||||
return onLocalLeaveFromBothJoined(txn, s);
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onMemberAddedAction(Transaction txn, PeerSession s)
|
||||
throws DbException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
return onMemberAddedFromStart(s);
|
||||
case AWAIT_MEMBER:
|
||||
return onMemberAddedFromAwaitMember(txn, s);
|
||||
case NEITHER_JOINED:
|
||||
case LOCAL_JOINED:
|
||||
case BOTH_JOINED:
|
||||
case LOCAL_LEFT:
|
||||
throw new ProtocolStateException(); // Invalid in these states
|
||||
case ERROR:
|
||||
return s; // Ignored in this state
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onInviteMessage(Transaction txn, PeerSession s,
|
||||
InviteMessage m) throws DbException, FormatException {
|
||||
return abort(txn, s); // Invalid in this role
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onJoinMessage(Transaction txn, PeerSession s,
|
||||
JoinMessage m) throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case AWAIT_MEMBER:
|
||||
case BOTH_JOINED:
|
||||
case LOCAL_LEFT:
|
||||
return abort(txn, s); // Invalid in these states
|
||||
case START:
|
||||
return onRemoteJoinFromStart(txn, s, m);
|
||||
case NEITHER_JOINED:
|
||||
return onRemoteJoinFromNeitherJoined(txn, s, m);
|
||||
case LOCAL_JOINED:
|
||||
return onRemoteJoinFromLocalJoined(txn, s, m);
|
||||
case ERROR:
|
||||
return s; // Ignored in this state
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onLeaveMessage(Transaction txn, PeerSession s,
|
||||
LeaveMessage m) throws DbException, FormatException {
|
||||
switch (s.getState()) {
|
||||
case START:
|
||||
case NEITHER_JOINED:
|
||||
case LOCAL_JOINED:
|
||||
return abort(txn, s); // Invalid in these states
|
||||
case AWAIT_MEMBER:
|
||||
return onRemoteLeaveFromAwaitMember(txn, s, m);
|
||||
case LOCAL_LEFT:
|
||||
return onRemoteLeaveFromLocalLeft(txn, s, m);
|
||||
case BOTH_JOINED:
|
||||
return onRemoteLeaveFromBothJoined(txn, s, m);
|
||||
case ERROR:
|
||||
return s; // Ignored in this state
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession onAbortMessage(Transaction txn, PeerSession s,
|
||||
AbortMessage m) throws DbException, FormatException {
|
||||
return abort(txn, s);
|
||||
}
|
||||
|
||||
private PeerSession onLocalJoinFromNeitherJoined(Transaction txn,
|
||||
PeerSession s) throws DbException {
|
||||
// Send a JOIN message
|
||||
Message sent = sendJoinMessage(txn, s, false);
|
||||
// Move to the LOCAL_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
LOCAL_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onLocalJoinFromLocalLeft(Transaction txn, PeerSession s)
|
||||
throws DbException {
|
||||
// Send a JOIN message
|
||||
Message sent = sendJoinMessage(txn, s, false);
|
||||
try {
|
||||
// Start syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, true);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group metadata
|
||||
}
|
||||
// Move to the BOTH_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
BOTH_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onLocalLeaveFromBothJoined(Transaction txn,
|
||||
PeerSession s) throws DbException {
|
||||
// Send a LEAVE message
|
||||
Message sent = sendLeaveMessage(txn, s, false);
|
||||
try {
|
||||
// Stop syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group metadata
|
||||
}
|
||||
// Move to the LOCAL_LEFT state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
LOCAL_LEFT);
|
||||
}
|
||||
|
||||
private PeerSession onLocalLeaveFromLocalJoined(Transaction txn,
|
||||
PeerSession s) throws DbException {
|
||||
// Send a LEAVE message
|
||||
Message sent = sendLeaveMessage(txn, s, false);
|
||||
// Move to the NEITHER_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
NEITHER_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onMemberAddedFromStart(PeerSession s) {
|
||||
// Move to the NEITHER_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), s.getLastRemoteMessageId(),
|
||||
s.getLocalTimestamp(), NEITHER_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onMemberAddedFromAwaitMember(Transaction txn,
|
||||
PeerSession s) throws DbException {
|
||||
// Send a JOIN message
|
||||
Message sent = sendJoinMessage(txn, s, false);
|
||||
try {
|
||||
// Start syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, true);
|
||||
} catch (FormatException e) {
|
||||
throw new DbException(e); // Invalid group metadata
|
||||
}
|
||||
// Move to the BOTH_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
BOTH_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onRemoteJoinFromStart(Transaction txn,
|
||||
PeerSession s, JoinMessage m) throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Move to the AWAIT_MEMBER state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
AWAIT_MEMBER);
|
||||
}
|
||||
|
||||
private PeerSession onRemoteJoinFromNeitherJoined(Transaction txn,
|
||||
PeerSession s, JoinMessage m) throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Send a JOIN message
|
||||
Message sent = sendJoinMessage(txn, s, false);
|
||||
// Start syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, true);
|
||||
// Move to the BOTH_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), m.getId(), sent.getTimestamp(), BOTH_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onRemoteJoinFromLocalJoined(Transaction txn,
|
||||
PeerSession s, JoinMessage m) throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Start syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, true);
|
||||
// Move to the BOTH_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
BOTH_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onRemoteLeaveFromAwaitMember(Transaction txn,
|
||||
PeerSession s, LeaveMessage m) throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Move to the START state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
START);
|
||||
}
|
||||
|
||||
private PeerSession onRemoteLeaveFromLocalLeft(Transaction txn,
|
||||
PeerSession s, LeaveMessage m) throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Move to the NEITHER_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
NEITHER_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession onRemoteLeaveFromBothJoined(Transaction txn,
|
||||
PeerSession s, LeaveMessage m) throws DbException, FormatException {
|
||||
// The dependency, if any, must be the last remote message
|
||||
if (!isValidDependency(s, m.getPreviousMessageId()))
|
||||
return abort(txn, s);
|
||||
// Stop syncing the private group with the contact
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
// Move to the LOCAL_JOINED state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
|
||||
LOCAL_JOINED);
|
||||
}
|
||||
|
||||
private PeerSession abort(Transaction txn, PeerSession s)
|
||||
throws DbException, FormatException {
|
||||
// If the session has already been aborted, do nothing
|
||||
if (s.getState() == ERROR) return s;
|
||||
// Stop syncing the private group with the contact, if we subscribe
|
||||
if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId()))
|
||||
syncPrivateGroupWithContact(txn, s, false);
|
||||
// Send an ABORT message
|
||||
Message sent = sendAbortMessage(txn, s);
|
||||
// Move to the ERROR state
|
||||
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
|
||||
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
|
||||
ERROR);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.PeerState.START;
|
||||
import static org.briarproject.privategroup.invitation.Role.PEER;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class PeerSession extends Session<PeerState> {
|
||||
|
||||
private final PeerState state;
|
||||
|
||||
PeerSession(GroupId contactGroupId, GroupId privateGroupId,
|
||||
@Nullable MessageId lastLocalMessageId,
|
||||
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||
PeerState state) {
|
||||
super(contactGroupId, privateGroupId, lastLocalMessageId,
|
||||
lastRemoteMessageId, localTimestamp, 0);
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
PeerSession(GroupId contactGroupId, GroupId privateGroupId) {
|
||||
this(contactGroupId, privateGroupId, null, null, 0, START);
|
||||
}
|
||||
|
||||
@Override
|
||||
Role getRole() {
|
||||
return PEER;
|
||||
}
|
||||
|
||||
@Override
|
||||
PeerState getState() {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
|
||||
enum PeerState implements State {
|
||||
|
||||
START(0), AWAIT_MEMBER(1), NEITHER_JOINED(2), LOCAL_JOINED(3),
|
||||
BOTH_JOINED(4), LOCAL_LEFT(5), ERROR(6);
|
||||
|
||||
private final int value;
|
||||
|
||||
PeerState(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static PeerState fromValue(int value) throws FormatException {
|
||||
for (PeerState s : values()) if (s.value == value) return s;
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.db.DbException;
|
||||
import org.briarproject.api.db.Transaction;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@NotNullByDefault
|
||||
interface ProtocolEngine<S extends Session> {
|
||||
|
||||
S onInviteAction(Transaction txn, S session, @Nullable String message,
|
||||
long timestamp, byte[] signature) throws DbException;
|
||||
|
||||
S onJoinAction(Transaction txn, S session) throws DbException;
|
||||
|
||||
S onLeaveAction(Transaction txn, S session) throws DbException;
|
||||
|
||||
S onMemberAddedAction(Transaction txn, S session) throws DbException;
|
||||
|
||||
S onInviteMessage(Transaction txn, S session, InviteMessage m)
|
||||
throws DbException, FormatException;
|
||||
|
||||
S onJoinMessage(Transaction txn, S session, JoinMessage m)
|
||||
throws DbException, FormatException;
|
||||
|
||||
S onLeaveMessage(Transaction txn, S session, LeaveMessage m)
|
||||
throws DbException, FormatException;
|
||||
|
||||
S onAbortMessage(Transaction txn, S session, AbortMessage m)
|
||||
throws DbException, FormatException;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
interface ProtocolEngineFactory {
|
||||
|
||||
ProtocolEngine<CreatorSession> createCreatorEngine();
|
||||
|
||||
ProtocolEngine<InviteeSession> createInviteeEngine();
|
||||
|
||||
ProtocolEngine<PeerSession> createPeerEngine();
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.clients.ClientHelper;
|
||||
import org.briarproject.api.db.DatabaseComponent;
|
||||
import org.briarproject.api.identity.IdentityManager;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.privategroup.GroupMessageFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupFactory;
|
||||
import org.briarproject.api.privategroup.PrivateGroupManager;
|
||||
import org.briarproject.api.system.Clock;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class ProtocolEngineFactoryImpl implements ProtocolEngineFactory {
|
||||
|
||||
private final DatabaseComponent db;
|
||||
private final ClientHelper clientHelper;
|
||||
private final PrivateGroupManager privateGroupManager;
|
||||
private final PrivateGroupFactory privateGroupFactory;
|
||||
private final GroupMessageFactory groupMessageFactory;
|
||||
private final IdentityManager identityManager;
|
||||
private final MessageParser messageParser;
|
||||
private final MessageEncoder messageEncoder;
|
||||
private final Clock clock;
|
||||
|
||||
@Inject
|
||||
ProtocolEngineFactoryImpl(DatabaseComponent db, ClientHelper clientHelper,
|
||||
PrivateGroupManager privateGroupManager,
|
||||
PrivateGroupFactory privateGroupFactory,
|
||||
GroupMessageFactory groupMessageFactory,
|
||||
IdentityManager identityManager, MessageParser messageParser,
|
||||
MessageEncoder messageEncoder,
|
||||
Clock clock) {
|
||||
this.db = db;
|
||||
this.clientHelper = clientHelper;
|
||||
this.privateGroupManager = privateGroupManager;
|
||||
this.privateGroupFactory = privateGroupFactory;
|
||||
this.groupMessageFactory = groupMessageFactory;
|
||||
this.identityManager = identityManager;
|
||||
this.messageParser = messageParser;
|
||||
this.messageEncoder = messageEncoder;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolEngine<CreatorSession> createCreatorEngine() {
|
||||
return new CreatorProtocolEngine(db, clientHelper, privateGroupManager,
|
||||
privateGroupFactory, groupMessageFactory, identityManager,
|
||||
messageParser, messageEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolEngine<InviteeSession> createInviteeEngine() {
|
||||
return new InviteeProtocolEngine(db, clientHelper, privateGroupManager,
|
||||
privateGroupFactory, groupMessageFactory, identityManager,
|
||||
messageParser, messageEncoder, clock);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProtocolEngine<PeerSession> createPeerEngine() {
|
||||
return new PeerProtocolEngine(db, clientHelper, privateGroupManager,
|
||||
privateGroupFactory, groupMessageFactory, identityManager,
|
||||
messageParser, messageEncoder, clock);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
|
||||
enum Role {
|
||||
|
||||
CREATOR(0), INVITEE(1), PEER(2);
|
||||
|
||||
private final int value;
|
||||
|
||||
Role(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
int getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
static Role fromValue(int value) throws FormatException {
|
||||
for (Role r : values()) if (r.value == value) return r;
|
||||
throw new FormatException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
abstract class Session<S extends State> {
|
||||
|
||||
private final GroupId contactGroupId, privateGroupId;
|
||||
@Nullable
|
||||
private final MessageId lastLocalMessageId, lastRemoteMessageId;
|
||||
private final long localTimestamp, inviteTimestamp;
|
||||
|
||||
Session(GroupId contactGroupId, GroupId privateGroupId,
|
||||
@Nullable MessageId lastLocalMessageId,
|
||||
@Nullable MessageId lastRemoteMessageId, long localTimestamp,
|
||||
long inviteTimestamp) {
|
||||
this.contactGroupId = contactGroupId;
|
||||
this.privateGroupId = privateGroupId;
|
||||
this.lastLocalMessageId = lastLocalMessageId;
|
||||
this.lastRemoteMessageId = lastRemoteMessageId;
|
||||
this.localTimestamp = localTimestamp;
|
||||
this.inviteTimestamp = inviteTimestamp;
|
||||
}
|
||||
|
||||
abstract Role getRole();
|
||||
|
||||
abstract S getState();
|
||||
|
||||
GroupId getContactGroupId() {
|
||||
return contactGroupId;
|
||||
}
|
||||
|
||||
GroupId getPrivateGroupId() {
|
||||
return privateGroupId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MessageId getLastLocalMessageId() {
|
||||
return lastLocalMessageId;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
MessageId getLastRemoteMessageId() {
|
||||
return lastRemoteMessageId;
|
||||
}
|
||||
|
||||
long getLocalTimestamp() {
|
||||
return localTimestamp;
|
||||
}
|
||||
|
||||
long getInviteTimestamp() {
|
||||
return inviteTimestamp;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
|
||||
@NotNullByDefault
|
||||
interface SessionEncoder {
|
||||
|
||||
BdfDictionary encodeSession(Session s);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.api.data.BdfDictionary.NULL_VALUE;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_PRIVATE_GROUP_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_ROLE;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_SESSION_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_STATE;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class SessionEncoderImpl implements SessionEncoder {
|
||||
|
||||
@Inject
|
||||
SessionEncoderImpl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfDictionary encodeSession(Session s) {
|
||||
BdfDictionary d = new BdfDictionary();
|
||||
d.put(SESSION_KEY_SESSION_ID, s.getPrivateGroupId());
|
||||
d.put(SESSION_KEY_PRIVATE_GROUP_ID, s.getPrivateGroupId());
|
||||
MessageId lastLocalMessageId = s.getLastLocalMessageId();
|
||||
if (lastLocalMessageId == null)
|
||||
d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, NULL_VALUE);
|
||||
else d.put(SESSION_KEY_LAST_LOCAL_MESSAGE_ID, lastLocalMessageId);
|
||||
MessageId lastRemoteMessageId = s.getLastRemoteMessageId();
|
||||
if (lastRemoteMessageId == null)
|
||||
d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, NULL_VALUE);
|
||||
else d.put(SESSION_KEY_LAST_REMOTE_MESSAGE_ID, lastRemoteMessageId);
|
||||
d.put(SESSION_KEY_LOCAL_TIMESTAMP, s.getLocalTimestamp());
|
||||
d.put(SESSION_KEY_INVITE_TIMESTAMP, s.getInviteTimestamp());
|
||||
d.put(SESSION_KEY_ROLE, s.getRole().getValue());
|
||||
d.put(SESSION_KEY_STATE, s.getState().getValue());
|
||||
return d;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
|
||||
@NotNullByDefault
|
||||
interface SessionParser {
|
||||
|
||||
BdfDictionary getSessionQuery(SessionId s);
|
||||
|
||||
Role getRole(BdfDictionary d) throws FormatException;
|
||||
|
||||
CreatorSession parseCreatorSession(GroupId contactGroupId, BdfDictionary d)
|
||||
throws FormatException;
|
||||
|
||||
InviteeSession parseInviteeSession(GroupId contactGroupId, BdfDictionary d)
|
||||
throws FormatException;
|
||||
|
||||
PeerSession parsePeerSession(GroupId contactGroupId, BdfDictionary d)
|
||||
throws FormatException;
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
import org.briarproject.api.FormatException;
|
||||
import org.briarproject.api.clients.SessionId;
|
||||
import org.briarproject.api.data.BdfDictionary;
|
||||
import org.briarproject.api.data.BdfEntry;
|
||||
import org.briarproject.api.nullsafety.NotNullByDefault;
|
||||
import org.briarproject.api.sync.GroupId;
|
||||
import org.briarproject.api.sync.MessageId;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import javax.annotation.concurrent.Immutable;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_INVITE_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_LOCAL_MESSAGE_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LAST_REMOTE_MESSAGE_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_LOCAL_TIMESTAMP;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_PRIVATE_GROUP_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_ROLE;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_SESSION_ID;
|
||||
import static org.briarproject.privategroup.invitation.GroupInvitationConstants.SESSION_KEY_STATE;
|
||||
import static org.briarproject.privategroup.invitation.Role.CREATOR;
|
||||
import static org.briarproject.privategroup.invitation.Role.INVITEE;
|
||||
import static org.briarproject.privategroup.invitation.Role.PEER;
|
||||
|
||||
@Immutable
|
||||
@NotNullByDefault
|
||||
class SessionParserImpl implements SessionParser {
|
||||
|
||||
@Inject
|
||||
SessionParserImpl() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public BdfDictionary getSessionQuery(SessionId s) {
|
||||
return BdfDictionary.of(new BdfEntry(SESSION_KEY_SESSION_ID, s));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Role getRole(BdfDictionary d) throws FormatException {
|
||||
return Role.fromValue(d.getLong(SESSION_KEY_ROLE).intValue());
|
||||
}
|
||||
|
||||
@Override
|
||||
public CreatorSession parseCreatorSession(GroupId contactGroupId,
|
||||
BdfDictionary d) throws FormatException {
|
||||
if (getRole(d) != CREATOR) throw new IllegalArgumentException();
|
||||
return new CreatorSession(contactGroupId, getPrivateGroupId(d),
|
||||
getLastLocalMessageId(d), getLastRemoteMessageId(d),
|
||||
getLocalTimestamp(d), getInviteTimestamp(d),
|
||||
CreatorState.fromValue(getState(d)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public InviteeSession parseInviteeSession(GroupId contactGroupId,
|
||||
BdfDictionary d) throws FormatException {
|
||||
if (getRole(d) != INVITEE) throw new IllegalArgumentException();
|
||||
return new InviteeSession(contactGroupId, getPrivateGroupId(d),
|
||||
getLastLocalMessageId(d), getLastRemoteMessageId(d),
|
||||
getLocalTimestamp(d), getInviteTimestamp(d),
|
||||
InviteeState.fromValue(getState(d)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeerSession parsePeerSession(GroupId contactGroupId,
|
||||
BdfDictionary d) throws FormatException {
|
||||
if (getRole(d) != PEER) throw new IllegalArgumentException();
|
||||
return new PeerSession(contactGroupId, getPrivateGroupId(d),
|
||||
getLastLocalMessageId(d), getLastRemoteMessageId(d),
|
||||
getLocalTimestamp(d), PeerState.fromValue(getState(d)));
|
||||
}
|
||||
|
||||
private int getState(BdfDictionary d) throws FormatException {
|
||||
return d.getLong(SESSION_KEY_STATE).intValue();
|
||||
}
|
||||
|
||||
private GroupId getPrivateGroupId(BdfDictionary d) throws FormatException {
|
||||
return new GroupId(d.getRaw(SESSION_KEY_PRIVATE_GROUP_ID));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MessageId getLastLocalMessageId(BdfDictionary d)
|
||||
throws FormatException {
|
||||
byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_LOCAL_MESSAGE_ID);
|
||||
return b == null ? null : new MessageId(b);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private MessageId getLastRemoteMessageId(BdfDictionary d)
|
||||
throws FormatException {
|
||||
byte[] b = d.getOptionalRaw(SESSION_KEY_LAST_REMOTE_MESSAGE_ID);
|
||||
return b == null ? null : new MessageId(b);
|
||||
}
|
||||
|
||||
private long getLocalTimestamp(BdfDictionary d) throws FormatException {
|
||||
return d.getLong(SESSION_KEY_LOCAL_TIMESTAMP);
|
||||
}
|
||||
|
||||
private long getInviteTimestamp(BdfDictionary d) throws FormatException {
|
||||
return d.getLong(SESSION_KEY_INVITE_TIMESTAMP);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package org.briarproject.privategroup.invitation;
|
||||
|
||||
interface State {
|
||||
|
||||
int getValue();
|
||||
}
|
||||
Reference in New Issue
Block a user