Documentation
¶
Overview ¶
internal/saltstore/backend.go — Backend interface + path policy.
Backend is the per-tier storage contract used by the saltstore resolver. StoragePaths overrides the default OS-derived storage roots, used by integration tests and custom installations.
internal/saltstore/blob.go — versioned Argon2id + AES-256-GCM export blob. Wire layout:
Magic "KRSALT01" 8 B Version u8 (0x01 for v1) 1 B KDF params iters||mem_kib||parallelism (3 × u32LE) 12 B KDF salt random per-blob 16 B Nonce random per-blob (AES-GCM IV) 12 B Ciphertext AES-256-GCM seal(plaintext, key, nonce, aad) var B Tag GCM-appended 16 B
aad = Magic (8B) || Version (1B) — exactly 9 bytes.
plaintext (CBOR-encoded):
{"created_at": <unix-sec u64>,
"entries": [{"lineage_id": <16B bstr>, "salt": <32B bstr>}, ...]}
Package saltstore stores and retrieves per-lineage 32-byte Type-D pod salts via a 3-tier OS-conditional backend chain (Keychain → EncryptedFile → PlainFile). Public surface:
- Backend, StoragePaths
- Resolve, ResolveOpts
- ExportBlob, ImportBlob, Entry
- MockBackend (testing)
Every rejection returns a *SaltStoreError whose Code is a stable identifier (e.g. ERR_SALT_NOT_FOUND). Callers branch on the code via errors.Is(err, ErrSaltNotFound).
internal/saltstore/encrypted_file.go — passphrase-encrypted single file holding all lineage salts for one local installation.
On-disk layout is the same blob format from blob.go (Magic, Version, KDF params, KDF salt, nonce, sealed-CBOR(entries)). Put / Get / Delete rewrite the entire file each time (the working set is small — one installation has O(10s) of lineages at most). This keeps the read path simple and tag-verifies the entire blob on every access.
internal/saltstore/errors.go — sentinels per PRD P1.7.1 error catalog.
All sentinels are *SaltStoreError; wrapped errors compare equal via errors.Is. Code(err) extracts the stable identifier string for golden-vector comparison and logging.
internal/saltstore/keychain.go — OS keyring backend.
service name = "konareef-pod-salt" account = lineage_id hex (32 chars, lowercase) secret value = salt bytes hex (64 chars, lowercase)
Unit tests use MockBackend; this backend's real OS-API coverage is the integration_test.go suite (build tag "integration") which runs on the darwin CI runner against macOS Keychain and on the linux CI runner against gnome-keyring-daemon-provided Secret Service.
internal/saltstore/manifest.go — bridge between the `pod` package's in-memory Spec and the saltstore-typed [16]byte lineage_id.
internal/saltstore/mock.go — in-memory MockBackend for unit tests.
MockBackend is goroutine-safe and exposes AvailableErr so tests can drive the resolver's fallthrough decision table without spinning up real OS backends.
internal/saltstore/plain_file.go — last-resort plain-file backend.
All hardened controls for the plain-file salt backend apply:
- Mode 0600 file; O_CREAT|O_EXCL|O_NOFOLLOW on open
- Path: ${KONAREEF_STATE_DIR:-~/.konareef}/typed/{lineage_id_hex}/salt.bin
- Refuse tmpfs / network share mounts at Available() (stub today)
- CACHEDIR.TAG at typed/ root for backup exclusion
- Never log the salt bytes (the leak-grep test guarantees this)
internal/saltstore/resolver.go — backend selection per PRD §"Resolver". Tries backends in preference order (Keychain → EncryptedFile → PlainFile), logging each skip and returning the first whose Available() is nil.
Index ¶
- Variables
- func ArmorBlob(raw []byte) []byte
- func Code(err error) string
- func ExportBlob(entries []Entry, passphrase []byte) ([]byte, error)
- func ExportBlobWithParams(entries []Entry, passphrase []byte, params KDFParams, createdAt uint64) ([]byte, error)
- func LineageIDFromManifest(spec *pod.Spec) ([16]byte, error)
- func SealBlobDeterministic(entries []Entry, passphrase []byte, params KDFParams, createdAt uint64, ...) ([]byte, error)
- func ValidateKDFParams(iterations, memoryKiB, parallelism uint32) error
- func WriteLineageID(spec *pod.Spec, lid [16]byte)
- type Backend
- func NewEncryptedFileBackend(paths *StoragePaths) Backend
- func NewEncryptedFileBackendForTest(path string, passphrase []byte) Backend
- func NewEncryptedFileBackendWithProvider(path string, provider PassphraseProvider) Backend
- func NewKeychainBackend() Backend
- func NewPlainFileBackend(paths *StoragePaths) Backend
- func Resolve(opts ResolveOpts) (Backend, error)
- type Entry
- type KDFParams
- type MockBackend
- type PassphraseProvider
- type PassphraseProviderFunc
- type ResolveOpts
- type SaltStoreError
- type StoragePaths
Constants ¶
This section is empty.
Variables ¶
var ( ErrSaltNotFound = &SaltStoreError{CodeStr: "ERR_SALT_NOT_FOUND"} ErrSaltAlreadyExists = &SaltStoreError{CodeStr: "ERR_SALT_ALREADY_EXISTS"} ErrSaltBlobDecrypt = &SaltStoreError{CodeStr: "ERR_SALT_BLOB_DECRYPT"} ErrSaltBlobMalformed = &SaltStoreError{CodeStr: "ERR_SALT_BLOB_MALFORMED"} ErrSaltBlobUnsupportedVersion = &SaltStoreError{CodeStr: "ERR_SALT_BLOB_UNSUPPORTED_VERSION"} ErrSaltBlobKDFParamsInvalid = &SaltStoreError{CodeStr: "ERR_SALT_BLOB_KDF_PARAMS_INVALID"} ErrSaltBackendIO = &SaltStoreError{CodeStr: "ERR_SALT_BACKEND_IO"} ErrSaltLineageIDInvalid = &SaltStoreError{CodeStr: "ERR_SALT_LINEAGE_ID_INVALID"} ErrSaltManifestMissingLineageID = &SaltStoreError{CodeStr: "ERR_SALT_MANIFEST_MISSING_LINEAGE_ID"} )
Sentinel errors for the saltstore package. See PRD §"Error catalog".
var ProdKDFParams = KDFParams{Iterations: 4, MemoryKiB: 131072, Parallelism: 1}
ProdKDFParams is the production Argon2id parameter set; recorded in the blob header so a future parameter bump is transparent to existing blobs.
var TestKDFParams = KDFParams{Iterations: 2, MemoryKiB: 16384, Parallelism: 1}
TestKDFParams is the test/CI parameter set; documented in the conformance vectors. Production code MUST NOT use these.
Functions ¶
func Code ¶
Code extracts the stable error string. Returns "" if err is nil or not (a wrap of) a *SaltStoreError.
func ExportBlob ¶
ExportBlob is the production helper: uses ProdKDFParams and the current wall-clock for created_at. Returns the raw binary blob.
func ExportBlobWithParams ¶
func ExportBlobWithParams(entries []Entry, passphrase []byte, params KDFParams, createdAt uint64) ([]byte, error)
ExportBlobWithParams is the testable core: callers fix KDFParams and created_at for byte-exact conformance vectors. The KDF salt and nonce remain CSPRNG (do not weaken production callers).
func LineageIDFromManifest ¶
LineageIDFromManifest decodes spec.Pod.LineageID into a [16]byte. Returns ErrSaltManifestMissingLineageID when the field is empty (Type-D session-start fail-closed gate) and ErrSaltLineageIDInvalid when the hex string is malformed or the wrong length.
func SealBlobDeterministic ¶
func SealBlobDeterministic(entries []Entry, passphrase []byte, params KDFParams, createdAt uint64, kdfSalt, nonce []byte) ([]byte, error)
SealBlobDeterministic is exported solely for the conformance test; callers in production MUST go through ExportBlob / ExportBlobWithParams.
func ValidateKDFParams ¶
ValidateKDFParams rejects Argon2id parameter sets outside the sane window enforced by the import path. Returns ErrSaltBlobKDFParamsInvalid (wrapped with a human-readable detail) for any out-of-range value; returns nil when all three parameters are inside the bounds documented above.
Callers MUST invoke this before passing attacker-controlled header values to argon2.IDKey — see ImportBlob.
func WriteLineageID ¶
WriteLineageID sets the 32-char lowercase-hex encoding into the spec. Safe to call repeatedly; the field is overwritten.
Types ¶
type Backend ¶
type Backend interface {
// Name returns a stable lowercase identifier for logging and
// audit. One of: "keychain", "encrypted-file", "plain-file",
// "mock".
Name() string
// Available returns nil if this backend is usable on the current
// machine and environment. It MUST be cheap (no disk I/O, no IPC).
Available() error
// Put stores salt for lineageID. Returns ErrSaltAlreadyExists if
// an entry exists; callers must pass --force via Delete+Put.
Put(lineageID [16]byte, salt [32]byte) error
// Get retrieves the salt for lineageID. Returns ErrSaltNotFound
// if no entry exists.
Get(lineageID [16]byte) ([32]byte, error)
// Delete removes the salt for lineageID. Irreversible.
Delete(lineageID [16]byte) error
// List returns all lineage IDs present in this backend. Used by
// `konareef salt status`. Order is implementation-defined.
List() ([][16]byte, error)
}
Backend stores and retrieves per-lineage 32-byte salts. Each implementation handles its own at-rest encryption; the resolver does no crypto.
func NewEncryptedFileBackend ¶
func NewEncryptedFileBackend(paths *StoragePaths) Backend
NewEncryptedFileBackend returns the production encrypted-file backend. The passphrase is expected to be cached via an out-of-band TTY prompt before any Put/Get call.
func NewEncryptedFileBackendForTest ¶
NewEncryptedFileBackendForTest builds a backend with the passphrase pre-supplied. Tests use this constructor; production callers must route via NewEncryptedFileBackend (with a PassphraseProvider) so the TTY prompt is exercised.
func NewEncryptedFileBackendWithProvider ¶
func NewEncryptedFileBackendWithProvider(path string, provider PassphraseProvider) Backend
NewEncryptedFileBackendWithProvider builds a backend whose passphrase is acquired lazily via the supplied PassphraseProvider on the first Put/Get/Delete/List call. Available() consults provider.CanProvideNow() so the resolver can skip this tier when the provider cannot currently produce a passphrase (e.g. headless host with no KONAREEF_SALT_PASSPHRASE). When CanProvideNow reports true, the actual Get call happens only on first access, so callers are never blocked merely by Resolve() inspecting backend availability.
func NewKeychainBackend ¶
func NewKeychainBackend() Backend
NewKeychainBackend returns the OS keyring backend (macOS Keychain on darwin, Secret Service / libsecret on linux). See keychain.go.
func NewPlainFileBackend ¶
func NewPlainFileBackend(paths *StoragePaths) Backend
NewPlainFileBackend returns the plain-file backend rooted at paths.PlainFileRoot (default: ${KONAREEF_STATE_DIR:-~/.konareef}).
func Resolve ¶
func Resolve(opts ResolveOpts) (Backend, error)
Resolve tries backends in preference order: KeychainBackend → EncryptedFileBackend → PlainFileBackend. It skips each backend whose Available() returns a non-nil error, logging the reason via log.Printf. It returns the first available backend.
SECURITY: the plain-file tier is included ONLY when opts.PlainFileOptIn is true. Hosts without a usable keychain and without an encrypted-file passphrase source fail closed with ErrSaltStorageUnavailable rather than silently writing salts in cleartext. Callers (today, the konareef CLI) opt in explicitly via KONAREEF_ALLOW_PLAINTEXT_SALT=1 after a deliberate user decision.
Returns ErrSaltStorageUnavailable if no backend is usable.
type Entry ¶
Entry is one (lineage_id, salt) pair persisted in or restored from a backup blob.
func ImportBlob ¶
ImportBlob decodes a binary OR base64-armored blob and decrypts it using passphrase. Returns the entries in the order they appeared in the plaintext CBOR array.
Binary blobs are consumed byte-exact: the wire format's final 16 bytes are the AES-GCM authentication tag, which is uniformly random and may legitimately end in an ASCII whitespace byte (~2.3% of blobs). Whitespace tolerance applies ONLY to detecting/decoding the base64-armored form (leading/trailing whitespace around the PEM-style markers, e.g. from copy-paste) — never to the raw binary bytes themselves, since trimming those would corrupt the GCM tag and make an otherwise-valid blob fail authentication. If your transport may introduce incidental trailing whitespace on binary output (shell redirection, editors), use the armored form instead — byte-exact binary blobs no longer tolerate it.
type KDFParams ¶
KDFParams names the Argon2id parameters baked into the blob header. The production defaults are exported as ProdKDFParams; tests use TestKDFParams.
type MockBackend ¶
type MockBackend struct {
// AvailableErr lets tests simulate an unavailable backend.
// nil → Available() returns nil.
// !nil → Available() returns AvailableErr.
AvailableErr error
// contains filtered or unexported fields
}
MockBackend is an in-memory Backend implementation used by unit tests and the resolver decision-table fixture. Not safe for use in production. The zero value is NOT useful; callers must construct via NewMockBackend.
func NewMockBackend ¶
func NewMockBackend() *MockBackend
NewMockBackend constructs a ready-to-use mock.
func (*MockBackend) Available ¶
func (m *MockBackend) Available() error
Available returns the configured AvailableErr (nil by default).
func (*MockBackend) Delete ¶
func (m *MockBackend) Delete(lid [16]byte) error
Delete removes lid. Returns ErrSaltNotFound if lid is absent.
func (*MockBackend) Get ¶
func (m *MockBackend) Get(lid [16]byte) ([32]byte, error)
Get returns the salt for lid or ErrSaltNotFound.
func (*MockBackend) List ¶
func (m *MockBackend) List() ([][16]byte, error)
List returns every lid present (unspecified order).
func (*MockBackend) Name ¶
func (m *MockBackend) Name() string
Name returns the stable identifier "mock".
type PassphraseProvider ¶
PassphraseProvider returns the passphrase that protects the encrypted-file backend. Implementations typically prompt on the TTY or read KONAREEF_SALT_PASSPHRASE. Get is invoked at most once per backend instance (the result is cached for the lifetime of the backend).
CanProvideNow reports whether a subsequent Get call could plausibly succeed in the current process environment without further user intervention. It MUST be a cheap, side-effect-free probe: the resolver consults it during backend selection so it can skip the encrypted-file tier when, for example, the only configured source is a TTY prompt but stdin is not a terminal AND no passphrase env var is set. This lets the plain-file opt-in fallback actually become reachable in headless / non-interactive contexts instead of dying inside the encrypted-file backend on first use.
type PassphraseProviderFunc ¶
PassphraseProviderFunc adapts a passphrase-returning function into a PassphraseProvider. The adapter unconditionally reports CanProvideNow() == true, so it is suitable only when the caller is certain the function can produce a passphrase (typically tests with a pre-baked value). Production callers MUST implement PassphraseProvider directly so CanProvideNow honestly reports availability.
func (PassphraseProviderFunc) CanProvideNow ¶
func (PassphraseProviderFunc) CanProvideNow() bool
CanProvideNow implements PassphraseProvider; the function adapter always reports availability.
func (PassphraseProviderFunc) Get ¶
func (f PassphraseProviderFunc) Get() ([]byte, error)
Get implements PassphraseProvider by delegating to the underlying function.
type ResolveOpts ¶
type ResolveOpts struct {
// Paths overrides default OS-determined storage directories.
// Nil uses the platform defaults.
Paths *StoragePaths
// EncryptedFilePassphrase, when non-nil, is injected directly
// into the encrypted-file backend. Tests and automation paths
// that already hold the passphrase use this field.
EncryptedFilePassphrase []byte
// EncryptedFilePassphraseProvider, when non-nil and
// EncryptedFilePassphrase is unset, is invoked lazily on first
// salt access. Production callers wire the TTY prompt /
// KONAREEF_SALT_PASSPHRASE reader here. Required for the
// encrypted-file tier to be selectable in production.
EncryptedFilePassphraseProvider PassphraseProvider
// PlainFileOptIn must be set true for the resolver to consider
// the plain-file backend. The CLI sets it from
// KONAREEF_ALLOW_PLAINTEXT_SALT=1 — without explicit opt-in we
// refuse to silently write salts to disk in cleartext.
PlainFileOptIn bool
// DisableKeychain, when true, drops the keychain backend from the
// candidate list. Hermetic CLI test subprocesses set this so the
// resolver never reaches macOS Keychain (which can pop a UI prompt
// and block the test). The konareef CLI wires it from
// KONAREEF_DISABLE_KEYCHAIN_BACKEND=1. Off by default so production
// keeps the keychain as the preferred tier.
DisableKeychain bool
}
ResolveOpts configures path overrides for non-default installation layouts (integration tests, custom KONAREEF_STATE_DIR) and the passphrase source for the encrypted-file tier.
type SaltStoreError ¶
type SaltStoreError struct {
CodeStr string
}
SaltStoreError is the concrete sentinel type. Every exported Err* variable is a *SaltStoreError so a wrap via fmt.Errorf("%w", e) compares equal under errors.Is via the default pointer-equality unwrap path.
func (*SaltStoreError) Code ¶
func (e *SaltStoreError) Code() string
Code returns the stable error code string.
func (*SaltStoreError) Error ¶
func (e *SaltStoreError) Error() string
Error returns the stable error code string.
type StoragePaths ¶
type StoragePaths struct {
// EncryptedFilePath is the absolute path to the
// EncryptedFileBackend's salts.enc file. Default:
// darwin: ~/Library/Application Support/konareef/salts.enc
// linux : ~/.config/konareef/salts.enc
// $KONAREEF_STATE_DIR overrides when set.
EncryptedFilePath string
// PlainFileRoot is the absolute path to the PlainFileBackend
// root directory ($ROOT/typed/{lineage_id_hex}/salt.bin lives
// underneath). Default: ${KONAREEF_STATE_DIR:-~/.konareef}.
PlainFileRoot string
}
StoragePaths overrides default OS-determined storage roots. Used by integration tests and custom installation layouts. Nil values fall back to the per-platform defaults.