Group invite link epoch support.

This commit is contained in:
Alan Evans
2020-08-18 14:26:09 -03:00
committed by Greyson Parrelli
parent e006306036
commit 477bb45df7
31 changed files with 2366 additions and 205 deletions

View File

@@ -5,16 +5,19 @@ import com.google.protobuf.ByteString;
import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.UUID;
@@ -23,12 +26,28 @@ import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.asMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class DecryptedGroupUtil_apply_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be applied by {@link DecryptedGroupUtil#apply}.
*/
@Test
public void ensure_DecryptedGroupUtil_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
19, maxFieldFound);
}
@Test
public void apply_revision() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
@@ -580,4 +599,168 @@ public final class DecryptedGroupUtil_apply_Test {
.build(),
newGroup);
}
@Test
public void invite_link_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setAccessControl(AccessControl.newBuilder()
.setAttributes(AccessControl.AccessRequired.MEMBER)
.setMembers(AccessControl.AccessRequired.MEMBER)
.setAddFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
.build())
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(11)
.setAccessControl(AccessControl.newBuilder()
.setAttributes(AccessControl.AccessRequired.MEMBER)
.setMembers(AccessControl.AccessRequired.MEMBER)
.setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
.build())
.build(),
newGroup);
}
@Test
public void apply_new_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedRequestingMember member1 = requestingMember(UUID.randomUUID());
DecryptedRequestingMember member2 = requestingMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.addRequestingMembers(member1)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.addNewRequestingMembers(member2)
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(11)
.addRequestingMembers(member1)
.addRequestingMembers(member2)
.build(),
newGroup);
}
@Test
public void apply_remove_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedRequestingMember member1 = requestingMember(UUID.randomUUID());
DecryptedRequestingMember member2 = requestingMember(UUID.randomUUID());
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(13)
.addRequestingMembers(member1)
.addRequestingMembers(member2)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(14)
.addDeleteRequestingMembers(member1.getUuid())
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(14)
.addRequestingMembers(member2)
.build(),
newGroup);
}
@Test
public void promote_requesting_member() throws NotAbleToApplyGroupV2ChangeException {
UUID uuid1 = UUID.randomUUID();
UUID uuid2 = UUID.randomUUID();
UUID uuid3 = UUID.randomUUID();
ProfileKey profileKey1 = newProfileKey();
ProfileKey profileKey2 = newProfileKey();
ProfileKey profileKey3 = newProfileKey();
DecryptedRequestingMember member1 = requestingMember(uuid1, profileKey1);
DecryptedRequestingMember member2 = requestingMember(uuid2, profileKey2);
DecryptedRequestingMember member3 = requestingMember(uuid3, profileKey3);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(13)
.addRequestingMembers(member1)
.addRequestingMembers(member2)
.addRequestingMembers(member3)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(14)
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUuid(member1.getUuid()))
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUuid(member2.getUuid()))
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(14)
.addMembers(member(uuid1, profileKey1))
.addMembers(admin(uuid2, profileKey2))
.addRequestingMembers(member3)
.build(),
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cannot_apply_promote_requesting_member_without_a_role() throws NotAbleToApplyGroupV2ChangeException {
UUID uuid = UUID.randomUUID();
DecryptedRequestingMember member = requestingMember(uuid);
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(13)
.addRequestingMembers(member)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(14)
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
.setUuid(member.getUuid()))
.build());
}
@Test
public void invite_link_password() throws NotAbleToApplyGroupV2ChangeException {
ByteString password1 = ByteString.copyFrom(Util.getSecretBytes(16));
ByteString password2 = ByteString.copyFrom(Util.getSecretBytes(16));
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setInviteLinkPassword(password1)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.setNewInviteLinkPassword(password2)
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(11)
.setInviteLinkPassword(password2)
.build(),
newGroup);
}
@Test
public void invite_link_password_not_changed() throws NotAbleToApplyGroupV2ChangeException {
ByteString password = ByteString.copyFrom(Util.getSecretBytes(16));
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setInviteLinkPassword(password)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(11)
.setInviteLinkPassword(password)
.build(),
newGroup);
}
}

View File

