diff --git a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java index 10d4280f41dcdc6634c535bd02d0ccf56abae2b3..6b149c13fa0269b7915e08ed4638c6e1e14c2a40 100644 --- a/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java +++ b/briar-android-tests/src/test/java/org/briarproject/PrivateGroupManagerTest.java @@ -11,6 +11,7 @@ import org.briarproject.api.crypto.CryptoComponent; import org.briarproject.api.crypto.KeyPair; import org.briarproject.api.crypto.SecretKey; 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.event.Event; @@ -20,6 +21,7 @@ import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.identity.IdentityManager; import org.briarproject.api.identity.LocalAuthor; import org.briarproject.api.lifecycle.LifecycleManager; +import org.briarproject.api.privategroup.GroupMember; import org.briarproject.api.privategroup.GroupMessage; import org.briarproject.api.privategroup.GroupMessageFactory; import org.briarproject.api.privategroup.GroupMessageHeader; @@ -59,6 +61,10 @@ import javax.inject.Inject; 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.privategroup.Visibility.INVISIBLE; +import static org.briarproject.api.privategroup.Visibility.REVEALED_BY_CONTACT; +import static org.briarproject.api.privategroup.Visibility.REVEALED_BY_YOU; +import static org.briarproject.api.privategroup.Visibility.VISIBLE; import static org.briarproject.api.privategroup.invitation.GroupInvitationManager.CLIENT_ID; import static org.briarproject.api.sync.ValidationManager.State.DELIVERED; import static org.briarproject.api.sync.ValidationManager.State.INVALID; @@ -69,13 +75,16 @@ import static org.junit.Assert.assertTrue; public class PrivateGroupManagerTest extends BriarIntegrationTest { - private LifecycleManager lifecycleManager0, lifecycleManager1; - private SyncSessionFactory sync0, sync1; - private PrivateGroupManager groupManager0, groupManager1; - private ContactManager contactManager0, contactManager1; - private ContactId contactId0, contactId1; - private IdentityManager identityManager0, identityManager1; - private LocalAuthor author0, author1; + private LifecycleManager lifecycleManager0, lifecycleManager1, + lifecycleManager2; + private SyncSessionFactory sync0, sync1, sync2; + private PrivateGroupManager groupManager0, groupManager1, groupManager2; + private ContactManager contactManager0, contactManager1, contactManager2; + private ContactId contactId01, contactId02, contactId1, contactId2; + private IdentityManager identityManager0, identityManager1, + identityManager2; + private LocalAuthor author0, author1, author2; + private DatabaseComponent db0, db1, db2; private PrivateGroup privateGroup0; private GroupId groupId0; @@ -101,13 +110,14 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { private final File testDir = TestUtils.getTestDirectory(); private final SecretKey master = TestUtils.getSecretKey(); private final int TIMEOUT = 15000; + private final String AUTHOR0 = "Author 0"; private final String AUTHOR1 = "Author 1"; private final String AUTHOR2 = "Author 2"; private static final Logger LOG = Logger.getLogger(PrivateGroupManagerTest.class.getName()); - private PrivateGroupManagerTestComponent t0, t1; + private PrivateGroupManagerTestComponent t0, t1, t2; @Rule public ExpectedException thrown = ExpectedException.none(); @@ -117,26 +127,36 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { PrivateGroupManagerTestComponent component = DaggerPrivateGroupManagerTestComponent.builder().build(); component.inject(this); - injectEagerSingletons(component); assertTrue(testDir.mkdirs()); - File t0Dir = new File(testDir, AUTHOR1); + File t0Dir = new File(testDir, AUTHOR0); t0 = DaggerPrivateGroupManagerTestComponent.builder() .testDatabaseModule(new TestDatabaseModule(t0Dir)).build(); injectEagerSingletons(t0); - File t1Dir = new File(testDir, AUTHOR2); + File t1Dir = new File(testDir, AUTHOR1); t1 = DaggerPrivateGroupManagerTestComponent.builder() .testDatabaseModule(new TestDatabaseModule(t1Dir)).build(); injectEagerSingletons(t1); + File t2Dir = new File(testDir, AUTHOR2); + t2 = DaggerPrivateGroupManagerTestComponent.builder() + .testDatabaseModule(new TestDatabaseModule(t2Dir)).build(); + injectEagerSingletons(t2); identityManager0 = t0.getIdentityManager(); identityManager1 = t1.getIdentityManager(); + identityManager2 = t2.getIdentityManager(); contactManager0 = t0.getContactManager(); contactManager1 = t1.getContactManager(); + contactManager2 = t2.getContactManager(); + db0 = t0.getDatabaseComponent(); + db1 = t1.getDatabaseComponent(); + db2 = t2.getDatabaseComponent(); groupManager0 = t0.getPrivateGroupManager(); groupManager1 = t1.getPrivateGroupManager(); + groupManager2 = t2.getPrivateGroupManager(); sync0 = t0.getSyncSessionFactory(); sync1 = t1.getSyncSessionFactory(); + sync2 = t2.getSyncSessionFactory(); // initialize waiters fresh for each test validationWaiter = new Waiter(); @@ -328,12 +348,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { 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); + Transaction txn0 = db0.startTransaction(false); + db0.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), true); + db0.commitTransaction(txn0); + db0.endTransaction(txn0); // author1 joins privateGroup0 with wrong timestamp joinTime = clock.currentTimeMillis(); @@ -353,12 +371,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { 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); + Transaction txn1 = db1.startTransaction(false); + db1.setVisibleToContact(txn1, contactId01, privateGroup0.getId(), true); + db1.commitTransaction(txn1); + db1.endTransaction(txn1); // sync join messages sync0To1(); @@ -399,12 +415,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { 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); + Transaction txn0 = db0.startTransaction(false); + db0.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), true); + db0.commitTransaction(txn0); + db0.endTransaction(txn0); // author1 joins privateGroup0 with wrong signature in join message joinTime = clock.currentTimeMillis(); @@ -424,12 +438,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { 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); + Transaction txn1 = db1.startTransaction(false); + db1.setVisibleToContact(txn1, contactId01, privateGroup0.getId(), true); + db1.commitTransaction(txn1); + db1.endTransaction(txn1); // sync join messages sync0To1(); @@ -445,6 +457,119 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { assertEquals(1, groupManager0.getHeaders(groupId0).size()); } + @Test + public void testGetMembers() throws Exception { + defaultInit(); + + Collection<GroupMember> members0 = groupManager0.getMembers(groupId0); + assertEquals(2, members0.size()); + for (GroupMember m : members0) { + if (m.getAuthor().equals(author0)) { + assertEquals(VISIBLE, m.getVisibility()); + } else { + assertEquals(author1, m.getAuthor()); + assertEquals(VISIBLE, m.getVisibility()); + } + } + + Collection<GroupMember> members1 = groupManager1.getMembers(groupId0); + assertEquals(2, members1.size()); + for (GroupMember m : members0) { + if (m.getAuthor().equals(author1)) { + assertEquals(VISIBLE, m.getVisibility()); + } else { + assertEquals(author0, m.getAuthor()); + assertEquals(VISIBLE, m.getVisibility()); + } + } + } + + @Test + public void testRevealingRelationships() throws Exception { + defaultInit(); + + // make group visible to 2 + Transaction txn0 = db0.startTransaction(false); + db0.setVisibleToContact(txn0, contactId2, privateGroup0.getId(), true); + db0.commitTransaction(txn0); + db0.endTransaction(txn0); + + // author2 joins privateGroup0 + long joinTime = clock.currentTimeMillis(); + long inviteTime = joinTime - 1; + Group invitationGroup = contactGroupFactory + .createContactGroup(CLIENT_ID, author0.getId(), + author2.getId()); + BdfList toSign = BdfList.of(0, inviteTime, invitationGroup.getId(), + privateGroup0.getId()); + byte[] creatorSignature = + clientHelper.sign(toSign, author0.getPrivateKey()); + GroupMessage joinMsg2 = groupMessageFactory + .createJoinMessage(privateGroup0.getId(), joinTime, author2, + inviteTime, creatorSignature); + Transaction txn2 = db2.startTransaction(false); + groupManager2.addPrivateGroup(txn2, privateGroup0, joinMsg2); + + // make group visible to 0 + db2.setVisibleToContact(txn2, contactId01, privateGroup0.getId(), true); + db2.commitTransaction(txn2); + db2.endTransaction(txn2); + + // sync join messages + deliverMessage(sync2, contactId2, sync0, contactId02, "2 to 0"); + deliveryWaiter.await(TIMEOUT, 1); + deliverMessage(sync0, contactId02, sync2, contactId2, "0 to 2"); + deliveryWaiter.await(TIMEOUT, 2); + sync0To1(); + deliveryWaiter.await(TIMEOUT, 1); + + // check that everybody sees everybody else as joined + Collection<GroupMember> members0 = groupManager0.getMembers(groupId0); + assertEquals(3, members0.size()); + Collection<GroupMember> members1 = groupManager1.getMembers(groupId0); + assertEquals(3, members1.size()); + Collection<GroupMember> members2 = groupManager2.getMembers(groupId0); + assertEquals(3, members2.size()); + + // assert that contact relationship is not revealed initially + for (GroupMember m : members1) { + if (m.getAuthor().equals(author2)) { + assertEquals(INVISIBLE, m.getVisibility()); + } + } + for (GroupMember m : members2) { + if (m.getAuthor().equals(author1)) { + assertEquals(INVISIBLE, m.getVisibility()); + } + } + + // reveal contact relationship + Transaction txn1 = db1.startTransaction(false); + groupManager1 + .relationshipRevealed(txn1, groupId0, author2.getId(), false); + db1.commitTransaction(txn1); + db1.endTransaction(txn1); + txn2 = db2.startTransaction(false); + groupManager2 + .relationshipRevealed(txn2, groupId0, author1.getId(), true); + db2.commitTransaction(txn2); + db2.endTransaction(txn2); + + // assert that contact relationship is now revealed properly + members1 = groupManager1.getMembers(groupId0); + for (GroupMember m : members1) { + if (m.getAuthor().equals(author2)) { + assertEquals(REVEALED_BY_YOU, m.getVisibility()); + } + } + members2 = groupManager2.getMembers(groupId0); + for (GroupMember m : members2) { + if (m.getAuthor().equals(author1)) { + assertEquals(REVEALED_BY_CONTACT, m.getVisibility()); + } + } + } + @After public void tearDown() throws Exception { stopLifecycles(); @@ -483,7 +608,7 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { byte[] publicKey0 = keyPair0.getPublic().getEncoded(); byte[] privateKey0 = keyPair0.getPrivate().getEncoded(); author0 = authorFactory - .createLocalAuthor(AUTHOR1, publicKey0, privateKey0); + .createLocalAuthor(AUTHOR0, publicKey0, privateKey0); identityManager0.registerLocalAuthor(author0); privateGroup0 = privateGroupFactory.createPrivateGroup("Testgroup", author0); @@ -493,21 +618,34 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { byte[] publicKey1 = keyPair1.getPublic().getEncoded(); byte[] privateKey1 = keyPair1.getPrivate().getEncoded(); author1 = authorFactory - .createLocalAuthor(AUTHOR2, publicKey1, privateKey1); + .createLocalAuthor(AUTHOR1, publicKey1, privateKey1); identityManager1.registerLocalAuthor(author1); + + KeyPair keyPair2 = crypto.generateSignatureKeyPair(); + byte[] publicKey2 = keyPair2.getPublic().getEncoded(); + byte[] privateKey2 = keyPair2.getPrivate().getEncoded(); + author2 = authorFactory + .createLocalAuthor(AUTHOR2, publicKey2, privateKey2); + identityManager2.registerLocalAuthor(author2); } private void addDefaultContacts() throws DbException { - // sharer adds invitee as contact - contactId1 = contactManager0.addContact(author1, - author0.getId(), master, clock.currentTimeMillis(), true, - true, true - ); - // invitee adds sharer back - contactId0 = contactManager1.addContact(author0, - author1.getId(), master, clock.currentTimeMillis(), true, - true, true - ); + // creator adds invitee as contact + contactId1 = contactManager0 + .addContact(author1, author0.getId(), master, + clock.currentTimeMillis(), true, true, true); + // invitee adds creator back + contactId01 = contactManager1 + .addContact(author0, author1.getId(), master, + clock.currentTimeMillis(), true, true, true); + // creator adds invitee as contact + contactId2 = contactManager0 + .addContact(author2, author0.getId(), master, + clock.currentTimeMillis(), true, true, true); + // invitee adds creator back + contactId02 = contactManager2 + .addContact(author0, author2.getId(), master, + clock.currentTimeMillis(), true, true, true); } private void listenToEvents() { @@ -515,6 +653,8 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { t0.getEventBus().addListener(listener0); Listener listener1 = new Listener(); t1.getEventBus().addListener(listener1); + Listener listener2 = new Listener(); + t2.getEventBus().addListener(listener2); } private void addGroup() throws Exception { @@ -527,12 +667,10 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { 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); + Transaction txn0 = db0.startTransaction(false); + db0.setVisibleToContact(txn0, contactId1, privateGroup0.getId(), true); + db0.commitTransaction(txn0); + db0.endTransaction(txn0); // author1 joins privateGroup0 joinTime = clock.currentTimeMillis(); @@ -547,17 +685,15 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { GroupMessage joinMsg1 = groupMessageFactory .createJoinMessage(privateGroup0.getId(), joinTime, author1, inviteTime, creatorSignature); - groupManager1.addPrivateGroup(privateGroup0, joinMsg1); - assertEquals(joinMsg1.getMessage().getId(), - groupManager1.getPreviousMsgId(groupId0)); + Transaction txn1 = db1.startTransaction(false); + groupManager1.addPrivateGroup(txn1, privateGroup0, joinMsg1); // 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); + db1.setVisibleToContact(txn1, contactId01, privateGroup0.getId(), true); + db1.commitTransaction(txn1); + db1.endTransaction(txn1); + assertEquals(joinMsg1.getMessage().getId(), + groupManager1.getPreviousMsgId(groupId0)); // sync join messages sync0To1(); @@ -567,11 +703,11 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { } private void sync0To1() throws IOException, TimeoutException { - deliverMessage(sync0, contactId0, sync1, contactId1, "0 to 1"); + deliverMessage(sync0, contactId01, sync1, contactId1, "0 to 1"); } private void sync1To0() throws IOException, TimeoutException { - deliverMessage(sync1, contactId1, sync0, contactId0, "1 to 0"); + deliverMessage(sync1, contactId1, sync0, contactId01, "1 to 0"); } private void deliverMessage(SyncSessionFactory fromSync, ContactId fromId, @@ -600,18 +736,23 @@ public class PrivateGroupManagerTest extends BriarIntegrationTest { // Start the lifecycle manager and wait for it to finish lifecycleManager0 = t0.getLifecycleManager(); lifecycleManager1 = t1.getLifecycleManager(); - lifecycleManager0.startServices(AUTHOR1); - lifecycleManager1.startServices(AUTHOR2); + lifecycleManager2 = t2.getLifecycleManager(); + lifecycleManager0.startServices(AUTHOR0); + lifecycleManager1.startServices(AUTHOR1); + lifecycleManager2.startServices(AUTHOR2); lifecycleManager0.waitForStartup(); lifecycleManager1.waitForStartup(); + lifecycleManager2.waitForStartup(); } private void stopLifecycles() throws InterruptedException { // Clean up lifecycleManager0.stopServices(); lifecycleManager1.stopServices(); + lifecycleManager2.stopServices(); lifecycleManager0.waitForShutdown(); lifecycleManager1.waitForShutdown(); + lifecycleManager2.waitForShutdown(); } private void injectEagerSingletons( diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java index e59f6cdacab3fef325737597d2c1a59d105aa5b9..75a194d3246ddbba327f39d0c64d7d1c2dd45b72 100644 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupActivity.java @@ -105,7 +105,7 @@ public class GroupActivity extends ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { actionBar.setSubtitle(getString(R.string.groups_created_by, - group.getAuthor().getName())); + group.getCreator().getName())); } controller.isCreator(group, new UiResultExceptionHandler<Boolean, DbException>(this) { diff --git a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java index 89de8033149d97050882402892d25185e36cd6d8..e0ba5d3d89fcd318a334117813d2ccebe90de489 100644 --- a/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java +++ b/briar-android/src/org/briarproject/android/privategroup/conversation/GroupControllerImpl.java @@ -196,7 +196,7 @@ public class GroupControllerImpl extends try { LocalAuthor author = identityManager.getLocalAuthor(); boolean isCreator = - author.getId().equals(group.getAuthor().getId()); + author.getId().equals(group.getCreator().getId()); handler.onResult(isCreator); } catch (DbException e) { if (LOG.isLoggable(WARNING)) diff --git a/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java b/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java index 31c6458a654bc0fbbd409e2e1810b85a36836aab..ded8051817fd42bedf7ca52bcefd95be37e85ac6 100644 --- a/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java +++ b/briar-android/src/org/briarproject/android/privategroup/list/GroupItem.java @@ -43,7 +43,7 @@ class GroupItem { } Author getCreator() { - return privateGroup.getAuthor(); + return privateGroup.getCreator(); } String getName() { diff --git a/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java b/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java index ef0c73d91f2e1a5cacccea458d6f1bbe0a44c92f..9ff87a510b2d04fc92af80ae264d9121ef183265 100644 --- a/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java +++ b/briar-android/src/org/briarproject/android/privategroup/memberlist/MemberListItem.java @@ -7,6 +7,8 @@ import org.briarproject.api.privategroup.GroupMember; import javax.annotation.concurrent.Immutable; +import static org.briarproject.api.privategroup.Visibility.INVISIBLE; + @Immutable @NotNullByDefault class MemberListItem { @@ -17,7 +19,7 @@ class MemberListItem { public MemberListItem(GroupMember groupMember) { this.member = groupMember.getAuthor(); - this.sharing = groupMember.isShared(); + this.sharing = groupMember.getVisibility() != INVISIBLE; // TODO #732 this.status = groupMember.getStatus(); } diff --git a/briar-api/src/org/briarproject/api/privategroup/GroupMember.java b/briar-api/src/org/briarproject/api/privategroup/GroupMember.java index e433aa792b30ad07efc8d1944122d856fa4f0bcd..2638dfe706b6e37b4006e45fb6f335d776825714 100644 --- a/briar-api/src/org/briarproject/api/privategroup/GroupMember.java +++ b/briar-api/src/org/briarproject/api/privategroup/GroupMember.java @@ -12,12 +12,12 @@ public class GroupMember { private final Author author; private final Status status; - private final boolean shared; + private final Visibility visibility; - public GroupMember(Author author, Status status, boolean shared) { + public GroupMember(Author author, Status status, Visibility visibility) { this.author = author; this.status = status; - this.shared = shared; + this.visibility = visibility; } public Author getAuthor() { @@ -28,8 +28,8 @@ public class GroupMember { return status; } - public boolean isShared() { - return shared; + public Visibility getVisibility() { + return visibility; } } diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java index ab8d520590072fc3bd2e6328a565f92173a47ab9..3095d2abbac6faf80d053fe42ce824a7d750f804 100644 --- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java +++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroup.java @@ -12,20 +12,22 @@ import javax.annotation.concurrent.Immutable; @NotNullByDefault public class PrivateGroup extends NamedGroup implements Shareable { - private final Author author; + private final Author creator; - public PrivateGroup(Group group, String name, Author author, byte[] salt) { + public PrivateGroup(Group group, String name, Author creator, byte[] salt) { super(group, name, salt); - this.author = author; + this.creator = creator; } - public Author getAuthor() { - return author; + public Author getCreator() { + return creator; } @Override public boolean equals(Object o) { - return o instanceof PrivateGroup && super.equals(o); + return o instanceof PrivateGroup && + creator.equals(((PrivateGroup) o).getCreator()) && + super.equals(o); } } diff --git a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java index 4a96a843b999537f809ca6fa99a33c69aab5cdab..f0197e0969ec7ca0f2da62ed77b54937f17166e5 100644 --- a/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java +++ b/briar-api/src/org/briarproject/api/privategroup/PrivateGroupManager.java @@ -1,9 +1,11 @@ package org.briarproject.api.privategroup; +import org.briarproject.api.FormatException; import org.briarproject.api.clients.MessageTracker; import org.briarproject.api.db.DbException; import org.briarproject.api.db.Transaction; import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorId; import org.briarproject.api.nullsafety.NotNullByDefault; import org.briarproject.api.sync.ClientId; import org.briarproject.api.sync.GroupId; @@ -20,16 +22,16 @@ public interface PrivateGroupManager extends MessageTracker { ClientId CLIENT_ID = new ClientId("org.briarproject.briar.privategroup"); /** - * Adds a new private group and joins it. + * Adds a new private group and joins it as the creator. * * @param group The private group to add - * @param joinMsg The new member's join message + * @param joinMsg The creators's join message */ void addPrivateGroup(PrivateGroup group, GroupMessage joinMsg) throws DbException; /** - * Adds a new private group and joins it. + * Adds a new private group and joins it as a member. * * @param group The private group to add * @param joinMsg The new member's join message @@ -103,6 +105,17 @@ public interface PrivateGroupManager extends MessageTracker { */ boolean isMember(Transaction txn, GroupId g, Author a) throws DbException; + /** + * This method needs to be called when a contact relationship + * has been revealed between you and the Author with AuthorId a + * in the Group identified by the GroupId g. + * + * @param byContact true if the remote contact has revealed + * the relationship first. Otherwise false. + */ + void relationshipRevealed(Transaction txn, GroupId g, AuthorId a, + boolean byContact) throws FormatException, DbException; + /** * Registers a hook to be called when members are added * or groups are removed. diff --git a/briar-api/src/org/briarproject/api/privategroup/Visibility.java b/briar-api/src/org/briarproject/api/privategroup/Visibility.java new file mode 100644 index 0000000000000000000000000000000000000000..9c535e86b8346c9681d8ce03e449a1e6a266edf4 --- /dev/null +++ b/briar-api/src/org/briarproject/api/privategroup/Visibility.java @@ -0,0 +1,25 @@ +package org.briarproject.api.privategroup; + +public enum Visibility { + + INVISIBLE(0), + VISIBLE(1), + REVEALED_BY_YOU(2), + REVEALED_BY_CONTACT(3); + + int value; + + Visibility(int value) { + this.value = value; + } + + public static Visibility valueOf(int value) { + for (Visibility v : values()) if (v.value == value) return v; + throw new IllegalArgumentException(); + } + + public int getInt() { + return value; + } + +} diff --git a/briar-core/src/org/briarproject/privategroup/GroupConstants.java b/briar-core/src/org/briarproject/privategroup/GroupConstants.java index 8bc82be42b4d6146282ec1d90496a7df1c35d349..14d9fb22efc46d29859633e777d11412b8cfcfab 100644 --- a/briar-core/src/org/briarproject/privategroup/GroupConstants.java +++ b/briar-core/src/org/briarproject/privategroup/GroupConstants.java @@ -9,13 +9,15 @@ interface Constants { String KEY_TIMESTAMP = "timestamp"; String KEY_READ = MSG_KEY_READ; String KEY_PARENT_MSG_ID = "parentMsgId"; - String KEY_NEW_MEMBER_MSG_ID = "newMemberMsgId"; String KEY_PREVIOUS_MSG_ID = "previousMsgId"; String KEY_MEMBER_ID = "memberId"; String KEY_MEMBER_NAME = "memberName"; String KEY_MEMBER_PUBLIC_KEY = "memberPublicKey"; - String KEY_MEMBERS = "members"; - String KEY_DISSOLVED = "dissolved"; + String GROUP_KEY_MEMBERS = "members"; + String GROUP_KEY_OUR_GROUP = "ourGroup"; + String GROUP_KEY_CREATOR_ID = "creatorId"; + String GROUP_KEY_DISSOLVED = "dissolved"; + String GROUP_KEY_VISIBILITY = "visibility"; } diff --git a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java index dda084d2e1e4fa7c13495468ab036dcf15692ee2..6b9a59dae9d7f1788fb5c67d391a9835b8dcba4b 100644 --- a/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java +++ b/briar-core/src/org/briarproject/privategroup/GroupMessageValidator.java @@ -98,7 +98,7 @@ class GroupMessageValidator extends BdfMessageValidator { PrivateGroup pg = privateGroupFactory.parsePrivateGroup(g); // invite is null if the member is the creator of the private group - Author creator = pg.getAuthor(); + Author creator = pg.getCreator(); BdfList invite = body.getOptionalList(3); if (invite == null) { if (!member.equals(creator)) diff --git a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java index b6117f65f198a4029d1eb8021069c031cacf3366..a407f1fcf3b1afee01f30a260230ba30647ea08a 100644 --- a/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java +++ b/briar-core/src/org/briarproject/privategroup/PrivateGroupManagerImpl.java @@ -2,8 +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.clients.ProtocolStateException; import org.briarproject.api.data.BdfDictionary; import org.briarproject.api.data.BdfEntry; import org.briarproject.api.data.BdfList; @@ -27,6 +26,7 @@ import org.briarproject.api.privategroup.MessageType; import org.briarproject.api.privategroup.PrivateGroup; import org.briarproject.api.privategroup.PrivateGroupFactory; import org.briarproject.api.privategroup.PrivateGroupManager; +import org.briarproject.api.privategroup.Visibility; import org.briarproject.api.sync.Group; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.Message; @@ -48,12 +48,17 @@ import java.util.concurrent.CopyOnWriteArrayList; import javax.inject.Inject; 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.VERIFIED; import static org.briarproject.api.privategroup.MessageType.JOIN; import static org.briarproject.api.privategroup.MessageType.POST; -import static org.briarproject.privategroup.Constants.KEY_DISSOLVED; -import static org.briarproject.privategroup.Constants.KEY_MEMBERS; +import static org.briarproject.api.privategroup.Visibility.INVISIBLE; +import static org.briarproject.api.privategroup.Visibility.REVEALED_BY_CONTACT; +import static org.briarproject.api.privategroup.Visibility.REVEALED_BY_YOU; +import static org.briarproject.api.privategroup.Visibility.VISIBLE; +import static org.briarproject.privategroup.Constants.GROUP_KEY_CREATOR_ID; +import static org.briarproject.privategroup.Constants.GROUP_KEY_DISSOLVED; +import static org.briarproject.privategroup.Constants.GROUP_KEY_MEMBERS; +import static org.briarproject.privategroup.Constants.GROUP_KEY_OUR_GROUP; +import static org.briarproject.privategroup.Constants.GROUP_KEY_VISIBILITY; 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; @@ -88,7 +93,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements throws DbException { Transaction txn = db.startTransaction(false); try { - addPrivateGroup(txn, group, joinMsg); + addPrivateGroup(txn, group, joinMsg, true); db.commitTransaction(txn); } finally { db.endTransaction(txn); @@ -98,11 +103,19 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements @Override public void addPrivateGroup(Transaction txn, PrivateGroup group, GroupMessage joinMsg) throws DbException { + addPrivateGroup(txn, group, joinMsg, false); + } + + private void addPrivateGroup(Transaction txn, PrivateGroup group, + GroupMessage joinMsg, boolean creator) throws DbException { try { db.addGroup(txn, group.getGroup()); + AuthorId creatorId = joinMsg.getMember().getId(); BdfDictionary meta = BdfDictionary.of( - new BdfEntry(KEY_MEMBERS, new BdfList()), - new BdfEntry(KEY_DISSOLVED, false) + new BdfEntry(GROUP_KEY_MEMBERS, new BdfList()), + new BdfEntry(GROUP_KEY_CREATOR_ID, creatorId), + new BdfEntry(GROUP_KEY_OUR_GROUP, creator), + new BdfEntry(GROUP_KEY_DISSOLVED, false) ); clientHelper.mergeGroupMetadata(txn, group.getId(), meta); joinPrivateGroup(txn, joinMsg); @@ -118,7 +131,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements addMessageMetadata(meta, m, true); clientHelper.addLocalMessage(txn, m.getMessage(), meta, true); trackOutgoingMessage(txn, m.getMessage()); - addMember(txn, m.getMessage().getGroupId(), m.getMember()); + addMember(txn, m.getMessage().getGroupId(), m.getMember(), VISIBLE); setPreviousMsgId(txn, m.getMessage().getGroupId(), m.getMessage().getId()); } @@ -171,7 +184,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements public void markGroupDissolved(Transaction txn, GroupId g) throws DbException { BdfDictionary meta = BdfDictionary.of( - new BdfEntry(KEY_DISSOLVED, true) + new BdfEntry(GROUP_KEY_DISSOLVED, true) ); try { clientHelper.mergeGroupMetadata(txn, g, meta); @@ -283,7 +296,7 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements public boolean isDissolved(GroupId g) throws DbException { try { BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(g); - return meta.getBoolean(KEY_DISSOLVED); + return meta.getBoolean(GROUP_KEY_DISSOLVED); } catch (FormatException e) { throw new DbException(e); } @@ -376,18 +389,11 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements Transaction txn = db.startTransaction(true); try { Collection<GroupMember> members = new ArrayList<GroupMember>(); - Collection<Author> authors = getMembers(txn, g); - for (Author a : authors) { - Status status = identityManager.getAuthorStatus(txn, a.getId()); - boolean shared = false; - if (status == VERIFIED || status == UNVERIFIED) { - Collection<Contact> contacts = - db.getContactsByAuthorId(txn, a.getId()); - if (contacts.size() != 1) throw new DbException(); - ContactId c = contacts.iterator().next().getId(); - shared = db.isVisibleToContact(txn, c, g); - } - members.add(new GroupMember(a, status, shared)); + Map<Author, Visibility> authors = getMembers(txn, g); + for (Entry<Author, Visibility> m : authors.entrySet()) { + Status status = identityManager + .getAuthorStatus(txn, m.getKey().getId()); + members.add(new GroupMember(m.getKey(), status, m.getValue())); } db.commitTransaction(txn); return members; @@ -396,17 +402,19 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements } } - private Collection<Author> getMembers(Transaction txn, GroupId g) + private Map<Author, Visibility> getMembers(Transaction txn, GroupId g) throws DbException { try { - Collection<Author> members = new ArrayList<Author>(); BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, g); - BdfList list = meta.getList(KEY_MEMBERS); + BdfList list = meta.getList(GROUP_KEY_MEMBERS); + Map<Author, Visibility> members = + new HashMap<Author, Visibility>(list.size()); for (Object o : list) { BdfDictionary d = (BdfDictionary) o; Author member = getAuthor(d); - members.add(member); + Visibility v = getVisibility(d); + members.put(member, v); } return members; } catch (FormatException e) { @@ -417,12 +425,34 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements @Override public boolean isMember(Transaction txn, GroupId g, Author a) throws DbException { - for (Author member : getMembers(txn, g)) { + for (Author member : getMembers(txn, g).keySet()) { if (member.equals(a)) return true; } return false; } + @Override + public void relationshipRevealed(Transaction txn, GroupId g, AuthorId a, + boolean byContact) throws FormatException, DbException { + BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, g); + BdfList members = meta.getList(GROUP_KEY_MEMBERS); + boolean foundMember = false; + for (Object o : members) { + BdfDictionary d = (BdfDictionary) o; + AuthorId memberId = new AuthorId(d.getRaw(KEY_MEMBER_ID)); + if (a.equals(memberId)) { + foundMember = true; + Visibility vOld = getVisibility(d); + if (vOld != INVISIBLE) throw new ProtocolStateException(); + Visibility vNew = + byContact ? REVEALED_BY_CONTACT : REVEALED_BY_YOU; + d.put(GROUP_KEY_VISIBILITY, vNew.getInt()); + } + } + if (!foundMember) throw new ProtocolStateException(); + clientHelper.mergeGroupMetadata(txn, g, meta); + } + @Override public void registerPrivateGroupHook(PrivateGroupHook hook) { hooks.add(hook); @@ -432,47 +462,14 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements protected boolean incomingMessage(Transaction txn, Message m, BdfList body, BdfDictionary meta) throws DbException, FormatException { - long timestamp = meta.getLong(KEY_TIMESTAMP); MessageType type = MessageType.valueOf(meta.getLong(KEY_TYPE).intValue()); switch (type) { case JOIN: - addMember(txn, m.getGroupId(), getAuthor(meta)); - trackIncomingMessage(txn, m); - attachGroupMessageAddedEvent(txn, m, meta, false); + handleJoinMessage(txn, m, meta); return true; case POST: - // timestamp must be greater than the timestamps of parent post - byte[] parentIdBytes = meta.getOptionalRaw(KEY_PARENT_MSG_ID); - if (parentIdBytes != null) { - MessageId parentId = new MessageId(parentIdBytes); - BdfDictionary parentMeta = clientHelper - .getMessageMetadataAsDictionary(txn, parentId); - if (timestamp <= parentMeta.getLong(KEY_TIMESTAMP)) - throw new FormatException(); - MessageType parentType = MessageType - .valueOf(parentMeta.getLong(KEY_TYPE).intValue()); - if (parentType != POST) - throw new FormatException(); - } - // and the member's previous message - byte[] previousMsgIdBytes = meta.getRaw(KEY_PREVIOUS_MSG_ID); - MessageId previousMsgId = new MessageId(previousMsgIdBytes); - BdfDictionary previousMeta = clientHelper - .getMessageMetadataAsDictionary(txn, previousMsgId); - if (timestamp <= previousMeta.getLong(KEY_TIMESTAMP)) - throw new FormatException(); - // previous message must be from same member - if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID), - previousMeta.getRaw(KEY_MEMBER_ID))) - throw new FormatException(); - // previous message must be a POST or JOIN - MessageType previousType = MessageType - .valueOf(previousMeta.getLong(KEY_TYPE).intValue()); - if (previousType != JOIN && previousType != POST) - throw new FormatException(); - trackIncomingMessage(txn, m); - attachGroupMessageAddedEvent(txn, m, meta, false); + handleGroupMessage(txn, m, meta); return true; default: // the validator should only let valid types pass @@ -480,6 +477,63 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements } } + private void handleJoinMessage(Transaction txn, Message m, + BdfDictionary meta) throws FormatException, DbException { + // find out if contact relationship is visible and then add new member + Author member = getAuthor(meta); + BdfDictionary groupMeta = clientHelper + .getGroupMetadataAsDictionary(txn, m.getGroupId()); + boolean ourGroup = groupMeta.getBoolean(GROUP_KEY_OUR_GROUP); + Visibility v = VISIBLE; + if (!ourGroup) { + AuthorId creatorId = new AuthorId( + groupMeta.getRaw(GROUP_KEY_CREATOR_ID)); + if (!creatorId.equals(member.getId())) + v = INVISIBLE; + } + addMember(txn, m.getGroupId(), member, v); + // track message and broadcast event + trackIncomingMessage(txn, m); + attachGroupMessageAddedEvent(txn, m, meta, false); + } + + private void handleGroupMessage(Transaction txn, Message m, + BdfDictionary meta) throws FormatException, DbException { + // timestamp must be greater than the timestamps of parent post + long timestamp = meta.getLong(KEY_TIMESTAMP); + byte[] parentIdBytes = meta.getOptionalRaw(KEY_PARENT_MSG_ID); + if (parentIdBytes != null) { + MessageId parentId = new MessageId(parentIdBytes); + BdfDictionary parentMeta = clientHelper + .getMessageMetadataAsDictionary(txn, parentId); + if (timestamp <= parentMeta.getLong(KEY_TIMESTAMP)) + throw new FormatException(); + MessageType parentType = MessageType + .valueOf(parentMeta.getLong(KEY_TYPE).intValue()); + if (parentType != POST) + throw new FormatException(); + } + // and the member's previous message + byte[] previousMsgIdBytes = meta.getRaw(KEY_PREVIOUS_MSG_ID); + MessageId previousMsgId = new MessageId(previousMsgIdBytes); + BdfDictionary previousMeta = clientHelper + .getMessageMetadataAsDictionary(txn, previousMsgId); + if (timestamp <= previousMeta.getLong(KEY_TIMESTAMP)) + throw new FormatException(); + // previous message must be from same member + if (!Arrays.equals(meta.getRaw(KEY_MEMBER_ID), + previousMeta.getRaw(KEY_MEMBER_ID))) + throw new FormatException(); + // previous message must be a POST or JOIN + MessageType previousType = MessageType + .valueOf(previousMeta.getLong(KEY_TYPE).intValue()); + if (previousType != JOIN && previousType != POST) + throw new FormatException(); + // track message and broadcast event + trackIncomingMessage(txn, m); + attachGroupMessageAddedEvent(txn, m, meta, false); + } + private void attachGroupMessageAddedEvent(Transaction txn, Message m, BdfDictionary meta, boolean local) throws DbException, FormatException { @@ -489,15 +543,16 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements txn.attach(e); } - private void addMember(Transaction txn, GroupId g, Author a) + private void addMember(Transaction txn, GroupId g, Author a, Visibility v) throws DbException, FormatException { BdfDictionary meta = clientHelper.getGroupMetadataAsDictionary(txn, g); - BdfList members = meta.getList(KEY_MEMBERS); + BdfList members = meta.getList(GROUP_KEY_MEMBERS); members.add(BdfDictionary.of( new BdfEntry(KEY_MEMBER_ID, a.getId()), new BdfEntry(KEY_MEMBER_NAME, a.getName()), - new BdfEntry(KEY_MEMBER_PUBLIC_KEY, a.getPublicKey()) + new BdfEntry(KEY_MEMBER_PUBLIC_KEY, a.getPublicKey()), + new BdfEntry(GROUP_KEY_VISIBILITY, v.getInt()) )); clientHelper.mergeGroupMetadata(txn, g, meta); for (PrivateGroupHook hook : hooks) { @@ -512,4 +567,10 @@ public class PrivateGroupManagerImpl extends BdfIncomingMessageHook implements return new Author(authorId, name, publicKey); } + private Visibility getVisibility(BdfDictionary meta) + throws FormatException { + return Visibility + .valueOf(meta.getLong(GROUP_KEY_VISIBILITY).intValue()); + } + } diff --git a/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java b/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java index b3048c7779a76b9e878f5cbe0037fb3769ace04c..c62e4831d6ee3d92b3a0b5707a6f88e2bf6a444b 100644 --- a/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java +++ b/briar-core/src/org/briarproject/privategroup/invitation/AbstractProtocolEngine.java @@ -105,7 +105,7 @@ abstract class AbstractProtocolEngine<S extends Session> } Message m = messageEncoder.encodeInviteMessage( session.getContactGroupId(), privateGroup.getId(), - timestamp, privateGroup.getName(), privateGroup.getAuthor(), + timestamp, privateGroup.getName(), privateGroup.getCreator(), privateGroup.getSalt(), message, signature); sendMessage(txn, m, INVITE, privateGroup.getId(), true); return m;