Merge branch '708-private-group-remove-new-member-announcement' into 'master'

Remove new member announcement and add signature to join message



See merge request !384
This commit is contained in:
akwizgran
2016-11-04 09:32:40 +00:00
11 changed files with 274 additions and 255 deletions

View File

@@ -2,12 +2,15 @@ package org.briarproject;
import net.jodah.concurrentunit.Waiter; import net.jodah.concurrentunit.Waiter;
import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.ContactGroupFactory;
import org.briarproject.api.clients.MessageTracker.GroupCount; import org.briarproject.api.clients.MessageTracker.GroupCount;
import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactId;
import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.CryptoComponent;
import org.briarproject.api.crypto.KeyPair; import org.briarproject.api.crypto.KeyPair;
import org.briarproject.api.crypto.SecretKey; import org.briarproject.api.crypto.SecretKey;
import org.briarproject.api.data.BdfList;
import org.briarproject.api.db.DbException; import org.briarproject.api.db.DbException;
import org.briarproject.api.db.Transaction; import org.briarproject.api.db.Transaction;
import org.briarproject.api.event.Event; import org.briarproject.api.event.Event;
@@ -24,6 +27,8 @@ import org.briarproject.api.privategroup.JoinMessageHeader;
import org.briarproject.api.privategroup.PrivateGroup; import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory; import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.PrivateGroupManager; 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.GroupId;
import org.briarproject.api.sync.MessageId; import org.briarproject.api.sync.MessageId;
import org.briarproject.api.sync.SyncSession; import org.briarproject.api.sync.SyncSession;
@@ -53,6 +58,7 @@ import java.util.logging.Logger;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.TestPluginsModule.MAX_LATENCY; import static org.briarproject.TestPluginsModule.MAX_LATENCY;
import static org.briarproject.TestUtils.getRandomBytes;
import static org.briarproject.api.identity.Author.Status.VERIFIED; import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; import static org.briarproject.api.sync.ValidationManager.State.DELIVERED;
import static org.briarproject.api.sync.ValidationManager.State.INVALID; import static org.briarproject.api.sync.ValidationManager.State.INVALID;
@@ -72,18 +78,23 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
private LocalAuthor author0, author1; private LocalAuthor author0, author1;
private PrivateGroup privateGroup0; private PrivateGroup privateGroup0;
private GroupId groupId0; private GroupId groupId0;
private GroupMessage newMemberMsg0;
@Inject @Inject
Clock clock; Clock clock;
@Inject @Inject
AuthorFactory authorFactory; AuthorFactory authorFactory;
@Inject @Inject
ClientHelper clientHelper;
@Inject
CryptoComponent crypto; CryptoComponent crypto;
@Inject @Inject
ContactGroupFactory contactGroupFactory;
@Inject
PrivateGroupFactory privateGroupFactory; PrivateGroupFactory privateGroupFactory;
@Inject @Inject
GroupMessageFactory groupMessageFactory; GroupMessageFactory groupMessageFactory;
@Inject
GroupInvitationManager groupInvitationManager;
// objects accessed from background threads need to be volatile // objects accessed from background threads need to be volatile
private volatile Waiter validationWaiter; private volatile Waiter validationWaiter;
@@ -222,20 +233,6 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
// assert that message did not arrive // assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size()); assertEquals(2, groupManager1.getHeaders(groupId0).size());
// create and add test message with previousMsgId of newMemberMsg
previousMsgId = newMemberMsg0.getMessage().getId();
msg = groupMessageFactory
.createGroupMessage(groupId0, clock.currentTimeMillis(), null,
author0, "test", previousMsgId);
groupManager0.addLocalMessage(msg);
// sync test message
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that message did not arrive
assertEquals(2, groupManager1.getHeaders(groupId0).size());
} }
@Test @Test
@@ -318,20 +315,18 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
} }
@Test @Test
public void testWrongJoinMessages() throws Exception { public void testWrongJoinMessages1() throws Exception {
addDefaultIdentities(); addDefaultIdentities();
addDefaultContacts(); addDefaultContacts();
listenToEvents(); listenToEvents();
// author0 joins privateGroup0 with later timestamp // author0 joins privateGroup0 with wrong join message
long joinTime = clock.currentTimeMillis(); long joinTime = clock.currentTimeMillis();
GroupMessage newMemberMsg = groupMessageFactory GroupMessage joinMsg0 = groupMessageFactory
.createNewMemberMessage(groupId0, joinTime, author0, author0); .createJoinMessage(privateGroup0.getId(), joinTime, author0,
GroupMessage joinMsg = groupMessageFactory joinTime, getRandomBytes(12));
.createJoinMessage(groupId0, joinTime + 1, author0, groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
newMemberMsg.getMessage().getId()); assertEquals(joinMsg0.getMessage().getId(),
groupManager0.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg);
assertEquals(joinMsg.getMessage().getId(),
groupManager0.getPreviousMsgId(groupId0)); groupManager0.getPreviousMsgId(groupId0));
// make group visible to 1 // make group visible to 1
@@ -342,15 +337,92 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
t0.getDatabaseComponent().commitTransaction(txn0); t0.getDatabaseComponent().commitTransaction(txn0);
t0.getDatabaseComponent().endTransaction(txn0); t0.getDatabaseComponent().endTransaction(txn0);
// author1 joins privateGroup0 and refers to wrong NEW_MEMBER message // author1 joins privateGroup0 with wrong timestamp
joinMsg = groupMessageFactory
.createJoinMessage(groupId0, joinTime, author1,
newMemberMsg.getMessage().getId());
joinTime = clock.currentTimeMillis(); joinTime = clock.currentTimeMillis();
newMemberMsg = groupMessageFactory long inviteTime = joinTime;
.createNewMemberMessage(groupId0, joinTime, author0, author1); Group invitationGroup = contactGroupFactory
groupManager1.addPrivateGroup(privateGroup0, newMemberMsg, joinMsg); .createContactGroup(groupInvitationManager.getClientId(),
assertEquals(joinMsg.getMessage().getId(), author0.getId(), author1.getId());
BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
privateGroup0.getId());
byte[] creatorSignature =
clientHelper.sign(toSign, author0.getPrivateKey());
GroupMessage joinMsg1 = groupMessageFactory
.createJoinMessage(privateGroup0.getId(), joinTime, author1,
inviteTime, creatorSignature);
groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
assertEquals(joinMsg1.getMessage().getId(),
groupManager1.getPreviousMsgId(groupId0));
// make group visible to 0
Transaction txn1 = t1.getDatabaseComponent().startTransaction(false);
t1.getDatabaseComponent()
.setVisibleToContact(txn1, contactId0, privateGroup0.getId(),
true);
t1.getDatabaseComponent().commitTransaction(txn1);
t1.getDatabaseComponent().endTransaction(txn1);
// sync join messages
sync0To1();
validationWaiter.await(TIMEOUT, 1);
// assert that 0 never joined the group from 1's perspective
assertEquals(1, groupManager1.getHeaders(groupId0).size());
sync1To0();
validationWaiter.await(TIMEOUT, 1);
// assert that 1 never joined the group from 0's perspective
assertEquals(1, groupManager0.getHeaders(groupId0).size());
}
@Test
public void testWrongJoinMessages2() throws Exception {
addDefaultIdentities();
addDefaultContacts();
listenToEvents();
// author0 joins privateGroup0 with wrong member's join message
long joinTime = clock.currentTimeMillis();
long inviteTime = joinTime - 1;
Group invitationGroup = contactGroupFactory
.createContactGroup(groupInvitationManager.getClientId(),
author0.getId(), author0.getId());
BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
privateGroup0.getId());
byte[] creatorSignature =
clientHelper.sign(toSign, author0.getPrivateKey());
// join message should not include invite time and creator's signature
GroupMessage joinMsg0 = groupMessageFactory
.createJoinMessage(privateGroup0.getId(), joinTime, author0,
inviteTime, creatorSignature);
groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
assertEquals(joinMsg0.getMessage().getId(),
groupManager0.getPreviousMsgId(groupId0));
// make group visible to 1
Transaction txn0 = t0.getDatabaseComponent().startTransaction(false);
t0.getDatabaseComponent()
.setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
true);
t0.getDatabaseComponent().commitTransaction(txn0);
t0.getDatabaseComponent().endTransaction(txn0);
// author1 joins privateGroup0 with wrong signature in join message
joinTime = clock.currentTimeMillis();
inviteTime = joinTime - 1;
invitationGroup = contactGroupFactory
.createContactGroup(groupInvitationManager.getClientId(),
author0.getId(), author1.getId());
toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
privateGroup0.getId());
// signature uses joiner's key, not creator's key
creatorSignature = clientHelper.sign(toSign, author1.getPrivateKey());
GroupMessage joinMsg1 = groupMessageFactory
.createJoinMessage(privateGroup0.getId(), joinTime, author1,
inviteTime, creatorSignature);
groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
assertEquals(joinMsg1.getMessage().getId(),
groupManager1.getPreviousMsgId(groupId0)); groupManager1.getPreviousMsgId(groupId0));
// make group visible to 0 // make group visible to 0
@@ -363,14 +435,12 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
// sync join messages // sync join messages
sync0To1(); sync0To1();
deliveryWaiter.await(TIMEOUT, 1);
validationWaiter.await(TIMEOUT, 1); validationWaiter.await(TIMEOUT, 1);
// assert that 0 never joined the group from 1's perspective // assert that 0 never joined the group from 1's perspective
assertEquals(1, groupManager1.getHeaders(groupId0).size()); assertEquals(1, groupManager1.getHeaders(groupId0).size());
sync1To0(); sync1To0();
deliveryWaiter.await(TIMEOUT, 1);
validationWaiter.await(TIMEOUT, 1); validationWaiter.await(TIMEOUT, 1);
// assert that 1 never joined the group from 0's perspective // assert that 1 never joined the group from 0's perspective
@@ -452,14 +522,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
private void addGroup() throws Exception { private void addGroup() throws Exception {
// author0 joins privateGroup0 // author0 joins privateGroup0
long joinTime = clock.currentTimeMillis(); long joinTime = clock.currentTimeMillis();
newMemberMsg0 = groupMessageFactory GroupMessage joinMsg0 = groupMessageFactory
.createNewMemberMessage(privateGroup0.getId(), joinTime, .createJoinMessage(privateGroup0.getId(), joinTime, author0);
author0, author0); groupManager0.addPrivateGroup(privateGroup0, joinMsg0);
GroupMessage joinMsg = groupMessageFactory assertEquals(joinMsg0.getMessage().getId(),
.createJoinMessage(privateGroup0.getId(), joinTime, author0,
newMemberMsg0.getMessage().getId());
groupManager0.addPrivateGroup(privateGroup0, newMemberMsg0, joinMsg);
assertEquals(joinMsg.getMessage().getId(),
groupManager0.getPreviousMsgId(groupId0)); groupManager0.getPreviousMsgId(groupId0));
// make group visible to 1 // make group visible to 1
@@ -467,19 +533,24 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
t0.getDatabaseComponent() t0.getDatabaseComponent()
.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), .setVisibleToContact(txn0, contactId1, privateGroup0.getId(),
true); true);
t0.getDatabaseComponent().commitTransaction(txn0);; t0.getDatabaseComponent().commitTransaction(txn0);
t0.getDatabaseComponent().endTransaction(txn0); t0.getDatabaseComponent().endTransaction(txn0);
// author1 joins privateGroup0 // author1 joins privateGroup0
joinTime = clock.currentTimeMillis(); joinTime = clock.currentTimeMillis();
GroupMessage newMemberMsg1 = groupMessageFactory long inviteTime = joinTime - 1;
.createNewMemberMessage(privateGroup0.getId(), joinTime, Group invitationGroup = contactGroupFactory
author0, author1); .createContactGroup(groupInvitationManager.getClientId(),
joinMsg = groupMessageFactory author0.getId(), author1.getId());
BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(),
privateGroup0.getId());
byte[] creatorSignature =
clientHelper.sign(toSign, author0.getPrivateKey());
GroupMessage joinMsg1 = groupMessageFactory
.createJoinMessage(privateGroup0.getId(), joinTime, author1, .createJoinMessage(privateGroup0.getId(), joinTime, author1,
newMemberMsg1.getMessage().getId()); inviteTime, creatorSignature);
groupManager1.addPrivateGroup(privateGroup0, newMemberMsg1, joinMsg); groupManager1.addPrivateGroup(privateGroup0, joinMsg1);
assertEquals(joinMsg.getMessage().getId(), assertEquals(joinMsg1.getMessage().getId(),
groupManager1.getPreviousMsgId(groupId0)); groupManager1.getPreviousMsgId(groupId0));
// make group visible to 0 // make group visible to 0
@@ -492,9 +563,9 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest {
// sync join messages // sync join messages
sync0To1(); sync0To1();
deliveryWaiter.await(TIMEOUT, 2); deliveryWaiter.await(TIMEOUT, 1);
sync1To0(); sync1To0();
deliveryWaiter.await(TIMEOUT, 2); deliveryWaiter.await(TIMEOUT, 1);
} }
private void sync0To1() throws IOException, TimeoutException { private void sync0To1() throws IOException, TimeoutException {

View File

@@ -82,29 +82,24 @@ public class CreateGroupControllerImpl extends DbControllerImpl
LOG.info("Creating group..."); LOG.info("Creating group...");
PrivateGroup group = PrivateGroup group =
groupFactory.createPrivateGroup(name, author); groupFactory.createPrivateGroup(name, author);
LOG.info("Creating new member announcement...");
GroupMessage newMemberMsg = groupMessageFactory
.createNewMemberMessage(group.getId(),
clock.currentTimeMillis(), author, author);
LOG.info("Creating new join announcement..."); LOG.info("Creating new join announcement...");
GroupMessage joinMsg = groupMessageFactory GroupMessage joinMsg = groupMessageFactory
.createJoinMessage(group.getId(), .createJoinMessage(group.getId(),
newMemberMsg.getMessage().getTimestamp(), clock.currentTimeMillis(), author);
author, newMemberMsg.getMessage().getId()); storeGroup(group, joinMsg, handler);
storeGroup(group, newMemberMsg, joinMsg, handler);
} }
}); });
} }
private void storeGroup(final PrivateGroup group, private void storeGroup(final PrivateGroup group,
final GroupMessage newMemberMsg, final GroupMessage joinMsg, final GroupMessage joinMsg,
final ResultExceptionHandler<GroupId, DbException> handler) { final ResultExceptionHandler<GroupId, DbException> handler) {
runOnDbThread(new Runnable() { runOnDbThread(new Runnable() {
@Override @Override
public void run() { public void run() {
LOG.info("Adding group to database..."); LOG.info("Adding group to database...");
try { try {
groupManager.addPrivateGroup(group, newMemberMsg, joinMsg); groupManager.addPrivateGroup(group, joinMsg);
handler.onResult(group.getId()); handler.onResult(group.getId());
} catch (DbException e) { } catch (DbException e) {
if (LOG.isLoggable(WARNING)) if (LOG.isLoggable(WARNING))

View File

@@ -1,9 +1,9 @@
package org.briarproject.api.clients; package org.briarproject.api.clients;
import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.Contact;
import org.briarproject.api.identity.AuthorId;
import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.ClientId;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.GroupFactory;
public interface ContactGroupFactory { public interface ContactGroupFactory {
@@ -13,4 +13,11 @@ public interface ContactGroupFactory {
/** Creates a group for the given client to share with the given contact. */ /** Creates a group for the given client to share with the given contact. */
Group createContactGroup(ClientId clientId, Contact contact); Group createContactGroup(ClientId clientId, Contact contact);
/**
* Creates a group for the given client to share between the given authors
* identified by their AuthorIds.
*/
Group createContactGroup(ClientId clientId, AuthorId authorId1,
AuthorId authorId2);
} }

View File

@@ -10,33 +10,29 @@ import org.jetbrains.annotations.Nullable;
public interface GroupMessageFactory { public interface GroupMessageFactory {
/** /**
* Creates a new member announcement that contains the joiner's identity * Creates a join announcement message for the creator of a group.
* and is signed by the creator.
* <p>
* When a new member accepts an invitation to the group,
* the creator sends this new member announcement to the group.
*
* @param groupId The ID of the group the new member joined
* @param timestamp The current timestamp
* @param creator The creator of the group with {@param groupId}
* @param member The new member that has just accepted an invitation
*/
@CryptoExecutor
GroupMessage createNewMemberMessage(GroupId groupId, long timestamp,
LocalAuthor creator, Author member);
/**
* Creates a join announcement message
* that depends on a previous new member announcement.
* *
* @param groupId The ID of the Group that is being joined * @param groupId The ID of the Group that is being joined
* @param timestamp Must be equal to the timestamp of the new member message * @param timestamp Must be greater than the timestamp of the invitation message
* @param member Our own LocalAuthor * @param creator The creator's LocalAuthor
* @param newMemberId The MessageId of the new member message
*/ */
@CryptoExecutor @CryptoExecutor
GroupMessage createJoinMessage(GroupId groupId, long timestamp, GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor member, MessageId newMemberId); LocalAuthor creator);
/**
* Creates a join announcement message for a joining member.
*
* @param groupId The ID of the Group that is being joined
* @param timestamp Must be greater than the timestamp of the
* invitation message
* @param member The member's LocalAuthor
* @param inviteTimestamp The timestamp of the group invitation message
* @param creatorSignature The creator's signature from the group invitation
*/
@CryptoExecutor
GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor member, long inviteTimestamp, byte[] creatorSignature);
/** /**
* Creates a group message * Creates a group message

View File

@@ -1,9 +1,8 @@
package org.briarproject.api.privategroup; package org.briarproject.api.privategroup;
public enum MessageType { public enum MessageType {
NEW_MEMBER(0), JOIN(0),
JOIN(1), POST(1);
POST(2);
int value; int value;

View File

@@ -21,12 +21,10 @@ public interface PrivateGroupManager extends MessageTracker {
* Adds a new private group and joins it. * Adds a new private group and joins it.
* *
* @param group The private group to add * @param group The private group to add
* @param newMemberMsg The creator's message announcing herself as
* first new member
* @param joinMsg The creator's own join message * @param joinMsg The creator's own join message
*/ */
void addPrivateGroup(PrivateGroup group, GroupMessage newMemberMsg, void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
GroupMessage joinMsg) throws DbException; throws DbException;
/** Removes a dissolved private group. */ /** Removes a dissolved private group. */
void removePrivateGroup(GroupId g) throws DbException; void removePrivateGroup(GroupId g) throws DbException;

View File

@@ -41,6 +41,13 @@ class ContactGroupFactoryImpl implements ContactGroupFactory {
return groupFactory.createGroup(clientId, descriptor); return groupFactory.createGroup(clientId, descriptor);
} }
@Override
public Group createContactGroup(ClientId clientId, AuthorId authorId1,
AuthorId authorId2) {
byte[] descriptor = createGroupDescriptor(authorId1, authorId2);
return groupFactory.createGroup(clientId, descriptor);
}
private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) { private byte[] createGroupDescriptor(AuthorId local, AuthorId remote) {
try { try {
if (Bytes.COMPARATOR.compare(local, remote) < 0) if (Bytes.COMPARATOR.compare(local, remote) < 0)

View File

@@ -3,7 +3,6 @@ package org.briarproject.privategroup;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.data.BdfList; import org.briarproject.api.data.BdfList;
import org.briarproject.api.identity.Author;
import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.identity.LocalAuthor;
import org.briarproject.api.nullsafety.NotNullByDefault; import org.briarproject.api.nullsafety.NotNullByDefault;
import org.briarproject.api.privategroup.GroupMessage; import org.briarproject.api.privategroup.GroupMessage;
@@ -18,7 +17,6 @@ import java.security.GeneralSecurityException;
import javax.inject.Inject; import javax.inject.Inject;
import static org.briarproject.api.privategroup.MessageType.JOIN; import static org.briarproject.api.privategroup.MessageType.JOIN;
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
import static org.briarproject.api.privategroup.MessageType.POST; import static org.briarproject.api.privategroup.MessageType.POST;
@NotNullByDefault @NotNullByDefault
@@ -32,45 +30,34 @@ class GroupMessageFactoryImpl implements GroupMessageFactory {
} }
@Override @Override
public GroupMessage createNewMemberMessage(GroupId groupId, long timestamp, public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor creator, Author member) { LocalAuthor creator) {
try {
// Generate the signature
int type = NEW_MEMBER.getInt();
BdfList toSign = BdfList.of(groupId, timestamp, type,
member.getName(), member.getPublicKey());
byte[] signature =
clientHelper.sign(toSign, creator.getPrivateKey());
// Compose the message return createJoinMessage(groupId, timestamp, creator, null);
BdfList body =
BdfList.of(type, member.getName(),
member.getPublicKey(), signature);
Message m = clientHelper.createMessage(groupId, timestamp, body);
return new GroupMessage(m, null, member);
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
} catch (FormatException e) {
throw new RuntimeException(e);
}
} }
@Override @Override
public GroupMessage createJoinMessage(GroupId groupId, long timestamp, public GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor member, MessageId newMemberId) { LocalAuthor member, long inviteTimestamp, byte[] creatorSignature) {
BdfList invite = BdfList.of(inviteTimestamp, creatorSignature);
return createJoinMessage(groupId, timestamp, member, invite);
}
private GroupMessage createJoinMessage(GroupId groupId, long timestamp,
LocalAuthor member, @Nullable BdfList invite) {
try { try {
// Generate the signature // Generate the signature
int type = JOIN.getInt(); int type = JOIN.getInt();
BdfList toSign = BdfList.of(groupId, timestamp, type, BdfList toSign = BdfList.of(groupId, timestamp, type,
member.getName(), member.getPublicKey(), newMemberId); member.getName(), member.getPublicKey(), invite);
byte[] signature = byte[] memberSignature =
clientHelper.sign(toSign, member.getPrivateKey()); clientHelper.sign(toSign, member.getPrivateKey());
// Compose the message // Compose the message
BdfList body = BdfList body =
BdfList.of(type, member.getName(), BdfList.of(type, member.getName(),
member.getPublicKey(), newMemberId, signature); member.getPublicKey(), invite, memberSignature);
Message m = clientHelper.createMessage(groupId, timestamp, body); Message m = clientHelper.createMessage(groupId, timestamp, body);
return new GroupMessage(m, null, member); return new GroupMessage(m, null, member);

View File

@@ -3,6 +3,7 @@ package org.briarproject.privategroup;
import org.briarproject.api.FormatException; import org.briarproject.api.FormatException;
import org.briarproject.api.clients.BdfMessageContext; import org.briarproject.api.clients.BdfMessageContext;
import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.ContactGroupFactory;
import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfDictionary;
import org.briarproject.api.data.BdfList; import org.briarproject.api.data.BdfList;
import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataEncoder;
@@ -11,6 +12,7 @@ import org.briarproject.api.identity.AuthorFactory;
import org.briarproject.api.privategroup.MessageType; import org.briarproject.api.privategroup.MessageType;
import org.briarproject.api.privategroup.PrivateGroup; import org.briarproject.api.privategroup.PrivateGroup;
import org.briarproject.api.privategroup.PrivateGroupFactory; import org.briarproject.api.privategroup.PrivateGroupFactory;
import org.briarproject.api.privategroup.invitation.GroupInvitationManager;
import org.briarproject.api.sync.Group; import org.briarproject.api.sync.Group;
import org.briarproject.api.sync.InvalidMessageException; import org.briarproject.api.sync.InvalidMessageException;
import org.briarproject.api.sync.Message; import org.briarproject.api.sync.Message;
@@ -21,19 +23,16 @@ import org.briarproject.clients.BdfMessageValidator;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import static org.briarproject.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH; 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_PUBLIC_KEY_LENGTH;
import static org.briarproject.api.identity.AuthorConstants.MAX_SIGNATURE_LENGTH; 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.JOIN;
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
import static org.briarproject.api.privategroup.MessageType.POST; 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.PrivateGroupConstants.MAX_GROUP_POST_BODY_LENGTH;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_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_NAME;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY; import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID; import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID; import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_READ; import static org.briarproject.privategroup.Constants.KEY_READ;
@@ -42,52 +41,50 @@ import static org.briarproject.privategroup.Constants.KEY_TYPE;
class GroupMessageValidator extends BdfMessageValidator { class GroupMessageValidator extends BdfMessageValidator {
private final ContactGroupFactory contactGroupFactory;
private final PrivateGroupFactory groupFactory; private final PrivateGroupFactory groupFactory;
private final AuthorFactory authorFactory; private final AuthorFactory authorFactory;
private final GroupInvitationManager groupInvitationManager; // TODO remove
GroupMessageValidator(PrivateGroupFactory groupFactory, GroupMessageValidator(ContactGroupFactory contactGroupFactory,
PrivateGroupFactory groupFactory,
ClientHelper clientHelper, MetadataEncoder metadataEncoder, ClientHelper clientHelper, MetadataEncoder metadataEncoder,
Clock clock, AuthorFactory authorFactory) { Clock clock, AuthorFactory authorFactory,
GroupInvitationManager groupInvitationManager) {
super(clientHelper, metadataEncoder, clock); super(clientHelper, metadataEncoder, clock);
this.contactGroupFactory = contactGroupFactory;
this.groupFactory = groupFactory; this.groupFactory = groupFactory;
this.authorFactory = authorFactory; this.authorFactory = authorFactory;
this.groupInvitationManager = groupInvitationManager;
} }
@Override @Override
protected BdfMessageContext validateMessage(Message m, Group g, protected BdfMessageContext validateMessage(Message m, Group g,
BdfList body) throws InvalidMessageException, FormatException { BdfList body) throws InvalidMessageException, FormatException {
checkSize(body, 4, 7); checkSize(body, 5, 7);
// message type (int) // message type (int)
int type = body.getLong(0).intValue(); int type = body.getLong(0).intValue();
body.removeElementAt(0);
// member_name (string) // member_name (string)
String memberName = body.getString(0); String memberName = body.getString(1);
checkLength(memberName, 1, MAX_AUTHOR_NAME_LENGTH); checkLength(memberName, 1, MAX_AUTHOR_NAME_LENGTH);
// member_public_key (raw) // member_public_key (raw)
byte[] memberPublicKey = body.getRaw(1); byte[] memberPublicKey = body.getRaw(2);
checkLength(memberPublicKey, 1, MAX_PUBLIC_KEY_LENGTH); checkLength(memberPublicKey, 1, MAX_PUBLIC_KEY_LENGTH);
Author member = authorFactory.createAuthor(memberName, memberPublicKey);
BdfMessageContext c; BdfMessageContext c;
switch (MessageType.valueOf(type)) { switch (MessageType.valueOf(type)) {
case NEW_MEMBER:
c = validateNewMember(m, g, body, memberName,
memberPublicKey);
addMessageMetadata(c, memberName, memberPublicKey,
m.getTimestamp());
break;
case JOIN: case JOIN:
c = validateJoin(m, g, body, memberName, memberPublicKey); c = validateJoin(m, g, body, member);
addMessageMetadata(c, memberName, memberPublicKey, addMessageMetadata(c, member, m.getTimestamp());
m.getTimestamp());
break; break;
case POST: case POST:
c = validatePost(m, g, body, memberName, memberPublicKey); c = validatePost(m, g, body, member);
addMessageMetadata(c, memberName, memberPublicKey, addMessageMetadata(c, member, m.getTimestamp());
m.getTimestamp());
break; break;
default: default:
throw new InvalidMessageException("Unknown Message Type"); throw new InvalidMessageException("Unknown Message Type");
@@ -96,26 +93,67 @@ class GroupMessageValidator extends BdfMessageValidator {
return c; return c;
} }
private BdfMessageContext validateNewMember(Message m, Group g, private BdfMessageContext validateJoin(Message m, Group g, BdfList body,
BdfList body, String memberName, byte[] memberPublicKey) Author member)
throws InvalidMessageException, FormatException { throws InvalidMessageException, FormatException {
// The content is a BDF list with three elements // The content is a BDF list with five elements
checkSize(body, 3); checkSize(body, 5);
PrivateGroup pg = groupFactory.parsePrivateGroup(g);
// signature (raw) // invite is null if the member is the creator of the private group
// signature with the creator's private key over a list with 4 elements BdfList invite = body.getOptionalList(3);
byte[] signature = body.getRaw(2); if (invite == null) {
checkLength(signature, 1, MAX_SIGNATURE_LENGTH); if (!member.equals(pg.getAuthor()))
throw new InvalidMessageException();
} else {
if (member.equals(pg.getAuthor()))
throw new InvalidMessageException();
// Otherwise invite is a list with two elements
checkSize(invite, 2);
// invite_timestamp (int)
// join_timestamp must be greater than invite_timestamp
long inviteTimestamp = invite.getLong(0);
if (m.getTimestamp() <= inviteTimestamp)
throw new InvalidMessageException();
// creator_signature (raw)
byte[] creatorSignature = invite.getRaw(1);
checkLength(creatorSignature, 1, MAX_SIGNATURE_LENGTH);
// derive invitation group
Group invitationGroup = contactGroupFactory
.createContactGroup(groupInvitationManager.getClientId(),
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());
try {
clientHelper.verifySignature(creatorSignature,
pg.getAuthor().getPublicKey(), signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
}
// member_signature (raw)
// a signature with the member's private key over a list with 6 elements
byte[] memberSignature = body.getRaw(4);
checkLength(memberSignature, 1, MAX_SIGNATURE_LENGTH);
// Verify Signature // Verify Signature
BdfList signed = BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), JOIN.getInt(),
BdfList.of(g.getId(), m.getTimestamp(), NEW_MEMBER.getInt(), member.getName(), member.getPublicKey(), invite);
memberName, memberPublicKey);
PrivateGroup group = groupFactory.parsePrivateGroup(g);
byte[] creatorPublicKey = group.getAuthor().getPublicKey();
try { try {
clientHelper.verifySignature(signature, creatorPublicKey, signed); clientHelper.verifySignature(memberSignature, member.getPublicKey(),
signed);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new InvalidMessageException(e); throw new InvalidMessageException(e);
} }
@@ -125,75 +163,39 @@ class GroupMessageValidator extends BdfMessageValidator {
return new BdfMessageContext(meta); return new BdfMessageContext(meta);
} }
private BdfMessageContext validateJoin(Message m, Group g, BdfList body,
String memberName, byte[] memberPublicKey)
throws InvalidMessageException, FormatException {
// The content is a BDF list with four elements
checkSize(body, 4);
// new_member_id (raw)
// the identifier of a new member message
// with the same member_name and member_public_key
byte[] newMemberId = body.getRaw(2);
checkLength(newMemberId, MessageId.LENGTH);
// signature (raw)
// a signature with the member's private key over a list with 5 elements
byte[] signature = body.getRaw(3);
checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
// Verify Signature
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), JOIN.getInt(),
memberName, memberPublicKey, newMemberId);
try {
clientHelper.verifySignature(signature, memberPublicKey, signed);
} catch (GeneralSecurityException e) {
throw new InvalidMessageException(e);
}
// The new member message is a dependency
Collection<MessageId> dependencies =
Collections.singleton(new MessageId(newMemberId));
// Return the metadata and dependencies
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_NEW_MEMBER_MSG_ID, newMemberId);
return new BdfMessageContext(meta, dependencies);
}
private BdfMessageContext validatePost(Message m, Group g, BdfList body, private BdfMessageContext validatePost(Message m, Group g, BdfList body,
String memberName, byte[] memberPublicKey) Author member)
throws InvalidMessageException, FormatException { throws InvalidMessageException, FormatException {
// The content is a BDF list with six elements // The content is a BDF list with seven elements
checkSize(body, 6); checkSize(body, 7);
// parent_id (raw or null) // parent_id (raw or null)
// the identifier of the post to which this is a reply, if any // the identifier of the post to which this is a reply, if any
byte[] parentId = body.getOptionalRaw(2); byte[] parentId = body.getOptionalRaw(3);
checkLength(parentId, MessageId.LENGTH); checkLength(parentId, MessageId.LENGTH);
// previous_message_id (raw) // previous_message_id (raw)
// the identifier of the member's previous post or join message // the identifier of the member's previous post or join message
byte[] previousMessageId = body.getRaw(3); byte[] previousMessageId = body.getRaw(4);
checkLength(previousMessageId, MessageId.LENGTH); checkLength(previousMessageId, MessageId.LENGTH);
// content (string) // content (string)
String content = body.getString(4); String content = body.getString(5);
checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH); checkLength(content, 0, MAX_GROUP_POST_BODY_LENGTH);
// signature (raw) // signature (raw)
// a signature with the member's private key over a list with 7 elements // a signature with the member's private key over a list with 7 elements
byte[] signature = body.getRaw(5); byte[] signature = body.getRaw(6);
checkLength(signature, 1, MAX_SIGNATURE_LENGTH); checkLength(signature, 1, MAX_SIGNATURE_LENGTH);
// Verify Signature // Verify Signature
BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), POST.getInt(), BdfList signed = BdfList.of(g.getId(), m.getTimestamp(), POST.getInt(),
memberName, memberPublicKey, parentId, previousMessageId, member.getName(), member.getPublicKey(), parentId,
content); previousMessageId, content);
try { try {
clientHelper.verifySignature(signature, memberPublicKey, signed); clientHelper
.verifySignature(signature, member.getPublicKey(), signed);
} catch (GeneralSecurityException e) { } catch (GeneralSecurityException e) {
throw new InvalidMessageException(e); throw new InvalidMessageException(e);
} }
@@ -211,14 +213,13 @@ class GroupMessageValidator extends BdfMessageValidator {
return new BdfMessageContext(meta, dependencies); return new BdfMessageContext(meta, dependencies);
} }
private void addMessageMetadata(BdfMessageContext c, String authorName, private void addMessageMetadata(BdfMessageContext c, Author member,
byte[] pubKey, long time) { long time) {
c.getDictionary().put(KEY_TIMESTAMP, time); c.getDictionary().put(KEY_TIMESTAMP, time);
c.getDictionary().put(KEY_READ, false); c.getDictionary().put(KEY_READ, false);
Author a = authorFactory.createAuthor(authorName, pubKey); c.getDictionary().put(KEY_MEMBER_ID, member.getId());
c.getDictionary().put(KEY_MEMBER_ID, a.getId()); c.getDictionary().put(KEY_MEMBER_NAME, member.getName());
c.getDictionary().put(KEY_MEMBER_NAME, authorName); c.getDictionary().put(KEY_MEMBER_PUBLIC_KEY, member.getPublicKey());
c.getDictionary().put(KEY_MEMBER_PUBLIC_KEY, pubKey);
} }
} }

View File

@@ -49,14 +49,12 @@ import static org.briarproject.api.identity.Author.Status.OURSELVES;
import static org.briarproject.api.identity.Author.Status.UNVERIFIED; import static org.briarproject.api.identity.Author.Status.UNVERIFIED;
import static org.briarproject.api.identity.Author.Status.VERIFIED; import static org.briarproject.api.identity.Author.Status.VERIFIED;
import static org.briarproject.api.privategroup.MessageType.JOIN; import static org.briarproject.api.privategroup.MessageType.JOIN;
import static org.briarproject.api.privategroup.MessageType.NEW_MEMBER;
import static org.briarproject.api.privategroup.MessageType.POST; import static org.briarproject.api.privategroup.MessageType.POST;
import static org.briarproject.privategroup.Constants.KEY_DISSOLVED; import static org.briarproject.privategroup.Constants.KEY_DISSOLVED;
import static org.briarproject.privategroup.Constants.KEY_MEMBERS; import static org.briarproject.privategroup.Constants.KEY_MEMBERS;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_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_NAME;
import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY; import static org.briarproject.privategroup.Constants.KEY_MEMBER_PUBLIC_KEY;
import static org.briarproject.privategroup.Constants.KEY_NEW_MEMBER_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID; import static org.briarproject.privategroup.Constants.KEY_PARENT_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID; import static org.briarproject.privategroup.Constants.KEY_PREVIOUS_MSG_ID;
import static org.briarproject.privategroup.Constants.KEY_READ; import static org.briarproject.privategroup.Constants.KEY_READ;
@@ -95,8 +93,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
} }
@Override @Override
public void addPrivateGroup(PrivateGroup group, public void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg)
GroupMessage newMemberMsg, GroupMessage joinMsg)
throws DbException { throws DbException {
Transaction txn = db.startTransaction(false); Transaction txn = db.startTransaction(false);
try { try {
@@ -106,7 +103,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
new BdfEntry(KEY_DISSOLVED, false) new BdfEntry(KEY_DISSOLVED, false)
); );
clientHelper.mergeGroupMetadata(txn, group.getId(), meta); clientHelper.mergeGroupMetadata(txn, group.getId(), meta);
announceNewMember(txn, newMemberMsg);
joinPrivateGroup(txn, joinMsg); joinPrivateGroup(txn, joinMsg);
db.commitTransaction(txn); db.commitTransaction(txn);
} catch (FormatException e) { } catch (FormatException e) {
@@ -116,14 +112,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
} }
} }
private void announceNewMember(Transaction txn, GroupMessage m)
throws DbException, FormatException {
BdfDictionary meta = new BdfDictionary();
meta.put(KEY_TYPE, NEW_MEMBER.getInt());
addMessageMetadata(meta, m, true);
clientHelper.addLocalMessage(txn, m.getMessage(), meta, true);
}
private void joinPrivateGroup(Transaction txn, GroupMessage m) private void joinPrivateGroup(Transaction txn, GroupMessage m)
throws DbException, FormatException { throws DbException, FormatException {
BdfDictionary meta = new BdfDictionary(); BdfDictionary meta = new BdfDictionary();
@@ -315,8 +303,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
// get all authors we need to get the status for // get all authors we need to get the status for
Set<AuthorId> authors = new HashSet<AuthorId>(); Set<AuthorId> authors = new HashSet<AuthorId>();
for (BdfDictionary meta : metadata.values()) { for (BdfDictionary meta : metadata.values()) {
if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
continue;
byte[] idBytes = meta.getRaw(KEY_MEMBER_ID); byte[] idBytes = meta.getRaw(KEY_MEMBER_ID);
authors.add(new AuthorId(idBytes)); authors.add(new AuthorId(idBytes));
} }
@@ -328,8 +314,6 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
// Parse the metadata // Parse the metadata
for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) { for (Entry<MessageId, BdfDictionary> entry : metadata.entrySet()) {
BdfDictionary meta = entry.getValue(); BdfDictionary meta = entry.getValue();
if (meta.getLong(KEY_TYPE) == NEW_MEMBER.getInt())
continue;
headers.add(getGroupMessageHeader(txn, g, entry.getKey(), meta, headers.add(getGroupMessageHeader(txn, g, entry.getKey(), meta,
statuses)); statuses));
} }
@@ -434,36 +418,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements
MessageType type = MessageType type =
MessageType.valueOf(meta.getLong(KEY_TYPE).intValue()); MessageType.valueOf(meta.getLong(KEY_TYPE).intValue());
switch (type) { switch (type) {
case NEW_MEMBER:
// don't track incoming message, because it won't show in the UI
return true;
case JOIN: case JOIN:
// new_member_id must be the identifier of a NEW_MEMBER message
byte[] newMemberIdBytes =
meta.getOptionalRaw(KEY_NEW_MEMBER_MSG_ID);
MessageId newMemberId = new MessageId(newMemberIdBytes);
BdfDictionary newMemberMeta = clientHelper
.getMessageMetadataAsDictionary(txn, newMemberId);
MessageType newMemberType = MessageType
.valueOf(newMemberMeta.getLong(KEY_TYPE).intValue());
if (newMemberType != NEW_MEMBER) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// timestamp must be equal to timestamp of NEW_MEMBER message
if (timestamp != newMemberMeta.getLong(KEY_TIMESTAMP)) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
// NEW_MEMBER must have same member_name and member_public_key
if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID),
newMemberMeta.getRaw(KEY_MEMBER_ID))) {
// FIXME throw new InvalidMessageException() (#643)
db.deleteMessage(txn, m.getId());
return false;
}
addMember(txn, m.getGroupId(), getAuthor(meta)); addMember(txn, m.getGroupId(), getAuthor(meta));
trackIncomingMessage(txn, m); trackIncomingMessage(txn, m);
return true; return true;

View File

@@ -1,6 +1,7 @@
package org.briarproject.privategroup; package org.briarproject.privategroup;
import org.briarproject.api.clients.ClientHelper; import org.briarproject.api.clients.ClientHelper;
import org.briarproject.api.clients.ContactGroupFactory;
import org.briarproject.api.contact.ContactManager; import org.briarproject.api.contact.ContactManager;
import org.briarproject.api.data.MetadataEncoder; import org.briarproject.api.data.MetadataEncoder;
import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.AuthorFactory;
@@ -58,14 +59,16 @@ public class PrivateGroupModule {
@Provides @Provides
@Singleton @Singleton
GroupMessageValidator provideGroupMessageValidator( GroupMessageValidator provideGroupMessageValidator(
ContactGroupFactory contactGroupFactory,
PrivateGroupFactory groupFactory, PrivateGroupFactory groupFactory,
ValidationManager validationManager, ClientHelper clientHelper, ValidationManager validationManager, ClientHelper clientHelper,
MetadataEncoder metadataEncoder, Clock clock, MetadataEncoder metadataEncoder, Clock clock,
AuthorFactory authorFactory) { AuthorFactory authorFactory,
GroupInvitationManager groupInvitationManager) {
GroupMessageValidator validator = new GroupMessageValidator( GroupMessageValidator validator = new GroupMessageValidator(
groupFactory, clientHelper, metadataEncoder, clock, contactGroupFactory, groupFactory, clientHelper,
authorFactory); metadataEncoder, clock, authorFactory, groupInvitationManager);
validationManager.registerMessageValidator( validationManager.registerMessageValidator(
PrivateGroupManagerImpl.CLIENT_ID, validator); PrivateGroupManagerImpl.CLIENT_ID, validator);