Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,57 @@ 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)
// A block is decrypted once MAC_SIZE more bytes follow it, since until then it may hold the tag. The
// blocks that start in the buffer are completed from 'in' one at a time; after that the input is
// block-aligned and every block the rule allows goes to the cipher as one run.
int pos = inOff, end = inOff + len;

while (bufPos > 0)
{
int need = BUF_SIZE - bufPos;
if (need > 0)
{
if (end - pos < need + MAC_SIZE)
{
break;
}
System.arraycopy(in, pos, buf, bufPos, need);
pos += need;
bufPos = BUF_SIZE;
}
else if (bufPos - BUF_SIZE + (end - pos) < MAC_SIZE)
{
break;
}

poly1305.update(buf, 0, BUF_SIZE);
processData(buf, 0, BUF_SIZE, out, outOff + resultLen);
resultLen += BUF_SIZE;
bufPos -= BUF_SIZE;
System.arraycopy(buf, BUF_SIZE, buf, 0, bufPos);
}

if (bufPos == 0 && end - pos - MAC_SIZE >= BUF_SIZE)
{
buf[bufPos] = in[inOff + i];
if (++bufPos == buf.length)
int run = wholeBlockRun(Math.min(end - pos - MAC_SIZE, out.length - (outOff + resultLen)));
if (run > 0)
{
poly1305.update(in, pos, run);
processData(in, pos, run, out, outOff + resultLen);
pos += run;
resultLen += run;
}
}

// what remains - the lookahead, a partial block, and any blocks wholeBlockRun held back - goes through
// the buffer, as much at a time as it takes
while (pos < end)
{
int n = Math.min(end - pos, buf.length - bufPos);
System.arraycopy(in, pos, buf, bufPos, n);
pos += n;
bufPos += n;
if (bufPos == buf.length)
{
poly1305.update(buf, 0, BUF_SIZE);
processData(buf, 0, BUF_SIZE, out, outOff + resultLen);
Expand Down Expand Up @@ -379,6 +426,20 @@ public int processBytes(byte[] in, int inOff, int len, byte[] out, int outOff) t
}
}

// From two blocks: a single block is cheaper through the loop below
if (len >= 2 * BUF_SIZE)
{
int run = wholeBlockRun(Math.min(len, out.length - (outOff + resultLen)));
if (run > 0)
{
processData(in, inOff, run, out, outOff + resultLen);
poly1305.update(out, outOff + resultLen, run);
inOff += run;
len -= run;
resultLen += run;
}
}

while (len >= BUF_SIZE)
{
processData(in, inOff, BUF_SIZE, out, outOff + resultLen);
Expand Down Expand Up @@ -552,6 +613,22 @@ private void finishData(int nextState)
this.state = nextState;
}

/*
* The length, a multiple of BUF_SIZE and at most maxLen, of a run of whole blocks that may go to the cipher and
* to Poly1305 in one call each - long runs are what let both process whole blocks straight from the arrays
* rather than one 64-byte call at a time. The last block before DATA_LIMIT is left to the one-block path, and
* maxLen is capped by the caller at the room left in the output, so what happens at either limit is unchanged.
*/
private int wholeBlockRun(int maxLen)
{
long headroom = DATA_LIMIT - BUF_SIZE - dataCount;
if (maxLen < BUF_SIZE || headroom < BUF_SIZE)
{
return 0;
}
return (int)Math.min(maxLen, headroom) & -BUF_SIZE;
}

private long incrementCount(long count, int increment, long limit)
{
if (Longs.compareUnsigned(count, limit - increment) > 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions docs/releasenotes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 hands runs of whole 64-byte blocks to ChaCha and to Poly1305 in one call each, where it used to alternate between them one block at a time and, when decrypting, copy every byte through its buffer. In a JMH comparison on an x86-64 machine decryption of 1 KB to 16 KB ran about 1.2 times as fast; together with the faster Salsa20Engine and Poly1305 block loops (github #2476, #2477), whose gains in this mode depend on it, encryption ran 1.5 times and decryption 1.8 to 2.0 times as fast. 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/<area>/**/*.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.
Expand Down