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;
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;
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) {
}
}
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);
}
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) {
package org.asamk.signal.manager.helper;
+import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.asamk.signal.manager.SignalDependencies;
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;
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;
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;
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 {
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 {
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));