secrets

package
v1.0.3 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 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 recovery path) returns it rather than clobber that concurrent write.

View Source
var ErrNamespaceExists = errors.New("namespace already exists")

ErrNamespaceExists reports that Create was asked to create a namespace that already has a manifest entry. The check is made inside the header swap, so it reflects the namespace's state at the instant of the write, not a stale read.

Functions

func Exists

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

Exists reports whether a namespace exists in the vault, 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 the namespace existing. Because namespaces are persistent, this is true for a namespace that holds no secrets too (one created empty, or emptied by deletes), not only one that holds some. 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
	By          string
	Sensitivity string
	Egress      []string
}

Meta is a live key's advisory metadata: what the secret is for, when its write happened (wall-clock Unix seconds; 0 means the write predates timestamps), and who last wrote it (By, a label). Sensitivity and Egress are decoded here for forward compatibility but are not populated or consumed yet. 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.

A commit whose result holds zero secrets is NOT special-cased: it records a normal blob with an empty entry set and keeps the manifest entry, so a namespace persists once it exists (created by Create or by a first set) even after its last secret is removed. Removal of the namespace itself is the deliberate, separate Delete; emptying it is just another write. This is what makes a namespace a first-class container rather than a side effect of holding secrets.

func (*Namespace) Create added in v0.21.0

func (n *Namespace) Create(ctx context.Context, pin func(*crypto.Header), description string) error

Create records a namespace that holds no secrets: a fresh empty blob (carrying the given description and this write's creation stamp) plus its manifest entry, so a namespace can be brought into existence deliberately rather than only as a side effect of the first set (which still works). If the namespace already has an entry it returns ErrNamespaceExists and touches nothing: the guard is evaluated against the freshly re-read header inside the swap, so a concurrent first write cannot be clobbered by a racing Create. Same swap, cleanup, and pin contract as Commit.

func (*Namespace) Delete added in v0.21.0

func (n *Namespace) Delete(ctx context.Context, pin func(*crypto.Header)) error

Delete removes a namespace entirely: it drops the manifest entry and reclaims every blob under the namespace prefix. It is the deliberate counterpart to the persistent-namespace model, where emptying a namespace (Commit to zero secrets) keeps it; this is how a namespace actually goes away. It never reads or decrypts the blob, so it removes a namespace whose current blob is corrupt or missing exactly as readily as a healthy one, doubling as a recovery tool. Deleting a namespace that has no entry is a harmless no-op (callers that want a "not found" error check the manifest first). Same swap, cleanup, and pin contract as Commit: the entry is dropped under the header compare-and-swap and the namespace's blobs are reclaimed once it commits.

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) WithStamp added in v0.21.0

func (n *Namespace) WithStamp(s Stamp) *Namespace

WithStamp sets the actor and time stamped onto namespace-level metadata (created/updated) at the WriteBlob chokepoint, so every write records who last changed the namespace and when, with no per-command drift. It returns the receiver for chaining: secrets.For(...).WithStamp(s).Commit(...). Per-secret `by` rides on each Write, not this.

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 NamespaceMeta added in v0.21.0

type NamespaceMeta struct {
	Description string
	Created     int64
	CreatedBy   string
	Updated     int64
	UpdatedBy   string
	Sensitivity string
	Egress      []string
}

NamespaceMeta is a namespace's advisory metadata (the in-memory, decoded form): a description, creation and last-modification stamps, and reserved sensitivity/egress defaults. Created/Updated are wall-clock Unix seconds (0 = unknown). Like Meta, it is advisory and forgeable.

type Stamp added in v0.21.0

type Stamp struct {
	By string
	TS int64
}

Stamp is the actor and wall-clock time the command layer attributes a write to: who (a label, e.g. user@host) and when (Unix seconds). The secrets package never reads a clock or the environment, so the caller supplies both, exactly as it already supplies per-Write TS. A zero Stamp (the default when WithStamp is not called) leaves the namespace's who/when metadata unset, which a read resolves as "unknown"; this is what tests and pure reads get.

type State

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

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

The plaintext here is deliberately not zeroed after use. notenv is a short-lived CLI invocation, not a long-running daemon, so a value lives only for the seconds the process runs; Go strings are immutable and cannot be wiped in place, and the age decrypt path allocates intermediate buffers the underlying library does not scrub either, so zeroing this map would be partial at best while implying a guarantee the rest of the path cannot keep. The defense against memory disclosure is the short process lifetime plus the OS (core-dump and swap controls), not in-process scrubbing; a compromised live machine is a documented non-goal.

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