diff --git a/CHANGELOG.md b/CHANGELOG.md index d96ac64a..3b7bb43a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,13 @@ - Remove isRegistered method without parameters from Signal dbus interface, which always returned `true` - Remove `sandbox` value for --service-environment parameter, use `staging` instead -- The `daemon` command now requires at least one channel parameter (`--socket`, `--dbus`, ...) and no longer defaults to dbus +- The `daemon` command now requires at least one channel parameter (`--socket`, `--dbus`, ...) and no longer defaults to + dbus + +### Added + +- Add --ignore-avatars flag to prevent downloading avatars +- Add --ignore-stickers flag to prevent downloading sticker packs ## [0.13.24] - 2026-02-05 @@ -27,8 +33,6 @@ Requires libsignal-client version 0.86.12. - Add sendPollCreate, sendPollVote, sendPollTerminate commands for polls - Add updateDevice command to set device name of linked devices -- Add --ignore-avatars flag to prevent downloading avatars -- Add --ignore-stickers flag to prevent downloading sticker packs ### Changed @@ -158,9 +162,11 @@ Requires libsignal-client version 0.68.1. Requires libsignal-client version 0.66.2. ### Added + - Allow setting nickname and note with `updateContact` command ### Fixed + - Fix syncing nickname, note and expiration timer - Fix check for registered users with a proxy - Improve handling of storage records not yet supported by signal-cli @@ -186,6 +192,7 @@ Requires libsignal-client version 0.65.2. Requires libsignal-client version 0.64.0. ### Fixed + - Fix issue with receiving messages that have an invalid destination ## [0.13.10] - 2024-11-30 @@ -198,6 +205,7 @@ Requires libsignal-client version 0.62.0. - Fix receiving expiration timer updates ### Improved + - Add support for new storage encryption scheme ## [0.13.9] - 2024-10-28 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 e0b1cd29..0bd1d280 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 @@ -107,10 +107,7 @@ public class GroupHelper { return group != null && group.isBlocked(); } - public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar, boolean ignoreAvatars) { - if (ignoreAvatars) { - return; - } + public void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { context.getAvatarStore() .storeGroupAvatar(groupId, @@ -133,7 +130,8 @@ public class GroupHelper { public GroupInfoV2 getOrMigrateGroup( final GroupMasterKey groupMasterKey, final int revision, - final byte[] signedGroupChange + final byte[] signedGroupChange, + final boolean ignoreAvatars ) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); @@ -169,8 +167,8 @@ public class GroupHelper { if (group != null) { storeProfileKeysFromMembers(group); final var avatar = group.avatar; - if (!avatar.isEmpty()) { - downloadGroupAvatar(groupId, groupSecretParams, avatar, false); + if (!avatar.isEmpty() && !ignoreAvatars) { + downloadGroupAvatar(groupId, groupSecretParams, avatar); } } groupInfoV2.setGroup(group); @@ -394,7 +392,8 @@ public class GroupHelper { .joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo); final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), groupJoinInfo.revision + 1, - changeResponse.group_change == null ? null : changeResponse.group_change.encode()); + changeResponse.group_change == null ? null : changeResponse.group_change.encode(), + false); if (group.getGroup() == null) { // Only requested member, can't send update to group members @@ -509,16 +508,13 @@ public class GroupHelper { storeProfileKeysFromMembers(decryptedGroup); final var avatar = decryptedGroup.avatar; if (!avatar.isEmpty()) { - downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar, false); + downloadGroupAvatar(groupInfoV2.getGroupId(), groupSecretParams, avatar); } groupInfoV2.setGroup(decryptedGroup); account.getGroupStore().updateGroup(group); } - private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey, boolean ignoreAvatars) { - if (ignoreAvatars) { - return; - } + private void downloadGroupAvatar(GroupIdV2 groupId, GroupSecretParams groupSecretParams, String cdnKey) { try { context.getAvatarStore() .storeGroupAvatar(groupId, 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 e514d44c..e3dfdc27 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 @@ -257,7 +257,7 @@ public final class IncomingMessageHandler { var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); final var groupContext = getGroupContext(content); if (groupContext != null && groupContext.getGroupV2().isPresent()) { - handleGroupV2Context(groupContext.getGroupV2().get()); + handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars()); } // Check again in case the user just joined the group notAllowedToSendToGroup = notAllowedToSendToGroup && isNotAllowedToSendToGroup(envelope, content); @@ -377,15 +377,12 @@ public final class IncomingMessageHandler { if (content.getStoryMessage().isPresent()) { final var message = content.getStoryMessage().get(); - actions.addAll(handleSignalServiceStoryMessage(message, sender, receiveConfig.ignoreAttachments())); + actions.addAll(handleSignalServiceStoryMessage(message, sender, receiveConfig)); } if (content.getSyncMessage().isPresent()) { var syncMessage = content.getSyncMessage().get(); - actions.addAll(handleSyncMessage(envelope, - syncMessage, - senderDeviceAddress, - receiveConfig)); + actions.addAll(handleSyncMessage(envelope, syncMessage, senderDeviceAddress, receiveConfig)); } return actions; @@ -499,7 +496,7 @@ public final class IncomingMessageHandler { if (message.getStoryMessage().isPresent()) { actions.addAll(handleSignalServiceStoryMessage(message.getStoryMessage().get(), sender.recipientId(), - receiveConfig.ignoreAttachments())); + receiveConfig)); } } if (syncMessage.getRequest().isPresent() && account.isPrimaryDevice()) { @@ -525,7 +522,9 @@ public final class IncomingMessageHandler { try { final var groupsMessage = syncMessage.getGroups().get(); context.getAttachmentHelper() - .retrieveAttachment(groupsMessage, input -> context.getSyncHelper().handleSyncDeviceGroups(input, receiveConfig.ignoreAvatars())); + .retrieveAttachment(groupsMessage, + input -> context.getSyncHelper() + .handleSyncDeviceGroups(input, receiveConfig.ignoreAvatars())); } catch (Exception e) { logger.warn("Failed to handle received sync groups, ignoring: {}", e.getMessage()); } @@ -553,7 +552,8 @@ public final class IncomingMessageHandler { final var contactsMessage = syncMessage.getContacts().get(); context.getAttachmentHelper() .retrieveAttachment(contactsMessage.getContactsStream(), - input -> context.getSyncHelper().handleSyncDeviceContacts(input, receiveConfig.ignoreAvatars())); + input -> context.getSyncHelper() + .handleSyncDeviceContacts(input, receiveConfig.ignoreAvatars())); } catch (Exception e) { logger.warn("Failed to handle received sync contacts, ignoring: {}", e.getMessage()); } @@ -758,9 +758,9 @@ public final class IncomingMessageHandler { groupV1 = new GroupInfoV1(groupId); } - if (groupInfo.getAvatar().isPresent()) { + if (groupInfo.getAvatar().isPresent() && !receiveConfig.ignoreAvatars()) { var avatar = groupInfo.getAvatar().get(); - context.getGroupHelper().downloadGroupAvatar(groupV1.getGroupId(), avatar, receiveConfig.ignoreAvatars()); + context.getGroupHelper().downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { @@ -800,7 +800,7 @@ public final class IncomingMessageHandler { } } if (groupContext.getGroupV2().isPresent()) { - handleGroupV2Context(groupContext.getGroupV2().get()); + handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars()); } } @@ -882,7 +882,8 @@ public final class IncomingMessageHandler { account.getStickerStore().addStickerPack(sticker); } if (!receiveConfig.ignoreStickers()) { - context.getJobExecutor().enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); + context.getJobExecutor() + .enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); } } return actions; @@ -895,14 +896,14 @@ public final class IncomingMessageHandler { private List handleSignalServiceStoryMessage( SignalServiceStoryMessage message, RecipientId source, - boolean ignoreAttachments + ReceiveConfig receiveConfig ) { var actions = new ArrayList(); if (message.getGroupContext().isPresent()) { - handleGroupV2Context(message.getGroupContext().get()); + handleGroupV2Context(message.getGroupContext().get(), receiveConfig.ignoreAvatars()); } - if (!ignoreAttachments) { + if (!receiveConfig.ignoreAttachments()) { if (message.getFileAttachment().isPresent()) { context.getAttachmentHelper().downloadAttachment(message.getFileAttachment().get()); } @@ -924,13 +925,14 @@ public final class IncomingMessageHandler { return actions; } - private void handleGroupV2Context(final SignalServiceGroupV2 groupContext) { + private void handleGroupV2Context(final SignalServiceGroupV2 groupContext, final boolean ignoreAvatars) { final var groupMasterKey = groupContext.getMasterKey(); context.getGroupHelper() .getOrMigrateGroup(groupMasterKey, groupContext.getRevision(), - groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); + groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null, + ignoreAvatars); } private void handleIncomingProfileKey(final byte[] profileKeyBytes, final RecipientId source) { 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 537738ce..72e3926a 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 @@ -116,7 +116,8 @@ public final class ProfileHelper { .filter(recipientId -> !ExpiringProfileCredentialUtil.isValid(account.getProfileStore() .getExpiringProfileKeyCredential(recipientId))) .map(recipientId -> retrieveProfile(recipientId, - SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL).onErrorComplete()); + SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, + false).onErrorComplete()); Maybe.merge(profileFetches, 10).blockingSubscribe(); return recipientIds.stream().map(r -> account.getProfileStore().getExpiringProfileKeyCredential(r)).toList(); @@ -129,7 +130,9 @@ public final class ProfileHelper { } try { - blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL)); + blockingGetProfile(retrieveProfile(recipientId, + SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL, + false)); } catch (IOException e) { logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); return null; @@ -241,7 +244,8 @@ public final class ProfileHelper { final var profileFetches = Flowable.fromIterable(recipientIds) .filter(recipientId -> force || isProfileRefreshRequired(profileStore.getProfile(recipientId))) .map(recipientId -> retrieveProfile(recipientId, - SignalServiceProfile.RequestType.PROFILE).onErrorComplete()); + SignalServiceProfile.RequestType.PROFILE, + false).onErrorComplete()); Maybe.merge(profileFetches, 10).blockingSubscribe(); return recipientIds.stream().map(profileStore::getProfile).toList(); @@ -255,7 +259,7 @@ public final class ProfileHelper { } try { - blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE)); + blockingGetProfile(retrieveProfile(recipientId, SignalServiceProfile.RequestType.PROFILE, false)); } catch (IOException e) { logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); } @@ -272,29 +276,11 @@ public final class ProfileHelper { return now - profile.getLastUpdateTimestamp() >= 6 * 60 * 60 * 1000; } - private Profile decryptProfileAndDownloadAvatar( - final RecipientId recipientId, - final ProfileKey profileKey, - final SignalServiceProfile encryptedProfile, - final boolean ignoreAvatars - ) { - final var avatarPath = encryptedProfile.getAvatar(); - if (!ignoreAvatars) { - downloadProfileAvatar(recipientId, avatarPath, profileKey, ignoreAvatars); - } - - return ProfileUtils.decryptProfile(profileKey, encryptedProfile); - } - public void downloadProfileAvatar( final RecipientId recipientId, final String avatarPath, - final ProfileKey profileKey, - final boolean ignoreAvatars + final ProfileKey profileKey ) { - if (ignoreAvatars) { - return; - } var profile = account.getProfileStore().getProfile(recipientId); if (profile == null || !Objects.equals(avatarPath, profile.getAvatarUrlPath())) { logger.trace("Downloading profile avatar for {}", recipientId); @@ -322,7 +308,8 @@ public final class ProfileHelper { private Single retrieveProfile( RecipientId recipientId, - SignalServiceProfile.RequestType requestType + SignalServiceProfile.RequestType requestType, + final boolean ignoreAvatars ) { var unidentifiedAccess = getUnidentifiedAccess(recipientId); var profileKey = Optional.ofNullable(account.getProfileStore().getProfileKey(recipientId)); @@ -348,7 +335,12 @@ public final class ProfileHelper { Profile newProfile = null; if (profileKey.isPresent()) { logger.trace("Decrypting profile"); - newProfile = decryptProfileAndDownloadAvatar(recipientId, profileKey.get(), encryptedProfile, false); + final var avatarPath = encryptedProfile.getAvatar(); + if (!ignoreAvatars) { + downloadProfileAvatar(recipientId, avatarPath, profileKey.get()); + } + + newProfile = ProfileUtils.decryptProfile(profileKey.get(), encryptedProfile); } if (newProfile == null) { diff --git a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java index 0030ff41..f9dc20fb 100644 --- a/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java +++ b/lib/src/main/java/org/asamk/signal/manager/helper/SyncHelper.java @@ -326,8 +326,8 @@ public class SyncHelper { syncGroup.color = g.getColor().get(); } - if (g.getAvatar().isPresent()) { - context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get(), ignoreAvatars); + if (g.getAvatar().isPresent() && !ignoreAvatars) { + context.getGroupHelper().downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); } syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); @@ -381,7 +381,11 @@ public class SyncHelper { account.getContactStore().storeContact(recipientId, builder.build()); if (c.getAvatar().isPresent()) { - storeContactAvatar(c.getAvatar().get(), address, ignoreAvatars); + if (!ignoreAvatars) { + storeContactAvatar(c.getAvatar().get(), address); + } else { + IOUtils.discardStream(c.getAvatar().get().getInputStream()); + } } } } @@ -430,10 +434,7 @@ public class SyncHelper { streamDetails.getContentType())); } - private void storeContactAvatar(DeviceContactAvatar avatar, RecipientAddress address, boolean ignoreAvatars) { - if (ignoreAvatars) { - return; - } + private void storeContactAvatar(DeviceContactAvatar avatar, RecipientAddress address) { try { context.getAvatarStore() .storeContactAvatar(address, diff --git a/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java b/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java index 1971d362..f46bac0c 100644 --- a/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java +++ b/lib/src/main/java/org/asamk/signal/manager/jobs/DownloadProfileAvatarJob.java @@ -18,6 +18,6 @@ public class DownloadProfileAvatarJob implements Job { logger.trace("Downloading profile avatar {}", avatarPath); final var account = context.getAccount(); context.getProfileHelper() - .downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, account.getProfileKey(), false); + .downloadProfileAvatar(account.getSelfRecipientId(), avatarPath, account.getProfileKey()); } } diff --git a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java index eebe6451..65d3b1d8 100644 --- a/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java +++ b/lib/src/main/java/org/asamk/signal/manager/util/IOUtils.java @@ -62,6 +62,14 @@ public class IOUtils { } } + /** + * Read and discard all data from the input stream. + * This is useful to ensure that all data is read and any resources are released, without needing to store the data in memory. + */ + public static void discardStream(InputStream input) throws IOException { + copyStream(input, OutputStream.nullOutputStream()); + } + public static void copyStream(InputStream input, OutputStream output) throws IOException { copyStream(input, output, 4096); } diff --git a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java index 216e5d07..215d481d 100644 --- a/src/main/java/org/asamk/signal/commands/ReceiveCommand.java +++ b/src/main/java/org/asamk/signal/commands/ReceiveCommand.java @@ -15,7 +15,6 @@ import org.asamk.signal.commands.exceptions.UserErrorException; import org.asamk.signal.json.JsonReceiveMessageHandler; import org.asamk.signal.manager.Manager; import org.asamk.signal.manager.api.AlreadyReceivingException; -import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.output.JsonWriter; import org.asamk.signal.output.OutputWriter; import org.asamk.signal.output.PlainTextWriter; @@ -29,6 +28,8 @@ import java.util.List; import java.util.Optional; import java.util.SequencedCollection; +import static org.asamk.signal.util.CommandUtil.getReceiveConfig; + public class ReceiveCommand implements LocalCommand, JsonRpcSingleCommand { private static final Logger logger = LoggerFactory.getLogger(ReceiveCommand.class); @@ -80,12 +81,7 @@ public class ReceiveCommand implements LocalCommand, JsonRpcSingleCommand new JsonReceiveMessageHandler(m, writer);