Preserve group filters for invalid content

This commit is contained in:
ziggy 2026-07-10 18:27:17 +03:00
parent 8b74f0653d
commit 712c9ea741
2 changed files with 185 additions and 42 deletions

View File

@ -32,6 +32,7 @@ import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.internal.SignalDependencies; import org.asamk.signal.manager.internal.SignalDependencies;
import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; import org.asamk.signal.manager.jobs.RetrieveStickerPackJob;
import org.asamk.signal.manager.storage.SignalAccount; 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.groups.GroupInfoV1;
import org.asamk.signal.manager.storage.recipients.RecipientAddress; import org.asamk.signal.manager.storage.recipients.RecipientAddress;
import org.asamk.signal.manager.storage.recipients.RecipientId; 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.groups.GroupSessionBuilder;
import org.signal.libsignal.protocol.message.DecryptionErrorMessage; import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
import org.signal.libsignal.zkgroup.InvalidInputException; import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey; import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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.Content;
import org.whispersystems.signalservice.internal.push.DataMessage; import org.whispersystems.signalservice.internal.push.DataMessage;
import org.whispersystems.signalservice.internal.push.Envelope; 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 org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
import java.io.IOException; import java.io.IOException;
@ -141,7 +145,7 @@ public final class IncomingMessageHandler {
account.getIdentityKeyStore().setRetryingDecryption(false); 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); return new Pair<>(actions, null);
} }
@ -155,6 +159,7 @@ public final class IncomingMessageHandler {
actions.add(RefreshPreKeysAction.create()); actions.add(RefreshPreKeysAction.create());
} }
SignalServiceContent content = null; SignalServiceContent content = null;
Content decryptedContent = null;
Exception exception = null; Exception exception = null;
if (envelope.getSourceServiceId() != null) { if (envelope.getSourceServiceId() != null) {
// Store uuid if we don't have it already // Store uuid if we don't have it already
@ -178,6 +183,7 @@ public final class IncomingMessageHandler {
final var cipherResult = dependencies.getCipher(destination == null final var cipherResult = dependencies.getCipher(destination == null
|| destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI) || destination.equals(account.getAci()) ? ServiceIdType.ACI : ServiceIdType.PNI)
.decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp()); .decrypt(envelope.getProto(), envelope.getServerDeliveredTimestamp());
decryptedContent = cipherResult.getContent();
content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp()); content = validate(envelope.getProto(), cipherResult, envelope.getServerDeliveredTimestamp());
if (content == null) { if (content == null) {
return new Pair<>(List.of(), 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); return new Pair<>(actions, exception);
} }
@ -371,6 +382,7 @@ public final class IncomingMessageHandler {
private List<HandleAction> checkAndHandleMessage( private List<HandleAction> checkAndHandleMessage(
final SignalServiceEnvelope envelope, final SignalServiceEnvelope envelope,
final SignalServiceContent content, final SignalServiceContent content,
final Content invalidContent,
final ReceiveConfig receiveConfig, final ReceiveConfig receiveConfig,
final Manager.ReceiveMessageHandler handler, final Manager.ReceiveMessageHandler handler,
final Exception exception final Exception exception
@ -398,19 +410,21 @@ public final class IncomingMessageHandler {
account.getMessageSendLogStore().deleteEntryForRecipient(envelope.getTimestamp(), sender, senderDeviceId); 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); final var groupContext = getGroupContext(content);
if (groupContext != null && groupContext.getGroupV2().isPresent()) { if (groupContext != null && groupContext.getGroupV2().isPresent()) {
handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars()); handleGroupV2Context(groupContext.getGroupV2().get(), receiveConfig.ignoreAvatars());
} }
// Check again in case the user just joined the group // 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()); logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
return List.of(); return List.of();
} else if (notAllowedToSendToGroup) { } 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): {} {}", logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}",
senderAddress == null ? null : senderAddress.getIdentifier(), senderAddress == null ? null : senderAddress.getIdentifier(),
envelope.getTimestamp()); envelope.getTimestamp());
@ -849,7 +863,7 @@ public final class IncomingMessageHandler {
return new Pair<>(actions, longTexts); return new Pair<>(actions, longTexts);
} }
private SignalServiceGroupContext getGroupContext(SignalServiceContent content) { private static SignalServiceGroupContext getGroupContext(SignalServiceContent content) {
if (content == null) { if (content == null) {
return null; return null;
} }
@ -878,52 +892,59 @@ public final class IncomingMessageHandler {
private boolean isMessageBlocked( private boolean isMessageBlocked(
SignalServiceEnvelope envelope, SignalServiceEnvelope envelope,
SignalServiceContent content, SignalServiceContent content,
Exception exception Exception exception,
GroupFilterInfo groupFilterInfo
) { ) {
SignalServiceAddress source = getSenderAddress(envelope, content, exception); SignalServiceAddress source = getSenderAddress(envelope, content, exception);
if (source == null) { if (source != null) {
return false; final var recipientId = account.getRecipientResolver().resolveRecipient(source);
} if (context.getContactHelper().isContactBlocked(recipientId)) {
final var recipientId = account.getRecipientResolver().resolveRecipient(source); return true;
if (context.getContactHelper().isContactBlocked(recipientId)) { }
return true;
} }
final var groupContext = getGroupContext(content); if (groupFilterInfo != null) {
if (groupContext != null) { return isGroupBlocked(context.getGroupHelper().getGroup(groupFilterInfo.groupId()));
var groupId = GroupUtils.getGroupId(groupContext);
return context.getGroupHelper().isGroupBlocked(groupId);
} }
return false; return false;
} }
private boolean isNotAllowedToSendToGroup(SignalServiceEnvelope envelope, SignalServiceContent content) { private boolean isNotAllowedToSendToGroup(
SignalServiceAddress source = getSenderAddress(envelope, content); SignalServiceEnvelope envelope,
SignalServiceContent content,
Exception exception,
GroupFilterInfo groupFilterInfo
) {
SignalServiceAddress source = getSenderAddress(envelope, content, exception);
if (source == null) { if (source == null) {
return false; return false;
} }
final var groupContext = getGroupContext(content); if (groupFilterInfo == null) {
if (groupContext == null) {
return false; return false;
} }
if (groupContext.getGroupV1().isPresent()) { if (groupFilterInfo.isQuit()) {
var groupInfo = groupContext.getGroupV1().get(); return false;
if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) {
return false;
}
} }
final var message = content.getDataMessage().orElse(null);
final var recipientId = account.getRecipientResolver().resolveRecipient(source); final var recipientId = account.getRecipientResolver().resolveRecipient(source);
final var groupId = GroupUtils.getGroupId(groupContext); final var group = context.getGroupHelper().getGroup(groupFilterInfo.groupId());
final var group = context.getGroupHelper().getGroup(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; return true;
} }
@ -931,24 +952,102 @@ public final class IncomingMessageHandler {
return false; return false;
} }
if (!group.isMember(recipientId) && !( if (!group.isMember(recipientId)
group.isPendingMember(recipientId) && message != null && message.isGroupV2Update() && !(group.isPendingMember(recipientId) && groupFilterInfo.isGroupV2Update())) {
)) {
return true; return true;
} }
if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) {
return message == null return groupFilterInfo.hasAnnouncementContent();
|| message.getBody().isPresent()
|| message.getAttachments().isPresent()
|| message.getQuote().isPresent()
|| message.getPreviews().isPresent()
|| message.getMentions().isPresent()
|| message.getSticker().isPresent();
} }
return false; 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<List<HandleAction>, Map<String, String>> handleSignalServiceDataMessage( private Pair<List<HandleAction>, Map<String, String>> handleSignalServiceDataMessage(
SignalServiceDataMessage message, SignalServiceDataMessage message,
boolean isSync, boolean isSync,

View File

@ -1,6 +1,10 @@
package org.asamk.signal.manager.helper; 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.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.junit.jupiter.api.Test;
import org.signal.core.models.ServiceId.ACI; import org.signal.core.models.ServiceId.ACI;
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata; 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.DataMessage;
import org.whispersystems.signalservice.internal.push.EditMessage; import org.whispersystems.signalservice.internal.push.EditMessage;
import org.whispersystems.signalservice.internal.push.Envelope; import org.whispersystems.signalservice.internal.push.Envelope;
import org.whispersystems.signalservice.internal.push.GroupContext;
import org.whispersystems.signalservice.internal.push.SyncMessage; import org.whispersystems.signalservice.internal.push.SyncMessage;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; 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 { class IncomingMessageHandlerTest {
@ -127,4 +134,41 @@ class IncomingMessageHandlerTest {
assertEquals(sender, source.getServiceId()); 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));
}
} }