From b28ac7af8c747f036bb35aac72b551a970e3361f Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Mon, 3 Aug 2020 12:06:53 -0300 Subject: [PATCH] Additional tests around rigid Groups V2 change application. --- .../securesms/groups/GroupManagerV2.java | 6 +- .../v2/processing/GroupStateMapper.java | 3 +- .../v2/processing/GroupsV2StateProcessor.java | 4 +- .../api/groupsv2/DecryptedGroupUtil.java | 33 +- .../api/groupsv2/GroupsV2Operations.java | 41 +-- .../NotAbleToApplyGroupV2ChangeException.java | 8 + .../DecryptedGroupUtil_apply_Test.java | 138 +++++-- ...roupsV2Operations_decrypt_change_Test.java | 342 ++++++++++++++++++ .../api/groupsv2/ProtoTestUtils.java | 9 + .../api/groupsv2/TestZkGroupServer.java | 48 +++ .../testutil/ZkGroupLibraryUtil.java | 42 +++ 11 files changed, 574 insertions(+), 100 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NotAbleToApplyGroupV2ChangeException.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java create mode 100644 libsignal/service/src/test/java/org/whispersystems/signalservice/testutil/ZkGroupLibraryUtil.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java index 9fef0482f..103c265c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupManagerV2.java @@ -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); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java index c6ff92ea7..d1ab29777 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupStateMapper.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java index 933110e7b..bb20b00a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessor.java @@ -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); } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java index 2650939b8..a0b3d8130 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil.java @@ -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 toUuidSet(Collection membersList) { - HashSet uuids = new HashSet<>(membersList.size()); - - for (DecryptedMember member : membersList) { - uuids.add(toUuid(member)); - } - - return uuids; - } - public static ArrayList toUuidList(Collection membersList) { ArrayList 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 { - } } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java index 2d4eca5bb..cfdd660c3 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java @@ -136,42 +136,9 @@ public final class GroupsV2Operations { this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams); } - public GroupChange.Actions.Builder createModifyGroupTitleAndMembershipChange(final Optional title, - final Set membersToAdd, - final Set 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 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()); } diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NotAbleToApplyGroupV2ChangeException.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NotAbleToApplyGroupV2ChangeException.java new file mode 100644 index 000000000..91aa2361c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/NotAbleToApplyGroupV2ChangeException.java @@ -0,0 +1,8 @@ +package org.whispersystems.signalservice.api.groupsv2; + +public final class NotAbleToApplyGroupV2ChangeException extends Exception { + + NotAbleToApplyGroupV2ChangeException() { + } + +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java index df0a8970c..4b01f969a 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/DecryptedGroupUtil_apply_Test.java @@ -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() diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java new file mode 100644 index 000000000..d30d83da3 --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations_decrypt_change_Test.java @@ -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 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); + } + } + +} \ No newline at end of file diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java index 16ddeb016..743185bd2 100644 --- a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/ProtoTestUtils.java @@ -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)) diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java new file mode 100644 index 000000000..38f05e12f --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/api/groupsv2/TestZkGroupServer.java @@ -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); + } + } +} diff --git a/libsignal/service/src/test/java/org/whispersystems/signalservice/testutil/ZkGroupLibraryUtil.java b/libsignal/service/src/test/java/org/whispersystems/signalservice/testutil/ZkGroupLibraryUtil.java new file mode 100644 index 000000000..fee55bfe2 --- /dev/null +++ b/libsignal/service/src/test/java/org/whispersystems/signalservice/testutil/ZkGroupLibraryUtil.java @@ -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. + *

+ * If that fails to link, then on Unix, it will fail as we rely on that for CI. + *

+ * 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"); + } +}