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
- func ScrubLegacyRivianTokens(ctx context.Context, s *Store, userID uuid.UUID) (scrubbed bool, err error)
- type RivianAccountSource
- type RivianSessionLister
- type Store
- func (s *Store) Delete(ctx context.Context, userID uuid.UUID, name string) error
- func (s *Store) DeleteTx(ctx context.Context, userID uuid.UUID, name string, ...) error
- func (s *Store) Get(ctx context.Context, userID uuid.UUID, name string) ([]byte, error)
- func (s *Store) GetVersioned(ctx context.Context, userID uuid.UUID, name string) ([]byte, time.Time, error)
- func (s *Store) Put(ctx context.Context, userID uuid.UUID, name string, plaintext []byte) error
- func (s *Store) PutTx(ctx context.Context, userID uuid.UUID, name string, plaintext []byte, ...) error
- func (s *Store) ReplaceIfUnchanged(ctx context.Context, userID uuid.UUID, name string, plaintext []byte, ...) (bool, error)
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.
var RivianSignedInUnknown = time.Unix(0, 0).UTC()
RivianSignedInUnknown is what users.rivian_signed_in_at holds for a stored session that has no sign-in time (1970-01-01): recorded, so the expiry sweep does not open it again.
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.
func ScrubLegacyRivianTokens ¶ added in v0.40.8
func ScrubLegacyRivianTokens(ctx context.Context, s *Store, userID uuid.UUID) (scrubbed bool, err error)
ScrubLegacyRivianTokens removes the legacy access and refresh tokens from a user's stored session, if it still has them, without touching anything else and without changing the row's version (updated_at): it re-seals the same session minus those two fields, and only if nobody wrote the row since it was read, so a sign-in on another pod in between is never overwritten. scrubbed is false when there was nothing to remove or the row changed meanwhile.
Types ¶
type RivianAccountSource ¶ added in v0.40.7
type RivianAccountSource struct {
// contains filtered or unexported fields
}
RivianAccountSource reads a user's shared Rivian account state for rivian.AccountSyncer: the version of the stored session (its updated_at, so any pod's sign-in or sign-out changes it) with the needs_reauth flag. It reads the session with LoadRivianSession.
func NewRivianAccountSource ¶ added in v0.40.7
func NewRivianAccountSource(pool *sql.DB, store *Store) *RivianAccountSource
NewRivianAccountSource returns the source over pool (the system pool: it reads the users and user_secrets tables of every user) and store.
type RivianSessionLister ¶ added in v0.40.8
type RivianSessionLister struct {
// Log, when set, gets one line per listing with the number of stored sessions the legacy-token
// scrub changed (a count only). The scrub is temporary: once prod logs 0 for a day it, and
// the decryption of every session it needs, go.
Log *slog.Logger
// contains filtered or unexported fields
}
RivianSessionLister lists every stored Rivian session for rivianexpiry. It reads the sign-in time from users.rivian_signed_in_at and never opens a session for it. The one exception is a session whose time was never recorded (NULL: made before the column existed): it is opened once to fill the column, and not again.
func NewRivianSessionLister ¶ added in v0.40.8
func NewRivianSessionLister(pool *sql.DB, store *Store) *RivianSessionLister
NewRivianSessionLister returns the lister over the system pool and the store.
func (*RivianSessionLister) List ¶ added in v0.40.8
func (l *RivianSessionLister) List(ctx context.Context) ([]rivianexpiry.Entry, error)
List implements rivianexpiry.Sessions.
func (*RivianSessionLister) WithLog ¶ added in v0.40.8
func (l *RivianSessionLister) WithLog(log *slog.Logger) *RivianSessionLister
WithLog makes the lister log the scrub count of each pass.
func (*RivianSessionLister) WithScrub ¶ added in v0.40.9
func (l *RivianSessionLister) WithScrub() *RivianSessionLister
WithScrub turns the temporary legacy-token scrub on. Without it a pass opens a session only to backfill a sign-in time that was never recorded.
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) DeleteTx ¶ added in v0.40.9
func (s *Store) DeleteTx(ctx context.Context, userID uuid.UUID, name string, fn func(ctx context.Context, tx *sql.Tx) error) error
DeleteTx is Delete plus fn, run in the same transaction.
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) GetVersioned ¶ added in v0.40.8
func (s *Store) GetVersioned(ctx context.Context, userID uuid.UUID, name string) ([]byte, time.Time, error)
GetVersioned is Get plus the row's updated_at, which is its version: every write sets it.
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.
func (*Store) PutTx ¶ added in v0.40.9
func (s *Store) PutTx(ctx context.Context, userID uuid.UUID, name string, plaintext []byte, fn func(ctx context.Context, tx *sql.Tx) error) error
PutTx is Put plus fn, run in the same transaction after the row is written: if fn fails nothing is written.
func (*Store) ReplaceIfUnchanged ¶ added in v0.40.8
func (s *Store) ReplaceIfUnchanged(ctx context.Context, userID uuid.UUID, name string, plaintext []byte, at time.Time) (bool, error)
ReplaceIfUnchanged re-seals the row's plaintext in place, only if its updated_at is still at (nobody wrote it since it was read), and KEEPS updated_at: the row's content changes without it becoming a new version, so nothing that watches versions (the Rivian account sync on every pod) sees a change. false when the row changed meanwhile or is gone; the caller just tries again on its next pass.