Documentation
¶
Overview ¶
Package credstore implements the shared encrypted credential store and the CI-aware loader described in external-agent-sources design §4.1.
The local store keeps secrets in an encrypted file at ~/.config/da/credentials.json. On first use a hybrid post-quantum recipient keypair (X25519 + ML-KEM-768) is generated; its private seed material lives in the OS keychain via a credential helper (macOS Keychain / Windows Credential Manager / Linux Secret Service) behind the Keyring seam. The file is sealed with AES-256-GCM under a key derived from a hybrid KEM so the data stays confidential if EITHER the classical (X25519) or the post-quantum (ML-KEM) primitive remains unbroken. Call sites address credentials by id and never see the raw key; tests inject a fake Keyring and never touch the real store.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNotImplemented is returned by the stub OIDC resolver until the // workload-identity resolver lands. It is also the marker the loader uses to // fall through a resolver that has nothing for a given id. ErrNotImplemented = errors.New(errPrefix + ": resolver not implemented") // ErrInsecurePlaintextFile is returned when DA_CREDENTIALS_FILE points at a // file that is group- or world-accessible — a plaintext secret must not be // trusted when other local users can read it. ErrInsecurePlaintextFile = errors.New(errPrefix + ": plaintext credentials file is group/world-accessible") )
var ( // ErrCredentialNotFound is returned when no credential matches an id. ErrCredentialNotFound = errors.New(errPrefix + ": credential not found") // ErrBadSeedLength is returned when the keyring yields a seed blob of the // wrong size (corrupt or foreign keychain entry). ErrBadSeedLength = errors.New(errPrefix + ": key seed has wrong length") // ErrBadFormatVersion is returned when the on-disk envelope is not a format // version this build can decrypt. ErrBadFormatVersion = errors.New(errPrefix + ": unsupported envelope format version") // ErrBadNonceLength is returned when the envelope's GCM nonce is not the // AEAD nonce size, i.e. the file is truncated or not a credstore envelope. ErrBadNonceLength = errors.New(errPrefix + ": gcm nonce has wrong length") // ErrKeyNotFound is the sentinel a Keyring returns (directly or wrapped) // when a key is absent, so the store mints a fresh one rather than fail. ErrKeyNotFound = errors.New(errPrefix + ": keyring key not found") // ErrKeyMissingExistingStore is returned when the OS keychain entry is absent // but the encrypted credential file already exists on disk. Minting a new key // would permanently strand the existing ciphertext. Recovery requires // restoring the keychain entry or re-sealing with a new key after a deliberate // export of the existing secrets. ErrKeyMissingExistingStore = errors.New(errPrefix + ": keyring key missing but encrypted store file exists") )
Functions ¶
func DefaultPath ¶
DefaultPath returns ~/.config/da/credentials.json, honoring XDG_CONFIG_HOME first so the store lands in the same local-secrets home as review auth state (never in the git-synced AGENTS_HOME tree).
Types ¶
type Keyring ¶
type Keyring interface {
// Get returns the secret stored under key. It returns ErrKeyNotFound (or a
// wrapped error satisfying errors.Is) when the key is absent.
Get(key string) ([]byte, error)
// Set stores secret under key, overwriting any existing value.
Set(key string, secret []byte) error
}
Keyring is the seam over the OS credential helper. Production wires it to the platform keychain; tests inject a fake so they never touch the real store.
func NewOSKeyring ¶ added in v0.4.0
func NewOSKeyring() Keyring
NewOSKeyring returns nil on platforms without a supported OS keyring implementation. The Loader treats a nil keyring as "skip the encrypted-store step" and falls through to env-var and OIDC resolver steps, so CI and non-macOS dev environments continue to work unchanged.
type Loader ¶
type Loader struct {
// contains filtered or unexported fields
}
Loader resolves credentials by id using a first-hit-wins chain (design §4.1):
- DA_CREDENTIAL_<id> env (CI default: in-memory, no disk, no keychain)
- DA_CREDENTIALS_FILE (ephemeral plaintext a CI job writes then removes)
- the encrypted store (keychain-unwrapped, local dev)
- a pluggable OIDC/workload-identity resolver (stub for now)
An empty/blank credential value from any step is treated as a MISS so resolution falls through to the next source rather than injecting an empty secret. Raw secrets are never logged; only ids and the winning source appear in any error or diagnostic.
func NewLoader ¶
func NewLoader(opts ...LoaderOption) *Loader
NewLoader builds a Loader with the production collaborators (real env and filesystem) and the stub OIDC resolver, then applies opts. On platforms with a supported OS keyring (currently macOS), the encrypted-store step is enabled by default via NewOSKeyring(). CI callers typically rely on the env steps alone and are unaffected because NewOSKeyring() returns nil on non-Darwin.
type LoaderOption ¶
type LoaderOption func(*Loader)
LoaderOption customizes a Loader at construction.
func WithKeyring ¶
func WithKeyring(ring Keyring) LoaderOption
WithKeyring sets the Keyring used by the encrypted-store step.
func WithResolver ¶
func WithResolver(r OIDCResolver) LoaderOption
WithResolver sets the pluggable OIDC/workload-identity resolver.
func WithStorePath ¶
func WithStorePath(path string) LoaderOption
WithStorePath overrides the encrypted-store path (defaults to DefaultPath()).
type OIDCResolver ¶
type OIDCResolver interface {
// Resolve returns the credential for id, or an error. Returning
// ErrCredentialNotFound (or ErrNotImplemented) lets the loader report a
// clean miss rather than a hard failure.
Resolve(id string) (string, error)
}
OIDCResolver is the pluggable last-resort resolution step: mint a short-lived token from the runner's OIDC/workload-identity JWT, with no static secret. The stub returns ErrNotImplemented; a real resolver is wired later.
func StubOIDCResolver ¶
func StubOIDCResolver() OIDCResolver
StubOIDCResolver returns the not-yet-implemented resolver used until the workload-identity resolver is built.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is an opened, decrypted credential store. Mutations are persisted with Save, which re-seals the whole map under the hybrid KEM.
func Open ¶
Open reads and decrypts the store at path, minting the hybrid key via ring on first use. A missing file yields an empty store so first-run callers can Set without a separate init step.