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.

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:
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:
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:
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.
Related tools
- AES-GCM Decrypt for testing raw key, IV, AAD, tag, and payload layout.
- AES-GCM Encrypt for producing a separate or combined example.
- Encryption Key Generator for generating fresh key bytes in a chosen encoding.
For mode selection, see AES-CBC vs AES-GCM. For the authentication check itself, see Authenticated Encryption Explained.