Merge a041826cd595e1c9f21ebbb4205076f49a6afcfb into 78dba20a597bbeda06b7a2b768e5d7401929df32

This commit is contained in:
Kostia R 2026-07-12 08:42:04 +02:00 committed by GitHub
commit 4e56fe839b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 612 additions and 50 deletions

View File

@ -0,0 +1,54 @@
package org.asamk.signal.manager.api;
import java.util.List;
public final class InvalidEnvelopeContentException extends Exception {
public static final String INVALID_ENVELOPE_CONTENT = "INVALID_ENVELOPE_CONTENT";
public static final String DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS = "DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS";
private final String code;
private final String sender;
private final int senderDevice;
private final Integer bodyLength;
private final List<InvalidBodyRange> invalidBodyRanges;
public InvalidEnvelopeContentException(
final String message,
final String code,
final String sender,
final int senderDevice,
final Integer bodyLength,
final List<InvalidBodyRange> invalidBodyRanges,
final Throwable cause
) {
super(message, cause);
this.code = code;
this.sender = sender;
this.senderDevice = senderDevice;
this.bodyLength = bodyLength;
this.invalidBodyRanges = List.copyOf(invalidBodyRanges);
}
public String getCode() {
return code;
}
public String getSender() {
return sender;
}
public int getSenderDevice() {
return senderDevice;
}
public Integer getBodyLength() {
return bodyLength;
}
public List<InvalidBodyRange> getInvalidBodyRanges() {
return invalidBodyRanges;
}
public record InvalidBodyRange(int index, Integer start, Integer length, String type) {}
}

View File

