secrets

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 28, 2026 License: MIT Imports: 33 Imported by: 0

Documentation

Overview

Package secrets implements the credential-layer machinery described in docs/SPEC-SECRETS.md. It provides four verbs -- exec, names, check, and keygen -- as a thin wrapper over sops and age-keygen, linking no cryptography of its own.

THE ONE RULE THIS PACKAGE EXISTS TO KEEP: a credential value is never printed, logged, put in an argument list, or written anywhere but the child process environment that asked for it by name.

Index

Constants

View Source
const DetailWithheld = "[detail withheld: it contained credential material]"

DetailWithheld replaces a WHOLE detail line found to contain credential material. Deliberately not a surgical redaction: cutting the secret out of the middle of a string and printing the rest is how a truncated secret gets printed.

View Source
const KeygenNextLine = "SECRETS RULE NEXT: add these two lines to .sops.yaml (or run `nova-secrets seat add`)"

KeygenNextLine is the one line that tells the reader what is left to do in the rule block. It is a NEXT STEP and says so: the same text as a state of the world ("the placeholder stands unfilled") read as a failure at the end of a green run (nova-tools#1393, Glenn on the Air, 2026-09-18).

View Source
const MinAgeKeygenVersion = "1.3.2"
View Source
const MinLeakFragment = 6

MinLeakFragment is how many consecutive bytes of a secret count as a leak. Six, not "the whole value", because the leak that actually happens is a truncated one.

View Source
const MinSopsVersion = "3.13.3"
View Source
const Redacted = "[redacted]"

Redacted is the only string a Secret ever yields to a formatter. One word, in one place, so no caller can produce a "helpfully" partial one.

Variables

This section is empty.

Functions

func CheckAgeKeygenVersion

func CheckAgeKeygenVersion(ageKeygenPath string) (string, error)

CheckAgeKeygenVersion probes the age-keygen binary.

func CheckInvariant6

func CheckInvariant6(keyPath string) error

CheckInvariant6 verifies that the key file mode is 0600 and directory is 0700.

func CheckInvariant8

func CheckInvariant8(storeDir string) (status GitRefStatus, failure *CheckFailure, refusal error)

CheckInvariant8 inspects .git directly for remote tracking ref sync.

func CheckSopsVersion

func CheckSopsVersion(sopsPath string) (string, error)

CheckSopsVersion probes the sops binary with --disable-version-check to prevent network calls.

func DecryptFile

func DecryptFile(sopsPath, keyPath, filePath string) ([]byte, error)

DecryptFile invokes sops -d on a file using an isolated environment. It sets SOPS_AGE_KEY_FILE to keyPath, strips all other SOPS_* variables, and sets HOME and XDG_CONFIG_HOME to an empty temporary directory.

func ExtractPublicKeyFromKeyFile

func ExtractPublicKeyFromKeyFile(keyPath string) (string, error)

ExtractPublicKeyFromKeyFile parses '# public key: (age1...)' from an age private key file.

func GitBlobSHA1

func GitBlobSHA1(content []byte) [20]byte

GitBlobSHA1 computes the SHA1 hash of a git blob object for content.

func IsValidAgePublicKey

func IsValidAgePublicKey(s string) bool

IsValidAgePublicKey verifies an age public key string (bech32).

func IsValidAsName

func IsValidAsName(name string) bool

IsValidAsName checks if a seat name matches [A-Za-z0-9_-]+

func IsValidEnvVar

func IsValidEnvVar(s string) bool

IsValidEnvVar verifies an environment variable name: ^[A-Z][A-Z0-9_]*$

func Leaks

func Leaks(text string, s Secret) bool

Leaks reports whether text contains this secret, whole or in a fragment of at least MinLeakFragment consecutive bytes.

func ParseDecryptedSecrets

func ParseDecryptedSecrets(data []byte) (map[string]Secret, []string, error)

ParseDecryptedSecrets parses the output of 'sops -d' into Secret values. Rejects multi-line values, NUL bytes, and illegal variable names.

func PullStore

func PullStore(o StorePullOptions) (string, error)

PullStore fast-forwards the store over the bench's own key and returns the new HEAD. The key is checked before any child starts, so a refused key is never offered to anybody.

func ReadFleetMachines

func ReadFleetMachines(path string) (map[string]FleetMachine, error)

ReadFleetMachines parses the tab-separated fleet registry: name, ssh target, home, and the optional fourth column the pulse fleet file carries. Blank lines and `#` comments are skipped; a line with fewer than two fields, or an empty name or target, is refused.

func ReadGitIndexTrackedFiles

func ReadGitIndexTrackedFiles(storeDir string) (map[string]bool, error)

ReadGitIndexTrackedFiles reads .git/index directly to find all tracked files.

func ReadHEADTreeBlobs

func ReadHEADTreeBlobs(storeDir string) (map[string]string, error)

ReadHEADTreeBlobs reads the tree of the HEAD commit and returns a map of relative path -> blob SHA1. It inspects loose objects directly in pure Go, with fallback to git ls-tree if needed.

func ReadRecoveryPub

func ReadRecoveryPub(storeDir string) (string, error)

ReadRecoveryPub reads and validates the recovery.pub file at store root.

func RunCheck

func RunCheck(storeDir, asName, keyPath, sopsPath string, maxShown int) (okLine string, failLines []string, moreLines []string, summaryLine string, exitCode int, err error)

RunCheck executes the complete verification suite for 'check'.

func RunExec

func RunExec(storeDir, asName, keyPath, sopsPath, onlyArg string, required []string, cmdArgs []string) (int, error)

RunExec validates all preflight invariants, decrypts <store>/<as>.yaml, applies --only and --require filters, prints the OK line to stderr, sets RLIMIT_CORE to 0, and replaces the current process with cmdArgs.

func RunGate

func RunGate(in GateInput) (string, int)

RunGate is the seat-rule gate as a verb: the store's shell gate, called by the workflow. It diffs --base..--head with git (no GitHub) and either prints "GATE APPROVE files=<n> machines=<registry|->" at exit 0 or "GATE REFUSE rule=<n> file=<f>: <why>" at exit 2.

func RunKeygen

func RunKeygen(asName, keyPath, ageKeygenPath, storeDir string) (lines []string, err error)

RunKeygen generates a new age private key and formats the .sops.yaml rule block. It returns the receipt as ordered lines; the caller prints them in that order.

func RunNames

func RunNames(storeDir, asName string, maxShown int) (okLine string, nameLines []string, moreLine string, err error)

RunNames reads <store>/<as>.yaml without decrypting and reports top-level key names.

func RunPlace

func RunPlace(in PlaceInput) (string, error)

RunPlace copies one secret to one machine and records a receipt. It returns the one OK line, or an error whose text is safe to print (it never contains the value).

func RunPlaced

func RunPlaced(in PlacedInput) (string, []string, error)

RunPlaced lists the receipts written for one machine, by name and hash.

func RunSeal

func RunSeal(opts SealOptions) (line string, err error)

RunSeal reads one value, folds it into the seat file under --name, and carries the change through a branch, a commit and, unless --no-pr, a pull request to its merge.

func RunSeatAdd

func RunSeatAdd(opts SeatAddOptions) ([]string, error)

RunSeatAdd re-seals the named values out of a source seat into a new seat's file and writes that seat's rule. It returns the receipt as ordered lines, the verdict last. No value appears on any of them, ever.

func RunSeatInject

func RunSeatInject(opts SeatInjectOptions) (string, error)

RunSeatInject re-seals the --only values out of the source seat into an existing seat's file, encrypted to that file's own recipients, and carries the change to the store as `seal` does. It returns the one OK line; no value appears on it, ever.

func SafeDetail

func SafeDetail(detail string, s Secret) (string, bool)

SafeDetail gates a human-readable detail line against a secret. It returns the line, or DetailWithheld, and whether it withheld.

func StorePullKey

func StorePullKey(o StorePullOptions) (string, error)

StorePullKey checks that o.Key is the bench's own key, and returns its cleaned absolute path. It refuses, in this order: a key inside the card wall; a symlink (the classic way a person's key is lent to a bench); a key not under <SeatHome>/.ssh; a key readable by anyone but its owner; a key whose private half is not the key its public half describes (the comment in <key>.pub proves nothing about the key ssh will offer until the two are shown to be one key); and a key whose public half names anyone but the seat, which is a person's credential wherever the file was put.

