]> nmode's Git Repositories - signal-cli/commitdiff
Rotate profile key after blocking a contact/group
authorAsamK <asamk@gmx.de>
Wed, 18 May 2022 10:19:06 +0000 (12:19 +0200)
committerAsamK <asamk@gmx.de>
Wed, 18 May 2022 17:19:16 +0000 (19:19 +0200)
12 files changed:
lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java
lib/src/main/java/org/asamk/signal/manager/actions/SendProfileKeyAction.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/actions/UpdateAccountAttributesAction.java [new file with mode: 0644]
lib/src/main/java/org/asamk/signal/manager/helper/GroupHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/GroupV2Helper.java
lib/src/main/java/org/asamk/signal/manager/helper/IncomingMessageHandler.java
lib/src/main/java/org/asamk/signal/manager/helper/ProfileHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/SendHelper.java
lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
lib/src/main/java/org/asamk/signal/manager/storage/SignalAccount.java
lib/src/main/java/org/asamk/signal/manager/storage/profiles/ProfileStore.java
lib/src/main/java/org/asamk/signal/manager/storage/recipients/RecipientStore.java

index f6205dba7c7c59b0fb3ae83644a5f48c01f35bbb..b3e6a0ae7b02461943d1bb7e996197bcba2ce851 100644 (file)
@@ -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<GroupId> 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 (file)
index 0000000..5695e45
--- /dev/null
@@ -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 (file)
index 0000000..da04dd1
--- /dev/null
@@ -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();
+    }
+}
index 9d4f712ce9a040d9b74e6fd263b033348d2ddcfb..ab3e1264ca658483bbab6a8e63de093f4cf5965b 100644 (file)
@@ -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<DecryptedGroup, GroupChange> 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<GroupId, SendGroupMessageResults> joinGroup(
             GroupInviteLinkUrl inviteLinkUrl
     ) throws IOException, InactiveGroupLinkException {
index 06a0b89a01e4cb4f4c30c1f6a39f19832c2e1069..385acc5998d239f1138d09dc19b46c9e7de3462c 100644 (file)
@@ -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<DecryptedGroup, GroupChange> updateSelfProfileKey(GroupInfoV2 groupInfoV2) throws IOException {
+        Optional<DecryptedMember> 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,
index 489be8346b24a86af6e45c3e87124d65da8e5ba4..5e310a4844942dd0ba273cf37267d8f25f9da143 100644 (file)
@@ -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,
index 7d68124aa2ea848c31b4d002459e4cb5839cbcc7..4bc00317e65d6acda3c88a0bffa5df5ab94b5178 100644 (file)
@@ -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<File> 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));
                 }
index 59a7a671342d64464fb9a3953c62399f092dc907..a2e2379b73a645c1c217ef19b383b8c38d1a182a 100644 (file)
@@ -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> groupId
     ) {
index de28638e7984057394986915fc21ca697eafa78e..469ca02e57954afdc4b6bad252229140125cfdc8 100644 (file)
@@ -229,6 +229,7 @@ public class StorageHelper {
 
         context.getProfileHelper()
                 .setProfile(false,
+                        false,
                         accountRecord.getGivenName().orElse(null),
                         accountRecord.getFamilyName().orElse(null),
                         null,
index 6b2d063b78530d1411c9f864504fe7dba5903f8b..ac41d791f11877fd8c731ee7582bc7d019ecee3a 100644 (file)
@@ -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() {
index 0ff20042ffe49147f9e7adad27aa5aeaeb1583be..df65db0d09a53006e66b5047b257d170f0c17c73 100644 (file)
@@ -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);
index 16ec9bed5fa8c240590b40c7da093362d43fd1bc..299a398073f3c44cecc1370f604bb6b21d708589 100644 (file)
@@ -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);
         }
     }