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 SaveRivianSessionVersioned(ctx context.Context, s *Store, userID uuid.UUID, sess rivian.Session) (time.Time, 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) PutTxVersioned(ctx context.Context, userID uuid.UUID, name string, plaintext []byte, ...) (time.Time, 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 SaveRivianSessionVersioned ¶ added in v0.40.16
func SaveRivianSessionVersioned(ctx context.Context, s *Store, userID uuid.UUID, sess rivian.Session) (time.Time, error)
SaveRivianSessionVersioned is SaveRivianSession that also returns the stored session's version, the database's own (zero for a sign-out or a nil store). The pod that signed the user in hands it to its client (SetSessionVersion), so the client knows which stored session it holds without comparing its clock with the database's.
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 sessions and how many still had
// no recorded sign-in time (counts only).
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 sign-in time counts of each pass.
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) PutTxVersioned ¶ added in v0.40.16
func (s *Store) PutTxVersioned(ctx context.Context, userID uuid.UUID, name string, plaintext []byte, fn func(ctx context.Context, tx *sql.Tx) error) (time.Time, error)
PutTxVersioned is PutTx that also returns the version the database assigned to the row (its updated_at), so the writer can tell its own client which stored session it now holds without comparing clocks.