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:
akwizgran
2016-11-08 17:22:00 +00:00
63 changed files with 3369 additions and 221 deletions

View File

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

View File

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

View File

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

View File

@@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +0,0 @@
package org.briarproject.api.privategroup.invitation;
public interface GroupInvitationConstants {
// Group Metadata Keys
String CONTACT_ID = "contactId";
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package org.briarproject.privategroup.invitation;
enum LocalAction {
INVITE, JOIN, LEAVE, MEMBER_ADDED
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package org.briarproject.privategroup.invitation;
interface State {
int getValue();
}