This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/camel-4.22.x by this push:
new 3e23aeb834e1 [backport camel-4.22.x] CAMEL-25179: camel-crypto -
CryptoDataFormat with HMAC fails to unmarshal messages larger than its buffer
(#27175)
3e23aeb834e1 is described below
commit 3e23aeb834e16d1c82f41c9ed0a835ac9111c923
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Oct 1 10:51:10 2026 +0200
[backport camel-4.22.x] CAMEL-25179: camel-crypto - CryptoDataFormat with
HMAC fails to unmarshal messages larger than its buffer (#27175)
* [backport camel-4.22.x] CAMEL-25179: camel-crypto - CryptoDataFormat with
HMAC fails to unmarshal messages larger than its buffer
* CAMEL-25179: adapt CryptoDataFormatLargePayloadTest to camel-4.22.x
- HMACAccumulator.AUTHENTICATION_FAILED only exists on main (CAMEL-24440),
so check the
"Expected mac did not match actual mac" message this branch reports
instead
- a truncated message fails on decryption (padding) before the mac is
checked on this branch,
so only assert that the unmarshal fails
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../camel/converter/crypto/HMACAccumulator.java | 27 ++---
.../crypto/CryptoDataFormatLargePayloadTest.java | 124 +++++++++++++++++++++
.../converter/crypto/HMACAccumulatorTest.java | 62 ++++++++++-
3 files changed, 197 insertions(+), 16 deletions(-)
diff --git
a/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/HMACAccumulator.java
b/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/HMACAccumulator.java
index a1482c3bb0ed..e1deb80f29f4 100644
---
a/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/HMACAccumulator.java
+++
b/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/HMACAccumulator.java
@@ -129,29 +129,26 @@ public class HMACAccumulator {
public void write(byte[] data, int pos, int len) {
if (available >= len) {
- if (write + len > buffer.length) {
- int overlap = write + len % buffer.length;
- System.arraycopy(data, 0, buffer, write, len - overlap);
- System.arraycopy(data, len - overlap, buffer, 0, overlap);
- } else {
- System.arraycopy(data, pos, buffer, write, len);
- }
+ // copy up to the end of the array, and the rest (if any) to
its start
+ int first = Math.min(len, buffer.length - write);
+ System.arraycopy(data, pos, buffer, write, first);
+ System.arraycopy(data, pos + first, buffer, 0, len - first);
write = (write + len) % buffer.length;
available -= len;
+ } else {
+ // cannot happen with the sizes used by decryptUpdate, but
fail loudly rather than drop data
+ throw new IllegalStateException(
+ "Cannot write " + len + " bytes to the circular
buffer, only " + available + " available");
}
}
public int read(byte[] dest, int position, int len) {
if (dest.length - position >= len) {
if (buffer.length - available >= len) {
- int overlap = (read + len) % buffer.length;
- if (read > write) {
- int x = buffer.length - read;
- System.arraycopy(buffer, read, dest, position,
buffer.length - read);
- System.arraycopy(buffer, 0, dest, position + x,
overlap);
- } else {
- System.arraycopy(buffer, read, dest, position, len);
- }
+ // copy up to the end of the array, and the rest (if any)
from its start
+ int first = Math.min(len, buffer.length - read);
+ System.arraycopy(buffer, read, dest, position, first);
+ System.arraycopy(buffer, 0, dest, position + first, len -
first);
read = (read + len) % buffer.length;
available += len;
return len;
diff --git
a/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/CryptoDataFormatLargePayloadTest.java
b/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/CryptoDataFormatLargePayloadTest.java
new file mode 100644
index 000000000000..4e2196041764
--- /dev/null
+++
b/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/CryptoDataFormatLargePayloadTest.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.converter.crypto;
+
+import java.util.Arrays;
+
+import javax.crypto.KeyGenerator;
+
+import org.apache.camel.CamelExecutionException;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * The HMAC (appended by default) is split off in a circular buffer on
unmarshal. The buffer must wrap around for any
+ * message larger than the buffer size (4096 bytes by default).
+ */
+public class CryptoDataFormatLargePayloadTest extends CamelTestSupport {
+
+ @Test
+ void testRoundTripSmallerThanBuffer() throws Exception {
+ doRoundTrip(100);
+ }
+
+ @Test
+ void testRoundTripBufferSize() throws Exception {
+ doRoundTrip(4096);
+ }
+
+ @Test
+ void testRoundTripLargerThanBuffer() throws Exception {
+ doRoundTrip(5000);
+ }
+
+ @Test
+ void testRoundTripLarge() throws Exception {
+ doRoundTrip(100_000);
+ }
+
+ @Test
+ void testTamperedCiphertextLargerThanBufferFailsAuthentication() {
+ byte[] encrypted = template.requestBody("direct:marshal",
payload(5000), byte[].class);
+ encrypted[encrypted.length / 2] ^= 0x01;
+
+ assertAuthenticationFailed(encrypted);
+ }
+
+ @Test
+ void testTruncatedCiphertextLargerThanBufferFailsAuthentication() {
+ byte[] encrypted = template.requestBody("direct:marshal",
payload(5000), byte[].class);
+ // drop the last cipher block (DES has 8 byte blocks)
+ byte[] truncated = Arrays.copyOf(encrypted, encrypted.length - 8);
+
+ // on this branch a truncated message fails on decryption (padding)
before the mac is checked
+ assertThrows(CamelExecutionException.class, () ->
template.requestBody("direct:unmarshal", truncated));
+ }
+
+ private void assertAuthenticationFailed(byte[] encrypted) {
+ CamelExecutionException e
+ = assertThrows(CamelExecutionException.class, () ->
template.requestBody("direct:unmarshal", encrypted));
+ IllegalStateException cause =
assertInstanceOf(IllegalStateException.class, e.getCause());
+ assertTrue(cause.getMessage().startsWith("Expected mac did not match
actual mac"), cause.getMessage());
+ }
+
+ private void doRoundTrip(int size) throws Exception {
+ byte[] payload = payload(size);
+
+ MockEndpoint mock = getMockEndpoint("mock:unencrypted");
+ mock.expectedMessageCount(1);
+ template.sendBody("direct:basic", payload);
+ MockEndpoint.assertIsSatisfied(context);
+
+ assertArrayEquals(payload,
mock.getReceivedExchanges().get(0).getIn().getMandatoryBody(byte[].class));
+ }
+
+ private static byte[] payload(int size) {
+ byte[] payload = new byte[size];
+ for (int i = 0; i < size; i++) {
+ payload[i] = (byte) (i * 31 + 7);
+ }
+ return payload;
+ }
+
+ @Override
+ protected RouteBuilder createRouteBuilder() throws Exception {
+ return new RouteBuilder() {
+ public void configure() throws Exception {
+ KeyGenerator generator = KeyGenerator.getInstance("DES");
+ CryptoDataFormat cryptoFormat = new CryptoDataFormat("DES",
generator.generateKey());
+
+ from("direct:basic")
+ .marshal(cryptoFormat)
+ .unmarshal(cryptoFormat)
+ .to("mock:unencrypted");
+
+ from("direct:marshal")
+ .marshal(cryptoFormat);
+
+ from("direct:unmarshal")
+ .unmarshal(cryptoFormat);
+ }
+ };
+ }
+}
diff --git
a/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/HMACAccumulatorTest.java
b/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/HMACAccumulatorTest.java
index 8a13500236b2..5438704d176d 100644
---
a/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/HMACAccumulatorTest.java
+++
b/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/HMACAccumulatorTest.java
@@ -16,9 +16,11 @@
*/
package org.apache.camel.converter.crypto;
+import java.io.ByteArrayOutputStream;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
@@ -27,6 +29,7 @@ import
org.apache.camel.converter.crypto.HMACAccumulator.CircularBuffer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -140,6 +143,33 @@ public class HMACAccumulatorTest {
validate(builder);
}
+ @Test
+ void testDecryptionWhereDataWrapsAroundBuffer() throws Exception {
+ int buffersize = 64;
+ // CipherInputStream hands out the decrypted data in chunks, so the
buffer has to wrap around for any input
+ // larger than the buffer
+ byte[] data = new byte[1000];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i * 31 + 7);
+ }
+ payload = data;
+ createExpectedMac();
+ byte[] input = Arrays.copyOf(payload, payload.length +
expected.length);
+ System.arraycopy(expected, 0, input, payload.length, expected.length);
+
+ HMACAccumulator builder = new HMACAccumulator(key, "HmacSHA1", null,
buffersize);
+ ByteArrayOutputStream plaintext = new ByteArrayOutputStream();
+ builder.attachStream(plaintext);
+ byte[] buffer = new byte[buffersize];
+ for (int pos = 0; pos < input.length; pos += 24) {
+ int read = Math.min(24, input.length - pos);
+ System.arraycopy(input, pos, buffer, 0, read);
+ builder.decryptUpdate(buffer, read);
+ }
+ validate(builder);
+ assertArrayEquals(payload, plaintext.toByteArray());
+ }
+
private void validate(HMACAccumulator builder) {
assertMacs(builder.getCalculatedMac(), builder.getCalculatedMac());
assertMacs(builder.getAppendedMac(), builder.getAppendedMac());
@@ -159,7 +189,8 @@ public class HMACAccumulatorTest {
assertEquals(payload.length, buffer.availableForWrite());
buffer.write(payload, 0, payload.length);
assertEquals(0, buffer.availableForWrite());
- buffer.write(payload, 0, payload.length);
+ // a write that does not fit is not silently dropped
+ assertThrows(IllegalStateException.class, () -> buffer.write(payload,
0, payload.length));
assertEquals(0, buffer.availableForWrite());
}
@@ -174,6 +205,35 @@ public class HMACAccumulatorTest {
assertEquals(0, buffer.read(data, 0, data.length));
}
+ @Test
+ void testBufferWriteWrapsAround() {
+ CircularBuffer buffer = new CircularBuffer(5);
+ byte[] data = new byte[3];
+ buffer.write(new byte[] { 1, 2, 3 }, 0, 3);
+ assertEquals(3, buffer.read(data, 0, 3));
+
+ // 3 bytes from position 3 of a 5 byte buffer: 2 go to the end of the
array, 1 to its start
+ buffer.write(new byte[] { 0, 4, 5, 6 }, 1, 3);
+ assertEquals(2, buffer.availableForWrite());
+ assertEquals(3, buffer.read(data, 0, 3));
+ assertArrayEquals(new byte[] { 4, 5, 6 }, data);
+ }
+
+ @Test
+ void testBufferReadBehindWritePosition() {
+ CircularBuffer buffer = new CircularBuffer(5);
+ byte[] data = new byte[2];
+ buffer.write(new byte[] { 1, 2, 3 }, 0, 3);
+ assertEquals(2, buffer.read(data, 0, 2));
+ // fills the array up to its end, so the write position is back at 0
and the read position is behind it
+ buffer.write(new byte[] { 4, 5 }, 0, 2);
+
+ assertEquals(1, buffer.read(data, 0, 1));
+ assertEquals(3, data[0]);
+ assertEquals(2, buffer.read(data, 0, 2));
+ assertArrayEquals(new byte[] { 4, 5 }, data);
+ }
+
private byte[] initializeBuffer(int buffersize) {
byte[] buffer = new byte[buffersize];
System.arraycopy(payload, 0, buffer, 0, payload.length);