Skip to content

API reference

Every exported symbol in gitlab.com/phpboyscout/go/credentials and its two subpackages, with what it does, what it defaults to, and what happens when it goes wrong. Signatures and doc comments are also generated on pkg.go.dev; this page adds the behaviour that a signature does not show.

Package credentials

Store

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

Writes secret under the service/account pair on the registered Backend, overwriting any existing entry.

  • Default backend: the stub, which returns ErrCredentialUnsupported and writes nothing.
  • Context: forwarded to the backend. The shipped OS-keychain backend bounds the call by the context and abandons it on expiry — see why keychain calls are bounded.
  • On error: nothing was written, unless the error is a context error, in which case the abandoned call may still complete later.

Retrieve

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

Reads the secret stored under service/account.

Outcome Return
Entry found the secret, nil
Backend healthy, no such entry "", ErrCredentialNotFound
No keychain-capable backend registered "", ErrCredentialUnsupported
Any other backend failure "", a wrapped backend error
Context cancelled or deadline exceeded "", the context error

Distinguish the first two error cases with errors.Is — they mean different things to a resolver, and only ErrCredentialNotFound says the store is working.

Delete

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

Removes the entry under service/account. Idempotent against a real backend: deleting an entry that does not exist returns nil.

The stub backend is the exception — it returns ErrCredentialUnsupported from Delete like everything else, because a process with no backend never stored anything to delete.

KeychainAvailable

func KeychainAvailable() bool

Reports whether the registered backend claims it can serve calls. It is the registered Backend's own Available() answer, so it is a cheap static check with no I/O: false under the default stub, true once the credentials/keychain subpackage is imported or a custom backend is registered.

It does not mean the keychain works. Use Probe before offering keychain storage to a user.

AvailableModes

func AvailableModes() []Mode

The modes this process is capable of: ModeEnvVar, then ModeKeychain when KeychainAvailable() is true, then ModeLiteral — always, including under CI. It does not filter by CI and does not probe. See storage modes.

Probe

func Probe(ctx context.Context) bool

Performs a live canary round-trip to decide whether keychain storage is usable right now: StoreRetrieveDelete under the reserved service credentials-keychain-probe, with a per-invocation account of the form probe-<pid>-<8 hex digits> and the value probe. The result is discarded.

  • Returns false immediately, touching nothing, when KeychainAvailable() is false.
  • Returns false when any of the three steps fails or the context expires.
  • Returns true only when all three succeed.

Each step is bounded by the caller's context independently of whether the backend honours it, so a wedged keychain cannot stall a setup flow. Pass a context with a deadline — KeychainOpTimeout is the suggested value.

A probe that times out can leave an entry behind

If the canary write is abandoned at the deadline but later completes, the credentials-keychain-probe entry is not cleaned up: the cleanup Delete runs only when the write and read both returned. The entry is inert — the value is the literal string probe — but it will be visible in the user's keychain. Clean up manually with secret-tool clear service credentials-keychain-probe on Linux, or the platform equivalent.

RegisterBackend

func RegisterBackend(b Backend)

Swaps the process-wide backend. Safe to call at any time from any goroutine — the registry is an atomic pointer — including from an init() while the program is already running credential calls.

  • A nil Backend is ignored and the previous backend stays in force.
  • The last registration wins. There is no chain and no fallback; composing backends means writing a Backend that wraps others.
  • The nil guard only catches a nil interface. A typed nil (var b *MyBackend; RegisterBackend(b)) is not nil as an interface, so it is installed and any method that dereferences the receiver panics on the next credential call. Register a real value.

ModeChoices

func ModeChoices(ci, keychainUsable bool, envLabel, keychainLabel, literalLabel string) []ModeChoice

Builds the selectable mode list for a UI. Filtering and ordering are documented under storage modes. Labels are used verbatim and never defaulted.

DefaultPrompter

func DefaultPrompter() Prompter

The stdlib Prompter: reads os.Stdin, writes prompts to os.Stderr (so prompts never pollute a piped stdout), and masks secret entry with golang.org/x/term when stdin is a terminal.

SelectMode

Prints the title, a numbered menu with the first entry marked *, and Select [1-N] (default 1):. An empty line selects the first choice. A non-numeric or out-of-range entry prints invalid selection … and re-prompts. An empty choices slice returns the error no storage modes offered.

InputEnvVarName

Prints title [placeholder]:, or title: when placeholder is empty. An empty line takes the placeholder. When validate is non-nil and returns an error, the error is printed and the prompt repeats; pass ValidateEnvVarName to enforce the POSIX shape. A nil validate accepts anything, including an empty string when there is no placeholder.

InputSecret

Prints title: and reads without echo only when stdin is a terminal. On a pipe — tests, echo secret | mytool, most CI — there is no terminal echo to suppress, so it falls back to a plain line read.

Cancellation limits of the default prompter

The context is checked before each prompt, not during a read. Once DefaultPrompter is blocked waiting for a line, cancelling the context does not unblock it; the check takes effect on the next iteration. A closed or exhausted input stream surfaces as a wrapped io.EOF, so a prompt against a closed stdin returns an error rather than looping.

IsCI

func IsCI() bool

True when the environment variable CI is exactly the string true. See configuration for the case-sensitivity trap.

ValidateEnvVarName

func ValidateEnvVarName(name string) error

Enforces ^[A-Z][A-Z0-9_]{0,63}$ — uppercase ASCII start, then uppercase ASCII, digits and underscores, 1 to 64 characters total.

