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

Reply via email to