AES-GCM combines encryption with an authentication tag; decryption must reject a record whose ciphertext or tag was changed.
Java AES-GCM: reject a changed ciphertext before accepting plaintext
Bind key, nonce and tag
The fixture generates a process-local AES key and a fresh 12-byte nonce, encrypts a dispatch note, then changes the final transmitted byte. The valid record decrypts; the changed record fails tag verification. Do not release plaintext from a failed decryption to callers.
A nonce must not be reused with the same key. A production record format must store the nonce beside the ciphertext and tag, define a key identifier, and enforce a key rotation policy. This small program tests the API boundary, not the storage design.
Authenticate context too
If a ciphertext belongs to one tenant or record ID, include that context as associated data before encryption and decryption. Associated data shows the mismatch failure. The encrypted payload remains subject to input-size limits.
Working program
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public class DispatchNoteSeal {
public static void main(String[] args) throws Exception {
KeyGenerator keys = KeyGenerator.getInstance("AES");
keys.init(128);
SecretKey key = keys.generateKey();
byte[] nonce = new byte[12];
new SecureRandom().nextBytes(nonce);
GCMParameterSpec parameters = new GCMParameterSpec(128, nonce);
Cipher encrypt = Cipher.getInstance("AES/GCM/NoPadding");
encrypt.init(Cipher.ENCRYPT_MODE, key, parameters);
byte[] sealed = encrypt.doFinal("dispatch=R-47".getBytes(StandardCharsets.UTF_8));
Cipher decrypt = Cipher.getInstance("AES/GCM/NoPadding");
decrypt.init(Cipher.DECRYPT_MODE, key, parameters);
System.out.println("roundtrip=" + "dispatch=R-47".equals(new String(decrypt.doFinal(sealed), StandardCharsets.UTF_8)));
byte[] changed = Arrays.copyOf(sealed, sealed.length);
changed[changed.length - 1] ^= 1;
decrypt.init(Cipher.DECRYPT_MODE, key, parameters);
try { decrypt.doFinal(changed); }
catch (AEADBadTagException rejected) { System.out.println("tamper rejected"); }
}
}Output
roundtrip=true
tamper rejectedCost and ownership
Encryption and decryption process O(n) plaintext or ciphertext bytes and allocate result arrays, including the authentication tag. This example's fresh random nonce is local to one operation; high-volume production systems also need a durable nonce-uniqueness strategy under one key.
Common Mistakes
- Do not reuse a GCM nonce with the same key.
- Do not ignore AEADBadTagException or return unauthenticated plaintext.
- Do not treat an ephemeral generated key as a recoverable stored key.
Read next
aes gcm associated data, Java SecureRandom token encoding: preserve unpredictable bytes in a URL-safe form, Java HmacSHA256 verification: reject a changed message, Java byte streams: partial reads and bounded copying.