@@ -1,8 +1,12 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.whispersystems.signalservice.api.util.UuidUtil;
@@ -31,7 +35,7 @@ public final class DecryptedGroupUtil_empty_Test {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("DecryptedGroupUtil and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
DecryptedGroupUtil.MAX_CHANGE_FIELD, maxFieldFound);
19, maxFieldFound);
}
@Test
@@ -158,4 +162,54 @@ public final class DecryptedGroupUtil_empty_Test {
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_modify_add_from_invite_link_access_field_15() {
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_an_add_requesting_member_field_16() {
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
.addNewRequestingMembers(DecryptedRequestingMember.getDefaultInstance())
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_a_delete_requesting_member_field_17() {
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
.addDeleteRequestingMembers(ByteString.copyFrom(new byte[16]))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_a_promote_requesting_member_field_18() {
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
.addPromoteRequestingMembers(DecryptedApproveMember.getDefaultInstance())
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
@Test
public void not_empty_with_a_new_invite_link_password_19() {
DecryptedGroupChange change = DecryptedGroupChange.newBuilder()
.setNewInviteLinkPassword(ByteString.copyFrom(new byte[16]))
.build();
assertFalse(DecryptedGroupUtil.changeIsEmpty(change));
assertFalse(DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(change));
}
}

View File

@@ -1,5 +1,7 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
@@ -8,21 +10,40 @@ import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.newProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeReconstructTest {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be detected by {@link GroupChangeReconstruct#reconstructGroupChange}.
*/
@Test
public void ensure_GroupChangeReconstruct_knows_about_all_fields_of_DecryptedGroup() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
assertEquals("GroupChangeReconstruct and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
10, maxFieldFound);
}
@Test
public void empty_to_empty() {
DecryptedGroup from = DecryptedGroup.newBuilder().build();
@@ -219,4 +240,122 @@ public final class GroupChangeReconstructTest {
assertEquals(DecryptedGroupChange.newBuilder().addModifiedProfileKeys(withProfileKey(admin(uuid),profileKey2)).build(), decryptedGroupChange);
}
@Test
public void new_invite_access() {
DecryptedGroup from = DecryptedGroup.newBuilder()
.setAccessControl(AccessControl.newBuilder()
.setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR))
.build();
DecryptedGroup to = DecryptedGroup.newBuilder()
.setAccessControl(AccessControl.newBuilder()
.setAddFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder()
.setNewInviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE)
.build(),
decryptedGroupChange);
}
@Test
public void new_requesting_members() {
UUID member1 = UUID.randomUUID();
ProfileKey profileKey1 = newProfileKey();
DecryptedGroup from = DecryptedGroup.newBuilder()
.build();
DecryptedGroup to = DecryptedGroup.newBuilder()
.addRequestingMembers(requestingMember(member1, profileKey1))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder()
.addNewRequestingMembers(requestingMember(member1, profileKey1))
.build(),
decryptedGroupChange);
}
@Test
public void new_requesting_members_ignores_existing_by_uuid() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
ProfileKey profileKey2 = newProfileKey();
DecryptedGroup from = DecryptedGroup.newBuilder()
.addRequestingMembers(requestingMember(member1, newProfileKey()))
.build();
DecryptedGroup to = DecryptedGroup.newBuilder()
.addRequestingMembers(requestingMember(member1, newProfileKey()))
.addRequestingMembers(requestingMember(member2, profileKey2))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder()
.addNewRequestingMembers(requestingMember(member2, profileKey2))
.build(),
decryptedGroupChange);
}
@Test
public void removed_requesting_members() {
UUID member1 = UUID.randomUUID();
DecryptedGroup from = DecryptedGroup.newBuilder()
.addRequestingMembers(requestingMember(member1, newProfileKey()))
.build();
DecryptedGroup to = DecryptedGroup.newBuilder()
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder()
.addDeleteRequestingMembers(UuidUtil.toByteString(member1))
.build(),
decryptedGroupChange);
}
@Test
public void promote_requesting_members() {
UUID member1 = UUID.randomUUID();
ProfileKey profileKey1 = newProfileKey();
UUID member2 = UUID.randomUUID();
ProfileKey profileKey2 = newProfileKey();
DecryptedGroup from = DecryptedGroup.newBuilder()
.addRequestingMembers(requestingMember(member1, profileKey1))
.addRequestingMembers(requestingMember(member2, profileKey2))
.build();
DecryptedGroup to = DecryptedGroup.newBuilder()
.addMembers(member(member1, profileKey1))
.addMembers(admin(member2, profileKey2))
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder()
.addPromoteRequestingMembers(approveMember(member1))
.addPromoteRequestingMembers(approveAdmin(member2))
.build(),
decryptedGroupChange);
}
@Test
public void new_invite_link_password() {
ByteString password1 = ByteString.copyFrom(Util.getSecretBytes(16));
ByteString password2 = ByteString.copyFrom(Util.getSecretBytes(16));
DecryptedGroup from = DecryptedGroup.newBuilder()
.setInviteLinkPassword(password1)
.build();
DecryptedGroup to = DecryptedGroup.newBuilder()
.setInviteLinkPassword(password2)
.build();
DecryptedGroupChange decryptedGroupChange = GroupChangeReconstruct.reconstructGroupChange(from, to);
assertEquals(DecryptedGroupChange.newBuilder()
.setNewInviteLinkPassword(password2)
.build(),
decryptedGroupChange);
}
}

View File

@@ -8,7 +8,7 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeUtilTest {
public final class GroupChangeUtil_changeIsEmpty_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
@@ -20,7 +20,7 @@ public final class GroupChangeUtilTest {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupChange.Actions.class);
assertEquals("GroupChangeUtil and its tests need updating to account for new fields on " + GroupChange.Actions.class.getName(),
GroupChangeUtil.CHANGE_ACTION_MAX_FIELD, maxFieldFound);
19, maxFieldFound);
}
@Test
@@ -31,7 +31,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_add_member_field_3() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder().getDefaultInstanceForType())
.addAddMembers(GroupChange.Actions.AddMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -40,7 +40,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_delete_member_field_4() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder().getDefaultInstanceForType())
.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -49,7 +49,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_member_roles_field_5() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder().getDefaultInstanceForType())
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -58,7 +58,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_profile_keys_field_6() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.newBuilder().getDefaultInstanceForType())
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -67,7 +67,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_add_pending_members_field_7() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder().getDefaultInstanceForType())
.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -76,7 +76,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_delete_pending_members_field_8() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder().getDefaultInstanceForType())
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -85,7 +85,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_promote_delete_pending_members_field_9() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().getDefaultInstanceForType())
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -94,7 +94,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_title_field_10() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder().getDefaultInstanceForType())
.setModifyTitle(GroupChange.Actions.ModifyTitleAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -103,7 +103,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_avatar_field_11() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().getDefaultInstanceForType())
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -112,7 +112,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_disappearing_message_timer_field_12() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.newBuilder().getDefaultInstanceForType())
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -121,7 +121,7 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_attributes_field_13() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder().getDefaultInstanceForType())
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
@@ -130,7 +130,52 @@ public final class GroupChangeUtilTest {
@Test
public void not_empty_with_modify_member_access_field_14() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder().getDefaultInstanceForType())
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_modify_add_from_invite_link_field_15() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_add_requesting_members_field_16() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_delete_requesting_members_field_17() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_promote_requesting_members_field_18() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));
}
@Test
public void not_empty_with_promote_requesting_members_field_19() {
GroupChange.Actions actions = GroupChange.Actions.newBuilder()
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.getDefaultInstance())
.build();
assertFalse(GroupChangeUtil.changeIsEmpty(actions));

View File

