From ec9e5e5dee914a5812c805973e1b5e409999ce72 Mon Sep 17 00:00:00 2001 From: Ehren Kret Date: Fri, 14 Aug 2026 11:25:34 -0500 Subject: [PATCH] Limit the size of incoming text to 2 kibibytes. --- .../securesms/jobs/IndividualSendJob.kt | 6 +- .../securesms/jobs/IndividualSendJobV2.kt | 6 +- .../securesms/jobs/PushGroupSendJob.java | 6 +- .../securesms/sharing/MultiShareSender.java | 3 +- .../securesms/util/MessageUtil.kt | 7 +- .../securesms/jobs/IndividualSendJobV2Test.kt | 4 +- .../core/util/OutputStreamExtensions.kt | 32 ++++++++ .../archive/stream/PaddedGzipOutputStream.kt | 4 +- .../api/SignalServiceMessageSender.java | 5 +- .../api/messages/EnvelopeContentValidator.kt | 13 ++++ .../messages/SignalServiceMessageLimits.kt | 13 ++++ .../internal/crypto/PaddingInputStream.java | 4 +- .../messages/EnvelopeContentValidatorTest.kt | 75 +++++++++++++++++++ .../internal/crypto/PaddingInputStreamTest.kt | 31 ++++++++ 14 files changed, 186 insertions(+), 23 deletions(-) create mode 100644 lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMessageLimits.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.kt index 15b5c8b14f..f427a8be09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJob.kt @@ -28,7 +28,6 @@ import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.transport.RetryLaterException import org.thoughtcrime.securesms.transport.UndeliverableMessageException -import org.thoughtcrime.securesms.util.MessageUtil import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.SignalLocalMetrics import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents @@ -38,6 +37,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage.PaymentActivation import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits import org.whispersystems.signalservice.api.messages.SignalServicePreview import org.whispersystems.signalservice.api.messages.shared.SharedContact import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException @@ -258,8 +258,8 @@ class IndividualSendJob private constructor(parameters: Parameters, private val @Throws(IOException::class, UnregisteredUserException::class, UntrustedIdentityException::class, UndeliverableMessageException::class) private fun deliver(message: OutgoingMessage, originalEditedMessage: MessageRecord?): Boolean { - if (message.body.utf8Size() > MessageUtil.MAX_INLINE_BODY_SIZE_BYTES) { - throw UndeliverableMessageException("The total body size was greater than our limit of " + MessageUtil.MAX_INLINE_BODY_SIZE_BYTES + " bytes.") + if (message.body.utf8Size() > SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES) { + throw UndeliverableMessageException("The total body size was greater than our limit of " + SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES + " bytes.") } try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt index 9c2daf68e6..7a81fe9618 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2.kt @@ -41,7 +41,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.ratelimit.ProofRequiredExceptionHandler import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientUtil -import org.thoughtcrime.securesms.util.MessageUtil import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.SignalLocalMetrics import org.thoughtcrime.securesms.util.isUrgent @@ -49,6 +48,7 @@ import org.thoughtcrime.securesms.util.toDataMessage import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.EnvelopeContent import org.whispersystems.signalservice.api.messages.SendMessageResult +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits import org.whispersystems.signalservice.api.push.SignalServiceAddress import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException import org.whispersystems.signalservice.internal.push.Content @@ -179,8 +179,8 @@ class IndividualSendJobV2 private constructor(parameters: Parameters, private va null } - if (message.body.utf8Size() > MessageUtil.MAX_INLINE_BODY_SIZE_BYTES) { - Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Body size exceeds limit of ${MessageUtil.MAX_INLINE_BODY_SIZE_BYTES} bytes; failing.") + if (message.body.utf8Size() > SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES) { + Log.w(TAG, "${logPrefix(message.sentTimeMillis)} Body size exceeds limit of ${SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES} bytes; failing.") return Result.failure() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java index da5d4d2120..d54aa86881 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushGroupSendJob.java @@ -46,7 +46,6 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.transport.RetryLaterException; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.GroupUtil; -import org.thoughtcrime.securesms.util.MessageUtil; import org.thoughtcrime.securesms.util.RecipientAccessList; import org.thoughtcrime.securesms.util.SignalLocalMetrics; import org.whispersystems.signalservice.api.crypto.ContentHint; @@ -56,6 +55,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits; import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; import org.whispersystems.signalservice.api.messages.shared.SharedContact; @@ -276,8 +276,8 @@ public final class PushGroupSendJob extends PushSendJob { private List deliver(OutgoingMessage message, @Nullable MessageRecord originalEditedMessage, @NonNull Recipient groupRecipient, @NonNull List destinations) throws IOException, UntrustedIdentityException, UndeliverableMessageException, NoSessionException { - if (Utf8.size(message.getBody()) > MessageUtil.MAX_INLINE_BODY_SIZE_BYTES) { - throw new UndeliverableMessageException("The total body size was greater than our limit of " + MessageUtil.MAX_INLINE_BODY_SIZE_BYTES + " bytes."); + if (Utf8.size(message.getBody()) > SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES) { + throw new UndeliverableMessageException("The total body size was greater than our limit of " + SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES + " bytes."); } try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java index 33ddab6647..ffe2d926fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sharing/MultiShareSender.java @@ -45,6 +45,7 @@ import org.signal.core.util.Base64; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MessageUtil; import org.signal.core.util.Util; +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits; import java.util.ArrayList; import java.util.Collection; @@ -111,7 +112,7 @@ public final class MultiShareSender { List contacts = multiShareArgs.getSharedContacts(); SlideDeck slideDeck = new SlideDeck(primarySlideDeck); - boolean needsSplit = message != null && Utf8.size(message) > MessageUtil.MAX_INLINE_BODY_SIZE_BYTES; + boolean needsSplit = message != null && Utf8.size(message) > SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES; boolean hasMmsMedia = !multiShareArgs.getMedia().isEmpty() || (multiShareArgs.getDataUri() != null && multiShareArgs.getDataUri() != Uri.EMPTY) || multiShareArgs.getStickerLocator() != null || diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MessageUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MessageUtil.kt index ca4bc99873..c663ef0943 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MessageUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MessageUtil.kt @@ -5,16 +5,13 @@ import org.signal.core.util.kibiBytes import org.signal.core.util.splitByByteLength import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.mms.TextSlide +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits import java.text.SimpleDateFormat import java.util.Date import java.util.Locale import java.util.Optional object MessageUtil { - /** The maximum size of an inlined text body we'll allow in a proto. Anything larger than this will need to be a long-text attachment. */ - @JvmField - val MAX_INLINE_BODY_SIZE_BYTES: Int = 2.kibiBytes.bytes.toInt() - /** The maximum total message size we'll allow ourselves to send, even as a long text attachment. */ @JvmField val MAX_TOTAL_BODY_SIZE_BYTES = 64.kibiBytes.bytes.toInt() @@ -25,7 +22,7 @@ object MessageUtil { */ @JvmStatic fun getSplitMessage(context: Context, rawText: String): SplitResult { - val (trimmed, remainder) = rawText.splitByByteLength(MAX_INLINE_BODY_SIZE_BYTES) + val (trimmed, remainder) = rawText.splitByByteLength(SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES) return if (remainder != null) { val textData = rawText.toByteArray() diff --git a/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt b/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt index 3126e0eda5..da1a2f1440 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/jobs/IndividualSendJobV2Test.kt @@ -64,11 +64,11 @@ import org.thoughtcrime.securesms.service.ExpiringMessageManager import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule import org.thoughtcrime.securesms.testutil.MockSignalStoreRule import org.thoughtcrime.securesms.util.DataMessageError -import org.thoughtcrime.securesms.util.MessageUtil import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.toDataMessage import org.whispersystems.signalservice.api.crypto.ContentHint import org.whispersystems.signalservice.api.crypto.EnvelopeContent +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits import org.whispersystems.signalservice.internal.push.Content import org.whispersystems.signalservice.internal.push.DataMessage import org.whispersystems.signalservice.internal.push.PniSignatureMessage @@ -293,7 +293,7 @@ class IndividualSendJobV2Test { @Test fun `Given body exceeds inline size limit, when run, then return failure`() { - val tooLargeBody = "x".repeat(MessageUtil.MAX_INLINE_BODY_SIZE_BYTES + 1) + val tooLargeBody = "x".repeat(SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES + 1) every { outgoingMessage.body } returns tooLargeBody val result = createAndRunJob() diff --git a/core/util-jvm/src/main/java/org/signal/core/util/OutputStreamExtensions.kt b/core/util-jvm/src/main/java/org/signal/core/util/OutputStreamExtensions.kt index 8cc75e9008..cbdc9d20dc 100644 --- a/core/util-jvm/src/main/java/org/signal/core/util/OutputStreamExtensions.kt +++ b/core/util-jvm/src/main/java/org/signal/core/util/OutputStreamExtensions.kt @@ -40,3 +40,35 @@ fun OutputStream.writeUInt(value: UInt) { // Note that casting to an int here is fine, because at the end of the day, we're just writing 4 bytes to the stream this.write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value.toInt()).array()) } + +/** + * Writes [count] zero bytes to the stream, using a bounded buffer so that we never allocate the full [count] at once. + * A non-positive [count] writes nothing. + */ +fun OutputStream.writeZeros(count: Long, maxBufferSize: Int = 32 * 1024) { + writeRepeated(0, count, maxBufferSize) +} + +/** + * Writes [value] to the stream [count] times, using a bounded buffer so that we never allocate the full [count] at once. + * A non-positive [count] writes nothing. + */ +fun OutputStream.writeRepeated(value: Byte, count: Long, maxBufferSize: Int = 32 * 1024) { + require(maxBufferSize > 0) { "maxBufferSize must be positive, was $maxBufferSize" } + + if (count <= 0) { + return + } + + val buffer = ByteArray(minOf(count, maxBufferSize.toLong()).toInt()) + if (value != 0.toByte()) { + buffer.fill(value) + } + + var remaining = count + while (remaining > 0) { + val chunkSize = minOf(remaining, buffer.size.toLong()).toInt() + this.write(buffer, 0, chunkSize) + remaining -= chunkSize + } +} diff --git a/lib/archive/src/main/java/org/signal/archive/stream/PaddedGzipOutputStream.kt b/lib/archive/src/main/java/org/signal/archive/stream/PaddedGzipOutputStream.kt index dcfcc04a9b..32b3a7d105 100644 --- a/lib/archive/src/main/java/org/signal/archive/stream/PaddedGzipOutputStream.kt +++ b/lib/archive/src/main/java/org/signal/archive/stream/PaddedGzipOutputStream.kt @@ -5,6 +5,7 @@ package org.signal.archive.stream +import org.signal.core.util.writeZeros import org.whispersystems.signalservice.internal.crypto.PaddingInputStream import java.io.FilterOutputStream import java.io.OutputStream @@ -24,9 +25,8 @@ class PaddedGzipOutputStream private constructor(private val outputStream: SizeO val totalLength = outputStream.size val paddedSize: Long = PaddingInputStream.getPaddedSize(totalLength) - val paddingToAdd: Int = (paddedSize - totalLength).toInt() - outputStream.write(ByteArray(paddingToAdd)) + outputStream.writeZeros(paddedSize - totalLength) } /** diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index de4f231a67..d6baebdc18 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -62,6 +62,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStre import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; +import org.whispersystems.signalservice.api.messages.SignalServiceMessageLimits; import org.whispersystems.signalservice.api.messages.SignalServicePreview; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage; @@ -1004,8 +1005,8 @@ public class SignalServiceMessageSender { Content.Builder container = new Content.Builder(); DataMessage.Builder dataMessage = createDataMessage(message); - if (dataMessage.body != null && Utf8.size(dataMessage.body) > 2048) { - throw new ContentTooLargeException(Utf8.size(dataMessage.body), "UTF-8 size of the data message body was over 2048 bytes!"); + if (dataMessage.body != null && Utf8.size(dataMessage.body) > SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES) { + throw new ContentTooLargeException(Utf8.size(dataMessage.body), "UTF-8 size of the data message body was over " + SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES + " bytes!"); } return enforceMaxContentSize(container.dataMessage(dataMessage.build()).build()); diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt index e6ac74f998..ce685dc792 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidator.kt @@ -1,6 +1,7 @@ package org.whispersystems.signalservice.api.messages import okio.ByteString +import okio.utf8Size import org.signal.core.models.ServiceId import org.signal.core.models.ServiceId.ACI import org.signal.libsignal.protocol.message.CiphertextMessage @@ -91,6 +92,10 @@ object EnvelopeContentValidator { return Result.Invalid("[DataMessage] Timestamps don't match! envelope: ${envelope.clientTimestamp}, content: ${dataMessage.timestamp}") } + if (dataMessage.body.isBodyTooLarge()) { + return Result.Invalid("[DataMessage] Body exceeds ${SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES} bytes!") + } + if (dataMessage.quote != null && ACI.parseOrNull(dataMessage.quote.authorAci, dataMessage.quote.authorAciBinary).isNullOrInvalidServiceId()) { return Result.Invalid("[DataMessage] Invalid ACI on quote!") } @@ -435,6 +440,10 @@ object EnvelopeContentValidator { return Result.Invalid("[EditMessage] Timestamps don't match! envelope: ${envelope.clientTimestamp}, content: ${dataMessage.timestamp}") } + if (dataMessage.body.isBodyTooLarge()) { + return Result.Invalid("[EditMessage] Body exceeds ${SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES} bytes!") + } + if (dataMessage.requiredProtocolVersion != null && dataMessage.requiredProtocolVersion > DataMessage.ProtocolVersion.CURRENT.value) { return Result.UnsupportedDataMessage( ourVersion = DataMessage.ProtocolVersion.CURRENT.value, @@ -484,6 +493,10 @@ object EnvelopeContentValidator { return this.attachments.any { it.contentType == LONG_TEXT_CONTENT_TYPE } } + private fun String?.isBodyTooLarge(): Boolean { + return this != null && this.utf8Size() > SignalServiceMessageLimits.MAX_INLINE_BODY_SIZE_BYTES + } + private fun BodyRange.isStyleRangeMissingOffsets(): Boolean { return this.style != null && (this.start == null || this.length == null) } diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMessageLimits.kt b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMessageLimits.kt new file mode 100644 index 0000000000..38152f1bd7 --- /dev/null +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceMessageLimits.kt @@ -0,0 +1,13 @@ +package org.whispersystems.signalservice.api.messages + +import org.signal.core.util.kibiBytes + +/** + * Size limits that messages must respect on the wire. Shared by the send path, the receive-side + * content validation, and the app layer so that all three agree on a single set of numbers. + */ +object SignalServiceMessageLimits { + /** The maximum size of an inlined text body we'll allow in a proto. Anything larger than this will need to be a long-text attachment. */ + @JvmField + val MAX_INLINE_BODY_SIZE_BYTES: Int = 2.kibiBytes.bytes.toInt() +} diff --git a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java index f34ff2ead9..33d166fcff 100644 --- a/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java +++ b/lib/libsignal-service/src/main/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStream.java @@ -56,10 +56,10 @@ public class PaddingInputStream extends FilterInputStream { } public static long getPaddedSize(long size) { - return (int) Math.max(541, Math.floor(Math.pow(1.05, Math.ceil(Math.log(size) / Math.log(1.05))))); + return (long) Math.max(541, Math.floor(Math.pow(1.05, Math.ceil(Math.log(size) / Math.log(1.05))))); } public static long getMaxUnpaddedSize(long maxPaddedSize) { - return (int) Math.floor(Math.pow(1.05, Math.floor(Math.log(maxPaddedSize) / Math.log(1.05)))); + return (long) Math.floor(Math.pow(1.05, Math.floor(Math.log(maxPaddedSize) / Math.log(1.05)))); } } diff --git a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt index 57b3118eba..a63c65343c 100644 --- a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt +++ b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/api/messages/EnvelopeContentValidatorTest.kt @@ -839,6 +839,81 @@ class EnvelopeContentValidatorTest { assert(result is EnvelopeContentValidator.Result.Valid) } + @Test + fun `validate - ensure data message body of exactly 2048 bytes is marked valid`() { + val content = Content( + dataMessage = DataMessage( + timestamp = 1234, + body = "a".repeat(2048) + ) + ) + + val result = EnvelopeContentValidator.validate(Envelope(clientTimestamp = 1234), content, SELF_ACI, CiphertextMessage.WHISPER_TYPE) + assert(result is EnvelopeContentValidator.Result.Valid) + } + + @Test + fun `validate - ensure data message body over 2048 bytes is marked invalid`() { + val content = Content( + dataMessage = DataMessage( + timestamp = 1234, + body = "a".repeat(2049) + ) + ) + + val result = EnvelopeContentValidator.validate(Envelope(clientTimestamp = 1234), content, SELF_ACI, CiphertextMessage.WHISPER_TYPE) + assert(result is EnvelopeContentValidator.Result.Invalid) + } + + @Test + fun `validate - ensure data message body over 2048 UTF-8 bytes is marked invalid`() { + val content = Content( + dataMessage = DataMessage( + timestamp = 1234, + body = "é".repeat(1025) + ) + ) + + val result = EnvelopeContentValidator.validate(Envelope(clientTimestamp = 1234), content, SELF_ACI, CiphertextMessage.WHISPER_TYPE) + assert(result is EnvelopeContentValidator.Result.Invalid) + } + + @Test + fun `validate - ensure edit message body over 2048 bytes is marked invalid`() { + val content = Content( + editMessage = EditMessage( + targetSentTimestamp = 1000, + dataMessage = DataMessage( + timestamp = 1234, + body = "a".repeat(2049) + ) + ) + ) + + val result = EnvelopeContentValidator.validate(Envelope(clientTimestamp = 1234), content, SELF_ACI, CiphertextMessage.WHISPER_TYPE) + assert(result is EnvelopeContentValidator.Result.Invalid) + } + + @Test + fun `validate - ensure sync sent body over 2048 bytes is marked invalid`() { + val envelope = Envelope(sourceServiceId = SELF_ACI.toString(), clientTimestamp = 1234) + val content = Content( + syncMessage = SyncMessage( + sent = SyncMessage.Sent( + timestamp = 1234, + destinationServiceId = OTHER_ACI.toString(), + message = DataMessage( + timestamp = 1234, + body = "a".repeat(2049) + ) + ) + ) + ) + + val result = EnvelopeContentValidator.validate(envelope, content, SELF_ACI, CiphertextMessage.WHISPER_TYPE) + assert(result is EnvelopeContentValidator.Result.Invalid) + } + @Test fun `validate - ensure sync sent without a timestamp is marked invalid`() { val envelope = Envelope(sourceServiceId = SELF_ACI.toString(), clientTimestamp = 1234) diff --git a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStreamTest.kt b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStreamTest.kt index 903e75adbc..38ae89a080 100644 --- a/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStreamTest.kt +++ b/lib/libsignal-service/src/test/java/org/whispersystems/signalservice/internal/crypto/PaddingInputStreamTest.kt @@ -6,6 +6,9 @@ package org.whispersystems.signalservice.internal.crypto import assertk.assertThat import assertk.assertions.isEqualTo +import assertk.assertions.isGreaterThan +import assertk.assertions.isGreaterThanOrEqualTo +import assertk.assertions.isLessThanOrEqualTo import org.junit.Test import org.signal.core.util.StreamUtil import java.io.ByteArrayInputStream @@ -37,4 +40,32 @@ class PaddingInputStreamTest { } } } + + /** + * Sizes above [Int.MAX_VALUE] must not be truncated. A padded size smaller than the input implies a *negative* amount of padding, which blows up + * callers that use the difference to size a buffer. + */ + @Test + fun `getPaddedSize does not truncate sizes above Int MAX_VALUE`() { + val sizes = listOf( + Int.MAX_VALUE.toLong() - 1, + Int.MAX_VALUE.toLong(), + Int.MAX_VALUE.toLong() + 1, + 3L * 1024 * 1024 * 1024, + 100L * 1024 * 1024 * 1024 + ) + + sizes.forEach { size -> + assertThat(PaddingInputStream.getPaddedSize(size), "padded size of $size").isGreaterThanOrEqualTo(size) + } + } + + @Test + fun `getMaxUnpaddedSize does not truncate sizes above Int MAX_VALUE`() { + val maxPaddedSize = 100L * 1024 * 1024 * 1024 + val maxUnpaddedSize = PaddingInputStream.getMaxUnpaddedSize(maxPaddedSize) + + assertThat(maxUnpaddedSize).isGreaterThan(Int.MAX_VALUE.toLong()) + assertThat(PaddingInputStream.getPaddedSize(maxUnpaddedSize)).isLessThanOrEqualTo(maxPaddedSize) + } }