Encryption & Privacy

How AES-256-GCM Works, and How Files Use It

By SRIFT maintainers · Published

Reviewed by SRIFT maintainers · last reviewed against the SRIFT 4.3.2 source.

Short answer

AES-256-GCM is authenticated encryption built from two parts: the AES block cipher run in counter mode, which turns a 256-bit key and a 96-bit nonce into a keystream that is XORed with the data, and GHASH, a polynomial hash over the ciphertext and any associated data that produces a 128-bit authentication tag. Decryption fails if a single bit of ciphertext, nonce, tag or associated data changes.

Its one hard rule is that a nonce must never repeat under the same key: a repeat leaks the XOR of two plaintexts and lets an attacker forge messages. Encrypting a large file safely also means binding each chunk's position and the end of the stream into the authentication, so chunks cannot be reordered, dropped or truncated.

Key
256 bits (AES-256: 14 rounds, 128-bit block)
Nonce (IV)
96 bits recommended; must be unique per key
Tag
128 bits by default
Random-nonce limit
2³² encryptions per key (NIST SP 800-38D)

AES itself: a block cipher, not an encryption scheme

AES (FIPS 197) is a keyed permutation of one 16-byte block. It accepts 128-, 192- or 256-bit keys and runs 10, 12 or 14 rounds respectively; the block is 128 bits in every case. On its own it can encrypt exactly 16 bytes, and encrypting the same block twice under the same key gives the same output. A mode of operation is what turns it into something that can encrypt a file.

The “256” is the key size. A 256-bit key puts brute force far out of reach and leaves a wide margin against multi-target attacks and against Grover's algorithm on a future quantum computer, which would roughly halve the effective key length. It does not change the 128-bit block size, and the block size is what drives GCM's usage limits below.

How GCM encrypts and authenticates

AES-GCM: counter-mode encryption plus a GHASH authentication tagWith a 96-bit IV, the first counter block is the IV followed by the value 1. AES under key K encrypts successive counter blocks; the outputs for counters 2 and 3 are XORed with plaintext blocks P1 and P2 to give ciphertext blocks C1 and C2. GHASH, keyed with H equal to AES of the all-zero block, absorbs the associated data, the ciphertext and a lengths block. Its output is XORed with AES of the first counter block to give the 128-bit tag.IV ‖ 0…01 (J₀)IV ‖ 0…02IV ‖ 0…03AES-256 (K)AES-256 (K)AES-256 (K)P₁P₂C₁C₂keystreamGHASH under HAAD, C₁, C₂, len(AAD) ‖ len(C)AADHAES_K(0¹²⁸)AES_K(J₀)Tag T (128 bits)Output: IV, C₁ C₂ …, T. The AAD is authenticated but not encrypted, and is not sent by GCM itself.
GCM with a 96-bit IV: counter blocks are encrypted to make a keystream; GHASH over the AAD and ciphertext, masked with AES_K(J₀), gives the tag.

Counter mode for confidentiality

Galois/Counter Mode is defined in NIST SP 800-38D. With the recommended 96-bit IV, the first counter block J₀ is the IV followed by a 32-bit counter set to 1. Data blocks are encrypted with counters 2, 3, 4 and so on: AES encrypts each counter block, and the output is XORed with 16 bytes of plaintext. Because only the counter is fed through AES, encryption is parallel, needs no padding (the last keystream block is simply truncated) and makes ciphertext exactly as long as the plaintext.

GHASH for integrity

The hash key H is AES applied to the all-zero block. GHASH treats the associated data, the ciphertext and a final block holding both lengths as coefficients of a polynomial and evaluates it at H in the field GF(2¹²⁸). The result is XORed with AESK(J₀) to give the tag. Decryption recomputes the tag and must reject the message, releasing no plaintext, if it differs.

Associated data

The additional authenticated data (AAD) is authenticated but not encrypted and not transmitted by GCM. Both sides must construct it independently. That makes it the right place for context the receiver already knows or can check: a protocol version, a file identifier, a chunk index. If the receiver's idea of the context differs from the sender's, authentication fails. This is the AEAD interface standardised in RFC 5116.

