kv

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 3, 2026 License: EUPL-1.2 Imports: 21 Imported by: 0

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

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

View Source
var ErrClosed = errors.New("kv: store closed")

ErrClosed is returned by every method after Close.

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

func Reap added in v0.1.0

func Reap(ctx context.Context, store Store, interval time.Duration)

Reap removes expired entries from store every interval until ctx ends, and logs failures: a store that cannot be cleaned now may well be cleaned next time. Without it, expired rows stay in a persistent store, where reads ignore but never delete them.

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

func NewMemBus

func NewMemBus() Bus

NewMemBus returns an in-process Bus.

type Entry

type Entry struct {
	Key   string
	Value []byte
	TTL   time.Duration
}

Entry is one key/value/TTL triple for SetMany. A zero TTL means no expiry.

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

func LoadKeyring(path string) (*Keyring, error)

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

func ParseKeyring(data []byte) (*Keyring, error)

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.

func (*Keyring) Current added in v0.1.0

func (k *Keyring) Current() (string, []byte)

Current implements Keys.

func (*Keyring) GoString added in v0.1.0

func (k *Keyring) GoString() string

GoString is String for %#v.

func (*Keyring) Lookup added in v0.1.0

func (k *Keyring) Lookup(id string) ([]byte, bool)

Lookup implements Keys.

func (*Keyring) String added in v0.1.0

func (k *Keyring) String() string

String names the ring without its keys, so that formatting one cannot leak them.

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.

func (*ReplayCache) Seen added in v0.1.0

func (c *ReplayCache) Seen(id string, until time.Time) (bool, error)

Seen records id until the given time and reports whether it was recorded already. An error means the store could not tell, on which a caller must refuse.

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

func NewSealed(inner Store, keys Keys, purpose string) Store

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.

Directories

Path Synopsis
Package postgres is the PostgreSQL backend for the kv.Store interface.
Package postgres is the PostgreSQL backend for the kv.Store interface.

Jump to

Keyboard shortcuts

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