Documentation
¶
Overview ¶
Package crypto provides at-rest AES-256-GCM encryption under a VERSIONED KEYRING, so a key can be rotated without making every already-sealed value permanently unreadable.
The wire format is the "CPK1" envelope, byte-for-byte compatible with the one control-plane has shipped; blobs sealed by either implementation open with the other. See envelope.go for the layout and the reasoning for it.
There is no process-global state and no hard-coded environment variable: build a *Keyring with ParseKeyring or KeyringFromEnv and pass it to whoever seals or opens.
Rotation ¶
The keyring is an ordered list; the FIRST entry is the primary and the only key that seals, while every entry can open. Rotating is one prepend:
before: v1:AAA... after: v2:BBB...,v1:AAA...
New writes use v2, old rows still open under v1. Keep v1 until a backfill has re-sealed every row (EnvelopePrefixForKeyID builds the SQL prefix for "which rows still use v1"), then delete it. At no point is a key referenced that is not present, so neither step has an ordering hazard.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrKeyNotConfigured: the keyring source (env var) is unset or blank. // Whether that is fatal is the caller's call; an absent keyring is not // the same as a malformed one. ErrKeyNotConfigured = errors.New("encryption keyring not configured") // ErrKeyringInvalid: present but malformed — bad entry, bad key id, key // not 32 bytes, duplicate id, or a corrupt envelope header. Fail at boot. ErrKeyringInvalid = errors.New("not a valid encryption keyring") // ErrUnknownKeyID: a well-formed blob naming a key the keyring does not // hold. This means "put the key back", not "someone tampered with it". ErrUnknownKeyID = errors.New("ciphertext was sealed under a key id that is not in the keyring") // ErrUnversionedCiphertext: no envelope header, i.e. the pre-keyring raw // nonce ‖ ciphertext format. Refused rather than opened under an // implicit "v1"; see decodeEnvelope. ErrUnversionedCiphertext = errors.New("ciphertext predates the versioned envelope and cannot be opened") )
Sentinels are distinguishable with errors.Is. An operator who rotates a key must be told what they did, not shown a bare authentication failure.
Functions ¶
func EnvelopePrefixForKeyID ¶
EnvelopePrefixForKeyID returns the exact leading bytes of every blob sealed under keyID — the SQL prefix for "which rows still use this key".
Types ¶
type Keyring ¶
type Keyring struct {
// contains filtered or unexported fields
}
Keyring is a set of AES-256 keys: every one can open, exactly one (the primary) seals. Safe for concurrent use after construction.
func KeyringFromEnv ¶
KeyringFromEnv parses the keyring held in the variable name, read through getenv — pass os.Getenv. forge/pkg never reads the ambient environment itself (a library changes behaviour only through its arguments), so the caller owns where the value comes from. An unset or blank variable yields ErrKeyNotConfigured (wrapped with the name); a malformed one yields ErrKeyringInvalid. Callers that accept "absent" but treat "malformed" as fatal can branch on errors.Is.
func ParseKeyring ¶
ParseKeyring reads the keyring grammar:
keyring = entry [ "," entry ]*
entry = key-id ":" base64(32 bytes)
| base64(32 bytes) -- single entry only; id becomes "v1"
The first entry is the primary. Position rather than a second "primary=" name was chosen because two settings can disagree in the worst way: a primary naming a key absent from the keyring boots fine and opens every old row, then fails on the first write. With position, you cannot prepend a key without adding it. Whitespace around entries and ids is tolerated.
func (*Keyring) Open ¶
Open decrypts a blob, selecting the key its envelope names. Failure modes are distinct sentinels:
ErrUnversionedCiphertext — sealed before the envelope existed ErrUnknownKeyID — a key that was removed from the keyring ErrKeyringInvalid — corrupt envelope header (wrapped GCM failure) — tampering or wrong aad; the only security event
Collapsing a rotation mistake into an authentication failure sends the operator on an intrusion hunt when the answer is "the key you removed is still in use".
func (*Keyring) PrimaryKeyID ¶
PrimaryKeyID returns the id new writes seal under.
func (*Keyring) Seal ¶
Seal encrypts plaintext under the PRIMARY key and cryptographically binds it to aad and to its own envelope header.
aad is not encrypted and not stored: the opener re-derives it from the context it believes the blob belongs to, and Open with different aad fails authentication. That makes a sealed value NON-TRANSPLANTABLE, a stronger claim than any access check — a WHERE clause does nothing if the ciphertext is copied into a row the wrong caller legitimately owns. nil aad is the unbound case. A blob sealed with aad does not open with nil aad.
New writes always use the primary, with no per-call override: a caller that could pick its key could keep writing under a key a rotation is retiring, and the backfill would never finish.