Documentation
¶
Overview ¶
Package secrets is the persistence layer on top of [`internal/crypto`](../crypto). It hides the Seal/Open plumbing and the user_secrets schema behind a tiny Put/Get/Delete API so feature code (rivian session, future AI keys, future push subscription private key) doesn't have to know either exists.
Why a package, not a function on the sealer ¶
- The sealer is a pure crypto primitive; it has no opinion on where blobs live. Keeping storage in a separate package lets the phase-3 KMSSealer swap in without touching any caller.
- Each caller wants the same behaviour ("store this blob under this user, that name"); inlining sql.Exec at every site would duplicate the kek_id audit stamp, the ON CONFLICT upsert, and the GDPR-delete handling.
Naming convention for secret names ¶
The `name` column is free-text but callers are expected to use dot-delimited namespaced identifiers so a grep of the codebase turns up every producer/consumer of a given secret:
- rivian.session — rivian.Session JSON blob
- ai.openai_key — OpenAI API key (future)
- push.vapid_priv — VAPID private key per user (future)
Index ¶
- Variables
- func ClearPendingMFA(ctx context.Context, s *Store, userID uuid.UUID) error
- func LoadPendingMFA(ctx context.Context, s *Store, userID uuid.UUID) (rivian.PendingMFA, bool)
- func LoadRivianSession(ctx context.Context, s *Store, userID uuid.UUID) (rivian.Session, error)
- func SavePendingMFA(ctx context.Context, s *Store, userID uuid.UUID, p rivian.PendingMFA) error
- func SaveRivianSession(ctx context.Context, s *Store, userID uuid.UUID, sess rivian.Session) error
- type Store
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("secrets: not found")
ErrNotFound is returned by Get when no row exists for (userID, name). Distinct from an open/decrypt failure — a missing row is expected (user hasn't linked Rivian yet, etc.) and callers usually translate it to a zero value.
Functions ¶
func ClearPendingMFA ¶ added in v0.23.1
ClearPendingMFA removes any stored OTP challenge for the user.
func LoadPendingMFA ¶ added in v0.23.1
LoadPendingMFA returns a stored OTP challenge and true when one is present and within the TTL. A missing, corrupt, or expired row returns false, matching LoadRivianSession's recover-by-retry stance.
func LoadRivianSession ¶
LoadRivianSession returns the persisted Rivian session for a user, or the zero value when none is stored. A corrupt / un-openable blob also returns zero rather than an error: matches the previous settings-backed behaviour where an operator could "recover" a bad row by logging in again, and means a failed KEK rotation doesn't brick every user at once. The caller gets an error log-worthy only when the DB itself misbehaves.
func SavePendingMFA ¶ added in v0.23.1
SavePendingMFA persists an in-flight OTP challenge so a peer pod can finish the second leg. An empty OTPToken clears the row instead.
func SaveRivianSession ¶
SaveRivianSession persists or clears the session. An empty UserSessionToken is the sentinel for "logged out"; we delete the row rather than store an empty sealed blob, because a missing row is cheaper to read and removes the only place where the user has stale token material on disk.
Types ¶
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps *sql.DB with Seal/Open plumbing. Safe for concurrent use — the underlying DB pool handles concurrency, and the sealer is stateless.
func New ¶
New builds a Store. Both arguments are required; passing a NoopSealer is allowed but only for tests and is logged at OpenStore callers so nobody accidentally ships with it.
func (*Store) Delete ¶
Delete removes a secret. Idempotent: no error if the row is absent. Used on explicit logout / unlink and by the future GDPR-delete pipeline (though that will usually go through ON DELETE CASCADE from users).
func (*Store) Get ¶
Get reads + decrypts (userID, name). Returns ErrNotFound when the row doesn't exist; any other error is either a DB failure or crypto.ErrSealedBlob (from the sealer, wrapped).
func (*Store) Put ¶
Put writes (userID, name) = plaintext, encrypted with the configured sealer. Upserts on (user_id, name) so the row's updated_at tracks the latest write and rotation tooling can re-wrap in place with the same primary key.
The plaintext is sealed with the caller's userID bound as AAD, so a later Get with a different userID will refuse to open the row even if the attacker can read the raw blob.