skip to content

How does cipher.NewGCMWithRandomNonce change the Seal and Open calls in Go?

level: middleimportance: nice to knowfreq 25%

answer

  1. who picks the nonce now
  2. NonceSize can legitimately be zero
  3. the overhead grew by twelve bytes
  4. the prefix is applied for you
  5. AES only, and Go 1.24 or newer

basics

~20 s

An AEAD from cipher.NewGCMWithRandomNonce generates its own 96-bit nonce inside Seal and prepends it to the ciphertext, and Open strips it back off. Its NonceSize() reports 0, its Overhead() is 28, and the nonce argument must be empty.

solid answer

~40 s

`cipher.NewGCMWithRandomNonce`, added in Go 1.24, takes the nonce out of the caller's hands. It wraps a block from `aes.NewCipher` only, and the AEAD it returns reports `NonceSize()` of **0** and `Overhead()` of **28** — 12 bytes of nonce plus the 16-byte tag. Each `Seal` call generates a fresh random 96-bit nonce internally and prepends it to the returned ciphertext, and `Open` reads it back off the front, so both are called with an empty nonce argument and neither the framing nor the nonce plumbing appears in your code. The practical differences from `cipher.NewGCM` are that you size buffers with `Overhead()` of 28 rather than 16, you no longer split the record at `NonceSize()` yourself, and the AEAD is AES-only rather than accepting any 128-bit block cipher.

code

go · 9 lines
go
gcm, err := cipher.NewGCMWithRandomNonce(block) // block from aes.NewCipher
if err != nil {
	return err
}

record := gcm.Seal(nil, nil, chunk, nil)
// len(record) == len(chunk) + gcm.Overhead() // Overhead() is 28

back, err := gcm.Open(nil, nil, record, nil)

go deeper

for a junior

Know that Go has a GCM constructor that handles nonces for you, and that with it you pass an empty nonce to Seal and Open. Recognise that NonceSize() of 0 is normal, not a bug.

for a middle

Explain the three concrete API differences — NonceSize() of 0, Overhead() of 28, an empty nonce argument — and what the constructor does inside Seal and Open to earn them.

for a senior

Judge when it fits: it removes the framing bug where writer and reader disagree about where the nonce lives, but it is AES-only and cannot serve a format that dictates the nonce.

for a principal

Weigh the migration: records stay byte-compatible with the hand-framed layout, so the win is deleted caller code and one fewer format decision per team, against a Go 1.24 floor for every service that reads or writes those records.

## What the constructor does ```go block, err := aes.NewCipher(key) if err != nil { return err } gcm, err := cipher.NewGCMWithRandomNonce(block) ``` Added in **Go 1.24**, `cipher.NewGCMWithRandomNonce` returns a `cipher.AEAD` that manages nonces itself. The block cipher must be one created by `aes.NewCipher`; it is not a general wrapper around any 128-bit `cipher.Block` the way `cipher.NewGCM` is. ## The three numbers that change | | `cipher.NewGCM` | `cipher.NewGCMWithRandomNonce` | |---|---|---| | `NonceSize()` | 12 | 0 | | `Overhead()` | 16 | 28 | | nonce argument | 12 bytes, caller-supplied | empty | `NonceSize()` of 0 is the signal that the caller has nothing to supply: `Seal` and `Open` are called with an empty nonce argument. `Overhead()` of 28 is 12 + 16 — the nonce that `Seal` prepends plus the authentication tag it appends — so a record is `len(plaintext) + 28` bytes. Code that sized buffers by calling `Overhead()` keeps working unchanged; code that hard-coded 16 does not. ## The calls ```go record := gcm.Seal(nil, nil, chunk, nil) // record == nonce || ciphertext || tag, len(chunk)+28 bytes chunk, err := gcm.Open(nil, nil, record, nil) ``` Compare that with the `cipher.NewGCM` version, where you had to obtain 12 bytes of nonce, pass them as both `dst` and `nonce` to get the prefix, and then split the record at `NonceSize()` on the way back in. All of that framing disappears, along with the two places it could be written inconsistently — a writer that prepends the nonce and a reader that expects it in a metadata field will produce records nobody can decrypt, and that bug is exactly what this constructor removes. `dst` still behaves the same way: it is an append target, so `Seal(nil, nil, chunk, nil)` allocates, and passing a slice with spare capacity appends into it. ## Interoperability The layout is the same one the hand-rolled idiom produces: a 12-byte nonce, then the ciphertext, then the 16-byte tag. A record written by `gcm.Seal(nonce, nonce, chunk, nil)` on a `cipher.NewGCM` AEAD is byte-for-byte what a `cipher.NewGCMWithRandomNonce` AEAD's `Open` expects, so a job can migrate its writer and keep reading its own archive. What is not interoperable is any other framing — a nonce stored beside the object rather than inside it, a nonce of a non-standard length, or a nonce written after the ciphertext. ## When it is the wrong tool It is not a drop-in when the nonce has to be something specific rather than random: a deterministic nonce derived from a record counter, a nonce dictated by an existing on-disk format, or one supplied by another system. It is also AES-only, so code that is generic over `cipher.Block` cannot use it. And because `Overhead()` grew, any format whose record size was fixed at `n + 16` needs its sizing recomputed. For a batch job encrypting archive chunks, though, it removes the most error-prone lines in the file: the nonce buffer, its length, and the split. Less caller code around a cryptographic primitive is worth having on its own terms.

  • Why does Overhead() report 28 rather than 16 for this AEAD?
    Because the record carries the nonce as well as the tag: 12 bytes of prepended nonce plus the 16-byte GCM tag. `Overhead()` is defined as how much longer the ciphertext is than the plaintext, and for this constructor the nonce is part of the ciphertext. Code that sizes buffers from `Overhead()` needs no change; code that hard-coded 16 will now under-allocate.
  • Can an AEAD from cipher.NewGCMWithRandomNonce read records written with plain cipher.NewGCM?
    Yes, if the writer used the standard framing — a 12-byte nonce prepended to the ciphertext and tag, which is what `Seal(nonce, nonce, plaintext, nil)` produces. The layout is identical, so `Open` finds the nonce where it expects it. It cannot read records whose nonce was stored separately, was a non-standard length, or was placed after the ciphertext.
  • When would you still reach for cipher.NewGCM instead?
    When the nonce is not yours to choose: an existing on-disk or wire format that dictates its position or length, a deterministic nonce derived from a record index, or a value handed to you by another system. Also when the code is generic over `cipher.Block`, since the random-nonce constructor accepts only a block from `aes.NewCipher`.

saying these in an interview costs you the question

  • Still allocates and passes a 12-byte nonce argument
  • Sizes records with an Overhead of 16
  • Expects it to wrap any 128-bit cipher.Block
  • Prepends the nonce again by hand, doubling it
  • Assumes it exists in older Go toolchains