secrets

package
v0.40.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: MIT Imports: 12 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
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

func ClearPendingMFA(ctx context.Context, s *Store, userID uuid.UUID) error

ClearPendingMFA removes any stored OTP challenge for the user.

func LoadPendingMFA added in v0.23.1

func LoadPendingMFA(ctx context.Context, s *Store, userID uuid.UUID) (rivian.PendingMFA, bool)

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

func LoadRivianSession(ctx context.Context, s *Store, userID uuid.UUID) (rivian.Session, error)

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

func SavePendingMFA(ctx context.Context, s *Store, userID uuid.UUID, p rivian.PendingMFA) error

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

func SaveRivianSession(ctx context.Context, s *Store, userID uuid.UUID, sess rivian.Session) error

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.

func (*RivianAccountSource) Session added in v0.40.7

func (s *RivianAccountSource) Session(ctx context.Context, uid uuid.UUID) (rivian.Session, error)

Session implements rivian.AccountStateSource.

func (*RivianAccountSource) States added in v0.40.7

States implements rivian.AccountStateSource.

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
	// this decryption of every session just to read the sign-in time, can go.
	Log *slog.Logger
	// contains filtered or unexported fields
}

RivianSessionLister lists every stored Rivian session for rivianexpiry. It decrypts each (the sign-in time is in the sealed session) and returns only the session and the user's needs_reauth flag.

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

List implements rivianexpiry.Sessions.

func (*RivianSessionLister) WithLog added in v0.40.8

WithLog makes the lister log the scrub count 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

func New(db *sql.DB, sealer crypto.Sealer) *Store

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

func (s *Store) Delete(ctx context.Context, userID uuid.UUID, name string) error

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

func (s *Store) Get(ctx context.Context, userID uuid.UUID, name string) ([]byte, error)

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

func (s *Store) Put(ctx context.Context, userID uuid.UUID, name string, plaintext []byte) error

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) 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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL