Authenticated Encryption Demystified: Implementing AES-GCM in Java

In the landscape of modern cryptography, encryption alone is no longer sufficient. An attacker might not be able to read encrypted data, but they could modify it in transit—potentially with devastating consequences. Authenticated Encryption with Associated Data (AEAD) addresses this by combining confidentiality, integrity, and authenticity into a single, cohesive primitive. AES-GCM (Galois/Counter Mode) stands as the most widely adopted AEAD construction, offering both high performance and strong security guarantees. For Java developers, mastering AES-GCM is essential for building secure applications that resist both eavesdropping and tampering.

What is AES-GCM?

AES-GCM is a mode of operation for the Advanced Encryption Standard that provides authenticated encryption. It combines:

  1. AES-CTR (Counter Mode): For confidentiality (encryption)
  2. GHASH (Galois Hash): For authentication (integrity)

The result is a single algorithm that outputs both ciphertext and an authentication tag, ensuring that:

  • Confidentiality: The data cannot be read without the key
  • Integrity: The data cannot be modified without detection
  • Authenticity: The data comes from a party possessing the key

Why AES-GCM Matters for Java Applications

  1. Tamper Detection: Prevents active attacks where ciphertext is modified
  2. Associated Data: Authenticates non-secret metadata (headers, IVs, etc.)
  3. Performance: Hardware-accelerated on modern CPUs (AES-NI, PCLMULQDQ)
  4. Standard Compliance: Approved by NIST (SP 800-38D)
  5. TLS Integration: Used in TLS 1.2 and TLS 1.3 cipher suites

Core Concepts

┌──────────────────┐     ┌──────────────────┐     ┌──────────────────┐
│   Plaintext      │     │   Associated     │     │      Key         │
│                  │     │     Data (AAD)   │     │                  │
└────────┬─────────┘     └────────┬─────────┘     └────────┬─────────┘
│                        │                        │
└──────────┬─────────────┴────────────┬───────────┘
│                          │
▼                          ▼
┌────────────────────┐      ┌────────────────────┐
│   AES-CTR          │      │   GHASH            │
│   (Encryption)     │      │   (Authentication) │
└────────┬───────────┘      └────────┬───────────┘
│                           │
└──────────────┬────────────┘
▼
┌────────────────────────┐
│   Ciphertext + Tag     │
└────────────────────────┘

Complete AES-GCM Implementation in Java

1. Basic Encryption and Decryption

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* Basic AES-GCM encryption/decryption utility
*/
public class AESGCMBasic {
// GCM parameters
private static final int GCM_IV_LENGTH = 12;      // 96 bits - recommended for GCM
private static final int GCM_TAG_LENGTH = 16;     // 128 bits - authentication tag size
private final SecureRandom secureRandom = new SecureRandom();
/**
* Generate a new AES-256 key
*/
public SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, secureRandom);
return keyGen.generateKey();
}
/**
* Encrypt plaintext with AES-GCM
* Returns: Base64 encoded IV + ciphertext + tag
*/
public String encrypt(String plaintext, SecretKey key) throws Exception {
// Generate random IV
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
// Initialize cipher for encryption
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
// Encrypt
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
// Combine IV and ciphertext for storage/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 ciphertext with AES-GCM
* Input: Base64 encoded IV + ciphertext + tag
*/
public String decrypt(String encryptedData, SecretKey key) throws Exception {
// Decode from Base64
byte[] combined = Base64.getDecoder().decode(encryptedData);
// Extract IV and ciphertext
byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
// Initialize cipher for decryption
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
// Decrypt (authentication happens automatically)
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
}
public static void main(String[] args) throws Exception {
AESGCMBasic aesGCM = new AESGCMBasic();
// Generate key
SecretKey key = aesGCM.generateKey();
System.out.println("Key: " + Base64.getEncoder().encodeToString(key.getEncoded()));
// Original message
String originalText = "This is a secret message with sensitive data!";
System.out.println("Original: " + originalText);
// Encrypt
String encrypted = aesGCM.encrypt(originalText, key);
System.out.println("Encrypted: " + encrypted);
// Decrypt
String decrypted = aesGCM.decrypt(encrypted, key);
System.out.println("Decrypted: " + decrypted);
// Verify integrity
System.out.println("Integrity check: " + originalText.equals(decrypted));
}
}