@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.helper.RecipientAddressResolver; import org.asamk.signal.manager.helper.RecipientAddressResolver;
import org.asamk.signal.manager.storage.recipients.RecipientResolver; import org.asamk.signal.manager.storage.recipients.RecipientResolver;
import org.asamk.signal.manager.util.MimeUtils; import org.asamk.signal.manager.util.MimeUtils;
import org.signal.core.models.ServiceId;
import org.signal.libsignal.metadata.ProtocolException; import org.signal.libsignal.metadata.ProtocolException;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
@ -1032,6 +1033,8 @@ public record MessageEnvelope(
? recipientResolver.resolveRecipient(serviceId) ? recipientResolver.resolveRecipient(serviceId)
: envelope.isUnidentifiedSender() && content != null : envelope.isUnidentifiedSender() && content != null
? recipientResolver.resolveRecipient(content.getSender()) ? recipientResolver.resolveRecipient(content.getSender())
: exception instanceof InvalidEnvelopeContentException e && e.getSender() != null
? recipientResolver.resolveRecipient(ServiceId.parseOrThrow(e.getSender()))
: exception instanceof ProtocolException e : exception instanceof ProtocolException e
? recipientResolver.resolveRecipient(e.getSender()) ? recipientResolver.resolveRecipient(e.getSender())
: null; : null;
@ -1039,6 +1042,7 @@ public record MessageEnvelope(
? envelope.getSourceDevice() ? envelope.getSourceDevice()
: content != null : content != null
? content.getSenderDevice() ? content.getSenderDevice()
: exception instanceof InvalidEnvelopeContentException e ? e.getSenderDevice()
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0; : exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
Optional<Receipt> receipt; Optional<Receipt> receipt;

View File

@ -21,6 +21,7 @@ import org.asamk.signal.manager.actions.SyncStorageDataAction;
import org.asamk.signal.manager.actions.UpdateAccountAttributesAction; import org.asamk.signal.manager.actions.UpdateAccountAttributesAction;
import org.asamk.signal.manager.api.GroupId; import org.asamk.signal.manager.api.GroupId;
import org.asamk.signal.manager.api.GroupNotFoundException; import org.asamk.signal.manager.api.GroupNotFoundException;
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
import org.asamk.signal.manager.api.MessageEnvelope; import org.asamk.signal.manager.api.MessageEnvelope;
import org.asamk.signal.manager.api.Pair; import org.asamk.signal.manager.api.Pair;
import org.asamk.signal.manager.api.ReceiveConfig; import org.asamk.signal.manager.api.ReceiveConfig;
@ -31,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;
@ -48,10 +50,12 @@ 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;
import org.whispersystems.signalservice.api.InvalidMessageStructureException; import org.whispersystems.signalservice.api.InvalidMessageStructureException;
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata;
import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder; import org.whispersystems.signalservice.api.crypto.SignalGroupSessionBuilder;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipherResult; import org.whispersystems.signalservice.api.crypto.SignalServiceCipherResult;
import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator; import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator;
@ -69,7 +73,12 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
import org.whispersystems.signalservice.api.push.ServiceIdType; import org.whispersystems.signalservice.api.push.ServiceIdType;
import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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.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;
@ -85,6 +94,12 @@ import java.util.stream.Collectors;
public final class IncomingMessageHandler { public final class IncomingMessageHandler {
private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class); private static final Logger logger = LoggerFactory.getLogger(IncomingMessageHandler.class);
private static final String DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON =
"[DataMessage] Body range with out-of-bounds start/length!";
private static final String DATA_MESSAGE_QUOTE_BODY_RANGE_OUT_OF_BOUNDS_REASON =
"[DataMessage] Quote body range with out-of-bounds start/length!";
private static final String EDIT_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON =
"[EditMessage] Body range with out-of-bounds start/length!";
private final SignalAccount account; private final SignalAccount account;
private final SignalDependencies dependencies; private final SignalDependencies dependencies;
@ -107,6 +122,8 @@ public final class IncomingMessageHandler {
} }
SignalServiceContent content = null; SignalServiceContent content = null;
Content decryptedContent = null;
InvalidEnvelopeContentException validationException = null;
if (!envelope.isReceipt()) { if (!envelope.isReceipt()) {
account.getIdentityKeyStore().setRetryingDecryption(true); account.getIdentityKeyStore().setRetryingDecryption(true);
try { try {
@ -114,6 +131,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);
@ -124,14 +142,21 @@ public final class IncomingMessageHandler {
.resolveRecipientAddress(recipientId) .resolveRecipientAddress(recipientId)
.toApiRecipientAddress(), e.getSenderDevice()); .toApiRecipientAddress(), e.getSenderDevice());
return new Pair<>(List.of(), exception); return new Pair<>(List.of(), exception);
} catch (InvalidEnvelopeContentException e) {
validationException = e;
} catch (Exception e) { } catch (Exception e) {
return new Pair<>(List.of(), e); return new Pair<>(List.of(), e);
} finally { } finally {
account.getIdentityKeyStore().setRetryingDecryption(false); account.getIdentityKeyStore().setRetryingDecryption(false);
} }
} }
actions.addAll(checkAndHandleMessage(envelope, content, receiveConfig, handler, null)); actions.addAll(checkAndHandleMessage(envelope,
return new Pair<>(actions, null); content,
validationException == null ? null : decryptedContent,
receiveConfig,
handler,
validationException));
return new Pair<>(actions, validationException);
} }
public Pair<List<HandleAction>, Exception> handleEnvelope( public Pair<List<HandleAction>, Exception> handleEnvelope(
@ -144,6 +169,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
@ -167,6 +193,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);
@ -217,7 +244,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);
} }
@ -225,7 +257,8 @@ public final class IncomingMessageHandler {
Envelope envelope, Envelope envelope,
SignalServiceCipherResult cipherResult, SignalServiceCipherResult cipherResult,
long serverDeliveredTimestamp long serverDeliveredTimestamp
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException { ) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException,
InvalidMessageStructureException, InvalidEnvelopeContentException {
final var content = cipherResult.getContent(); final var content = cipherResult.getContent();
final var envelopeMetadata = cipherResult.getMetadata(); final var envelopeMetadata = cipherResult.getMetadata();
final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope, final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope,
@ -234,8 +267,17 @@ public final class IncomingMessageHandler {
cipherResult.getMetadata().getCiphertextMessageType()); cipherResult.getMetadata().getCiphertextMessageType());
if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) { if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
logger.warn("Invalid content! {}", v.getReason(), v.getThrowable()); final var exception = createInvalidEnvelopeContentException(v, envelopeMetadata, content);
return null; logger.warn("Invalid content! reason={} code={} source={} sourceDevice={} timestamp={} bodyLength={} invalidBodyRanges={}",
exception.getMessage(),
exception.getCode(),
exception.getSender(),
exception.getSenderDevice(),
envelope.clientTimestamp,
exception.getBodyLength(),
exception.getInvalidBodyRanges());
logger.debug("Invalid content validation location", v.getThrowable());
throw exception;
} }
if (validationResult instanceof EnvelopeContentValidator.Result.UnsupportedDataMessage v) { if (validationResult instanceof EnvelopeContentValidator.Result.UnsupportedDataMessage v) {
@ -252,9 +294,105 @@ public final class IncomingMessageHandler {
serverDeliveredTimestamp); serverDeliveredTimestamp);
} }
static InvalidEnvelopeContentException createInvalidEnvelopeContentException(
final EnvelopeContentValidator.Result.Invalid validationResult,
final EnvelopeMetadata envelopeMetadata,
final Content content
) {
final var reason = validationResult.getReason();
final var dataMessage = getDataMessage(content, reason);
final String body;
final List<BodyRange> bodyRanges;
if (DATA_MESSAGE_QUOTE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason)) {
if (dataMessage == null || dataMessage.quote == null) {
body = null;
bodyRanges = null;
} else {
body = dataMessage.quote.text;
bodyRanges = dataMessage.quote.bodyRanges;
}
} else if (dataMessage == null) {
body = null;
bodyRanges = null;
} else {
body = dataMessage.body;
bodyRanges = dataMessage.bodyRanges;
}
final Integer bodyLength = bodyRanges == null ? null : body == null ? 0 : body.length();
final List<InvalidEnvelopeContentException.InvalidBodyRange> invalidBodyRanges = new ArrayList<>();
if (bodyRanges != null) {
for (int i = 0; i < bodyRanges.size(); i++) {
final var range = bodyRanges.get(i);
final long start = range.start == null ? 0 : range.start;
final long length = range.length == null ? 0 : range.length;
if (start < 0 || length < 0 || start + length > bodyLength) {
invalidBodyRanges.add(new InvalidEnvelopeContentException.InvalidBodyRange(i,
range.start,
range.length,
getBodyRangeType(range)));
}
}
}
final var code = isBodyRangeOutOfBoundsReason(reason)
? InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS
: InvalidEnvelopeContentException.INVALID_ENVELOPE_CONTENT;
return new InvalidEnvelopeContentException(reason,
code,
envelopeMetadata.getSourceServiceId().toString(),
envelopeMetadata.getSourceDeviceId(),
bodyLength,
invalidBodyRanges,
validationResult.getThrowable());
}
private static DataMessage getDataMessage(final Content content, final String validationReason) {
if (validationReason.startsWith("[EditMessage]")) {
return getEditDataMessage(content);
}
if (content.dataMessage != null) {
return content.dataMessage;
}
if (content.syncMessage != null && content.syncMessage.sent != null
&& content.syncMessage.sent.message != null) {
return content.syncMessage.sent.message;
}
return getEditDataMessage(content);
}
private static DataMessage getEditDataMessage(final Content content) {
if (content.editMessage != null) {
return content.editMessage.dataMessage;
}
if (content.syncMessage != null && content.syncMessage.sent != null
&& content.syncMessage.sent.editMessage != null) {
return content.syncMessage.sent.editMessage.dataMessage;
}
return null;
}
private static boolean isBodyRangeOutOfBoundsReason(final String reason) {
return DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason)
|| DATA_MESSAGE_QUOTE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason)
|| EDIT_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS_REASON.equals(reason);
}
private static String getBodyRangeType(final BodyRange range) {
if (range.style != null) {
return "STYLE_" + range.style.name();
}
if (range.mentionAci != null || range.mentionAciBinary != null) {
return "MENTION";
}
return "UNKNOWN";
}
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
@ -282,19 +420,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)) { 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());
@ -733,7 +873,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;
} }
@ -759,51 +899,62 @@ public final class IncomingMessageHandler {
return null; return null;
} }
private boolean isMessageBlocked(SignalServiceEnvelope envelope, SignalServiceContent content) { private boolean isMessageBlocked(
SignalServiceAddress source = getSenderAddress(envelope, content); SignalServiceEnvelope envelope,
if (source == null) { SignalServiceContent content,
return false; Exception exception,
} GroupFilterInfo groupFilterInfo
final var recipientId = account.getRecipientResolver().resolveRecipient(source); ) {
if (context.getContactHelper().isContactBlocked(recipientId)) { SignalServiceAddress source = getSenderAddress(envelope, content, exception);
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 (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;
} }
@ -811,24 +962,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,
@ -1067,7 +1296,10 @@ public final class IncomingMessageHandler {
this.account.getProfileStore().storeProfileKey(source, profileKey); this.account.getProfileStore().storeProfileKey(source, profileKey);
} }
private SignalServiceAddress getSenderAddress(SignalServiceEnvelope envelope, SignalServiceContent content) { private static SignalServiceAddress getSenderAddress(
final SignalServiceEnvelope envelope,
final SignalServiceContent content
) {
final var serviceId = envelope.getSourceServiceId(); final var serviceId = envelope.getSourceServiceId();
if (!envelope.isUnidentifiedSender() && serviceId != null) { if (!envelope.isUnidentifiedSender() && serviceId != null) {
return new SignalServiceAddress(serviceId); return new SignalServiceAddress(serviceId);
@ -1078,6 +1310,24 @@ public final class IncomingMessageHandler {
} }
} }
static SignalServiceAddress getSenderAddress(
final SignalServiceEnvelope envelope,
final SignalServiceContent content,
final Exception exception
) {
final var source = getSenderAddress(envelope, content);
if (source != null) {
return source;
}
if (exception instanceof InvalidEnvelopeContentException e && e.getSender() != null) {
final var sender = ServiceId.parseOrNull(e.getSender());
if (sender != null) {
return new SignalServiceAddress(sender);
}
}
return null;
}
private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) { private DeviceAddress getSender(SignalServiceEnvelope envelope, SignalServiceContent content) {
final var serviceId = envelope.getSourceServiceId(); final var serviceId = envelope.getSourceServiceId();
if (!envelope.isUnidentifiedSender() && serviceId != null) { if (!envelope.isUnidentifiedSender() && serviceId != null) {

View File

@ -0,0 +1,174 @@
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;
import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator;
import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope;
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.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 {
@Test
void invalidEnvelopeContentReportsOutOfBoundsBodyRange() {
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
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)).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);
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
assertEquals(sender.toString(), exception.getSender());
assertEquals(2, exception.getSenderDevice());
assertEquals(5, exception.getBodyLength());
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 4, 3, "STYLE_BOLD")),
exception.getInvalidBodyRanges());
}
@Test
void invalidEnvelopeContentReportsOutOfBoundsBodyRangeFromSyncMessage() {
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
final var bodyRange = new BodyRange.Builder().start(-1).length(2).mentionAci(sender.toString()).build();
final var dataMessage = new DataMessage.Builder().body("hello").bodyRanges(List.of(bodyRange)).build();
final var sent = new SyncMessage.Sent.Builder().message(dataMessage).build();
final var content = new Content.Builder().syncMessage(new SyncMessage.Builder().sent(sent).build()).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);
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
assertEquals(5, exception.getBodyLength());
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, -1, 2, "MENTION")),
exception.getInvalidBodyRanges());
}
@Test
void invalidEnvelopeContentReportsOutOfBoundsBodyRangeFromEditMessage() {
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
final var bodyRange = new BodyRange.Builder().start(5).length(1).style(BodyRange.Style.ITALIC).build();
final var dataMessage = new DataMessage.Builder().body("hello").bodyRanges(List.of(bodyRange)).build();
final var editMessage = new EditMessage.Builder().targetSentTimestamp(1L).dataMessage(dataMessage).build();
final var content = new Content.Builder().editMessage(editMessage).build();
final var metadata = new EnvelopeMetadata(sender, null, 2, false, null, sender, 1);
final var validationResult = new EnvelopeContentValidator.Result.Invalid(
"[EditMessage] Body range with out-of-bounds start/length!",
new Throwable());
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
metadata,
content);
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
assertEquals(5, exception.getBodyLength());
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 5, 1, "STYLE_ITALIC")),
exception.getInvalidBodyRanges());
}
@Test
void invalidEnvelopeContentReportsOutOfBoundsBodyRangeFromQuote() {
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
final var bodyRange = new BodyRange.Builder().start(2).length(2).style(BodyRange.Style.MONOSPACE).build();
final var quote = new DataMessage.Quote.Builder().text("hey").bodyRanges(List.of(bodyRange)).build();
final var dataMessage = new DataMessage.Builder().body("outer body").quote(quote).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] Quote body range with out-of-bounds start/length!",
new Throwable());
final var exception = IncomingMessageHandler.createInvalidEnvelopeContentException(validationResult,
metadata,
content);
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS, exception.getCode());
assertEquals(3, exception.getBodyLength());
assertEquals(List.of(new InvalidEnvelopeContentException.InvalidBodyRange(0, 2, 2, "STYLE_MONOSPACE")),
exception.getInvalidBodyRanges());
}
@Test
void invalidSealedSenderCanBeResolvedForBlocking() {
final var sender = ACI.parseOrThrow("2a04f0cc-199f-4b93-99d8-13c6b10a70de");
final var envelope = new SignalServiceEnvelope(new Envelope.Builder()
.type(Envelope.Type.UNIDENTIFIED_SENDER)
.clientTimestamp(1L)
.build(), 2L);
final var exception = new InvalidEnvelopeContentException("invalid body range",
InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS,
sender.toString(),
2,
5,
List.of(),
new Throwable());
final var source = IncomingMessageHandler.getSenderAddress(envelope, null, exception);
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));
}
}

View File

@ -1,11 +1,29 @@
package org.asamk.signal.json; package org.asamk.signal.json;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.micronaut.jsonschema.JsonSchema; import io.micronaut.jsonschema.JsonSchema;
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
import java.util.List;
@JsonSchema(title = "Error") @JsonSchema(title = "Error")
public record JsonError(String message, String type) { public record JsonError(
String message,
String type,
@JsonInclude(JsonInclude.Include.NON_NULL) Details details
) {
public static JsonError from(Throwable exception) { public static JsonError from(Throwable exception) {
return new JsonError(exception.getMessage(), exception.getClass().getSimpleName()); final var details = exception instanceof InvalidEnvelopeContentException e
? new Details(e.getCode(), e.getBodyLength(), e.getInvalidBodyRanges())
: null;
return new JsonError(exception.getMessage(), exception.getClass().getSimpleName(), details);
} }
public record Details(
String code,
Integer bodyLength,
List<InvalidEnvelopeContentException.InvalidBodyRange> invalidBodyRanges
) {}
} }

View File

@ -2949,6 +2949,12 @@
"allDeclaredMethods": true, "allDeclaredMethods": true,
"allDeclaredConstructors": true "allDeclaredConstructors": true
}, },
{
"type": "org.asamk.signal.json.JsonError$Details",
"allDeclaredFields": true,
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
{ {
"type": "org.asamk.signal.json.JsonGroupInfo", "type": "org.asamk.signal.json.JsonGroupInfo",
"allDeclaredFields": true, "allDeclaredFields": true,
@ -3419,6 +3425,12 @@
{ {
"type": "org.asamk.signal.logging.LogConfigurator" "type": "org.asamk.signal.logging.LogConfigurator"
}, },
{
"type": "org.asamk.signal.manager.api.InvalidEnvelopeContentException$InvalidBodyRange",
"allDeclaredFields": true,
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
{ {
"type": "org.asamk.signal.manager.api.PhoneNumberSharingMode", "type": "org.asamk.signal.manager.api.PhoneNumberSharingMode",
"allDeclaredFields": true "allDeclaredFields": true

View File

@ -0,0 +1,50 @@
package org.asamk.signal.json;
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
import org.asamk.signal.util.Util;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class JsonErrorTest {
@Test
void invalidEnvelopeContentIncludesStructuredDetails() throws Exception {
final var invalidRange = new InvalidEnvelopeContentException.InvalidBodyRange(2, 8, 4, "STYLE_BOLD");
final var exception = new InvalidEnvelopeContentException("invalid body range",
InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS,
null,
3,
10,
List.of(invalidRange),
new Throwable());
final var error = JsonError.from(exception);
assertEquals("invalid body range", error.message());
assertEquals("InvalidEnvelopeContentException", error.type());
assertEquals(InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS,
error.details().code());
assertEquals(10, error.details().bodyLength());
assertEquals(List.of(invalidRange), error.details().invalidBodyRanges());
final var json = Util.createJsonObjectMapper().writeValueAsString(error);
assertTrue(json.contains("\"code\":\"DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS\""));
assertTrue(json.contains("\"start\":8"));
}
@Test
void ordinaryExceptionDoesNotIncludeDetails() throws Exception {
final var error = JsonError.from(new IllegalArgumentException("bad argument"));
assertEquals("bad argument", error.message());
assertEquals("IllegalArgumentException", error.type());
assertNull(error.details());
final var json = Util.createJsonObjectMapper().writeValueAsString(error);
assertFalse(json.contains("details"));
}
}