secrets

package
v0.20.1 Latest Latest
Warning

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

Go to latest
Published: Jun 19, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package secrets stores one namespace's secrets as a single encrypted blob. A read decrypts the blob the vault header points at, verifies it against the header's manifest MAC, and returns its keys. A write decrypts the current blob, applies the change in memory (last write wins per key), writes a new, uniquely named blob, and points the header at it under the header compare-and-swap (see internal/keymgmt): two concurrent writers serialize on that swap, and the loser re-reads the now-current blob and re-applies its change, so writes to different keys both survive and only same-key writes resolve last-writer-wins.

Writing a fresh blob and only then repointing the header keeps a crash harmless: until the swap commits, the header still names the prior blob, which is untouched. The write the header just superseded is kept as a one-generation backup (the manifest's Prev pointer), so a corrupt or bit-rotted current blob can fall back to the last good one, losing at most the most recent write. A blob written by a crashed write that never swapped the header is an orphan no read ever consults; `notenv doctor` sweeps it.

The blob is one age message sealed under the master key, bound to the vault's authenticated header by its manifest MAC (a keyed MAC of its plaintext, see internal/crypto) and self-identifying its namespace, so a blob copied to another namespace cannot pass as that namespace's.

Index

Constants

This section is empty.

Variables

View Source
var ErrNamespaceChanged = errors.New("the namespace changed since it was read")

ErrNamespaceChanged reports that a namespace's current blob moved between the read an operation planned against and the swap it tried to commit: another writer landed in between. Rewrite (the evict recovery path) returns it rather than clobber that concurrent write.

Functions

func Exists

func Exists(ctx context.Context, store backend.HeaderStore, name string) (bool, error)

Exists reports whether a namespace holds committed secrets, by consulting the authenticated header manifest rather than the raw object listing: a crashed write can leave an orphan blob under the namespace prefix that no manifest entry references, and that must not read as "this namespace has secrets". It needs no master key (parsing the header is enough; the manifest's trustworthiness is confirmed at unlock). Virgin storage (no header) reports false.

func ValidateValue added in v0.20.0

func ValidateValue(value string) error

ValidateValue reports why a secret value cannot be stored. A value becomes an environment variable (passed to a child by execve) and may be written back out as a .env file, so it has to be text that survives both: valid UTF-8 with no control characters other than the newline family (\n, \t, \r). A NUL cannot ride in an environment variable at all, an ESC and friends cannot be represented in a .env, and invalid UTF-8 is silently coerced to U+FFFD by the blob's JSON encoder, so all of them are refused here, early, rather than stored as data notenv could not later hand back intact. Binary belongs base64-encoded, which is itself valid text and passes. The newline family is allowed because real secrets carry it (PEM keys, JSON blobs, CRLF certs) and a .env can represent it. This is the single definition of what may enter the vault; callers (set, import, edit) reuse it for friendly errors, WriteBlob enforces it.

Types

type CorruptBlob added in v0.18.0

type CorruptBlob struct {
	Blob   string
	Reason string
}

CorruptBlob is a blob a salvage read could not trust and read past: missing from storage, undecryptable, or MAC-mismatched. Blob is its object key; Reason is the read error that disqualified it.

type Meta added in v0.11.0

type Meta struct {
	Description string
	TS          int64
}

Meta is a live key's advisory metadata: what the secret is for and when its write happened (wall-clock Unix seconds; 0 means the write predates timestamps). Advisory means exactly that: nothing orders or trusts by it.

type Namespace

type Namespace struct {
	// contains filtered or unexported fields
}

Namespace reads and writes one namespace's secrets through a backend, sealing its blob under master.

func For

func For(store backend.Backend, name string, master *crypto.MasterKey) *Namespace

For binds a namespace to a backend and master key.

func (*Namespace) Commit added in v0.18.0

func (n *Namespace) Commit(ctx context.Context, apply func(*State) (*State, error), pin func(*crypto.Header)) (*State, *crypto.Header, error)

Commit performs a read-modify-write of the namespace blob under the header compare-and-swap. apply computes the new in-memory state from the current one; it is re-run on each swap retry against the freshly re-read blob, so two writers' changes to different keys both survive and only same-key writes resolve last-writer-wins. Commit writes a new uniquely-named blob, points the header at it carrying the prior blob forward as the one-generation backup, and once the swap commits deletes the generation that fell off and calls pin with the committed header. A blob a superseded or failed attempt wrote is cleaned up; errors (including keymgmt.ErrEpochChanged) propagate after that cleanup.

func (*Namespace) Read added in v0.18.0

func (n *Namespace) Read(ctx context.Context, entry crypto.ManifestEntry) (*State, error)

Read resolves the namespace's secrets from the blob the manifest entry names. An untrustable blob (missing, undecryptable, MAC-mismatched) fails closed, naming it: a dropped or altered write must never be silently skipped. ReadSalvage is the opt-in escape for a vault stuck on honest media loss. A zero entry (the namespace has no blob yet) yields empty state.

func (*Namespace) ReadSalvage added in v0.18.0

func (n *Namespace) ReadSalvage(ctx context.Context, entry crypto.ManifestEntry) (*State, error)

ReadSalvage resolves what it can when a strict Read refuses. If the current blob is untrustable it falls back to the verified one-generation backup (entry.Prev), reporting the dropped blob on State.Corrupt instead of failing, so the user sees exactly what reverted. It is non-destructive and deliberately opt-in: silently serving an older blob would hide an attacker who suppressed the latest write. A transient error or a format-version skew still stops the read (those are not "this blob rotted").

func (*Namespace) Rewrite added in v0.18.0

func (n *Namespace) Rewrite(ctx context.Context, state *State, expected crypto.ManifestEntry, pin func(*crypto.Header)) (*crypto.Header, error)

Rewrite replaces the namespace blob with a fresh one sealed from state, its backup reset: the recovery path, where state came from a salvage read so the corrupt generations are dropped rather than carried. If state holds no secrets the namespace entry is removed entirely. expected is the manifest entry the state was salvaged under; if the live entry no longer matches it (a concurrent write, perhaps a legitimate repair, landed since), Rewrite aborts with ErrNamespaceChanged rather than overwrite that write with the older salvaged state. Same swap, cleanup, and pin contract as Commit.

func (*Namespace) WriteBlob added in v0.18.0

func (n *Namespace) WriteBlob(ctx context.Context, state *State, prev crypto.ManifestEntry) (string, crypto.ManifestEntry, error)

WriteBlob seals state into a fresh, uniquely named blob and returns its object key and the manifest entry that records it, carrying prev forward as the one-generation backup. It is the low-level primitive Commit and Rewrite build on (they own the header swap and the cleanup of superseded blobs). The blob is read back after writing (putVerified) so a corrupt write never reaches the manifest.

type State

type State struct {
	Secrets map[string]string
	Meta    map[string]Meta
	Corrupt []CorruptBlob
	// contains filtered or unexported fields
}

State is a namespace's resolved secrets. Corrupt is populated only by a salvage read that fell back past an untrustable blob; a strict read fails instead of listing.

func (*State) Apply added in v0.18.0

func (s *State) Apply(writes []Write) *State

Apply returns the state after applying writes under last-write-wins: a value overwrites, a deletion removes. The receiver is not mutated, so a caller can re-apply the same writes against a freshly re-read state on a swap-race retry.

func (*State) HasHistory

func (s *State) HasHistory() bool

HasHistory reports whether the namespace has ever stored a blob; false means it is untouched (distinct from one emptied by deletes, which still has a blob).

type Write added in v0.11.0

type Write struct {
	Key             string
	Value           string
	Description     string
	KeepDescription bool
	TS              int64
	Deleted         bool
}

Write is one key change to apply: a value (with optional advisory metadata) or a deletion. TS is the write's wall-clock Unix seconds, supplied by the caller so this package never reads a clock; 0 omits it. KeepDescription carries the key's existing description forward instead of setting Description, evaluated against the state being applied to (the live blob inside Commit, not a stale pre-read), so re-stating a value never reverts a concurrent description edit.

Jump to

Keyboard shortcuts

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