Input Result
"" error: env var name is required
anything not matching the pattern error: env var name must match ^[A-Z][A-Z0-9_]{0,63}$
a matching name nil

Both errors are written for direct display in a prompt. Lowercase names, leading digits or underscores, and names over 64 characters are rejected — see configuration.

RefuseLiteralUnderCI

func RefuseLiteralUnderCI(mode Mode) error

Returns an error when mode is exactly ModeLiteral and IsCI() is true; nil otherwise. The error carries a cockroachdb/errors hint:

  • message: literal credential storage is refused under CI
  • hint: CI environments must use platform-injected secrets referenced via env-var mode.

Nothing calls this for you. It is the single place the rule lives, so call it on every path that can select a mode — including paths that read a mode from a config file rather than a menu.

ClearKeysExcept

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

Blanks every key in all that is not listed in keep, by calling w.Set(key, ""). Setup flows call it after writing the selected mode's key so that re-running setup in a different mode cannot leave a prior secret — or a stale reference — behind to mask the new one.

  • Keys are set to the empty string, not deleted — Viper has no unset primitive. Callers must treat an empty value as absent.
  • Empty strings in all and in keep are ignored.
  • It returns nothing and cannot fail; whether the write reaches disk is the KeyWriter's business.

Mode

type Mode string

The three constants and their string values are in storage modes.

ModeChoice

type ModeChoice struct {
    Mode  Mode
    Label string
}

A mode paired with the human-facing label you supplied to ModeChoices. Plain data, so any UI can render it.

KeychainOpTimeout

const KeychainOpTimeout = 5 * time.Second

The suggested bound on a single backend operation. It is advisory: nothing in the package applies it. Derive your own context from it before calling Probe, Store, Retrieve or Delete, or a locked keychain will block for as long as the platform takes.

ctx, cancel := context.WithTimeout(ctx, credentials.KeychainOpTimeout)
defer cancel()

Backend

type Backend interface {
    Store(ctx context.Context, service, account, secret string) error
    Retrieve(ctx context.Context, service, account string) (string, error)
    Delete(ctx context.Context, service, account string) error
    Available() bool
}

The contract every credential store satisfies. The obligations of each method — in particular that Retrieve MUST return ErrCredentialNotFound rather than an empty string, and that Delete MUST be idempotent — are tabulated in implement a custom backend.

Available must be a cheap static check and must not perform I/O; Probe is the live check.

Prompter

type Prompter interface {
    SelectMode(ctx context.Context, title string, choices []ModeChoice) (Mode, error)
    InputEnvVarName(ctx context.Context, title, placeholder string, validate func(string) error) (string, error)
    InputSecret(ctx context.Context, title string) (string, error)
}

The interactive-capture seam. Implementations MUST NOT echo or log a secret, and SHOULD honour cancellation where the underlying reader allows it. See theme the credential prompts.

KeyWriter

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

The minimal config-write surface ClearKeysExcept needs. A Viper instance satisfies it, as does any wrapper with a Set(string, any) method, so the module needs no dependency on a config package.

Sentinel errors

ErrCredentialUnsupported and ErrCredentialNotFound have their own page: errors reference.

Package credentials/keychain

Blank-importing this package registers an OS-keychain backend at init():

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

keychain.Backend

type Backend struct{}

A credentials.Backend over go-keyring: macOS Keychain, Linux Secret Service (GNOME Keyring, KWallet) over D-Bus, Windows Credential Manager. The zero value is usable, so keychain.Backend{} can be registered explicitly or embedded in a wrapping backend.

  • Available() returns true unconditionally — importing the package is the declaration that you want keychain behaviour. It is not a health check.
  • Retrieve maps go-keyring's ErrNotFound to credentials.ErrCredentialNotFound; other failures are wrapped as keyring.Get <service>/<account>.
  • Delete maps ErrNotFound to nil, making it idempotent.
  • Every call is bounded by the caller's context and abandoned on expiry — see why keychain calls are bounded.
  • Neither the service, the account nor the secret is ever interpolated into an error message beyond the service/account pair.

Package credentials/test

Import aliased, so the reading context is obvious:

import credtest "gitlab.com/phpboyscout/go/credentials/test"

test.MemoryBackend

type MemoryBackend struct { /* unexported */ }

An in-process credentials.Backend backed by a mutex-guarded map. The zero value is usable and it is safe for concurrent use, so tests that inject it directly may run in parallel. It performs no IPC and never pulls in go-keyring, so linking it does not compromise a keychain-free build.

It mirrors the real contract: Retrieve on an unknown pair returns ErrCredentialNotFound, Delete is idempotent, Store overwrites. It differs in one respect — a cancelled context makes every method return ctx.Err() immediately, where the OS backend may already have committed an abandoned call.

Available() returns true, so KeychainAvailable and Probe both succeed under it and your keychain-mode branches are exercised.

test.Install

func Install(t testingT) *MemoryBackend

Registers a fresh MemoryBackend as the process-wide backend and returns it, so a test can seed entries directly. On t.Cleanup it registers a stub backend.

Install is not parallel-safe, and cleanup restores a stub

Install mutates process-wide state, so a test that calls it must not call t.Parallel(), and an installed backend must not be shared across parallel subtests. Cleanup registers a stub rather than restoring whatever was registered before, so in a test binary that blank-imports credentials/keychain, the first Install permanently replaces the real keychain backend with a stub for the rest of the run. Register the keychain backend again yourself if a later test needs it.