Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/encrypt"
AES-GCM encryption for payloads moving between systems: a database column, a queue message, a cache entry, a cookie.
Raw byte APIs, plus helpers that serialise a Go value first with gob (ByteEncryptAny) or JSON (*SerializeAny).
Authenticated, which is the part that matters
AES-GCM provides confidentiality and authenticity. Tampering with a ciphertext makes decryption fail rather than producing plausible-looking garbage.
That second property is what unauthenticated modes lack, and why AES-CBC keeps appearing in vulnerability reports. Without authentication an attacker who can modify stored ciphertext can flip bits in the plaintext in controlled ways, and padding-oracle attacks can recover the plaintext outright given a decryption endpoint that distinguishes padding errors from other errors.
The random 96-bit nonce is generated per call and prefixed to the ciphertext, so callers do not manage nonces and cannot accidentally store one separately from the data it belongs to.
Three caveats the package states, and each is real
Rotate keys before 2^32 messages. With random nonces, the number of messages safely encrypted under one key is bounded by the birthday paradox (see NIST SP 800-38D). A nonce collision under the same key breaks both confidentiality and authentication, not just one of them. Two billion messages sounds distant until it is a per-row encrypted column on a large table.
Nonce uniqueness depends entirely on the randomness source. Encrypt uses crypto/rand.Reader. EncryptWith and WithRandReader allow substitution, and the package says plainly that this is for tests. A non-cryptographic or repeating reader causes nonce reuse, which is the failure above.
Base64 output is standard encoding, not URL-safe. RFC 4648 with + and /, which breaks when embedded in a URL path or a filename. Re-encode at the call site for those.
gob against JSON, here
The gob helpers decode with encoding/gob, which is not built for adversarial input. The package makes a careful distinction: because the payload is authenticated before decoding, only data produced by a holder of the key ever reaches the decoder. The authentication closes the exposure.
Even so, the *SerializeAny JSON helpers are the ones to reach for on cross-language or lower-trust payloads, and they are also faster:
| Operation | ns/op | B/op | allocs/op |
|---|---|---|---|
Encrypt | 569 | 1,424 | 5 |
Decrypt | 472 | 1,376 | 4 |
EncryptSerializeAny (JSON) | 1,572 | 2,273 | 11 |
DecryptSerializeAny (JSON) | 2,442 | 2,828 | 18 |
Measured with go test -bench on Go 1.27.1, linux/amd64, Intel Core i7-1260P. Run go test -bench=. ./pkg/encrypt/ to reproduce them on your own hardware; the absolute values will differ, the ratios much less so.
The raw byte path costs roughly half a microsecond, so the encryption itself is cheap; the serialisation around it dominates the value-level helpers. Encrypting a database column per row is affordable at these numbers.
Key management is not here
The package takes a key. Where it comes from, how it rotates, and how it is protected at rest are outside it.
awssecretcache fetches and caches secrets from AWS Secrets Manager, which covers the common deployment. The 2^32 bound above is what makes rotation a design requirement rather than a nice-to-have, and a rotation scheme needs to keep old keys available long enough to decrypt data encrypted under them.
Dependencies
This package reaches no external module: it uses only the Go standard library.
Links
- Reference documentation
- Source code
- Key retrieval: awssecretcache
- Password storage: passwordhash
- Unencrypted serialisation: encode
- All nurago packages