Skip to Content
Course content

186: Cryptography with the javax.crypto Package

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Cryptography in Java can feel like walking through a minefield. Between the checked exceptions and the cryptic naming conventions in the javax.crypto package, it's easy to get overwhelmed. But at its core, most of what we do boils down to three things: a Cipher, a Key, and sometimes an Initialization Vector (IV). To show you how this works, we're going to build a simple SecretVault class that encrypts and decrypts sensitive configuration strings—think of it like a way to store an API key in a file without leaving it in plain text.

Picking the right algorithm

First, we need a Cipher instance. I've always preferred AES (Advanced Encryption Standard) because it's the industry standard for symmetric encryption. In symmetric encryption, the same key is used for both locking and unlocking the data. If you lose the key, the data is gone. Period.

Cipher cipher = Cipher.getInstance("AES");

Now, here is where I usually see people trip up. If you just use "AES", Java often defaults to ECB (Electronic Codebook) mode. I'll be honest: ECB is dangerous. It encrypts identical blocks of data into identical ciphertext, which means patterns in your data stay visible. We want AES/CBC/PKCS5Padding. CBC (Cipher Block Chaining) ensures that even if you encrypt the same word twice, the result looks completely different both times.

The "Invalid Key Length" Trap

Let's try to set up our key. I remember the first time I did this; I tried to be "clever" by just converting a password string into bytes. I wrote something like this:

String myKey = "my-secret-password"; // 18 characters
SecretKeySpec keySpec = new SecretKeySpec(myKey.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);

And immediately, Java threw a java.security.InvalidKeyException: Invalid AES key length. I stared at it for ten minutes before realizing that AES is incredibly picky. It doesn't just take "a string"; it requires a key that is exactly 16, 24, or 32 bytes long. My 18-character string was just wrong.

To fix this and make it professional, I'm going to use a KeyGenerator. This ensures we get a cryptographically strong key of the correct length every time.

KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256); // Use AES-256 for maximum security
SecretKey secretKey = keyGen.generateKey();

Handling the Initialization Vector (IV)

Since we're using CBC mode, we can't just use a key. We need an IV—a random block of data that "seeds" the first block of encryption. If you reuse the same IV with the same key, you're back to square one with security vulnerabilities. The IV doesn't need to be secret (you can store it right next to the encrypted text), but it must be random.

Here is the full implementation of our vault. Notice how I handle the IV by prepending it to the encrypted data. This is a common pattern: the decryption method reads the first 16 bytes to figure out what the IV was, then decrypts the rest.

import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;
import java.util.Base64;

public class SecretVault {
    private static final String ALGORITHM = "AES/CBC/PKCS5Padding";

    public static String encrypt(String plainText, SecretKey key) throws Exception {
        Cipher cipher = Cipher.getInstance(ALGORITHM);
        
        // Generate a random IV
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);
        IvParameterSpec ivSpec = new IvParameterSpec(iv);

        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes());

        // Combine IV and encrypted text so we have the IV for decryption later
        byte[] combined = new byte[iv.length + encrypted.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(encrypted, 0, combined, iv.length, encrypted.length);

        return Base64.getEncoder().encodeToString(combined);
    }

    public static String decrypt(String cipherText, SecretKey key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(cipherText);

        // Extract the IV from the start of the array
        byte[] iv = new byte[16];
        System.arraycopy(combined, 0, iv, 0, iv.length);
        IvParameterSpec ivSpec = new IvParameterSpec(iv);

        // Extract the actual encrypted data
        int encryptedSize = combined.length - 16;
        byte[] encrypted = new byte[encryptedSize];
        System.arraycopy(combined, 16, encrypted, 0, encryptedSize);

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
        
        return new String(cipher.doFinal(encrypted));
    }
}

I used Base64 encoding at the end because encrypted bytes are often "unprintable" characters that break when you try to save them in a text file or a database. Base64 turns those messy bytes into a clean string of alphanumeric characters.




📋 Practical Task

Exercise: Building a Secure Configuration Manager

Your task is to expand the SecretVault concept into a ConfigManager class. This class should simulate a system that loads an API key from a "protected" source.

Requirements:

  • Create a ConfigManager class that contains a SecretKey generated once during the class instantiation.
  • Implement a method String protectKey(String rawKey) that uses the AES/CBC/PKCS5Padding logic from the lesson to encrypt the key.
  • Implement a method String revealKey(String encryptedKey) that decrypts it.
  • In your main method, encrypt the string "AI_SECRET_TOKEN_2024_XYZ", print the Base64 result, and then immediately decrypt it to verify the original string is recovered.
  • Constraint: You must handle the GeneralSecurityException (or its subclasses) using a try-catch block rather than just throwing Exception from the method signatures.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.