From: AsamK Date: Wed, 18 May 2022 10:19:06 +0000 (+0200) Subject: Rotate profile key after blocking a contact/group X-Git-Tag: v0.10.6~3 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/cf1626ea315ba3d7d76fc8f9ec0acc1c7597d631?hp=b1e56faab2b2417af4f4a7f1033ffd752a9d16e4 Rotate profile key after blocking a contact/group --- diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java index f6205dba..b3e6a0ae 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -694,27 +694,48 @@ class ManagerImpl implements Manager { return; } final var recipientIds = context.getRecipientHelper().resolveRecipients(recipients); + final var selfRecipientId = account.getSelfRecipientId(); + boolean shouldRotateProfileKey = false; for (final var recipientId : recipientIds) { + if (context.getContactHelper().isContactBlocked(recipientId) == blocked) { + continue; + } context.getContactHelper().setContactBlocked(recipientId, blocked); + // if we don't have a common group with the blocked contact we need to rotate the profile key + shouldRotateProfileKey = blocked && ( + shouldRotateProfileKey || account.getGroupStore() + .getGroups() + .stream() + .noneMatch(g -> g.isMember(selfRecipientId) && g.isMember(recipientId)) + ); + } + if (shouldRotateProfileKey) { + context.getProfileHelper().rotateProfileKey(); } - // TODO cycle our profile key, if we're not together in a group with recipient context.getSyncHelper().sendBlockedList(); } @Override public void setGroupsBlocked( final Collection groupIds, final boolean blocked - ) throws GroupNotFoundException, NotMasterDeviceException { + ) throws GroupNotFoundException, NotMasterDeviceException, IOException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } if (groupIds.size() == 0) { return; } + boolean shouldRotateProfileKey = false; for (final var groupId : groupIds) { + if (context.getGroupHelper().isGroupBlocked(groupId) == blocked) { + continue; + } context.getGroupHelper().setGroupBlocked(groupId, blocked); + shouldRotateProfileKey = blocked; + } + if (shouldRotateProfileKey) { + context.getProfileHelper().rotateProfileKey(); } - // TODO cycle our profile key context.getSyncHelper().sendBlockedList(); } diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java new file mode 100644 index 00000000..5695e458 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java @@ -0,0 +1,33 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.helper.Context; +import org.asamk.signal.manager.storage.recipients.RecipientId; + +import java.util.Objects; + +public class SendProfileKeyAction implements HandleAction { + + private final RecipientId recipientId; + + public SendProfileKeyAction(final RecipientId recipientId) { + this.recipientId = recipientId; + } + + @Override + public void execute(Context context) throws Throwable { + context.getSendHelper().sendProfileKey(recipientId); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + final SendProfileKeyAction that = (SendProfileKeyAction) o; + return recipientId.equals(that.recipientId); + } + + @Override + public int hashCode() { + return Objects.hash(recipientId); + } +} diff --git a/lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java b/lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java new file mode 100644 index 00000000..da04dd18 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java @@ -0,0 +1,20 @@ +package org.asamk.signal.manager.actions; + +import org.asamk.signal.manager.helper.Context; + +public class UpdateAccountAttributesAction implements HandleAction { + + private static final UpdateAccountAttributesAction INSTANCE = new UpdateAccountAttributesAction(); + + private UpdateAccountAttributesAction() { + } + + public static UpdateAccountAttributesAction create() { + return INSTANCE; + } + + @Override + public void execute(Context context) throws Throwable { + context.getAccountHelper().updateAccountAttributes(); + } +} 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 9d4f712c..ab3e1264 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 @@ -254,6 +254,24 @@ public class GroupHelper { return result; } + public void updateGroupProfileKey(GroupIdV2 groupId) throws GroupNotFoundException, NotAGroupMemberException, IOException { + var group = getGroupForUpdating(groupId); + + if (group instanceof GroupInfoV2 groupInfoV2) { + Pair groupChangePair; + try { + groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + groupInfoV2 = (GroupInfoV2) getGroup(groupId, true); + groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2); + } + if (groupChangePair != null) { + sendUpdateGroupV2Message(groupInfoV2, groupChangePair.first(), groupChangePair.second()); + } + } + } + public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, InactiveGroupLinkException { 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 06a0b89a..385acc59 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 @@ -25,6 +25,7 @@ import org.signal.storageservice.protos.groups.Member; 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.DecryptedMember; import org.signal.storageservice.protos.groups.local.DecryptedPendingMember; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,6 +46,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Optional; @@ -340,6 +342,36 @@ class GroupV2Helper { return commitChange(groupInfoV2, change); } + Pair updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException { + Optional selfInGroup = groupInfoV2.getGroup() == null + ? Optional.empty() + : DecryptedGroupUtil.findMemberByUuid(groupInfoV2.getGroup().getMembersList(), getSelfAci().uuid()); + if (selfInGroup.isEmpty()) { + logger.trace("Not updating group, self not in group " + groupInfoV2.getGroupId().toBase64()); + return null; + } + + final var profileKey = context.getAccount().getProfileKey(); + if (Arrays.equals(profileKey.serialize(), selfInGroup.get().getProfileKey().toByteArray())) { + logger.trace("Not updating group, own Profile Key is already up to date in group " + + groupInfoV2.getGroupId().toBase64()); + return null; + } + logger.debug("Updating own profile key in group " + groupInfoV2.getGroupId().toBase64()); + + final var selfRecipientId = context.getAccount().getSelfRecipientId(); + final var profileKeyCredential = context.getProfileHelper().getRecipientProfileKeyCredential(selfRecipientId); + if (profileKeyCredential == null) { + logger.trace("Cannot update profile key as self does not have a versioned profile"); + return null; + } + + final GroupsV2Operations.GroupOperations groupOperations = getGroupOperations(groupInfoV2); + final var change = groupOperations.createUpdateProfileKeyCredentialChange(profileKeyCredential); + change.setSourceUuid(getSelfAci().toByteString()); + return commitChange(groupInfoV2, change); + } + GroupChange joinGroup( GroupMasterKey groupMasterKey, GroupLinkPassword groupLinkPassword, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java index 489be834..5e310a48 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java @@ -11,6 +11,7 @@ import org.asamk.signal.manager.actions.RetrieveStorageDataAction; import org.asamk.signal.manager.actions.SendGroupInfoAction; import org.asamk.signal.manager.actions.SendGroupInfoRequestAction; import org.asamk.signal.manager.actions.SendPniIdentityKeyAction; +import org.asamk.signal.manager.actions.SendProfileKeyAction; import org.asamk.signal.manager.actions.SendReceiptAction; import org.asamk.signal.manager.actions.SendRetryMessageRequestAction; import org.asamk.signal.manager.actions.SendSyncBlockedListAction; @@ -18,6 +19,7 @@ import org.asamk.signal.manager.actions.SendSyncConfigurationAction; import org.asamk.signal.manager.actions.SendSyncContactsAction; import org.asamk.signal.manager.actions.SendSyncGroupsAction; import org.asamk.signal.manager.actions.SendSyncKeysAction; +import org.asamk.signal.manager.actions.UpdateAccountAttributesAction; import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.StickerPackId; @@ -246,6 +248,13 @@ public final class IncomingMessageHandler { if (content.isNeedsReceipt()) { actions.add(new SendReceiptAction(sender, message.getTimestamp())); + } else { + // Message wasn't sent as unidentified sender message + final var contact = context.getAccount().getContactStore().getContact(sender); + if (contact != null && !contact.isBlocked() && contact.isProfileSharingEnabled()) { + actions.add(UpdateAccountAttributesAction.create()); + actions.add(new SendProfileKeyAction(sender)); + } } actions.addAll(handleSignalServiceDataMessage(message, diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java index 7d68124a..4bc00317 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java @@ -4,11 +4,15 @@ import com.google.protobuf.InvalidProtocolBufferException; import org.asamk.signal.manager.SignalDependencies; import org.asamk.signal.manager.config.ServiceConfig; +import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.util.IOUtils; +import org.asamk.signal.manager.util.KeyUtils; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.protocol.IdentityKey; @@ -57,6 +61,35 @@ public final class ProfileHelper { this.context = context; } + public void rotateProfileKey() throws IOException { + var profileKey = KeyUtils.createProfileKey(); + account.setProfileKey(profileKey); + context.getAccountHelper().updateAccountAttributes(); + setProfile(true, true, null, null, null, null, null); + // TODO update profile key in storage + + final var recipientIds = account.getRecipientStore().getRecipientIdsWithEnabledProfileSharing(); + for (final var recipientId : recipientIds) { + context.getSendHelper().sendProfileKey(recipientId); + } + + final var selfRecipientId = account.getSelfRecipientId(); + final var activeGroupIds = account.getGroupStore() + .getGroups() + .stream() + .filter(g -> g instanceof GroupInfoV2 && g.isMember(selfRecipientId)) + .map(g -> (GroupInfoV2) g) + .map(GroupInfoV2::getGroupId) + .toList(); + for (final var groupId : activeGroupIds) { + try { + context.getGroupHelper().updateGroupProfileKey(groupId); + } catch (GroupNotFoundException | NotAGroupMemberException | IOException e) { + logger.warn("Failed to update group profile key: {}", e.getMessage()); + } + } + } + public Profile getRecipientProfile(RecipientId recipientId) { return getRecipientProfile(recipientId, false); } @@ -106,11 +139,12 @@ public final class ProfileHelper { public void setProfile( String givenName, final String familyName, String about, String aboutEmoji, Optional avatar ) throws IOException { - setProfile(true, givenName, familyName, about, aboutEmoji, avatar); + setProfile(true, false, givenName, familyName, about, aboutEmoji, avatar); } public void setProfile( boolean uploadProfile, + boolean forceUploadAvatar, String givenName, final String familyName, String about, @@ -134,13 +168,14 @@ public final class ProfileHelper { var newProfile = builder.build(); if (uploadProfile) { - try (final var streamDetails = avatar != null && avatar.isPresent() ? Utils.createStreamDetailsFromFile( - avatar.get()) : null) { - final var avatarUploadParams = avatar == null - ? AvatarUploadParams.unchanged(true) - : avatar.isPresent() - ? AvatarUploadParams.forAvatar(streamDetails) - : AvatarUploadParams.unchanged(false); + final var streamDetails = avatar != null && avatar.isPresent() + ? Utils.createStreamDetailsFromFile(avatar.get()) + : forceUploadAvatar && avatar == null ? context.getAvatarStore() + .retrieveProfileAvatar(account.getSelfRecipientAddress()) : null; + try (streamDetails) { + final var avatarUploadParams = streamDetails != null + ? AvatarUploadParams.forAvatar(streamDetails) + : avatar == null ? AvatarUploadParams.unchanged(true) : AvatarUploadParams.unchanged(false); final var paymentsAddress = Optional.ofNullable(newProfile.getPaymentAddress()).map(data -> { try { return SignalServiceProtos.PaymentAddress.parseFrom(data); @@ -148,6 +183,7 @@ public final class ProfileHelper { return null; } }); + logger.debug("Uploading new profile"); final var avatarPath = dependencies.getAccountManager() .setVersionedProfile(account.getAci(), account.getProfileKey(), @@ -156,7 +192,7 @@ public final class ProfileHelper { newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), paymentsAddress, avatarUploadParams, - List.of(/* TODO */)); + List.of(/* TODO implement support for badges */)); if (!avatarUploadParams.keepTheSame) { builder.withAvatarUrlPath(avatarPath.orElse(null)); } diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java index 59a7a671..a2e2379b 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java @@ -139,6 +139,21 @@ public class SendHelper { return result; } + public SendMessageResult sendProfileKey(RecipientId recipientId) { + logger.debug("Sending updated profile key to recipient: {}", recipientId); + final var profileKey = account.getProfileKey().serialize(); + final var message = SignalServiceDataMessage.newBuilder() + .asProfileKeyUpdate(true) + .withProfileKey(profileKey) + .build(); + return handleSendMessage(recipientId, + (messageSender, address, unidentifiedAccess) -> messageSender.sendDataMessage(address, + unidentifiedAccess, + ContentHint.IMPLICIT, + message, + SignalServiceMessageSender.IndividualSendEvents.EMPTY)); + } + public SendMessageResult sendRetryReceipt( DecryptionErrorMessage errorMessage, RecipientId recipientId, Optional groupId ) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java index de28638e..469ca02e 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java @@ -229,6 +229,7 @@ public class StorageHelper { context.getProfileHelper() .setProfile(false, + false, accountRecord.getGivenName().orElse(null), accountRecord.getFamilyName().orElse(null), null, diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java index 6b2d063b..ac41d791 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java @@ -373,7 +373,7 @@ public class SignalAccount implements Closeable { setProfileKey(KeyUtils.createProfileKey()); } // Ensure our profile key is stored in profile store - getProfileStore().storeProfileKey(getSelfRecipientId(), getProfileKey()); + getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey()); if (previousStorageVersion < 3) { for (final var group : groupStore.getGroups()) { if (group instanceof GroupInfoV2 && group.getDistributionId() == null) { @@ -1266,6 +1266,7 @@ public class SignalAccount implements Closeable { } this.profileKey = profileKey; save(); + getProfileStore().storeSelfProfileKey(getSelfRecipientId(), getProfileKey()); } public byte[] getSelfUnidentifiedAccessKey() { diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java index 0ff20042..df65db0d 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java @@ -15,6 +15,8 @@ public interface ProfileStore { void storeProfile(RecipientId recipientId, Profile profile); + void storeSelfProfileKey(RecipientId recipientId, ProfileKey profileKey); + void storeProfileKey(RecipientId recipientId, ProfileKey profileKey); void storeProfileKeyCredential(RecipientId recipientId, ProfileKeyCredential profileKeyCredential); diff --git a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java index 16ec9bed..299a3980 100644 --- a/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java +++ b/lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java @@ -325,8 +325,17 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile } } + @Override + public void storeSelfProfileKey(final RecipientId recipientId, final ProfileKey profileKey) { + storeProfileKey(recipientId, profileKey, false); + } + @Override public void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey) { + storeProfileKey(recipientId, profileKey, true); + } + + private void storeProfileKey(RecipientId recipientId, final ProfileKey profileKey, boolean resetProfile) { synchronized (recipients) { final var recipient = recipients.get(recipientId); if (profileKey != null && profileKey.equals(recipient.getProfileKey()) && ( @@ -339,13 +348,15 @@ public class RecipientStore implements RecipientResolver, ContactsStore, Profile return; } - final var newRecipient = Recipient.newBuilder(recipient) + final var builder = Recipient.newBuilder(recipient) .withProfileKey(profileKey) - .withProfileKeyCredential(null) - .withProfile(recipient.getProfile() == null - ? null - : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build()) - .build(); + .withProfileKeyCredential(null); + if (resetProfile) { + builder.withProfile(recipient.getProfile() == null + ? null + : Profile.newBuilder(recipient.getProfile()).withLastUpdateTimestamp(0).build()); + } + final var newRecipient = builder.build(); storeRecipientLocked(recipientId, newRecipient); } }