Use AES/GCM instead of AES/CBC for json-crypto session encryption#277
Conversation
CodeQL java/weak-cryptographic-algorithm flagged the hardcoded AES/CBC/PKCS5Padding session cipher in SimpleEncryptor.asymmetric() as vulnerable to padding-oracle attacks. Switch it to authenticated AES/GCM/NoPadding: a fresh session key is generated per message, so the random 96-bit nonce is never reused under the same key. Make SimpleDecryptor GCM-aware — it selects a GCMParameterSpec for GCM transformations and keeps IvParameterSpec for CBC and other modes. The self-describing "cipher" field means existing CBC/ECB values still decrypt unchanged. Add a GCM round-trip test alongside the existing CBC coverage.
maximthomas
left a comment
There was a problem hiding this comment.
Right direction — GCM is the correct cipher, and driving the decryptor's parameter spec off the self-describing cipher field is the right mechanism. Verified backward compat holds for both legacy ECB and CBC values. Three asks, none blocking.
1. Encryptor trusts a provider default the decryptor hardcodes
SimpleDecryptor.java:46 pins GCM_TAG_LENGTH_BITS = 128, but SimpleEncryptor.java:117 calls init(ENCRYPT_MODE, sessionKey) with no GCMParameterSpec — the 12-byte nonce / 128-bit tag is a SunJCE convention, not a JCE guarantee, and the tag length isn't persisted. Under a provider with a different default (BouncyCastle preferred, PKCS#11/HSM, FIPS), every value fails as AEADBadTagException: Tag mismatch! — indistinguishable from real tampering. Pass the spec explicitly:
byte[] nonce = new byte[GCM_NONCE_LENGTH_BYTES]; // 12
random.nextBytes(nonce);
symmetric.init(Cipher.ENCRYPT_MODE, sessionKey, new GCMParameterSpec(GCM_TAG_LENGTH_BITS, nonce));2. No test covers the changed line
The only behavioural change is symmetricCipher at SimpleEncryptor.java:112, inside asymmetric(). The new test passes a SecretKey, so encrypt() dispatches to symmetric() — unchanged code. Reverting line 112 to CBC leaves the suite green. Add to testAsymmetricEncryption:
assertThat(((Map<?, ?>) value.getObject()).get("cipher")).isEqualTo(SYMMETRIC_GCM_CIPHER);A checked-in legacy ECB payload would also turn the backward-compat claim into something CI enforces — currently every test encrypts and decrypts with the same build.
3. Comment overstates its scope
SimpleEncryptor.java:107-109 ("fresh session key per message, so the nonce is never reused") holds for asymmetric() only. The new test also blesses GCM for symmetric(), where the key is caller-supplied and long-lived — random 96-bit nonces there are bounded by NIST SP 800-38D §8.3 (2^32 invocations/key). Scope the comment to asymmetric().
- Pass GCMParameterSpec (96-bit nonce, 128-bit tag) explicitly instead of relying on provider-specific defaults, in both the session-key and the caller-supplied-key paths, matching the tag length SimpleDecryptor expects - Assert the asymmetric envelope uses AES/GCM/NoPadding so reverting the cipher switch fails the suite - Add checked-in AES/ECB and AES/CBC fixtures with a fixed key so legacy decryption is enforced by CI, not just same-build roundtrips - Document the NIST SP 800-38D 2^32-invocations bound for GCM under long-lived caller-supplied keys
|
@maximthomas Thanks for the thorough review — all three points addressed in 3c16364. 1. Explicit GCM parameters. 2. Coverage of the changed line. 3. Comment scope. The "fresh session key per message" comment lives inside Module suite: 7 tests, 0 failures. |
Summary
Fixes CodeQL high alert
java/weak-cryptographic-algorithmincommons/json-crypto.SimpleEncryptor.asymmetric()hardcodedAES/CBC/PKCS5Paddingfor the per-message session key, which CodeQL flags as vulnerable to padding-oracle attacks.Changes
SimpleEncryptor— the session cipher is nowAES/GCM/NoPadding(authenticated encryption). A fresh session key is generated per message, so the randomly generated 96-bit nonce is never reused under the same key.SimpleDecryptor— made GCM-aware. GCM needs aGCMParameterSpec, whereas the old code always built anIvParameterSpec(which throws for GCM). The mode is read from the self-describingcipherfield, so GCM usesGCMParameterSpec(128, nonce)and CBC/other modes keepIvParameterSpec.Verification
mvn clean teston thejson-cryptomodule: core 5/5, cli 1/1, all green.