@@ -13,24 +13,69 @@ import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.admin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.approveMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.demoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encrypt;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.encryptedRequestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.member;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.pendingMemberRemoval;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.presentation;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.promoteAdmin;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.randomProfileKey;
import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.requestingMember;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupChangeUtil_resolveConflict_Test {
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroupChange.class.getName(),
19, maxFieldFound);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_GroupChange() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroupChange.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + GroupChange.class.getName(),
19, maxFieldFound);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be resolved by {@link GroupChangeUtil#resolveConflict}.
*/
@Test
public void ensure_resolveConflict_knows_about_all_fields_of_DecryptedGroup() {
int maxFieldFound = getMaxDeclaredFieldNumber(DecryptedGroup.class);
assertEquals("GroupChangeUtil#resolveConflict and its tests need updating to account for new fields on " + DecryptedGroup.class.getName(),
10, maxFieldFound);
}
@Test
public void empty_actions() {
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(DecryptedGroup.newBuilder().build(),
@@ -471,4 +516,160 @@ public final class GroupChangeUtil_resolveConflict_Test {
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_15__no_membership_access_change_is_removed() {
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.setAccessControl(AccessControl.newBuilder().setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR))
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction.newBuilder().setAddFromInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
assertTrue(GroupChangeUtil.changeIsEmpty(resolvedActions));
}
@Test
public void field_16__changes_to_add_requesting_members_when_full_members_are_removed() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey2 = randomProfileKey();
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.addMembers(member(member1))
.addMembers(member(member3))
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.addNewRequestingMembers(requestingMember(member1))
.addNewRequestingMembers(requestingMember(member2))
.addNewRequestingMembers(requestingMember(member3))
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member1, randomProfileKey())))
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member3, randomProfileKey())))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_16__changes_to_add_requesting_members_when_pending_are_promoted() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.addPendingMembers(pendingMember(member1))
.addPendingMembers(pendingMember(member3))
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.addNewRequestingMembers(requestingMember(member1, profileKey1))
.addNewRequestingMembers(requestingMember(member2, profileKey2))
.addNewRequestingMembers(requestingMember(member3, profileKey3))
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member1, profileKey1)))
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member3, profileKey3)))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member1, profileKey1)))
.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction.newBuilder().setAdded(encryptedRequestingMember(member2, profileKey2)))
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder().setPresentation(presentation(member3, profileKey3)))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_17__changes_to_remove_missing_requesting_members_are_excluded() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.addRequestingMembers(requestingMember(member2))
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.addDeleteRequestingMembers(UuidUtil.toByteString(member1))
.addDeleteRequestingMembers(UuidUtil.toByteString(member2))
.addDeleteRequestingMembers(UuidUtil.toByteString(member3))
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member1)))
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member3)))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction.newBuilder().setDeletedUserId(encrypt(member2)))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_18__promote_requesting_members() {
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID member3 = UUID.randomUUID();
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.addMembers(member(member1))
.addRequestingMembers(requestingMember(member2))
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.addPromoteRequestingMembers(approveMember(member1))
.addPromoteRequestingMembers(approveMember(member2))
.addPromoteRequestingMembers(approveMember(member3))
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member1)))
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member2)))
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member3)))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction.newBuilder().setRole(Member.Role.DEFAULT).setUserId(UuidUtil.toByteString(member2)))
.build();
assertEquals(expected, resolvedActions);
}
@Test
public void field_19__password_change_is_kept() {
ByteString password1 = ByteString.copyFrom(Util.getSecretBytes(16));
ByteString password2 = ByteString.copyFrom(Util.getSecretBytes(16));
DecryptedGroup groupState = DecryptedGroup.newBuilder()
.setInviteLinkPassword(password1)
.build();
DecryptedGroupChange decryptedChange = DecryptedGroupChange.newBuilder()
.setNewInviteLinkPassword(password2)
.build();
GroupChange.Actions change = GroupChange.Actions.newBuilder()
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder().setInviteLinkPassword(password2))
.build();
GroupChange.Actions resolvedActions = GroupChangeUtil.resolveConflict(groupState, decryptedChange, change).build();
GroupChange.Actions expected = GroupChange.Actions.newBuilder()
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder().setInviteLinkPassword(password2))
.build();
assertEquals(expected, resolvedActions);
}
}

View File

