skip to content

Standard Library Primitives

The crypto packages you actually call: crypto/rand for tokens, sha256 and hmac for integrity, subtle for comparisons, cipher.AEAD for encryption. Interviewers probe which one you reach for.

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

explore

questions

30

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

What does Go's hmac.Equal do that bytes.Equal does not, and what does it return?

level: juniorimportance: must knowfreq 45%

basics

~20 s

hmac.Equal(mac1, mac2 []byte) returns a bool and scans every byte, so its running time does not depend on the contents. bytes.Equal stops at the first differing byte, so its timing reveals how much of the input matched.

open as a page

What is the difference between sha256.Sum256 and sha256.New, and when do you use each?

level: juniorimportance: must knowfreq 52%

basics

~20 s

sha256.Sum256 hashes a byte slice you already hold and returns a [32]byte array. sha256.New returns a streaming hash.Hash you write data into and finish with Sum(nil) - use that when the input is a file or stream too large to buffer.

open as a page

How do you use `hkdf.Key` in Go to turn one master secret into a separate key per purpose?

level: juniorimportance: must knowfreq 45%

basics

~10 s

Call hkdf.Key once per purpose with a different info string each time, for example "cookie-encryption-v1" versus "url-signing-v1". Same secret, same salt, different info, and you get independent keys you never have to store.

open as a page

Why is math/rand/v2 unsafe for generating a password-reset token in Go, and what do you use instead?

level: juniorimportance: must knowfreq 62%

basics

~20 s

math/rand/v2 is a statistical generator that makes no unpredictability promise, so its output can be reconstructed no matter how it is seeded. Use crypto/rand instead: rand.Text() for a token string, or crypto/rand.Read to fill a byte slice.

open as a page

What does ed25519.GenerateKey return in Go, and how do you sign and verify a message with it?

level: juniorimportance: must knowfreq 50%

basics

~10 s

ed25519.GenerateKey returns a public key, a private key, and an error. ed25519.Sign(priv, message) returns a 64-byte signature over the whole message, and ed25519.Verify(pub, message, sig) returns a bool saying whether that signature is valid.

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, why use crypto/pbkdf2 rather than hkdf.Key to derive a key from a user passphrase?

level: middleimportance: must knowfreq 52%

basics

~20 s

HKDF has no work factor: hkdf.Key runs only a couple of hash operations, so guessing a weak passphrase stays cheap. pbkdf2.Key takes an explicit iteration count that makes every guess as costly as you choose.

open as a page

What does subtle.ConstantTimeCompare return, and how does it behave on unequal-length slices?

level: middleimportance: should knowfreq 38%

basics

~20 s

subtle.ConstantTimeCompare(x, y []byte) returns an int: 1 if the slices have equal contents, 0 otherwise. If the lengths differ it returns 0 immediately, so it hides which bytes differ but not that the lengths do.

open as a page

What do GOFIPS140 at build time and GODEBUG=fips140 at run time each control in a Go binary?

level: middleimportance: should knowfreq 45%

basics

~10 s

GOFIPS140 is a build setting: it selects which snapshot of the Go Cryptographic Module is compiled in and makes fips140=on the binary's default GODEBUG. GODEBUG=fips140 then picks the run-time posture: off, on, or only.

open as a page

How do you compute an HMAC-SHA256 tag in Go, and what must hmac.New's first argument be?

level: middleimportance: should knowfreq 50%

basics

~20 s

Call hmac.New(sha256.New, key) - the first argument is a function that returns a fresh hash.Hash, written without parentheses. The result is an ordinary hash.Hash: write the message into it and call Sum(nil) for the 32-byte tag.

open as a page

In crypto/hkdf, when would you call Extract and Expand yourself instead of hkdf.Key?

level: middleimportance: should knowfreq 38%

basics

~20 s

hkdf.Key is Extract followed by Expand in one call. Call the halves yourself when one secret feeds many keys: Extract once to get a pseudorandom key, then Expand it repeatedly with different info strings, skipping the repeated extraction.

open as a page

What does crypto/rand.Text() return in Go, and why prefer it over hand-rolling a token?

level: middleimportance: should knowfreq 40%

basics

~10 s

crypto/rand.Text returns a 26-character string over the base32 alphabet A-Z and 2-7, carrying at least 128 bits of randomness. It takes no arguments and returns no error, so a token is one call.

open as a page

What two methods does Go's crypto.Signer interface declare, and what problem does it solve?

level: middleimportance: should knowfreq 45%

basics

~20 s

crypto.Signer declares Public() and Sign(rand, digest, opts). It lets code produce signatures without ever holding the private key bytes, so a key locked in a hardware token or a remote key service satisfies the same interface as an in-memory key.

open as a page

What does ecdsa.SignASN1 take as its input, and how is the signature it returns encoded?

level: middleimportance: should knowfreq 40%