func StorePullSSHCommand

func StorePullSSHCommand(o StorePullOptions) (string, error)

StorePullSSHCommand is the GIT_SSH_COMMAND the pull runs under: the checked key and only it, with no agent, so a forwarded or loaded person's key is never offered in its place.

func ValidateAdmissibleStore

func ValidateAdmissibleStore(storeDir string) (status GitRefStatus, headBlobs map[string]string, indexData *GitIndexData, failures []CheckFailure, refusal error)

ValidateAdmissibleStore verifies that the git working copy at storeDir is an admissible store. It verifies: 1. Invariant 8 ref tracking: HEAD equals remote-tracking ref (via CheckGitWorkingCopy). 2. HEAD commit tree and git index can be read. 3. Every tracked store file (*.yaml, .sops.yaml, recovery.pub) in HEAD tree or git index:

  • exists in working copy as a regular file (no deletions or non-regular artifact replacements)
  • is tracked in index and committed in HEAD tree (no uncommitted additions or index drift)
  • working copy blob SHA1 matches HEAD tree blob SHA1 and git index blob SHA1 (no unstaged or staged modifications)

func VerifyFileMatchesHEADTree

func VerifyFileMatchesHEADTree(storeDir, filePath string, headBlobs map[string]string) error

VerifyFileMatchesHEADTree checks that filePath matches the blob committed in HEAD commit tree.

