Documentation
¶
Overview ¶
Package configkeychain contributes secrets from the operating system's keychain as a first-class config layer — and, unusually for a secrets backend, accepts writes into it.
It exists because tokens keep ending up in plaintext config files. A tool that obtains an OAuth token has to put it somewhere, and without a layer that can hold it the fallback is the config file, which is how a refresh token comes to sit on disk in the clear next to the port number.
With a keychain layer above the file, two things follow from routing the core already does: a first-ever token is written INTO the keychain, because that is the highest-precedence writable layer; and a token the keychain already holds can never be written to the file beneath, because the core refuses a write whose target is not sensitive. What was a silent fallback becomes either a correct write or a refusal.
A keychain cannot be enumerated — the platform APIs offer get, set and delete and nothing that lists — so the key set is declared rather than discovered. See New.
The keychain is reached through credentials.Backend, the interface, never the go-keyring implementation. That keeps the keyring and dbus dependencies out of a consumer's graph unless they blank-import credentials/keychain themselves, which is the property that package's split exists to preserve.
See the config-keychain spec for the decisions behind all of this.
Index ¶
Constants ¶
const DefaultTimeout = 10 * time.Second
DefaultTimeout bounds a single keychain operation.
A keychain call is milliseconds when it works and unbounded when it does not: a locked collection waits on an unlock prompt, which on a headless host nobody can answer. credentials honours a context, but a Load on a startup path usually carries no deadline of its own, so this adapter imposes one rather than letting a configuration layer hang an application.
Ten seconds is generous for a working keychain and short enough that a stuck one looks like a failure rather than a hang. A desktop consumer expecting a human to type a passphrase should raise it with WithTimeout.
const SourceKind = config.SourceKind("keychain")
SourceKind is what a layer from this backend reports as, so provenance can name the keychain as the origin of a value — and so that an empty keychain does not present as a file. See [backend.SourceKind].
Variables ¶
ErrKeychainUnavailable is returned by Load when the injected backend reports that it cannot serve requests at all.
It is deliberately not an absent source. Contributing nothing would be indistinguishable from "no tokens stored yet", which is an ordinary state, and silently degrading is precisely what put tokens in plaintext files to begin with. A consumer that would rather carry on can catch this and omit the layer — a decision written in their code rather than one nobody made.
var ErrUndeclaredKey = errors.New("configkeychain: no keychain account declared for this key")
ErrUndeclaredKey is returned when a write targets a path this backend was not given an account for.
The mapping is the whole key space here, because a keychain cannot be enumerated: there is no account to write to unless the consumer named one. The alternative — inventing an account name from the path — is the naming convention D3 rejects, and it would put a secret somewhere no read would ever look for it.
var ErrUnwritableValue = errors.New("configkeychain: value cannot be written to a keychain entry")
ErrUnwritableValue is returned when a Set carries something a keychain cannot hold. Entries are strings; a map or slice has no representation here.
Functions ¶
func New ¶
func New(backend credentials.Backend, service string, keys map[string]string, opts ...Option) config.Backend
New returns a backend contributing the declared secrets as one config layer.
keys maps a config path to the keychain account holding it, all under one service name:
configkeychain.New(backend, "keryx", map[string]string{
"platforms.instagram.access_token": "instagram-access-token",
})
The mapping is declared rather than discovered because a keychain cannot be enumerated, and it is explicit in both directions rather than derived from the path: a naming convention would compute account names that existing entries do not have, making every already-stored credential unreadable.
backend is credentials.Backend — a fake in tests, and in production the keychain implementation the consumer activates by blank-importing credentials/keychain.
func Registered ¶
func Registered() credentials.Backend
Registered returns a credentials.Backend backed by whatever the consumer activated in the credentials registry.
It exists because the two packages have different shapes. credentials is built around a blank import — importing credentials/keychain registers the OS backend, and the package-level Store, Retrieve and Delete delegate to whatever is registered — while this adapter takes a Backend value so its unit suite can inject a fake. Without a bridge, a consumer would have to import the keychain implementation to get a value, which is exactly what drags go-keyring and godbus into their dependency graph whether they wanted it or not.
So the usual production wiring is:
import _ "gitlab.com/phpboyscout/go/credentials/keychain" // activate config.WithBackend(configkeychain.New(configkeychain.Registered(), "myapp", keys))
A build that must not carry keychain IPC simply omits the blank import, and Registered then reports unavailable — which Load turns into ErrKeychainUnavailable rather than silence.
Types ¶
type Option ¶
type Option func(*backend)
Option configures a backend.
func WithTimeout ¶
WithTimeout bounds each keychain operation. The default is DefaultTimeout.
Raise it where a human may be asked to unlock a keychain interactively; lower it on a service that must fail fast.