AES Encryption in Java: CBC, GCM Modes & SecretKey Generation

AES (Advanced Encryption Standard) is the industry-standard symmetric block cipher used to protect data at rest and in transit. Java's JCA exposes AES through the Cipher class. This article covers key generation, CBC mode with padding, and the preferred GCM mode which provides authenticated encryption.

Share

AES Fundamentals: Key Sizes & Modes

PropertyDetail
Block sizeAlways 128 bits (16 bytes)
Key sizes128, 192, or 256 bits
Recommended key size256-bit (strongest; 128-bit is also considered secure)
IV (Initialisation Vector)Required for CBC (16 bytes) and GCM (12 bytes recommended)
Recommended modeGCM: provides confidentiality + integrity + authenticity
AvoidECB mode: deterministic, leaks patterns; CBC without authentication

Generating a SecretKey

Use KeyGenerator to create a random AES key, or reconstruct a key from raw bytes using SecretKeySpec.

Java
import javax.crypto.*;
import javax.crypto.spec.*;
import java.security.*;

// Generate a new random 256-bit AES key
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom());
SecretKey key = keyGen.generateKey();

// Serialise to bytes (for storage/transmission)
byte[] keyBytes = key.getEncoded();

// Reconstruct from bytes
SecretKey reconstructed = new SecretKeySpec(keyBytes, "AES");
Store secret keys in a KeyStore or a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault). Never hardcode key bytes in source code or commit them to version control.

AES-GCM: Authenticated Encryption (Preferred)

GCM (Galois/Counter Mode) provides authenticated encryption: it simultaneously encrypts and computes an authentication tag. Decryption verifies the tag; any tampering causes an AEADBadTagException.

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

public class AesGcm {

    private static final int GCM_IV_LENGTH  = 12;   // 96 bits
    private static final int GCM_TAG_LENGTH = 128;  // bits

    /** Encrypt plaintext with AES-GCM. Returns IV + ciphertext (base64). */
    public static String encrypt(String plaintext, SecretKey key) throws Exception {
        // 1. Generate a random 12-byte IV (never reuse an IV with the same key!)
        byte[] iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);

        // 2. Initialise cipher
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, spec);

        // 3. Optionally add AAD (Additional Authenticated Data — not encrypted, but authenticated)
        // cipher.updateAAD("context-info".getBytes());

        // 4. Encrypt
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

        // 5. Prepend IV to ciphertext for transmission
        byte[] combined = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, combined, 0, iv.length);
        System.arraycopy(ciphertext, 0, combined, iv.length, ciphertext.length);

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

    /** Decrypt AES-GCM ciphertext. Throws AEADBadTagException if tampered. */
    public static String decrypt(String encoded, SecretKey key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(encoded);

        // Extract IV and ciphertext
        byte[] iv         = new byte[GCM_IV_LENGTH];
        byte[] ciphertext = new byte[combined.length - GCM_IV_LENGTH];
        System.arraycopy(combined, 0, iv, 0, iv.length);
        System.arraycopy(combined, iv.length, ciphertext, 0, ciphertext.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
        return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
    }

    public static void main(String[] args) throws Exception {
        KeyGenerator kg = KeyGenerator.getInstance("AES");
        kg.init(256);
        SecretKey key = kg.generateKey();

        String encrypted = encrypt("Secret message!", key);
        System.out.println("Encrypted: " + encrypted);

        String decrypted = decrypt(encrypted, key);
        System.out.println("Decrypted: " + decrypted);  // Secret message!
    }
}
The IV (nonce) must be unique for every encryption with the same key. Reusing an IV in GCM completely breaks confidentiality and allows an attacker to recover the key. Always generate a fresh random IV per encryption operation.

AES-CBC Mode: With PKCS5 Padding

CBC (Cipher Block Chaining) is older and widely supported but provides only confidentiality: it has no built-in integrity check. If you use CBC, always add a separate HMAC for authentication (Encrypt-then-MAC).

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

public class AesCbc {

    public static byte[] encrypt(byte[] plaintext, SecretKey key) throws Exception {
        // 1. Random 16-byte IV
        byte[] iv = new byte[16];
        new SecureRandom().nextBytes(iv);

        // 2. Encrypt with PKCS5Padding (auto-pads to 16-byte block boundary)
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
        byte[] ciphertext = cipher.doFinal(plaintext);

        // 3. Prepend IV
        byte[] result = new byte[16 + ciphertext.length];
        System.arraycopy(iv, 0, result, 0, 16);
        System.arraycopy(ciphertext, 0, result, 16, ciphertext.length);
        return result;
    }

    public static byte[] decrypt(byte[] ivPlusCiphertext, SecretKey key) throws Exception {
        byte[] iv         = new byte[16];
        byte[] ciphertext = new byte[ivPlusCiphertext.length - 16];
        System.arraycopy(ivPlusCiphertext, 0, iv, 0, 16);
        System.arraycopy(ivPlusCiphertext, 16, ciphertext, 0, ciphertext.length);

        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
        return cipher.doFinal(ciphertext);
    }
}
AES-CBC is vulnerable to padding oracle attacks (BEAST, POODLE) when error messages reveal padding validity. Prefer AES-GCM for all new code.

Deriving a Key from a Password

When the encryption key must come from a user password, use PBKDF2 to derive a key: never use a raw password as a key. See also the SecureRandom & PBKDF2 article for full details.

Java
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

public static SecretKey deriveKey(char[] password, byte[] salt) throws Exception {
    PBEKeySpec spec = new PBEKeySpec(
            password,
            salt,
            310_000,        // iterations (OWASP 2023 recommendation for PBKDF2-SHA256)
            256             // key length in bits
    );
    SecretKeyFactory skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
    byte[] keyBytes = skf.generateSecret(spec).getEncoded();
    spec.clearPassword();  // wipe password from memory
    return new SecretKeySpec(keyBytes, "AES");
}

Best Practices Summary

TopicRecommendation
ModeAlways use AES-GCM; avoid ECB and raw CBC
Key size256-bit; 128-bit is acceptable
IVRandom per-encryption; never reuse with the same key
IV size (GCM)12 bytes (96 bits): optimal for GCM counter
Key storageKeyStore or external secrets manager
Password-derived keyUse PBKDF2 / bcrypt / Argon2: never hash the password directly
EncodingBase64 for text transport; raw bytes for binary protocols
AADBind ciphertext to context (user ID, file name) using GCM's AAD