From 6f788ee3df04d61dec8be3d036cf7c6148ff2d74 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Wed, 16 Feb 2022 16:30:45 -0500 Subject: [PATCH] Improve GV2 state change processing speed. --- .../securesms/groups/GroupProtoUtil.java | 10 ++-- .../v2/processing/GroupsV2StateProcessor.java | 11 ++-- .../processing/GroupsV2StateProcessorTest.kt | 11 +++- .../api/groupsv2/GroupsV2Api.java | 12 +++- .../api/groupsv2/GroupsV2Operations.java | 32 ++++++++++ .../api/groupsv2/PartialDecryptedGroup.java | 58 +++++++++++++++++++ 6 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/PartialDecryptedGroup.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java index 320cc01bf..fdc4f82bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/GroupProtoUtil.java @@ -17,7 +17,7 @@ import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; +import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; @@ -30,19 +30,19 @@ public final class GroupProtoUtil { private GroupProtoUtil() { } - public static int findRevisionWeWereAdded(@NonNull DecryptedGroup group, @NonNull UUID uuid) + public static int findRevisionWeWereAdded(@NonNull PartialDecryptedGroup partialDecryptedGroup, @NonNull UUID uuid) throws GroupNotAMemberException { ByteString bytes = UuidUtil.toByteString(uuid); - for (DecryptedMember decryptedMember : group.getMembersList()) { + for (DecryptedMember decryptedMember : partialDecryptedGroup.getMembersList()) { if (decryptedMember.getUuid().equals(bytes)) { return decryptedMember.getJoinedAtRevision(); } } - for (DecryptedPendingMember decryptedMember : group.getPendingMembersList()) { + for (DecryptedPendingMember decryptedMember : partialDecryptedGroup.getPendingMembersList()) { if (decryptedMember.getUuid().equals(bytes)) { // Assume latest, we don't have any information about when pending members were invited - return group.getRevision(); + return partialDecryptedGroup.getRevision(); } } throw new GroupNotAMemberException(); 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 efcbda760..c0d2385f8 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 @@ -51,6 +51,7 @@ import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage; 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.groupsv2.PartialDecryptedGroup; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.push.exceptions.GroupNotFoundException; @@ -302,11 +303,11 @@ public final class GroupsV2StateProcessor { Log.i(TAG, "Paging from server revision: " + (revision == LATEST ? "latest" : revision) + ", latestOnly: " + latestRevisionOnly); - DecryptedGroup latestServerGroup; - GlobalGroupState inputGroupState; + PartialDecryptedGroup latestServerGroup; + GlobalGroupState inputGroupState; try { - latestServerGroup = groupsV2Api.getGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams)); + latestServerGroup = groupsV2Api.getPartialDecryptedGroup(groupSecretParams, groupsV2Authorization.getAuthorizationForToday(selfAci, groupSecretParams)); } catch (NotInGroupException | GroupNotFoundException e) { throw new GroupNotAMemberException(e); } catch (VerificationFailedException | InvalidGroupStateException e) { @@ -315,12 +316,12 @@ public final class GroupsV2StateProcessor { if (localState != null && localState.getRevision() >= latestServerGroup.getRevision()) { Log.i(TAG, "Local state is at or later than server"); - return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, latestServerGroup); + return new GroupUpdateResult(GroupState.GROUP_CONSISTENT_OR_AHEAD, null); } if (latestRevisionOnly || !GroupProtoUtil.isMember(selfAci.uuid(), latestServerGroup.getMembersList())) { Log.i(TAG, "Latest revision or not a member, use latest only"); - inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup, null))); + inputGroupState = new GlobalGroupState(localState, Collections.singletonList(new ServerGroupLogEntry(latestServerGroup.getFullyDecryptedGroup(), null))); } else { int revisionWeWereAdded = GroupProtoUtil.findRevisionWeWereAdded(latestServerGroup, selfAci.uuid()); int logsNeededFrom = localState != null ? Math.max(localState.getRevision(), revisionWeWereAdded) : revisionWeWereAdded; diff --git a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt index c98ec241c..c062dc2a0 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/groups/v2/processing/GroupsV2StateProcessorTest.kt @@ -19,6 +19,7 @@ import org.mockito.Mockito.verify import org.robolectric.RobolectricTestRunner import org.robolectric.annotation.Config import org.signal.core.util.logging.Log +import org.signal.storageservice.protos.groups.local.DecryptedGroup import org.signal.storageservice.protos.groups.local.DecryptedGroupChange import org.signal.storageservice.protos.groups.local.DecryptedMember import org.signal.storageservice.protos.groups.local.DecryptedTimer @@ -40,6 +41,7 @@ import org.thoughtcrime.securesms.testutil.SystemOutLogger import org.thoughtcrime.securesms.util.Hex.fromStringCondensed import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api +import org.whispersystems.signalservice.api.groupsv2.PartialDecryptedGroup import org.whispersystems.signalservice.api.push.ACI import java.util.UUID @@ -92,8 +94,13 @@ class GroupsV2StateProcessorTest { doReturn(data.groupRecord).`when`(groupDatabase).getGroup(any(GroupId.V2::class.java)) doReturn(!data.groupRecord.isPresent).`when`(groupDatabase).isUnknownGroup(any()) - if (data.serverState != null) { - doReturn(data.serverState).`when`(groupsV2API).getGroup(any(), any()) + data.serverState?.let { serverState -> + val testPartial = object : PartialDecryptedGroup(null, serverState, null, null) { + override fun getFullyDecryptedGroup(): DecryptedGroup { + return serverState + } + } + doReturn(testPartial).`when`(groupsV2API).getPartialDecryptedGroup(any(), any()) } data.changeSet?.let { changeSet -> diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java index ed9c4b197..a9a046f75 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Api.java @@ -20,7 +20,6 @@ import org.signal.zkgroup.auth.AuthCredentialResponse; import org.signal.zkgroup.auth.ClientZkAuthOperations; import org.signal.zkgroup.groups.ClientZkGroupCipher; import org.signal.zkgroup.groups.GroupSecretParams; -import org.whispersystems.libsignal.logging.Log; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.internal.push.PushServiceSocket; @@ -30,7 +29,6 @@ import java.io.IOException; import java.security.SecureRandom; import java.util.ArrayList; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; public class GroupsV2Api { @@ -85,6 +83,16 @@ public class GroupsV2Api { socket.putNewGroupsV2Group(group, authorization); } + public PartialDecryptedGroup getPartialDecryptedGroup(GroupSecretParams groupSecretParams, + GroupsV2AuthorizationString authorization) + throws IOException, InvalidGroupStateException, VerificationFailedException + { + Group group = socket.getGroupsV2Group(authorization); + + return groupsOperations.forGroup(groupSecretParams) + .partialDecryptGroup(group); + } + public DecryptedGroup getGroup(GroupSecretParams groupSecretParams, GroupsV2AuthorizationString authorization) throws IOException, InvalidGroupStateException, VerificationFailedException 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 31016ed97..1ffff969c 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 @@ -369,6 +369,38 @@ public final class GroupsV2Operations { .setMember(member); } + public PartialDecryptedGroup partialDecryptGroup(Group group) + throws VerificationFailedException, InvalidGroupStateException + { + List membersList = group.getMembersList(); + List pendingMembersList = group.getPendingMembersList(); + List decryptedMembers = new ArrayList<>(membersList.size()); + List decryptedPendingMembers = new ArrayList<>(pendingMembersList.size()); + + for (Member member : membersList) { + UUID memberUuid = decryptUuid(member.getUserId()); + decryptedMembers.add(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(memberUuid)) + .setJoinedAtRevision(member.getJoinedAtRevision()) + .build()); + } + + for (PendingMember member : pendingMembersList) { + UUID pendingMemberUuid = decryptUuidOrUnknown(member.getMember().getUserId()); + decryptedMembers.add(DecryptedMember.newBuilder() + .setUuid(UuidUtil.toByteString(pendingMemberUuid)) + .build()); + } + + DecryptedGroup decryptedGroup = DecryptedGroup.newBuilder() + .setRevision(group.getRevision()) + .addAllMembers(decryptedMembers) + .addAllPendingMembers(decryptedPendingMembers) + .build(); + + return new PartialDecryptedGroup(group, decryptedGroup, GroupsV2Operations.this, groupSecretParams); + } + public DecryptedGroup decryptGroup(Group group) throws VerificationFailedException, InvalidGroupStateException { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/PartialDecryptedGroup.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/PartialDecryptedGroup.java new file mode 100644 index 000000000..5c0a9043c --- /dev/null +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/PartialDecryptedGroup.java @@ -0,0 +1,58 @@ +package org.whispersystems.signalservice.api.groupsv2; + +import org.signal.storageservice.protos.groups.Group; +import org.signal.storageservice.protos.groups.local.DecryptedGroup; +import org.signal.storageservice.protos.groups.local.DecryptedMember; +import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; +import org.signal.zkgroup.VerificationFailedException; +import org.signal.zkgroup.groups.GroupSecretParams; + +import java.io.IOException; +import java.util.List; + +/** + * Decrypting an entire group can be expensive for large groups. Since not every + * operation requires all data to be decrypted, this class can be populated with only + * the minimalist about of information need to perform an operation. Currently, only + * updating from the server utilizes it. + */ +public class PartialDecryptedGroup { + private final Group group; + private final DecryptedGroup decryptedGroup; + private final GroupsV2Operations groupsOperations; + private final GroupSecretParams groupSecretParams; + + public PartialDecryptedGroup(Group group, + DecryptedGroup decryptedGroup, + GroupsV2Operations groupsOperations, + GroupSecretParams groupSecretParams) + { + this.group = group; + this.decryptedGroup = decryptedGroup; + this.groupsOperations = groupsOperations; + this.groupSecretParams = groupSecretParams; + } + + public int getRevision() { + return decryptedGroup.getRevision(); + } + + public List getMembersList() { + return decryptedGroup.getMembersList(); + } + + public List getPendingMembersList() { + return decryptedGroup.getPendingMembersList(); + } + + public DecryptedGroup getFullyDecryptedGroup() + throws IOException + { + try { + return groupsOperations.forGroup(groupSecretParams) + .decryptGroup(group); + } catch (VerificationFailedException | InvalidGroupStateException e) { + throw new IOException(e); + } + } +}