@@ -8,11 +8,13 @@ import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.InvalidInputException;
@@ -93,6 +95,23 @@ public final class GroupsV2Operations_decrypt_change_Test {
.setUuid(UuidUtil.toByteString(newMember))));
}
@Test
public void can_decrypt_member_direct_join_field3() {
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createGroupJoinDirect(groupCandidate.getProfileKeyCredential().get())
.setRevision(10),
DecryptedGroupChange.newBuilder()
.setRevision(10)
.addNewMembers(DecryptedMember.newBuilder()
.setRole(Member.Role.DEFAULT)
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setJoinedAtRevision(10)
.setUuid(UuidUtil.toByteString(newMember))));
}
@Test
public void can_decrypt_member_additions_direct_to_admin_field3() {
UUID self = UUID.randomUUID();
@@ -266,20 +285,80 @@ public final class GroupsV2Operations_decrypt_change_Test {
@Test
public void can_pass_through_new_attribute_access_rights_field_13() {
assertDecryption(GroupChange.Actions.newBuilder()
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction.newBuilder()
.setAttributesAccess(AccessControl.AccessRequired.MEMBER)),
DecryptedGroupChange.newBuilder()
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER));
assertDecryption(groupOperations.createChangeAttributesRights(AccessControl.AccessRequired.MEMBER),
DecryptedGroupChange.newBuilder()
.setNewAttributeAccess(AccessControl.AccessRequired.MEMBER));
}
@Test
public void can_pass_through_new_membership_rights_field_14() {
assertDecryption(groupOperations.createChangeMembershipRights(AccessControl.AccessRequired.ADMINISTRATOR),
DecryptedGroupChange.newBuilder()
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR));
}
@Test
public void can_pass_through_new_add_by_invite_link_rights_field_15() {
assertDecryption(groupOperations.createChangeJoinByLinkRights(AccessControl.AccessRequired.ADMINISTRATOR),
DecryptedGroupChange.newBuilder()
.setNewInviteLinkAccess(AccessControl.AccessRequired.ADMINISTRATOR));
}
@Test
public void can_pass_through_new_add_by_invite_link_rights_field_15_unsatisfiable() {
assertDecryption(groupOperations.createChangeJoinByLinkRights(AccessControl.AccessRequired.UNSATISFIABLE),
DecryptedGroupChange.newBuilder()
.setNewInviteLinkAccess(AccessControl.AccessRequired.UNSATISFIABLE));
}
@Test
public void can_decrypt_member_requests_field16() {
UUID newRequestingMember = UUID.randomUUID();
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newRequestingMember, profileKey);
assertDecryption(groupOperations.createGroupJoinRequest(groupCandidate.getProfileKeyCredential().get())
.setRevision(10),
DecryptedGroupChange.newBuilder()
.setRevision(10)
.addNewRequestingMembers(DecryptedRequestingMember.newBuilder()
.setUuid(UuidUtil.toByteString(newRequestingMember))
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))));
}
@Test
public void can_decrypt_member_requests_refusals_field17() {
UUID newRequestingMember = UUID.randomUUID();
assertDecryption(groupOperations.createRefuseGroupJoinRequest(Collections.singleton(newRequestingMember))
.setRevision(10),
DecryptedGroupChange.newBuilder()
.setRevision(10)
.addDeleteRequestingMembers(UuidUtil.toByteString(newRequestingMember)));
}
@Test
public void can_decrypt_promote_requesting_members_field18() {
UUID newRequestingMember = UUID.randomUUID();
assertDecryption(groupOperations.createApproveGroupJoinRequest(Collections.singleton(newRequestingMember))
.setRevision(15),
DecryptedGroupChange.newBuilder()
.setRevision(15)
.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(newRequestingMember))));
}
@Test
public void can_pass_through_new_invite_link_password_field19() {
byte[] newPassword = Util.getSecretBytes(16);
assertDecryption(GroupChange.Actions.newBuilder()
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder()
.setMembersAccess(AccessControl.AccessRequired.ADMINISTRATOR)),
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction.newBuilder()
.setInviteLinkPassword(ByteString.copyFrom(newPassword))),
DecryptedGroupChange.newBuilder()
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR));
.setNewInviteLinkPassword(ByteString.copyFrom(newPassword)));
}
private static ProfileKey newProfileKey() {

View File

@@ -0,0 +1,110 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.junit.Before;
import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.testutil.ZkGroupLibraryUtil;
import static org.junit.Assert.assertEquals;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupsV2Operations_decrypt_groupJoinInfo_Test {
private GroupsV2Operations.GroupOperations groupOperations;
@Before
public void setup() throws InvalidInputException {
ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS();
TestZkGroupServer server = new TestZkGroupServer();
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
groupOperations = new GroupsV2Operations(clientZkOperations).forGroup(groupSecretParams);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be decrypted by {@link GroupsV2Operations.GroupOperations#decryptGroupJoinInfo}.
*/
@Test
public void ensure_GroupOperations_knows_about_all_fields_of_Group() {
int maxFieldFound = getMaxDeclaredFieldNumber(GroupJoinInfo.class);
assertEquals("GroupOperations and its tests need updating to account for new fields on " + GroupJoinInfo.class.getName(),
6, maxFieldFound);
}
@Test
public void decrypt_title_field_2() {
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
.setTitle(groupOperations.encryptTitle("Title!"))
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals("Title!", decryptedGroupJoinInfo.getTitle());
}
@Test
public void avatar_field_passed_through_3() {
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
.setAvatar("AvatarCdnKey")
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals("AvatarCdnKey", decryptedGroupJoinInfo.getAvatar());
}
@Test
public void member_count_passed_through_4() {
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
.setMemberCount(97)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(97, decryptedGroupJoinInfo.getMemberCount());
}
@Test
public void add_from_invite_link_access_control_passed_though_5_administrator() {
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
.setAddFromInviteLink(AccessControl.AccessRequired.ADMINISTRATOR)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(AccessControl.AccessRequired.ADMINISTRATOR, decryptedGroupJoinInfo.getAddFromInviteLink());
}
@Test
public void add_from_invite_link_access_control_passed_though_5_any() {
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
.setAddFromInviteLink(AccessControl.AccessRequired.ANY)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(AccessControl.AccessRequired.ANY, decryptedGroupJoinInfo.getAddFromInviteLink());
}
@Test
public void revision_passed_though_6() {
GroupJoinInfo groupJoinInfo = GroupJoinInfo.newBuilder()
.setRevision(11)
.build();
DecryptedGroupJoinInfo decryptedGroupJoinInfo = groupOperations.decryptGroupJoinInfo(groupJoinInfo);
assertEquals(11, decryptedGroupJoinInfo.getRevision());
}
}

View File

@@ -0,0 +1,286 @@
package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.junit.Before;
import org.junit.Test;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.PendingMember;
import org.signal.storageservice.protos.groups.RequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.zkgroup.groups.GroupMasterKey;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequestContext;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import org.whispersystems.signalservice.testutil.ZkGroupLibraryUtil;
import java.util.Collections;
import java.util.UUID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.whispersystems.signalservice.api.groupsv2.ProtobufTestUtils.getMaxDeclaredFieldNumber;
public final class GroupsV2Operations_decrypt_group_Test {
private GroupSecretParams groupSecretParams;
private GroupsV2Operations.GroupOperations groupOperations;
@Before
public void setup() throws InvalidInputException {
ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS();
TestZkGroupServer server = new TestZkGroupServer();
ClientZkOperations clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
groupOperations = new GroupsV2Operations(clientZkOperations).forGroup(groupSecretParams);
}
/**
* Reflects over the generated protobuf class and ensures that no new fields have been added since we wrote this.
* <p>
* If we didn't, newly added fields would not be decrypted by {@link GroupsV2Operations.GroupOperations#decryptGroup}.
*/
@Test
public void ensure_GroupOperations_knows_about_all_fields_of_Group() {
int maxFieldFound = getMaxDeclaredFieldNumber(Group.class);
assertEquals("GroupOperations and its tests need updating to account for new fields on " + Group.class.getName(),
10, maxFieldFound);
}
@Test
public void decrypt_title_field_2() throws VerificationFailedException, InvalidGroupStateException {
Group group = Group.newBuilder()
.setTitle(groupOperations.encryptTitle("Title!"))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals("Title!", decryptedGroup.getTitle());
}
@Test
public void avatar_field_passed_through_3() throws VerificationFailedException, InvalidGroupStateException {
Group group = Group.newBuilder()
.setAvatar("AvatarCdnKey")
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals("AvatarCdnKey", decryptedGroup.getAvatar());
}
@Test
public void decrypt_message_timer_field_4() throws VerificationFailedException, InvalidGroupStateException {
Group group = Group.newBuilder()
.setDisappearingMessagesTimer(groupOperations.encryptTimer(123))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(123, decryptedGroup.getDisappearingMessagesTimer().getDuration());
}
@Test
public void pass_through_access_control_field_5() throws VerificationFailedException, InvalidGroupStateException {
AccessControl accessControl = AccessControl.newBuilder()
.setMembers(AccessControl.AccessRequired.ADMINISTRATOR)
.setAttributes(AccessControl.AccessRequired.MEMBER)
.setAddFromInviteLink(AccessControl.AccessRequired.UNSATISFIABLE)
.build();
Group group = Group.newBuilder()
.setAccessControl(accessControl)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(accessControl, decryptedGroup.getAccessControl());
}
@Test
public void set_revision_field_6() throws VerificationFailedException, InvalidGroupStateException {
Group group = Group.newBuilder()
.setRevision(99)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(99, decryptedGroup.getRevision());
}
@Test
public void decrypt_full_members_field_7() throws VerificationFailedException, InvalidGroupStateException {
UUID admin1 = UUID.randomUUID();
UUID member1 = UUID.randomUUID();
ProfileKey adminProfileKey = newProfileKey();
ProfileKey memberProfileKey = newProfileKey();
Group group = Group.newBuilder()
.addMembers(Member.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUserId(groupOperations.encryptUuid(admin1))
.setJoinedAtRevision(4)
.setProfileKey(encryptProfileKey(admin1, adminProfileKey)))
.addMembers(Member.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUserId(groupOperations.encryptUuid(member1))
.setJoinedAtRevision(7)
.setProfileKey(encryptProfileKey(member1, memberProfileKey)))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(DecryptedGroup.newBuilder()
.addMembers(DecryptedMember.newBuilder()
.setJoinedAtRevision(4)
.setUuid(UuidUtil.toByteString(admin1))
.setRole(Member.Role.ADMINISTRATOR)
.setProfileKey(ByteString.copyFrom(adminProfileKey.serialize())))
.addMembers(DecryptedMember.newBuilder()
.setJoinedAtRevision(7)
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(member1))
.setProfileKey(ByteString.copyFrom(memberProfileKey.serialize())))
.build().getMembersList(),
decryptedGroup.getMembersList());
}
@Test
public void decrypt_pending_members_field_8() throws VerificationFailedException, InvalidGroupStateException {
UUID admin1 = UUID.randomUUID();
UUID member1 = UUID.randomUUID();
UUID member2 = UUID.randomUUID();
UUID inviter1 = UUID.randomUUID();
UUID inviter2 = UUID.randomUUID();
Group group = Group.newBuilder()
.addPendingMembers(PendingMember.newBuilder()
.setAddedByUserId(groupOperations.encryptUuid(inviter1))
.setTimestamp(100)
.setMember(Member.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUserId(groupOperations.encryptUuid(admin1))))
.addPendingMembers(PendingMember.newBuilder()
.setAddedByUserId(groupOperations.encryptUuid(inviter1))
.setTimestamp(200)
.setMember(Member.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUserId(groupOperations.encryptUuid(member1))))
.addPendingMembers(PendingMember.newBuilder()
.setAddedByUserId(groupOperations.encryptUuid(inviter2))
.setTimestamp(1500)
.setMember(Member.newBuilder()
.setUserId(groupOperations.encryptUuid(member2))))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(DecryptedGroup.newBuilder()
.addPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(admin1))
.setUuidCipherText(groupOperations.encryptUuid(admin1))
.setTimestamp(100)
.setAddedByUuid(UuidUtil.toByteString(inviter1))
.setRole(Member.Role.ADMINISTRATOR))
.addPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(member1))
.setUuidCipherText(groupOperations.encryptUuid(member1))
.setTimestamp(200)
.setAddedByUuid(UuidUtil.toByteString(inviter1))
.setRole(Member.Role.DEFAULT))
.addPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(member2))
.setUuidCipherText(groupOperations.encryptUuid(member2))
.setTimestamp(1500)
.setAddedByUuid(UuidUtil.toByteString(inviter2))
.setRole(Member.Role.DEFAULT))
.build().getPendingMembersList(),
decryptedGroup.getPendingMembersList());
}
@Test
public void decrypt_requesting_members_field_9() throws VerificationFailedException, InvalidGroupStateException {
UUID admin1 = UUID.randomUUID();
UUID member1 = UUID.randomUUID();
ProfileKey adminProfileKey = newProfileKey();
ProfileKey memberProfileKey = newProfileKey();
Group group = Group.newBuilder()
.addRequestingMembers(RequestingMember.newBuilder()
.setUserId(groupOperations.encryptUuid(admin1))
.setProfileKey(encryptProfileKey(admin1, adminProfileKey))
.setTimestamp(5000))
.addRequestingMembers(RequestingMember.newBuilder()
.setUserId(groupOperations.encryptUuid(member1))
.setProfileKey(encryptProfileKey(member1, memberProfileKey))
.setTimestamp(15000))
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(DecryptedGroup.newBuilder()
.addRequestingMembers(DecryptedRequestingMember.newBuilder()
.setUuid(UuidUtil.toByteString(admin1))
.setProfileKey(ByteString.copyFrom(adminProfileKey.serialize()))
.setTimestamp(5000))
.addRequestingMembers(DecryptedRequestingMember.newBuilder()
.setUuid(UuidUtil.toByteString(member1))
.setProfileKey(ByteString.copyFrom(memberProfileKey.serialize()))
.setTimestamp(15000))
.build().getRequestingMembersList(),
decryptedGroup.getRequestingMembersList());
}
@Test
public void pass_through_group_link_password_field_10() throws VerificationFailedException, InvalidGroupStateException {
ByteString password = ByteString.copyFrom(Util.getSecretBytes(16));
Group group = Group.newBuilder()
.setInviteLinkPassword(password)
.build();
DecryptedGroup decryptedGroup = groupOperations.decryptGroup(group);
assertEquals(password, decryptedGroup.getInviteLinkPassword());
}
private ByteString encryptProfileKey(UUID uuid, ProfileKey profileKey) {
return ByteString.copyFrom(new ClientZkGroupCipher(groupSecretParams).encryptProfileKey(profileKey, uuid).serialize());
}
private static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -3,13 +3,17 @@ package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.RequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.profiles.ProfileKey;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.util.Util;
import java.security.SecureRandom;
import java.util.Arrays;
@@ -70,6 +74,12 @@ final class ProtoTestUtils {
.build();
}
static RequestingMember encryptedRequestingMember(UUID uuid, ProfileKey profileKey) {
return RequestingMember.newBuilder()
.setPresentation(presentation(uuid, profileKey))
.build();
}
static DecryptedMember member(UUID uuid) {
return DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
@@ -101,6 +111,32 @@ final class ProtoTestUtils {
.build();
}
static DecryptedRequestingMember requestingMember(UUID uuid) {
return requestingMember(uuid, newProfileKey());
}
static DecryptedRequestingMember requestingMember(UUID uuid, ProfileKey profileKey) {
return DecryptedRequestingMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.build();
}
static DecryptedApproveMember approveMember(UUID uuid) {
return approve(uuid, Member.Role.DEFAULT);
}
static DecryptedApproveMember approveAdmin(UUID uuid) {
return approve(uuid, Member.Role.ADMINISTRATOR);
}
private static DecryptedApproveMember approve(UUID uuid, Member.Role role) {
return DecryptedApproveMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setRole(role)
.build();
}
static DecryptedMember member(UUID uuid, ProfileKey profileKey) {
return withProfileKey(member(uuid), profileKey);
}
@@ -135,4 +171,12 @@ final class ProtoTestUtils {
.setRole(Member.Role.DEFAULT)
.build();
}
public static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
}

