shashank created CAMEL-25179:
--------------------------------

             Summary: camel-crypto - CryptoDataFormat with the default HMAC 
cannot unmarshal a message larger than its buffer size (4096 bytes): 
ArrayIndexOutOfBoundsException in HMACAccumulator.CircularBuffer
                 Key: CAMEL-25179
                 URL: https://issues.apache.org/jira/browse/CAMEL-25179
             Project: Camel
          Issue Type: Bug
          Components: camel-core
            Reporter: shashank


{{CryptoDataFormat}} appends an HMAC by default ({{shouldAppendHMAC=true}}). On 
unmarshal, {{HMACAccumulator.decryptUpdate}} holds back the last {{maclength}} 
bytes in a {{CircularBuffer}} of {{bufferSize + maclength}} bytes. Both 
{{write}} and {{read}} of that buffer get the wrap-around at the end of the 
array wrong:

{code:java}
public void write(byte[] data, int pos, int len) {
    if (available >= len) {
        if (write + len > buffer.length) {
            int overlap = write + len % buffer.length;        // is write + 
(len % length)
            System.arraycopy(data, 0, buffer, write, len - overlap);   // 
length is -write
            System.arraycopy(data, len - overlap, buffer, 0, overlap);
        } else { ... }
...
public int read(byte[] dest, int position, int len) {
    ...
            if (read > write) {
                int x = buffer.length - read;
                System.arraycopy(buffer, read, dest, position, buffer.length - 
read);  // copies length - read bytes, not len
                System.arraycopy(buffer, 0, dest, position + x, overlap);
            } else {
                System.arraycopy(buffer, read, dest, position, len);            
       // no wrap-around at all
            }
{code}

Every {{write}} that has to wrap throws (the first copy gets a negative 
length), and a {{read}} throws or copies the wrong bytes as soon as the read 
position has passed the write position. {{CipherInputStream}} hands the 
decrypted data out in chunks of about 512 bytes (GCM: in chunks of 
{{bufferSize}}), so the buffer wraps as soon as a message is larger than 
{{bufferSize}}: unmarshal of any message of 4096 bytes or more fails with the 
default settings:

{noformat}
java.lang.ArrayIndexOutOfBoundsException: arraycopy: length -4088 is negative
    at 
org.apache.camel.converter.crypto.HMACAccumulator$CircularBuffer.write(HMACAccumulator.java:138)
    at 
org.apache.camel.converter.crypto.HMACAccumulator.decryptUpdate(HMACAccumulator.java:71)
    at 
org.apache.camel.converter.crypto.CryptoDataFormat.unmarshal(CryptoDataFormat.java:197)
{noformat}

The data format documentation example ({{new CryptoDataFormat("DES", key)}}, 
marshal then unmarshal) fails for a 5000 byte body. The existing tests only use 
a 49 byte payload, and {{HMACAccumulatorTest}} never wraps the buffer. The 
workarounds are {{shouldAppendHMAC=false}} (no integrity check) or a 
{{bufferSize}} larger than every message.

h3. Reproduction

{code:java}
CryptoDataFormat crypto = new CryptoDataFormat("DES", 
KeyGenerator.getInstance("DES").generateKey());
from("direct:basic").marshal(crypto).unmarshal(crypto).to("mock:unencrypted");

template.sendBody("direct:basic", new byte[5000]);   // fails; 100 bytes works
{code}

Unit tests round-trip 100, 4096, 5000 and 100000 bytes through the data format 
with the default options, and exercise the two {{CircularBuffer}} cases and a 
1000 byte input fed to {{HMACAccumulator}} in 24 byte chunks: on main every 
case from 4096 bytes up fails, the 100 byte control passes (checked again on 
main {{c3632125413b}}, 2026-09-30). The buffer code comes from the original 
contribution (CAMEL-2482, 2010) and is the same in 3.0.0, 4.0.0, 4.14.0, 4.18.0 
and 4.22.0; later changes to the class (CAMEL-19194, CAMEL-23766, CAMEL-24440) 
did not touch it.

A small formal model (Lean 4) of the two methods shows that every {{write}} 
which has to wrap throws, that a non-wrapping {{read}} with {{read > write}} 
throws when the destination only has room for {{len}} bytes (the 
{{decryptUpdate}} case), and that the fixed methods implement a FIFO queue; 
together with a model of {{decryptUpdate}} this gives: for every way the input 
is split into chunks of at most {{bufferSize}} bytes, the bytes passed on are 
the input without its last {{maclength}} bytes, and the bytes kept back for the 
check are exactly those last {{maclength}} bytes.

h3. Proposed fix

Split both copies at the end of the array:

{code:java}
public void write(byte[] data, int pos, int len) {
    if (available >= len) {
        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;
    }
}

public int read(byte[] dest, int position, int len) {
    if (dest.length - position >= len) {
        if (buffer.length - available >= len) {
            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;
        }
    }
    return 0;
}
{code}

The format of the data is unchanged (marshal is not affected), so messages 
written by earlier versions can be read. With the fix the new tests pass, and 
so does the whole camel-crypto suite (84 tests).

This is a failure, not a weakness in the MAC check: the MAC is still compared 
over exactly the bytes that are passed on.

Duplicate check (2026-09-30): JIRA "HMACAccumulator", "CircularBuffer", 
"CryptoDataFormat" with "large" or "ArrayIndexOutOfBoundsException": only 
CAMEL-19194, CAMEL-23766 and CAMEL-24440, none about the buffer. No open or 
closed pull request touches the circular buffer.

_Filed with Claude Code on behalf of allthingssecurity._




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to