One invocation can encrypt at most 2³⁹ − 256 bits of plaintext, a little under 64 GiB. Large files therefore need to be split into chunks even before memory and streaming enter the picture.

Why a nonce must never repeat

GCM is a stream cipher with a MAC attached, and both halves fail if a nonce repeats under the same key.

  • Keystream reuse. Two messages encrypted with the same key and IV are XORed with the same keystream, so C₁ ⊕ C₂ = P₁ ⊕ P₂. Knowing or guessing parts of one plaintext reveals the same positions of the other, the classic two-time-pad failure.
  • Authentication key recovery. Two tags computed with the same nonce share the same mask AESK(J₀). XORing them cancels the mask and leaves a polynomial equation in H whose roots an attacker can find. With H in hand, the attacker can compute valid tags for modified ciphertexts. Antoine Joux described this “forbidden attack” in 2006, and Böck et al. found TLS servers on the internet that repeated GCM nonces and were open to it.

How to keep nonces unique

SP 800-38D allows two IV constructions, and real systems add a third strategy:

  1. Deterministic: a counter or structured value that the sender guarantees never repeats. Safe for any volume, but the counter must survive crashes and must not be shared between two senders using one key.
  2. Random 96-bit IVs: stateless, but collisions become likely as volume grows. NIST caps a key at 2³² invocations with random IVs, which keeps the collision probability below 2⁻³².
  3. A fresh key per message or per file. If every file is encrypted under its own random or freshly derived key, nonces only need to be unique within that one file, and the per-key limits stop mattering in practice.

Where nonce uniqueness cannot be guaranteed, a misuse-resistant mode such as AES-GCM-SIV (RFC 8452) limits the damage of a repeat to revealing that two identical messages were sent.

Encrypting a large file in chunks

Chunking is unavoidable for large files: it bounds memory, lets the receiver write data as it arrives, keeps each call under GCM's length limit and lets a transfer retry a single chunk. But encrypting each chunk as an independent GCM message is not enough, because each tag only proves that that chunk is genuine. An attacker who can modify the stream could still:

  • Reorder chunks, or duplicate one in place of another;
  • Truncate the file by dropping the last chunks, leaving a shorter file in which every chunk verifies;
  • Splice chunks from another file encrypted under the same key.

The fix is to make each chunk's tag depend on its position, on whether it is the last one, and on which file it belongs to. Position and file identity can go in the AAD or the nonce; the end of the stream needs either a total chunk count bound into every chunk or a flag that marks the final chunk.

The STREAM construction

Hoang, Reyhanitabar, Rogaway and Vizár formalised this in “Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance” (CRYPTO 2015). Their STREAM construction builds each chunk's nonce from a per-stream prefix, a chunk counter and a one-bit last-chunk flag, and proves that the resulting scheme detects reordering, deletion and truncation. The decryptor still releases plaintext chunk by chunk, so it only knows the file is complete when it has verified a chunk carrying the final flag.

A correct WebCrypto example

The browser's Web Crypto API exposes AES-GCM directly (see AesGcmParams). This version uses a fresh key per file, a random 96-bit IV per chunk, and binds the file, chunk index and chunk count in the AAD:

chunk-crypto.js
// One key per file; an IV must never repeat under the same key.
const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,                 // extractable, so it can be wrapped for the recipient
  ["encrypt", "decrypt"],
)

const enc = new TextEncoder()
const aad = (fileId, index, total) => enc.encode(`file|${fileId}|${index}|${total}`)

async function sealChunk(key, fileId, index, total, plaintext) {
  const iv = crypto.getRandomValues(new Uint8Array(12))   // 96-bit nonce
  const ct = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv, additionalData: aad(fileId, index, total), tagLength: 128 },
    key,
    plaintext,
  )
  return { iv, ct }     // ct is the ciphertext followed by the 16-byte tag
}

async function openChunk(key, fileId, index, total, { iv, ct }) {
  // Rejects with an OperationError if the key, IV, AAD, ciphertext or tag do not match.
  return crypto.subtle.decrypt(
    { name: "AES-GCM", iv, additionalData: aad(fileId, index, total), tagLength: 128 },
    key,
    ct,
  )
}

