diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java index 38ace43a3eae587d873785d1c7175185b17b1706..10d4280f41dcdc6634c535bd02d0ccf56abae2b3 100644 --- a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java @@ -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; diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java index 68191f4f87b7099a9723749fb5d12184a050ab6a..08bdfd8a6b0dc1951a2a5ae0aacfa8ea46334fbc 100644 --- a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java +++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTestComponent.java @@ -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, diff --git a/briar-android/src/org/briarproject/android/AndroidComponent.java b/briar-android/src/org/briarproject/android/AndroidComponent.java index a436f9430fc9dc9d0e6b3582b8ed87f928160220..0a8feb91110a925c60cc88d00cc3319d13d1c844 100644 --- a/briar-android/src/org/briarproject/android/AndroidComponent.java +++ b/briar-android/src/org/briarproject/android/AndroidComponent.java @@ -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(); diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index 849a8222f5a352194ab4341a17ec76a7370ce1af..c8eb36da9d9754558ff07256ddfac244fde64690 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -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() { diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/BaseGroupInviteActivity.java b/briar-android/src/org/briarproject/android/privategroup/creation/BaseGroupInviteActivity.java index 44c57cb14392a6e28a1d49c25580c43e42b684b5..895f6188f7f83a81404066958d1299c0caf5f50d 100644 --- a/briar-android/src/org/briarproject/android/privategroup/creation/BaseGroupInviteActivity.java +++ b/briar-android/src/org/briarproject/android/privategroup/creation/BaseGroupInviteActivity.java @@ -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(); } diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java index da1a00a0d1275559ebdb13272a91d626ef27fffb..cc46f6e47a2cd675f6a69cef72cd6f3ae58083ce 100644 --- a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java +++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupController.java @@ -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, diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java index b5632e9bf35d0aaa14cf2e40ae3bdea038ea2d61..b37fc5e499829e0ef914a95937c225b28eabc3f2 100644 --- a/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java +++ b/briar-android/src/org/briarproject/android/privategroup/creation/CreateGroupControllerImpl.java @@ -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() { + 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() { - // TODO actually send invitation - //noinspection ConstantConditions - result.onResult(null); + 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; + } + } } diff --git a/briar-android/src/org/briarproject/android/privategroup/creation/GroupInviteActivity.java b/briar-android/src/org/briarproject/android/privategroup/creation/GroupInviteActivity.java index 7872c34cb72acc5abe01615d2f5d234ae7a85da6..abc776f5136ccecc338f95bed91610c9aec0de5b 100644 --- a/briar-android/src/org/briarproject/android/privategroup/creation/GroupInviteActivity.java +++ b/briar-android/src/org/briarproject/android/privategroup/creation/GroupInviteActivity.java @@ -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); } } diff --git a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java index 9dba77d895f3e4bc009d4c4e1b90c6748c0ebeb6..d77f04c25d8293f1233a296df8ae8ca1267eb7ae 100644 --- a/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java +++ b/briar-android/src/org/briarproject/android/privategroup/invitation/GroupInvitationControllerImpl.java @@ -2,12 +2,13 @@ package org.briarproject.android.privategroup.invitation; import org.briarproject.android.controller.handler.ResultExceptionHandler; import org.briarproject.android.sharing.InvitationControllerImpl; -import org.briarproject.api.contact.Contact; +import org.briarproject.api.contact.ContactId; import org.briarproject.api.db.DatabaseExecutor; import org.briarproject.api.db.DbException; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; -import org.briarproject.api.event.GroupInvitationReceivedEvent; +import org.briarproject.api.event.GroupInvitationRequestReceivedEvent; +import org.briarproject.api.event.GroupInvitationResponseReceivedEvent; import org.briarproject.api.lifecycle.LifecycleManager; import org.briarproject.api.privategroup.PrivateGroup; import org.briarproject.api.privategroup.PrivateGroupManager; @@ -44,8 +45,11 @@ public class GroupInvitationControllerImpl public void eventOccurred(Event e) { super.eventOccurred(e); - if (e instanceof GroupInvitationReceivedEvent) { - LOG.info("Group invitation received, reloading"); + if (e instanceof GroupInvitationRequestReceivedEvent) { + LOG.info("Group invitation request received, reloading"); + listener.loadInvitations(false); + } else if (e instanceof GroupInvitationResponseReceivedEvent) { + LOG.info("Group invitation response received, reloading"); listener.loadInvitations(false); } } @@ -70,8 +74,8 @@ public class GroupInvitationControllerImpl public void run() { try { PrivateGroup g = item.getShareable(); - Contact c = item.getCreator(); - groupInvitationManager.respondToInvitation(g, c, accept); + ContactId c = item.getCreator().getId(); + groupInvitationManager.respondToInvitation(c, g, accept); } catch (DbException e) { if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java index f464b598027ddd872b0730e41f85dde8fe0f18db..d2dc087f3b9a33a94740cca9322dd77926dfb4de 100644 --- a/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java +++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupListControllerImpl.java @@ -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) { diff --git a/briar-api/src/org/briarproject/api/event/GroupInvitationReceivedEvent.java b/briar-api/src/org/briarproject/api/event/GroupInvitationRequestReceivedEvent.java similarity index 58% rename from briar-api/src/org/briarproject/api/event/GroupInvitationReceivedEvent.java rename to briar-api/src/org/briarproject/api/event/GroupInvitationRequestReceivedEvent.java index c8244a39248f621d4a829002afb232eed4f7208c..1fc2599c22f77ee4183c7da1836caa31b5a57af8 100644 --- a/briar-api/src/org/briarproject/api/event/GroupInvitationReceivedEvent.java +++ b/briar-api/src/org/briarproject/api/event/GroupInvitationRequestReceivedEvent.java @@ -1,15 +1,14 @@ package org.briarproject.api.event; import org.briarproject.api.contact.ContactId; -import org.briarproject.api.forum.ForumInvitationRequest; import org.briarproject.api.privategroup.PrivateGroup; import org.briarproject.api.privategroup.invitation.GroupInvitationRequest; -public class GroupInvitationReceivedEvent extends +public class GroupInvitationRequestReceivedEvent extends InvitationRequestReceivedEvent<PrivateGroup> { - public GroupInvitationReceivedEvent(PrivateGroup group, ContactId contactId, - GroupInvitationRequest request) { + public GroupInvitationRequestReceivedEvent(PrivateGroup group, + ContactId contactId, GroupInvitationRequest request) { super(group, contactId, request); } diff --git a/briar-api/src/org/briarproject/api/event/GroupInvitationResponseReceivedEvent.java b/briar-api/src/org/briarproject/api/event/GroupInvitationResponseReceivedEvent.java new file mode 100644 index 0000000000000000000000000000000000000000..a72fd313af7b3899b85fc7539b756270456f99cd --- /dev/null +++ b/briar-api/src/org/briarproject/api/event/GroupInvitationResponseReceivedEvent.java @@ -0,0 +1,13 @@ +package org.briarproject.api.event; + +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.sharing.InvitationResponse; + +public class GroupInvitationResponseReceivedEvent + extends InvitationResponseReceivedEvent { + + public GroupInvitationResponseReceivedEvent(ContactId contactId, + InvitationResponse response) { + super(contactId, response); + } +} diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java index 85b190452ffd875010d07b2f1a416f12e11ed46f..4a96a843b999537f809ca6fa99a33c69aab5cdab 100644 --- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java +++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java @@ -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 diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationConstants.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationConstants.java deleted file mode 100644 index a22272b22b2f3bb4549e7abebc4e65dc2677694a..0000000000000000000000000000000000000000 --- a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationConstants.java +++ /dev/null @@ -1,8 +0,0 @@ -package org.briarproject.api.privategroup.invitation; - -public interface GroupInvitationConstants { - - // Group Metadata Keys - String CONTACT_ID = "contactId"; - -} diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationFactory.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e5cf41adf714a6e73ee0a36bdac94e3305a2b596 --- /dev/null +++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationFactory.java @@ -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); +} diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java index 5cb23afee77575127930ea215d866d8179995fd9..7fb91d6acd6689d80c9bdecc717d97ff423e1f51 100644 --- a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java +++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationItem.java @@ -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; } diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java index 745b69fc8cc5a34872391628e7044c051064ad54..489e51a5917bc5f477cef44bd6ff2d71d82d9218 100644 --- a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java +++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationManager.java @@ -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; } diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java index 3115a6bc27d49a8c32e1ef28c03afc4b1cca4028..d98bcad2e187deefa1214ebebe2ead758ea53890 100644 --- a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java +++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationRequest.java @@ -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); diff --git a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java index dda5d554ac6ce1b8b78cc8a68f271038a695b457..ba41a8b25ef0c67f581630c94ff6dcb2a18aa0d4 100644 --- a/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java +++ b/briar-api/src/org/briarproject/api/privategroup/invitation/GroupInvitationResponse.java @@ -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; - } - } diff --git a/briar-core/src/org/briarproject/CoreEagerSingletons.java b/briar-core/src/org/briarproject/CoreEagerSingletons.java index b4b7402dd9b7fb265ab9aa30c887f8f4089f3da4..314543a31374e4701c6cd3cfd40e0666330294c9 100644 --- a/briar-core/src/org/briarproject/CoreEagerSingletons.java +++ b/briar-core/src/org/briarproject/CoreEagerSingletons.java @@ -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); diff --git a/briar-core/src/org/briarproject/CoreModule.java b/briar-core/src/org/briarproject/CoreModule.java index 82754d66880e509cd1d3dbdafd78a4a6579b8a4f..aee24de22f3d4264c2c88f63bd528a6aa01ce97d 100644 --- a/briar-core/src/org/briarproject/CoreModule.java +++ b/briar-core/src/org/briarproject/CoreModule.java @@ -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()); diff --git a/briar-core/src/org/briarproject/privategroup/Constants.java b/briar-core/src/org/briarproject/privategroup/GroupConstants.java similarity index 100% rename from briar-core/src/org/briarproject/privategroup/Constants.java rename to briar-core/src/org/briarproject/privategroup/GroupConstants.java diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java index e1613b04562ee4a6d94387ffc32438abe0f98369..dda084d2e1e4fa7c13495468ab036dcf15692ee2 100644 --- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java +++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java @@ -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); } diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java index d7f705eb2ed2bee7edee7b241b6587511065cd94..b6117f65f198a4029d1eb8021069c031cacf3366 100644 --- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java +++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java @@ -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 { diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java index b4c80cbabd410decd01958419c5a005f1700bcd4..0ec50e101c8fab0ccd3cb4be5542e6f8ee3905aa 100644 --- a/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java +++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupModule.java @@ -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; - } - } diff --git a/briar-core/src/org/briarproject/privategroup/invitation/AbortMessage.java b/briar-core/src/org/briarproject/privategroup/invitation/AbortMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..7dd247ee1c18eafcf8f664476b4c2ec10044dc21 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/AbortMessage.java @@ -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); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..b3048c7779a76b9e878f5cbe0037fb3769ace04c --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java @@ -0,0 +1,215 @@ +package org.briarproject.privategroup.invitation; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.data.BdfDictionary; +import org.briarproject.api.data.BdfList; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.identity.LocalAuthor; +import org.briarproject.api.nullsafety.NotNullByDefault; +import org.briarproject.api.privategroup.GroupMessage; +import org.briarproject.api.privategroup.GroupMessageFactory; +import org.briarproject.api.privategroup.PrivateGroup; +import org.briarproject.api.privategroup.PrivateGroupFactory; +import org.briarproject.api.privategroup.PrivateGroupManager; +import org.briarproject.api.sync.Group; +import org.briarproject.api.sync.GroupId; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.system.Clock; + +import java.util.Map; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.privategroup.invitation.GroupInvitationConstants.GROUP_KEY_CONTACT_ID; +import static org.briarproject.privategroup.invitation.MessageType.ABORT; +import static org.briarproject.privategroup.invitation.MessageType.INVITE; +import static org.briarproject.privategroup.invitation.MessageType.JOIN; +import static org.briarproject.privategroup.invitation.MessageType.LEAVE; + +@Immutable +@NotNullByDefault +abstract class AbstractProtocolEngine<S extends Session> + implements ProtocolEngine<S> { + + protected final DatabaseComponent db; + protected final ClientHelper clientHelper; + protected final PrivateGroupManager privateGroupManager; + protected final PrivateGroupFactory privateGroupFactory; + + private final GroupMessageFactory groupMessageFactory; + private final IdentityManager identityManager; + private final MessageParser messageParser; + private final MessageEncoder messageEncoder; + private final Clock clock; + + AbstractProtocolEngine(DatabaseComponent db, ClientHelper clientHelper, + PrivateGroupManager privateGroupManager, + PrivateGroupFactory privateGroupFactory, + GroupMessageFactory groupMessageFactory, + IdentityManager identityManager, MessageParser messageParser, + MessageEncoder messageEncoder, Clock clock) { + this.db = db; + this.clientHelper = clientHelper; + this.privateGroupManager = privateGroupManager; + this.privateGroupFactory = privateGroupFactory; + this.groupMessageFactory = groupMessageFactory; + this.identityManager = identityManager; + this.messageParser = messageParser; + this.messageEncoder = messageEncoder; + this.clock = clock; + } + + ContactId getContactId(Transaction txn, GroupId contactGroupId) + throws DbException, FormatException { + BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, + contactGroupId); + return new ContactId(meta.getLong(GROUP_KEY_CONTACT_ID).intValue()); + } + + boolean isSubscribedPrivateGroup(Transaction txn, GroupId g) + throws DbException { + if (!db.containsGroup(txn, g)) return false; + Group group = db.getGroup(txn, g); + return group.getClientId().equals(PrivateGroupManager.CLIENT_ID); + } + + boolean isValidDependency(S session, @Nullable MessageId dependency) { + MessageId expected = session.getLastRemoteMessageId(); + if (dependency == null) return expected == null; + return dependency.equals(expected); + } + + void syncPrivateGroupWithContact(Transaction txn, S session, boolean sync) + throws DbException, FormatException { + ContactId contactId = getContactId(txn, session.getContactGroupId()); + db.setVisibleToContact(txn, contactId, session.getPrivateGroupId(), + sync); + } + + Message sendInviteMessage(Transaction txn, S session, + @Nullable String message, long timestamp, byte[] signature) + throws DbException { + Group g = db.getGroup(txn, session.getPrivateGroupId()); + PrivateGroup privateGroup; + try { + privateGroup = privateGroupFactory.parsePrivateGroup(g); + } catch (FormatException e) { + throw new DbException(e); // Invalid group descriptor + } + Message m = messageEncoder.encodeInviteMessage( + session.getContactGroupId(), privateGroup.getId(), + timestamp, privateGroup.getName(), privateGroup.getAuthor(), + privateGroup.getSalt(), message, signature); + sendMessage(txn, m, INVITE, privateGroup.getId(), true); + return m; + } + + Message sendJoinMessage(Transaction txn, S session, boolean visibleInUi) + throws DbException { + Message m = messageEncoder.encodeJoinMessage( + session.getContactGroupId(), session.getPrivateGroupId(), + getLocalTimestamp(session), session.getLastLocalMessageId()); + sendMessage(txn, m, JOIN, session.getPrivateGroupId(), visibleInUi); + return m; + } + + Message sendLeaveMessage(Transaction txn, S session, boolean visibleInUi) + throws DbException { + Message m = messageEncoder.encodeLeaveMessage( + session.getContactGroupId(), session.getPrivateGroupId(), + getLocalTimestamp(session), session.getLastLocalMessageId()); + sendMessage(txn, m, LEAVE, session.getPrivateGroupId(), visibleInUi); + return m; + } + + Message sendAbortMessage(Transaction txn, S session) throws DbException { + Message m = messageEncoder.encodeAbortMessage( + session.getContactGroupId(), session.getPrivateGroupId(), + getLocalTimestamp(session)); + sendMessage(txn, m, ABORT, session.getPrivateGroupId(), false); + return m; + } + + void markMessageVisibleInUi(Transaction txn, MessageId m, boolean visible) + throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setVisibleInUi(meta, visible); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + void markMessageAvailableToAnswer(Transaction txn, MessageId m, + boolean available) throws DbException { + BdfDictionary meta = new BdfDictionary(); + messageEncoder.setAvailableToAnswer(meta, available); + try { + clientHelper.mergeMessageMetadata(txn, m, meta); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + + void markInvitesUnavailableToAnswer(Transaction txn, S session) + throws DbException, FormatException { + GroupId privateGroupId = session.getPrivateGroupId(); + BdfDictionary query = + messageParser.getInvitesAvailableToAnswerQuery(privateGroupId); + Map<MessageId, BdfDictionary> results = + clientHelper.getMessageMetadataAsDictionary(txn, + session.getContactGroupId(), query); + for (MessageId m : results.keySet()) + markMessageAvailableToAnswer(txn, m, false); + } + + void subscribeToPrivateGroup(Transaction txn, MessageId inviteId) + throws DbException, FormatException { + InviteMessage invite = getInviteMessage(txn, inviteId); + PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup( + invite.getGroupName(), invite.getCreator(), invite.getSalt()); + long timestamp = + Math.max(clock.currentTimeMillis(), invite.getTimestamp() + 1); + // TODO: Create the join message on the crypto executor + LocalAuthor member = identityManager.getLocalAuthor(txn); + GroupMessage joinMessage = groupMessageFactory.createJoinMessage( + privateGroup.getId(), timestamp, member, invite.getTimestamp(), + invite.getSignature()); + privateGroupManager.addPrivateGroup(txn, privateGroup, joinMessage); + } + + long getLocalTimestamp(S session) { + return Math.max(clock.currentTimeMillis(), + Math.max(session.getLocalTimestamp(), + session.getInviteTimestamp()) + 1); + } + + private InviteMessage getInviteMessage(Transaction txn, MessageId m) + throws DbException, FormatException { + Message message = clientHelper.getMessage(txn, m); + if (message == null) throw new DbException(); + BdfList body = clientHelper.toList(message); + return messageParser.parseInviteMessage(message, body); + } + + private void sendMessage(Transaction txn, Message m, MessageType type, + GroupId privateGroupId, boolean visibleInConversation) + throws DbException { + BdfDictionary meta = messageEncoder.encodeMetadata(type, privateGroupId, + m.getTimestamp(), true, true, visibleInConversation, false); + try { + clientHelper.addLocalMessage(txn, m, meta, true); + } catch (FormatException e) { + throw new AssertionError(e); + } + } + +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..e3e6c0540443c144196cafced5c7fd85ac70ce5c --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/CreatorProtocolEngine.java @@ -0,0 +1,247 @@ +package org.briarproject.privategroup.invitation; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.ProtocolStateException; +import org.briarproject.api.clients.SessionId; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.event.GroupInvitationResponseReceivedEvent; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.nullsafety.NotNullByDefault; +import org.briarproject.api.privategroup.GroupMessageFactory; +import org.briarproject.api.privategroup.PrivateGroupFactory; +import org.briarproject.api.privategroup.PrivateGroupManager; +import org.briarproject.api.privategroup.invitation.GroupInvitationResponse; +import org.briarproject.api.sync.Message; +import org.briarproject.api.system.Clock; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.privategroup.invitation.CreatorState.DISSOLVED; +import static org.briarproject.privategroup.invitation.CreatorState.ERROR; +import static org.briarproject.privategroup.invitation.CreatorState.INVITED; +import static org.briarproject.privategroup.invitation.CreatorState.INVITEE_JOINED; +import static org.briarproject.privategroup.invitation.CreatorState.INVITEE_LEFT; +import static org.briarproject.privategroup.invitation.CreatorState.START; + +@Immutable +@NotNullByDefault +class CreatorProtocolEngine extends AbstractProtocolEngine<CreatorSession> { + + CreatorProtocolEngine(DatabaseComponent db, ClientHelper clientHelper, + PrivateGroupManager privateGroupManager, + PrivateGroupFactory privateGroupFactory, + GroupMessageFactory groupMessageFactory, + IdentityManager identityManager, MessageParser messageParser, + MessageEncoder messageEncoder, Clock clock) { + super(db, clientHelper, privateGroupManager, privateGroupFactory, + groupMessageFactory, identityManager, messageParser, + messageEncoder, clock); + } + + @Override + public CreatorSession onInviteAction(Transaction txn, CreatorSession s, + @Nullable String message, long timestamp, byte[] signature) + throws DbException { + switch (s.getState()) { + case START: + return onLocalInvite(txn, s, message, timestamp, signature); + case INVITED: + case INVITEE_JOINED: + case INVITEE_LEFT: + case DISSOLVED: + case ERROR: + throw new ProtocolStateException(); // Invalid in these states + default: + throw new AssertionError(); + } + } + + @Override + public CreatorSession onJoinAction(Transaction txn, CreatorSession s) + throws DbException { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public CreatorSession onLeaveAction(Transaction txn, CreatorSession s) + throws DbException { + switch (s.getState()) { + case START: + case DISSOLVED: + case ERROR: + return s; // Ignored in these states + case INVITED: + case INVITEE_JOINED: + case INVITEE_LEFT: + return onLocalLeave(txn, s); + default: + throw new AssertionError(); + } + } + + @Override + public CreatorSession onMemberAddedAction(Transaction txn, CreatorSession s) + throws DbException { + return s; // Ignored in this role + } + + @Override + public CreatorSession onInviteMessage(Transaction txn, CreatorSession s, + InviteMessage m) throws DbException, FormatException { + return abort(txn, s); // Invalid in this role + } + + @Override + public CreatorSession onJoinMessage(Transaction txn, CreatorSession s, + JoinMessage m) throws DbException, FormatException { + switch (s.getState()) { + case START: + case INVITEE_JOINED: + case INVITEE_LEFT: + return abort(txn, s); // Invalid in these states + case INVITED: + return onRemoteAccept(txn, s, m); + case DISSOLVED: + case ERROR: + return s; // Ignored in these states + default: + throw new AssertionError(); + } + } + + @Override + public CreatorSession onLeaveMessage(Transaction txn, CreatorSession s, + LeaveMessage m) throws DbException, FormatException { + switch (s.getState()) { + case START: + case INVITEE_LEFT: + return abort(txn, s); // Invalid in these states + case INVITED: + return onRemoteDecline(txn, s, m); + case INVITEE_JOINED: + return onRemoteLeave(txn, s, m); + case DISSOLVED: + case ERROR: + return s; // Ignored in these states + default: + throw new AssertionError(); + } + } + + @Override + public CreatorSession onAbortMessage(Transaction txn, CreatorSession s, + AbortMessage m) throws DbException, FormatException { + return abort(txn, s); + } + + private CreatorSession onLocalInvite(Transaction txn, CreatorSession s, + @Nullable String message, long timestamp, byte[] signature) + throws DbException { + // Send an INVITE message + Message sent = sendInviteMessage(txn, s, message, timestamp, signature); + long localTimestamp = Math.max(timestamp, getLocalTimestamp(s)); + // Move to the INVITED state + return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), localTimestamp, + timestamp, INVITED); + } + + private CreatorSession onLocalLeave(Transaction txn, CreatorSession s) + throws DbException { + try { + // Stop syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, false); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Send a LEAVE message + Message sent = sendLeaveMessage(txn, s, false); + // Move to the DISSOLVED state + return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp(), DISSOLVED); + } + + private CreatorSession onRemoteAccept(Transaction txn, CreatorSession s, + JoinMessage m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getId(), true); + // Start syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, true); + // Broadcast an event + ContactId contactId = getContactId(txn, m.getContactGroupId()); + txn.attach(new GroupInvitationResponseReceivedEvent(contactId, + createInvitationResponse(m, contactId, true))); + // Move to the INVITEE_JOINED state + return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp(), INVITEE_JOINED); + } + + private CreatorSession onRemoteDecline(Transaction txn, CreatorSession s, + LeaveMessage m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Mark the response visible in the UI + markMessageVisibleInUi(txn, m.getId(), true); + // Broadcast an event + ContactId contactId = getContactId(txn, m.getContactGroupId()); + txn.attach(new GroupInvitationResponseReceivedEvent(contactId, + createInvitationResponse(m, contactId, false))); + // Move to the START state + return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp(), START); + } + + private CreatorSession onRemoteLeave(Transaction txn, CreatorSession s, + LeaveMessage m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Stop syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, false); + // Move to the INVITEE_LEFT state + return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp(), INVITEE_LEFT); + } + + private CreatorSession abort(Transaction txn, CreatorSession s) + throws DbException, FormatException { + // If the session has already been aborted, do nothing + if (s.getState() == ERROR) return s; + // If we subscribe, stop syncing the private group with the contact + if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId())) + syncPrivateGroupWithContact(txn, s, false); + // Send an ABORT message + Message sent = sendAbortMessage(txn, s); + // Move to the ERROR state + return new CreatorSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp(), ERROR); + } + + private GroupInvitationResponse createInvitationResponse( + GroupInvitationMessage m, ContactId c, boolean accept) { + SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes()); + return new GroupInvitationResponse(m.getId(), sessionId, + m.getContactGroupId(), c, accept, m.getTimestamp(), false, + false, true, false); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/CreatorSession.java b/briar-core/src/org/briarproject/privategroup/invitation/CreatorSession.java new file mode 100644 index 0000000000000000000000000000000000000000..62b6a05f730826a3b1cfb1fa24b9664b6b3d3509 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/CreatorSession.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/CreatorState.java b/briar-core/src/org/briarproject/privategroup/invitation/CreatorState.java new file mode 100644 index 0000000000000000000000000000000000000000..3a54b00a37bc65e52e99f303547a5360c766e5bc --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/CreatorState.java @@ -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(); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationConstants.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..7cfe17f2dd09dcf92e00e78d75cc7fd48f281872 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationConstants.java @@ -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"; +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationFactoryImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3e9b8c88c67aaab47b7d9ab8c56e491c4ae93109 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationFactoryImpl.java @@ -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 + ); + } + +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java index f7dba6e1f1e7955c5514f7b0f0608da6399814e7..ae11b48b850f13154046c447614061e910e8dd4c 100644 --- a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java +++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationManagerImpl.java @@ -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; } + private SessionId getSessionId(GroupId privateGroupId) { + return new SessionId(privateGroupId.getBytes()); + } + + @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 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 void respondToInvitation(ContactId c, PrivateGroup g, boolean accept) + throws DbException { + respondToInvitation(c, getSessionId(g.getId()), accept); + } + @Override - public void sendInvitation(GroupId groupId, ContactId contactId, - String message) throws DbException { + 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 void respondToInvitation(PrivateGroup g, Contact c, boolean accept) + 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 { + 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 void respondToInvitation(SessionId id, boolean accept) + 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 Collection<InvitationMessage> getInvitationMessages( - ContactId contactId) throws DbException { - Collection<InvitationMessage> invitations = - new ArrayList<InvitationMessage>(); + 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); + } - return invitations; + 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 Collection<GroupInvitationItem> getInvitations() throws DbException { - Collection<GroupInvitationItem> invitations = - new ArrayList<GroupInvitationItem>(); + 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); + } + } - return invitations; + 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; + } + } } diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationMessage.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..9ab719cb04d10317143ea76b8a8276f803a014d4 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationMessage.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationModule.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationModule.java new file mode 100644 index 0000000000000000000000000000000000000000..5e2733806dcbf45fe00858719cae408933446b17 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationModule.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationValidator.java b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationValidator.java new file mode 100644 index 0000000000000000000000000000000000000000..15de431499b54f4c3abd969a6626ccf348d392cb --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/GroupInvitationValidator.java @@ -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); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteAction.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteAction.java new file mode 100644 index 0000000000000000000000000000000000000000..f43af230a80b3b2be49176e91fbe2770d8d5e761 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteAction.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteMessage.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..b286e8c09cb1f894678228a0c75986b809b3ae5b --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteMessage.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..1526e23bd923ac65e3c4257d6f68a01643379e0a --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteeProtocolEngine.java @@ -0,0 +1,260 @@ +package org.briarproject.privategroup.invitation; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.ProtocolStateException; +import org.briarproject.api.clients.SessionId; +import org.briarproject.api.contact.ContactId; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.event.GroupInvitationRequestReceivedEvent; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.nullsafety.NotNullByDefault; +import org.briarproject.api.privategroup.GroupMessageFactory; +import org.briarproject.api.privategroup.PrivateGroup; +import org.briarproject.api.privategroup.PrivateGroupFactory; +import org.briarproject.api.privategroup.PrivateGroupManager; +import org.briarproject.api.privategroup.invitation.GroupInvitationRequest; +import org.briarproject.api.sync.Message; +import org.briarproject.api.sync.MessageId; +import org.briarproject.api.system.Clock; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.privategroup.invitation.InviteeState.DISSOLVED; +import static org.briarproject.privategroup.invitation.InviteeState.ERROR; +import static org.briarproject.privategroup.invitation.InviteeState.INVITED; +import static org.briarproject.privategroup.invitation.InviteeState.INVITEE_JOINED; +import static org.briarproject.privategroup.invitation.InviteeState.INVITEE_LEFT; +import static org.briarproject.privategroup.invitation.InviteeState.START; + +@Immutable +@NotNullByDefault +class InviteeProtocolEngine extends AbstractProtocolEngine<InviteeSession> { + + InviteeProtocolEngine(DatabaseComponent db, ClientHelper clientHelper, + PrivateGroupManager privateGroupManager, + PrivateGroupFactory privateGroupFactory, + GroupMessageFactory groupMessageFactory, + IdentityManager identityManager, MessageParser messageParser, + MessageEncoder messageEncoder, Clock clock) { + super(db, clientHelper, privateGroupManager, privateGroupFactory, + groupMessageFactory, identityManager, messageParser, + messageEncoder, clock); + } + + @Override + public InviteeSession onInviteAction(Transaction txn, InviteeSession s, + @Nullable String message, long timestamp, byte[] signature) + throws DbException { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public InviteeSession onJoinAction(Transaction txn, InviteeSession s) + throws DbException { + switch (s.getState()) { + case START: + case INVITEE_JOINED: + case INVITEE_LEFT: + case DISSOLVED: + case ERROR: + throw new ProtocolStateException(); // Invalid in these states + case INVITED: + return onLocalAccept(txn, s); + default: + throw new AssertionError(); + } + } + + @Override + public InviteeSession onLeaveAction(Transaction txn, InviteeSession s) + throws DbException { + switch (s.getState()) { + case START: + case INVITEE_LEFT: + case DISSOLVED: + case ERROR: + return s; // Ignored in these states + case INVITED: + return onLocalDecline(txn, s); + case INVITEE_JOINED: + return onLocalLeave(txn, s); + default: + throw new AssertionError(); + } + } + + @Override + public InviteeSession onMemberAddedAction(Transaction txn, InviteeSession s) + throws DbException { + return s; // Ignored in this role + } + + @Override + public InviteeSession onInviteMessage(Transaction txn, InviteeSession s, + InviteMessage m) throws DbException, FormatException { + switch (s.getState()) { + case START: + return onRemoteInvite(txn, s, m); + case INVITED: + case INVITEE_JOINED: + case INVITEE_LEFT: + case DISSOLVED: + return abort(txn, s); // Invalid in these states + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + @Override + public InviteeSession onJoinMessage(Transaction txn, InviteeSession s, + JoinMessage m) throws DbException, FormatException { + return abort(txn, s); // Invalid in this role + } + + @Override + public InviteeSession onLeaveMessage(Transaction txn, InviteeSession s, + LeaveMessage m) throws DbException, FormatException { + switch (s.getState()) { + case START: + case DISSOLVED: + return abort(txn, s); // Invalid in these states + case INVITED: + case INVITEE_JOINED: + case INVITEE_LEFT: + return onRemoteLeave(txn, s, m); + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + @Override + public InviteeSession onAbortMessage(Transaction txn, InviteeSession s, + AbortMessage m) throws DbException, FormatException { + return abort(txn, s); + } + + private InviteeSession onLocalAccept(Transaction txn, InviteeSession s) + throws DbException { + // Mark the invite message unavailable to answer + MessageId inviteId = s.getLastRemoteMessageId(); + if (inviteId == null) throw new IllegalStateException(); + markMessageAvailableToAnswer(txn, inviteId, false); + // Send a JOIN message + Message sent = sendJoinMessage(txn, s, true); + try { + // Subscribe to the private group + subscribeToPrivateGroup(txn, inviteId); + // Start syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, true); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Move to the INVITEE_JOINED state + return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp(), INVITEE_JOINED); + } + + private InviteeSession onLocalDecline(Transaction txn, InviteeSession s) + throws DbException { + // Mark the invite message unavailable to answer + MessageId inviteId = s.getLastRemoteMessageId(); + if (inviteId == null) throw new IllegalStateException(); + markMessageAvailableToAnswer(txn, inviteId, false); + // Send a LEAVE message + Message sent = sendLeaveMessage(txn, s, true); + // Move to the START state + return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp(), START); + } + + private InviteeSession onLocalLeave(Transaction txn, InviteeSession s) + throws DbException { + // Send a LEAVE message + Message sent = sendLeaveMessage(txn, s, false); + // Move to the INVITEE_LEFT state + return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp(), INVITEE_LEFT); + } + + private InviteeSession onRemoteInvite(Transaction txn, InviteeSession s, + InviteMessage m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message, if any + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // Check that the contact is the creator + ContactId contactId = getContactId(txn, s.getContactGroupId()); + Author contact = db.getContact(txn, contactId).getAuthor(); + if (!contact.getId().equals(m.getCreator().getId())) + return abort(txn, s); + // Mark the invite message visible in the UI and available to answer + markMessageVisibleInUi(txn, m.getId(), true); + markMessageAvailableToAnswer(txn, m.getId(), true); + // Broadcast an event + PrivateGroup privateGroup = privateGroupFactory.createPrivateGroup( + m.getGroupName(), m.getCreator(), m.getSalt()); + txn.attach(new GroupInvitationRequestReceivedEvent(privateGroup, + contactId, createInvitationRequest(m, contactId))); + // Move to the INVITED state + return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + m.getTimestamp(), INVITED); + } + + private InviteeSession onRemoteLeave(Transaction txn, InviteeSession s, + LeaveMessage m) throws DbException, FormatException { + // The timestamp must be higher than the last invite message, if any + if (m.getTimestamp() <= s.getInviteTimestamp()) return abort(txn, s); + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + try { + // Stop syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, false); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Mark the group dissolved + privateGroupManager.markGroupDissolved(txn, s.getPrivateGroupId()); + // Move to the DISSOLVED state + return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + s.getInviteTimestamp(), DISSOLVED); + } + + private InviteeSession abort(Transaction txn, InviteeSession s) + throws DbException, FormatException { + // If the session has already been aborted, do nothing + if (s.getState() == ERROR) return s; + // Mark any invite messages in the session unavailable to answer + markInvitesUnavailableToAnswer(txn, s); + // Stop syncing the private group with the contact, if we subscribe + if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId())) + syncPrivateGroupWithContact(txn, s, false); + // Send an ABORT message + Message sent = sendAbortMessage(txn, s); + // Move to the ERROR state + return new InviteeSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + s.getInviteTimestamp(), ERROR); + } + + private GroupInvitationRequest createInvitationRequest(InviteMessage m, + ContactId c) { + SessionId sessionId = new SessionId(m.getPrivateGroupId().getBytes()); + return new GroupInvitationRequest(m.getId(), sessionId, + m.getContactGroupId(), c, m.getMessage(), m.getGroupName(), + m.getCreator(), true, m.getTimestamp(), false, false, true, + false); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteeSession.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteeSession.java new file mode 100644 index 0000000000000000000000000000000000000000..12730ccbdccd982bdd8bb7d02a472df48ab0aa84 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteeSession.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/InviteeState.java b/briar-core/src/org/briarproject/privategroup/invitation/InviteeState.java new file mode 100644 index 0000000000000000000000000000000000000000..97e6480a5d04a9b16846b8580fc55b19e1e34f6c --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/InviteeState.java @@ -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(); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/JoinMessage.java b/briar-core/src/org/briarproject/privategroup/invitation/JoinMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..7b9c7df179d3163dfcb14bb9e08470d02d5c046f --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/JoinMessage.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/LeaveMessage.java b/briar-core/src/org/briarproject/privategroup/invitation/LeaveMessage.java new file mode 100644 index 0000000000000000000000000000000000000000..86abb687e49609ae73512b566f2e4bb1dd59531a --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/LeaveMessage.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/LocalAction.java b/briar-core/src/org/briarproject/privategroup/invitation/LocalAction.java new file mode 100644 index 0000000000000000000000000000000000000000..3d2532ab1878c97b628fffed51ecf866bfbc4bf2 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/LocalAction.java @@ -0,0 +1,6 @@ +package org.briarproject.privategroup.invitation; + +enum LocalAction { + + INVITE, JOIN, LEAVE, MEMBER_ADDED +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/MessageEncoder.java b/briar-core/src/org/briarproject/privategroup/invitation/MessageEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..7ced5fbaa12a1cf2c2245aa408deb7c9f4fad8d6 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/MessageEncoder.java @@ -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); +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/MessageEncoderImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/MessageEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..18180fe81a998e4fcaa02a7d63ba61195585fa24 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/MessageEncoderImpl.java @@ -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); + } + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/MessageMetadata.java b/briar-core/src/org/briarproject/privategroup/invitation/MessageMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..503a5e79db8e1a3e5e0cefd7f3ba4650c45ee9ac --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/MessageMetadata.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/MessageParser.java b/briar-core/src/org/briarproject/privategroup/invitation/MessageParser.java new file mode 100644 index 0000000000000000000000000000000000000000..c2e39d460609d3ea320b121b6671eb205f9ddd84 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/MessageParser.java @@ -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; + +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/MessageParserImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/MessageParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..13837cb36c98aac84ed1c612f08464e7bd577d12 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/MessageParserImpl.java @@ -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()); + } + +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/MessageType.java b/briar-core/src/org/briarproject/privategroup/invitation/MessageType.java new file mode 100644 index 0000000000000000000000000000000000000000..a8f04a28746b2f993170e341460761c86b244a0e --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/MessageType.java @@ -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(); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..6d3c721f38fea70a2a25c5396abc0e0f54bce442 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/PeerProtocolEngine.java @@ -0,0 +1,324 @@ +package org.briarproject.privategroup.invitation; + +import org.briarproject.api.FormatException; +import org.briarproject.api.clients.ClientHelper; +import org.briarproject.api.clients.ProtocolStateException; +import org.briarproject.api.db.DatabaseComponent; +import org.briarproject.api.db.DbException; +import org.briarproject.api.db.Transaction; +import org.briarproject.api.identity.IdentityManager; +import org.briarproject.api.nullsafety.NotNullByDefault; +import org.briarproject.api.privategroup.GroupMessageFactory; +import org.briarproject.api.privategroup.PrivateGroupFactory; +import org.briarproject.api.privategroup.PrivateGroupManager; +import org.briarproject.api.sync.Message; +import org.briarproject.api.system.Clock; + +import javax.annotation.Nullable; +import javax.annotation.concurrent.Immutable; + +import static org.briarproject.privategroup.invitation.PeerState.AWAIT_MEMBER; +import static org.briarproject.privategroup.invitation.PeerState.BOTH_JOINED; +import static org.briarproject.privategroup.invitation.PeerState.ERROR; +import static org.briarproject.privategroup.invitation.PeerState.LOCAL_JOINED; +import static org.briarproject.privategroup.invitation.PeerState.LOCAL_LEFT; +import static org.briarproject.privategroup.invitation.PeerState.NEITHER_JOINED; +import static org.briarproject.privategroup.invitation.PeerState.START; + +@Immutable +@NotNullByDefault +class PeerProtocolEngine extends AbstractProtocolEngine<PeerSession> { + + PeerProtocolEngine(DatabaseComponent db, ClientHelper clientHelper, + PrivateGroupManager privateGroupManager, + PrivateGroupFactory privateGroupFactory, + GroupMessageFactory groupMessageFactory, + IdentityManager identityManager, MessageParser messageParser, + MessageEncoder messageEncoder, Clock clock) { + super(db, clientHelper, privateGroupManager, privateGroupFactory, + groupMessageFactory, identityManager, messageParser, + messageEncoder, clock); + } + + @Override + public PeerSession onInviteAction(Transaction txn, PeerSession s, + @Nullable String message, long timestamp, byte[] signature) + throws DbException { + throw new UnsupportedOperationException(); // Invalid in this role + } + + @Override + public PeerSession onJoinAction(Transaction txn, PeerSession s) + throws DbException { + switch (s.getState()) { + case START: + case AWAIT_MEMBER: + case LOCAL_JOINED: + case BOTH_JOINED: + case ERROR: + throw new ProtocolStateException(); // Invalid in these states + case NEITHER_JOINED: + return onLocalJoinFromNeitherJoined(txn, s); + case LOCAL_LEFT: + return onLocalJoinFromLocalLeft(txn, s); + default: + throw new AssertionError(); + } + } + + @Override + public PeerSession onLeaveAction(Transaction txn, PeerSession s) + throws DbException { + switch (s.getState()) { + case START: + case AWAIT_MEMBER: + case NEITHER_JOINED: + case LOCAL_LEFT: + case ERROR: + return s; // Ignored in these states + case LOCAL_JOINED: + return onLocalLeaveFromLocalJoined(txn, s); + case BOTH_JOINED: + return onLocalLeaveFromBothJoined(txn, s); + default: + throw new AssertionError(); + } + } + + @Override + public PeerSession onMemberAddedAction(Transaction txn, PeerSession s) + throws DbException { + switch (s.getState()) { + case START: + return onMemberAddedFromStart(s); + case AWAIT_MEMBER: + return onMemberAddedFromAwaitMember(txn, s); + case NEITHER_JOINED: + case LOCAL_JOINED: + case BOTH_JOINED: + case LOCAL_LEFT: + throw new ProtocolStateException(); // Invalid in these states + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + @Override + public PeerSession onInviteMessage(Transaction txn, PeerSession s, + InviteMessage m) throws DbException, FormatException { + return abort(txn, s); // Invalid in this role + } + + @Override + public PeerSession onJoinMessage(Transaction txn, PeerSession s, + JoinMessage m) throws DbException, FormatException { + switch (s.getState()) { + case AWAIT_MEMBER: + case BOTH_JOINED: + case LOCAL_LEFT: + return abort(txn, s); // Invalid in these states + case START: + return onRemoteJoinFromStart(txn, s, m); + case NEITHER_JOINED: + return onRemoteJoinFromNeitherJoined(txn, s, m); + case LOCAL_JOINED: + return onRemoteJoinFromLocalJoined(txn, s, m); + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + @Override + public PeerSession onLeaveMessage(Transaction txn, PeerSession s, + LeaveMessage m) throws DbException, FormatException { + switch (s.getState()) { + case START: + case NEITHER_JOINED: + case LOCAL_JOINED: + return abort(txn, s); // Invalid in these states + case AWAIT_MEMBER: + return onRemoteLeaveFromAwaitMember(txn, s, m); + case LOCAL_LEFT: + return onRemoteLeaveFromLocalLeft(txn, s, m); + case BOTH_JOINED: + return onRemoteLeaveFromBothJoined(txn, s, m); + case ERROR: + return s; // Ignored in this state + default: + throw new AssertionError(); + } + } + + @Override + public PeerSession onAbortMessage(Transaction txn, PeerSession s, + AbortMessage m) throws DbException, FormatException { + return abort(txn, s); + } + + private PeerSession onLocalJoinFromNeitherJoined(Transaction txn, + PeerSession s) throws DbException { + // Send a JOIN message + Message sent = sendJoinMessage(txn, s, false); + // Move to the LOCAL_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + LOCAL_JOINED); + } + + private PeerSession onLocalJoinFromLocalLeft(Transaction txn, PeerSession s) + throws DbException { + // Send a JOIN message + Message sent = sendJoinMessage(txn, s, false); + try { + // Start syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, true); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Move to the BOTH_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + BOTH_JOINED); + } + + private PeerSession onLocalLeaveFromBothJoined(Transaction txn, + PeerSession s) throws DbException { + // Send a LEAVE message + Message sent = sendLeaveMessage(txn, s, false); + try { + // Stop syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, false); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Move to the LOCAL_LEFT state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + LOCAL_LEFT); + } + + private PeerSession onLocalLeaveFromLocalJoined(Transaction txn, + PeerSession s) throws DbException { + // Send a LEAVE message + Message sent = sendLeaveMessage(txn, s, false); + // Move to the NEITHER_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + NEITHER_JOINED); + } + + private PeerSession onMemberAddedFromStart(PeerSession s) { + // Move to the NEITHER_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), s.getLastRemoteMessageId(), + s.getLocalTimestamp(), NEITHER_JOINED); + } + + private PeerSession onMemberAddedFromAwaitMember(Transaction txn, + PeerSession s) throws DbException { + // Send a JOIN message + Message sent = sendJoinMessage(txn, s, false); + try { + // Start syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, true); + } catch (FormatException e) { + throw new DbException(e); // Invalid group metadata + } + // Move to the BOTH_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + BOTH_JOINED); + } + + private PeerSession onRemoteJoinFromStart(Transaction txn, + PeerSession s, JoinMessage m) throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Move to the AWAIT_MEMBER state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + AWAIT_MEMBER); + } + + private PeerSession onRemoteJoinFromNeitherJoined(Transaction txn, + PeerSession s, JoinMessage m) throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Send a JOIN message + Message sent = sendJoinMessage(txn, s, false); + // Start syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, true); + // Move to the BOTH_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), m.getId(), sent.getTimestamp(), BOTH_JOINED); + } + + private PeerSession onRemoteJoinFromLocalJoined(Transaction txn, + PeerSession s, JoinMessage m) throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Start syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, true); + // Move to the BOTH_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + BOTH_JOINED); + } + + private PeerSession onRemoteLeaveFromAwaitMember(Transaction txn, + PeerSession s, LeaveMessage m) throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Move to the START state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + START); + } + + private PeerSession onRemoteLeaveFromLocalLeft(Transaction txn, + PeerSession s, LeaveMessage m) throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Move to the NEITHER_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + NEITHER_JOINED); + } + + private PeerSession onRemoteLeaveFromBothJoined(Transaction txn, + PeerSession s, LeaveMessage m) throws DbException, FormatException { + // The dependency, if any, must be the last remote message + if (!isValidDependency(s, m.getPreviousMessageId())) + return abort(txn, s); + // Stop syncing the private group with the contact + syncPrivateGroupWithContact(txn, s, false); + // Move to the LOCAL_JOINED state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + s.getLastLocalMessageId(), m.getId(), s.getLocalTimestamp(), + LOCAL_JOINED); + } + + private PeerSession abort(Transaction txn, PeerSession s) + throws DbException, FormatException { + // If the session has already been aborted, do nothing + if (s.getState() == ERROR) return s; + // Stop syncing the private group with the contact, if we subscribe + if (isSubscribedPrivateGroup(txn, s.getPrivateGroupId())) + syncPrivateGroupWithContact(txn, s, false); + // Send an ABORT message + Message sent = sendAbortMessage(txn, s); + // Move to the ERROR state + return new PeerSession(s.getContactGroupId(), s.getPrivateGroupId(), + sent.getId(), s.getLastRemoteMessageId(), sent.getTimestamp(), + ERROR); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/PeerSession.java b/briar-core/src/org/briarproject/privategroup/invitation/PeerSession.java new file mode 100644 index 0000000000000000000000000000000000000000..2a6517dde443b9964791c51d24e6dd1b1205923e --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/PeerSession.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/PeerState.java b/briar-core/src/org/briarproject/privategroup/invitation/PeerState.java new file mode 100644 index 0000000000000000000000000000000000000000..eda52e5de71c34a11e572f0ec36bc9eaa0b3afc8 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/PeerState.java @@ -0,0 +1,25 @@ +package org.briarproject.privategroup.invitation; + +import org.briarproject.api.FormatException; + +enum PeerState implements State { + + START(0), AWAIT_MEMBER(1), NEITHER_JOINED(2), LOCAL_JOINED(3), + BOTH_JOINED(4), LOCAL_LEFT(5), ERROR(6); + + private final int value; + + PeerState(int value) { + this.value = value; + } + + @Override + public int getValue() { + return value; + } + + static PeerState fromValue(int value) throws FormatException { + for (PeerState s : values()) if (s.value == value) return s; + throw new FormatException(); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngine.java new file mode 100644 index 0000000000000000000000000000000000000000..1908548f54b333219b5b39b35f51204414c99e16 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngine.java @@ -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; + +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngineFactory.java b/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngineFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..95e5882de72547ea387502be6c8664e2b3105189 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngineFactory.java @@ -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(); +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngineFactoryImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngineFactoryImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..3c2c9ce84c25a46a9f98a4ae1eb259d7dc0d10e3 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/ProtocolEngineFactoryImpl.java @@ -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); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/Role.java b/briar-core/src/org/briarproject/privategroup/invitation/Role.java new file mode 100644 index 0000000000000000000000000000000000000000..2e84f63b57250cbf424da0946ba91f7bb445de12 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/Role.java @@ -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(); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/Session.java b/briar-core/src/org/briarproject/privategroup/invitation/Session.java new file mode 100644 index 0000000000000000000000000000000000000000..1cd5c76889b96324a26ecac69e51da3c5816e944 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/Session.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/SessionEncoder.java b/briar-core/src/org/briarproject/privategroup/invitation/SessionEncoder.java new file mode 100644 index 0000000000000000000000000000000000000000..5ba2cb48c5fc27ee54b56d5ac8aaac02db810cff --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/SessionEncoder.java @@ -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); +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/SessionEncoderImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/SessionEncoderImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..32f85534a58452e07b0cebdcf604ec5e47b77200 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/SessionEncoderImpl.java @@ -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; + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/SessionParser.java b/briar-core/src/org/briarproject/privategroup/invitation/SessionParser.java new file mode 100644 index 0000000000000000000000000000000000000000..8f488298b0b327db3fb94e2a8ca87178ded9fb97 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/SessionParser.java @@ -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; +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/SessionParserImpl.java b/briar-core/src/org/briarproject/privategroup/invitation/SessionParserImpl.java new file mode 100644 index 0000000000000000000000000000000000000000..c8e468db39654bca7c41d0dc4b1112a59c6e75e7 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/SessionParserImpl.java @@ -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); + } +} diff --git a/briar-core/src/org/briarproject/privategroup/invitation/State.java b/briar-core/src/org/briarproject/privategroup/invitation/State.java new file mode 100644 index 0000000000000000000000000000000000000000..6c112668ac1ab2484cd10ea2106c8c64724ac2c7 --- /dev/null +++ b/briar-core/src/org/briarproject/privategroup/invitation/State.java @@ -0,0 +1,6 @@ +package org.briarproject.privategroup.invitation; + +interface State { + + int getValue(); +}