mirror of
https://github.com/AsamK/signal-cli
synced 2025-08-29 18:40:39 +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.libsignal.zkgroup.profiles.ProfileKey;
|
||||||
import org.signal.storageservice.protos.groups.GroupChange;
|
import org.signal.storageservice.protos.groups.GroupChange;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
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.DecryptedGroupJoinInfo;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupHistoryEntry;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||||
|
@ -50,8 +52,10 @@ import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@ -123,12 +127,22 @@ public class GroupHelper {
|
||||||
if (signedGroupChange != null
|
if (signedGroupChange != null
|
||||||
&& groupInfoV2.getGroup() != null
|
&& groupInfoV2.getGroup() != null
|
||||||
&& groupInfoV2.getGroup().getRevision() + 1 == revision) {
|
&& groupInfoV2.getGroup().getRevision() + 1 == revision) {
|
||||||
group = context.getGroupV2Helper()
|
final var decryptedGroupChange = context.getGroupV2Helper()
|
||||||
.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey);
|
.getDecryptedGroupChange(signedGroupChange, groupMasterKey);
|
||||||
|
|
||||||
|
if (decryptedGroupChange != null) {
|
||||||
|
storeProfileKeyFromChange(decryptedGroupChange);
|
||||||
|
group = context.getGroupV2Helper()
|
||||||
|
.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), decryptedGroupChange);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (group == null) {
|
if (group == null) {
|
||||||
try {
|
try {
|
||||||
group = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
|
group = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
|
||||||
|
|
||||||
|
if (group != null) {
|
||||||
|
storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group);
|
||||||
|
}
|
||||||
} catch (NotAGroupMemberException ignored) {
|
} catch (NotAGroupMemberException ignored) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -373,6 +387,17 @@ public class GroupHelper {
|
||||||
groupInfoV2.setPermissionDenied(true);
|
groupInfoV2.setPermissionDenied(true);
|
||||||
decryptedGroup = null;
|
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());
|
groupInfoV2.setGroup(decryptedGroup, account.getRecipientStore());
|
||||||
account.getGroupStore().updateGroup(group);
|
account.getGroupStore().updateGroup(group);
|
||||||
}
|
}
|
||||||
|
@ -417,14 +442,63 @@ public class GroupHelper {
|
||||||
for (var member : group.getMembersList()) {
|
for (var member : group.getMembersList()) {
|
||||||
final var serviceId = ServiceId.fromByteString(member.getUuid());
|
final var serviceId = ServiceId.fromByteString(member.getUuid());
|
||||||
final var recipientId = account.getRecipientStore().resolveRecipient(serviceId);
|
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 {
|
try {
|
||||||
account.getProfileStore()
|
profileStore.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
|
||||||
.storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray()));
|
|
||||||
} catch (InvalidInputException ignored) {
|
} 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 {
|
private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException {
|
||||||
var g = getGroup(groupId);
|
var g = getGroup(groupId);
|
||||||
if (g == null) {
|
if (g == null) {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package org.asamk.signal.manager.helper;
|
package org.asamk.signal.manager.helper;
|
||||||
|
|
||||||
|
import com.google.protobuf.ByteString;
|
||||||
import com.google.protobuf.InvalidProtocolBufferException;
|
import com.google.protobuf.InvalidProtocolBufferException;
|
||||||
|
|
||||||
import org.asamk.signal.manager.SignalDependencies;
|
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.GroupMasterKey;
|
||||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||||
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
|
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.AccessControl;
|
||||||
import org.signal.storageservice.protos.groups.GroupChange;
|
import org.signal.storageservice.protos.groups.GroupChange;
|
||||||
import org.signal.storageservice.protos.groups.Member;
|
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.DecryptedGroupJoinInfo;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
import org.signal.storageservice.protos.groups.local.DecryptedMember;
|
||||||
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
|
||||||
|
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupCandidate;
|
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.GroupLinkNotActiveException;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString;
|
||||||
import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations;
|
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.ServiceId;
|
||||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||||
|
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
|
@ -53,7 +58,9 @@ import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
class GroupV2Helper {
|
class GroupV2Helper {
|
||||||
|
|
||||||
|
@ -96,6 +103,35 @@ class GroupV2Helper {
|
||||||
getGroupAuthForToday(groupSecretParams));
|
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(
|
Pair<GroupInfoV2, DecryptedGroup> createGroup(
|
||||||
String name, Set<RecipientId> members, File avatarFile
|
String name, Set<RecipientId> members, File avatarFile
|
||||||
) throws IOException {
|
) throws IOException {
|
||||||
|
@ -522,21 +558,43 @@ class GroupV2Helper {
|
||||||
Optional.ofNullable(password).map(GroupLinkPassword::serialize));
|
Optional.ofNullable(password).map(GroupLinkPassword::serialize));
|
||||||
}
|
}
|
||||||
|
|
||||||
DecryptedGroup getUpdatedDecryptedGroup(
|
Pair<ServiceId, ProfileKey> getAuthoritativeProfileKeyFromChange(final DecryptedGroupChange change) {
|
||||||
DecryptedGroup group, byte[] signedGroupChange, GroupMasterKey groupMasterKey
|
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 {
|
try {
|
||||||
final var decryptedGroupChange = getDecryptedGroupChange(signedGroupChange, groupMasterKey);
|
|
||||||
if (decryptedGroupChange == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return DecryptedGroupUtil.apply(group, decryptedGroupChange);
|
return DecryptedGroupUtil.apply(group, decryptedGroupChange);
|
||||||
} catch (NotAbleToApplyGroupV2ChangeException e) {
|
} catch (NotAbleToApplyGroupV2ChangeException e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
|
DecryptedGroupChange getDecryptedGroupChange(byte[] signedGroupChange, GroupMasterKey groupMasterKey) {
|
||||||
if (signedGroupChange != null) {
|
if (signedGroupChange != null) {
|
||||||
var groupOperations = dependencies.getGroupsV2Operations()
|
var groupOperations = dependencies.getGroupsV2Operations()
|
||||||
.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
|
.forGroup(GroupSecretParams.deriveFromMasterKey(groupMasterKey));
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue