From 06e281101223662c6ad4e1243a3d997b26cb8bc4 Mon Sep 17 00:00:00 2001 From: AsamK Date: Thu, 19 May 2022 12:23:35 +0200 Subject: [PATCH] Only update profile keys from authoritative group changes --- .../signal/manager/helper/GroupHelper.java | 82 ++++++++++++++++++- .../signal/manager/helper/GroupV2Helper.java | 74 +++++++++++++++-- 2 files changed, 144 insertions(+), 12 deletions(-) diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java index ab3e1264..9346372c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java @@ -31,9 +31,11 @@ import org.signal.libsignal.zkgroup.groups.GroupSecretParams; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.storageservice.protos.groups.GroupChange; 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.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; @@ -50,8 +52,10 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -123,12 +127,22 @@ public class GroupHelper { if (signedGroupChange != null && groupInfoV2.getGroup() != null && groupInfoV2.getGroup().getRevision() + 1 == revision) { - group = context.getGroupV2Helper() - .getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey); + final var decryptedGroupChange = context.getGroupV2Helper() + .getDecryptedGroupChange(signedGroupChange, groupMasterKey); + + if (decryptedGroupChange != null) { + storeProfileKeyFromChange(decryptedGroupChange); + group = context.getGroupV2Helper() + .getUpdatedDecryptedGroup(groupInfoV2.getGroup(), decryptedGroupChange); + } } if (group == null) { try { group = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams); + + if (group != null) { + storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group); + } } catch (NotAGroupMemberException ignored) { } } @@ -373,6 +387,17 @@ public class GroupHelper { groupInfoV2.setPermissionDenied(true); decryptedGroup = null; } + if (decryptedGroup != null) { + try { + storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, decryptedGroup); + } catch (NotAGroupMemberException ignored) { + } + storeProfileKeysFromMembers(decryptedGroup); + final var avatar = decryptedGroup.getAvatar(); + if (avatar != null && !avatar.isEmpty()) { + downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar); + } + } groupInfoV2.setGroup(decryptedGroup, account.getRecipientStore()); account.getGroupStore().updateGroup(group); } @@ -417,14 +442,63 @@ public class GroupHelper { for (var member : group.getMembersList()) { final var serviceId = ServiceId.fromByteString(member.getUuid()); final var recipientId = account.getRecipientStore().resolveRecipient(serviceId); + final var profileStore = account.getProfileStore(); + if (profileStore.getProfileKey(recipientId) != null) { + // We already have a profile key, not updating it from a non-authoritative source + continue; + } try { - account.getProfileStore() - .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); + profileStore.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); } catch (InvalidInputException ignored) { } } } + private void storeProfileKeyFromChange(final DecryptedGroupChange decryptedGroupChange) { + final var profileKeyFromChange = context.getGroupV2Helper() + .getAuthoritativeProfileKeyFromChange(decryptedGroupChange); + + if (profileKeyFromChange != null) { + final var serviceId = profileKeyFromChange.first(); + final var profileKey = profileKeyFromChange.second(); + final var recipientId = account.getRecipientStore().resolveRecipient(serviceId); + account.getProfileStore().storeProfileKey(recipientId, profileKey); + } + } + + private void storeProfileKeysFromHistory( + final GroupSecretParams groupSecretParams, + final GroupInfoV2 localGroup, + final DecryptedGroup newDecryptedGroup + ) throws NotAGroupMemberException { + final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup); + final var localRevision = localGroup.getGroup() == null ? 0 : localGroup.getGroup().getRevision(); + var fromRevision = Math.max(revisionWeWereAdded, localRevision); + final var newProfileKeys = new HashMap(); + while (true) { + final var page = context.getGroupV2Helper().getDecryptedGroupHistoryPage(groupSecretParams, fromRevision); + page.getResults() + .stream() + .map(DecryptedGroupHistoryEntry::getChange) + .filter(Optional::isPresent) + .map(Optional::get) + .map(context.getGroupV2Helper()::getAuthoritativeProfileKeyFromChange) + .filter(Objects::nonNull) + .forEach(p -> { + final var serviceId = p.first(); + final var profileKey = p.second(); + final var recipientId = account.getRecipientStore().resolveRecipient(serviceId); + newProfileKeys.put(recipientId, profileKey); + }); + if (!page.getPagingData().hasMorePages()) { + break; + } + fromRevision = page.getPagingData().getNextPageRevision(); + } + + newProfileKeys.forEach(account.getProfileStore()::storeProfileKey); + } + private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { var g = getGroup(groupId); if (g == null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java index 385acc59..9b934580 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java @@ -1,5 +1,6 @@ package org.asamk.signal.manager.helper; +import com.google.protobuf.ByteString; import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.SignalDependencies; @@ -19,6 +20,7 @@ import org.signal.libsignal.zkgroup.auth.AuthCredentialResponse; import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.groups.GroupSecretParams; 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.Member; @@ -27,10 +29,12 @@ 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.DecryptedGroupUtil; import org.whispersystems.signalservice.api.groupsv2.GroupCandidate; +import org.whispersystems.signalservice.api.groupsv2.GroupHistoryPage; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; @@ -40,6 +44,7 @@ import org.whispersystems.signalservice.api.push.ACI; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException; +import org.whispersystems.signalservice.api.util.UuidUtil; import java.io.File; import java.io.FileInputStream; @@ -53,7 +58,9 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.TimeUnit; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; class GroupV2Helper { @@ -96,6 +103,35 @@ class GroupV2Helper { getGroupAuthForToday(groupSecretParams)); } + GroupHistoryPage getDecryptedGroupHistoryPage( + final GroupSecretParams groupSecretParams, int fromRevision + ) throws NotAGroupMemberException { + try { + final var groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams); + return dependencies.getGroupsV2Api() + .getGroupHistoryPage(groupSecretParams, fromRevision, groupsV2AuthorizationString, false); + } catch (NonSuccessfulResponseCodeException e) { + if (e.getCode() == 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) { + logger.warn("Failed to retrieve Group V2 history, ignoring: {}", e.getMessage()); + return null; + } + } + + int findRevisionWeWereAdded(DecryptedGroup partialDecryptedGroup) { + ByteString bytes = UuidUtil.toByteString(getSelfAci().uuid()); + for (DecryptedMember decryptedMember : partialDecryptedGroup.getMembersList()) { + if (decryptedMember.getUuid().equals(bytes)) { + return decryptedMember.getJoinedAtRevision(); + } + } + return partialDecryptedGroup.getRevision(); + } + Pair createGroup( String name, Set members, File avatarFile ) throws IOException { @@ -522,21 +558,43 @@ class GroupV2Helper { Optional.ofNullable(password).map(GroupLinkPassword::serialize)); } - DecryptedGroup getUpdatedDecryptedGroup( - DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey - ) { + Pair getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) { + UUID editor = UuidUtil.fromByteStringOrNull(change.getEditor()); + final var editorProfileKeyBytes = Stream.concat(Stream.of(change.getNewMembersList().stream(), + change.getPromotePendingMembersList().stream(), + change.getModifiedProfileKeysList().stream()) + .flatMap(Function.identity()) + .filter(m -> UuidUtil.fromByteString(m.getUuid()).equals(editor)) + .map(DecryptedMember::getProfileKey), + change.getNewRequestingMembersList() + .stream() + .filter(m -> UuidUtil.fromByteString(m.getUuid()).equals(editor)) + .map(DecryptedRequestingMember::getProfileKey)).findFirst(); + + if (editorProfileKeyBytes.isEmpty()) { + return null; + } + + ProfileKey profileKey; + try { + profileKey = new ProfileKey(editorProfileKeyBytes.get().toByteArray()); + } catch (InvalidInputException e) { + logger.debug("Bad profile key in group"); + return null; + } + + return new Pair<>(ServiceId.from(editor), profileKey); + } + + DecryptedGroup getUpdatedDecryptedGroup(DecryptedGroup group, DecryptedGroupChange decryptedGroupChange) { try { - final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey); - if (decryptedGroupChange == null) { - return null; - } return DecryptedGroupUtil.apply(group, decryptedGroupChange); } catch (NotAbleToApplyGroupV2ChangeException e) { return null; } } - private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) { + DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) { if (signedGroupChange != null) { var groupOperations = dependencies.getGroupsV2Operations() .forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey)); -- 2.50.1