]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
Fix NPR when loading an inactive group
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / GroupV2Helper.java
index 30de7b19a62d2240429b7e1b71e035c29572c5ab..8eb66843335af224dcac1b9ba51bb2919458e2aa 100644 (file)
@@ -1,8 +1,5 @@
 package org.asamk.signal.manager.helper;
 
-import com.google.protobuf.ByteString;
-import com.google.protobuf.InvalidProtocolBufferException;
-
 import org.asamk.signal.manager.api.GroupLinkState;
 import org.asamk.signal.manager.api.GroupPermission;
 import org.asamk.signal.manager.api.NotAGroupMemberException;
@@ -22,15 +19,17 @@ import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
 import org.signal.storageservice.protos.groups.AccessControl;
 import org.signal.storageservice.protos.groups.GroupChange;
+import org.signal.storageservice.protos.groups.GroupChangeResponse;
 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.DecryptedGroupJoinInfo;
 import org.signal.storageservice.protos.groups.local.DecryptedMember;
 import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
-import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.whispersystems.signalservice.api.groupsv2.DecryptChangeVerificationMode;
+import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
 import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
 import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
 import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage;
@@ -45,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ServiceId.PNI;
 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
 import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
 import org.whispersystems.signalservice.api.util.UuidUtil;
+import org.whispersystems.signalservice.internal.push.exceptions.NotInGroupException;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -59,9 +59,11 @@ import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
+import okio.ByteString;
+
 class GroupV2Helper {
 
-    private final static Logger logger = LoggerFactory.getLogger(GroupV2Helper.class);
+    private static final Logger logger = LoggerFactory.getLogger(GroupV2Helper.class);
 
     private final SignalDependencies dependencies;
     private final Context context;
@@ -77,24 +79,25 @@ class GroupV2Helper {
         groupApiCredentials = null;
     }
 
-    DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
+    DecryptedGroupResponse getDecryptedGroup(final GroupSecretParams groupSecretParams) throws NotAGroupMemberException {
         try {
             final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
             return dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupsV2AuthorizationString);
         } catch (NonSuccessfulResponseCodeException e) {
-            if (e.getCode() == 403) {
+            if (e.code == 403) {
                 throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
             }
             logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
             return null;
-        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+        } catch (IOException | VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
             logger.warn("Failed to retrieve Group V2 info, ignoring: {}", e.getMessage());
             return null;
         }
     }
 
     DecryptedGroupJoinInfo getDecryptedGroupJoinInfo(
-            GroupMasterKey groupMasterKey, GroupLinkPassword password
+            GroupMasterKey groupMasterKey,
+            GroupLinkPassword password
     ) throws IOException, GroupLinkNotActiveException {
         var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
 
@@ -105,19 +108,27 @@ class GroupV2Helper {
     }
 
     GroupHistoryPage getDecryptedGroupHistoryPage(
-            final GroupSecretParams groupSecretParams, int fromRevision
+            final GroupSecretParams groupSecretParams,
+            int fromRevision,
+            long sendEndorsementsExpirationMs
     ) throws NotAGroupMemberException {
         try {
             final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
             return dependencies.getGroupsV2Api()
-                    .getGroupHistoryPage(groupSecretParams, fromRevision, groupsV2AuthorizationString, false);
+                    .getGroupHistoryPage(groupSecretParams,
+                            fromRevision,
+                            groupsV2AuthorizationString,
+                            false,
+                            sendEndorsementsExpirationMs);
+        } catch (NotInGroupException e) {
+            throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
         } catch (NonSuccessfulResponseCodeException e) {
-            if (e.getCode() == 403) {
+            if (e.code == 403) {
                 throw new NotAGroupMemberException(GroupUtils.getGroupIdV2(groupSecretParams), null);
             }
             logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
             return null;
-        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+        } catch (IOException | VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
             logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage());
             return null;
         }
@@ -126,17 +137,15 @@ class GroupV2Helper {
     int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup) {
         ByteString aciBytes = getSelfAci().toByteString();
         ByteString pniBytes = getSelfPni().toByteString();
-        for (DecryptedMember decryptedMember : partialDecryptedGroup.getMembersList()) {
-            if (decryptedMember.getAciBytes().equals(aciBytes) || decryptedMember.getPniBytes().equals(pniBytes)) {
-                return decryptedMember.getJoinedAtRevision();
+        for (DecryptedMember decryptedMember : partialDecryptedGroup.members) {
+            if (decryptedMember.aciBytes.equals(aciBytes) || decryptedMember.pniBytes.equals(pniBytes)) {
+                return decryptedMember.joinedAtRevision;
             }
         }
-        return partialDecryptedGroup.getRevision();
+        return partialDecryptedGroup.revision;
     }
 
-    Pair<GroupInfoV2, DecryptedGroup> createGroup(
-            String name, Set<RecipientId> members, byte[] avatarFile
-    ) {
+    Pair<GroupInfoV2, DecryptedGroupResponse> createGroup(String name, Set<RecipientId> members, byte[] avatarFile) {
         final var newGroup = buildNewGroup(name, members, avatarFile);
         if (newGroup == null) {
             return null;
@@ -145,16 +154,16 @@ class GroupV2Helper {
         final var groupSecretParams = newGroup.getGroupSecretParams();
 
         final GroupsV2AuthorizationString groupAuthForToday;
-        final DecryptedGroup decryptedGroup;
+        final DecryptedGroupResponse response;
         try {
             groupAuthForToday = getGroupAuthForToday(groupSecretParams);
             dependencies.getGroupsV2Api().putNewGroup(newGroup, groupAuthForToday);
-            decryptedGroup = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday);
-        } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+            response = dependencies.getGroupsV2Api().getGroup(groupSecretParams, groupAuthForToday);
+        } catch (IOException | VerificationFailedException | InvalidGroupStateException | InvalidInputException e) {
             logger.warn("Failed to create V2 group: {}", e.getMessage());
             return null;
         }
-        if (decryptedGroup == null) {
+        if (response == null) {
             logger.warn("Failed to create V2 group, unknown error!");
             return null;
         }
@@ -163,12 +172,10 @@ class GroupV2Helper {
         final var masterKey = groupSecretParams.getMasterKey();
         var g = new GroupInfoV2(groupId, masterKey, context.getAccount().getRecipientResolver());
 
-        return new Pair<>(g, decryptedGroup);
+        return new Pair<>(g, response);
     }
 
-    private GroupsV2Operations.NewGroup buildNewGroup(
-            String name, Set<RecipientId> members, byte[] avatar
-    ) {
+    private GroupsV2Operations.NewGroup buildNewGroup(String name, Set<RecipientId> members, byte[] avatar) {
         final var profileKeyCredential = context.getProfileHelper()
                 .getExpiringProfileKeyCredential(context.getAccount().getSelfRecipientId());
         if (profileKeyCredential == null) {
@@ -197,31 +204,35 @@ class GroupV2Helper {
                         0);
     }
 
-    Pair<DecryptedGroup, GroupChange> updateGroup(
-            GroupInfoV2 groupInfoV2, String name, String description, byte[] avatarFile
+    Pair<DecryptedGroup, GroupChangeResponse> updateGroup(
+            GroupInfoV2 groupInfoV2,
+            String name,
+            String description,
+            byte[] avatarFile
     ) throws IOException {
         final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
         var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
 
-        var change = name != null ? groupOperations.createModifyGroupTitle(name) : GroupChange.Actions.newBuilder();
+        var change = name != null ? groupOperations.createModifyGroupTitle(name) : new GroupChange.Actions.Builder();
 
         if (description != null) {
-            change.setModifyDescription(groupOperations.createModifyGroupDescriptionAction(description));
+            change.modifyDescription(groupOperations.createModifyGroupDescriptionAction(description).build());
         }
 
         if (avatarFile != null) {
             var avatarCdnKey = dependencies.getGroupsV2Api()
                     .uploadAvatar(avatarFile, groupSecretParams, getGroupAuthForToday(groupSecretParams));
-            change.setModifyAvatar(GroupChange.Actions.ModifyAvatarAction.newBuilder().setAvatar(avatarCdnKey));
+            change.modifyAvatar(new GroupChange.Actions.ModifyAvatarAction.Builder().avatar(avatarCdnKey).build());
         }
 
-        change.setSourceServiceId(getSelfAci().toByteString());
+        change.sourceServiceId(getSelfAci().toByteString());
 
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> addMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> newMembers
+    Pair<DecryptedGroup, GroupChangeResponse> addMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> newMembers
     ) throws IOException {
         GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
@@ -241,15 +252,16 @@ class GroupV2Helper {
         final var aci = getSelfAci();
         final var change = groupOperations.createModifyGroupMembershipChange(candidates, bannedUuids, aci);
 
-        change.setSourceServiceId(getSelfAci().toByteString());
+        change.sourceServiceId(getSelfAci().toByteString());
 
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> leaveGroup(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> membersToMakeAdmin
+    Pair<DecryptedGroup, GroupChangeResponse> leaveGroup(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> membersToMakeAdmin
     ) throws IOException {
-        var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
+        var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
         final var selfAci = getSelfAci();
         var selfPendingMember = DecryptedGroupUtil.findPendingByServiceId(pendingMembersList, selfAci);
 
@@ -266,8 +278,9 @@ class GroupV2Helper {
         return commitChange(groupInfoV2, groupOperations.createLeaveAndPromoteMembersToAdmin(selfAci, adminUuids));
     }
 
-    Pair<DecryptedGroup, GroupChange> removeMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> members
+    Pair<DecryptedGroup, GroupChangeResponse> removeMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> members
     ) throws IOException {
         final var memberUuids = members.stream()
                 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
@@ -278,8 +291,9 @@ class GroupV2Helper {
         return ejectMembers(groupInfoV2, memberUuids);
     }
 
-    Pair<DecryptedGroup, GroupChange> approveJoinRequestMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> members
+    Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequestMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> members
     ) throws IOException {
         final var memberUuids = members.stream()
                 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
@@ -289,8 +303,9 @@ class GroupV2Helper {
         return approveJoinRequest(groupInfoV2, memberUuids);
     }
 
-    Pair<DecryptedGroup, GroupChange> refuseJoinRequestMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> members
+    Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequestMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> members
     ) throws IOException {
         final var memberUuids = members.stream()
                 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
@@ -299,10 +314,11 @@ class GroupV2Helper {
         return refuseJoinRequest(groupInfoV2, memberUuids);
     }
 
-    Pair<DecryptedGroup, GroupChange> revokeInvitedMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> members
+    Pair<DecryptedGroup, GroupChangeResponse> revokeInvitedMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> members
     ) throws IOException {
-        var pendingMembersList = groupInfoV2.getGroup().getPendingMembersList();
+        var pendingMembersList = groupInfoV2.getGroup().pendingMembers;
         final var memberUuids = members.stream()
                 .map(context.getRecipientHelper()::resolveSignalServiceAddress)
                 .map(SignalServiceAddress::getServiceId)
@@ -313,8 +329,9 @@ class GroupV2Helper {
         return revokeInvites(groupInfoV2, memberUuids);
     }
 
-    Pair<DecryptedGroup, GroupChange> banMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> block
+    Pair<DecryptedGroup, GroupChangeResponse> banMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> block
     ) throws IOException {
         GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
@@ -324,15 +341,16 @@ class GroupV2Helper {
 
         final var change = groupOperations.createBanServiceIdsChange(serviceIds,
                 false,
-                groupInfoV2.getGroup().getBannedMembersList());
+                groupInfoV2.getGroup().bannedMembers);
 
-        change.setSourceServiceId(getSelfAci().toByteString());
+        change.sourceServiceId(getSelfAci().toByteString());
 
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> unbanMembers(
-            GroupInfoV2 groupInfoV2, Set<RecipientId> block
+    Pair<DecryptedGroup, GroupChangeResponse> unbanMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<RecipientId> block
     ) throws IOException {
         GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
@@ -342,27 +360,27 @@ class GroupV2Helper {
 
         final var change = groupOperations.createUnbanServiceIdsChange(serviceIds);
 
-        change.setSourceServiceId(getSelfAci().toByteString());
+        change.sourceServiceId(getSelfAci().toByteString());
 
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
+    Pair<DecryptedGroup, GroupChangeResponse> resetGroupLinkPassword(GroupInfoV2 groupInfoV2) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         final var newGroupLinkPassword = GroupLinkPassword.createNew().serialize();
         final var change = groupOperations.createModifyGroupLinkPasswordChange(newGroupLinkPassword);
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> setGroupLinkState(
-            GroupInfoV2 groupInfoV2, GroupLinkState state
+    Pair<DecryptedGroup, GroupChangeResponse> setGroupLinkState(
+            GroupInfoV2 groupInfoV2,
+            GroupLinkState state
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
         final var accessRequired = toAccessControl(state);
-        final var requiresNewPassword = state != GroupLinkState.DISABLED && groupInfoV2.getGroup()
-                .getInviteLinkPassword()
-                .isEmpty();
+        final var requiresNewPassword = state != GroupLinkState.DISABLED
+                && groupInfoV2.getGroup().inviteLinkPassword.toByteArray().length == 0;
 
         final var change = requiresNewPassword ? groupOperations.createModifyGroupLinkPasswordAndRightsChange(
                 GroupLinkPassword.createNew().serialize(),
@@ -370,8 +388,9 @@ class GroupV2Helper {
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> setEditDetailsPermission(
-            GroupInfoV2 groupInfoV2, GroupPermission permission
+    Pair<DecryptedGroup, GroupChangeResponse> setEditDetailsPermission(
+            GroupInfoV2 groupInfoV2,
+            GroupPermission permission
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
@@ -380,8 +399,9 @@ class GroupV2Helper {
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> setAddMemberPermission(
-            GroupInfoV2 groupInfoV2, GroupPermission permission
+    Pair<DecryptedGroup, GroupChangeResponse> setAddMemberPermission(
+            GroupInfoV2 groupInfoV2,
+            GroupPermission permission
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
@@ -390,17 +410,17 @@ class GroupV2Helper {
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException {
+    Pair<DecryptedGroup, GroupChangeResponse> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException {
         Optional<DecryptedMember> selfInGroup = groupInfoV2.getGroup() == null
                 ? Optional.empty()
-                : DecryptedGroupUtil.findMemberByAci(groupInfoV2.getGroup().getMembersList(), getSelfAci());
+                : DecryptedGroupUtil.findMemberByAci(groupInfoV2.getGroup().members, getSelfAci());
         if (selfInGroup.isEmpty()) {
             logger.trace("Not updating group, self not in group " + groupInfoV2.getGroupId().toBase64());
             return null;
         }
 
         final var profileKey = context.getAccount().getProfileKey();
-        if (Arrays.equals(profileKey.serialize(), selfInGroup.get().getProfileKey().toByteArray())) {
+        if (Arrays.equals(profileKey.serialize(), selfInGroup.get().profileKey.toByteArray())) {
             logger.trace("Not updating group, own Profile Key is already up to date in group "
                     + groupInfoV2.getGroupId().toBase64());
             return null;
@@ -416,11 +436,11 @@ class GroupV2Helper {
 
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential);
-        change.setSourceServiceId(getSelfAci().toByteString());
+        change.sourceServiceId(getSelfAci().toByteString());
         return commitChange(groupInfoV2, change);
     }
 
-    GroupChange joinGroup(
+    GroupChangeResponse joinGroup(
             GroupMasterKey groupMasterKey,
             GroupLinkPassword groupLinkPassword,
             DecryptedGroupJoinInfo decryptedGroupJoinInfo
@@ -434,20 +454,20 @@ class GroupV2Helper {
             throw new IOException("Cannot join a V2 group as self does not have a versioned profile");
         }
 
-        var requestToJoin = decryptedGroupJoinInfo.getAddFromInviteLink() == AccessControl.AccessRequired.ADMINISTRATOR;
+        var requestToJoin = decryptedGroupJoinInfo.addFromInviteLink == AccessControl.AccessRequired.ADMINISTRATOR;
         var change = requestToJoin
                 ? groupOperations.createGroupJoinRequest(profileKeyCredential)
                 : groupOperations.createGroupJoinDirect(profileKeyCredential);
 
-        change.setSourceServiceId(context.getRecipientHelper()
+        change.sourceServiceId(context.getRecipientHelper()
                 .resolveSignalServiceAddress(selfRecipientId)
                 .getServiceId()
                 .toByteString());
 
-        return commitChange(groupSecretParams, decryptedGroupJoinInfo.getRevision(), change, groupLinkPassword);
+        return commitChange(groupSecretParams, decryptedGroupJoinInfo.revision, change, groupLinkPassword);
     }
 
-    Pair<DecryptedGroup, GroupChange> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
+    Pair<DecryptedGroup, GroupChangeResponse> acceptInvite(GroupInfoV2 groupInfoV2) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
 
         final var selfRecipientId = context.getAccount().getSelfRecipientId();
@@ -459,13 +479,15 @@ class GroupV2Helper {
         final var change = groupOperations.createAcceptInviteChange(profileKeyCredential);
 
         final var aci = context.getRecipientHelper().resolveSignalServiceAddress(selfRecipientId).getServiceId();
-        change.setSourceServiceId(aci.toByteString());
+        change.sourceServiceId(aci.toByteString());
 
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> setMemberAdmin(
-            GroupInfoV2 groupInfoV2, RecipientId recipientId, boolean admin
+    Pair<DecryptedGroup, GroupChangeResponse> setMemberAdmin(
+            GroupInfoV2 groupInfoV2,
+            RecipientId recipientId,
+            boolean admin
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         final var address = context.getRecipientHelper().resolveSignalServiceAddress(recipientId);
@@ -478,16 +500,18 @@ class GroupV2Helper {
         }
     }
 
-    Pair<DecryptedGroup, GroupChange> setMessageExpirationTimer(
-            GroupInfoV2 groupInfoV2, int messageExpirationTimer
+    Pair<DecryptedGroup, GroupChangeResponse> setMessageExpirationTimer(
+            GroupInfoV2 groupInfoV2,
+            int messageExpirationTimer
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         final var change = groupOperations.createModifyGroupTimerChange(messageExpirationTimer);
         return commitChange(groupInfoV2, change);
     }
 
-    Pair<DecryptedGroup, GroupChange> setIsAnnouncementGroup(
-            GroupInfoV2 groupInfoV2, boolean isAnnouncementGroup
+    Pair<DecryptedGroup, GroupChangeResponse> setIsAnnouncementGroup(
+            GroupInfoV2 groupInfoV2,
+            boolean isAnnouncementGroup
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         final var change = groupOperations.createAnnouncementGroupChange(isAnnouncementGroup);
@@ -514,13 +538,14 @@ class GroupV2Helper {
         return dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
     }
 
-    private Pair<DecryptedGroup, GroupChange> revokeInvites(
-            GroupInfoV2 groupInfoV2, Set<DecryptedPendingMember> pendingMembers
+    private Pair<DecryptedGroup, GroupChangeResponse> revokeInvites(
+            GroupInfoV2 groupInfoV2,
+            Set<DecryptedPendingMember> pendingMembers
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         final var uuidCipherTexts = pendingMembers.stream().map(member -> {
             try {
-                return new UuidCiphertext(member.getServiceIdCipherText().toByteArray());
+                return new UuidCiphertext(member.serviceIdCipherText.toByteArray());
             } catch (InvalidInputException e) {
                 throw new AssertionError(e);
             }
@@ -528,35 +553,39 @@ class GroupV2Helper {
         return commitChange(groupInfoV2, groupOperations.createRemoveInvitationChange(uuidCipherTexts));
     }
 
-    private Pair<DecryptedGroup, GroupChange> approveJoinRequest(
-            GroupInfoV2 groupInfoV2, Set<UUID> uuids
+    private Pair<DecryptedGroup, GroupChangeResponse> approveJoinRequest(
+            GroupInfoV2 groupInfoV2,
+            Set<UUID> uuids
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         return commitChange(groupInfoV2, groupOperations.createApproveGroupJoinRequest(uuids));
     }
 
-    private Pair<DecryptedGroup, GroupChange> refuseJoinRequest(
-            GroupInfoV2 groupInfoV2, Set<ServiceId> serviceIds
+    private Pair<DecryptedGroup, GroupChangeResponse> refuseJoinRequest(
+            GroupInfoV2 groupInfoV2,
+            Set<ServiceId> serviceIds
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         return commitChange(groupInfoV2, groupOperations.createRefuseGroupJoinRequest(serviceIds, false, List.of()));
     }
 
-    private Pair<DecryptedGroup, GroupChange> ejectMembers(
-            GroupInfoV2 groupInfoV2, Set<ACI> members
+    private Pair<DecryptedGroup, GroupChangeResponse> ejectMembers(
+            GroupInfoV2 groupInfoV2,
+            Set<ACI> members
     ) throws IOException {
         final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2);
         return commitChange(groupInfoV2, groupOperations.createRemoveMembersChange(members, false, List.of()));
     }
 
-    private Pair<DecryptedGroup, GroupChange> commitChange(
-            GroupInfoV2 groupInfoV2, GroupChange.Actions.Builder change
+    private Pair<DecryptedGroup, GroupChangeResponse> commitChange(
+            GroupInfoV2 groupInfoV2,
+            GroupChange.Actions.Builder change
     ) throws IOException {
         final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
         final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
         final var previousGroupState = groupInfoV2.getGroup();
-        final var nextRevision = previousGroupState.getRevision() + 1;
-        final var changeActions = change.setRevision(nextRevision).build();
+        final var nextRevision = previousGroupState.revision + 1;
+        final var changeActions = change.revision(nextRevision).build();
         final DecryptedGroupChange decryptedChange;
         final DecryptedGroup decryptedGroupState;
 
@@ -570,17 +599,19 @@ class GroupV2Helper {
         var signedGroupChange = dependencies.getGroupsV2Api()
                 .patchGroup(changeActions, getGroupAuthForToday(groupSecretParams), Optional.empty());
 
+        groupInfoV2.setGroup(decryptedGroupState);
+
         return new Pair<>(decryptedGroupState, signedGroupChange);
     }
 
-    private GroupChange commitChange(
+    private GroupChangeResponse commitChange(
             GroupSecretParams groupSecretParams,
             int currentRevision,
             GroupChange.Actions.Builder change,
             GroupLinkPassword password
     ) throws IOException {
         final var nextRevision = currentRevision + 1;
-        final var changeActions = change.setRevision(nextRevision).build();
+        final var changeActions = change.revision(nextRevision).build();
 
         return dependencies.getGroupsV2Api()
                 .patchGroup(changeActions,
@@ -589,17 +620,16 @@ class GroupV2Helper {
     }
 
     Pair<ServiceId, ProfileKey> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) {
-        UUID editor = UuidUtil.fromByteStringOrNull(change.getEditorServiceIdBytes());
-        final var editorProfileKeyBytes = Stream.concat(Stream.of(change.getNewMembersList().stream(),
-                                change.getPromotePendingMembersList().stream(),
-                                change.getModifiedProfileKeysList().stream())
+        UUID editor = UuidUtil.fromByteStringOrNull(change.editorServiceIdBytes);
+        final var editorProfileKeyBytes = Stream.concat(Stream.of(change.newMembers.stream(),
+                                change.promotePendingMembers.stream(),
+                                change.modifiedProfileKeys.stream())
                         .flatMap(Function.identity())
-                        .filter(m -> UuidUtil.fromByteString(m.getAciBytes()).equals(editor))
-                        .map(DecryptedMember::getProfileKey),
-                change.getNewRequestingMembersList()
-                        .stream()
-                        .filter(m -> UuidUtil.fromByteString(m.getAciBytes()).equals(editor))
-                        .map(DecryptedRequestingMember::getProfileKey)).findFirst();
+                        .filter(m -> UuidUtil.fromByteString(m.aciBytes).equals(editor))
+                        .map(m -> m.profileKey),
+                change.newRequestingMembers.stream()
+                        .filter(m -> UuidUtil.fromByteString(m.aciBytes).equals(editor))
+                        .map(m -> m.profileKey)).findFirst();
 
         if (editorProfileKeyBytes.isEmpty()) {
             return null;
@@ -626,12 +656,14 @@ class GroupV2Helper {
 
     DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
         if (signedGroupChange != null) {
-            var groupOperations = dependencies.getGroupsV2Operations()
-                    .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
+            final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
+            final var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams);
+            final var groupId = groupSecretParams.getPublicParams().getGroupIdentifier();
 
             try {
-                return groupOperations.decryptChange(GroupChange.parseFrom(signedGroupChange), true).orElse(null);
-            } catch (VerificationFailedException | InvalidGroupStateException | InvalidProtocolBufferException e) {
+                return groupOperations.decryptChange(GroupChange.ADAPTER.decode(signedGroupChange),
+                        DecryptChangeVerificationMode.verify(groupId)).orElse(null);
+            } catch (VerificationFailedException | InvalidGroupStateException | IOException e) {
                 return null;
             }
         }
@@ -672,7 +704,8 @@ class GroupV2Helper {
     }
 
     private GroupsV2AuthorizationString getAuthorizationString(
-            final GroupSecretParams groupSecretParams, final long todaySeconds
+            final GroupSecretParams groupSecretParams,
+            final long todaySeconds
     ) throws VerificationFailedException {
         var authCredentialResponse = groupApiCredentials.get(todaySeconds);
         final var aci = getSelfAci();