Skip to main content
Authenticated Encryption (AEAD) Intermediate

AES-GCM: Authenticated Encryption

AES-GCM does two jobs at once: it hides your data AND proves nobody tampered with it. Learn how Galois/Counter Mode combines AES with GHASH authentication to become the default choice for TLS, IPsec, and disk encryption.

PL
Pashalis Laoutaris
August 4, 2026
10 min read

Interactive AES-GCM Visualizer

🔐 AES-GCM Authenticated Encryption

Defaults are a well-known NIST GCM test vector.
Enter text and click a button to start!
Hash Subkey (H = AES(key, 0))
Ciphertext (CTR mode)
Authentication Tag
Verification
Set inputs and click Encrypt & Authenticate.

AES-GCM: Authenticated Encryption

Introduction

Plain AES encryption only guarantees confidentiality: that an eavesdropper can’t read your data. It says nothing about integrity: whether an attacker silently altered the ciphertext in transit. AES-GCM (Galois/Counter Mode) solves both problems in a single, efficient pass. It encrypts your data using AES in Counter (CTR) mode. Simultaneously, it computes a cryptographic authentication tag using a specialized hash function called GHASH, built on Galois field (finite field) arithmetic. The combination, encryption plus built-in tamper detection, is called AEAD: Authenticated Encryption with Associated Data. It’s the default choice for TLS 1.2/1.3, IPsec, SSH, and disk encryption throughout the industry.

Table of Contents

Why Encryption Alone Isn’t Enough

Consider plain AES-CBC without any separate integrity check. An attacker who can’t read your encrypted traffic can still often flip specific bits of the ciphertext in predictable, exploitable ways. That causes the decrypted plaintext to change in a controlled fashion, without ever needing the key. Several real, practical TLS attacks (like the BEAST and Lucky 13 attacks against CBC mode) exploited exactly this class of weakness. AEAD modes close this gap entirely: any tampering with the ciphertext, even a single flipped bit, causes authentication to fail and the entire message to be rejected before decryption is ever trusted.

Counter (CTR) Mode: The Encryption Half

GCM’s encryption step uses AES in Counter mode. Rather than encrypting the plaintext directly, AES encrypts a sequence of counter values (derived from a nonce plus an incrementing counter). The resulting keystream is simply XORed with the plaintext. That turns the AES block cipher into a stream cipher, structurally similar in spirit to how ChaCha20 works, just built from AES rather than the ChaCha quarter-round function. Counter mode is fully parallelizable, requires no padding, and lets you decrypt or even randomly access any block independently.

GHASH: The Authentication Half

GHASH computes the authentication tag by processing the associated data and ciphertext through repeated multiplication in GF(2¹²⁸), a Galois field of 2¹²⁸ elements. It uses the specific reduction polynomial that GCM defines. Each 128-bit block of input is XORed into a running accumulator, which is then multiplied by a secret hash subkey H (itself just AES encrypting an all-zero block under the session key). This construction is fast in hardware: modern CPUs include dedicated PCLMULQDQ instructions specifically to accelerate this Galois field multiplication. It also provides a cryptographically strong authentication tag from relatively simple, well-understood finite-field math.

Interactive Visualizer

The visualizer above runs genuine AES-GCM: real AES encryption for the CTR-mode keystream, real GF(2¹²⁸) GHASH multiplication for the authentication tag. It was verified against Node.js’s own built-in AES-GCM implementation (including with associated data) before being published here. Try tampering with the ciphertext after encrypting to see authentication correctly fail.

Associated Data (AAD)

GCM supports authenticating data that isn’t actually encrypted: Associated Data (AAD). This is essential for real protocols. A TLS record’s header (sequence number, length, record type) needs to be protected from tampering, but doesn’t need to be secret, since it’s sent in the clear anyway. GCM folds the AAD into the same GHASH computation as the ciphertext, so any tampering with either the AAD or the ciphertext is caught by the same single authentication tag.

A Verified Example

Using one of the most widely cited NIST GCM test vectors:

  • Key: 16 zero bytes
  • IV: 12 zero bytes
  • Plaintext: 16 zero bytes
  • Ciphertext: 0388dace60b6a392f328c2b971b2fe78
  • Authentication tag: ab6e47d42cec13bdf53a67b21257bddf (16 bytes)

Every correct AES-GCM implementation, including the one running in the visualizer above, must reproduce this exact ciphertext and tag from these inputs.

Python Implementation

This is a genuine, complete AES-128-GCM implementation: real Rijndael S-box, key expansion, and MixColumns for the block cipher; real CTR-mode keystream generation; real GF(2¹²⁸) GHASH multiplication with GCM’s reduction polynomial, for the authentication tag. It reproduces the NIST test vector above exactly:

