Encrypt Online
Theme

Protect & Encrypt

Fix AES-GCM Authentication Failures

Debug AES-GCM authentication failures by matching key, IV, tag, ciphertext, and AAD bytes. Reproduce a Node.js and Python example and isolate changed inputs.

Encrypt Online Editorial Team5 min read
Encrypt Online guide cover on a matte sage background with the headline Fix AES-GCM decryption and the established AES-GCM Decrypt glyph.

An AES-GCM authentication failure means the supplied inputs did not authenticate together. A wrong key, changed IV, damaged ciphertext, different authentication tag, or different additional authenticated data (AAD) can produce the same failure.

Start with a known-good exchange, then compare the decoded bytes at each application boundary. The example below produces the same ciphertext and tag in Node.js, Python, and the AES-GCM tools.

Reproduce a known-good exchange

This synthetic fixture uses AES-256-GCM, a 32-byte key, a 12-byte IV, a 16-byte authentication tag, plaintext hello gcm, and UTF-8 AAD order:42. The code below was verified with Node.js 18.16.0 and Python 3.12.14 with cryptography 50.0.2.

The fixed key and IV make the example reproducible. For real encryption, generate a suitable secret key and use a fresh nonce for every encryption under that key; do not reuse this example's key/IV pair for messages.

Save this as gcm-example.mjs and run node gcm-example.mjs:

JavaScript
import { createCipheriv, createDecipheriv } from 'node:crypto';

const key = Buffer.from(
  '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f', 'hex');
const iv = Buffer.from('101112131415161718191a1b', 'hex');
const aad = Buffer.from('order:42', 'utf8');
const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 });
cipher.setAAD(aad);
const ciphertext = Buffer.concat([cipher.update('hello gcm', 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
console.log(ciphertext.toString('hex'));
console.log(tag.toString('hex'));

const decipher = createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 });
decipher.setAAD(aad);
decipher.setAuthTag(tag);
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
console.log(plaintext.toString('utf8'));

Expected output:

Text
159bf47a26e95dd0a7
c932aef82138409100debd46b7e2a838
hello gcm

Keep the output from decipher.update() inside the operation until decipher.final() succeeds. Node's authenticated decryption contract reports a missing or incorrect tag at finalization; partial output from an earlier call has not passed that check.

Compare separate and combined tag layouts

Node exposes the tag separately through getAuthTag(). Python's AESGCM and Web Crypto use a combined byte sequence: ciphertext followed by the tag. Appending the IV is a separate application-format choice; neither API adds it to this example's ciphertext automatically.

Value Hex Base64
Key, 32 bytes 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=
IV, 12 bytes 101112131415161718191a1b EBESExQVFhcYGRob
AAD, 8 bytes 6f726465723a3432 b3JkZXI6NDI=
Ciphertext, 9 bytes 159bf47a26e95dd0a7 FZv0eibpXdCn
Tag, 16 bytes c932aef82138409100debd46b7e2a838 yTKu+CE4QJEA3r1Gt+KoOA==
Ciphertext followed by tag, 25 bytes 159bf47a26e95dd0a7c932aef82138409100debd46b7e2a838 FZv0eibpXdCnyTKu+CE4QJEA3r1Gt+KoOA==

Tip: Compare byte lengths and decoded values, not just the encoded strings. A 32-character hex key represents 16 bytes. Passing those characters as UTF-8 produces 32 different bytes. Likewise, Base64-encode the combined bytes rather than joining two independently padded Base64 strings.

With the cryptography package installed, save this as gcm-example.py and run python3 gcm-example.py:

PYTHON
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

key = bytes.fromhex(
    "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")
iv = bytes.fromhex("101112131415161718191a1b")
aad = b"order:42"
payload = AESGCM(key).encrypt(iv, b"hello gcm", aad)
print(payload.hex())
print(AESGCM(key).decrypt(iv, payload, aad).decode("utf-8"))

The first output is the 25-byte combined value from the table, followed by hello gcm. Python's AESGCM API uses a 128-bit tag. When importing this payload into Node, take the final 16 bytes as the tag and pass the preceding bytes to decipher.update().

Change one input at a time

Restore the working values before each test. The following changes were tested against the fixed fixture. Each changed cryptographic input fails authentication; the tag-size case is rejected before decryption by the browser tool.

Change Expected result Compare in the producing and receiving applications
Change the key's first hex byte from 00 to 01 Authentication fails Decoded key bytes and key version
Change the IV's first byte from 10 to 11 Authentication fails IV bytes and where the payload stores them
Change AAD from order:42 to order:43 Authentication fails Exact AAD bytes, including whitespace and text encoding
Change the ciphertext's first byte from 15 to 14 Authentication fails Ciphertext after decoding or transport
Change the tag's first byte from c9 to c8 Authentication fails Tag bytes and the payload boundary
Put the tag before the ciphertext in a combined payload Authentication fails The agreed order: ciphertext followed by tag
Remove one byte from the separate tag while selecting 128 bits Tag-length validation fails 16 bytes equals 128 bits

Adding a newline to AAD changes the authenticated bytes too. If AAD is JSON, both applications must use the same serialization rather than independently reformatting it. If no AAD was used, supply empty AAD rather than a new description of the message.

Interpret OperationError and InvalidTag

Web Crypto reports an OperationError when AES-GCM authentication fails, and it can use that error for other operation constraints too. Python raises InvalidTag for a failed authentication check. Neither error identifies whether the key, IV, payload, or AAD caused the disagreement. The Web Crypto AES-GCM algorithm describes the tag as the final portion of its input and rejects failed authenticated decryption.

Check encoding and layout before replacing the key. A hex value entered as Base64, a tag removed twice, or an IV accidentally included in the ciphertext can make two applications disagree even when they share the intended key.

This example accepts the exact fixed inputs and rejects each altered case without returning plaintext from the browser tool. A failed operation should stay failed; removing the tag check does not repair the message.

Reproduce the fixture in AES-GCM Decrypt

Open AES-GCM Decrypt and choose Hex for key, IV, and ciphertext/tag encoding. Enter the table's key and IV, select a 128-bit tag, and set AAD encoding to UTF-8 with order:42.

For separate fields, enter the 9-byte ciphertext and 16-byte tag. For the combined layout, enter the 25-byte ciphertext-followed-by-tag value. Both layouts return hello gcm. Repeat one row from the failure table, then restore the original values to confirm the successful case again.

For mode selection, see AES-CBC vs AES-GCM. For the authentication check itself, see Authenticated Encryption Explained.