Private group invitation protocol.

This commit is contained in:
akwizgran
2016-10-21 14:37:09 +01:00
parent edbf5ff5b4
commit d2434123a9
61 changed files with 3266 additions and 214 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,7 +2,7 @@ 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;
@@ -70,8 +70,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

@@ -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;
private final IdentityManager identityManager;
private final PrivateGroupFactory privateGroupFactory;
private final GroupMessageFactory groupMessageFactory;
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,227 @@
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.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
markMessageVisibleInUi(txn, m.getId(), true);
// Start syncing the private group with the contact
syncPrivateGroupWithContact(txn, s, 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
markMessageVisibleInUi(txn, m.getId(), true);
// 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);
}
}

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,242 @@
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.contact.ContactId;
import org.briarproject.api.db.DatabaseComponent;
import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction;
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.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager;
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);
// 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);
}
}

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,282 @@
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.NEITHER_JOINED;
import static org.briarproject.privategroup.invitation.PeerState.REMOTE_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 onLocalJoinFirst(txn, s);
case REMOTE_JOINED:
return onLocalJoinSecond(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 REMOTE_JOINED:
case ERROR:
return s; // Ignored in these states
case LOCAL_JOINED:
return onLocalLeaveSecond(txn, s);
case BOTH_JOINED:
return onLocalLeaveFirst(txn, s);
default:
throw new AssertionError();
}
}
@Override
public PeerSession onMemberAddedAction(Transaction txn, PeerSession s)
throws DbException {
switch (s.getState()) {
case START:
case AWAIT_MEMBER:
return onRemoteMemberAdded(s);
case NEITHER_JOINED:
case LOCAL_JOINED:
case REMOTE_JOINED:
case BOTH_JOINED:
case ERROR:
throw new ProtocolStateException(); // Invalid in these states
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 REMOTE_JOINED:
case BOTH_JOINED:
return abort(txn, s); // Invalid in these states
case START:
case NEITHER_JOINED:
return onRemoteJoinFirst(txn, s, m);
case LOCAL_JOINED:
return onRemoteJoinSecond(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);
case AWAIT_MEMBER:
case REMOTE_JOINED:
return onRemoteLeaveSecond(txn, s, m);
case BOTH_JOINED:
return onRemoteLeaveFirst(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 onLocalJoinFirst(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 onLocalJoinSecond(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 onLocalLeaveFirst(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 REMOTE_JOINED state
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(),
REMOTE_JOINED);
}
private PeerSession onLocalLeaveSecond(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 onRemoteMemberAdded(PeerSession s) {
// Move to the NEITHER_JOINED or REMOTE_JOINED state
PeerState next = s.getState() == START ? NEITHER_JOINED : REMOTE_JOINED;
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
s.getLastLocalMessageId(), s.getLastRemoteMessageId(),
s.getLocalTimestamp(), next);
}
private PeerSession onRemoteJoinFirst(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 or REMOTE_JOINED state
PeerState next = s.getState() == START ? AWAIT_MEMBER : REMOTE_JOINED;
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
next);
}
private PeerSession onRemoteJoinSecond(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 onRemoteLeaveSecond(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 or NEITHER_JOINED state
PeerState next = s.getState() == AWAIT_MEMBER ? START : NEITHER_JOINED;
return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(),
s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(),
next);
}
private PeerSession onRemoteLeaveFirst(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),
REMOTE_JOINED(4), BOTH_JOINED(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();
}