Java Crypto Is Still a Mess in 2025 (Here’s How to Fix It)

I was reviewing a pull request this morning—let’s call the developer “Dave”—and I saw something that made me physically wince. It was a single line of Java code, buried in a utility class meant to encrypt user tokens.

Cipher.getInstance("AES");

If you know, you know. If you don’t: Dave just defaulted to ECB mode. In 2025. I nearly spilled my coffee.

Look, I get it. Java’s standard cryptography APIs (JCA/JCE) are archaic. They were designed decades ago when we thought MD5 was secure and 56-bit keys were “good enough.” But we are literally staring down the barrel of quantum computing now. The NIST standards for Post-Quantum Cryptography (PQC) are out there. We have libraries like Bouncy Castle pushing updates constantly to keep us safe. Yet I still see code that belongs in a 2008 Struts application.

So, I’m going to rant a bit. But I’m also going to show you how to actually write Java cryptography that doesn’t get your company on the front page of a security breach news site.

Stop Using Defaults

The biggest lie the JDK documentation ever told you is that defaults are safe. They aren’t. When you ask for “AES” without specifying a mode or padding, some providers give you ECB (Electronic Codebook). ECB is deterministic. If you encrypt the same block of data twice, you get the same ciphertext.

That means if I’m an attacker looking at your encrypted database, I can see patterns. I can see that User A and User B have the same password hash, or the same credit card prefix. It leaks information like a sieve.

Here is what I see in bad tutorials all over the web:

// ❌ BAD CODE - DO NOT USE
// This often defaults to AES/ECB/PKCS5Padding which is insecure
Cipher cipher = Cipher.getInstance("AES"); 
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] cipherText = cipher.doFinal(plainText.getBytes());

Gross.

You need Authenticated Encryption (AEAD). This ensures confidentiality and integrity. If someone tampers with the ciphertext in transit, the decryption will fail hard, rather than giving you garbage data or opening you up to padding oracle attacks.

Use AES-GCM. It’s fast, it’s secure, and it’s standard. Here is how you actually write this in modern Java:

// ✅ GOOD CODE - AES-GCM
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.SecureRandom;

public byte[] encrypt(byte[] plaintext, SecretKey key) throws Exception {
    // 1. Get the instance specifically asking for GCM
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    
    // 2. Generate a random IV (Initialization Vector)
    // NEVER reuse an IV with the same key. Ever.
    byte[] iv = new byte[12]; // 12 bytes is standard for GCM
    new SecureRandom().nextBytes(iv);
    
    // 3. Initialize with GCM parameters
    GCMParameterSpec spec = new GCMParameterSpec(128, iv); // 128-bit tag length
    cipher.init(Cipher.ENCRYPT_MODE, key, spec);
    
    // 4. Encrypt
    byte[] cipherText = cipher.doFinal(plaintext);
    
    // 5. Prepend IV to ciphertext so we can decrypt later
    // (You need the same IV to decrypt, it's not secret, just unique)
    byte[] output = new byte[iv.length + cipherText.length];
    System.arraycopy(iv, 0, output, 0, iv.length);
    System.arraycopy(cipherText, 0, output, iv.length, cipherText.length);
    
    return output;
}

See the difference? We handle the IV explicitly. We use SecureRandom (never java.util.Random, which is predictable). We use a specific mode string. It’s more verbose, yeah. But it works.

The Bouncy Castle Factor

The JDK is slow to update. Like, glacial. If you want the latest algorithms—especially now that we are transitioning to Post-Quantum standards like ML-KEM (Kyber) and ML-DSA (Dilithium)—you can’t rely on the vanilla JDK providers alone.

I’ve been using Bouncy Castle for years. It’s basically the Swiss Army knife of Java crypto. Recently, they’ve been pushing heavy updates for crypto agility and PQC support. If you aren’t using it, you’re likely stuck with older implementations that might not pass a compliance audit in 2026.

Adding it is simple, but people still mess it up by not registering the provider correctly.

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;

public class CryptoSetup {
    static {
        // Check if installed, if not, add it
        if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
            Security.addProvider(new BouncyCastleProvider());
        }
    }
}

Once you have that, you unlock algorithms that the standard JDK barely knows exist. For instance, if you’re building an IoT backend right now, you should be looking at lightweight crypto or hybrid schemes. The JDK defaults are too heavy and too old for constrained environments.

Preparing for the Quantum Apocalypse (It’s Closer Than You Think)

I hate the term “Quantum Apocalypse,” but it grabs attention. The reality is boring but dangerous: Harvest Now, Decrypt Later. Attackers are scraping your encrypted traffic today, storing it on cheap hard drives, and waiting for a quantum computer powerful enough to break RSA and ECC.

That means your current 2048-bit RSA keys are already technically a liability for long-term data.

With Bouncy Castle, you can start experimenting with PQC now. You don’t have to rip out everything, but you should be looking at hybrid approaches. Here is a conceptual snippet of how you might generate a key pair for a post-quantum algorithm using the Bouncy Castle PQC provider. Note that API specifics shift as the standards finalize, but the pattern remains:

import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.spec.ECGenParameterSpec;

// Register PQC provider
Security.addProvider(new BouncyCastlePQCProvider());

// Example: Generating a key for a PQC algorithm (e.g., Kyber/ML-KEM)
// Note: Exact algorithm names depend on the specific BC version and NIST standardization status
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Kyber", "BCPQC");
kpg.initialize(1024, new SecureRandom()); // Security level parameter
KeyPair kp = kpg.generateKeyPair();

System.out.println("PQC Public Key generated: " + kp.getPublic().getAlgorithm());

The trick here isn’t just swapping algorithms. It’s Crypto Agility.

If you have “RSA” hardcoded as a string in 50 different places in your codebase, you are going to have a terrible time when you need to migrate to Dilithium or Falcon. Abstract that stuff away. Create a CryptoService interface. Make the algorithm configurable via properties.

A Note on Randomness

I mentioned this earlier, but it bears repeating because I debugged this exact issue last week. SecureRandom can block. If you are running on a headless Linux server (which, let’s be honest, you are), it relies on system entropy (/dev/random vs /dev/urandom).

In older Java versions, SecureRandom.getInstanceStrong() could hang your application startup for minutes if the entropy pool was empty. While modern Java (17+) and OS improvements have largely mitigated this, be aware of it. If your app hangs at startup on the crypto init line, check your entropy source.

But please, for the love of code, do not switch back to java.util.Random just to make it faster. That’s how keys get guessed.

Just Don’t Roll Your Own

I know, I know. You took a discrete math class in college. You understand XOR. You think you can write a “simple” obfuscation wrapper.

Don’t.

Every time I see a custom xorString method, a security auditor gets their wings. The tools are there. Bouncy Castle is open source, robust, and maintained by people who do nothing but math all day. Use their work. Your job is to glue it together securely, not to invent a new cipher.

Check your dependencies today. If you’re running an old version of Bouncy Castle (like pre-1.70), update it. The new stuff—especially regarding PQC and recent vulnerability fixes—is worth the ten minutes of regression testing.