Documentation
¶
Overview ¶
Package kv is a small Redis-style key/value store with per-entry TTLs. Values are opaque bytes (JSON records, encrypted snapshots, or at-rest ciphertext); callers handle their own (de)serialization. The store is the single persistence primitive for the authorization server and the pep — multi-index lookups are modelled with secondary-index keys (a record under "<ns>:<id>" plus "<ns>:<field>:<v>" keys holding the id). NewSealed wraps a Store so that it sees ciphertext only (server rule 5).
This package has no external dependencies: it ships the interface and the in-memory backend (NewMemory — the default for tests and dev). The Postgres backend is the package kv/postgres in this module: kv's go.mod requires pgx, but a binary links the driver only if it imports kv/postgres. Consumers accept the kv.Store interface (the duck type), not a concrete backend.
Atomicity: Take (get+delete), SetIfAbsent and SetMany (multi-key write) are atomic, which the security-critical single-use (nonce redeem, authorization-code exchange, proof replay) and index-consistency operations rely on.
Index ¶
Constants ¶
const ( // BareKeyID is the ID of the key in a file holding one bare base64 key. A rotation away from such a file // names it: `k2 <new key>` above `default <the old key>`. BareKeyID = "default" )
Variables ¶
var ErrClosed = errors.New("kv: store closed")
ErrClosed is returned by every method after Close.
var ErrIntegrity = errors.New("kv: record failed its integrity check")
ErrIntegrity is what a sealed store returns for a record it cannot authenticate: tampered, moved to another key or purpose, sealed under a key the ring does not hold, or not sealed at all. It can mean an attack on the store, so callers treat the record as absent and raise an alert; a failure of the store itself stays a different error.
Functions ¶
Types ¶
type Bus ¶
type Bus interface {
// Publish sends payload to every subscriber of channel, including in other processes.
Publish(ctx context.Context, channel, payload string) error
// Subscribe returns a stream of payloads published to channel until ctx is cancelled (then it is closed).
Subscribe(ctx context.Context, channel string) (<-chan string, error)
// Close releases resources.
Close() error
}
Bus is a lightweight publish/subscribe channel for small control messages (e.g. session revocations), fanned out to every subscriber across processes. Like Store, the interface lives here (no driver) and the Postgres backend (LISTEN/NOTIFY) lives in kv/postgres. Delivery is best-effort: a slow or briefly disconnected subscriber may miss a message, so callers must not rely on the bus alone for correctness (pair it with a durable backstop).
type Keyring ¶ added in v0.1.0
type Keyring struct {
// contains filtered or unexported fields
}
Keyring holds several 32-byte keys with IDs: the first one is current and seals, all of them open.
The text format is one key per line, `<id> <base64 of 32 bytes>`, with `#` comments and blank lines ignored. A file holding one bare base64 key is a ring of the single key BareKeyID.
Rotating, so that no replica meets a record it cannot open (server rule 22): add the new key second on all pods; once all of them hold it, make it first; drop the old key after the longest TTL of a record it sealed. Nothing needs a restart: a ring from LoadKeyring follows its file.
func LoadKeyring ¶ added in v0.1.0
LoadKeyring reads a ring from the file at path, and keeps following it: at most every 30 s, during Current or Lookup, it compares the file's mtime and size and re-reads it when they differ, so rotation needs no restart (server rule 6). A failed reload keeps the keys in force and logs at error level until it succeeds.
func ParseKeyring ¶ added in v0.1.0
ParseKeyring reads a ring in the text format of Keyring. It refuses wrong key lengths, duplicate IDs and an empty ring. Its errors name line numbers, never the content of a line.
type Keys ¶ added in v0.1.0
type Keys interface {
// Current returns the key that seals new records. The slice is a fresh copy the caller owns and clears.
Current() (id string, key []byte)
// Lookup returns the key with the given ID, for opening. The slice is a fresh copy the caller owns and
// clears.
Lookup(id string) (key []byte, ok bool)
}
Keys is where a sealed store gets its key material. It is small so that a ring backed by a KMS can replace Keyring behind it; go/nonce declares the same shape, so one ring serves both.
type ReplayCache ¶ added in v0.1.0
type ReplayCache struct {
// contains filtered or unexported fields
}
ReplayCache has the shape of dpop.ReplayCache, on a Store, so a proof replayed at another replica is refused (RFC 9449 Section 11.1). The kv module stays free of dpop: the interface is met, not imported.
func NewReplayCache ¶ added in v0.1.0
func NewReplayCache(store Store) *ReplayCache
NewReplayCache returns a cache on store. Records carry their own TTL and leave with Reap.
type Store ¶
type Store interface {
// Get returns the value for key. found is false when the key is absent or expired.
Get(ctx context.Context, key string) (value []byte, found bool, err error)
// Set stores value under key with an optional TTL (0 = no expiry), overwriting any existing value.
Set(ctx context.Context, key string, value []byte, ttl time.Duration) error
// SetIfAbsent stores value under key only if the key is absent or expired, atomically, and reports whether
// it did: of concurrent calls for one key across all replicas exactly one gets set = true. TTL as for Set.
SetIfAbsent(ctx context.Context, key string, value []byte, ttl time.Duration) (set bool, err error)
// SetMany writes all entries atomically (one transaction), used to keep a record and its
// secondary-index keys consistent.
SetMany(ctx context.Context, entries ...Entry) error
// Take atomically returns and deletes key — single-use semantics for authorization-code exchange. found is false when the key is absent or expired.
Take(ctx context.Context, key string) (value []byte, found bool, err error)
// Delete removes key. No error if it doesn't exist.
Delete(ctx context.Context, key string) error
// Keys returns all non-expired keys with the given prefix, sorted. The prefix is matched
// literally (callers use wildcard-free namespace prefixes).
Keys(ctx context.Context, prefix string) ([]string, error)
// Cleanup removes expired entries and returns the count removed.
Cleanup(ctx context.Context) (removed int, err error)
// Close releases resources; subsequent calls return ErrClosed.
Close() error
}
Store is the persistence interface. Implementations must be safe for concurrent use.
func NewMemory ¶
func NewMemory() Store
NewMemory returns an in-memory Store — the default backend for tests and development. It is single-replica only: nothing in it reaches another process, so nonce, replay and session state in it holds for this process alone, and it is lost on exit. Several replicas need the Postgres backend (kv/postgres).
func NewSealed ¶ added in v0.1.0
NewSealed wraps inner so that it sees ciphertext only (server rule 5). Each record is sealed with AES-256-GCM under a key derived by HKDF-SHA256 from the ring's current key, purpose and a fresh random salt, and bound through the associated data to its version, key ID, purpose and kv key, so a record opens neither under another kv key nor another purpose.
record = version(1) | len(key ID)(1) | key ID | salt(16) | nonce(12) | ciphertext and tag
Set, SetMany and SetIfAbsent seal; Get and Take open and return ErrIntegrity for anything that does not open. A Take that finds such a record has deleted it by then, as it would a good one, and the error reports it. Delete, Keys, Cleanup and Close pass through; kv keys stay in the clear, so a key must not carry a secret.
It panics on a nil inner or keys or an empty purpose: those are wiring mistakes at start-up, and carrying on could only mean storing plaintext.