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.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<HandleAction> 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<List<HandleAction>, Map<String, String>> handleSignalServiceDataMessage(
SignalServiceDataMessage message,
boolean isSync,

View File

@ -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));
}
}