Additional tests around rigid Groups V2 change application.

master
Alan Evans 2020-08-03 12:06:53 -03:00 committed by Greyson Parrelli
parent 2dcaa21a44
commit b28ac7af8c
11 changed files with 574 additions and 100 deletions

View File

@ -44,6 +44,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupChangeUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
import org.whispersystems.signalservice.api.push.exceptions.ConflictException;
import org.whispersystems.signalservice.api.util.UuidUtil;
@ -233,7 +234,8 @@ final class GroupManagerV2 {
throws GroupChangeFailedException, GroupInsufficientRightsException, IOException, GroupNotAMemberException
{
try {
GroupChange.Actions.Builder change = groupOperations.createModifyGroupTitleAndMembershipChange(Optional.fromNullable(title), Collections.emptySet(), Collections.emptySet());
GroupChange.Actions.Builder change = title != null ? groupOperations.createModifyGroupTitle(title)
: GroupChange.Actions.newBuilder();
if (avatarChanged) {
String cdnKey = avatarBytes != null ? groupsV2Api.uploadAvatar(avatarBytes, groupSecretParams, authorization.getAuthorizationForToday(selfUuid, groupSecretParams))
@ -424,7 +426,7 @@ final class GroupManagerV2 {
try {
decryptedChange = groupOperations.decryptChange(changeActions, selfUuid);
decryptedGroupState = DecryptedGroupUtil.apply(v2GroupProperties.getDecryptedGroup(), decryptedChange);
} catch (VerificationFailedException | InvalidGroupStateException | DecryptedGroupUtil.NotAbleToApplyChangeException e) {
} catch (VerificationFailedException | InvalidGroupStateException | NotAbleToApplyGroupV2ChangeException e) {
Log.w(TAG, e);
throw new IOException(e);
}

View File

@ -7,6 +7,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.thoughtcrime.securesms.logging.Log;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupChangeReconstruct;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import java.util.ArrayList;
import java.util.Collections;
@ -88,7 +89,7 @@ final class GroupStateMapper {
DecryptedGroup groupWithChangeApplied;
try {
groupWithChangeApplied = DecryptedGroupUtil.applyWithoutRevisionCheck(current, changeAtRevision);
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
} catch (NotAbleToApplyGroupV2ChangeException e) {
Log.w(TAG, "Unable to apply V" + revision, e);
continue;
}

View File

@ -32,7 +32,6 @@ import org.thoughtcrime.securesms.groups.GroupsV2Authorization;
import org.thoughtcrime.securesms.groups.v2.ProfileKeySet;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.JobTracker;
import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
@ -49,6 +48,7 @@ import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api;
import org.whispersystems.signalservice.api.groupsv2.InvalidGroupStateException;
import org.whispersystems.signalservice.api.groupsv2.NotAbleToApplyGroupV2ChangeException;
import org.whispersystems.signalservice.api.util.UuidUtil;
import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
@ -170,7 +170,7 @@ public final class GroupsV2StateProcessor {
DecryptedGroup newState = DecryptedGroupUtil.apply(localState, signedGroupChange);
inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(newState, signedGroupChange)));
} catch (DecryptedGroupUtil.NotAbleToApplyChangeException e) {
} catch (NotAbleToApplyGroupV2ChangeException e) {
Log.w(TAG, "Unable to apply P2P group change", e);
}
}

View File

@ -3,6 +3,7 @@ package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
@ -25,16 +26,6 @@ public final class DecryptedGroupUtil {
static final int MAX_CHANGE_FIELD = 14;
public static Set<UUID> toUuidSet(Collection<DecryptedMember> membersList) {
HashSet<UUID> uuids = new HashSet<>(membersList.size());
for (DecryptedMember member : membersList) {
uuids.add(toUuid(member));
}
return uuids;
}
public static ArrayList<UUID> toUuidList(Collection<DecryptedMember> membersList) {
ArrayList<UUID> uuidList = new ArrayList<>(membersList.size());
@ -191,17 +182,17 @@ public final class DecryptedGroupUtil {
}
public static DecryptedGroup apply(DecryptedGroup group, DecryptedGroupChange change)
throws NotAbleToApplyChangeException
throws NotAbleToApplyGroupV2ChangeException
{
if (change.getRevision() != group.getRevision() + 1) {
throw new NotAbleToApplyChangeException();
throw new NotAbleToApplyGroupV2ChangeException();
}
return applyWithoutRevisionCheck(group, change);
}
public static DecryptedGroup applyWithoutRevisionCheck(DecryptedGroup group, DecryptedGroupChange change)
throws NotAbleToApplyChangeException
throws NotAbleToApplyGroupV2ChangeException
{
DecryptedGroup.Builder builder = DecryptedGroup.newBuilder(group);
@ -211,7 +202,7 @@ public final class DecryptedGroupUtil {
int index = indexOfUuid(builder.getMembersList(), removedMember);
if (index == -1) {
throw new NotAbleToApplyChangeException();
throw new NotAbleToApplyGroupV2ChangeException();
}
builder.removeMembers(index);
@ -221,7 +212,11 @@ public final class DecryptedGroupUtil {
int index = indexOfUuid(builder.getMembersList(), modifyMemberRole.getUuid());
if (index == -1) {
throw new NotAbleToApplyChangeException();
throw new NotAbleToApplyGroupV2ChangeException();
}
if (modifyMemberRole.getRole() != Member.Role.ADMINISTRATOR && modifyMemberRole.getRole() != Member.Role.DEFAULT) {
throw new NotAbleToApplyGroupV2ChangeException();
}
builder.setMembers(index, DecryptedMember.newBuilder(builder.getMembers(index)).setRole(modifyMemberRole.getRole()).build());
@ -231,7 +226,7 @@ public final class DecryptedGroupUtil {
int index = indexOfUuid(builder.getMembersList(), modifyProfileKey.getUuid());
if (index == -1) {
throw new NotAbleToApplyChangeException();
throw new NotAbleToApplyGroupV2ChangeException();
}
builder.setMembers(index, DecryptedMember.newBuilder(builder.getMembers(index)).setProfileKey(modifyProfileKey.getProfileKey()).build());
@ -241,7 +236,7 @@ public final class DecryptedGroupUtil {
int index = findPendingIndexByUuidCipherText(builder.getPendingMembersList(), removedMember.getUuidCipherText());
if (index == -1) {
throw new NotAbleToApplyChangeException();
throw new NotAbleToApplyGroupV2ChangeException();
}
builder.removePendingMembers(index);
@ -251,7 +246,7 @@ public final class DecryptedGroupUtil {
int index = findPendingIndexByUuid(builder.getPendingMembersList(), newMember.getUuid());
if (index == -1) {
throw new NotAbleToApplyChangeException();
throw new NotAbleToApplyGroupV2ChangeException();
}
builder.removePendingMembers(index);
@ -336,6 +331,4 @@ public final class DecryptedGroupUtil {
return newAttributeAccess == AccessControl.AccessRequired.UNKNOWN;
}
public static class NotAbleToApplyChangeException extends Throwable {
}
}

View File

@ -136,42 +136,9 @@ public final class GroupsV2Operations {
this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
}
public GroupChange.Actions.Builder createModifyGroupTitleAndMembershipChange(final Optional<String> title,
final Set<GroupCandidate> membersToAdd,
final Set<UUID> membersToRemove)
{
if (!Collections.disjoint(GroupCandidate.toUuidList(membersToAdd), membersToRemove)) {
throw new IllegalArgumentException("Overlap between add and remove sets");
}
final GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
if (title.isPresent()) {
actions.setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder()
.setTitle(encryptTitle(title.get())));
}
for (GroupCandidate credential : membersToAdd) {
Member.Role newMemberRole = Member.Role.DEFAULT;
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
if (profileKeyCredential != null) {
actions.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder()
.setAdded(groupOperations.member(profileKeyCredential, newMemberRole)));
} else {
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction.newBuilder()
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)));
}
}
for (UUID remove: membersToRemove) {
actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder()
.setDeletedUserId(encryptUuid(remove)));
}
return actions;
public GroupChange.Actions.Builder createModifyGroupTitle(final String title) {
return GroupChange.Actions.newBuilder().setModifyTitle(GroupChange.Actions.ModifyTitleAction.newBuilder()
.setTitle(encryptTitle(title)));
}
public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set<GroupCandidate> membersToAdd, UUID selfUuid) {
@ -511,7 +478,7 @@ public final class GroupsV2Operations {
return ByteString.copyFrom(UUIDUtil.serialize(decryptUuid(userId)));
}
private ByteString encryptUuid(UUID uuid) {
ByteString encryptUuid(UUID uuid) {
return ByteString.copyFrom(clientZkGroupCipher.encryptUuid(uuid).serialize());
}

View File

@ -0,0 +1,8 @@
package org.whispersystems.signalservice.api.groupsv2;
public final class NotAbleToApplyGroupV2ChangeException extends Exception {
NotAbleToApplyGroupV2ChangeException() {
}
}

View File

@ -30,7 +30,7 @@ import static org.whispersystems.signalservice.api.groupsv2.ProtoTestUtils.withP
public final class DecryptedGroupUtil_apply_Test {
@Test
public void apply_revision() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_revision() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(9)
.build(),
@ -42,7 +42,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void apply_new_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_new_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
@ -64,7 +64,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void apply_remove_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_remove_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
@ -86,7 +86,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void apply_remove_members() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_remove_members() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
@ -107,8 +107,8 @@ public final class DecryptedGroupUtil_apply_Test {
newGroup);
}
@Test(expected = DecryptedGroupUtil.NotAbleToApplyChangeException.class)
public void apply_remove_members_not_found() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void apply_remove_members_not_found() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
@ -123,7 +123,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void apply_modify_member_role() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_modify_member_role() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = admin(UUID.randomUUID());
@ -146,8 +146,42 @@ public final class DecryptedGroupUtil_apply_Test {
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void not_able_to_apply_modify_member_role_for_non_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedMember member2 = member(UUID.randomUUID());
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(13)
.addMembers(member1)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(14)
.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(Member.Role.ADMINISTRATOR)
.setUuid(member2.getUuid())
.build())
.build());
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void not_able_to_apply_modify_member_role_for_no_role() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(13)
.addMembers(member1)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(14)
.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setUuid(member1.getUuid())
.build())
.build());
}
@Test
public void apply_modify_member_profile_keys() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_modify_member_profile_keys() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2a = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
@ -173,8 +207,28 @@ public final class DecryptedGroupUtil_apply_Test {
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cant_apply_modify_member_profile_keys_if_member_not_in_group() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2a = randomProfileKey();
ProfileKey profileKey2b = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID(), profileKey1);
DecryptedMember member2a = member(UUID.randomUUID(), profileKey2a);
DecryptedMember member2b = member(UUID.randomUUID(), profileKey2b);
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(13)
.addMembers(member1)
.addMembers(member2a)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(14)
.addModifiedProfileKeys(member2b)
.build());
}
@Test
public void apply_modify_admin_profile_keys() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_modify_admin_profile_keys() throws NotAbleToApplyGroupV2ChangeException {
UUID adminUuid = UUID.randomUUID();
ProfileKey profileKey1 = randomProfileKey();
ProfileKey profileKey2a = randomProfileKey();
@ -204,7 +258,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void apply_new_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void apply_new_pending_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
DecryptedPendingMember pending = pendingMember(UUID.randomUUID());
@ -226,7 +280,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void remove_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void remove_pending_member() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID pendingUuid = UUID.randomUUID();
DecryptedPendingMember pending = pendingMember(pendingUuid);
@ -250,8 +304,25 @@ public final class DecryptedGroupUtil_apply_Test {
newGroup);
}
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cannot_remove_pending_member_if_not_in_group() throws NotAbleToApplyGroupV2ChangeException {
DecryptedMember member1 = member(UUID.randomUUID());
UUID pendingUuid = UUID.randomUUID();
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.addMembers(member1)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuidCipherText(ProtoTestUtils.encrypt(pendingUuid))
.build())
.build());
}
@Test
public void promote_pending_member() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void promote_pending_member() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID pending2Uuid = UUID.randomUUID();
@ -276,34 +347,25 @@ public final class DecryptedGroupUtil_apply_Test {
newGroup);
}
@Test
public void promote_direct_to_admin() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
@Test(expected = NotAbleToApplyGroupV2ChangeException.class)
public void cannot_promote_pending_member_if_not_in_group() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
UUID pending2Uuid = UUID.randomUUID();
DecryptedPendingMember pending2 = pendingMember(pending2Uuid);
DecryptedMember member2 = withProfileKey(admin(pending2Uuid), profileKey2);
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.addMembers(member1)
.addPendingMembers(pending2)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.addPromotePendingMembers(member2)
.build());
assertEquals(DecryptedGroup.newBuilder()
.setRevision(11)
.addMembers(member1)
.addMembers(member2)
.build(),
newGroup);
DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.addMembers(member1)
.build(),
DecryptedGroupChange.newBuilder()
.setRevision(11)
.addPromotePendingMembers(member2)
.build());
}
@Test
public void skip_promote_pending_member_by_direct_add() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void skip_promote_pending_member_by_direct_add() throws NotAbleToApplyGroupV2ChangeException {
ProfileKey profileKey2 = randomProfileKey();
ProfileKey profileKey3 = randomProfileKey();
DecryptedMember member1 = member(UUID.randomUUID());
@ -340,7 +402,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void title() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void title() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setTitle("Old title")
@ -358,7 +420,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void avatar() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void avatar() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setAvatar("https://cnd/oldavatar")
@ -376,7 +438,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void timer() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void timer() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(100))
@ -394,7 +456,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void attribute_access() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void attribute_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setAccessControl(AccessControl.newBuilder()
@ -418,7 +480,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void membership_access() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void membership_access() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setAccessControl(AccessControl.newBuilder()
@ -442,7 +504,7 @@ public final class DecryptedGroupUtil_apply_Test {
}
@Test
public void change_both_access_levels() throws DecryptedGroupUtil.NotAbleToApplyChangeException {
public void change_both_access_levels() throws NotAbleToApplyGroupV2ChangeException {
DecryptedGroup newGroup = DecryptedGroupUtil.apply(DecryptedGroup.newBuilder()
.setRevision(10)
.setAccessControl(AccessControl.newBuilder()

View File

@ -0,0 +1,342 @@
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.GroupChange;
import org.signal.storageservice.protos.groups.Member;
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.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.VerificationFailedException;
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;
public final class GroupsV2Operations_decrypt_change_Test {
private GroupSecretParams groupSecretParams;
private GroupsV2Operations.GroupOperations groupOperations;
private ClientZkOperations clientZkOperations;
private TestZkGroupServer server;
@Before
public void setup() throws InvalidInputException {
ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS();
server = new TestZkGroupServer();
groupSecretParams = GroupSecretParams.deriveFromMasterKey(new GroupMasterKey(Util.getSecretBytes(32)));
clientZkOperations = new ClientZkOperations(server.getServerPublicParams());
groupOperations = new GroupsV2Operations(clientZkOperations).forGroup(groupSecretParams);
}
@Test
public void cannot_decrypt_change_with_epoch_higher_than_known() throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException {
GroupChange change = GroupChange.newBuilder()
.setChangeEpoch(GroupsV2Operations.HIGHEST_KNOWN_EPOCH + 1)
.build();
Optional<DecryptedGroupChange> decryptedGroupChangeOptional = groupOperations.decryptChange(change, false);
assertFalse(decryptedGroupChangeOptional.isPresent());
}
@Test
public void can_pass_revision_through_encrypt_and_decrypt_methods() {
assertDecryption(GroupChange.Actions.newBuilder()
.setRevision(1),
DecryptedGroupChange.newBuilder()
.setRevision(1));
}
@Test
public void can_decrypt_member_additions_field3() {
UUID self = UUID.randomUUID();
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createModifyGroupMembershipChange(Collections.singleton(groupCandidate), self)
.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();
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createModifyGroupMembershipChange(Collections.singleton(groupCandidate), self)
.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(expected = InvalidGroupStateException.class)
public void cannot_decrypt_member_additions_with_bad_cipher_text_field3() throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException {
byte[] randomPresentation = Util.getSecretBytes(ProfileKeyCredentialPresentation.SIZE);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
actions.addAddMembers(GroupChange.Actions.AddMemberAction.newBuilder()
.setAdded(Member.newBuilder().setRole(Member.Role.DEFAULT)
.setPresentation(ByteString.copyFrom(randomPresentation))));
groupOperations.decryptChange(GroupChange.newBuilder().setActions(actions.build().toByteString()).build(), false);
}
@Test
public void can_decrypt_member_removals_field4() {
UUID oldMember = UUID.randomUUID();
assertDecryption(groupOperations.createRemoveMembersChange(Collections.singleton(oldMember))
.setRevision(10),
DecryptedGroupChange.newBuilder()
.setRevision(10)
.addDeleteMembers(UuidUtil.toByteString(oldMember)));
}
@Test(expected = InvalidGroupStateException.class)
public void cannot_decrypt_member_removals_with_bad_cipher_text_field4() throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException {
byte[] randomPresentation = Util.getSecretBytes(UuidCiphertext.SIZE);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction.newBuilder()
.setDeletedUserId(ByteString.copyFrom(randomPresentation)));
groupOperations.decryptChange(GroupChange.newBuilder().setActions(actions.build().toByteString()).build(), false);
}
@Test
public void can_decrypt_modify_member_action_role_to_admin_field5() {
UUID member = UUID.randomUUID();
assertDecryption(groupOperations.createChangeMemberRole(member, Member.Role.ADMINISTRATOR),
DecryptedGroupChange.newBuilder()
.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setUuid(UuidUtil.toByteString(member))
.setRole(Member.Role.ADMINISTRATOR)));
}
@Test
public void can_decrypt_modify_member_action_role_to_member_field5() {
UUID member = UUID.randomUUID();
assertDecryption(groupOperations.createChangeMemberRole(member, Member.Role.DEFAULT),
DecryptedGroupChange.newBuilder()
.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setUuid(UuidUtil.toByteString(member))
.setRole(Member.Role.DEFAULT)));
}
@Test
public void can_decrypt_modify_member_profile_key_action_field6() {
UUID self = UUID.randomUUID();
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(self, profileKey);
assertDecryption(groupOperations.createUpdateProfileKeyCredentialChange(groupCandidate.getProfileKeyCredential().get())
.setRevision(10),
DecryptedGroupChange.newBuilder()
.setRevision(10)
.addModifiedProfileKeys(DecryptedMember.newBuilder()
.setRole(Member.Role.UNKNOWN)
.setJoinedAtRevision(-1)
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setUuid(UuidUtil.toByteString(self))));
}
@Test
public void can_decrypt_member_invitations_field7() {
UUID self = UUID.randomUUID();
UUID newMember = UUID.randomUUID();
GroupCandidate groupCandidate = groupCandidate(newMember);
assertDecryption(groupOperations.createModifyGroupMembershipChange(Collections.singleton(groupCandidate), self)
.setRevision(13),
DecryptedGroupChange.newBuilder()
.setRevision(13)
.addNewPendingMembers(DecryptedPendingMember.newBuilder()
.setAddedByUuid(UuidUtil.toByteString(self))
.setUuidCipherText(groupOperations.encryptUuid(newMember))
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(newMember))));
}
@Test
public void can_decrypt_pending_member_removals_field8() throws InvalidInputException {
UUID oldMember = UUID.randomUUID();
UuidCiphertext uuidCiphertext = new UuidCiphertext(groupOperations.encryptUuid(oldMember).toByteArray());
assertDecryption(groupOperations.createRemoveInvitationChange(Collections.singleton(uuidCiphertext)),
DecryptedGroupChange.newBuilder()
.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(oldMember))
.setUuidCipherText(ByteString.copyFrom(uuidCiphertext.serialize()))));
}
@Test
public void can_decrypt_pending_member_removals_with_bad_cipher_text_field8() {
byte[] uuidCiphertext = Util.getSecretBytes(60);
assertDecryption(GroupChange.Actions
.newBuilder()
.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction.newBuilder()
.setDeletedUserId(ByteString.copyFrom(uuidCiphertext))),
DecryptedGroupChange.newBuilder()
.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(UuidUtil.UNKNOWN_UUID))
.setUuidCipherText(ByteString.copyFrom(uuidCiphertext))));
}
@Test
public void can_decrypt_promote_pending_member_field9() {
UUID newMember = UUID.randomUUID();
ProfileKey profileKey = newProfileKey();
GroupCandidate groupCandidate = groupCandidate(newMember, profileKey);
assertDecryption(groupOperations.createAcceptInviteChange(groupCandidate.getProfileKeyCredential().get()),
DecryptedGroupChange.newBuilder()
.addPromotePendingMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(newMember))
.setRole(Member.Role.DEFAULT)
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setJoinedAtRevision(-1)));
}
@Test
public void can_decrypt_title_field_10() {
assertDecryption(groupOperations.createModifyGroupTitle("New title"),
DecryptedGroupChange.newBuilder()
.setNewTitle(DecryptedString.newBuilder().setValue("New title")));
}
@Test
public void can_decrypt_avatar_key_field_11() {
assertDecryption(GroupChange.Actions.newBuilder()
.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar("New avatar")),
DecryptedGroupChange.newBuilder()
.setNewAvatar(DecryptedString.newBuilder().setValue("New avatar")));
}
@Test
public void can_decrypt_timer_value_field_12() {
assertDecryption(groupOperations.createModifyGroupTimerChange(100),
DecryptedGroupChange.newBuilder()
.setNewTimer(DecryptedTimer.newBuilder().setDuration(100)));
}
@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));
}
@Test
public void can_pass_through_new_membership_rights_field_14() {
assertDecryption(GroupChange.Actions.newBuilder()
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction.newBuilder()
.setMembersAccess(AccessControl.AccessRequired.ADMINISTRATOR)),
DecryptedGroupChange.newBuilder()
.setNewMemberAccess(AccessControl.AccessRequired.ADMINISTRATOR));
}
private static ProfileKey newProfileKey() {
try {
return new ProfileKey(Util.getSecretBytes(32));
} catch (InvalidInputException e) {
throw new AssertionError(e);
}
}
static GroupCandidate groupCandidate(UUID uuid) {
return new GroupCandidate(uuid, Optional.absent());
}
GroupCandidate groupCandidate(UUID uuid, ProfileKey profileKey) {
try {
ClientZkProfileOperations profileOperations = clientZkOperations.getProfileOperations();
ProfileKeyCommitment commitment = profileKey.getCommitment(uuid);
ProfileKeyCredentialRequestContext requestContext = profileOperations.createProfileKeyCredentialRequestContext(uuid, profileKey);
ProfileKeyCredentialRequest request = requestContext.getRequest();
ProfileKeyCredentialResponse profileKeyCredentialResponse = server.getProfileKeyCredentialResponse(request, uuid, commitment);
ProfileKeyCredential profileKeyCredential = profileOperations.receiveProfileKeyCredential(requestContext, profileKeyCredentialResponse);
GroupCandidate groupCandidate = new GroupCandidate(uuid, Optional.of(profileKeyCredential));
ProfileKeyCredentialPresentation presentation = profileOperations.createProfileKeyCredentialPresentation(groupSecretParams, profileKeyCredential);
server.assertProfileKeyCredentialPresentation(groupSecretParams.getPublicParams(), presentation);
return groupCandidate;
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
void assertDecryption(GroupChange.Actions.Builder inputChange,
DecryptedGroupChange.Builder expectedDecrypted)
{
UUID editor = UUID.randomUUID();
GroupChange.Actions actions = inputChange.setSourceUuid(groupOperations.encryptUuid(editor))
.build();
GroupChange change = GroupChange.newBuilder()
.setActions(actions.toByteString())
.build();
DecryptedGroupChange decryptedGroupChange = decrypt(change);
assertEquals(expectedDecrypted.setEditor(UuidUtil.toByteString(editor))
.build(),
decryptedGroupChange);
}
private DecryptedGroupChange decrypt(GroupChange build) {
try {
return groupOperations.decryptChange(build, false).get();
} catch (InvalidProtocolBufferException | VerificationFailedException | InvalidGroupStateException e) {
throw new AssertionError(e);
}
}
}

View File

@ -77,6 +77,15 @@ final class ProtoTestUtils {
.build();
}
static DecryptedMember member(UUID uuid, ByteString profileKey, int joinedAtRevision) {
return DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setRole(Member.Role.DEFAULT)
.setJoinedAtRevision(joinedAtRevision)
.setProfileKey(profileKey)
.build();
}
static DecryptedPendingMemberRemoval pendingMemberRemoval(UUID uuid) {
return DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))

