Safeguard
Best Practices

Getting AES right in Java: JCA/JCE mistakes that break your encryption

Call `Cipher.getInstance("AES")` in Java and you silently get ECB mode — no warning, no error, just plaintext patterns leaking through.

Safeguard Research Team
Research
7 min read

Ask the JCA for Cipher.getInstance("AES") and Java hands you AES/ECB/PKCS5Padding without so much as a compiler warning — a default that has been sitting in the platform since J2SE 1.4 shipped javax.crypto in 2002. ECB mode encrypts each 16-byte block independently, so identical plaintext blocks always produce identical ciphertext blocks; the resulting pattern leakage is well known enough to have a nickname, the "ECB penguin," after an image of Tux that remains recognizable after ECB encryption. CERT's Oracle Secure Coding standard flags this exact default under rule MSC61-J, and Android's static lint checker ships a dedicated GetInstance rule for the same reason. This is not a theoretical footgun — it is the single most common Java crypto mistake in code review, because the insecure path requires less typing than the secure one. This post walks through the three places Java symmetric encryption most often goes wrong: cipher mode selection, IV generation and handling for GCM versus CBC, and key derivation, closing with what NIST and OWASP currently recommend for each.

Why does Cipher.getInstance("AES") produce insecure output?

Because the JCA transformation string accepts a bare algorithm name and silently fills in a mode and padding when you don't specify one, and the SunJCE provider's fallback has always been ECB with PKCS5 padding. There is no runtime warning, no deprecation notice, and the code compiles and runs correctly against unit tests that only check round-trip decryption — a test suite comparing decrypt(encrypt(x)) to x will pass under ECB just as well as under a secure mode, which is exactly why the bug survives code review. The fix is to always write the full three-part transformation string, such as AES/GCM/NoPadding, so the mode is explicit and auditable by anyone reading the call site or grepping the codebase. Static analysis tools that flag Cipher.getInstance calls missing an explicit mode (rather than just flagging the string "ECB") catch this reliably, because the vulnerable pattern is an omission, not a keyword.

Which cipher mode should new Java code use by default?

AES/GCM/NoPadding should be the default for new code, because Galois/Counter Mode is an authenticated encryption mode — it provides confidentiality and integrity in a single pass, so tampered ciphertext fails to decrypt instead of silently producing corrupted plaintext. NIST Special Publication 800-38D, which standardizes GCM, specifies a 96-bit (12-byte) IV as the recommended size for performance and security, and a 128-bit authentication tag for general-purpose use, with shorter tags (down to 32 bits) permitted only in specifically justified, constrained scenarios. Java's SunJCE provider follows this: when you don't supply a GCMParameterSpec, Cipher.init() with ENCRYPT_MODE generates a 12-byte IV via SecureRandom internally and defaults to a 128-bit tag, matching the NIST recommendation out of the box. The older alternative, AES/CBC/PKCS5Padding, is still common in legacy code but is vulnerable to padding-oracle attacks when an application distinguishes padding-invalid errors from other decryption failures, and it provides no built-in integrity check at all — a separate HMAC must be added correctly, in the right order (encrypt-then-MAC), to reach parity with what GCM gives you natively.

What goes wrong with IV handling specifically?

The most damaging mistake is reusing a nonce under the same GCM key: NIST 800-38D's construction relies on the (key, IV) pair never repeating, and a single repeat lets an attacker recover the GCM authentication subkey and forge arbitrary ciphertexts that pass integrity checks. In practice this happens in Java when developers hardcode a GCMParameterSpec with a fixed byte array "for reproducibility," reuse a static Cipher instance across requests without re-initializing the IV, or derive the IV from a non-random counter that resets on process restart. The correct pattern is to let Cipher.init() generate the IV via SecureRandom on every encryption call, retrieve it afterward with cipher.getIV(), and prepend it to the ciphertext before storage or transmission — GCM IVs are not secret and are safe to store alongside the ciphertext. CBC mode has a related but less catastrophic failure mode: a predictable or reused CBC IV leaks whether two messages share a common leading plaintext block, which is weaker than full key recovery but has still been used as a practical attack primitive against session tokens and structured data formats.

What are the most common Java key-management mistakes?

The most common mistake is embedding the AES key as a string literal in source code — SecretKeySpec key = new SecretKeySpec("MySecretKey12345".getBytes(), "AES") is a pattern that shows up repeatedly in real codebases and is trivially recovered by anyone who can read the compiled class file or decompile the JAR. A close second is deriving a key by running a password through a single round of a general-purpose hash like SHA-256 instead of a purpose-built key-derivation function; a plain hash has no configurable work factor, so an attacker with a leaked ciphertext can brute-force passwords at full hash speed. Java's built-in PBEKeySpec with PBKDF2WithHmacSHA1 and a low iteration count — 1,000 iterations was a common default in older sample code — is now considered weak; OWASP's Password Storage Cheat Sheet guidance as of its 2023 revision recommends at least 600,000 iterations for PBKDF2-HMAC-SHA256 (or fewer iterations if using a memory-hard alternative like Argon2 via a third-party library). Finally, key and IV generation must use java.security.SecureRandom, never java.util.Random, since the latter is a deterministic linear congruential generator with a seed space small enough to be exhaustively searched.

How should keys be generated and stored instead?

Keys should be generated with KeyGenerator.getInstance("AES") initialized to 256 bits and backed by SecureRandom, never typed by hand or derived ad hoc from a password unless a password is genuinely the only available secret. When a password-derived key is unavoidable — for example, encrypting a local file with a user-supplied passphrase — SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") with a random 16-byte-or-larger salt and an iteration count in the hundreds of thousands is the JCA-native option, though Argon2id via a library such as Bouncy Castle is stronger against GPU-based cracking for the same reason it was chosen as the Password Hashing Competition winner in 2015. For production systems, the better answer is usually to avoid deriving or hardcoding keys in application code entirely and instead retrieve them at runtime from a managed key store or KMS — AWS KMS, Google Cloud KMS, HashiCorp Vault, or a hardware security module — so the raw key material never exists in source control, environment variables, or a JVM heap dump that an attacker might obtain.

Are there Java-specific gotchas beyond the algorithm choice?

Yes — two recurring ones are the historical Java Cryptography Extension (JCE) "unlimited strength" jurisdiction policy files and provider inconsistency across JDK versions. Before JDK 8u151 (2017), Oracle's JCE shipped with export-control restrictions that capped AES keys at 128 bits unless developers manually installed separate "Unlimited Strength Jurisdiction Policy" JAR files; forgetting this step caused 256-bit AES to throw InvalidKeyException: Illegal key size at runtime in production while working fine in developers' machines that had already installed the policy files. JDK 8u151 and all versions of JDK 9 onward ship with unlimited strength enabled by default, but code that still targets older JDK 8 patch levels can silently hit this wall. Separately, the exact set of supported transformation strings and default parameter choices is provider-dependent — SunJCE's defaults are not guaranteed to match a third-party provider like Bouncy Castle's, so code that omits explicit AlgorithmParameterSpec objects and relies on provider defaults can behave differently when the provider changes, which is one more reason to make every parameter — mode, padding, IV length, tag length — explicit in code rather than implicit in configuration.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.