Skip to content

Commit e330bcf

Browse files
committed
Make CipherOutputStream.close() idempotent in crypto.io and jcajce.io
1 parent 6d7d611 commit e330bcf

5 files changed

Lines changed: 111 additions & 0 deletions

File tree

‎core/src/main/java/org/bouncycastle/crypto/io/CipherOutputStream.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public class CipherOutputStream
2727

2828
private final byte[] oneByte = new byte[1];
2929
private byte[] buf;
30+
private boolean closed;
3031

3132
/**
3233
* Constructs a CipherOutputStream from an OutputStream and a
@@ -223,6 +224,12 @@ public void flush()
223224
public void close()
224225
throws IOException
225226
{
227+
if (closed)
228+
{
229+
return;
230+
}
231+
closed = true;
232+
226233
ensureCapacity(0, true);
227234
IOException error = null;
228235
try

‎core/src/test/java/org/bouncycastle/crypto/test/CipherStreamTest.java‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,54 @@ private void testReadWrite(Object cipher, CipherParameters params, boolean block
494494
}
495495
}
496496

497+
private void testDoubleClose()
498+
throws Exception
499+
{
500+
KeyParameter key = new KeyParameter(new byte[16]);
501+
502+
testDoubleClose("AES/CBC/PKCS7", new PaddedBufferedBlockCipher(CBCBlockCipher.newInstance(AESEngine.newInstance()), new PKCS7Padding()),
503+
new ParametersWithIV(key, new byte[16]));
504+
testDoubleClose("AES/EAX", new EAXBlockCipher(AESEngine.newInstance()), new ParametersWithIV(key, new byte[16]));
505+
testDoubleClose("AES/GCM", GCMBlockCipher.newInstance(AESEngine.newInstance()), new ParametersWithIV(key, new byte[12]));
506+
}
507+
508+
private void testDoubleClose(String label, Object cipher, CipherParameters params)
509+
throws Exception
510+
{
511+
byte[] data = new byte[33];
512+
513+
init(cipher, true, params);
514+
515+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
516+
OutputStream cOut = createCipherOutputStream(bOut, cipher);
517+
518+
cOut.write(data);
519+
cOut.close();
520+
521+
byte[] expected = bOut.toByteArray();
522+
523+
cOut.close();
524+
525+
if (!Arrays.areEqual(expected, bOut.toByteArray()))
526+
{
527+
fail("second close changed the output for " + label);
528+
}
529+
530+
init(cipher, false, params);
531+
532+
ByteArrayOutputStream pOut = new ByteArrayOutputStream();
533+
OutputStream dOut = createCipherOutputStream(pOut, cipher);
534+
535+
dOut.write(expected);
536+
dOut.close();
537+
dOut.close();
538+
539+
if (!Arrays.areEqual(data, pOut.toByteArray()))
540+
{
541+
fail("double closed decryption failed for " + label);
542+
}
543+
}
544+
497545
public void performTest()
498546
throws Exception
499547
{
@@ -503,6 +551,8 @@ public void performTest()
503551
this.streamSize = testSizes[i];
504552
performTests();
505553
}
554+
555+
testDoubleClose();
506556
}
507557

508558
private void performTests()

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Date: 2026, TBD
6363
- The ML-KEM KeyGenerator (KEMGenerateSpec/KEMExtractSpec), Cipher (wrap/unwrap), javax.crypto.KEM and KeyFactory.translateKey services accepted only BC's own ML-KEM key objects, and the KeyGenerator failed with a ClassCastException at generateKey() rather than at init, so an ML-KEM key from another provider could not be used with BC even though its standard encoding was one BC reads. This broke BCJSSE handshakes over the ML-KEM and hybrid groups whenever another provider ahead of BC decoded the peer's key or generated the ephemeral key pair. A foreign key is now converted from its X.509 or PKCS#8 encoding, with the usual parameter-set checks, and an unusable one is rejected at init (github #2466).
6464
- The raw JCA provider bounded the PBKDF2 iteration count taken from an encoding (org.bouncycastle.pbe.max_iteration_count, default 10,000,000) but not the counts of the legacy PBES1 (PKCS#5 scheme 1) and PKCS#12 PBE families beside it. Their AlgorithmParameters (PKCS12PBE and its OID aliases, PBKDF1) accepted any count, narrowing one beyond the int range with intValue() so that 2^32 arrived as 0, and every Cipher, Mac and SecretKeyFactory derivation ran with whatever count it was given - including a count decoded by another provider's AlgorithmParameters, as when javax.crypto.EncryptedPrivateKeyInfo.getKeySpec() decrypts a PKCS#12 PBE-protected key with BC. As these schemes carry the count in unauthenticated parameters and derive before anything can be checked, a supplied blob could hold a derivation for tens of minutes. The parameter parse now rejects a negative, beyond-int or over-limit count, and the derivations reject a negative or over-limit count, under the same property as PBKDF2. The PKCS#12 key store derives through the same code, so a org.bouncycastle.pkcs12.max_it_count raised above 10,000,000 now needs org.bouncycastle.pbe.max_iteration_count raised with it.
6565
- The light-weight CryptoProWrapEngine (RFC 4357 sec. 6.3) diversified the key encryption key in the caller's own array, so after init the KeyParameter it was given held the diversified key, and initialising again with the same parameters - to unwrap what had just been wrapped, say - diversified it a second time and used a different key. It also failed with a NullPointerException when given no S-box, although init has a branch for that case. It now diversifies a copy, and given no S-box uses the GOST 28147 engine's default S-box for the diversification, the one the wrap itself then uses. The provider's GOST 28147 key wrap ciphers were unaffected, as they always supply an S-box and build a new KeyParameter on every init.
66+
- Closing an org.bouncycastle.crypto.io.CipherOutputStream or org.bouncycastle.jcajce.io.CipherOutputStream a second time finalised the cipher again, although java.io.Closeable specifies that closing a stream that is already closed has no effect, and javax.crypto.CipherOutputStream does nothing on a repeated close(). The first close() leaves the cipher reset, so the second finalised it with no input and wrote the result after the ciphertext: one more padding block for a padded block cipher, one more tag for EAX, CCM, OCB and GCM-SIV. The output then failed to decrypt, or in ECB mode decrypted without error to different data. GCM, which may not be reused for encryption, made the second close() throw an IOException instead, as did writing the extra block or tag to a sink that refuses writes once closed, such as a FileOutputStream. In decrypt mode a padded or AEAD stream threw on the second close() after writing out the whole plaintext, an AEAD mode reporting invalid ciphertext for a message whose tag had already been verified. A try-with-resources block that also closed the stream itself, or that declared a wrapping stream such as a BufferedOutputStream as well, was enough to cause this. Both streams now record the first close() and return at once from any later call.
6667

6768
### 2.1.3 Additional Features and Functionality
6869

‎prov/src/main/java/org/bouncycastle/jcajce/io/CipherOutputStream.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class CipherOutputStream
3131
{
3232
private final Cipher cipher;
3333
private final byte[] oneByte = new byte[1];
34+
private boolean closed;
3435

3536
/**
3637
* Constructs a CipherOutputStream from an OutputStream and a Cipher.
@@ -108,6 +109,12 @@ public void flush()
108109
public void close()
109110
throws IOException
110111
{
112+
if (closed)
113+
{
114+
return;
115+
}
116+
closed = true;
117+
111118
IOException error = null;
112119
try
113120
{

‎prov/src/test/java/org/bouncycastle/jce/provider/test/CipherStreamTest2.java‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,44 @@ private static Key generateKey(String name)
463463
return kGen.generateKey();
464464
}
465465

466+
private void testDoubleClose(String name)
467+
throws Exception
468+
{
469+
Key key = generateKey(name);
470+
Cipher encrypt = Cipher.getInstance(name, "BC");
471+
Cipher decrypt = Cipher.getInstance(name, "BC");
472+
encrypt.init(Cipher.ENCRYPT_MODE, key);
473+
decrypt.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(encrypt.getIV()));
474+
475+
byte[] data = new byte[33];
476+
ByteArrayOutputStream bOut = new ByteArrayOutputStream();
477+
OutputStream cOut = new CipherOutputStream(bOut, encrypt);
478+
479+
cOut.write(data);
480+
cOut.close();
481+
482+
byte[] expected = bOut.toByteArray();
483+
484+
cOut.close();
485+
486+
if (!Arrays.areEqual(expected, bOut.toByteArray()))
487+
{
488+
fail("second close changed the output: " + name);
489+
}
490+
491+
ByteArrayOutputStream pOut = new ByteArrayOutputStream();
492+
OutputStream dOut = new CipherOutputStream(pOut, decrypt);
493+
494+
dOut.write(expected);
495+
dOut.close();
496+
dOut.close();
497+
498+
if (!Arrays.areEqual(data, pOut.toByteArray()))
499+
{
500+
fail("double closed decryption failed: " + name);
501+
}
502+
}
503+
466504
public void performTest()
467505
throws Exception
468506
{
@@ -472,6 +510,14 @@ public void performTest()
472510
this.streamSize = testSizes[i];
473511
performTests();
474512
}
513+
514+
testDoubleClose("AES/CBC/PKCS5Padding");
515+
testDoubleClose("AES/EAX/NoPadding");
516+
String jvm = System.getProperty("java.version");
517+
if (!(jvm.length() > 2 && (jvm.charAt(2) == '5' || jvm.charAt(2) == '6')))
518+
{
519+
testDoubleClose("AES/GCM/NoPadding");
520+
}
475521
}
476522

477523
private void performTests()

0 commit comments

Comments
 (0)