skip to content

In Go's AES-GCM API, why is Seal(nonce, nonce, plaintext, nil) the idiomatic way to prepend a nonce?

level: middleimportance: must knowfreq 45%

answer

  1. Seal behaves like the append builtin
  2. the first argument is kept, not overwritten
  3. the nonce is not secret
  4. dst becomes the record's header
  5. NonceSize and Overhead give the sizing

basics

~20 s

Seal appends to its dst argument and returns the extended slice, like the append builtin. Passing the nonce as dst therefore yields one buffer laid out as nonce, ciphertext, tag — the standard framing, since the nonce is not secret.

solid answer

~50 s

`Seal(dst, nonce, plaintext, additionalData)` does not write into a preallocated output buffer; it **appends** the sealed bytes to `dst` and returns the resulting slice, so `dst` is a prefix you want to keep. Passing the nonce as `dst` gives you `nonce || ciphertext || tag` in one allocation, which is the layout every reader can parse: split at `gcm.NonceSize()`, pass the first part as the nonce and the rest to `Open`. It works because the nonce is not secret — it only has to be unique per key and available to the decrypter. Sizing follows from the interface: the record is `NonceSize() + len(plaintext) + Overhead()` bytes, 12 + n + 16 for standard AES-GCM. And the nonce argument itself must be exactly `NonceSize()` bytes; a wrong length is a programming error and `Seal` panics.

code

go · 10 lines
go
// nonce is exactly gcm.NonceSize() bytes, unique for this chunk
record := gcm.Seal(nonce, nonce, chunk, nil)
// record == nonce || ciphertext || tag

// reader side
n := gcm.NonceSize()
if len(record) < n+gcm.Overhead() {
	return nil, errShortRecord
}
chunk, err := gcm.Open(nil, record[:n], record[n:], nil)

go deeper

for a junior

Remember that Seal's first argument is appended to, not written into, and that a nonce travels with the ciphertext rather than being kept secret. Recognise Seal(nonce, nonce, plaintext, nil) when you see it.

for a middle

Explain the append convention, why the same slice can serve as both dst and nonce, and how NonceSize() and Overhead() give you the exact record length without hard-coded constants.

for a senior

Show the decrypt path defending itself: a length check before slicing the record, so a truncated or corrupt object returns an error instead of panicking, and a capacity chosen so Seal never reallocates in a hot loop.

for a principal

Own the record format itself. Decide once whether the nonce is framed in the payload or carried in storage metadata, write it down, and make every service read the same layout so records stay decryptable across teams and versions.

## dst is an append target, not an output buffer The signature is: ```go Seal(dst, nonce, plaintext, additionalData []byte) []byte ``` Almost every mistake with this call comes from reading `dst` as "the buffer the result will be written into, which I must size correctly". It is not. `Seal` follows the same convention as the `append` builtin and as `hash.Hash.Sum`: it appends its output to `dst` and returns the extended slice. Whatever was already in `dst` is **kept**, and sits in front of the output. That has two consequences. First, `Seal(nil, ...)` is a perfectly good call and simply allocates a fresh slice. Second, anything you put in `dst` beforehand becomes a prefix of the record for free — which is precisely what you want for the nonce. ## Why the nonce goes in front The decrypting side must pass `Open` the same nonce that `Seal` used, so the nonce has to reach it somehow. It does not have to be kept secret — its requirement is uniqueness per key, not confidentiality — so the simplest transport is to ship it with the ciphertext. Putting it first makes parsing trivial, because `NonceSize()` is a constant for a given AEAD: ```go n := gcm.NonceSize() if len(record) < n { return nil, errShort } plain, err := gcm.Open(nil, record[:n], record[n:], nil) ``` The length check matters: slicing a record shorter than the nonce panics, and a corrupt or truncated object in storage is exactly the input that will produce one. ## The one-line idiom Putting those together gives the form that appears in the `crypto/cipher` documentation itself: ```go record := gcm.Seal(nonce, nonce, chunk, nil) ``` The same slice is passed twice, in two different roles: as `dst`, the prefix to append to, and as `nonce`, the value that seeds the counter. This is safe because `Seal` consumes the nonce before it writes anything after `dst`'s length, and the output region begins past the end of the nonce. It reads oddly the first time, which is why it is worth being able to explain rather than merely copy. ## Sizing the buffer The `cipher.AEAD` interface exposes exactly the two numbers you need to size a record without guessing: - `NonceSize()` — 12 for an AEAD from `cipher.NewGCM`. - `Overhead()` — 16 for GCM, the authentication tag. So the finished record is `NonceSize() + len(plaintext) + Overhead()` bytes. In a batch job that seals a stream of archive chunks, allocating with that capacity up front means `Seal` never has to grow the slice: ```go out := make([]byte, gcm.NonceSize(), gcm.NonceSize()+len(chunk)+gcm.Overhead()) copy(out, nonce) record := gcm.Seal(out, out, chunk, nil) // no reallocation ``` If capacity is short, `Seal` allocates a bigger array and copies, exactly as `append` would — correct, just not free. Hard-coding 12 and 16 instead of calling the methods works today for standard GCM and breaks the day someone swaps in `cipher.NewGCMWithNonceSize` or a different AEAD behind the same interface. ## The nonce length is checked with a panic `Seal` requires `len(nonce) == NonceSize()`. A shorter or longer nonce is not an error return — it is a panic, because it is a programming mistake rather than a runtime condition. The practical consequence is that a nonce read from a decoded header must be length-checked before it reaches `Seal` or `Open`, or a malformed input turns into a panicking goroutine. On the `Open` side, a wrong-length nonce panics for the same reason, so validate the record's length first and return your own error. ## What this idiom is not It is not a way to hide the nonce, and it is not a substitute for `Open`'s error check. It is a framing convention: a self-describing record whose fixed-size header is a nonce. If your storage format already carries the nonce in a separate metadata field, you can pass `nil` as `dst` and keep the two apart — the API is indifferent. What matters is that the decrypter can reconstruct the exact nonce bytes, byte for byte, that `Seal` was given.

  • How does the decrypting side know where the nonce ends?
    From `NonceSize()`, which is a constant for a given `cipher.AEAD` — 12 for standard AES-GCM. The reader slices `record[:n]` as the nonce and `record[n:]` as the ciphertext-plus-tag. Because the length is fixed rather than encoded, the record needs no length prefix, but the reader must check that it is at least `NonceSize() + Overhead()` bytes before slicing, or a truncated object panics.
  • What happens if you pass an 8-byte nonce to an AEAD from cipher.NewGCM?
    `Seal` panics. The nonce length is part of the API contract, not a runtime condition, so it is enforced with a panic rather than an error return. The same is true of `Open`. This is why a nonce parsed out of an untrusted record must be length-checked in your own code first; otherwise a malformed input becomes a panic in the goroutine doing the decrypting.
  • Why not just hard-code 12 and 16 when sizing the record?
    It works for an AEAD from `cipher.NewGCM` and silently breaks for anything else. `cipher.NewGCMWithNonceSize` and `cipher.NewGCMWithTagSize` change those numbers, and `cipher.NewGCMWithRandomNonce` reports a `NonceSize()` of 0 with an `Overhead()` of 28. Calling the methods costs nothing and keeps the code correct against the interface rather than against one implementation.

saying these in an interview costs you the question

  • Reads dst as a preallocated output buffer
  • Thinks passing dst pre-sized makes Seal write in place
  • Believes the nonce must be kept secret
  • Assumes Seal always prepends the nonce for you
  • Hard-codes 12 and 16 instead of NonceSize and Overhead
  • Slices a record at NonceSize without a length check