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)