saltstore

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var (
	ErrSaltStorageUnavailable       = &SaltStoreError{CodeStr: "ERR_SALT_STORAGE_UNAVAILABLE"}
	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".

View Source
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.

View Source
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 ArmorBlob

func ArmorBlob(raw []byte) []byte

ArmorBlob wraps a raw binary blob in base64 between PEM-style markers.

func Code

func Code(err error) string

Code extracts the stable error string. Returns "" if err is nil or not (a wrap of) a *SaltStoreError.

func ExportBlob

func ExportBlob(entries []Entry, passphrase []byte) ([]byte, error)

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

func LineageIDFromManifest(spec *pod.Spec) ([16]byte, error)

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

func ValidateKDFParams(iterations, memoryKiB, parallelism uint32) error

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

func WriteLineageID(spec *pod.Spec, lid [16]byte)

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

func NewEncryptedFileBackendForTest(path string, passphrase []byte) Backend

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

type Entry struct {
	LineageID [16]byte
	Salt      [32]byte
}

Entry is one (lineage_id, salt) pair persisted in or restored from a backup blob.

func ImportBlob

func ImportBlob(blob, passphrase []byte) ([]Entry, error)

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

type KDFParams struct {
	Iterations  uint32
	MemoryKiB   uint32
	Parallelism uint32
}

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".

func (*MockBackend) Put

func (m *MockBackend) Put(lid [16]byte, salt [32]byte) error

Put stores salt for lid. Returns ErrSaltAlreadyExists if lid is set.

type PassphraseProvider

type PassphraseProvider interface {
	Get() ([]byte, error)
	CanProvideNow() bool
}

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

type PassphraseProviderFunc func() ([]byte, error)

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.

Jump to

Keyboard shortcuts

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