2. Advanced Implementation with Associated Data (AAD)

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.ByteBuffer;
import java.security.SecureRandom;
import java.util.Arrays;
/**
* Advanced AES-GCM with Associated Data support
*/
public class AESGCMWithAAD {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private final SecureRandom secureRandom = new SecureRandom();
/**
* Encrypted data structure containing IV, ciphertext, and tag
*/
public static class EncryptedData {
private final byte[] iv;
private final byte[] ciphertext;
private final byte[] tag;
public EncryptedData(byte[] iv, byte[] ciphertext, byte[] tag) {
this.iv = iv.clone();
this.ciphertext = ciphertext.clone();
this.tag = tag.clone();
}
public byte[] getIv() { return iv.clone(); }
public byte[] getCiphertext() { return ciphertext.clone(); }
public byte[] getTag() { return tag.clone(); }
public byte[] toByteArray() {
ByteBuffer buffer = ByteBuffer.allocate(iv.length + ciphertext.length + tag.length);
buffer.put(iv);
buffer.put(ciphertext);
buffer.put(tag);
return buffer.array();
}
public static EncryptedData fromByteArray(byte[] data) {
byte[] iv = Arrays.copyOfRange(data, 0, GCM_IV_LENGTH);
byte[] tag = Arrays.copyOfRange(data, data.length - GCM_TAG_LENGTH, data.length);
byte[] ciphertext = Arrays.copyOfRange(data, GCM_IV_LENGTH, data.length - GCM_TAG_LENGTH);
return new EncryptedData(iv, ciphertext, tag);
}
}
/**
* Encrypt with associated data
* @param plaintext Data to encrypt
* @param aad Additional authenticated data (not encrypted but authenticated)
* @param key Secret key
* @return Encrypted data with IV and tag
*/
public EncryptedData encrypt(byte[] plaintext, byte[] aad, SecretKey key) throws Exception {
// Generate random IV
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
// Initialize cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
// Add associated data (must be done before encryption)
if (aad != null && aad.length > 0) {
cipher.updateAAD(aad);
}
// Encrypt
byte[] ciphertext = cipher.doFinal(plaintext);
// Extract tag (last GCM_TAG_LENGTH bytes of ciphertext)
int ciphertextLength = ciphertext.length - GCM_TAG_LENGTH;
byte[] actualCiphertext = Arrays.copyOfRange(ciphertext, 0, ciphertextLength);
byte[] tag = Arrays.copyOfRange(ciphertext, ciphertextLength, ciphertext.length);
return new EncryptedData(iv, actualCiphertext, tag);
}
/**
* Decrypt with associated data verification
* @param encryptedData Encrypted data structure
* @param aad Additional authenticated data (must match encryption)
* @param key Secret key
* @return Decrypted plaintext
* @throws Exception if authentication fails
*/
public byte[] decrypt(EncryptedData encryptedData, byte[] aad, SecretKey key) throws Exception {
// Initialize cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, encryptedData.getIv());
cipher.init(Cipher.DECRYPT_MODE, key, spec);
// Add associated data (must match exactly what was used in encryption)
if (aad != null && aad.length > 0) {
cipher.updateAAD(aad);
}
// Combine ciphertext and tag for decryption
byte[] combinedCiphertext = new byte[encryptedData.getCiphertext().length + encryptedData.getTag().length];
System.arraycopy(encryptedData.getCiphertext(), 0, combinedCiphertext, 0, encryptedData.getCiphertext().length);
System.arraycopy(encryptedData.getTag(), 0, combinedCiphertext, encryptedData.getCiphertext().length, encryptedData.getTag().length);
// Decrypt (authentication happens automatically)
return cipher.doFinal(combinedCiphertext);
}
public static void main(String[] args) throws Exception {
AESGCMWithAAD aesGCM = new AESGCMWithAAD();
// Generate key (in practice, use a proper key management system)
SecretKey key = aesGCM.generateKey();
// Original data
String plaintext = "Sensitive financial transaction: $1,000,000";
String aad = "transaction-id-12345|timestamp=2024-01-15T10:30:00Z";
System.out.println("Plaintext: " + plaintext);
System.out.println("AAD: " + aad);
// Encrypt
EncryptedData encrypted = aesGCM.encrypt(
plaintext.getBytes("UTF-8"),
aad.getBytes("UTF-8"),
key
);
System.out.println("IV (hex): " + bytesToHex(encrypted.getIv()));
System.out.println("Ciphertext (hex): " + bytesToHex(encrypted.getCiphertext()));
System.out.println("Tag (hex): " + bytesToHex(encrypted.getTag()));
// Successful decryption
byte[] decrypted = aesGCM.decrypt(encrypted, aad.getBytes("UTF-8"), key);
System.out.println("Decrypted: " + new String(decrypted, "UTF-8"));
// Tampering detection - modify ciphertext
byte[] tamperedCiphertext = encrypted.getCiphertext().clone();
tamperedCiphertext[0] ^= 0x01; // Flip one bit
EncryptedData tampered = new EncryptedData(
encrypted.getIv(),
tamperedCiphertext,
encrypted.getTag()
);
try {
aesGCM.decrypt(tampered, aad.getBytes("UTF-8"), key);
System.out.println("ERROR: Tampering not detected!");
} catch (Exception e) {
System.out.println("✓ Tampering detected: " + e.getMessage());
}
// Tampering detection - modify AAD
String wrongAad = "transaction-id-12345|timestamp=2024-01-15T10:30:01Z";
try {
aesGCM.decrypt(encrypted, wrongAad.getBytes("UTF-8"), key);
System.out.println("ERROR: AAD tampering not detected!");
} catch (Exception e) {
System.out.println("✓ AAD tampering detected: " + e.getMessage());
}
}
private SecretKey generateKey() throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, secureRandom);
return keyGen.generateKey();
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
}

3. Key Derivation from Passwords

import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* AES-GCM with password-based key derivation (PBKDF2)
*/
public class PasswordBasedAESGCM {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private static final int PBKDF2_ITERATIONS = 310000; // NIST recommended
private static final int KEY_LENGTH = 256; // bits
private final SecureRandom secureRandom = new SecureRandom();
/**
* Derive an AES key from a password and salt
*/
public SecretKey deriveKey(char[] password, byte[] salt) throws Exception {
PBEKeySpec spec = new PBEKeySpec(
password,
salt,
PBKDF2_ITERATIONS,
KEY_LENGTH
);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] keyBytes = factory.generateSecret(spec).getEncoded();
return new SecretKeySpec(keyBytes, "AES");
}
/**
* Generate random salt for key derivation
*/
public byte[] generateSalt() {
byte[] salt = new byte[16];
secureRandom.nextBytes(salt);
return salt;
}
/**
* Encrypt with password-based key
* Returns: Base64 encoded salt + IV + ciphertext + tag
*/
public String encrypt(String plaintext, char[] password) throws Exception {
// Generate random salt and IV
byte[] salt = generateSalt();
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
// Derive key from password
SecretKey key = deriveKey(password, salt);
// Encrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
// Combine all components
byte[] combined = new byte[salt.length + iv.length + ciphertext.length];
System.arraycopy(salt, 0, combined, 0, salt.length);
System.arraycopy(iv, 0, combined, salt.length, iv.length);
System.arraycopy(ciphertext, 0, combined, salt.length + iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(combined);
}
/**
* Decrypt with password-based key
*/
public String decrypt(String encryptedData, char[] password) throws Exception {
byte[] combined = Base64.getDecoder().decode(encryptedData);
// Extract components
byte[] salt = Arrays.copyOfRange(combined, 0, 16);
byte[] iv = Arrays.copyOfRange(combined, 16, 16 + GCM_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(combined, 16 + GCM_IV_LENGTH, combined.length);
// Derive key from password
SecretKey key = deriveKey(password, salt);
// Decrypt
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
}
public static void main(String[] args) throws Exception {
PasswordBasedAESGCM aesGCM = new PasswordBasedAESGCM();
String password = "MyStrongPassword123!";
String plaintext = "This is a secret document that needs password protection.";
System.out.println("Plaintext: " + plaintext);
// Encrypt with password
String encrypted = aesGCM.encrypt(plaintext, password.toCharArray());
System.out.println("Encrypted: " + encrypted);
// Decrypt with correct password
String decrypted = aesGCM.decrypt(encrypted, password.toCharArray());
System.out.println("Decrypted: " + decrypted);
// Attempt with wrong password
try {
aesGCM.decrypt(encrypted, "WrongPassword123!".toCharArray());
System.out.println("ERROR: Wrong password accepted!");
} catch (Exception e) {
System.out.println("✓ Wrong password rejected: " + e.getMessage());
}
// Clear sensitive data
Arrays.fill(password.toCharArray(), '\0');
}
}

4. Streaming Encryption for Large Data

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.io.*;
import java.security.SecureRandom;
/**
* AES-GCM streaming for large files
*/
public class AESGCMStreaming {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private static final int BUFFER_SIZE = 8192;
private final SecureRandom secureRandom = new SecureRandom();
/**
* Encrypt a large file
*/
public void encryptFile(File inputFile, File outputFile, SecretKey key) throws Exception {
// Generate random IV
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
// Initialize cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
try (FileInputStream fis = new FileInputStream(inputFile);
FileOutputStream fos = new FileOutputStream(outputFile);
CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {
// Write IV first (needed for decryption)
fos.write(iv);
// Encrypt and write file content
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = fis.read(buffer)) != -1) {
cos.write(buffer, 0, bytesRead);
}
// Flush to ensure all data is written
cos.flush();
}
}
/**
* Decrypt a large file
*/
public void decryptFile(File inputFile, File outputFile, SecretKey key) throws Exception {
try (FileInputStream fis = new FileInputStream(inputFile)) {
// Read IV from the beginning of the file
byte[] iv = new byte[GCM_IV_LENGTH];
if (fis.read(iv) != GCM_IV_LENGTH) {
throw new IOException("Failed to read IV");
}
// Initialize cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
try (FileOutputStream fos = new FileOutputStream(outputFile);
CipherInputStream cis = new CipherInputStream(fis, cipher)) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;
while ((bytesRead = cis.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
}
}
}
public static void main(String[] args) throws Exception {
AESGCMStreaming streaming = new AESGCMStreaming();
// Create test file
File inputFile = new File("input.txt");
try (FileWriter writer = new FileWriter(inputFile)) {
for (int i = 0; i < 10000; i++) {
writer.write("This is line " + i + " of a large file.\n");
}
}
// Generate key
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom());
SecretKey key = keyGen.generateKey();
// Encrypt
File encryptedFile = new File("encrypted.bin");
streaming.encryptFile(inputFile, encryptedFile, key);
System.out.println("Encrypted file size: " + encryptedFile.length() + " bytes");
// Decrypt
File decryptedFile = new File("decrypted.txt");
streaming.decryptFile(encryptedFile, decryptedFile, key);
// Verify
System.out.println("Decrypted file size: " + decryptedFile.length() + " bytes");
// Cleanup
inputFile.delete();
encryptedFile.delete();
decryptedFile.delete();
}
}

5. Secure Key Wrapping with AES-GCM

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
/**
* Secure key wrapping using AES-GCM
*/
public class AESGCMKeyWrapping {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private final SecureRandom secureRandom = new SecureRandom();
/**
* Wrap (encrypt) a key using a master key
*/
public String wrapKey(SecretKey keyToWrap, SecretKey masterKey) throws Exception {
// Generate random IV
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
// Initialize cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, masterKey, spec);
// Add key metadata as AAD
byte[] aad = ("AES/" + keyToWrap.getEncoded().length * 8).getBytes("UTF-8");
cipher.updateAAD(aad);
// Wrap the key
byte[] wrappedKey = cipher.doFinal(keyToWrap.getEncoded());
// Combine IV and wrapped key
byte[] combined = new byte[iv.length + wrappedKey.length];
System.arraycopy(iv, 0, combined, 0, iv.length);
System.arraycopy(wrappedKey, 0, combined, iv.length, wrappedKey.length);
return Base64.getEncoder().encodeToString(combined);
}
/**
* Unwrap (decrypt) a key using a master key
*/
public SecretKey unwrapKey(String wrappedKeyBase64, SecretKey masterKey, String algorithm) throws Exception {
byte[] combined = Base64.getDecoder().decode(wrappedKeyBase64);
// Extract IV
byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
byte[] wrappedKey = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
// Initialize cipher
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, masterKey, spec);
// Add the same AAD used during wrapping
byte[] aad = ("AES/" + wrappedKey.length * 8).getBytes("UTF-8");
cipher.updateAAD(aad);
// Unwrap the key
byte[] keyBytes = cipher.doFinal(wrappedKey);
return new SecretKeySpec(keyBytes, algorithm);
}
public static void main(String[] args) throws Exception {
AESGCMKeyWrapping keyWrapper = new AESGCMKeyWrapping();
// Generate master key (for wrapping)
KeyGenerator masterKeyGen = KeyGenerator.getInstance("AES");
masterKeyGen.init(256, new SecureRandom());
SecretKey masterKey = masterKeyGen.generateKey();
// Generate key to wrap
SecretKey dataKey = masterKeyGen.generateKey();
System.out.println("Master Key: " + 
Base64.getEncoder().encodeToString(masterKey.getEncoded()));
System.out.println("Data Key: " + 
Base64.getEncoder().encodeToString(dataKey.getEncoded()));
// Wrap the data key
String wrappedKey = keyWrapper.wrapKey(dataKey, masterKey);
System.out.println("Wrapped Key: " + wrappedKey);
// Unwrap the data key
SecretKey unwrappedKey = keyWrapper.unwrapKey(wrappedKey, masterKey, "AES");
System.out.println("Unwrapped Key: " + 
Base64.getEncoder().encodeToString(unwrappedKey.getEncoded()));
// Verify
boolean match = Arrays.equals(dataKey.getEncoded(), unwrappedKey.getEncoded());
System.out.println("Keys match: " + match);
}
}

6. Spring Integration with Configuration

package com.example.crypto.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.beans.factory.annotation.Value;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
/**
* Spring configuration for AES-GCM services
*/
@Configuration
public class AESGCMConfig {
@Value("${encryption.key.base64}")
private String base64Key;
@Value("${encryption.key.algorithm:AES}")
private String algorithm;
@Bean
public SecretKey encryptionKey() {
byte[] keyBytes = Base64.getDecoder().decode(base64Key);
return new SecretKeySpec(keyBytes, algorithm);
}
@Bean
public AESGCMService aesGCMService(SecretKey encryptionKey) {
return new AESGCMService(encryptionKey);
}
}
/**
* Service for AES-GCM operations
*/
class AESGCMService {
private static final int GCM_IV_LENGTH = 12;
private static final int GCM_TAG_LENGTH = 16;
private final SecretKey key;
private final SecureRandom secureRandom = new SecureRandom();
public AESGCMService(SecretKey key) {
this.key = key;
}
/**
* Encrypt sensitive data
*/
public String encrypt(String plaintext) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
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 sensitive data
*/
public String decrypt(String encryptedData) throws Exception {
byte[] combined = Base64.getDecoder().decode(encryptedData);
byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
}
/**
* Encrypt with associated data (e.g., user ID)
*/
public String encryptWithContext(String plaintext, String context) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH];
secureRandom.nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
// Add context as AAD
if (context != null && !context.isEmpty()) {
cipher.updateAAD(context.getBytes("UTF-8"));
}
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
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 with context verification
*/
public String decryptWithContext(String encryptedData, String context) throws Exception {
byte[] combined = Base64.getDecoder().decode(encryptedData);
byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
byte[] ciphertext = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
// Add the same context for verification
if (context != null && !context.isEmpty()) {
cipher.updateAAD(context.getBytes("UTF-8"));
}
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, "UTF-8");
}
}

7. Testing and Validation

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.RepeatedTest;
import javax.crypto.SecretKey;
import javax.crypto.KeyGenerator;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
import static org.junit.jupiter.api.Assertions.*;
class AESGCMTest {
private AESGCMWithAAD aesGCM;
private SecretKey key;
@BeforeEach
void setUp() throws Exception {
aesGCM = new AESGCMWithAAD();
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom());
key = keyGen.generateKey();
}
@Test
void testBasicEncryptionDecryption() throws Exception {
String plaintext = "Hello, World!";
AESGCMWithAAD.EncryptedData encrypted = aesGCM.encrypt(
plaintext.getBytes("UTF-8"),
null,
key
);
byte[] decrypted = aesGCM.decrypt(encrypted, null, key);
assertEquals(plaintext, new String(decrypted, "UTF-8"));
}
@Test
void testAADVerification() throws Exception {
String plaintext = "Sensitive data";
String aad = "user123";
AESGCMWithAAD.EncryptedData encrypted = aesGCM.encrypt(
plaintext.getBytes("UTF-8"),
aad.getBytes("UTF-8"),
key
);
// Correct AAD should work
byte[] decrypted = aesGCM.decrypt(encrypted, aad.getBytes("UTF-8"), key);
assertEquals(plaintext, new String(decrypted, "UTF-8"));
// Wrong AAD should fail
String wrongAad = "user456";
assertThrows(Exception.class, () -> {
aesGCM.decrypt(encrypted, wrongAad.getBytes("UTF-8"), key);
});
}
@Test
void testTamperDetection() throws Exception {
String plaintext = "Important message";
AESGCMWithAAD.EncryptedData encrypted = aesGCM.encrypt(
plaintext.getBytes("UTF-8"),
null,
key
);
// Tamper with ciphertext
byte[] tamperedCiphertext = encrypted.getCiphertext().clone();
tamperedCiphertext[0] ^= 0x01;
AESGCMWithAAD.EncryptedData tampered = new AESGCMWithAAD.EncryptedData(
encrypted.getIv(),
tamperedCiphertext,
encrypted.getTag()
);
assertThrows(Exception.class, () -> {
aesGCM.decrypt(tampered, null, key);
});
// Tamper with tag
byte[] tamperedTag = encrypted.getTag().clone();
tamperedTag[0] ^= 0x01;
AESGCMWithAAD.EncryptedData tamperedTagData = new AESGCMWithAAD.EncryptedData(
encrypted.getIv(),
encrypted.getCiphertext(),
tamperedTag
);
assertThrows(Exception.class, () -> {
aesGCM.decrypt(tamperedTagData, null, key);
});
}
@RepeatedTest(100)
void testRandomness() throws Exception {
// Generate multiple encrypted versions of same plaintext
String plaintext = "Constant message";
Set<String> encryptedResults = new HashSet<>();
for (int i = 0; i < 10; i++) {
AESGCMWithAAD.EncryptedData encrypted = aesGCM.encrypt(
plaintext.getBytes("UTF-8"),
null,
key
);
// Each encryption should be unique (different IV)
encryptedResults.add(Arrays.toString(encrypted.getCiphertext()));
}
assertEquals(10, encryptedResults.size());
}
@Test
void testThreadSafety() throws InterruptedException {
int threadCount = 10;
int operationsPerThread = 1000;
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failureCount = new AtomicInteger(0);
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executor.submit(() -> {
try {
for (int j = 0; j < operationsPerThread; j++) {
String plaintext = "Thread-safe test " + j;
AESGCMWithAAD.EncryptedData encrypted = aesGCM.encrypt(
plaintext.getBytes("UTF-8"),
null,
key
);
byte[] decrypted = aesGCM.decrypt(encrypted, null, key);
if (plaintext.equals(new String(decrypted, "UTF-8"))) {
successCount.incrementAndGet();
} else {
failureCount.incrementAndGet();
}
}
} catch (Exception e) {
failureCount.incrementAndGet();
} finally {
latch.countDown();
}
});
}
latch.await();
executor.shutdown();
assertEquals(threadCount * operationsPerThread, successCount.get());
assertEquals(0, failureCount.get());
}
}

Security Considerations

AspectBest Practice
IV GenerationAlways use a cryptographically secure random number generator for IVs. Never reuse IVs with the same key.
IV Length12 bytes (96 bits) is the recommended size for GCM. Longer IVs are hashed, reducing performance.
Tag LengthUse 16 bytes (128 bits) for the authentication tag. Never use less than 12 bytes.
Key SizeUse 256-bit keys for AES-GCM. 128-bit is acceptable but 256-bit provides future-proofing.
AAD UsageInclude all relevant context (user IDs, timestamps, purpose) in AAD to bind ciphertext to context.
Key ReuseAvoid encrypting more than 2^32 blocks (64 GB) with the same key/IV pair.
Error HandlingNever reveal the cause of decryption failures (timing attacks). Use constant-time comparisons.

Performance Benchmarks

public class AESGCMPerformance {
public static void main(String[] args) throws Exception {
AESGCMWithAAD aesGCM = new AESGCMWithAAD();
// Generate key
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(256, new SecureRandom());
SecretKey key = keyGen.generateKey();
int[] dataSizes = {1024, 10240, 102400, 1048576}; // 1KB to 1MB
int iterations = 1000;
for (int size : dataSizes) {
byte[] plaintext = new byte[size];
new SecureRandom().nextBytes(plaintext);
// Warmup
for (int i = 0; i < 100; i++) {
aesGCM.encrypt(plaintext, null, key);
}
// Benchmark encryption
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
aesGCM.encrypt(plaintext, null, key);
}
long encryptionTime = System.nanoTime() - start;
// Benchmark decryption
AESGCMWithAAD.EncryptedData encrypted = aesGCM.encrypt(plaintext, null, key);
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
aesGCM.decrypt(encrypted, null, key);
}
long decryptionTime = System.nanoTime() - start;
System.out.printf("Size: %d bytes%n", size);
System.out.printf("  Encryption: %.2f MB/s%n", 
(size * iterations) / (encryptionTime / 1e9) / 1_000_000);
System.out.printf("  Decryption: %.2f MB/s%n", 
(size * iterations) / (decryptionTime / 1e9) / 1_000_000);
System.out.println();
}
}
}

Conclusion

AES-GCM represents the gold standard for authenticated encryption in modern applications. Its combination of confidentiality, integrity, and authenticity in a single, efficient primitive makes it ideal for everything from securing API payloads to encrypting files and protecting database fields.

The Java implementations provided here demonstrate:

  1. Basic encryption/decryption for simple use cases
  2. Associated Data (AAD) for binding ciphertext to context
  3. Password-based encryption for user-facing applications
  4. Streaming for large files
  5. Key wrapping for secure key management
  6. Spring integration for enterprise applications

When implementing AES-GCM in production:

  • Always use fresh random IVs
  • Include context in AAD
  • Never ignore authentication failures
  • Rotate keys periodically
  • Consider hardware acceleration (AES-NI)

By following these patterns and best practices, Java developers can build applications that resist both passive eavesdropping and active tampering, providing the strong cryptographic guarantees that modern security demands.

Java Programming Intermediate Topics – Modifiers, Loops, Math, Methods & Projects (Related to Java Programming)


Access Modifiers in Java:
Access modifiers control how classes, variables, and methods are accessed from different parts of a program. Java provides four main access levels—public, private, protected, and default—which help protect data and control visibility in object-oriented programming.
Read more: https://macronepal.com/blog/access-modifiers-in-java-a-complete-guide/


Static Variables in Java:
Static variables belong to the class rather than individual objects. They are shared among all instances of the class and are useful for storing values that remain common across multiple objects.
Read more: https://macronepal.com/blog/static-variables-in-java-a-complete-guide/


Method Parameters in Java:
Method parameters allow values to be passed into methods so that operations can be performed using supplied data. They help make methods flexible and reusable in different parts of a program.
Read more: https://macronepal.com/blog/method-parameters-in-java-a-complete-guide/


Random Numbers in Java:
This topic explains how to generate random numbers in Java for tasks such as simulations, games, and random selections. Random numbers help create unpredictable results in programs.
Read more: https://macronepal.com/blog/random-numbers-in-java-a-complete-guide/


Math Class in Java:
The Math class provides built-in methods for performing mathematical calculations such as powers, square roots, rounding, and other advanced calculations used in Java programs.
Read more: https://macronepal.com/blog/math-class-in-java-a-complete-guide/


Boolean Operations in Java:
Boolean operations use true and false values to perform logical comparisons. They are commonly used in conditions and decision-making statements to control program flow.
Read more: https://macronepal.com/blog/boolean-operations-in-java-a-complete-guide/


Nested Loops in Java:
Nested loops are loops placed inside other loops to perform repeated operations within repeated tasks. They are useful for pattern printing, tables, and working with multi-level data.
Read more: https://macronepal.com/blog/nested-loops-in-java-a-complete-guide/


Do-While Loop in Java:
The do-while loop allows a block of code to run at least once before checking the condition. It is useful when the program must execute a task before verifying whether it should continue.
Read more: https://macronepal.com/blog/do-while-loop-in-java-a-complete-guide/


Simple Calculator Project in Java:
This project demonstrates how to create a basic calculator program using Java. It combines input handling, arithmetic operations, and conditional logic to perform simple mathematical calculations.
Read more: https://macronepal.com/blog/simple-calculator-project-in-java/

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper