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