Learn · Technologies we use
AES-GCM, explained
The mode that seals every cipher.ink message, how its tag works, and the one rule you must never break with it.
AES-GCM (AES in Galois/Counter Mode) is a way of using the AES block cipher that does two jobs at once. It encrypts a message, and it attaches a 128-bit tag that lets the receiver detect any change to the message, or a wrong key. It seals every cipher.ink message and carries most of the web's encrypted traffic. It also comes with exactly one rule you must never break: never encrypt two different messages with the same key and the same nonce. Below we build the mode from its two parts, work one encryption through with real numbers, show what the tag buys you, and then show what happens when the rule is broken.
Two ideas stuck together
GCM is counter mode for secrecy plus a polynomial hash called GHASH for authenticity. AES alone scrambles just one 16-byte block at a time, so encrypting a message of any length takes a mode of operation, a recipe for applying it repeatedly. (The modes article compares ECB, CBC, CTR and GCM.) The older popular recipes, such as CBC, hide the message but say nothing about whether anyone tampered with it, so people bolted a separate checksum on afterward and often got it wrong. An authenticated encryption (AEAD) mode builds the checking into the cipher. GCM, designed by David McGrew and John Viega and standardized by NIST in 2007 as SP 800-38D, is the AEAD mode that won. TLS 1.3 requires it, it runs very fast on modern processors, and every browser implements it in Web Crypto.
Counter mode: AES as a keystream generator
In counter mode the message never goes through AES at all. You encrypt a counter instead: a 16-byte block made of the 12-byte nonce (also called the IV) followed by a 4-byte number that counts 2, 3, 4, and so on. Each encrypted counter block gives 16 bytes that look random, and the stream of them is the keystream. XOR the keystream with the plaintext to get the ciphertext, and XOR it with the ciphertext again to get the plaintext back. That turns a block cipher into a stream cipher, with a handy side effect: the ciphertext is exactly as long as the plaintext, and no padding is needed.
The counter starts at 2, not 0 or 1, because block number 1 is reserved. That block (the nonce followed by 1, called J0 in the standard) is encrypted separately, and its output masks the tag at the very end.
GHASH: the part that makes it authenticated
Counter mode alone is malleable. Flip a bit of the ciphertext and the same bit flips in the plaintext, and nothing notices. (The authenticated encryption article demonstrates this with an interactive example.) GHASH closes that hole. It's a keyed hash, and its key is H, the encryption of an all-zero block, H = AESK(0). GHASH treats each 16-byte block of the data as a number in a finite field with 2128 elements, and computes
where each Xi is a block (any associated data first, then the ciphertext, then a final block holding the two lengths), "⊕" is XOR, and "·" is multiplication in GF(2128), defined by the polynomial x128 + x7 + x2 + x + 1. The result of the last step is XORed with the encrypted J0 block, and that's the tag:
Every step multiplies by a secret H, so nobody without the key can produce a ciphertext and a matching tag, short of guessing the tag. The multiplication is cheap on processors with a carry-less multiply instruction (PCLMULQDQ on x86, introduced alongside the AES instructions in Intel's 2010 Westmere chips), so GCM runs at several gigabytes a second per core. The receiver recomputes the tag from the ciphertext it got and compares. If they differ, it must not output any plaintext at all.
A worked example, with real numbers
Here's one complete AES-GCM encryption, using a 128-bit key to keep the numbers short (cipher.ink uses a 256-bit key, and only the number of AES rounds changes). scripts/examples/gcm-worked.mjs computed every value below. It implements the GF(2128) multiplication from scratch, checks the result against OpenSSL, and reproduces McGrew and Viega's published test case 2.
| Step | Value (hex) |
|---|---|
| Key | 000102030405060708090a0b0c0d0e0f |
| Nonce (96 bits) | 000102030405060708090a0b |
| Plaintext, "attack at dawn!!" | 61747461636b206174206461776e2121 |
| H = AESK(0128) | c6a13b37878f5b826f4f8162a1c8d879 |
| Counter block 2 = nonce ‖ 00000002 | 000102030405060708090a0b00000002 |
| Keystream = AESK(counter block 2) | 936ca7ce661bf7544bd2618a36a37008 |
| Ciphertext = plaintext ⊕ keystream | f218d3af0570d7353ff205eb41cd5129 |
| Length block (0 bits of data to authenticate, 128 bits of ciphertext) | 00000000000000000000000000000080 |
| GHASH(H, ciphertext, lengths) | 631a467669814c5d45708a8a89af3474 |
| AESK(J0), J0 = nonce ‖ 00000001 | 435b9ba12d75a4be8a977ea3cd011890 |
| Tag = GHASH ⊕ AESK(J0) | 2041ddd744f4e8e3cfe7f42944ae2ce4 |
The ciphertext you send is f218d3af…cd5129 and the tag is 2041ddd7…ae2ce4, exactly what openssl and Web Crypto produce for those inputs. In the browser, Web Crypto's encrypt returns the ciphertext with the tag appended, 16 bytes longer than the message, and that's what cipher.ink stores in the payload field c.
What the tag buys you
Open a cipher.ink message with the wrong passphrase and nothing garbled appears. The page says "That passphrase doesn't open it." That's the tag at work. A wrong passphrase makes a different key, so the receiver computes a different H and a different keystream, its recomputed tag doesn't match the stored one, and crypto.subtle.decrypt throws instead of returning bytes. Compare the 2016 messages, which used a mode without a tag (OFB, see how we still open them). There, a wrong passphrase decrypts to noise, and the page has to guess the noise is wrong by checking whether the result is valid text. The tag turns a guess into a proof, which is why we write nothing new in the old format.
The tag also guards against someone altering the ciphertext on the server or in transit. Change one bit of the stored payload and every future open fails. It can't stop a server from deleting a message or refusing to serve it, though, since authentication detects change but can't force delivery.
The one rule: never reuse a nonce
The nonce is public. It's written next to the ciphertext in the clear. What matters is that it's never used twice with the same key. If it is, both messages get the same keystream, and XORing the two ciphertexts cancels it out and leaves the XOR of the two plaintexts, which leaks a great deal. Here's a real run, using the key and nonce from the worked example, encrypting two different 17-byte messages:
| Quantity | Value (hex) |
|---|---|
| C₁ ⊕ C₂ | 000000000c0d050f0a525d1114091e0000 |
| P₁ ⊕ P₂ for "pay Alice $100.00" and "pay Mallory $9000" | 000000000c0d050f0a525d1114091e0000 |
The two lines are identical: the keystream canceled. An eavesdropper who knows or guesses one message (a standard header, say, or a line of a form letter) can now read the other. XOR C₁ ⊕ C₂ with "pay Alice $100.00" and "pay Mallory $9000" falls out at once. Reuse also exposes the authentication key, which is worse. In 2006 Antoine Joux showed that two messages under one nonce give an equation in H that can be solved, and then the attacker can forge tags for messages of their choosing. People call this the forbidden attack, and it's why NIST's text says the IV must not repeat. It isn't only theoretical, either. In 2016 Böck and colleagues found HTTPS servers on the public internet that repeated GCM nonces, and showed they could forge content on them.
Limits to know about
- One message can be about 64 GiB. The 32-bit counter allows 232 − 2 blocks per nonce, which is 239 − 256 bits, roughly 68.7 GB.
- The nonce should be 96 bits. Other lengths work, but the standard handles them with more caution, and Web Crypto recommends 12 bytes.
- Random 96-bit nonces stop being safe around 232 messages per key, because of the birthday problem. NIST requires staying below that. NIST is revising SP 800-38D, and some cryptographers argue random 96-bit nonces should be discouraged altogether.
- The tag can be shortened to 96 bits or less, but each bit removed makes forgery easier. Use 128.
- GCM doesn't hide the length of the message, and it authenticates the ciphertext and not who sent it. Anyone with the key can create valid messages.
How cipher.ink uses this
Every message is sealed with AES-256-GCM: a 256-bit key, a random 12 bytes (96 bits) IV, and a 128-bit tag. The key is new for every message. In secret-link mode it is 32 random bytes, and in passphrase mode it's derived from the passphrase and a fresh random 16-byte salt, so even the same passphrase gives a different key each time. A message and its attachments use that one key for at most 11 encryptions, each with its own freshly drawn IV, so the 232 limit is nowhere near. You can read the exact lines in the annotated source.
The IV comes from crypto.getRandomValues. The header of that file is blunt about the older version of the site that didn't do this, and random numbers in the browser tells that story. For the rest of the design, including how the key reaches the reader and what the server can and can't see, read how cipher.ink works.
Further reading
- NIST SP 800-38D: Recommendation for GCM and GMAC (2007)
- McGrew and Viega: The Security and Performance of the Galois/Counter Mode (GCM) of Operation (2004)
- Joux: Authentication Failures in NIST version of GCM (2006)
- Böck et al.: Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS (2016)
- RFC 8446: TLS 1.3 (requires TLS_AES_128_GCM_SHA256)
- RFC 8452: AES-GCM-SIV: Nonce Misuse-Resistant Authenticated Encryption
- MDN: SubtleCrypto.encrypt(), AES-GCM parameters