This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch backport/CAMEL-24440-camel-4.22.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit ec3b9aaa5ddbffff7586dc982e4e81fccacd1821 Author: Andrea Cosentino <[email protected]> AuthorDate: Fri Sep 25 11:25:50 2026 +0200 [backport camel-4.22.x] CAMEL-24440: camel-crypto - per-message IV when inlining, and uniform authentication failures (#26725) * CAMEL-24440: camel-crypto - per-message IV when inlining, and uniform authentication failures Three changes to CryptoDataFormat, none of which alters the format of data already written. Per-message initialization vector. marshal() threw "Inlining cannot be performed, as no initialization vector was specified" whenever shouldInlineInitializationVector was set without a statically configured vector, which pushed routes into configuring one and reusing it for every message - the thing inlining exists to avoid. A fresh vector is now generated per message when none is supplied. The vector is written into the message, so readers take it from the stream and need no change; a vector supplied explicitly, by configuration or by the CamelCryptoInitVector header, is still used as given. Uniform authentication failure. A tampered message surfaced two distinguishable outcomes - "Given final block not properly padded" from CipherInputStream, or "Expected mac did not match actual mac" from the MAC check - and a caller who can submit ciphertext and observe which one came back can use that to recover plaintext. Both now report "Message authentication failed". Checked on JDK 21 that CipherInputStream does surface bad padding as an IOException wrapping BadPaddingException, since older JDKs swallowed it. MAC values out of the failure message. It carried both MACs in hex, and the computed one is HMAC_k over the plaintext just produced, so it handed the caller a value they could not otherwise compute. Also bound the inlined vector length, which was read from the message and used directly to size an allocation. Left alone: the HMAC key is still derived from the same material as the cipher key. Separating them changes the MAC written into the message, so neither direction stays readable across versions - that needs an opt-in or a format version marker and is tracked on the issue, along with the MAC-then-encrypt composition, where validating before streaming would mean buffering the whole plaintext. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Signed-off-by: Andrea Cosentino <[email protected]> * CAMEL-24440: camel-crypto - cache the cipher block size and clarify the failure-reporting test Addresses review feedback on #26725: - generateInitializationVector no longer builds a throwaway Cipher on every marshal just to read the block size; the size is fixed by the algorithm, so it is looked up once and cached. - clarified the failure-reporting test's comment: the MAC is encrypted inside the ciphertext (written through the CipherOutputStream), so flipping the last byte corrupts the final block's padding - the BadPaddingException path - which the test already exercises. Co-Authored-By: Claude Opus 4.8 <[email protected]> Signed-off-by: Andrea Cosentino <[email protected]> * CAMEL-24440: camel-crypto - address review on inline IV, MAC-off failures, and test style Addresses davsclaus's review on #26725: - inlining with a configured algorithmParameterSpec now fails loudly instead of generating a per-message IV the cipher ignores (the spec wins in initializeCipher), which silently reused one IV across every message. - the BadPaddingException -> "Message authentication failed" rewrite is now gated on shouldAppendHMAC: with the MAC off nothing is authenticating, so the real cipher cause is surfaced (at debug) rather than misdescribed. - moved MAX_INLINE_IV_LENGTH and SECURE_RANDOM up with the other class fields. - imported java.util.Arrays and java.io.ByteArrayInputStream in the test rather than using fully-qualified names (OpenRewrite / CI uncommitted-changes check). - added tests for both behaviour fixes, each verified to fail against the un-fixed code. Co-authored-by: Claude Opus 4.8 <[email protected]> Signed-off-by: Andrea Cosentino <[email protected]> * Update docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc Co-authored-by: Guillaume Nodet - AI Bot <[email protected]> --------- Signed-off-by: Andrea Cosentino <[email protected]> Co-authored-by: Claude Opus 5 (1M context) <[email protected]> Co-authored-by: Guillaume Nodet - AI Bot <[email protected]> (cherry picked from commit 4ebacf60e46cc3e2d74a07076cabb7c9e268713a) Backport notes: the upgrade note goes to camel-4x-upgrade-guide-4_22.adoc (4.22.2 section) instead of 4_23, and CryptoDataFormatLargePayloadTest takes the version from main again (the CAMEL-25179 backport had changed it to expect the old MAC mismatch message). Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/converter/crypto/CryptoDataFormat.java | 75 +++++++- .../camel/converter/crypto/HMACAccumulator.java | 16 +- .../crypto/CryptoDataFormatIvAndFailureTest.java | 210 +++++++++++++++++++++ .../crypto/CryptoDataFormatLargePayloadTest.java | 7 +- .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc | 26 +++ 5 files changed, 322 insertions(+), 12 deletions(-) diff --git a/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/CryptoDataFormat.java b/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/CryptoDataFormat.java index eb0449959608..09a0764b5a1e 100644 --- a/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/CryptoDataFormat.java +++ b/components/camel-crypto/src/main/java/org/apache/camel/converter/crypto/CryptoDataFormat.java @@ -21,7 +21,9 @@ import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.GeneralSecurityException; import java.security.Key; +import java.security.SecureRandom; import java.security.spec.AlgorithmParameterSpec; import javax.crypto.Cipher; @@ -72,6 +74,13 @@ public class CryptoDataFormat extends ServiceSupport implements DataFormat, Data private static final Logger LOG = LoggerFactory.getLogger(CryptoDataFormat.class); private static final String INIT_VECTOR = "CamelCryptoInitVector"; + private static final SecureRandom SECURE_RANDOM = new SecureRandom(); + /** + * Upper bound on the length of an inlined initialization vector read from the stream. A JCE initialization vector + * is at most a cipher block, so this is generous; the bound exists because the length is read from the message and + * used directly to size an allocation. + */ + private static final int MAX_INLINE_IV_LENGTH = 1024; private String algorithm; private String cryptoProvider; private Key key; @@ -81,6 +90,9 @@ public class CryptoDataFormat extends ServiceSupport implements DataFormat, Data private String macAlgorithm = "HmacSHA1"; private boolean shouldAppendHMAC = true; private AlgorithmParameterSpec algorithmParameterSpec; + // The cipher block size is fixed by the algorithm, so it is looked up once and cached rather than building a + // Cipher on every marshal purely to read it. + private volatile int cachedBlockSize; public CryptoDataFormat() { } @@ -124,6 +136,20 @@ public class CryptoDataFormat extends ServiceSupport implements DataFormat, Data @Override public void marshal(Exchange exchange, Object graph, OutputStream outputStream) throws Exception { byte[] iv = getInitializationVector(exchange); + if (iv == null && inline) { + if (algorithmParameterSpec != null) { + // initializeCipher gives algorithmParameterSpec precedence over the IV, so a generated vector would be + // written into the message but never used - every message would encrypt identically behind a vector + // that only looks per-message. Keep failing loudly, as this configuration did before. + throw new IllegalStateException( + "Inlining cannot be performed when an algorithmParameterSpec is configured, as the spec is" + + " used instead of the initialization vector"); + } + // The whole point of inlining is that the IV travels with the message, so there is no reason to make + // the caller supply a fixed one - and requiring it is what used to push users into reusing a single IV + // across every message. + iv = generateInitializationVector(); + } Key key = getKey(exchange); InputStream plaintextStream = ExchangeHelper.convertToMandatoryType(exchange, InputStream.class, graph); @@ -166,8 +192,23 @@ public class CryptoDataFormat extends ServiceSupport implements DataFormat, Data byte[] buffer = new byte[bufferSize]; hmac.attachStream(osb); int read; - while ((read = cipherStream.read(buffer)) >= 0) { - hmac.decryptUpdate(buffer, read); + try { + while ((read = cipherStream.read(buffer)) >= 0) { + hmac.decryptUpdate(buffer, read); + } + } catch (IOException e) { + if (shouldAppendHMAC && e.getCause() instanceof GeneralSecurityException) { + // CipherInputStream surfaces bad padding as an IOException wrapping + // BadPaddingException, while a bad MAC surfaces from validate() below. Reporting the two + // differently is exactly what lets a caller who can submit ciphertext and watch the + // outcome tell them apart, which is the padding-oracle distinguisher. Report the same + // authentication failure for both - but only when a MAC is actually appended: with + // shouldAppendHMAC=false nothing is authenticating, so calling it an authentication failure + // would misdescribe a plain padding error and drop its cause. + LOG.debug("Reporting cipher failure as an authentication failure", e); + throw new IllegalStateException(HMACAccumulator.AUTHENTICATION_FAILED); + } + throw e; } hmac.validate(); return osb.build(); @@ -207,6 +248,11 @@ public class CryptoDataFormat extends ServiceSupport implements DataFormat, Data if (inline) { try { int ivLength = new DataInputStream(encryptedStream).readInt(); + if (ivLength < 0 || ivLength > MAX_INLINE_IV_LENGTH) { + throw new IOException( + String.format("Inlined initialization vector length '%d' is not between 0 and %d", + ivLength, MAX_INLINE_IV_LENGTH)); + } iv = new byte[ivLength]; int read = encryptedStream.read(iv); if (read != ivLength) { @@ -247,6 +293,31 @@ public class CryptoDataFormat extends ServiceSupport implements DataFormat, Data }; } + /** + * A fresh initialization vector, sized to the cipher's block length. Only used when the vector is inlined into the + * message, so the reader takes it from the stream and nothing needs to be shared out of band. + */ + private byte[] generateInitializationVector() throws Exception { + byte[] iv = new byte[getCipherBlockSize()]; + SECURE_RANDOM.nextBytes(iv); + return iv; + } + + private int getCipherBlockSize() throws Exception { + int blockSize = cachedBlockSize; + if (blockSize == 0) { + Cipher cipher + = cryptoProvider == null ? Cipher.getInstance(algorithm) : Cipher.getInstance(algorithm, cryptoProvider); + blockSize = cipher.getBlockSize(); + if (blockSize <= 0) { + // A stream cipher reports no block size; 16 bytes is the usual nonce length + blockSize = 16; + } + cachedBlockSize = blockSize; + } + return blockSize; + } + private byte[] getInitializationVector(Exchange exchange) { byte[] iv = exchange.getIn().getHeader(INIT_VECTOR, byte[].class); if (iv == null) { 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 e1deb80f29f4..e0b7181fffa4 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 @@ -24,8 +24,6 @@ import java.security.MessageDigest; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; -import static org.apache.camel.converter.crypto.HexUtils.byteArrayToHexString; - /** * <code>HMACAccumulator</code> is used to build Hash Message Authentication Codes. It has two modes, one where all the * data acquired is used to build the MAC and a second that assumes that the last n bytes of the acquired data will @@ -96,15 +94,21 @@ public class HMACAccumulator { return appended; } + /** + * The single message reported for every authentication failure. Bad padding and a bad MAC must be indistinguishable + * to a caller who can submit ciphertext and observe the outcome, because telling them apart is what turns a CBC + * decryption into a padding oracle. + */ + static final String AUTHENTICATION_FAILED = "Message authentication failed"; + public void validate() { byte[] actual = getCalculatedMac(); byte[] expected = getAppendedMac(); // Use a constant-time comparison to avoid leaking MAC-match progress through timing (side-channel). if (!MessageDigest.isEqual(expected, actual)) { - throw new IllegalStateException( - "Expected mac did not match actual mac\nexpected:" - + byteArrayToHexString(expected) + "\n actual:" - + byteArrayToHexString(actual)); + // The computed MAC is HMAC_k over the plaintext that was just produced, so reporting it hands the + // caller a value they could not otherwise compute. Neither MAC belongs in the message. + throw new IllegalStateException(AUTHENTICATION_FAILED); } } diff --git a/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/CryptoDataFormatIvAndFailureTest.java b/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/CryptoDataFormatIvAndFailureTest.java new file mode 100644 index 000000000000..a5c9ff59d717 --- /dev/null +++ b/components/camel-crypto/src/test/java/org/apache/camel/converter/crypto/CryptoDataFormatIvAndFailureTest.java @@ -0,0 +1,210 @@ +/* + * 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.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.util.Arrays; + +import javax.crypto.KeyGenerator; +import javax.crypto.spec.IvParameterSpec; + +import org.apache.camel.Exchange; +import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.support.DefaultExchange; +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.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CryptoDataFormatIvAndFailureTest { + + private static final String PAYLOAD = "the quick brown fox jumps over the lazy dog"; + + /** + * Inlining exists so the initialization vector travels with the message. Requiring a statically configured one as + * well is what pushed routes into reusing a single vector for every message. + */ + @Test + void inliningGeneratesAFreshInitializationVectorPerMessage() throws Exception { + Key key = key(); + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + CryptoDataFormat encryptor = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + encryptor.setShouldInlineInitializationVector(true); + + byte[] first = marshal(context, encryptor, PAYLOAD); + byte[] second = marshal(context, encryptor, PAYLOAD); + + assertFalse(Arrays.equals(first, second), + "the same plaintext must not produce identical ciphertext twice"); + + CryptoDataFormat decryptor = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + decryptor.setShouldInlineInitializationVector(true); + assertEquals(PAYLOAD, unmarshal(context, decryptor, first)); + assertEquals(PAYLOAD, unmarshal(context, decryptor, second)); + } + } + + /** + * A caller who can submit ciphertext and observe the outcome must not be able to tell a padding failure from a MAC + * failure - telling them apart is what turns CBC decryption into a padding oracle. + */ + @Test + void badPaddingAndBadMacAreReportedIdentically() throws Exception { + Key key = key(); + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + // a static vector, not inlining, so this exercises the failure reporting and nothing else + CryptoDataFormat format = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + format.setInitVector(new byte[16]); + + byte[] ciphertext = marshal(context, format, PAYLOAD); + + // The MAC is written through the CipherOutputStream during marshal, so it is encrypted inside the + // ciphertext, not appended in the clear. The last wire byte is therefore the final ciphertext block, and + // flipping it makes that block no longer decrypt to valid PKCS5 padding - the BadPaddingException path, + // distinct from the bad-MAC path exercised just below. + byte[] badPadding = ciphertext.clone(); + badPadding[badPadding.length - 1] ^= 0x01; + + // corrupt a byte in the middle: padding still validates, the appended MAC does not + byte[] badMac = ciphertext.clone(); + badMac[badMac.length / 2] ^= 0x01; + + String paddingFailure = failureMessage(context, format, badPadding); + String macFailure = failureMessage(context, format, badMac); + + assertEquals(macFailure, paddingFailure, "the two failures must be indistinguishable"); + assertTrue(paddingFailure.contains("authentication failed"), "unexpected message: " + paddingFailure); + } + } + + /** + * The inlined length is read from the message and used to size an allocation, so it has to be bounded. + */ + @Test + void anOversizedInlinedInitializationVectorLengthIsRejected() throws Exception { + Key key = key(); + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + CryptoDataFormat decryptor = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + decryptor.setShouldInlineInitializationVector(true); + + // a four byte length of 0x7FFFFFFF followed by nothing + byte[] hostile = { 0x7F, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF }; + + Exception e = assertThrows(Exception.class, () -> unmarshal(context, decryptor, hostile)); + assertTrue(rootMessage(e).contains("is not between 0 and"), "unexpected message: " + rootMessage(e)); + } + } + + @Test + void aRoundTripWithAStaticVectorStillWorks() throws Exception { + Key key = key(); + byte[] iv = new byte[16]; + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + CryptoDataFormat format = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + format.setInitVector(iv); + + byte[] ciphertext = marshal(context, format, PAYLOAD); + assertEquals(PAYLOAD, unmarshal(context, format, ciphertext)); + assertArrayEquals(iv, format.getInitVector()); + } + } + + /** + * Inlining plus a configured algorithmParameterSpec used to be a silent IV-reuse trap: the spec wins in the cipher, + * so a generated per-message vector was written into the message but never used. It must fail loudly. + */ + @Test + void inliningWithAnAlgorithmParameterSpecIsRejected() throws Exception { + Key key = key(); + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + CryptoDataFormat format = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + format.setShouldInlineInitializationVector(true); + format.setAlgorithmParameterSpec(new IvParameterSpec(new byte[16])); + + IllegalStateException e = assertThrows(IllegalStateException.class, + () -> marshal(context, format, PAYLOAD)); + assertTrue(e.getMessage().contains("algorithmParameterSpec"), "unexpected message: " + e.getMessage()); + } + } + + /** + * With the MAC turned off there is nothing authenticating, so a cipher failure must surface as itself rather than + * be relabelled "Message authentication failed" (which would also drop the real cause). + */ + @Test + void aCipherFailureWithoutAMacIsNotReportedAsAnAuthenticationFailure() throws Exception { + Key key = key(); + try (DefaultCamelContext context = new DefaultCamelContext()) { + context.start(); + CryptoDataFormat format = new CryptoDataFormat("AES/CBC/PKCS5Padding", key); + format.setInitVector(new byte[16]); + format.setShouldAppendHMAC(false); + + byte[] ciphertext = marshal(context, format, PAYLOAD); + byte[] corrupted = ciphertext.clone(); + corrupted[corrupted.length - 1] ^= 0x01; + + String message = failureMessage(context, format, corrupted); + assertFalse(message.contains("authentication failed"), + "a cipher failure with no MAC must not be relabelled an authentication failure: " + message); + } + } + + private static String failureMessage(DefaultCamelContext context, CryptoDataFormat format, byte[] body) { + Exception e = assertThrows(Exception.class, () -> unmarshal(context, format, body)); + return rootMessage(e); + } + + private static String rootMessage(Throwable t) { + while (t.getCause() != null) { + t = t.getCause(); + } + return String.valueOf(t.getMessage()); + } + + private static byte[] marshal(DefaultCamelContext context, CryptoDataFormat format, String payload) + throws Exception { + Exchange exchange = new DefaultExchange(context); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + format.marshal(exchange, payload.getBytes(StandardCharsets.UTF_8), out); + return out.toByteArray(); + } + + private static String unmarshal(DefaultCamelContext context, CryptoDataFormat format, byte[] body) + throws Exception { + Exchange exchange = new DefaultExchange(context); + Object result = format.unmarshal(exchange, new ByteArrayInputStream(body)); + return context.getTypeConverter().convertTo(String.class, exchange, result); + } + + private static Key key() throws Exception { + KeyGenerator generator = KeyGenerator.getInstance("AES"); + generator.init(128); + return generator.generateKey(); + } +} 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 index 4e2196041764..20fc659df409 100644 --- 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 @@ -27,9 +27,9 @@ 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.assertEquals; 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 @@ -71,15 +71,14 @@ public class CryptoDataFormatLargePayloadTest extends CamelTestSupport { // 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)); + assertAuthenticationFailed(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()); + assertEquals(HMACAccumulator.AUTHENTICATION_FAILED, cause.getMessage()); } private void doRoundTrip(int size) throws Exception { diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc index 01071fd0570e..2395f6c2a04e 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc @@ -29,6 +29,32 @@ binding mode allowed it and marshalled the body, so a verb producing `text/plain a binary body failed. When `produces` lists several media types, the first json (or xml) type is used, otherwise the first one. Wildcards such as `*/*` are skipped, falling back to `application/json` or `application/xml`. +=== camel-crypto + +Three changes to `CryptoDataFormat`, none of which affects the format of data already written. + +*A per-message initialization vector when inlining.* `marshal` used to throw +`Inlining cannot be performed, as no initialization vector was specified` when +`shouldInlineInitializationVector` was set without a statically configured vector — which pushed routes +into reusing one vector for every message, the thing inlining exists to avoid. A fresh vector is now +generated per message when none is supplied. Because the vector is written into the message, readers pick +it up from the stream and need no change. A vector supplied explicitly, by configuration or by the +`CamelCryptoInitVector` header, is still used as given. + +*Authentication failures report uniformly.* A tampered message previously surfaced two distinguishable +outcomes: `Given final block not properly padded` from the cipher, or `Expected mac did not match actual +mac` from the MAC check. A caller able to submit ciphertext and observe which one came back can use that +distinction to recover plaintext. Both now report `Message authentication failed`, and the message no +longer includes the expected and computed MAC values — the computed one is `HMAC_k` over the plaintext +just produced. Code matching on the old text must be updated. + +*The inlined vector length is bounded.* The length prefix is read from the message and used to size an +allocation; a declared length outside 0–1024 is now rejected instead of attempted. + +Not changed: the HMAC key is still derived from the same key material as the cipher. Separating them +would change the MAC written into the message and so could not be read by earlier versions; that is +tracked separately. + == Upgrading from 4.22.0 to 4.22.1 === camel-hazelcast - ReplicatedHazelcastAggregationRepository now applies the default serialization filter
