diff --git a/core/src/main/java/org/bouncycastle/crypto/modes/ChaCha20Poly1305.java b/core/src/main/java/org/bouncycastle/crypto/modes/ChaCha20Poly1305.java index 78956b013f..02e88dae0e 100644 --- a/core/src/main/java/org/bouncycastle/crypto/modes/ChaCha20Poly1305.java +++ b/core/src/main/java/org/bouncycastle/crypto/modes/ChaCha20Poly1305.java @@ -346,10 +346,14 @@ public int processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) t { case State.DEC_DATA: { - for (int i = 0; i < len; ++i) + while (len > 0) { - buf[bufPos] = in[inOff + i]; - if (++bufPos == buf.length) + int n = Math.min(len, buf.length - bufPos); + System.arraycopy(in, inOff, buf, bufPos, n); + inOff += n; + len -= n; + bufPos += n; + if (bufPos == buf.length) { poly1305.update(buf, 0, BUF_SIZE); processData(buf, 0, BUF_SIZE, out, outOff + resultLen); diff --git a/core/src/test/java/org/bouncycastle/crypto/test/ChaCha20Poly1305Test.java b/core/src/test/java/org/bouncycastle/crypto/test/ChaCha20Poly1305Test.java index b8dc6a5c29..d456681182 100644 --- a/core/src/test/java/org/bouncycastle/crypto/test/ChaCha20Poly1305Test.java +++ b/core/src/test/java/org/bouncycastle/crypto/test/ChaCha20Poly1305Test.java @@ -59,9 +59,84 @@ public void performTest() throws Exception outputSizeTests(); randomTests(); + testPiecewiseDecryption(); testExceptions(); } + /* + * Decryption holds back the last MAC_SIZE bytes it has seen, since they may be the tag, so how the ciphertext is + * split across calls decides what sits in the buffer when the next call arrives. Every split must decrypt as + * one call does. + */ + private void testPiecewiseDecryption() + throws InvalidCipherTextException + { + SecureRandom random = new SecureRandom(); + byte[] K = new byte[32]; + random.nextBytes(K); + byte[] nonce = new byte[12]; + random.nextBytes(nonce); + AEADParameters parameters = new AEADParameters(new KeyParameter(K), 16 * 8, nonce); + + int[] lengths = { 0, 1, 15, 16, 17, 48, 63, 64, 65, 79, 80, 81, 127, 128, 129, 143, 144, 145, 300, 1000 }; + for (int i = 0; i < lengths.length; ++i) + { + byte[] P = new byte[lengths[i]]; + random.nextBytes(P); + + ChaCha20Poly1305 cipher = initCipher(true, parameters); + byte[] C = new byte[cipher.getOutputSize(P.length)]; + int len = cipher.processBytes(P, 0, P.length, C, 0); + cipher.doFinal(C, len); + + // pieces of one size, then pieces of random sizes with single bytes through processByte + for (int piece = 1; piece <= 2 * (64 + 16) + 1; ++piece) + { + checkPiecewiseDecryption(parameters, P, C, random, piece); + } + for (int j = 0; j < 50; ++j) + { + checkPiecewiseDecryption(parameters, P, C, random, 0); + } + } + } + + private void checkPiecewiseDecryption(AEADParameters parameters, byte[] P, byte[] C, SecureRandom random, + int piece) + throws InvalidCipherTextException + { + ChaCha20Poly1305 cipher = initCipher(false, parameters); + byte[] decP = new byte[cipher.getOutputSize(C.length)]; + + int len = 0; + for (int pos = 0; pos < C.length; ) + { + int n = Math.min(C.length - pos, piece > 0 ? piece : random.nextInt(3 * 64)); + int predicted = cipher.getUpdateOutputSize(n); + int written; + if (n == 1 && random.nextBoolean()) + { + written = cipher.processByte(C[pos], decP, len); + } + else + { + written = cipher.processBytes(C, pos, n, decP, len); + } + if (written != predicted) + { + fail("piecewise decryption reported incorrect update length"); + } + pos += n; + len += written; + } + len += cipher.doFinal(decP, len); + + if (len != P.length || !areEqual(P, decP)) + { + fail("incorrect piecewise decrypt"); + } + } + private void checkTestCase( ChaCha20Poly1305 encCipher, ChaCha20Poly1305 decCipher, diff --git a/docs/releasenotes.md b/docs/releasenotes.md index 3983bd59b6..39937a0de5 100644 --- a/docs/releasenotes.md +++ b/docs/releasenotes.md @@ -81,6 +81,8 @@ Date: 2026, TBD - The BCJSSE provider adds an org.bouncycastle.jsse.BCSSLContext interface exposing extended functionality of its SSLContext, obtained with org.bouncycastle.jsse.util.ContextUtil.getBCSSLContext() by way of the new BCSSLSessionContext interface the context's session contexts implement. Its getDefaultParameters(boolean) and getSupportedParameters(boolean) return the context's default and supported parameters as a BCSSLParameters for either client or server mode, including the BC-specific properties, where SSLContext.getDefaultSSLParameters() and getSupportedSSLParameters() report client mode only and cannot carry those properties. A BCSSLContext describes the initialization of the SSLContext it was obtained from, and is not updated if the SSLContext is re-initialized. +- ChaCha20Poly1305 - and with it XChaCha20Poly1305, the provider's ChaCha20-Poly1305 and XChaCha20-Poly1305 ciphers, HPKE and MLS - now copies the ciphertext into its decryption buffer with System.arraycopy, as much at a time as the buffer takes, where every byte used to be copied on its own. In a JMH comparison on an x86-64 machine decryption ran about 1.1 times as fast for 64 bytes and 1.2 times as fast for 1 KB to 16 KB on JDK 21 and 25, and 1.35 to 1.45 times as fast on JDK 17. The output is unchanged. + ### 2.1.4 Additional Notes - The sources and javadoc jars of the Ant-built distributions (jdk14, jdk15to18 and jdk13) no longer carry test material. Each module's javadoc target copies the package documentation it needs - org/bouncycastle//**/*.html - back into the module source directory that has already been compiled from, and zip-src zips that directory afterwards, so every test package's package.html arrived in the sources jar by that route; javadoc-util additionally copied org/bouncycastle/asn1/isismtt/**/*.java, which put test classes into the bcutil javadoc as generated pages, and javadoc-pg deliberately copied the gpg and bcpg test sources in order to document them. Separately the source copies excluded test material only one directory deep and only for *.java, because Ant reads ** as an any-depth wildcard just where it is a whole path segment, so anything nested further or with another extension - the PEM certificate fixtures under org/bouncycastle/est/test/san corrected in 1.86, and an ICAO master list under org/bouncycastle/asn1/icao/test - went through. The source and javadoc copies of every module now exclude test directories at any depth, and javadoc-pg no longer documents the test packages. org.bouncycastle.util.test is unaffected and still ships in the bcprov binary, sources and javadoc jars, as it does from the Gradle build: it is the SimpleTest framework the light-weight API's own test classes are written against, not test material of the distribution. No binary changes - the classes and resources of every Ant-built jar are identical to those of the 1.86 release - and the Gradle-built jdk18on artifacts never carried any of this.