View File

@ -0,0 +1,48 @@
package org.whispersystems.signalservice.api.groupsv2;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.ServerSecretParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.groups.GroupPublicParams;
import org.signal.zkgroup.profiles.ProfileKeyCommitment;
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.zkgroup.profiles.ProfileKeyCredentialRequest;
import org.signal.zkgroup.profiles.ProfileKeyCredentialResponse;
import org.signal.zkgroup.profiles.ServerZkProfileOperations;
import org.whispersystems.signalservice.testutil.ZkGroupLibraryUtil;
import java.util.UUID;
/**
* Provides Zk group operations that the server would provide.
*/
final class TestZkGroupServer {
private final ServerPublicParams serverPublicParams;
private final ServerZkProfileOperations serverZkProfileOperations;
TestZkGroupServer() {
ZkGroupLibraryUtil.assumeZkGroupSupportedOnOS();
ServerSecretParams serverSecretParams = ServerSecretParams.generate();
serverPublicParams = serverSecretParams.getPublicParams();
serverZkProfileOperations = new ServerZkProfileOperations(serverSecretParams);
}
public ServerPublicParams getServerPublicParams() {
return serverPublicParams;
}
public ProfileKeyCredentialResponse getProfileKeyCredentialResponse(ProfileKeyCredentialRequest request, UUID uuid, ProfileKeyCommitment commitment) throws VerificationFailedException {
return serverZkProfileOperations.issueProfileKeyCredential(request, uuid, commitment);
}
public void assertProfileKeyCredentialPresentation(GroupPublicParams publicParams, ProfileKeyCredentialPresentation profileKeyCredentialPresentation) {
try {
serverZkProfileOperations.verifyProfileKeyCredentialPresentation(publicParams, profileKeyCredentialPresentation);
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
}

View File

@ -0,0 +1,42 @@
package org.whispersystems.signalservice.testutil;
import org.signal.zkgroup.internal.Native;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeNoException;
public final class ZkGroupLibraryUtil {
private ZkGroupLibraryUtil() {
}
/**
* Attempts to initialize the ZkGroup Native class, which will load the native binaries.
* <p>
* If that fails to link, then on Unix, it will fail as we rely on that for CI.
* <p>
* If that fails to link, and it's not Unix, it will skip the test via assumption violation.
*/
public static void assumeZkGroupSupportedOnOS() {
try {
Class.forName(Native.class.getName());
} catch (ClassNotFoundException e) {
fail();
} catch (UnsatisfiedLinkError | NoClassDefFoundError e) {
String osName = System.getProperty("os.name");
if (isUnix(osName)) {
fail("Not able to link native ZkGroup on a key OS: " + osName);
} else {
assumeNoException("Not able to link native ZkGroup on this operating system: " + osName, e);
}
}
}
private static boolean isUnix(String osName) {
assertNotNull(osName);
osName = osName.toLowerCase();
return osName.contains("nix") || osName.contains("nux") || osName.contains("aix");
}
}