Skip to content

Commit c576fe9

Browse files
committed
OpenPGP: add PGPEncryptedDataList.extractSessionKeyEncryptedData(boolean) so a session key the caller recovered from a password is quick checked again, and use it on the high-level API's passphrase paths, relates to github #2459.
1 parent 94270ff commit c576fe9

11 files changed

Lines changed: 235 additions & 30 deletions

File tree

‎CONTRIBUTORS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ We also wish to acknowledge financial and collaborative support from [CISCO](htt
388388
- Adam Vartanian \<https://github.com/flooey\> use of ShortBuffer exception and buffer size pre-check in Cipher.doFinal().
389389
- Bernd \<https://github.com/ecki\> Fix to make PGPUtil.pipeFileContents use buffer and not leak file handle.
390390
- Shartung \<https://github.com/shartung\> Additional EC Key Agreement algorithms in support of German BSI TR-03111.
391-
- Paul Schaub \<https://github.com/vanitasvitae\> bringing PGPSecretKey.getUserIds() into line with PGPPublicKey.getUserIds(). Exception message fix in BcPublicKeyDataDecryptorFactory. Additional tests on PGP key ring generation. Improved functionality of PGPSignatureSubpacketGenerator, PGPPublicKeyRing. Tweaks to PGPDataEncryptorBuilder interface, fix for JcaPGP/BcPGP Ed25519 private key conversion. Added configurable CRC detection to ArmoredInputStream, additional control character skipping in ArmoredInputStream. Rewind code for PGPPBEEncryptedData, addition of PGPSignature.getDigestPrefix(). Wrong list traversal fix in PGPSecretKeyRing. Further improvement to use of generics in PGP API. General interop improvements. PGP Public / Secure keyring ignore marker packets when reading. Initial work on PGP session key handling, filtering literal data for canoncialization. Addition of direct key identified key-ring construction. PGPSecretKeyRing.insertOrReplacePublicKey addition. Addition of utility methods for joining/merging signatures and public keys. Addition of PGP regexp packet, PolicyURI packet handling, UTF8 comment testing. Efficiency improvements to TruncatedStream. Initial Argon2 support for OpenPGP. General cleanups. Fast CRC24 implementation, SHA3 addtions to BcImplProvider, improvements to One Pass Signature support, signatue validation, read() consistency in BCPGInputStream. Contributions to AEAD support (v6 & v5) in PGP API. Addition of PGP WildCard ID, moving the PGP example code into the 21st century. Security patches for encrypted data generation, initial thread safe certification verification. Support for V6 EC keys, V6 signatures, V6 encryption, V6 PKESK, PGP packet criticality, and Preferred AEAD CipherSuites sigsubpacket support. Introduce high-level OpenPGP API for message creation/consumption and certificate evaluation. OpenPGP fuzz testing. Fix to prevent a null pointer exception on processing a partial stripped key. Moving the Argon2 memory size exponent bounds check from S2K packet parsing to decryption time. ArmoredInputStream CSF dash-escape hardening. Report and initial patch for OnePassSignaturePacket defaulting to the Legacy packet format for v6 packets (github #2347). PGPKeyPairGenerator factory methods for the OpenPGP brainpool curves (github #2375). Support for OpenPGP External Secret Keys, and the initial OpenPGP smart card API (bcpgsc) with YubiKey and simulator backends (github #2339). Initial implementation of the JCE bindings for smart card decryption and the pluggable YubiKey decryptor factory provider (github #2374). Correcting MessageEncryptionMechanism.unencrypted() to report no encryption mode rather than SEIPDv1. Initial implementation of OpenPGP smart card signature support: the low-level sign and decrypt operations moved onto OpenPGPSmartCard so a backend can be emulated in software, external-key detection in the document signature generators, a pluggable PGPContentSignerBuilderProviderFactory, the generalised smart-card decryptor factories and the key conversion helpers (PR #2430).
391+
- Paul Schaub \<https://github.com/vanitasvitae\> bringing PGPSecretKey.getUserIds() into line with PGPPublicKey.getUserIds(). Exception message fix in BcPublicKeyDataDecryptorFactory. Additional tests on PGP key ring generation. Improved functionality of PGPSignatureSubpacketGenerator, PGPPublicKeyRing. Tweaks to PGPDataEncryptorBuilder interface, fix for JcaPGP/BcPGP Ed25519 private key conversion. Added configurable CRC detection to ArmoredInputStream, additional control character skipping in ArmoredInputStream. Rewind code for PGPPBEEncryptedData, addition of PGPSignature.getDigestPrefix(). Wrong list traversal fix in PGPSecretKeyRing. Further improvement to use of generics in PGP API. General interop improvements. PGP Public / Secure keyring ignore marker packets when reading. Initial work on PGP session key handling, filtering literal data for canoncialization. Addition of direct key identified key-ring construction. PGPSecretKeyRing.insertOrReplacePublicKey addition. Addition of utility methods for joining/merging signatures and public keys. Addition of PGP regexp packet, PolicyURI packet handling, UTF8 comment testing. Efficiency improvements to TruncatedStream. Initial Argon2 support for OpenPGP. General cleanups. Fast CRC24 implementation, SHA3 addtions to BcImplProvider, improvements to One Pass Signature support, signatue validation, read() consistency in BCPGInputStream. Contributions to AEAD support (v6 & v5) in PGP API. Addition of PGP WildCard ID, moving the PGP example code into the 21st century. Security patches for encrypted data generation, initial thread safe certification verification. Support for V6 EC keys, V6 signatures, V6 encryption, V6 PKESK, PGP packet criticality, and Preferred AEAD CipherSuites sigsubpacket support. Introduce high-level OpenPGP API for message creation/consumption and certificate evaluation. OpenPGP fuzz testing. Fix to prevent a null pointer exception on processing a partial stripped key. Moving the Argon2 memory size exponent bounds check from S2K packet parsing to decryption time. ArmoredInputStream CSF dash-escape hardening. Report and initial patch for OnePassSignaturePacket defaulting to the Legacy packet format for v6 packets (github #2347). PGPKeyPairGenerator factory methods for the OpenPGP brainpool curves (github #2375). Support for OpenPGP External Secret Keys, and the initial OpenPGP smart card API (bcpgsc) with YubiKey and simulator backends (github #2339). Initial implementation of the JCE bindings for smart card decryption and the pluggable YubiKey decryptor factory provider (github #2374). Correcting MessageEncryptionMechanism.unencrypted() to report no encryption mode rather than SEIPDv1. Initial implementation of OpenPGP smart card signature support: the low-level sign and decrypt operations moved onto OpenPGPSmartCard so a backend can be emulated in software, external-key detection in the document signature generators, a pluggable PGPContentSignerBuilderProviderFactory, the generalised smart-card decryptor factories and the key conversion helpers (PR #2430). Report and initial patch for two-step session key decryption losing the SEIPD v1 wrong-passphrase check (PR #2461).
392392
- Nick of Nexxar \<https://github.com/nros\> update to OpenPGP package to handle a broader range of EC curves.
393393
- catbref \<https://github.com/catbref\> sample implementation of RFC 7748/Ed25519 (incorporated work from github users Valodim and str4d as well).
394394
- gerlion \<https://github.com/gerlion\> detection of concurrency issue with pre-1.60 EC math library.

‎docs/releasenotes.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Date: 2026, TBD
3838
- A custom Argon2BytesGenerator.BlockPool was left to zeroise the blocks it recycled itself, and had no way to know how many blocks to hold: the generator returned each block to the pool with the password-derived data still in it, so only the FixedBlockPool BC ships cleared them, and sizing any other pool meant replicating the internal memory alignment and the block count of the fill step. The generator now clears every block before it goes back, so a pool neither has to clear nor can observe that data, and Argon2BytesGenerator.getBlockCount(memory, lanes) gives the number of blocks a run takes - which the default pool now uses, so it no longer discards and reallocates the four blocks of the fill step on every call. FixedBlockPool drops the two clears it no longer needs, leaving one zeroisation per block per use rather than two, and a generateBytes() that fails part way through now returns and clears the blocks it took, along with its own working buffer, rather than leaving both to the garbage collector (github #2452).
3939
- The PKCS#12 key stores wrote the MAC key-derivation parameters of a file they had loaded into every file they wrote afterwards, under whatever password the caller stored with. For PKCS12-PBMAC1 that carried the loaded file's PBKDF2 salt, iteration count, key length and PRF, because the parameters were minted only when the store held none and were then assigned back, so the branch ran once per store object rather than once per write - which also meant one store reused a single PBKDF2 salt across every write, including writes under different passwords, with no file loaded at all. The classic store inherited the MAC salt length and digest algorithm the same way, so a file declaring a zero-length MAC salt was re-stored with one, and a file it had loaded under RFC 9579 handed on that file's PBKDF2 salt too, both stores reading PBMAC1. The values were also latched before the MAC was verified and were not cleared by the load(null, null) a caller must issue to recover, so a file that failed the check left them behind for the caller's own file. The PBKDF2 salt and the MAC salt are now generated for every write, the MAC salt at no fewer than 8 octets, nothing is latched until the file has verified, and an AlgorithmIdentifier supplied through a PKCS12StoreParameter is still written as it was given. A loaded file's PRF, key length, digest algorithm and MacData iteration count are still kept, the last as before; the PBKDF2 count is kept where it is at least the count being written with and raised to it otherwise, since the file being re-stored is not the one that count was chosen for - RFC 9579's own test vectors ask for 2048. That count is now org.bouncycastle.pkcs12.pbkdf2_it_count, default 65,536, the write-side counterpart for a PBMAC1 MAC of what org.bouncycastle.pkcs12.store_it_count is for the PBE. Reading is unaffected: a file's MAC is verified with the parameters it carries, whatever they are (github #2450).
4040
- Cipher.SM9 took its data-encapsulation mode for decryption from the ciphertext rather than from the mode the Cipher was configured with. The GM/T 0080-2020 SM9Cipher structure names the mode in an enType field, but GM/T 0044.4 defines the authenticator as C3 = MAC(K2, C2), over the encapsulated message alone, so enType is not covered by it: re-encoding a ciphertext with the other enType leaves C1, C3 and C2 untouched and steers the recipient into the other mode. GM/T 0044.4 takes K1 and K2 from a single KDF output of klen = mlen + K2_len bits in stream mode and K1_len + K2_len bits in SM4 mode, where K1_len = 128, so when C2 is 16 bytes long the two modes make the identical KDF call and derive the same K1 and K2: a one-block SM4 ciphertext relabelled as stream mode passes the MAC check and the recipient returns K1 xor C2 - from which both the SM4 key K1 and the padded plaintext block follow, wherever that output is observable. CipherSpi now decrypts in the configured mode and rejects a ciphertext whose enType disagrees with it, so the mode is symmetric between encryption and decryption. That check compares two values a relabelling attacker can make agree, and so does not by itself protect a recipient whose Cipher is configured for stream mode - the relabelled one-block ciphertext then matches the configuration - so SM9Engine additionally refuses a 16-byte C2, the one C2 length at which the two modes collide, in both modes and both directions: on decryption, and on encryption a 16-byte message in stream mode and a message of fewer than 16 bytes, which pads to one block, in SM4 mode. Refusing the length on decryption protects the recipient that does so, but the message a relabelled ciphertext gives away is the SM4-mode sender's, who cannot tell whether the recipient's implementation refuses it, which is why the SM4 mode no longer produces one; messages of every other length are unchanged in both modes. A stream-mode ciphertext must accordingly be decrypted through a stream-mode Cipher ("SM9/XOR/NoPadding") rather than the SM4-mode default that Cipher.getInstance("SM9") gives; a message of fewer than 16 bytes has to be sent in stream mode, and one of exactly 16 bytes - a 128-bit key, say - in SM4 mode; and a ciphertext made by an earlier version whose C2 is 16 bytes long is no longer decrypted, whichever mode wrote it - a one-block SM4-mode ciphertext, or a stream-mode one carrying a 16-byte message. The SM9 KEM is unaffected.
41+
- Decrypting an OpenPGP message in two steps - recovering the session key from a SKESK packet and then decrypting the SEIPD v1 body through PGPEncryptedDataList.extractSessionKeyEncryptedData() - stopped detecting a wrong passphrase. 1.86 suppressed the legacy CFB "quick check" on the two repeated prefix bytes for every session-key decryption, to close the Mister-Zuccherato oracle on the path a PKESK session key reaches, but the same class also carries password-derived session keys, where reporting the check is what identifies a wrong passphrase and lets the next passphrase or SKESK packet be tried. A SKESK v4 packet deriving the session key from the S2K output directly (no encrypted session key) yields a well formed session key for any passphrase, so a wrong one no longer failed at all: it surfaced as a parse or integrity failure further down the stream. BouncyCastle's own high-level API decrypts this way, so OpenPGPMessageProcessor took the first wrong passphrase offered for a success and never tried the remaining ones. A new PGPEncryptedDataList.extractSessionKeyEncryptedData(boolean) states whether the session key came from a password: true restores the check and with it the PGPDataValidationException on a wrong passphrase, the existing no-argument method goes on suppressing it, and the high-level API passes true on its passphrase paths alone, so a session key recovered from a public key operation is still never quick checked (github #2459).
4142

4243
### 2.1.3 Additional Features and Functionality
4344

‎pg/src/main/java/org/bouncycastle/openpgp/PGPEncryptedData.java‎

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ public int getAlgorithm()
276276
}
277277

278278
boolean processSymmetricEncIntegrityPacketDataStream(boolean withIntegrityPacket, PGPDataDecryptor dataDecryptor,
279-
InputStream encIn, boolean publicKeyEncrypted)
279+
InputStream encIn, boolean performQuickCheck)
280280
throws IOException
281281
{
282282
encStream = dataDecryptor.getInputStream(encIn);
@@ -311,20 +311,12 @@ boolean processSymmetricEncIntegrityPacketDataStream(boolean withIntegrityPacket
311311
throw new EOFException("unexpected end of stream.");
312312
}
313313

314-
// For a public-key / session-key decryption there is no multi-SKESK passphrase retry to
315-
// drive, so the CFB "quick check" on the two repeated prefix bytes serves no purpose here and
316-
// only re-creates the Mister-Zuccherato oracle (the reason PGPPublicKeyEncryptedData suppresses
317-
// it). Consume the check bytes (done above) but do not signal a mismatch; for SEIPD v1 the MDC
318-
// is the integrity check. This path is reached for PKESK session keys via the high-level API.
319-
if (publicKeyEncrypted)
314+
// The check bytes are consumed either way; signalling a mismatch is what makes the CFB quick check the Mister-Zuccherato oracle, so it is done only where PGPSymmetricKeyEncryptedData.useQuickCheck() allows it. For SEIPD v1 the MDC is the integrity check.
315+
if (!performQuickCheck)
320316
{
321317
return false;
322318
}
323319

324-
// Note: the oracle attack on "quick check" bytes is not deemed a security risk for PBE; the
325-
// quick check is retained on the PBE path because its failure + stream reset is what lets the
326-
// decryptor detect a wrong passphrase and rewind to try the next SKESK packet.
327-
328320
boolean repeatCheckPassed = iv[iv.length - 2] == (byte)v1
329321
&& iv[iv.length - 1] == (byte)v2;
330322

‎pg/src/main/java/org/bouncycastle/openpgp/PGPEncryptedDataList.java‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,4 +202,32 @@ public PGPSessionKeyEncryptedData extractSessionKeyEncryptedData()
202202
{
203203
return new PGPSessionKeyEncryptedData(data);
204204
}
205+
206+
/**
207+
* Create a decryption method using a {@link PGPSessionKey}, stating whether the session key was recovered
208+
* from a password. This method can be used to decrypt messages which do not contain a SKESK or PKESK packet
209+
* using a session key.
210+
* <p>
211+
* A session key recovered from a SKESK packet with the wrong passphrase is a well formed key which simply
212+
* decrypts to garbage, and on a SEIPD v1 (or SED) packet the legacy CFB "quick check" on the two repeated
213+
* prefix bytes is what detects that - so passing true here makes the wrong passphrase surface as a
214+
* {@link PGPDataValidationException} from {@link PGPSessionKeyEncryptedData#getDataStream(org.bouncycastle.openpgp.operator.SessionKeyDataDecryptorFactory)},
215+
* as it does when the same packet is decrypted in one step through {@link PGPPBEEncryptedData}, rather than
216+
* as a parse failure further down the stream.
217+
* </p><p>
218+
* It must be passed true only for a session key that was recovered from a password. Reporting the quick
219+
* check for a session key that came from a public key operation - one recovered from a PKESK packet, or one
220+
* held from an earlier decryption - re-creates the Mister-Zuccherato oracle on the CFB prefix, which is why
221+
* {@link #extractSessionKeyEncryptedData()} never reports it. A SEIPD v2 (AEAD) packet carries no such
222+
* check and is unaffected either way.
223+
* </p>
224+
*
225+
* @param passwordDerivedSessionKey true if the session key was recovered from a password (a SKESK packet),
226+
* false if it came from a public key operation or from anywhere else.
227+
* @return session key encrypted data
228+
*/
229+
public PGPSessionKeyEncryptedData extractSessionKeyEncryptedData(boolean passwordDerivedSessionKey)
230+
{
231+
return new PGPSessionKeyEncryptedData(data, passwordDerivedSessionKey);
232+
}
205233
}

‎pg/src/main/java/org/bouncycastle/openpgp/PGPPublicKeyEncryptedData.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ private InputStream getDataStream(
234234
{
235235
PGPDataDecryptor dataDecryptor = dataDecryptorFactory.createDataDecryptor(true, sessionKey.getAlgorithm(), sessionKey.getKey());
236236

237-
processSymmetricEncIntegrityPacketDataStream(true, dataDecryptor, encIn, true);
237+
processSymmetricEncIntegrityPacketDataStream(true, dataDecryptor, encIn, false);
238238
}
239239
// SEIPD v2 (OpenPGP v6 AEAD)
240240
else
@@ -249,7 +249,7 @@ private InputStream getDataStream(
249249
{
250250
PGPDataDecryptor dataDecryptor = dataDecryptorFactory.createDataDecryptor(false, sessionKey.getAlgorithm(), sessionKey.getKey());
251251

252-
processSymmetricEncIntegrityPacketDataStream(false, dataDecryptor, encIn, true);
252+
processSymmetricEncIntegrityPacketDataStream(false, dataDecryptor, encIn, false);
253253
}
254254

255255
//

‎pg/src/main/java/org/bouncycastle/openpgp/PGPSessionKeyEncryptedData.java‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,18 @@
1313
public class PGPSessionKeyEncryptedData
1414
extends PGPSymmetricKeyEncryptedData
1515
{
16+
private final boolean passwordDerivedSessionKey;
17+
1618
PGPSessionKeyEncryptedData(InputStreamPacket encData)
19+
{
20+
this(encData, false);
21+
}
22+
23+
PGPSessionKeyEncryptedData(InputStreamPacket encData, boolean passwordDerivedSessionKey)
1724
{
1825
super(encData);
26+
27+
this.passwordDerivedSessionKey = passwordDerivedSessionKey;
1928
}
2029

2130
@Override
@@ -63,12 +72,10 @@ public InputStream getDataStream(
6372
return encStream;
6473
}
6574

66-
// Decryption from an already-recovered session key (including PKESK/public-key via the high-level
67-
// API): no multi-SKESK passphrase retry, so the CFB quick check is suppressed to avoid the
68-
// Mister-Zuccherato oracle. Integrity is enforced by the SEIPD v1 MDC.
75+
// Only a session key the caller states came from a password may be quick checked - for one recovered from a public key operation the check is the Mister-Zuccherato oracle, and integrity is enforced by the SEIPD v1 MDC.
6976
@Override
70-
protected boolean isPublicKeyEncrypted()
77+
protected boolean useQuickCheck()
7178
{
72-
return true;
79+
return passwordDerivedSessionKey;
7380
}
7481
}

0 commit comments

Comments
 (0)