func VerifyFileMatchesIndex

func VerifyFileMatchesIndex(storeDir, filePath string, index *GitIndexData) error

VerifyFileMatchesIndex checks that the file on disk matches its index blob SHA1.

Types

type CheckFailure

type CheckFailure struct {
	Kind   string // invariant kind: "rule-shape", "recipients-drift", "unsealed", "decrypt-failed", "foreign-openable", "store-private-key", "untracked-plaintext", "stale-working-copy"
	File   string // file path or subject
	Reason string // diagnostic detail
}

CheckFailure records one invariant failure found during 'check'.

func CheckInvariant1

func CheckInvariant1(storeDir string, sopsCfg *SopsConfig, recoveryKey string) []CheckFailure

CheckInvariant1 checks .sops.yaml syntax and shape against the declared recovery key.

func CheckInvariant2

func CheckInvariant2(storeDir string, sopsCfg *SopsConfig, files []string) []CheckFailure

CheckInvariant2 verifies that each file's sops recipient block matches its creation rule.

func CheckInvariant3

func CheckInvariant3(storeDir string, sopsCfg *SopsConfig, files []string) (failures []CheckFailure, sealedCount int, clearCount int)

CheckInvariant3 verifies that every file is sealed and only permitted keys are in the clear.

func CheckInvariant4

func CheckInvariant4(storeDir, sopsPath, keyPath, seatPubKey string, files []string) (failures []CheckFailure, mineCount int, foreignCount int)

CheckInvariant4 tests that the seat key opens exactly the files listing its public key.

func CheckInvariant5

func CheckInvariant5(storeDir, keyPath string) []CheckFailure

CheckInvariant5 verifies that no private key is stored under storeDir.

func CheckInvariant7

func CheckInvariant7(storeDir string, trackedFiles map[string]bool) []CheckFailure

CheckInvariant7 verifies that untracked files hold no plaintext secrets.

type CheckSummary

type CheckSummary struct {
	Recipients int // unique recipient public keys across all rules
	Files      int // count of *.yaml files (excluding .sops.yaml)
	Sealed     int // count of files that are sealed
	Mine       int // count of files whose recipients list this seat
	Foreign    int // count of files whose recipients do not list this seat
	Clear      int // count of keys in the clear under unencrypted_regex
	Failures   []CheckFailure
}

CheckSummary aggregates counts and failures across the store.

type CreationRule

type CreationRule struct {
	PathRegex        string
	UnencryptedRegex string
	Recipients       []string
	NonAgeRecipients []string
}

CreationRule defines one rule in .sops.yaml.

func FindMatchingRule

func FindMatchingRule(cfg *SopsConfig, relPath string) (*CreationRule, error)

FindMatchingRule locates the creation rule that governs filePath.

type FleetMachine

type FleetMachine struct {
	Name   string
	Target string
	Home   string
}

FleetMachine is one machine in the fleet registry: a name, the ssh target that reaches it, and its home directory (used as the default root for a placed secret).

type GateInput

