credstore

package
v0.4.2 Latest Latest
Warning

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

Go to latest
Published: Jun 29, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

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

View Source
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")
)
View Source
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

func DefaultPath() (string, error)

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):

  1. DA_CREDENTIAL_<id> env (CI default: in-memory, no disk, no keychain)
  2. DA_CREDENTIALS_FILE (ephemeral plaintext a CI job writes then removes)
  3. the encrypted store (keychain-unwrapped, local dev)
  4. 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.

func (*Loader) Resolve

func (l *Loader) Resolve(id string) (string, error)

Resolve returns the credential for id, walking the resolution chain in order and returning the first non-empty hit. ErrCredentialNotFound is returned only when no step yields a value.

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

func Open(path string, ring Keyring) (*Store, error)

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.

func (*Store) Delete

func (s *Store) Delete(id string)

Delete removes the credential under id in memory; call Save to persist.

func (*Store) Get

func (s *Store) Get(id string) (string, error)

Get returns the credential stored under id, or ErrCredentialNotFound.

func (*Store) Save

func (s *Store) Save() error

Save re-seals the credential map under the hybrid KEM and writes it atomically (temp file + rename) with 0600 perms because it holds secrets.

func (*Store) Set

func (s *Store) Set(id, secret string)

Set records secret under id in memory; call Save to persist.

Jump to

Keyboard shortcuts

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