keychain

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 12, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

README

keychain

CI Go Reference Go Report Card

Pure-Go (CGO_ENABLED=0) store for macOS Keychain generic-password items. It reaches the OS through ebitengine/puregodlsym'd CoreFoundation and Security C functions — so it links with no cgo and never shells out to /usr/bin/security (the secret never appears in any process's argv). It is a sibling of go-macos/notify and reuses go-macos/objc for the canonical framework paths.

API

Three byte-oriented calls over a (service, account) pair, each backed by one kSecClassGenericPassword item:

import "github.com/go-macos/keychain"

// Store (adds on first write, overwrites in place afterwards).
err := keychain.Set("my-app", "alice@example.com", []byte(secretJSON))

// Read (keychain.ErrNotFound when absent).
secret, err := keychain.Get("my-app", "alice@example.com")

// Remove (deleting an absent item is not an error).
err = keychain.Delete("my-app", "alice@example.com")

Errors are typed and comparable with errors.Is / errors.As:

Error Meaning
ErrNotFound Get found no item for the pair
ErrEmptySecret Set was given an empty secret (the Keychain cannot hold one)
ErrUnsupported called on a non-darwin platform
*Error any other Security-framework failure; carries Op and the raw Status (OSStatus)
Access control

By default an item is a plain generic password. To protect it with a SecAccessControl, pass WithAccessControl:

// Gate every read behind Touch ID or the device passcode.
err := keychain.Set("my-app", "alice", secret, keychain.WithAccessControl(keychain.UserPresence))

// At-rest protection (WhenUnlockedThisDeviceOnly) with no interactive prompt.
err := keychain.Set("my-app", "alice", secret, keychain.WithAccessControl(0))

Access-controlled items are pinned to kSecAttrAccessibleWhenUnlockedThisDeviceOnly (never synced to iCloud) and are replaced atomically (delete-then-add) on each write. Reading a UserPresence item triggers the system biometric prompt.

Why not zalando/go-keyring or os/exec?

go-keyring shells out to the security CLI on macOS, which puts the secret on a command line and adds a runtime dependency on an external binary. This package binds the Security framework in-process instead — nothing to install, nothing in argv, and it builds CGO_ENABLED=0.

Platforms

Darwin only. Every exported symbol is defined on all platforms so consumers cross-compile; on non-darwin GOOS the functions return ErrUnsupported.

Testing

The darwin lane runs a real, on-device store → get → update → delete round trip against the login Keychain (plain and access-controlled, no biometric prompt), plus an FFI-plumbing check that every symbol resolved. The OS-independent logic (empty-secret guard, not-found mapping, error wrapping, load-error propagation) is covered to 100% on every lane through injected backend seams. CGO_ENABLED=0 throughout.

CGO_ENABLED=0 go test ./...

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package keychain is a pure-Go (CGO_ENABLED=0) store for macOS Keychain generic-password items. It reaches the OS through github.com/ebitengine/purego — dlsym'd CoreFoundation and Security C functions — so it links with no cgo and never shells out to /usr/bin/security (the secret never appears in any process's argv).

The public surface is three byte-oriented calls over a (service, account) pair, each mapping to a kSecClassGenericPassword item:

err          := keychain.Set(service, account, secret)
secret, err  := keychain.Get(service, account)   // keychain.ErrNotFound if absent
err          := keychain.Delete(service, account)

Errors are typed: ErrNotFound for a miss, ErrEmptySecret for an empty write, ErrUnsupported off darwin, and *Error (carrying the raw OSStatus) for any other Security-framework failure.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNotFound is returned by [Get] when no item exists for the
	// (service, account) pair.
	ErrNotFound = errors.New("keychain: item not found")
	// ErrEmptySecret is returned by [Set] when secret is empty; the Keychain
	// cannot hold a zero-length generic password and [Get] could not tell it
	// apart from a miss.
	ErrEmptySecret = errors.New("keychain: empty secret")
	// ErrUnsupported is returned by every entry point on non-darwin platforms
	// (the Keychain is macOS-only). Exported symbols exist everywhere so
	// consumers cross-compile.
	ErrUnsupported = errors.New("keychain: unsupported on this platform (darwin only)")
)

Sentinel errors. They are stable and may be compared with errors.Is.

Functions

func Delete

func Delete(service, account string) error

Delete removes the item identified by (service, account). Deleting an absent item is not an error. It returns ErrUnsupported off darwin.

func Get

func Get(service, account string) ([]byte, error)

Get returns the secret stored under (service, account). It returns ErrNotFound when no such item exists and ErrUnsupported off darwin. Reading an item created with WithAccessControl and UserPresence triggers the system biometric prompt.

func Set

func Set(service, account string, secret []byte, opts ...Option) error

Set stores secret under the generic-password item identified by (service, account), replacing any existing value. An empty secret is rejected with ErrEmptySecret. On a non-darwin platform it returns ErrUnsupported.

Types

type AccessControlFlags

type AccessControlFlags uint

AccessControlFlags selects the SecAccessControlCreateFlags constraints an item created with WithAccessControl must satisfy on read. The zero value applies no interactive constraint (the item is readable whenever the accessibility class is satisfied, with no Touch ID / passcode prompt).

const (
	// UserPresence (kSecAccessControlUserPresence) gates every read behind
	// Touch ID or the device passcode. Reading such an item triggers the
	// system biometric prompt.
	UserPresence AccessControlFlags = 1 << 0
	// BiometryAny (kSecAccessControlBiometryAny) requires any currently
	// enrolled biometric.
	BiometryAny AccessControlFlags = 1 << 1
	// BiometryCurrentSet (kSecAccessControlBiometryCurrentSet) invalidates the
	// item if the enrolled biometric set changes.
	BiometryCurrentSet AccessControlFlags = 1 << 3
	// DevicePasscode (kSecAccessControlDevicePasscode) gates reads behind the
	// device passcode.
	DevicePasscode AccessControlFlags = 1 << 4
)

type Error

type Error struct {
	// Op is the failing operation: "set", "get" or "delete".
	Op string
	// Status is the raw OSStatus returned by the Security framework.
	Status int32
}

Error wraps a non-success, non-not-found OSStatus from the Security framework, tagged with the operation that produced it.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

type Option

type Option func(*config)

Option customises a Set. Options are applied left to right.

func WithAccessControl

func WithAccessControl(flags AccessControlFlags) Option

WithAccessControl stores the item behind a SecAccessControl protecting it with the given flags and the accessibility class kSecAttrAccessibleWhenUnlockedThisDeviceOnly (so it never leaves this device and is never synced to iCloud). Pass UserPresence to gate reads behind Touch ID or the passcode; pass 0 for an at-rest-protected item with no interactive prompt. Without this option an item is a plain generic password created (or updated in place) with the default accessibility.

Jump to

Keyboard shortcuts

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