type GateInput struct {
	StoreDir     string
	Base         string
	Head         string
	MachinesPath string // the fleet machines registry; "" leaves the recipient rule dormant
}

GateInput is one call of the gate: the store, the two refs, and -- optionally -- the fleet's machines registry, the only thing that can vouch for a seat nobody has seen before.

type GitIndexData

type GitIndexData struct {
	Entries map[string]GitIndexEntry
}

GitIndexData holds all parsed entries from .git/index.

func ReadGitIndex

func ReadGitIndex(storeDir string) (*GitIndexData, error)

ReadGitIndex reads .git/index directly to find all tracked files and their blob SHAs. Supports index formats v2, v3, and v4 (prefix compression).

type GitIndexEntry

type GitIndexEntry struct {
	Path     string
	BlobSHA1 [20]byte
}

GitIndexEntry holds path and blob SHA1 parsed from .git/index.

type GitRefStatus

type GitRefStatus struct {
	HeadSHA   string
	RemoteRef string
	RemoteSHA string
	Clean     bool
	Refusal   string
}

GitRefStatus holds the result of verifying invariant 8 against a git working copy.

func CheckGitWorkingCopy

func CheckGitWorkingCopy(storeDir string) (GitRefStatus, error)

CheckGitWorkingCopy inspects .git directly without invoking the git binary. It verifies invariant 8: HEAD equals the remote-tracking ref it tracks. Refusals are returned for: - .git is missing or not a directory (a file indicates submodule or worktree) - detached HEAD - branch with no remote tracking upstream

type PlaceInput

type PlaceInput struct {
	StoreDir   string
	AsName     string
	KeyPath    string
	SopsPath   string
	Machine    string
	Secret     string
	RemotePath string
	Machines   string
	Receipts   string
	SSH        string
	Now        func() time.Time
}

PlaceInput is one `nova-secrets place` invocation.

type PlacedInput

type PlacedInput struct {
	Machine  string
	Receipts string
}

PlacedInput is one `nova-secrets placed` invocation.

type SealOptions

type SealOptions struct {
	StoreDir string
	AsName   string
	KeyPath  string
	SopsPath string
	Name     string
	GHPath   string
	GitPath  string

	NoPR     bool
	UseStdin bool

	Stdin           io.Reader
	StdinIsTerminal bool

	// Progress receives one short line per step that can take time (nil = silent).
	// A person at a terminal must be able to tell waiting from hung: any interactive
	// verb says what it is doing before each step that can exceed a blink. The lines
	// go to stderr so the one-line result on stdout stays the whole machine answer.
	Progress io.Writer

	Now   func() time.Time
	Check func(storeDir, asName, keyPath, sopsPath string) error

	// Exec replaces the os/exec child process. Tests set it to a pure-Go fake
	// so the seal path runs where a POSIX shell-script fake cannot.
	Exec execCommand
}

SealOptions carries one seal request and the test seams around it.

The value travels from Stdin (or the controlling terminal) to the encrypt child's stdin and nowhere else: not argv, not a file in the clear, not an output line.

type SeatAddOptions

type SeatAddOptions struct {
	StoreDir string
	AsName   string // the new seat
	Pub      string // the new seat's age public key
	From     string // a seat this machine can open
	Only     string // comma-separated key names to carry over
	KeyPath  string // this machine's key: the source seat's identity
	SopsPath string

	// Progress receives one short line per step that can take time (nil = silent).
	// It never carries a value: step names and public facts only.
	Progress io.Writer

	// Exec replaces the os/exec child process, as it does for seal.
	Exec execCommand
}

SeatAddOptions carries one `seat add` request and the test seam around it.

KeyPath is THIS machine's key, the one that opens From. Pub is the NEW seat's public half, which arrives from the new bench's own `keygen` receipt -- never a private key, which never leaves the bench that made it.

type SeatFile

type SeatFile struct {
	Path    string            // <store>/<seat>.yaml
	HeadSHA string            // the store's HEAD the file was read at
	Secrets map[string]Secret // every key in the file, by name
}

SeatFile is one seat's file, opened. Values stay inside Secret: nothing here prints, logs or returns one as a string.

func OpenSeatFile

func OpenSeatFile(storeDir, asName, keyPath, sopsPath string) (SeatFile, error)