The receiver must also check completeness: every index from 0 to total - 1 exactly once. The AAD stops a chunk from verifying at the wrong position or in a file of a different length; it cannot make a missing chunk appear.

Two real designs: SRIFT relay chunks and encrypted links

SRIFT uses AES-256-GCM in two places that took the two approaches above. Both are open source (lib/e2e-keys.ts and cli/e2ee.ts), so the details below can be checked.

AES-256-GCM chunk formats in SRIFT 4.3.2.
Relayed file chunks (pgcm1)Encrypted quick-share links (SRE1)
KeyPer transfer: ECDH P-256 between fresh key pairs on each side, then HKDF-SHA25632 random bytes per link, carried in the URL fragment (#k=); optionally mixed with a PBKDF2 password key via HKDF
NonceRandom 12 bytes per chunk7-byte random prefix ‖ 32-bit chunk counter ‖ 1-byte last-chunk flag
AADpgcm1|fileId|chunkIndex|totalChunksThe 36-byte file header
End of streamTotal chunk count in every chunk's AAD; the receiver expects that manyLast-chunk flag in the nonce (STREAM)
Chunk size256 KB1 MiB by default
Wire layoutsender public key (65) ‖ IV (12) ‖ ciphertext ‖ tag (16)header ‖ encrypted metadata ‖ chunks, each ciphertext ‖ tag

The relay format takes the random-IV route, which is safe here because the key is unique to one transfer: even the 2³² random-IV budget would cover about a pebibyte of 256 KB chunks. Binding the chunk index and total in the AAD means a relay that reorders, duplicates or drops chunks makes the transfer fail or stall instead of producing a wrong file.

The link format follows STREAM. Counter 0 encrypts a small metadata block (file name, size and MIME type, so the relay never sees the name), and the file chunks use counters 1 to n, with the flag set only on the last. Since every link has its own random key, the nonce prefix only has to be unique within one file.

Common mistakes with AES-GCM

  • Resetting a nonce counter after a restart, or using one counter in two processes that share a key.
  • Encrypting many files under one long-lived key with random IVs and never counting invocations. Derive a key per file or per session instead.
  • Short tags. SP 800-38D permits tags down to 96 bits, and shorter only under strict conditions; there is rarely a reason to use anything but 128.
  • Acting on plaintext before the tag is checked. Streaming decryptors that hand out bytes before verification reintroduce the attacks GCM exists to stop.
  • Using a password as the key. A password needs a slow key derivation function (PBKDF2, scrypt or Argon2) with a salt; even then it is the weakest part of the system.
  • Independent chunks. Per-chunk GCM without position and end-of-stream binding allows reordering and truncation, as shown above.

For how these keys are stored and who can hold them, see client-side vs server-side encryption.

Frequently asked questions

Is AES-256-GCM better than AES-128-GCM?

Both are considered secure. AES-256 has a larger safety margin, including against future quantum attacks, at the cost of 14 rounds instead of 10. The nonce rules and usage limits are the same for both, because they depend on the 128-bit block size, not the key size.

Does the nonce need to be secret?

No. The nonce is sent alongside the ciphertext in the clear. It only needs to be unique for each encryption under a given key; random 96-bit values are fine within NIST's limit of 2^32 encryptions per key.

How does AES-GCM compare with ChaCha20-Poly1305?

Both are AEAD ciphers with a 96-bit nonce and a 128-bit tag, and both fail badly on nonce reuse. AES-GCM is very fast on CPUs with AES and carry-less multiplication instructions; ChaCha20-Poly1305 is faster in software on devices without them. Neither is weaker in practice; the choice is usually about hardware and what the platform's crypto library offers.

References

  1. SP 800-38D: Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode (GCM) and GMAC (NIST)
  2. FIPS 197: Advanced Encryption Standard (AES) (NIST)
  3. RFC 5116: An Interface and Algorithms for Authenticated Encryption (IETF)
  4. Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance (STREAM) (IACR ePrint)
  5. Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS (IACR ePrint)
  6. RFC 8452: AES-GCM-SIV: Nonce Misuse-Resistant Authenticated Encryption (IETF)
  7. AesGcmParams (MDN)

See exactly what SRIFT encrypts

Every key, algorithm and metadata field — including what servers can see and what the design does not stop.