SBOX = [
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16,
]
RCON = [0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36]

def xtime(a):
    a <<= 1
    return (a ^ 0x11b) & 0xff if a & 0x100 else a

def gmul(a, b):
    p = 0
    for _ in range(8):
        if b & 1:
            p ^= a
        a = xtime(a)
        b >>= 1
    return p

def key_expansion(key):
    words = [list(key[4 * i:4 * i + 4]) for i in range(4)]
    for i in range(4, 44):
        temp = list(words[i - 1])
        if i % 4 == 0:
            temp = temp[1:] + temp[:1]
            temp = [SBOX[b] for b in temp]
            temp[0] ^= RCON[i // 4 - 1]
        words.append([words[i - 4][j] ^ temp[j] for j in range(4)])
    return [b''.join(bytes(words[r * 4 + c]) for c in range(4)) for r in range(11)]

def add_round_key(state, round_key):
    return bytes(s ^ k for s, k in zip(state, round_key))

def sub_bytes(state):
    return bytes(SBOX[b] for b in state)

def shift_rows(state):
    return bytes(state[((c + r) % 4) * 4 + r] for c in range(4) for r in range(4))

def mix_columns(state):
    out = bytearray(16)
    for c in range(4):
        a = state[c * 4:c * 4 + 4]
        out[c*4+0] = gmul(a[0],2) ^ gmul(a[1],3) ^ a[2] ^ a[3]
        out[c*4+1] = a[0] ^ gmul(a[1],2) ^ gmul(a[2],3) ^ a[3]
        out[c*4+2] = a[0] ^ a[1] ^ gmul(a[2],2) ^ gmul(a[3],3)
        out[c*4+3] = gmul(a[0],3) ^ a[1] ^ a[2] ^ gmul(a[3],2)
    return bytes(out)

def aes128_encrypt_block(key, block):
    round_keys = key_expansion(key)
    state = add_round_key(block, round_keys[0])
    for r in range(1, 10):
        state = mix_columns(shift_rows(sub_bytes(state)))
        state = add_round_key(state, round_keys[r])
    state = add_round_key(shift_rows(sub_bytes(state)), round_keys[10])
    return state

def gf128_mult(x, y):
    """Multiplication in GF(2^128) using GCM's bit ordering and reduction polynomial."""
    x, y = int.from_bytes(x, 'big'), int.from_bytes(y, 'big')
    z, v, R = 0, x, 0xE1000000000000000000000000000000
    for i in range(127, -1, -1):
        if (y >> i) & 1:
            z ^= v
        v = (v >> 1) ^ R if v & 1 else v >> 1
    return z.to_bytes(16, 'big')

def ghash(H, aad, ciphertext):
    pad16 = lambda d: d + b'\x00' * (-len(d) % 16)
    blocks = pad16(aad) + pad16(ciphertext)
    y = b'\x00' * 16
    for i in range(0, len(blocks), 16):
        y = gf128_mult(bytes(a ^ b for a, b in zip(y, blocks[i:i + 16])), H)
    len_block = (len(aad) * 8).to_bytes(8, 'big') + (len(ciphertext) * 8).to_bytes(8, 'big')
    return gf128_mult(bytes(a ^ b for a, b in zip(y, len_block)), H)

def inc32(block):
    counter = (int.from_bytes(block[12:], 'big') + 1) % (2 ** 32)
    return block[:12] + counter.to_bytes(4, 'big')

def gcm_encrypt(key, iv, plaintext, aad=b''):
    H = aes128_encrypt_block(key, b'\x00' * 16)
    J0 = iv + b'\x00\x00\x00\x01'  # standard 96-bit IV only

    counter = inc32(J0)
    ciphertext = bytearray()
    for i in range(0, len(plaintext), 16):
        keystream = aes128_encrypt_block(key, counter)
        ciphertext.extend(c ^ k for c, k in zip(plaintext[i:i + 16], keystream))
        counter = inc32(counter)
    ciphertext = bytes(ciphertext)

    tag = bytes(a ^ b for a, b in zip(ghash(H, aad, ciphertext), aes128_encrypt_block(key, J0)))
    return ciphertext, tag

if __name__ == "__main__":
    key, iv, plaintext = bytes(16), bytes(12), bytes(16)

    ciphertext, tag = gcm_encrypt(key, iv, plaintext)

    print(f"Ciphertext: {ciphertext.hex()}")
    print(f"Tag:        {tag.hex()}")

This reproduces the NIST test vector above exactly: ciphertext 0388dace60b6a392f328c2b971b2fe78, tag ab6e47d42cec13bdf53a67b21257bddf. I also cross-checked it against a second, non-trivial NIST GCM test vector with a real key, a real IV, and a 60-byte plaintext (not a multiple of the 16-byte block size), and against the pycryptodome library’s own AES-GCM, before writing this up.

Limitations

This code is a genuine, working AES-128-GCM implementation, not a simplified stand-in, but it’s still a teaching artifact, not something to deploy:

  • 96-bit IVs only. GCM’s spec defines a more involved derivation for other IV lengths; this code only implements the (overwhelmingly common) 96-bit case, matching the assertion in gcm_encrypt.
  • No constant-time guarantees. The GF(2¹²⁸) multiplication here is a straightforward bit-by-bit loop, not the constant-time, table-based, or PCLMULQDQ-accelerated implementations real cryptographic libraries use. On real hardware, naive finite-field multiplication like this can leak timing information about the hash subkey.
  • No nonce-management logic. As the Security Considerations section below stresses, nonce reuse is catastrophic for GCM; this code takes whatever IV it’s given and doesn’t track, randomize, or validate uniqueness across calls.
  • Never use this in production. This exists to make the algorithm’s mechanics inspectable in Python, not to replace a vetted library. Real applications should use their language’s standard cryptographic library (Python’s own cryptography package, for instance) for actual AES-GCM, exactly the same principle the visualizer’s own “verified against Node.js” claim above is built on.

Security Considerations

  • Never reuse a nonce with the same key. This is GCM’s single most important operational rule. Nonce reuse doesn’t just weaken confidentiality: it can let an attacker recover the authentication key entirely, breaking integrity protection for every message ever sent under that (key, nonce) pair. Most protocols either use a securely random 96-bit nonce or a carefully managed counter that’s guaranteed never to repeat.
  • Always check the tag before trusting the plaintext. Decrypting first and checking authentication after (rather than verifying, then decrypting) opens the door to padding-oracle-style timing attacks.
  • Short tags are dangerous. GCM supports truncated tags, but shortening below the standard 128 bits meaningfully increases forgery risk and should be avoided outside very specific, well-analyzed constraints.

Real-World Applications

  • TLS 1.2 and 1.3: AES-GCM is one of the two mandatory AEAD cipher suites in TLS 1.3 (the other being ChaCha20-Poly1305), protecting the vast majority of HTTPS traffic today.
  • IPsec: widely used to authenticate and encrypt VPN tunnel traffic.
  • Disk encryption: several full-disk and file encryption systems use AES-GCM (or closely related AEAD constructions) to detect tampering with stored ciphertext, not just hide its contents.
  • SSH: supported as one of several authenticated encryption cipher options in modern SSH implementations.

FAQ

What does “GCM” stand for?

Galois/Counter Mode. “Galois” refers to the GF(2¹²⁸) Galois field arithmetic used for authentication (GHASH), and “Counter” refers to the CTR-mode encryption.

Is AES-GCM better than AES-CBC?

For nearly all new systems, yes. GCM provides authentication (tamper detection) that plain CBC lacks entirely. That closes off an entire class of real-world attacks (like BEAST and Lucky 13) that have historically plagued CBC-based TLS implementations.

What happens if a nonce is reused with AES-GCM?

It’s catastrophic. Beyond weakening confidentiality (as with any CTR-mode cipher), nonce reuse can let an attacker recover GCM’s internal authentication key entirely. That lets them forge valid authentication tags for arbitrary messages going forward.

What’s the difference between AES-GCM and ChaCha20-Poly1305?

Both are AEAD constructions offering equivalent security guarantees. AES-GCM benefits enormously from hardware acceleration (AES-NI and PCLMULQDQ) on modern CPUs, while ChaCha20-Poly1305 performs better in pure software on devices lacking that hardware. That’s why TLS 1.3 supports both and negotiates based on what each side can accelerate.

Can GCM encrypt data without also authenticating it?

Not really. GCM is designed as a unified encrypt-and-authenticate operation. If you only need authentication without encryption, GCM supports AAD-only operation (authenticating data that stays in plaintext), but there’s no supported “GCM encryption with authentication disabled” mode, by design.

References

  1. NIST SP 800-38D. “Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC.” Available at: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf

  2. McGrew, D. and Viega, J. “The Galois/Counter Mode of Operation (GCM).” This is the original GCM design paper, including the widely-used reference test vectors.

  3. Wikipedia. “Galois/Counter Mode.” Available at: https://en.wikipedia.org/wiki/Galois/Counter_Mode

  4. RFC 5116. “An Interface and Algorithms for Authenticated Encryption.” IETF, 2008. Defines the general AEAD interface GCM implements.