skip to content

AEAD Encryption and Nonces

cipher.AEAD gives you Seal and Open with an authentication tag attached, plus NonceSize bytes you must supply yourself. Interviewers ask whether you reach for cipher.NewGCMWithRandomNonce.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

How do you build an AES-GCM cipher.AEAD in Go and encrypt one message with it?

level: juniorimportance: must knowfreq 50%

answer

  1. two packages, two constructors
  2. a block cipher is not a mode
  3. the mode lives in crypto/cipher
  4. Seal takes dst, nonce, plaintext, extra data
  5. the tag rides on the ciphertext

basics

~10 s

Pass the key to aes.NewCipher to get a cipher.Block, wrap that block with cipher.NewGCM to get a cipher.AEAD, then call Seal(dst, nonce, plaintext, additionalData). Seal returns the ciphertext with a 16-byte authentication tag appended.

solid answer

~40 s

It is two constructors. `aes.NewCipher(key)` returns a `cipher.Block` for a 16, 24 or 32-byte key (AES-128/192/256), and a bare block encrypts one 16-byte block, not a message. `cipher.NewGCM(block)` wraps it into a `cipher.AEAD`, which is the interface you actually use: `NonceSize()`, `Overhead()`, `Seal(dst, nonce, plaintext, additionalData) []byte` and `Open(dst, nonce, ciphertext, additionalData) ([]byte, error)`. Encrypting is `out := gcm.Seal(nil, nonce, plaintext, nil)`, where `nonce` must be exactly `gcm.NonceSize()` bytes — 12 for standard GCM — and the result is the ciphertext plus a 16-byte tag. Decrypting is `gcm.Open(nil, nonce, out, nil)` with the same nonce and the same additional data; if the tag does not verify you get an error and must discard the output entirely.

code

go · 12 lines
go
block, err := aes.NewCipher(key) // key is 16, 24 or 32 bytes
if err != nil {
	return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
	return nil, err
}

// nonce must be exactly gcm.NonceSize() bytes (12 for standard GCM)
record := gcm.Seal(nil, nonce, chunk, nil)
// len(record) == len(chunk) + gcm.Overhead() // Overhead() is 16

go deeper

for a junior

Be ready to write the four lines from memory: aes.NewCipher, cipher.NewGCM, Seal, Open. Know that the key must be 16, 24 or 32 bytes and that Seal's result is 16 bytes longer than the plaintext.

for a middle

Explain why the block cipher and the mode are separate types, what each of the four cipher.AEAD methods reports, and why Open's error means you have no plaintext at all rather than a partially decrypted one.

for a senior

Show where the AEAD is constructed in a real service or batch job — once per key, shared across goroutines — and how the decrypt path proves it never touches Open's output after an error.

for a principal

Be ready to argue for keeping cipher.AEAD as the type your internal helpers accept, so the encryption primitive stays swappable, and for centralising this construction in one reviewed package rather than letting each team hand-roll it.