basics

~10 s

ecdsa.SignASN1(rand, priv, hash) takes an already-computed digest, not the message, and returns the signature ASN.1 DER-encoded as the integer pair r and s. That encoding is variable-length. ecdsa.VerifyASN1 checks it and returns a bool.

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

Your HMAC webhook check calls subtle.ConstantTimeCompare — what still leaks timing?

level: seniorimportance: should knowfreq 28%

basics

~20 s

The comparison is only one step. Whether the signature header is present, whether it decodes, whether the sender's key was found, and how early the middleware returns all take measurably different time before the constant-time compare ever runs.

open as a page

Under GODEBUG=fips140=only a service that built cleanly fails on its first request. How do you find every rejected call before the next deploy?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Strict enforcement is a run-time guard inside the standard library's crypto packages, so nothing shows at compile time. Run the test suite and a canary instance under GODEBUG=fips140=only in CI, and audit which non-approved algorithms the code actually reaches.

open as a page

A CLI hashes each file with sha256.New and io.Copy into a content-addressed store, but some digests are wrong. How do you find the bug?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Wrong digests mean the hash was fed the wrong bytes, not that SHA-256 misbehaved. hash.Hash.Write never fails, so the only error io.Copy returns is the read error - drop it and you digest a prefix. Golden-vector tests pin it down.

open as a page

Your hkdf.Key wrapper let an empty info string through and two features now derive the same key — how do you catch this?

level: seniorimportance: should knowfreq 30%

basics

~20 s

crypto/hkdf validates only the requested key length, so an empty info string derives silently and every purpose that omits it shares one key. Audit the info string at every call site, and make the wrapper reject empty and unregistered labels.

open as a page

A Go worker minting reset tokens emits repeats after every restart. How do you find the cause?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Repeats aligned with restarts mean the token source is deterministic and rebuilt at startup, almost always a seeded math/rand/v2 generator. Switch the path to crypto/rand and invalidate every token already issued.

open as a page

Should every service build under GOFIPS140 and run with fips140=only, or only the regulated one?

level: principalimportance: should knowfreq 26%

basics

~20 s

Scope it to the audited boundary. Build the regulated services with a pinned GOFIPS140 snapshot and run those with fips140=only, leave the rest off, and gate shared libraries with a CI job under strict mode so common code stays compatible.

open as a page

Your exported signing function takes ed25519.PrivateKey. Would you change it to crypto.Signer before other teams depend on it?

level: principalimportance: should knowfreq 35%

basics

~20 s

Usually yes, and before consumers exist. crypto.Signer lets a hardware token or a remote key service be dropped in without touching a caller, while ed25519.PrivateKey hard-codes the algorithm and the assumption that the key loads into your process.

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

Why did FIPS builds need GOEXPERIMENT=boringcrypto before Go 1.24, and what changed after it?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Before Go 1.24 a FIPS build meant GOEXPERIMENT=boringcrypto, which reached a C cryptographic library through cgo and worked on only a couple of Linux platforms. Go 1.24 shipped a pure-Go Go Cryptographic Module selected by the GOFIPS140 build setting.

open as a page

What does hash.Hash's Sum(b []byte) method do to b and to the hash's internal state?

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Sum appends the current digest to the slice b and returns the extended slice; it does not hash b and does not change or reset the running hash. h.Sum(nil) is the usual form and returns just the digest.

open as a page

Since Go 1.24 crypto/rand.Read never returns an error. What changed, and how should code handle it?

level: middleimportance: nice to knowfreq 31%

basics

~20 s

Go 1.24 made crypto/rand.Read always fill the whole slice and never report an error: a failure of the system's random source now stops the program. No error branch is left to write, and no fallback is justified.

open as a page

What does subtle.WithDataIndependentTiming do, and on which hardware does it matter?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

subtle.WithDataIndependentTiming(f func()) runs f with the processor's data-independent-timing mode enabled. On arm64 chips with FEAT_DIT it sets PSTATE.DIT for the duration; on every other architecture it simply calls f. It does not make variable-time code constant-time.

open as a page

Signing through crypto.Signer with a SHA-256 digest works for ECDSA keys but fails for Ed25519. Why?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Ed25519 signs the whole message and hashes it internally, so ed25519.PrivateKey.Sign requires opts.HashFunc() to be crypto.Hash(0) and rejects a SHA-256 digest. ECDSA and RSA keys expect the opposite: a named hash and a digest of matching length.

open as a page

You own a Go library wrapping hkdf.Key for ten teams — how do you shape the info-string namespace you can never change?

level: principalimportance: nice to knowfreq 22%

basics

~20 s

Treat info strings as a versioned public namespace — owner, purpose, algorithm, version — allocated centrally, and export named accessors rather than a raw string parameter. Changing a label later means re-deriving everything that key protects.

open as a page