mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 10:30:38 +00:00
Only update profile keys from authoritative group changes
This commit is contained in:
parent
be28d13d0d
commit
06e2811012
2 changed files with 144 additions and 12 deletions
|
@ -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<RecipientId, ProfileKey>();
|
||||
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) {
|
||||
|
|
|
@ -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<GroupInfoV2, DecryptedGroup> createGroup(
|
||||
String name, Set<RecipientId> 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<ServiceId, ProfileKey> 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));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue