mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-24 05:16:45 +00:00
Expose invalid envelope content in receive output
This commit is contained in:
parent
98d9960e66
commit
1cce283aca
@ -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) {}
|
||||
}
|
||||
@ -4,6 +4,7 @@ import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.helper.RecipientAddressResolver;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientResolver;
|
||||
import org.asamk.signal.manager.util.MimeUtils;
|
||||
import org.signal.core.models.ServiceId;
|
||||
import org.signal.libsignal.metadata.ProtocolException;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer;
|
||||
@ -1032,6 +1033,8 @@ public record MessageEnvelope(
|
||||
? recipientResolver.resolveRecipient(serviceId)
|
||||
: envelope.isUnidentifiedSender() && content != null
|
||||
? recipientResolver.resolveRecipient(content.getSender())
|
||||
: exception instanceof InvalidEnvelopeContentException e && e.getSender() != null
|
||||
? recipientResolver.resolveRecipient(ServiceId.parseOrThrow(e.getSender()))
|
||||
: exception instanceof ProtocolException e
|
||||
? recipientResolver.resolveRecipient(e.getSender())
|
||||
: null;
|
||||
@ -1039,6 +1042,7 @@ public record MessageEnvelope(
|
||||
? envelope.getSourceDevice()
|
||||
: content != null
|
||||
? content.getSenderDevice()
|
||||
: exception instanceof InvalidEnvelopeContentException e ? e.getSenderDevice()
|
||||
: exception instanceof ProtocolException e ? e.getSenderDevice() : 0;
|
||||
|
||||
Optional<Receipt> receipt;
|
||||
|
||||
@ -21,6 +21,7 @@ import org.asamk.signal.manager.actions.SyncStorageDataAction;
|
||||
import org.asamk.signal.manager.actions.UpdateAccountAttributesAction;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
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.Pair;
|
||||
import org.asamk.signal.manager.api.ReceiveConfig;
|
||||
@ -52,6 +53,7 @@ import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.SignalServiceCipherResult;
|
||||
import org.whispersystems.signalservice.api.messages.EnvelopeContentValidator;
|
||||
@ -69,6 +71,8 @@ import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSy
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage;
|
||||
import org.whispersystems.signalservice.api.push.ServiceIdType;
|
||||
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.Envelope;
|
||||
import org.whispersystems.signalservice.internal.push.UnsupportedDataMessageException;
|
||||
|
||||
@ -225,7 +229,8 @@ public final class IncomingMessageHandler {
|
||||
Envelope envelope,
|
||||
SignalServiceCipherResult cipherResult,
|
||||
long serverDeliveredTimestamp
|
||||
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException, InvalidMessageStructureException {
|
||||
) throws ProtocolInvalidKeyException, ProtocolInvalidMessageException, UnsupportedDataMessageException,
|
||||
InvalidMessageStructureException, InvalidEnvelopeContentException {
|
||||
final var content = cipherResult.getContent();
|
||||
final var envelopeMetadata = cipherResult.getMetadata();
|
||||
final var validationResult = EnvelopeContentValidator.INSTANCE.validate(envelope,
|
||||
@ -234,8 +239,17 @@ public final class IncomingMessageHandler {
|
||||
cipherResult.getMetadata().getCiphertextMessageType());
|
||||
|
||||
if (validationResult instanceof EnvelopeContentValidator.Result.Invalid v) {
|
||||
logger.warn("Invalid content! {}", v.getReason(), v.getThrowable());
|
||||
return null;
|
||||
final var exception = createInvalidEnvelopeContentException(v, envelopeMetadata, content);
|
||||
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) {
|
||||
@ -252,6 +266,55 @@ public final class IncomingMessageHandler {
|
||||
serverDeliveredTimestamp);
|
||||
}
|
||||
|
||||
static InvalidEnvelopeContentException createInvalidEnvelopeContentException(
|
||||
final EnvelopeContentValidator.Result.Invalid validationResult,
|
||||
final EnvelopeMetadata envelopeMetadata,
|
||||
final Content content
|
||||
) {
|
||||
final var dataMessage = content.dataMessage;
|
||||
final Integer bodyLength = dataMessage == null
|
||||
? null
|
||||
: dataMessage.body == null ? 0 : dataMessage.body.length();
|
||||
final List<InvalidEnvelopeContentException.InvalidBodyRange> invalidBodyRanges;
|
||||
if (dataMessage == null) {
|
||||
invalidBodyRanges = List.of();
|
||||
} else {
|
||||
invalidBodyRanges = new ArrayList<>();
|
||||
for (int i = 0; i < dataMessage.bodyRanges.size(); i++) {
|
||||
final var range = dataMessage.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 = "[DataMessage] Body range with out-of-bounds start/length!".equals(validationResult.getReason())
|
||||
? InvalidEnvelopeContentException.DATA_MESSAGE_BODY_RANGE_OUT_OF_BOUNDS
|
||||
: InvalidEnvelopeContentException.INVALID_ENVELOPE_CONTENT;
|
||||
return new InvalidEnvelopeContentException(validationResult.getReason(),
|
||||
code,
|
||||
envelopeMetadata.getSourceServiceId().toString(),
|
||||
envelopeMetadata.getSourceDeviceId(),
|
||||
bodyLength,
|
||||
invalidBodyRanges,
|
||||
validationResult.getThrowable());
|
||||
}
|
||||
|
||||
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(
|
||||
final SignalServiceEnvelope envelope,
|
||||
final SignalServiceContent content,
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package org.asamk.signal.manager.helper;
|
||||
|
||||
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
|
||||
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.internal.push.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.Content;
|
||||
import org.whispersystems.signalservice.internal.push.DataMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -1,11 +1,29 @@
|
||||
package org.asamk.signal.json;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import io.micronaut.jsonschema.JsonSchema;
|
||||
|
||||
import org.asamk.signal.manager.api.InvalidEnvelopeContentException;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@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) {
|
||||
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
|
||||
) {}
|
||||
}
|
||||
|
||||
50
src/test/java/org/asamk/signal/json/JsonErrorTest.java
Normal file
50
src/test/java/org/asamk/signal/json/JsonErrorTest.java
Normal 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"));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user