credentials

package module
v0.2.1 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: MIT Imports: 14 Imported by: 0

README

credentials

Storage-mode abstraction for user-supplied secrets in Go CLIs — env-var reference, OS keychain, or literal — with a pluggable backend and an auditable keychain opt-out

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit — small, framework-free Go modules extracted from go-tool-base. Docs: credentials.go.phpboyscout.uk


gitlab.com/phpboyscout/go/credentials decides how a user-supplied secret (an API key, a VCS token, a passphrase) is persisted by an interactive setup flow and resolved at runtime. It defines three storage modes in descending order of preference — environment-variable reference, OS keychain, and literal — and a pluggable Backend behind them, so the same wizard, doctor check, and resolver logic works whether or not a given build ships keychain support.

Design

  • Framework-free core. The core's only external dependency is cockroachdb/errors (plus golang.org/x/term for the stdlib prompter's masked input). No config framework, no TUI, no go-tool-base — a depfootprint_test.go guard enforces it.
  • Auditable keychain opt-out. The go-keyring-backed OS keychain lives in the optional credentials/keychain subpackage. A tool activates it with a single blank import from its main; regulated or air-gapped builds omit the import and ship a binary the linker has stripped of go-keyring and all keychain IPC — provable via SBOM on the linked artefact. Without it, Store/Retrieve/ Delete return ErrCredentialUnsupported and resolvers fall through.
  • UI-agnostic capture, overridable. A Prompter seam captures the interactive input (pick a mode, name an env var, enter a secret). The module ships a stdlib DefaultPrompter; a tool overrides it with a themed TUI so credential prompts match the rest of its UX. ModeChoices returns plain data any UI can render.
  • CI-safe by default. Literal mode is refused under CI=true (RefuseLiteralUnderCI); env-var reference is the recommended, CI-friendly default.

Install

go get gitlab.com/phpboyscout/go/credentials

Add OS-keychain support (optional) with a blank import from your main:

import _ "gitlab.com/phpboyscout/go/credentials/keychain"

What's inside

  • ModesModeEnvVar, ModeKeychain, ModeLiteral; AvailableModes and ModeChoices filter them by CI state and a live keychain Probe.
  • Backend — the Store/Retrieve/Delete/Available contract, an always-compiled no-op stub default, and RegisterBackend for the keychain subpackage or a custom store (Vault, AWS SSM, 1Password — tool-author supplied).
  • Prompter — the interactive-capture seam plus the stdlib DefaultPrompter.
  • HelpersIsCI, ValidateEnvVarName, RefuseLiteralUnderCI, ClearKeysExcept (enforce the one-key-per-mode invariant), and Probe (canary round-trip before offering keychain).
  • test — an in-memory Backend for downstream tests that need keychain-mode behaviour without touching the host keychain.

Documentation

Full guides and the trust model: credentials.go.phpboyscout.uk. API reference: pkg.go.dev.

License

See LICENSE.

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:

  1. 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.
  2. 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.
  3. 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

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

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

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

func ClearKeysExcept(w KeyWriter, all []string, keep ...string)

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

func Delete(ctx context.Context, service, account string) error

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

func Probe(ctx context.Context) bool

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

func RefuseLiteralUnderCI(mode Mode) error

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

func Retrieve(ctx context.Context, service, account string) (string, error)

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

func Store(ctx context.Context, service, account, secret string) error

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

func ValidateEnvVarName(name string) error

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

type KeyWriter interface {
	Set(key string, value any)
}

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

type ModeChoice struct {
	Mode  Mode
	Label string
}

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.

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.

Jump to

Keyboard shortcuts

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