Help us shape the future of Go! Take the 2026 Go Developer Survey

secrets

package
v0.40.21 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 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.

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

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

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

List implements rivianexpiry.Sessions.

func (*RivianSessionLister) WithLog added in v0.40.8

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

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

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

Jump to

Keyboard shortcuts

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