Documentation
¶
Overview ¶
Package keychain stores and retrieves secrets in the operating system's native secret store — the macOS Keychain, the Linux/*BSD Secret Service, or the Windows Credential Manager — behind one small, cgo-free interface.
It is a successor to zalando/go-keyring that removes go-keyring's two hard limits: the ~4 KB macOS command-line cap and the 2560-byte Windows credential-blob cap. Secrets of any size round-trip on every platform (Windows chunks transparently under the hood). There is no cgo on any platform, so callers cross-compile freely, and a rebuilt binary keeps access to what it stored — the properties a headless, frequently-rebuilt daemon needs.
Usage ¶
The package-level Set, Get, and Delete cover the common case with a silent default configuration. For a logger or a non-default access mode, construct a Keychain with New and call its methods.
Semantics ¶
Set is an upsert. Get returns the exact bytes previously stored, or ErrNotFound. Delete is idempotent — removing an absent item is not an error. Service and account together form the lookup key; neither may be empty. An empty secret is allowed and is distinct from an absent item.
Logging ¶
The library is silent by default. Pass WithLogger a *slog.Logger to trace, at debug level, which backend ran and how an operation resolved. The secret value is never logged.
Security ¶
Every backend protects data-at-rest and never writes a plaintext file itself. On Linux and Windows an item is readable, without a prompt, by any process of the same user — the deliberate trade for a headless daemon. None of the backends defends against code already executing as that user.
macOS access ¶
macOS additionally gates each keychain item by an access partition keyed to the creating binary's code identity. A process of the same identity reads without a prompt across restarts and rebuilds: this holds for the same binary and for any binary code-signed with the same stable Apple Team ID. An unsigned or ad-hoc-signed binary — the default go build output — is bound to its cdhash, which changes on every rebuild, so a rebuilt copy can no longer read what it stored. WithSecurityCLI is the opt-in escape hatch for that case; a stable Team ID signature needs nothing extra.
Example ¶
Store, read, then remove a secret with the package-level API.
package main
import (
"fmt"
"github.com/lexfrei/keychain"
)
func main() {
err := keychain.Set("myapp", "alice", []byte("s3cr3t"))
if err != nil {
fmt.Println("set:", err)
return
}
secret, err := keychain.Get("myapp", "alice")
if err != nil {
fmt.Println("get:", err)
return
}
fmt.Printf("stored %d bytes\n", len(secret))
err = keychain.Delete("myapp", "alice")
if err != nil {
fmt.Println("delete:", err)
}
}
Output:
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrAccessDenied = errors.New("keychain: access denied by the store's ACL or partition")
ErrAccessDenied wraps a macOS denial where the item exists but this process is not permitted to read it — the access partition or ACL rejected it, which an unlock cannot fix. The common cause is an unsigned or ad-hoc-signed binary reading its own item after a rebuild changed its code identity; such a caller should fall back to WithSecurityCLI or another store. It does not occur on Linux or Windows. Test for it with errors.Is.
var ErrInvalidKey = errors.New("keychain: service and account must both be non-empty")
ErrInvalidKey is returned when service or account is empty. The two together form the lookup key, so neither may be blank.
var ErrLocked = errors.New("keychain: store is locked and needs an interactive unlock")
ErrLocked wraps an error from a store that is locked and would need an interactive unlock to proceed — a headless caller cannot answer the prompt. It occurs on the Linux Secret Service when the collection is locked, and on macOS when a read would need a keychain prompt (errSecInteractionNotAllowed). Test for it with errors.Is; the wrapped error carries the platform detail.
var ErrNotFound = errors.New("keychain: item not found")
ErrNotFound is returned by Get when no item matches the service and account.
ErrUnavailable wraps an error from a store that is not reachable at all: on Linux, no session bus or no default collection; on macOS, the Security framework failed to load. It signals the caller to fall back to another store rather than retry. Test for it with errors.Is.
var ErrUnsupported = errors.New("keychain: platform not supported")
ErrUnsupported is returned on a platform whose backend is not implemented.
Functions ¶
func Get ¶
Get returns the secret stored under service and account, or ErrNotFound.
Example ¶
Branch on the typed errors with errors.Is instead of matching a message.
package main
import (
"errors"
"fmt"
"github.com/lexfrei/keychain"
)
func main() {
secret, err := keychain.Get("myapp", "alice")
switch {
case errors.Is(err, keychain.ErrNotFound):
fmt.Println("no such item")
case errors.Is(err, keychain.ErrLocked):
fmt.Println("unlock the store and retry")
case errors.Is(err, keychain.ErrUnavailable):
fmt.Println("no secret store available; fall back")
case errors.Is(err, keychain.ErrAccessDenied):
fmt.Println("this build cannot read the item; sign it or use WithSecurityCLI")
case err != nil:
fmt.Println("error:", err)
default:
fmt.Printf("got %d bytes\n", len(secret))
}
}
Output:
Types ¶
type AccessMode ¶
type AccessMode int
AccessMode controls read access. It is only meaningful on macOS; on Linux and Windows secrets are user-scoped and every mode behaves like TrustAll.
const ( // TrustAll lets any process of the same user read without a prompt. It // matches security -A, go-keyring, and the Linux and Windows default: // rebuild-safe and daemon-friendly. It protects data-at-rest, not against // code already running as the user. This is the default. TrustAll AccessMode = iota // TrustCurrentApp, on macOS only, lets only the creating binary read // silently; other apps are prompted. Stronger, but a rebuilt binary loses // access. TrustCurrentApp )
type Keychain ¶
type Keychain struct {
// contains filtered or unexported fields
}
Keychain is a handle to the OS secret store with a fixed configuration (a logger and a default access mode). Construct one with New; the package-level Set, Get, and Delete delegate to a default, silent instance.
func New ¶
New returns a Keychain configured by opts. Without WithLogger it is silent.
Example ¶
Construct a Keychain that traces at debug level; the library is silent otherwise, and never logs the secret value.
package main
import (
"fmt"
"log/slog"
"os"
"github.com/lexfrei/keychain"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))
kc := keychain.New(keychain.WithLogger(logger))
err := kc.Set("myapp", "alice", []byte("s3cr3t"))
if err != nil {
fmt.Println("set:", err)
}
}
Output:
func (*Keychain) Delete ¶
Delete removes the item under service and account. A missing item is not an error: Delete is idempotent.
type Option ¶
type Option func(*config)
Option configures a Keychain (via New) or a single Set call.
func WithAccessMode ¶
func WithAccessMode(mode AccessMode) Option
WithAccessMode selects the macOS read-access ACL. It defaults to TrustAll and is a no-op on Linux and Windows.
func WithLabel ¶
WithLabel sets a human-readable label where the store supports one — the macOS Keychain item label. It is ignored on Linux and Windows.
func WithLogger ¶
WithLogger routes debug-level tracing to logger. Without it the library is silent. The secret value is never logged, only its length and the lookup key.
func WithSecurityCLI ¶
func WithSecurityCLI() Option
WithSecurityCLI routes macOS operations through the /usr/bin/security tool instead of the native Security.framework API. It exists for one case: an unsigned or ad-hoc-signed binary that must read its own secret again after a rebuild, or share it with another application of the same user.
The native default binds an item to its creator's code identity (its cdhash), which changes on every rebuild, so a rebuilt unsigned binary can no longer read what it stored. Items written through /usr/bin/security instead live in the stable "apple-tool" access partition and stay readable across rebuilds and across apps. The trade is that any process of the user can then read the item without a prompt.
The value reaches the tool on its standard input, so it does not appear in this process's argument list. security(1) says the same thing about the alternative in its own usage text: "Use of the -p or -w options is insecure."
Two cases still use the argument list, where the value is briefly visible to the same user in ps: a secret whose base64 does not fit the roughly 4 KB line the tool reads per command (about 3 KB of secret, less the length of the service and account), and a service or account holding a newline, which one line cannot carry. Exposure there is the lesser fault against storing a truncated secret. The same tool buffer is what limits go-keyring on macOS, which guards the identical command with ErrSetDataTooBig. A service or account holding a NUL works on neither route, as it never did: Go rejects such an argument before the process starts.
It is a no-op on Linux and Windows, whose stores are user-scoped with no such partitioning. A binary code-signed with a stable Apple Team ID does not need it: the native default is already rebuild-stable for same-team readers.
Use it consistently: an item written with it must also be read with it. The two paths store the value differently, so mixing them on one item does not error — the value comes back garbled or missing. WithLabel and WithAccessMode have no effect in this mode.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
cf
Package cf is a minimal, cgo-free CoreFoundation binding for the darwin keychain backend.
|
Package cf is a minimal, cgo-free CoreFoundation binding for the darwin keychain backend. |
|
chunk
Package chunk stores an arbitrary-size secret across several fixed-capacity credential blobs, hiding the Windows Credential Manager per-blob byte cap behind a header-plus-chunks layout.
|
Package chunk stores an arbitrary-size secret across several fixed-capacity credential blobs, hiding the Windows Credential Manager per-blob byte cap behind a header-plus-chunks layout. |
|
secitem
Package secitem is the cgo-free Security.framework binding for the darwin keychain backend.
|
Package secitem is the cgo-free Security.framework binding for the darwin keychain backend. |
|
winapi
Package winapi is a dependency-free Windows Credential Manager binding: inline advapi32 CredWriteW/CredReadW/CredDeleteW/CredFree calls through the standard library's syscall package, with no golang.org/x/sys and no third-party wrapper.
|
Package winapi is a dependency-free Windows Credential Manager binding: inline advapi32 CredWriteW/CredReadW/CredDeleteW/CredFree calls through the standard library's syscall package, with no golang.org/x/sys and no third-party wrapper. |