## Two packages, two calls Go splits symmetric encryption across two standard-library packages, and the split is the thing to understand first. `crypto/aes` gives you a **block cipher**; `crypto/cipher` gives you the **mode** that turns a block cipher into something that can encrypt a message of any length and detect tampering. ```go block, err := aes.NewCipher(key) if err != nil { return err } gcm, err := cipher.NewGCM(block) if err != nil { return err } ``` `aes.NewCipher(key []byte) (cipher.Block, error)` accepts a key of **16, 24 or 32 bytes**, selecting AES-128, AES-192 or AES-256. Any other length comes back as an `aes.KeySizeError`. The returned `cipher.Block` has `BlockSize()`, `Encrypt(dst, src []byte)` and `Decrypt(dst, src []byte)` — each of those transforms **exactly one 16-byte block**, with no chaining, no padding and no integrity. Calling `Encrypt` in a loop over a buffer is the classic misuse; that is ECB, and it is never what you want. `cipher.NewGCM(block) (cipher.AEAD, error)` requires a block cipher with a 128-bit block size, which AES is, and returns an error otherwise. There are two siblings for unusual requirements — `cipher.NewGCMWithNonceSize` for a non-standard nonce length and `cipher.NewGCMWithTagSize` for a truncated tag — and reaching for either without a specific reason is a review smell. ## The cipher.AEAD interface `cipher.AEAD` is a four-method interface, and every AEAD in Go presents the same shape: - `NonceSize() int` — how many bytes the nonce argument must be. For `cipher.NewGCM` this is 12. - `Overhead() int` — how much longer the ciphertext is than the plaintext. For GCM this is 16, the authentication tag. - `Seal(dst, nonce, plaintext, additionalData []byte) []byte` - `Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error)` Because the API is an interface, code that takes a `cipher.AEAD` is not tied to AES-GCM; the construction is the only AES-specific part. ## Seal and Open ```go record := gcm.Seal(nil, nonce, chunk, nil) plain, err := gcm.Open(nil, nonce, record, nil) ``` Three things about `Seal` surprise newcomers: 1. **The first parameter is a destination to append to, not a preallocated output buffer.** `Seal` behaves like the `append` builtin: it appends the sealed output to `dst` and returns the extended slice. Passing `nil` means "allocate a fresh slice", which is the correct default. 2. **`Seal` does not choose the nonce for you.** With an AEAD from `cipher.NewGCM`, the nonce is a caller-supplied argument that must be exactly `NonceSize()` bytes; a wrong length is a programming error and `Seal` panics rather than returning an error. (`cipher.NewGCMWithRandomNonce`, added in Go 1.24, is the variant that does pick nonces itself.) 3. **The tag is not returned separately.** The 16 bytes of `Overhead()` are appended to the ciphertext in the single returned slice, so `len(record) == len(chunk) + gcm.Overhead()`. `Open` is the mirror image. It takes the same nonce and the same `additionalData`, verifies the tag, and only then returns the plaintext. Its error is the whole safety mechanism of the API: **if `err != nil` you have no plaintext**, and you must not look at, log, or partially process whatever is in the destination slice. A decrypt path that ignores `Open`'s error has thrown away the reason for using an AEAD in the first place. The `additionalData` argument is data that is authenticated but not encrypted — it travels in the clear, and `Open` fails if it differs from what `Seal` saw. Passing `nil` on both sides is normal when there is nothing to bind. ## A batch job, end to end An offline job that encrypts archive chunks on their way to object storage typically builds the AEAD **once** — a `cipher.AEAD` is safe for concurrent use and cheap to reuse, while `aes.NewCipher` does key expansion work you do not want per chunk — and then loops: ```go for _, chunk := range chunks { // nonce is 12 bytes, unique for this chunk record := gcm.Seal(nonce, nonce, chunk, nil) if _, err := sink.Write(record); err != nil { return err } } ``` Here `dst` is the nonce slice itself, so the record comes out as nonce, then ciphertext, then tag, in one buffer — the standard framing, because the reader needs the nonce and it is not secret. The reader splits at `gcm.NonceSize()`. ## What goes wrong The errors are worth handling rather than dropping: `aes.NewCipher` fails on a wrong-sized key, which usually means a key that was hex-decoded incorrectly or read as a string. `cipher.NewGCM` fails on a block cipher of the wrong block size. Neither error is recoverable at run time, but swallowing them yields a nil AEAD and a nil dereference one line later.

  • What happens if you hand aes.NewCipher a 20-byte key?
    It returns an `aes.KeySizeError` and a nil `cipher.Block`. AES is defined only for 128, 192 and 256-bit keys, so 16, 24 and 32 bytes are the only accepted lengths. In practice a wrong length means the key was mis-decoded — a hex or base64 string used raw, or a passphrase used directly. Handle the error; ignoring it gives you a nil block and a panic on the next line.
  • Will cipher.NewGCM wrap any cipher.Block?
    No. GCM is defined for 128-bit block ciphers, so `cipher.NewGCM` returns an error if `block.BlockSize()` is not 16. That rules out ciphers such as 3DES from `crypto/des`, whose block is 8 bytes. In the standard library, AES is the block cipher you pair it with, and the AES-GCM path is the one with assembly-optimised implementations on common architectures.
  • Should you build the cipher.AEAD once or per message?
    Once per key. `aes.NewCipher` performs AES key expansion and `cipher.NewGCM` precomputes GCM tables, so rebuilding both per message is measurable waste in a hot loop. A `cipher.AEAD` holds no per-message state and is safe for concurrent use by multiple goroutines, so a long-running job or server can build it at start-up and share it.
  • What is the additionalData argument for at the API level?
    It is authenticated but not encrypted: `Seal` folds it into the tag, and `Open` fails if the value it is given differs by even one byte from the one `Seal` saw. It never appears in the ciphertext, so it must be transmitted or reconstructed independently. Passing `nil` on both sides is fine when there is nothing to bind.

saying these in an interview costs you the question

  • Thinks aes.NewCipher alone encrypts a message
  • Calls cipher.Block.Encrypt in a loop over a whole buffer
  • Expects cipher.NewGCM's Seal to invent the nonce
  • Looks for the authentication tag as a second return value
  • Ignores the error from Open and uses the output anyway
  • Rebuilds the AEAD for every message in a loop
open as a page

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%

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.

open as a page

In Go's cipher.AEAD, which overlap between Seal's dst and plaintext is allowed?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Only exact overlap: passing plaintext[:0] as dst, so the output starts at the plaintext's first byte, is the documented in-place form. Any other overlap between the destination's output region and the plaintext makes Go's crypto/cipher panic rather than corrupt the data.

open as a page

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

level: middleimportance: nice to knowfreq 25%

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.

open as a page