OpenSeatFile runs every check exec runs before it decrypts -- the store is a directory working copy with a .sops.yaml and the seat's file, invariant 8 (HEAD equals its upstream, nothing uncommitted or untracked), invariant 1 (recovery.pub and the creation rules), invariant 6 (the key file's mode) and the sops version -- then decrypts <store>/<seat>.yaml with sops isolated from the caller's identities and parses it. It is the one path from a seat to its values: `nova-secrets exec` takes it before it replaces itself with the command, and a nova tool given --seat takes it in its own process (internal/seatcred, nova-tools#4052), so no shell wrapper stands between the two and neither can check less than the other.

type SeatInjectOptions

type SeatInjectOptions struct {
	StoreDir string
	AsName   string // the existing seat receiving the values
	From     string // a seat this machine can open
	Only     string // comma-separated key names to deliver
	KeyPath  string // this machine's key: the source seat's identity
	SopsPath string
	GHPath   string
	GitPath  string

	NoPR bool

	// Progress receives one short line per step that can take time (nil = silent).
	// It never carries a value: step names and public facts only.
	Progress io.Writer

	Now   func() time.Time
	Check func(storeDir, asName, keyPath, sopsPath string) error

	// Exec replaces the os/exec child process, as it does for seal.
	Exec execCommand
}

SeatInjectOptions carries one `seat inject` request and the test seams around it.

type Secret

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

Secret carries a credential value. The zero value is "never looked up", which is distinct from "looked up and found empty".

WHY A TYPE AND NOT A string: every formatting route is closed -- String, GoString, Format (which covers %v %s %q %x %#v and every other verb), MarshalText and MarshalJSON all yield Redacted. There is deliberately no accessor that RETURNS the value: Use hands it to a callback and takes it back, so an exposure is a visible, deliberate shape rather than an interpolation.

func NewSecret

func NewSecret(v string) Secret

NewSecret wraps a value. The caller should drop its own copy immediately.

func (Secret) Empty

func (s Secret) Empty() bool

Empty reports whether the value is the empty string. A present-but-empty entry is not a credential.

func (Secret) Format

func (s Secret) Format(f fmt.State, verb rune)

Format closes every fmt verb at once. String() alone is not enough: %#v goes to GoStringer, %x and %d bypass Stringer entirely.

func (Secret) GoString

func (s Secret) GoString() string

func (Secret) Len

func (s Secret) Len() int

Len is the byte length of the value.

func (Secret) Loaded

func (s Secret) Loaded() bool

Loaded reports whether a lookup actually produced a value.

func (Secret) MarshalJSON

func (s Secret) MarshalJSON() ([]byte, error)

func (Secret) MarshalText

func (s Secret) MarshalText() ([]byte, error)

func (Secret) String

func (s Secret) String() string

func (Secret) Use

func (s Secret) Use(fn func(string) error) error

Use is the ONLY route from a Secret back to a string, and it does not return one.

type SopsConfig

type SopsConfig struct {
	CreationRules []CreationRule
}

SopsConfig holds creation rules parsed from .sops.yaml.

func ParseSopsConfig

func ParseSopsConfig(storeDir string) (*SopsConfig, error)

ParseSopsConfig parses creation rules from .sops.yaml in storeDir.

type StoreFileKey

type StoreFileKey struct {
	Name  string
	Value string
	Clear bool
}

StoreFileKey represents a top-level key and clear status in a store file.

func ParseStoreFileWithoutDecrypting

func ParseStoreFileWithoutDecrypting(filePath string) (keys []StoreFileKey, recipients []string, hasSops bool, err error)

ParseStoreFileWithoutDecrypting extracts keys, clear status, and recipients from a sealed yaml.

type StorePullOptions

type StorePullOptions struct {
	StoreDir string   // the store's working copy on this bench
	Seat     string   // the bench seat pulling, e.g. swarm-studio
	SeatHome string   // the seat's own home; its key lives in <SeatHome>/.ssh
	Key      string   // the private half of the seat's SSH key
	Wall     []string // the card wall's directories (slot, job, work): the key may lie in none
	Git      string   // the git binary; empty is `git`
	SSH      string   // the ssh binary; empty is `ssh`
}

StorePullOptions names everything the pull uses. Nothing is a default except the two binaries; every path is typed by the caller.

Jump to

Keyboard shortcuts

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