Safeguard
AppSec

Encryption Algorithms in Java: A Practical Overview

Java ships a wide menu of encryption algorithms through its Java Cryptography Architecture, but picking the wrong mode or a deprecated cipher is one of the most common security findings in Java codebases.

Safeguard Research Team
Research
6 min read

Java encryption algorithms are exposed through the Java Cryptography Architecture (JCA) and its provider framework, giving developers access to symmetric ciphers like AES, asymmetric ciphers like RSA, and hashing functions like SHA-256 — but the JCA's flexibility is also its biggest risk, since it happily lets you instantiate an insecure cipher mode with the same one-line API call as a secure one. Most Java cryptography incidents aren't caused by broken math; they're caused by developers picking ECB mode over GCM, using java.util.Random where SecureRandom was needed, or sticking with MD5 and SHA-1 because old tutorials still reference them. This overview covers what to actually use.

What encryption algorithms does Java support out of the box?

The JCA and its provider ecosystem (the default SunJCE provider, plus optional providers like Bouncy Castle) support the standard modern algorithm set: AES for symmetric encryption, RSA and elliptic-curve algorithms (ECDSA, ECDH) for asymmetric encryption and key exchange, and SHA-2/SHA-3 family functions for hashing. Java also still exposes legacy algorithms — DES, RC4, MD5, SHA-1 — for backward compatibility, and this is precisely where things go wrong: the API doesn't stop you from calling Cipher.getInstance("DES"), it just quietly gives you a cipher that's been considered broken for decades. Since JDK 8u versions and more aggressively from JDK 17 onward, the JDK's default security policy has started disabling some legacy algorithms by default via the jdk.tls.disabledAlgorithms and jdk.certpath.disabledAlgorithms security properties, but explicit application code that calls a legacy algorithm by name will generally still work unless a provider actively blocks it.

Which AES mode should you actually use in Java?

Use AES in GCM mode (AES/GCM/NoPadding), not ECB and not unauthenticated CBC. AES-GCM is an authenticated encryption mode, meaning it provides both confidentiality and integrity in one operation — a tampered ciphertext fails to decrypt rather than silently producing corrupted plaintext. AES in ECB mode, by contrast, encrypts each block independently with no randomization, so identical plaintext blocks produce identical ciphertext blocks — the well-known "ECB penguin" image (where an encrypted bitmap still visibly shows the penguin outline) illustrates exactly why this leaks structural information about the plaintext. AES-CBC without a separate authentication step (like HMAC) protects confidentiality but not integrity, leaving it open to padding-oracle style attacks if implemented carelessly. In Java specifically:

  • Cipher.getInstance("AES/GCM/NoPadding") — the recommended default for new code.
  • Cipher.getInstance("AES/ECB/PKCS5Padding") — avoid; the mode itself is the vulnerability, not just the padding.
  • A unique, randomly generated IV/nonce is required per encryption operation under GCM; reusing a nonce with the same key catastrophically breaks GCM's security guarantees.

What's wrong with using java.util.Random for cryptographic purposes?

java.util.Random is a deterministic pseudo-random number generator seeded in a way that's predictable enough for simulations and games but not for anything security-sensitive — tokens, session identifiers, encryption keys, or nonces generated with it can, in principle, be predicted by an attacker who knows or can guess the seed. java.security.SecureRandom is the cryptographically secure alternative, pulling entropy from the OS (/dev/urandom on Linux, CryptGenRandom-family APIs on Windows) rather than a simple linear algorithm. This is one of the most common findings static analysis tools flag in Java codebases, precisely because both classes have nearly identical APIs (.nextInt(), .nextBytes()) and it's easy for a developer to reach for the wrong one out of habit, especially when copying code from a non-security context.

What about RSA and hashing — what should you avoid there?

For RSA, avoid PKCS#1 v1.5 padding for encryption in new code (it's vulnerable to padding-oracle attacks, notably the Bleichenbacher attack class) and prefer OAEP padding (RSA/ECB/OAEPWithSHA-256AndMGF1Padding — note "ECB" here refers to RSA's block handling, not a symmetric mode, and is not a vulnerability in this context). For hashing, MD5 and SHA-1 are both cryptographically broken for collision resistance — practical collision attacks against both have been publicly demonstrated — and should never be used for anything security-relevant like password storage, digital signatures, or integrity verification; SHA-256 or SHA-3 are the current baseline. For password storage specifically, don't use a general-purpose hash function at all, even a modern one — use a dedicated password-hashing algorithm like bcrypt, scrypt, or Argon2, all available in Java through libraries like Bouncy Castle or dedicated wrappers, because these are deliberately slow and memory-hard in ways SHA-256 alone isn't, which is exactly what makes brute-forcing a stolen password database impractical.

How do you catch cryptographic misuse before it ships?

Manual code review catches some of this, but cryptographic misuse is a pattern-matching problem that static analysis tools are well suited to automate: flagging any call to Cipher.getInstance with ECB mode or a legacy algorithm name, any use of java.util.Random in a class that also handles authentication tokens, and any hardcoded key or IV literal in source. Because these are syntactic patterns rather than deep logic bugs, they're cheap to check in every pull request rather than relying on a security team catching them in periodic manual audits.

How Safeguard Helps

Safeguard's static analysis flags exactly these patterns — legacy algorithms, insecure cipher modes, weak random number generation, and hardcoded cryptographic material — directly in pull requests across Java codebases, so a misconfigured Cipher.getInstance call gets caught before merge rather than in a later security audit. See the SAST/DAST product page for how this static coverage extends across languages beyond Java.

FAQ

Is AES-256 better than AES-128 in practice?

Both are considered secure against realistic attacks today; AES-256 has a larger security margin against theoretical future attacks (including some quantum-computing scenarios) but at a modest performance cost. Most guidance treats AES-128-GCM as adequate for typical applications, with AES-256 reserved for higher-assurance requirements.

Why does Java still let me use MD5 and DES if they're insecure?

Backward compatibility — Java's provider architecture supports algorithms for decades to avoid breaking legacy systems that still rely on them for non-security or interoperability purposes. The JDK has progressively restricted some legacy algorithms via disabled-algorithm security properties, but explicit application code can still request them unless a provider blocks it.

What's the difference between encryption and hashing in Java?

Encryption (AES, RSA) is reversible with the correct key, producing ciphertext that can be decrypted back to plaintext. Hashing (SHA-256, bcrypt) is one-way and produces a fixed-size digest that cannot be reversed to recover the original input — use hashing for passwords and integrity checks, encryption for data you need to read back later.

Should I write my own cryptographic implementation in Java?

No — use vetted library implementations (JCA/JCE providers, Bouncy Castle) rather than hand-rolling cipher logic. Cryptographic implementations are notoriously easy to get subtly wrong even when the underlying algorithm is sound, and those subtle bugs are exactly what real-world attacks exploit.

Never miss an update

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