Documentation
¶
Overview ¶
Package secret provides authenticated envelope encryption for sensitive values that the server persists at rest (TOTP secrets, Cloudflare API tokens, notification credentials, ...).
The threat model is disk-at-rest exposure: a backup, snapshot, or stray copy of the state file must not leak usable credentials. It is NOT a defense against an attacker with live process memory or the master key itself.
Design choices:
- AES-256-GCM (AEAD): confidentiality + integrity in one primitive. A tampered or truncated ciphertext fails to open instead of returning garbage.
- A fresh 96-bit random nonce per Encrypt call, prepended to the ciphertext. GCM nonces must never repeat under the same key; random nonces are safe far below the birthday bound for our volume.
- A versioned, self-identifying envelope prefix so we can (a) tell ciphertext from legacy plaintext during migration and (b) evolve the scheme later without ambiguity.
- Empty strings pass through unchanged so encoding/json `omitempty` semantics are preserved and we never grow empty fields.
Index ¶
Constants ¶
const DefaultKeyFile = defaultKeyFile
DefaultKeyFile is the basename auto-managed under the data directory when no key is supplied via env or flag. It is exported for internal ops tooling that must refuse accidental key generation but still follow the server convention.
const EnvMasterKey = "LATTICE_MASTER_KEY"
EnvMasterKey holds an inline master key (base64, hex, or raw 32 bytes), or one of the disable sentinels. Highest precedence — intended for KMS / secret manager injection.
const EnvMasterKeyFile = "LATTICE_MASTER_KEY_FILE"
EnvMasterKeyFile points at a file containing the master key.
const KeySize = 32
KeySize is the required master-key length: AES-256.
Variables ¶
This section is empty.
Functions ¶
func IsEnvelope ¶
IsEnvelope reports whether s is a well-formed envelope produced by this package: the version prefix followed by base64url that decodes to at least a nonce + GCM tag. The structural check (not a bare prefix match) prevents operator-supplied plaintext that merely starts with the prefix from being mistaken for ciphertext — which would otherwise either skip encryption in Encrypt or be treated as corrupt-on-load in Decrypt.
Types ¶
type Cipher ¶
type Cipher interface {
// Encrypt returns a fresh envelope for plaintext (a new random nonce every
// call). Empty input returns empty output. Encrypt does NOT inspect its
// input for an existing envelope: callers must hold plaintext. This avoids
// an in-band "is this already encrypted?" heuristic that operator-supplied
// secrets could collide with. The store upholds this by keeping in-memory
// state decrypted at all times.
Encrypt(plaintext string) (string, error)
// Decrypt reverses Encrypt. Input that is not an envelope is returned
// unchanged so pre-encryption (legacy plaintext) state loads cleanly and
// gets encrypted on the next save. Envelope input that fails
// authentication (tamper or wrong key) returns an error.
Decrypt(envelope string) (string, error)
// Enabled reports whether this cipher actually encrypts. A disabled cipher
// is a passthrough used for in-memory stores and explicit opt-out.
Enabled() bool
}
Cipher transforms a plaintext secret into a persistable envelope and back. Implementations must be safe for concurrent use.
func Disabled ¶
func Disabled() Cipher
Disabled returns a passthrough Cipher that performs no encryption. It is used for in-memory stores (nothing is persisted) and for explicit operator opt-out. It returns its input verbatim in both directions; the store is responsible for warning if it ever hands a real envelope to a disabled cipher (which would indicate a lost key).
type ResolveResult ¶
type ResolveResult struct {
Cipher Cipher
Source string // "env", "file:<path>", "generated:<path>", "disabled"
Generated bool // a new key file was created
KeyFilePath string // populated for file/generated sources
}
ResolveResult reports how a Cipher was obtained, for startup logging.
func Resolve ¶
func Resolve(dataDir, keyFileOverride string) (ResolveResult, error)
Resolve builds a Cipher using, in precedence order:
- $LATTICE_MASTER_KEY (a disable sentinel yields a passthrough cipher)
- keyFileOverride argument, else $LATTICE_MASTER_KEY_FILE
- <dataDir>/master.key — read if present, otherwise generated (0600)
dataDir is only consulted for case 3.