View File

@@ -0,0 +1,67 @@
package org.whispersystems.util;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.whispersystems.signalservice.internal.util.Hex;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
@RunWith(Parameterized.class)
public final class Base64UrlSafeTest {
private final byte[] data;
private final String encoded;
private final String encodedWithoutPadding;
@Parameterized.Parameters
public static Collection<Object[]> data() {
return Arrays.asList(new Object[][]{
{ "", "", "" },
{ "01", "AQ==", "AQ" },
{ "0102", "AQI=", "AQI" },
{ "010203", "AQID", "AQID" },
{ "030405", "AwQF", "AwQF" },
{ "03040506", "AwQFBg==", "AwQFBg" },
{ "0304050708", "AwQFBwg=", "AwQFBwg" },
{ "af4d6cff", "r01s_w==", "r01s_w" },
{ "ffefde", "_-_e", "_-_e" },
});
}
public Base64UrlSafeTest(String hexData, String encoded, String encodedWithoutPadding) throws IOException {
this.data = Hex.fromStringCondensed(hexData);
this.encoded = encoded;
this.encodedWithoutPadding = encodedWithoutPadding;
}
@Test
public void encodes_as_expected() {
assertEquals(encoded, Base64UrlSafe.encodeBytes(data));
}
@Test
public void encodes_as_expected_without_padding() {
assertEquals(encodedWithoutPadding, Base64UrlSafe.encodeBytesWithoutPadding(data));
}
@Test
public void decodes_as_expected() throws IOException {
assertArrayEquals(data, Base64UrlSafe.decode(encoded));
}
@Test
public void decodes_padding_agnostic_as_expected() throws IOException {
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encoded));
}
@Test
public void decodes_as_expected_without_padding() throws IOException {
assertArrayEquals(data, Base64UrlSafe.decodePaddingAgnostic(encodedWithoutPadding));
}
}