Documentation
¶
Overview ¶
Package credentials describes how a user-supplied secret (VCS token, AI-provider API key, passphrase) is persisted by an interactive setup flow and resolved by a runtime config chain.
Storage modes ¶
Three modes are supported, in descending order of preference:
- ModeEnvVar — the config records the NAME of an environment variable. The actual secret lives outside the config file, typically in the user's shell profile or a CI platform's secret-injection mechanism. This is the recommended default and the only mode permitted under CI.
- ModeKeychain — the config records a keychain reference (`<service>/<account>`). The secret lives in the OS keychain (macOS Keychain, Linux Secret Service via godbus, Windows Credential Manager). Available only when the process has a keychain-capable Backend registered — typically by blank- importing the optional gitlab.com/phpboyscout/go/credentials/keychain subpackage. Without such a registration, Store / Retrieve / Delete return ErrCredentialUnsupported and resolvers fall through.
- ModeLiteral — the secret is written as plaintext in the config file. Supported for backward compatibility and for air-gapped or throwaway environments. Refused under CI.
Why a dedicated package ¶
A setup wizard, config masking, doctor checks, and migration tooling all reason about "which storage modes are available" and "how do we retrieve a credential stored in each mode". Consolidating those concerns here avoids scattering the same switch statement across every call site.
Interactive capture ¶
Choosing a mode, naming an environment variable, and entering a secret are captured through the Prompter seam. The package ships a stdlib default (DefaultPrompter); a tool with its own themed TUI implements Prompter so credential prompts match the rest of its UX. ModeChoices returns the selectable modes as plain data any UI can render.
Separation of the keychain backend ¶
The go-keyring-backed implementation lives in a dedicated subpackage (gitlab.com/phpboyscout/go/credentials/keychain) that registers itself via RegisterBackend during its init(). Downstream tools that want OS keychain support blank-import that subpackage from their main; regulated or compliance-constrained downstreams omit the import and run with the stub Backend, so linker dead-code elimination keeps go-keyring, godbus, and wincred out of their binary even though the packages exist in the module. Binary-level SBOM review (syft, cyclonedx-gomod on the linked artefact) shows the opt-out surface unambiguously.
Index ¶
- Constants
- Variables
- func ClearKeysExcept(w KeyWriter, all []string, keep ...string)
- func Delete(ctx context.Context, service, account string) error
- func IsCI() bool
- func KeychainAvailable() bool
- func Probe(ctx context.Context) bool
- func RefuseLiteralUnderCI(mode Mode) error
- func RegisterBackend(b Backend)
- func Retrieve(ctx context.Context, service, account string) (string, error)
- func Store(ctx context.Context, service, account, secret string) error
- func ValidateEnvVarName(name string) error
- type Backend
- type KeyWriter
- type Mode
- type ModeChoice
- type Prompter
Constants ¶
const KeychainOpTimeout = 5 * time.Second
KeychainOpTimeout bounds any single credentials-backend operation (probe, store, retrieve, delete) performed by a setup flow so a misbehaving remote or locked keychain cannot stall first-run setup indefinitely. Callers SHOULD derive a context with this timeout before calling Probe, Store, Retrieve, or Delete.
Variables ¶
var ErrCredentialNotFound = errors.New("credential not found in keychain")
ErrCredentialNotFound is returned by Retrieve when the backend is reachable but no entry exists for the given service/account pair. Distinguished from ErrCredentialUnsupported so resolvers can decide whether to fall through to a literal config value.
var ErrCredentialUnsupported = errors.New("keychain support not compiled (import the credentials/keychain subpackage or register a custom Backend)")
ErrCredentialUnsupported is returned by Store, Retrieve, and Delete when no keychain-capable Backend has been registered — either because the optional credentials/keychain subpackage was not imported, or because a custom backend has opted out. Callers that want to fall through to a literal or env-var step should errors.Is against this sentinel.
Functions ¶
func ClearKeysExcept ¶
ClearKeysExcept blanks every config key in all that is not present in keep. Setup flows call it after writing the selected storage mode's key(s) so that re-running the flow in a different mode never leaves a prior secret — or a stale reference — behind. This enforces the invariant that exactly one of the env / literal / keychain key paths carries a value, so a prior mode cannot mask the new one at resolve time.
Keys are blanked (Set to "") rather than deleted because Viper has no unset primitive; the runtime resolution chain and doctor checks both treat an empty value as absent. Empty strings in all and keep are ignored.
func Delete ¶
Delete removes a secret from the registered Backend. Idempotent: returns nil when the entry is missing. Returns ErrCredentialUnsupported when no real backend is registered.
func IsCI ¶
func IsCI() bool
IsCI reports whether the process appears to be running under a CI system. The single source of truth for every setup flow's CI gate — in particular the ModeLiteral refusal enforced by RefuseLiteralUnderCI.
func KeychainAvailable ¶
func KeychainAvailable() bool
KeychainAvailable reports whether a keychain-capable Backend is registered. Returns false in the default (stub-backend) process. Setup flows use this as a first-pass gate before Probe.
func Probe ¶
Probe reports whether the registered Backend is reachable at the time of the call — useful for setup flows that want to hide the ModeKeychain option on hosts where the backend is compiled in but locked, unreachable (headless Linux without a Secret Service provider, Vault unreachable), or otherwise unusable. It performs a canary Set → Get → Delete round-trip under the reserved "credentials-keychain-probe" service with a per-invocation random account and discards the result.
With the stub backend, Probe short-circuits to false without touching anything. A true return therefore means both "a backend is registered" AND "the backend accepts round-trip calls right now", which is the precise guard a wizard needs before offering keychain as a storage option.
Callers SHOULD pass a context with a short timeout (a few seconds is reasonable) so a misbehaving remote backend cannot stall the setup flow indefinitely.
func RefuseLiteralUnderCI ¶
RefuseLiteralUnderCI returns a hinted error when literal credential storage is selected while running under CI, and nil otherwise. This is the single source of truth for the CI-literal-refusal invariant: literal-mode writes in CI almost certainly leak the secret to build artefacts or logs, so every setup flow funnels its defence-in-depth check through this helper rather than re-deriving it.
func RegisterBackend ¶
func RegisterBackend(b Backend)
RegisterBackend swaps the active backend. The keychain subpackage calls this from its init(); custom implementations (Vault, AWS SSM, …) may call it from anywhere safe, typically a tool's main() before the first credential call.
func Retrieve ¶
Retrieve reads a secret from the registered Backend. Returns ErrCredentialUnsupported when no real backend is registered, ErrCredentialNotFound when the backend is present but the entry is missing, or a wrapped backend error for other failures. A cancelled context aborts the lookup.
func Store ¶
Store writes a secret to the registered Backend. Returns ErrCredentialUnsupported when no real backend is registered. The context is forwarded to the backend so network-backed implementations (Vault, AWS SSM) can honour deadlines and cancellation; OS-keychain backends ignore it.
func ValidateEnvVarName ¶
ValidateEnvVarName enforces a conservative `^[A-Z][A-Z0-9_]{0,63}$` shape so the chosen name is a valid POSIX environment variable and fits downstream shell/YAML contexts without quoting. Returns a descriptive error suitable for surfacing directly in a prompt.
Types ¶
type Backend ¶
type Backend interface {
// Store writes a secret under the service/account pair. Must
// overwrite any existing entry with the same key. Neither argument
// may be logged by the implementation — resolvers rely on being
// able to pass these to DEBUG log surfaces safely. Implementations
// SHOULD return ctx.Err() when the context is cancelled before
// the write commits.
Store(ctx context.Context, service, account, secret string) error
// Retrieve reads a secret. MUST return [ErrCredentialNotFound] when
// the backend is healthy but the entry does not exist, so resolvers
// can distinguish "missing" from "unavailable" and fall through
// cleanly. Other failures should wrap the underlying error. A
// cancelled context MUST abort the call.
Retrieve(ctx context.Context, service, account string) (string, error)
// Delete removes a secret. Idempotent: must return nil when the
// entry does not exist. Only surface real failures (e.g. keychain
// locked, remote unreachable) as errors.
Delete(ctx context.Context, service, account string) error
// Available reports whether this backend can currently satisfy
// Store/Retrieve/Delete without an immediate [ErrCredentialUnsupported].
// A freshly-registered keychain backend typically returns true; the
// default stub returns false. Available SHOULD be a cheap static
// check — use [Probe] for a live round-trip.
Available() bool
}
Backend is the minimal contract the credential store presents to setup flows, resolvers, and doctor checks. The default implementation (see default_backend.go) returns ErrCredentialUnsupported from every call, so callers that never register a real backend behave exactly as if the keychain feature were compiled out entirely.
Real backends are registered by a downstream optional module — gitlab.com/phpboyscout/go/credentials/keychain — via RegisterBackend. Tool authors may also plug in a custom backend (Vault, AWS SSM, 1Password Connect, …) by implementing this interface and calling RegisterBackend during program init. See the custom-backend how-to at https://credentials.go.phpboyscout.uk for a worked example.
Every method takes a context.Context so backends that perform network I/O (remote secret stores, hardware security modules) can honour deadlines and cancellation. OS-keychain backends ignore the context — local IPC is fast — but implementers SHOULD still accept it for uniformity.
type KeyWriter ¶
KeyWriter is the minimal config-write surface needed to clear stale credential keys. It is satisfied by a Viper instance and most config wrappers, so a setup flow can enforce the single-credential invariant on either the live config or a file-write handle without a dependency on any particular config package.
type Mode ¶
type Mode string
Mode identifies how a credential is persisted by a setup flow and resolved at runtime. See package doc for the full trust model.
const ( // ModeEnvVar stores the name of an environment variable that // contains the credential. Recommended default; the only mode // permitted under CI. ModeEnvVar Mode = "env" // ModeKeychain stores the credential in the OS keychain. The // config records a keychain reference; the secret never hits // disk. Available only when the process has registered a // keychain-capable [Backend] — typically by importing the // optional credentials/keychain subpackage. ModeKeychain Mode = "keychain" // ModeLiteral stores the credential value directly in the // config file. Supported for backward compatibility and // throwaway environments. Refused under CI. ModeLiteral Mode = "literal" )
func AvailableModes ¶
func AvailableModes() []Mode
AvailableModes returns the credential storage modes supported by this process. ModeKeychain is present only when a keychain-capable Backend is registered.
type ModeChoice ¶
ModeChoice pairs a storage Mode with a human-facing label for presentation in a mode selector.
func ModeChoices ¶
func ModeChoices(ci, keychainUsable bool, envLabel, keychainLabel, literalLabel string) []ModeChoice
ModeChoices returns the selectable storage modes for the current environment, each paired with the supplied label. It is the UI-agnostic replacement for the old huh-returning helper: callers render the returned slice with whatever UI they use (the stdlib DefaultPrompter, a themed TUI form, a web form, …).
ModeEnvVar is always included and always first. ModeKeychain is included only when keychainUsable is true (backend registered AND a live Probe succeeded — never offer a dead option). ModeLiteral is included only when not under CI, mirroring RefuseLiteralUnderCI.
type Prompter ¶
type Prompter interface {
// SelectMode asks the user to choose one storage mode from choices
// and returns the chosen Mode. choices is never empty ([ModeEnvVar]
// is always present). Implementations SHOULD default to the first
// choice when the user makes no explicit selection.
SelectMode(ctx context.Context, title string, choices []ModeChoice) (Mode, error)
// InputEnvVarName reads the name of an environment variable,
// re-prompting until validate returns nil (or the context is
// cancelled). placeholder is the default used when the user submits
// an empty line. validate may be nil.
InputEnvVarName(ctx context.Context, title, placeholder string, validate func(string) error) (string, error)
// InputSecret reads a secret value without echoing it to the
// terminal. Implementations MUST NOT echo the typed characters and
// MUST NOT log the result.
InputSecret(ctx context.Context, title string) (string, error)
}
Prompter captures the interactive input a credential-storage flow needs: choosing a storage mode, naming an environment variable, and entering a secret value. The module ships a stdlib implementation (DefaultPrompter); a tool that wants a themed TUI (e.g. huh) supplies its own so credential prompts match the rest of its UX.
Every method takes a context so a caller can bound how long it waits for input; implementations SHOULD honour cancellation where the underlying reader allows it. Implementations MUST NOT log secrets or echo them to the terminal.
func DefaultPrompter ¶
func DefaultPrompter() Prompter
DefaultPrompter returns the stdlib Prompter reading os.Stdin and writing prompts to os.Stderr. It suits any CLI that does not ship its own TUI; a tool with a themed TUI implements Prompter instead.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package keychain is the optional OS-keychain backend for gitlab.com/phpboyscout/go/credentials.
|
Package keychain is the optional OS-keychain backend for gitlab.com/phpboyscout/go/credentials. |
|
Package test provides test-only credential backends so downstream tests can exercise keychain-mode behaviour without pulling in github.com/zalando/go-keyring.
|
Package test provides test-only credential backends so downstream tests can exercise keychain-mode behaviour without pulling in github.com/zalando/go-keyring. |