Only update profile keys from authoritative group changes

This commit is contained in:
AsamK 2022-05-19 12:23:35 +02:00
parent be28d13d0d
commit 06e2811012
2 changed files with 144 additions and 12 deletions

View file

@ -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) {

View file

@ -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));