Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Java AES-GCM: reject a changed ciphertext before accepting plaintext

Last updated: 5 Oct 20264 min read
tutorial
AdvancedBy AITrove Editorial

AES-GCM combines encryption with an authentication tag; decryption must reject a record whose ciphertext or tag was changed.

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

Java
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

Output
roundtrip=true
tamper rejected

Cost 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.

java
cryptography
aes-gcm-authenticated-decryption
Storage details