From 712c9ea741dcacb44d658ab9d9d02b2d2514f714 Mon Sep 17 00:00:00 2001 From: ziggy Date: Fri, 10 Jul 2026 18:27:17 +0300 Subject: [PATCH] Preserve group filters for invalid content --- .../helper/IncomingMessageHandler.java | 183 ++++++++++++++---- .../helper/IncomingMessageHandlerTest.java | 44 +++++ 2 files changed, 185 insertions(+), 42 deletions(-) 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 a929ff9b..85368459 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 @@ -32,6 +32,7 @@ import org.asamk.signal.manager.groups.GroupUtils; import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientId; @@ -49,6 +50,7 @@ import org.signal.libsignal.protocol.InvalidMessageException; import org.signal.libsignal.protocol.groups.GroupSessionBuilder; import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.zkgroup.InvalidInputException; +import org.signal.libsignal.zkgroup.groups.GroupMasterKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -75,6 +77,8 @@ import org.whispersystems.signalservice.internal.push.BodyRange; import org.whispersystems.signalservice.internal.push.Content; import org.whispersystems.signalservice.internal.push.DataMessage; import org.whispersystems.signalservice.internal.push.Envelope; +import org.whispersystems.signalservice.internal.push.GroupContext; +import org.whispersystems.signalservice.internal.push.GroupContextV2; import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException; import java.io.IOException; @@ -141,7 +145,7 @@ public final class IncomingMessageHandler { account.getIdentityKeyStore().setRetryingDecryption(false); } } - actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, null)); + actions.addAll(checkAndHandleMessage(envelope, content, null, receiveConfig, handler, null)); return new Pair<>(actions, null); } @@ -155,6 +159,7 @@ public final class IncomingMessageHandler { actions.add(RefreshPreKeysAction.create()); } SignalServiceContent content = null; + Content decryptedContent = null; Exception exception = null; if (envelope.getSourceServiceId() != null) { // Store uuid if we don't have it already @@ -178,6 +183,7 @@ public final class IncomingMessageHandler { final var cipherResult = dependencies.getCipher(destination == null || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); + decryptedContent = cipherResult.getContent(); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); if (content == null) { return new Pair<>(List.of(), null); @@ -228,7 +234,12 @@ public final class IncomingMessageHandler { } } - actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, exception)); + actions.addAll(checkAndHandleMessage(envelope, + content, + exception instanceof InvalidEnvelopeContentException ? decryptedContent : null, + receiveConfig, + handler, + exception)); return new Pair<>(actions, exception); } @@ -371,6 +382,7 @@ public final class IncomingMessageHandler { private List checkAndHandleMessage( final SignalServiceEnvelope envelope, final SignalServiceContent content, + final Content invalidContent, final ReceiveConfig receiveConfig, final Manager.ReceiveMessageHandler handler, final Exception exception @@ -398,19 +410,21 @@ public final class IncomingMessageHandler { account.getMessageSendLogStore().deleteEntryForRecipient(envelope.getTimestamp(), sender, senderDeviceId); } - var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); + final var groupFilterInfo = getGroupFilterInfo(content, invalidContent, exception); + var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content, exception, groupFilterInfo); final var groupContext = getGroupContext(content); if (groupContext != null && groupContext.getGroupV2().isPresent()) { handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars()); } // Check again in case the user just joined the group - notAllowedToSendToGroup = notAllowedToSendToGroup && isNotAllowedToSendToGroup(envelope, content); + notAllowedToSendToGroup = notAllowedToSendToGroup + && isNotAllowedToSendToGroup(envelope, content, exception, groupFilterInfo); - if (isMessageBlocked(envelope, content, exception)) { + if (isMessageBlocked(envelope, content, exception, groupFilterInfo)) { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); return List.of(); } else if (notAllowedToSendToGroup) { - final var senderAddress = getSenderAddress(envelope, content); + final var senderAddress = getSenderAddress(envelope, content, exception); logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", senderAddress == null ? null : senderAddress.getIdentifier(), envelope.getTimestamp()); @@ -849,7 +863,7 @@ public final class IncomingMessageHandler { return new Pair<>(actions, longTexts); } - private SignalServiceGroupContext getGroupContext(SignalServiceContent content) { + private static SignalServiceGroupContext getGroupContext(SignalServiceContent content) { if (content == null) { return null; } @@ -878,52 +892,59 @@ public final class IncomingMessageHandler { private boolean isMessageBlocked( SignalServiceEnvelope envelope, SignalServiceContent content, - Exception exception + Exception exception, + GroupFilterInfo groupFilterInfo ) { SignalServiceAddress source = getSenderAddress(envelope, content, exception); - if (source == null) { - return false; - } - final var recipientId = account.getRecipientResolver().resolveRecipient(source); - if (context.getContactHelper().isContactBlocked(recipientId)) { - return true; + if (source != null) { + final var recipientId = account.getRecipientResolver().resolveRecipient(source); + if (context.getContactHelper().isContactBlocked(recipientId)) { + return true; + } } - final var groupContext = getGroupContext(content); - if (groupContext != null) { - var groupId = GroupUtils.getGroupId(groupContext); - return context.getGroupHelper().isGroupBlocked(groupId); + if (groupFilterInfo != null) { + return isGroupBlocked(context.getGroupHelper().getGroup(groupFilterInfo.groupId())); } return false; } - private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) { - SignalServiceAddress source = getSenderAddress(envelope, content); + private boolean isNotAllowedToSendToGroup( + SignalServiceEnvelope envelope, + SignalServiceContent content, + Exception exception, + GroupFilterInfo groupFilterInfo + ) { + SignalServiceAddress source = getSenderAddress(envelope, content, exception); if (source == null) { return false; } - final var groupContext = getGroupContext(content); - if (groupContext == null) { + if (groupFilterInfo == null) { return false; } - if (groupContext.getGroupV1().isPresent()) { - var groupInfo = groupContext.getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { - return false; - } + if (groupFilterInfo.isQuit()) { + return false; } - final var message = content.getDataMessage().orElse(null); - final var recipientId = account.getRecipientResolver().resolveRecipient(source); - final var groupId = GroupUtils.getGroupId(groupContext); - final var group = context.getGroupHelper().getGroup(groupId); + final var group = context.getGroupHelper().getGroup(groupFilterInfo.groupId()); + return isNotAllowedToSendToGroup(group, recipientId, groupFilterInfo); + } - if (message != null && message.getAdminDelete().isPresent() && (group == null || !group.isAdmin(recipientId))) { + static boolean isGroupBlocked(final GroupInfo group) { + return group != null && group.isBlocked(); + } + + static boolean isNotAllowedToSendToGroup( + final GroupInfo group, + final RecipientId recipientId, + final GroupFilterInfo groupFilterInfo + ) { + if (groupFilterInfo.hasAdminDelete() && (group == null || !group.isAdmin(recipientId))) { return true; } @@ -931,24 +952,102 @@ public final class IncomingMessageHandler { return false; } - if (!group.isMember(recipientId) && !( - group.isPendingMember(recipientId) && message != null && message.isGroupV2Update() - )) { + if (!group.isMember(recipientId) + && !(group.isPendingMember(recipientId) && groupFilterInfo.isGroupV2Update())) { return true; } if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { - return message == null - || message.getBody().isPresent() - || message.getAttachments().isPresent() - || message.getQuote().isPresent() - || message.getPreviews().isPresent() - || message.getMentions().isPresent() - || message.getSticker().isPresent(); + return groupFilterInfo.hasAnnouncementContent(); } return false; } + static GroupFilterInfo getGroupFilterInfo( + final SignalServiceContent content, + final Content invalidContent, + final Exception exception + ) { + if (content != null) { + final var groupContext = getGroupContext(content); + if (groupContext == null) { + return null; + } + final var message = content.getDataMessage().orElse(null); + return new GroupFilterInfo(GroupUtils.getGroupId(groupContext), + groupContext.getGroupV1() + .map(group -> group.getType() == SignalServiceGroup.Type.QUIT) + .orElse(false), + message != null && message.getAdminDelete().isPresent(), + message != null && message.isGroupV2Update(), + message == null + || message.getBody().isPresent() + || message.getAttachments().isPresent() + || message.getQuote().isPresent() + || message.getPreviews().isPresent() + || message.getMentions().isPresent() + || message.getSticker().isPresent()); + } + + if (invalidContent == null || !(exception instanceof InvalidEnvelopeContentException e)) { + return null; + } + + final var message = getDataMessage(invalidContent, e.getMessage()); + if (message != null) { + final var groupId = getGroupId(message); + if (groupId == null) { + return null; + } + return new GroupFilterInfo(groupId, + message.group != null + && message.group.type == GroupContext.Type.QUIT, + message.adminDelete != null, + message.groupV2 != null + && message.groupV2.groupChange != null + && message.groupV2.groupChange.size() > 0, + message.body != null + || !message.attachments.isEmpty() + || message.quote != null + || !message.preview.isEmpty() + || message.bodyRanges.stream() + .anyMatch(range -> range.mentionAci != null || range.mentionAciBinary != null) + || message.sticker != null); + } + + if (invalidContent.storyMessage != null && invalidContent.storyMessage.group != null) { + final var groupId = getGroupId(invalidContent.storyMessage.group); + return groupId == null ? null : new GroupFilterInfo(groupId, false, false, false, true); + } + return null; + } + + private static GroupId getGroupId(final DataMessage message) { + if (message.group != null && message.group.id != null) { + return GroupId.v1(message.group.id.toByteArray()); + } + return message.groupV2 == null ? null : getGroupId(message.groupV2); + } + + private static GroupId getGroupId(final GroupContextV2 groupContext) { + if (groupContext.masterKey == null) { + return null; + } + try { + return GroupUtils.getGroupIdV2(new GroupMasterKey(groupContext.masterKey.toByteArray())); + } catch (InvalidInputException e) { + return null; + } + } + + record GroupFilterInfo( + GroupId groupId, + boolean isQuit, + boolean hasAdminDelete, + boolean isGroupV2Update, + boolean hasAnnouncementContent + ) {} + private Pair, Map> handleSignalServiceDataMessage( SignalServiceDataMessage message, boolean isSync, diff --git a/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java b/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java index 569ad51a..9bd2fd92 100644 --- a/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java +++ b/lib/src/test/java/org/asamk/signal/manager/helper/IncomingMessageHandlerTest.java @@ -1,6 +1,10 @@ package org.asamk.signal.manager.helper; +import okio.ByteString; +import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.InvalidEnvelopeContentException; +import org.asamk.signal.manager.storage.groups.GroupInfoV1; +import org.asamk.signal.manager.storage.recipients.TestRecipientId; import org.junit.jupiter.api.Test; import org.signal.core.models.ServiceId.ACI; import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata; @@ -11,11 +15,14 @@ import org.whispersystems.signalservice.internal.push.Content; import org.whispersystems.signalservice.internal.push.DataMessage; import org.whispersystems.signalservice.internal.push.EditMessage; import org.whispersystems.signalservice.internal.push.Envelope; +import org.whispersystems.signalservice.internal.push.GroupContext; import org.whispersystems.signalservice.internal.push.SyncMessage; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; class IncomingMessageHandlerTest { @@ -127,4 +134,41 @@ class IncomingMessageHandlerTest { assertEquals(sender, source.getServiceId()); } + + @Test + void invalidGroupContextIsAvailableForFiltering() { + final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de"); + final var groupId = new byte[16]; + final var group = new GroupContext.Builder() + .id(ByteString.of(groupId)) + .type(GroupContext.Type.DELIVER) + .build(); + final var bodyRange = new BodyRange.Builder().start(4).length(3).style(BodyRange.Style.BOLD).build(); + final var dataMessage = new DataMessage.Builder() + .body("hello") + .bodyRanges(List.of(bodyRange)) + .group(group) + .build(); + final var content = new Content.Builder().dataMessage(dataMessage).build(); + final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1); + final var validationResult = new EnvelopeContentValidator.Result.Invalid( + "[DataMessage] Body range with out-of-bounds start/length!", + new Throwable()); + final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult, + metadata, + content); + + final var filterInfo = IncomingMessageHandler.getGroupFilterInfo(null, content, exception); + + assertNotNull(filterInfo); + assertEquals(GroupId.v1(groupId), filterInfo.groupId()); + assertTrue(filterInfo.hasAnnouncementContent()); + + final var storedGroup = new GroupInfoV1(GroupId.v1(groupId)); + storedGroup.setBlocked(true); + assertTrue(IncomingMessageHandler.isGroupBlocked(storedGroup)); + assertTrue(IncomingMessageHandler.isNotAllowedToSendToGroup(storedGroup, + TestRecipientId.createTestId(1), + filterInfo)); + } }