keeper

package module
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Apr 7, 2026 License: MIT Imports: 32 Imported by: 0

README

keeper

Keeper is a cryptographic secret store for Go. It encrypts arbitrary byte payloads at rest using Argon2id key derivation and XChaCha20-Poly1305 (default) authenticated encryption, and stores them in an embedded bbolt database.

It ships as three things you can use independently:

  • A Go library — embed a hardened secret store directly in your process, with four security levels, per-bucket DEK isolation, and a tamper-evident audit chain.
  • An HTTP handler (x/keephandler) — mount keeper endpoints on any net/http mux in one call, with pluggable hooks, guards, and response encoders for access control and audit logging.
  • A CLI (cmd/keeper) — a terminal interface with a persistent REPL session, no-echo secret entry, and zero shell-history exposure.

Keeper was designed as the foundational secret management layer for the Agbero load balancer but has no dependency on Agbero and works in any Go project.


Contents


Security model

Keeper partitions secrets into buckets. Every bucket has an immutable BucketSecurityPolicy that governs how its Data Encryption Key (DEK) is protected. Four levels are available.

The URI scheme (vault://, certs://, space://, or any name you register) is independent of the security level. A scheme is just a namespace prefix that groups related buckets. The security level is a property of the BucketSecurityPolicy set at CreateBucket time and cannot be changed afterwards. You can mix security levels freely within the same scheme.

LevelPasswordOnly

The bucket DEK is derived from the master key using HKDF-SHA256 with a domain-separated info string per bucket (keeper-bucket-dek-v1:scheme:namespace). All LevelPasswordOnly buckets are unlocked automatically when UnlockDatabase is called with the correct master passphrase. No per-bucket credential is required at runtime. This level is appropriate for secrets the process needs at startup without human interaction.

LevelAdminWrapped

The bucket has a randomly generated 32-byte DEK unique to that bucket. The DEK is never stored in plaintext. For each authorised admin a Key Encryption Key (KEK) is derived from HKDF(masterKey‖adminCred, dekSalt) and used to wrap the DEK via XChaCha20-Poly1305. The bucket is inaccessible until an admin calls UnlockBucket with their credential. The master passphrase alone cannot decrypt the bucket. Revoking one admin does not affect any other admin's wrapped copy.

LevelHSM

The bucket DEK is generated at CreateBucket time and immediately wrapped by a caller-supplied HSMProvider. The provider performs the wrap and unwrap operations — keeper never handles the raw DEK after handing it to the provider. UnlockDatabase automatically calls the provider to unwrap and seed the Envelope for all registered HSM buckets. Master key rotation does not re-encrypt these buckets; the DEK is provider-controlled.

A built-in SoftHSM implementation backed by a memguard-protected wrapping key is available in pkg/hsm for testing and CI environments. Do not use it in production.

LevelRemote

Identical to LevelHSM in key management behaviour, but the HSMProvider is implemented by pkg/remote.Provider — a configurable HTTPS adapter that delegates wrap and unwrap to any remote KMS service over TLS. Pre-built configurations for HashiCorp Vault Transit, AWS KMS, and GCP Cloud KMS are provided in pkg/remote. For production use, configure TLSClientCert and TLSClientKey to enable mutual TLS authentication.


Cryptographic design

Master key derivation
salt ← random 32 bytes, generated once, stored as a versioned SaltStore (unencrypted)
masterKey ← Argon2id(passphrase, salt, t=3, m=64 MiB, p=4) → 32 bytes

A verification hash is stored on first derivation:

verifyHash ← Argon2id(masterKey, "verification", t=1, m=64 MiB, p=4) → 32 bytes

Subsequent DeriveMaster calls recompute this hash and compare it with crypto/subtle.ConstantTimeCompare. A mismatch returns ErrInvalidPassphrase.

The KDF salt is stored unencrypted by design. It must be readable before UnlockDatabase to derive the master key — encrypting it with a key derived from the master would be circular. A KDF salt is not a secret; its purpose is uniqueness, not confidentiality.

Secret encryption

Each plaintext value is encrypted with XChaCha20-Poly1305 using the bucket DEK:

nonce ← random 24 bytes
ciphertext ← XChaCha20-Poly1305.Seal(nonce, DEK, plaintext)

The stored record is a msgpack-encoded Secret struct containing the ciphertext, encrypted metadata, and schema version. Authentication is implicit: a ciphertext decrypted with the wrong key produces an AEAD authentication failure before any plaintext is returned.

KEK derivation — LevelAdminWrapped
salt ← random 32 bytes, generated at bucket creation, stored in policy
ikm ← masterKey ‖ adminCredential
KEK ← HKDF-SHA256(ikm, salt, info="keeper-kek-v1") → 32 bytes
wrappedDEK ← XChaCha20-Poly1305.Seal(nonce, KEK, DEK)

The KEK is derived using HKDF rather than a second Argon2 pass. The master key was already produced by a high-cost KDF; a second Argon2 invocation would add hundreds of milliseconds of latency to every UnlockBucket call with no security benefit. HKDF-SHA256 operates in approximately one microsecond.

The neither-alone property holds: an attacker who compromises only the database obtains the wrapped DEK and the HKDF salt but cannot derive the KEK without the master key. An attacker who compromises only the master key cannot unwrap any LevelAdminWrapped DEK without also knowing the admin credential.

Metadata encryption — secrets

Secret metadata (creation time, update time, access count, version) is encrypted separately from the ciphertext:

metaKey ← HKDF-SHA256(bucketDEK, nil, info="keeper-metadata-v1") → 32 bytes
encryptedMeta ← XChaCha20-Poly1305.Seal(nonce, metaKey, msgpack(metadata))

For LevelAdminWrapped, LevelHSM, and LevelRemote buckets this means metadata is inaccessible without the bucket credential, preventing an attacker with read access to the database file from learning access patterns or timestamps.

Metadata encryption — policies, WAL, and audit

All structural metadata is also encrypted at rest. Two keys are derived from the master key at UnlockDatabase time:

policyEncKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-enc-v1") → 32 bytes
auditEncKey  ← HKDF-SHA256(masterKey, nil, info="keeper-audit-enc-v1")  → 32 bytes

policyEncKey encrypts: BucketSecurityPolicy values and the rotation WAL.

auditEncKey encrypts: the Scheme, Namespace, and Details fields of every audit event.

Both keys are cleared from memory at Lock(). The cipher used for metadata encryption is the same configurable crypt.Cipher interface used for secrets — the user's cipher choice (AES-256-GCM for FIPS, XChaCha20-Poly1305 by default) flows through automatically.

Wire format for all encrypted metadata blobs:

nonce (cipher.NonceSize() bytes) || AEAD-ciphertext
Policy bucket key hashing

On-disk policy keys are opaque hashes rather than plaintext scheme:namespace strings, preventing offline enumeration of bucket names:

base ← hex(SHA-256("scheme:namespace"))[:32]   // 32 hex chars = 128-bit key space
_policies/<base>          → encrypted BucketSecurityPolicy
_policies/<base>__hash__  → SHA-256(encrypted policy bytes)
_policies/<base>__hmac__  → HMAC-SHA256(policyKey, encrypted policy bytes)

The in-memory schemeRegistry continues to use "scheme:namespace" as its key — only the on-disk representation changes.

Policy authentication

Each policy record carries two integrity tags written atomically in one bbolt transaction:

hash ← SHA-256(encryptedPolicyBytes)                         — unauthenticated, pre-unlock integrity
policyKey ← HKDF-SHA256(masterKey, nil, info="keeper-policy-hmac-v1") → 32 bytes
hmac ← HMAC-SHA256(policyKey, encryptedPolicyBytes)          — authenticated, post-unlock integrity

Before UnlockDatabase, only the SHA-256 hash is available. After unlock, loadPolicy verifies the HMAC tag. UnlockDatabase calls upgradePolicyHMACs to backfill HMAC tags on policies created before this feature existed.

Audit HMAC signing
auditKey ← HKDF-SHA256(masterKey, nil, info="keeper-audit-hmac-v1") → 32 bytes
HMAC ← HMAC-SHA256(auditKey, event fields including Seq)

The signing key is activated at UnlockDatabase and cleared at Lock. When the master key is rotated, Rotate appends a key-rotation checkpoint event to every active audit chain, signed with the old audit key as the final event of the old epoch. History is never rewritten; the checkpoint is the trust bridge between epochs.


Key hierarchy

passphrase
    │
    └─ Argon2id(salt) ──→ masterKey (32 bytes, memguard Enclave)
                              │
                              ├─ HKDF("keeper-audit-hmac-v1")  ──→ auditKey    (HMAC signing)
                              ├─ HKDF("keeper-audit-enc-v1")   ──→ auditEncKey (audit field encryption)
                              ├─ HKDF("keeper-policy-hmac-v1") ──→ policyKey   (policy HMAC)
                              ├─ HKDF("keeper-policy-enc-v1")  ──→ policyEncKey (policy/WAL encryption)
                              │
                              ├─ [LevelPasswordOnly]
                              │       └─ HKDF("keeper-bucket-dek-v1:scheme:ns") ──→ DEK
                              │               └─ HKDF("keeper-metadata-v1") ──→ metaKey
                              │
                              ├─ [LevelAdminWrapped]
                              │       ├─ random 32 bytes ──→ DEK
                              │       │       └─ HKDF("keeper-metadata-v1") ──→ metaKey
                              │       │
                              │       └─ HKDF("keeper-kek-v1", masterKey‖adminCred, dekSalt)
                              │                 └─ KEK
                              │                       └─ XChaCha20-Poly1305(KEK, DEK) ──→ wrappedDEK
                              │
                              └─ [LevelHSM / LevelRemote]
                                      ├─ random 32 bytes ──→ DEK
                                      │       └─ HKDF("keeper-metadata-v1") ──→ metaKey
                                      │
                                      └─ HSMProvider.WrapDEK(DEK) ──→ wrappedDEK
                                         (stored; provider controls the wrapping key)

All intermediate keys are zeroed immediately after use. The master key is never written to disk in any form.


Storage schema

The underlying database is bbolt. All buckets and their contents:

bbolt bucket Key Value
__meta__ salt msgpack — SaltStore (unencrypted; circular dependency if encrypted)
__meta__ verify raw bytes — Argon2id verification hash
__meta__ rotation_wal nonce‖AEAD(msgpack(RotationWAL))
__meta__ bucket_dek_done "1" — DEK migration completion marker
__policies__ hex(SHA-256(scheme:ns))[:32] nonce‖AEAD(msgpack(BucketSecurityPolicy))
__policies__ <base>__hash__ hex SHA-256 of encrypted policy bytes
__policies__ <base>__hmac__ hex HMAC-SHA256(policyKey, encrypted policy bytes)
__audit__/scheme/namespace event UUID JSON — audit Event
__audit__/scheme/namespace __chain_index__ JSON — chainIndex
scheme/namespace key string msgpack — Secret struct
Secret struct (msgpack)
type Secret struct {
    Ciphertext    []byte `msgpack:"ct"`
    EncryptedMeta []byte `msgpack:"em,omitempty"`
    SchemaVersion int    `msgpack:"sv"`  // always 1
}
Audit Event fields

The Event struct uses separate plaintext routing fields (Scheme, Namespace) alongside encrypted payload fields (EncScheme, EncNamespace, EncDetails). Checksums are computed over the plaintext routing fields and the encrypted EncDetails bytes, so chain integrity can be verified at three tiers without any key:

Tier Has Can verify
Public Nothing SHA-256 checksum chain (detects tampering and insertion)
Audit-key holder auditEncKey Full chain + decrypt Scheme/Namespace/Details
Operator Master passphrase Everything
Versioned salt store

The KDF salt is stored as a msgpack-encoded SaltStore under the salt metadata key. Each salt rotation appends a new SaltEntry and advances CurrentVersion. Old entries are retained as an audit trail. The SaltStore is stored unencrypted — see Security decisions.

Crash-safe rotation WAL

Rotate writes a WAL before touching any record. The WAL carries WrappedOldKey: the pre-rotation master key encrypted with the new master key. After a crash the old passphrase is gone; WrappedOldKey is the only correct way to carry the old key across the boundary. At UnlockDatabase, when a WAL is present, the new master key decrypts WrappedOldKey and rotation resumes from the WAL cursor. The WAL itself is encrypted with policyEncKey.


Audit chain

Every significant operation appends a tamper-evident event to the bucket's audit chain. Chain integrity depends on two mechanisms.

Checksum. SHA-256 over prevChecksum, ID, BucketID, Scheme, Namespace, EncDetails, EventType, and Timestamp. Using Scheme/Namespace as plaintext (always preserved alongside the encrypted forms) ensures the checksum is stable across load paths. EncDetails provides integrity over the encrypted payload.

HMAC. HMAC-SHA256 over all fields including Seq. An attacker who can write to the database but does not know the audit key cannot produce a valid HMAC. VerifyIntegrity checks both layers for every event.

Key rotation epoch boundary. At Rotate, a checkpoint event is appended to every active chain carrying fingerprints of both the outgoing and incoming audit keys. The checkpoint is signed with the outgoing key. Auditors holding any epoch key can recover subsequent epoch keys from the wrapped_new_key field and verify HMAC continuity across the full chain.

Automatic pruning. When AuditPruneInterval is set in Config, a jack.Scheduler runs periodically and calls PruneEvents on every registered bucket. LevelHSM and LevelRemote buckets are never pruned regardless of this setting.


Jack integration

Jack is an optional process supervision library. When a JackConfig is provided via WithJack, keeper activates background components automatically: auto-lock Looper, per-bucket DEK Reaper, health monitoring patients (bbolt read latency + encrypt/decrypt round-trip), audit prune scheduler, and async event Pool. Keeper never calls pool.Shutdown — the pool lifecycle belongs to the caller.


x/keepcmd

x/keepcmd provides reusable keeper operations decoupled from any CLI framework. Embed it in your own application to get typed, testable secret management without pulling in the CLI binary.

import "github.com/agberohq/keeper/x/keepcmd"

cmds := &keepcmd.Commands{
    Store: func() (*keeper.Keeper, error) {
        return security.KeeperOpen(cfg)  // your own config
    },
    Out:     keepcmd.PlainOutput{},
    NoClose: false, // true in REPL / session contexts
}

cmds.List()                                          // all keys: scheme://namespace/key
cmds.List("vault")                                   // all keys in scheme vault
cmds.List("vault", "system")                         // all keys in vault://system
cmds.Get("vault://system/jwt_secret")
cmds.Set("vault://system/jwt_secret", "newsecret", keepcmd.SetOptions{})
cmds.Rotate(newPassphraseBytes)    // caller resolved the passphrase — no prompter dependency
cmds.RotateSalt(currentPassBytes)  // same

keepcmd never calls prompter or reads from stdin. Passphrase resolution is entirely the caller's responsibility — this keeps the package safe in headless server contexts.

NoClose: true prevents Commands from calling store.Close() after each operation. Use this in REPL / session contexts where one store is shared across many calls.


x/keephandler

x/keephandler mounts keeper HTTP endpoints on any net/http mux. No external router dependency — it uses Go 1.22+ method+pattern routing with stdlib http.ServeMux.

import "github.com/agberohq/keeper/x/keephandler"

keephandler.Mount(mux, store,
    keephandler.WithPrefix("/api/keeper"),
    keephandler.WithGuard(func(w http.ResponseWriter, r *http.Request, route string) bool {
        if !acl.Allow(r.Header.Get("X-Principal"), route) {
            http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
            return false
        }
        return true
    }),
    keephandler.WithHooks(
        keephandler.Hook{
            Route:       keephandler.RouteGet,
            CaptureBody: false,
            After: func(r *http.Request, status int, _ []byte) {
                audit.Log(r.Context(), route, status)
            },
        },
    ),
    keephandler.WithEncoder(func(w http.ResponseWriter, route string, status int, data any) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(status)
        json.NewEncoder(w).Encode(map[string]any{
            "ok":    status < 400,
            "route": route,
            "data":  data,
        })
    }),
    keephandler.WithRoutes(func(m *http.ServeMux) {
        m.HandleFunc("POST /api/keeper/totp/{user}", myTOTPHandler)
    }),
)
Endpoints
Method Path Description
POST {prefix}/unlock Unlock the store with a passphrase
POST {prefix}/lock Lock the store
GET {prefix}/status Lock state — safe to poll without auth
GET {prefix}/keys List all secret keys
GET {prefix}/keys/{key} Retrieve a secret value
POST {prefix}/keys Store a secret (JSON or multipart)
DELETE {prefix}/keys/{key} Delete a secret
POST {prefix}/rotate Rotate the master passphrase
POST {prefix}/rotate/salt Rotate the KDF salt
GET {prefix}/backup Stream a database snapshot
Hook contract

BeforeFunc returns (allow bool, err error).

  • (true, nil) — let the request proceed.
  • (false, nil) — abort; the hook has already written a complete response.
  • (false, err) — abort; the framework writes a 500 using err.Error(). The hook must not have written anything to w.

Hook.CaptureBody bool controls whether AfterFunc receives the response body. false (default) costs one lightweight statusWriter wrapper; true buffers the full body into a bytes.Buffer for the AfterFunc — one allocation per request.


API reference

Construction and unlock
store, err := keeper.New(keeper.Config{
    DBPath:              "/var/lib/agbero/keeper.db",
    AutoLockInterval:    30 * time.Minute,
    EnableAudit:         true,
    AuditPruneInterval:  24 * time.Hour,
    AuditPruneKeepLastN: 10_000,
    AuditPruneOlderThan: 90 * 24 * time.Hour,
    DBLatencyThreshold:  200 * time.Millisecond,
    Logger:              logger,
}, keeper.WithJack(keeper.JackConfig{
    Pool:     jackPool,
    Shutdown: jackShutdown,
}))
defer store.Close()

// Shorthand (wraps DeriveMaster + UnlockDatabase):
if err := store.Unlock([]byte(os.Getenv("KEEPER_PASSPHRASE"))); err != nil {
    log.Fatal(err) // ErrInvalidPassphrase on wrong passphrase
}

UnlockDatabase performs the following in order:

  1. Derives and activates the audit HMAC signing key
  2. Derives and activates the policy HMAC key
  3. Derives and activates policyEncKey and auditEncKey
  4. Clears and reloads schemeRegistry (decrypts all policy blobs)
  5. Resumes any interrupted rotation WAL
  6. Upgrades policy HMAC tags
  7. Seeds all LevelPasswordOnly bucket DEKs into the Envelope
  8. Starts background tasks (migration looper, auto-lock, health patients)
LevelPasswordOnly bucket — full lifecycle
err := store.CreateBucket("vault", "system", keeper.LevelPasswordOnly, "init")

store.Set("vault://system/jwt_secret", []byte("supersecret"))
val, err := store.Get("vault://system/jwt_secret")

// Namespaced convenience wrappers
store.SetNamespaced("admin", "jwt_secret", secretBytes)
val, err = store.GetNamespaced("admin", "jwt_secret")
LevelAdminWrapped bucket — full lifecycle
err := store.CreateBucket("finance", "payroll", keeper.LevelAdminWrapped, "ops-team")
err = store.AddAdminToPolicy("finance", "payroll", "alice", []byte("alicepass"))

store.SetNamespacedFull("finance", "payroll", "salary_key", []byte("AES256..."))

store.LockBucket("finance", "payroll")
err = store.UnlockBucket("finance", "payroll", "bob", []byte("bobpass"))
// ErrAuthFailed — does not distinguish wrong password from unknown admin (CWE-204)

err = store.RevokeAdmin("finance", "payroll", "alice")
err = store.RotateAdminWrappedDEK("finance", "payroll", "bob", []byte("bobpass"))

needs, err := store.NeedsAdminRekey("finance", "payroll")
LevelHSM / LevelRemote buckets
import (
    "github.com/agberohq/keeper/pkg/hsm"
    "github.com/agberohq/keeper/pkg/remote"
)

// SoftHSM — testing only
provider, _ := hsm.NewSoftHSM()
store.RegisterHSMProvider("secure", "keys", provider)
store.CreateBucket("secure", "keys", keeper.LevelHSM, "ops")

// Vault Transit
cfg := remote.VaultTransit("https://vault.corp:8200", vaultToken, "my-key")
cfg.TLSClientCert = "/etc/keeper/client.crt"
cfg.TLSClientKey  = "/etc/keeper/client.key"
provider, _ = remote.New(cfg)
store.RegisterHSMProvider("tenant", "secrets", provider)
store.CreateBucket("tenant", "secrets", keeper.LevelRemote, "ops")
Audit key export
// Export the audit encryption key to allow a third-party auditor to decrypt
// event details without access to the master passphrase.
auditKey, err := store.ExportAuditKey()
defer zero.Bytes(auditKey)

events, err := auditStore.LoadChain("vault", "system", auditKey)
Key rotation
// Rotate passphrase — crash-safe WAL, resumes on next Unlock if interrupted
store.Rotate([]byte("new-passphrase"))

// Rotate KDF salt — re-derives master key, re-encrypts LevelPasswordOnly
store.RotateSalt([]byte("current-passphrase"))
Compare-and-swap
err := store.CompareAndSwapNamespacedFull("vault", "system", "counter",
    []byte("old"), []byte("new"))
// ErrCASConflict if current value does not match old
Backup
f, _ := os.Create("keeper.db.bak")
info, err := store.Backup(f)
// info.Bytes, info.Timestamp, info.DBPath

Error catalogue

Error Meaning
ErrStoreLocked Operation attempted while the store is locked
ErrInvalidPassphrase Wrong master passphrase
ErrAuthFailed Any UnlockBucket failure — does not distinguish wrong password from unknown admin ID (CWE-204)
ErrKeyNotFound Secret key does not exist
ErrBucketLocked Bucket has not been unlocked
ErrPolicyImmutable Second policy for an existing bucket
ErrPolicyNotFound No policy for the given scheme/namespace
ErrAdminNotFound Admin ID not in policy — RevokeAdmin only
ErrHSMProviderNil HSM/Remote bucket created without a registered provider
ErrCheckLatency DB read latency exceeded DBLatencyThreshold
ErrCASConflict Current value does not match expected in CompareAndSwap
ErrSecurityDowngrade Cross-bucket move from higher to lower security level
ErrAlreadyUnlocked UnlockDatabase called on an already-unlocked store
ErrMasterRequired UnlockDatabase called with nil or destroyed Master
ErrChainBroken Audit chain integrity verification failed
ErrMetadataDecrypt Encrypted metadata could not be decrypted
ErrPolicySignature Policy HMAC verification failed — record was tampered

Security decisions

ErrAuthFailed unifies all UnlockBucket failures (CWE-204 / CVSS 5.3). Both an unknown admin ID and a wrong password return ErrAuthFailed. This prevents admin ID enumeration by timing or error-string comparison. RevokeAdmin retains ErrAdminNotFound because it is an administrative operation on an already-unlocked store.

Argon2id dominates timing. Argon2id takes 200–500 ms on typical hardware. Post-derivation comparison differences are four or more orders of magnitude smaller and are not measurable remotely. No artificial equalisation is applied.

DEK retrieved inside the CAS transaction boundary. CompareAndSwapNamespacedFull retrieves the bucket DEK inside the bbolt write transaction, eliminating the window where a concurrent Rotate could change the DEK between retrieval and use.

Passphrase never stored as a Go string in the HTTP handler. All three passphrase fields (passphrase, new_passphrase) are decoded from JSON directly into []byte via raw-map extraction, keeping the string backing array off the long-lived heap. The []byte copy is zeroed with wipeBytes after use.

No --passphrase flag in the CLI. Flags appear in ps output and shell history. The CLI accepts the passphrase only from KEEPER_PASSPHRASE env or an interactive no-echo prompt.

REPL secret values are never visible. set <key> in the REPL without an inline value uses term.ReadPassword — it does not appear in terminal scrollback, shell history, or ps. An inline value (set key value) can be supplied for non-sensitive data when convenient.

SaltStore is intentionally unencrypted. The KDF salt must be readable before UnlockDatabase to derive the master key. policyEncKey (used for all other metadata encryption) is itself derived from the master key — encrypting the salt with policyEncKey would be circular. A KDF salt provides uniqueness, not confidentiality; there is no security value in encrypting it.

Policy bucket keys are hashed, not plaintext. On-disk policy keys are hex(SHA-256("scheme:namespace"))[:32] — 128 bits of key space — rather than readable strings. An offline attacker reading the bbolt file cannot enumerate bucket names without decrypting the policy blobs.

Metadata encryption uses the same cipher interface as secrets. All policyEncKey and auditEncKey operations go through s.config.NewCipher(key) — the same crypt.Cipher interface configured for secret values. The user's cipher choice (AES-256-GCM for FIPS 140, XChaCha20-Poly1305 by default) flows through to policy, WAL, and audit encryption automatically. No code path hard-codes a specific algorithm.

LevelHSM and LevelRemote buckets skipped during master key rotation. reencryptAllWithKey and RotateSalt explicitly skip these buckets. The DEK is provider-controlled; master salt rotation does not affect it.

Crash-safe rotation with WrappedOldKey. Rotate writes a WAL before touching any record. The WAL carries WrappedOldKey: the pre-rotation master key encrypted with the new master key. After a crash, UnlockDatabase decrypts WrappedOldKey using the verified new key and resumes rotation from the cursor.


Dependencies

Package Purpose
go.etcd.io/bbolt Embedded key-value store
golang.org/x/crypto Argon2id, XChaCha20-Poly1305, HKDF, scrypt
github.com/awnumar/memguard Memory-safe key enclave (master key, DEKs)
github.com/vmihailenco/msgpack/v5 Binary serialisation for secrets and policies
github.com/olekukonko/jack Process supervision (optional Jack integration)
github.com/olekukonko/ll Structured logging
github.com/olekukonko/errors Sentinel errors with stack traces
github.com/olekukonko/zero Safe byte-slice zeroing
github.com/olekukonko/prompter No-echo terminal prompts (CLI only)
github.com/integrii/flaggy CLI flag parsing (cmd/keeper only)
golang.org/x/term TTY detection and raw password reading (CLI only)

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrStoreLocked        = errors.New("secret store is locked")
	ErrInvalidPassphrase  = errors.New("invalid passphrase or admin credential")
	ErrKeyNotFound        = errors.New("secret key not found")
	ErrNamespaceNotFound  = errors.New("namespace not found")
	ErrNamespaceEmpty     = errors.New("namespace cannot be empty")
	ErrNamespaceInvalid   = errors.New("invalid namespace name")
	ErrSchemeInvalid      = errors.New("invalid scheme name")
	ErrAlreadyUnlocked    = errors.New("store already unlocked")
	ErrInvalidConfig      = errors.New("invalid store configuration")
	ErrCASConflict        = errors.New("compare-and-swap conflict: value changed")
	ErrMigrationFailed    = errors.New("database migration failed")
	ErrMasterRequired     = errors.New("master key required")
	ErrPolicyImmutable    = errors.New("bucket policy is immutable after creation")
	ErrChainBroken        = errors.New("audit chain integrity check failed")
	ErrPolicyNotFound     = errors.New("bucket policy not found")
	ErrBucketLocked       = errors.New("bucket is locked — call UnlockBucket first")
	ErrSecurityDowngrade  = errors.New("security downgrade requires explicit confirmation")
	ErrRotationIncomplete = errors.New("incomplete key rotation detected: call Rotate again with the new passphrase")
	ErrAdminNotFound      = errors.New("admin ID not found in bucket policy")
	ErrPolicySignature    = errors.New("policy signature verification failed")
	ErrMetadataDecrypt    = errors.New("metadata decryption failed")
	ErrHSMProviderNil     = errors.New("LevelHSM and LevelRemote require a non-nil HSMProvider")
	ErrCheckLatency       = errors.New("database read latency exceeded threshold")

	// ErrAuthFailed is returned for any authentication failure in UnlockBucket.
	// It deliberately does not distinguish between an unknown admin ID and a
	// wrong password to prevent admin ID enumeration (CWE-204 / CVSS 5.3).
	ErrAuthFailed = errors.New("authentication failed")
)

Sentinel errors.

Functions

func DeriveKEK

func DeriveKEK(masterKey, adminCred, salt []byte) ([]byte, error)

DeriveKEK derives a Key Encryption Key from master key + admin credential + salt. Uses HKDF-SHA256. The KEK must be used immediately and then zeroed — never stored.

Neither masterKey alone nor adminCred alone can derive the KEK. Changing admin password requires only re-wrapping the DEK, not re-encrypting secrets.

func GenerateDEK

func GenerateDEK() (*memguard.Enclave, error)

GenerateDEK generates a fresh random 32-byte Data Encryption Key. Returns it sealed in a memguard Enclave — protected from birth.

func GenerateDEKSalt

func GenerateDEKSalt() ([]byte, error)

GenerateDEKSalt generates a fresh random salt for a new bucket's DEK.

func GlobalClear added in v0.0.2

func GlobalClear()

GlobalClear removes the global store reference. Call this before or after Close() to prevent subsequent GlobalGetKey calls from operating on a closed store. Register with jack.Shutdown when available, or call explicitly in your shutdown sequence.

func GlobalGetKey

func GlobalGetKey(key string) ([]byte, error)

GlobalGetKey retrieves a secret from the global store.

func GlobalStore

func GlobalStore(store *Keeper)

GlobalStore sets the process-wide default Keeper instance.

func UnwrapDEK

func UnwrapDEK(wrapped, kek []byte) (*memguard.Enclave, error)

UnwrapDEK decrypts a wrapped DEK. Returns it sealed in a new Enclave. kek is zeroed after use. Returns ErrInvalidPassphrase on authentication failure.

func WithJack

func WithJack(cfg JackConfig) func(*Config)

WithJack returns an option that attaches Jack integration handles to the Config.

func WrapDEK

func WrapDEK(dek *memguard.Enclave, kek []byte) ([]byte, error)

WrapDEK encrypts a DEK (from its Enclave) with a KEK. Format: [24-byte nonce][ciphertext+16-byte tag]. kek is zeroed after use.

Types

type AuditEvent

type AuditEvent = pkgaudit.Event

AuditEvent is the public-facing audit record re-exported from pkg/audit.

type BackupInfo

type BackupInfo struct {
	Bytes     int64
	Timestamp time.Time
	DBPath    string
}

BackupInfo holds metadata about a completed backup.

type BucketEvent

type BucketEvent = pkgaudit.Event

BucketEvent is an audit event (alias of pkg/audit.Event).

type BucketSecurityPolicy

type BucketSecurityPolicy struct {
	ID                string        `json:"id"                   msgpack:"id"`
	Scheme            string        `json:"scheme"               msgpack:"scheme"`
	Namespace         string        `json:"namespace"            msgpack:"namespace"`
	Level             SecurityLevel `json:"level"                msgpack:"level"`
	CreatedAt         time.Time     `json:"created_at"           msgpack:"created_at"`
	CreatedBy         string        `json:"created_by"           msgpack:"created_by"`
	EncryptionVersion int           `json:"encryption_version"   msgpack:"encryption_version"`

	// LastRekeyed records when RotateAdminWrappedDEK last succeeded.
	// A zero value means the bucket predates this field and should be treated
	// as needing re-keying when NeedsAdminRekey is called after a salt rotation.
	LastRekeyed time.Time `json:"last_rekeyed,omitempty" msgpack:"last_rekeyed,omitempty"`

	// LevelAdminWrapped only:
	DEKSalt     []byte            `json:"dek_salt,omitempty"   msgpack:"dek_salt,omitempty"`
	WrappedDEKs map[string][]byte `json:"wrapped_deks,omitempty" msgpack:"wrapped_deks,omitempty"`

	// HSMProvider is required for LevelHSM and LevelRemote buckets.
	// Excluded from both JSON and msgpack serialisation — callers must
	// register it via Keeper.RegisterHSMProvider after opening the database.
	HSMProvider HSMProvider `json:"-" msgpack:"-"`

	// Handler provides optional pre/post-processing hooks for this bucket.
	Handler SchemeHandler `json:"-" msgpack:"-"`
}

BucketSecurityPolicy is immutable after creation. On-disk encoding: msgpack (new databases) or JSON (legacy, migrated on unlock). Audit events referencing this policy remain JSON — they are intentionally human-readable and verified without the passphrase.

func (*BucketSecurityPolicy) HasAdmin

func (p *BucketSecurityPolicy) HasAdmin(adminID string) bool

HasAdmin reports whether adminID has a wrapped DEK copy in this policy.

func (*BucketSecurityPolicy) Validate

func (p *BucketSecurityPolicy) Validate() error

Validate checks policy constraints before creation.

type Chain

type Chain struct {
	// contains filtered or unexported fields
}

Chain manages immutable policy storage and append-only audit events.

func NewPolicyChainManager

func NewPolicyChainManager(store *Keeper) *Chain

NewPolicyChainManager creates a new chain manager.

func (*Chain) AppendEvent

func (m *Chain) AppendEvent(scheme, namespace, eventType string, details interface{}) error

AppendEvent adds a new audit event to the bucket's chain.

func (*Chain) CreatePolicy

func (m *Chain) CreatePolicy(policy *BucketSecurityPolicy) error

CreatePolicy registers a new immutable bucket policy.

func (*Chain) GetPolicy

func (m *Chain) GetPolicy(scheme, namespace string) (*BucketSecurityPolicy, error)

GetPolicy retrieves a bucket policy (read-only).

func (*Chain) PruneEvents

func (m *Chain) PruneEvents(scheme, namespace string, olderThan time.Duration, keepLastN int) error

PruneEvents removes old events; never prunes high-security buckets.

func (*Chain) VerifyChainIntegrity

func (m *Chain) VerifyChainIntegrity(scheme, namespace string) error

VerifyChainIntegrity checks the entire audit chain for a bucket.

type Config

type Config struct {
	DBPath           string
	KeyLen           int
	AutoLockInterval time.Duration
	EnableAudit      bool
	DefaultScheme    string
	DefaultNamespace string
	Logger           *ll.Logger
	Jack             JackConfig

	// AuditPruneInterval controls how often the scheduler prunes audit events.
	// Set to 0 to disable automatic pruning.
	AuditPruneInterval time.Duration

	// AuditPruneOlderThan removes events older than this duration each prune cycle.
	AuditPruneOlderThan time.Duration

	// AuditPruneKeepLastN retains at least this many recent events regardless of age.
	AuditPruneKeepLastN int

	// DBLatencyThreshold is the maximum acceptable database read latency.
	// Defaults to defaultDBLatencyThreshold when zero.
	DBLatencyThreshold time.Duration

	KDF       crypt.KDF
	NewCipher func(key []byte) (crypt.Cipher, error)

	// BucketDEKMigrationBatchSize is the number of records to re-encrypt per
	// migration looper tick. Default 500. Reduce to 50 in I/O-constrained
	// environments to limit write amplification.
	BucketDEKMigrationBatchSize int

	// BucketDEKMigrationInterval is the pause between migration batches.
	// Default 100ms. At defaults: 100k records migrate in ~20s with no
	// perceptible service impact.
	BucketDEKMigrationInterval time.Duration

	// BucketDEKMigrationProgress is called after each batch with cumulative
	// progress. Optional — safe to leave nil.
	BucketDEKMigrationProgress func(scheme, namespace string, done, total int)

	Argon2Time              uint32
	Argon2Memory            uint32
	Argon2Parallelism       uint8
	VerifyArgon2Time        uint32
	VerifyArgon2Memory      uint32
	VerifyArgon2Parallelism uint8
}

Config holds all configuration for a Keeper instance.

type EncryptedMetadata

type EncryptedMetadata struct {
	CreatedAt   time.Time `msgpack:"ca"`
	UpdatedAt   time.Time `msgpack:"ua"`
	AccessCount int       `msgpack:"ac"`
	LastAccess  time.Time `msgpack:"la,omitempty"`
	Version     int       `msgpack:"v"`
}

EncryptedMetadata holds per-secret metadata encrypted with a key derived from the bucket DEK. Inaccessible without the bucket key.

type Envelope

type Envelope struct {
	// contains filtered or unexported fields
}

Envelope is the in-memory secure vault for Data Encryption Keys (DEKs).

Every unlocked bucket has exactly one entry here. The DEK is sealed inside a memguard Enclave which: mlocks the memory so the OS never pages it to disk mprotects the pages read-only when not in active use zeros the memory on release

Replaces the previous plain-map bucketKeys — Go's GC gave no guarantee that key bytes would ever be zeroed.

func NewEnvelope

func NewEnvelope() *Envelope

NewEnvelope creates an empty, ready-to-use Envelope.

func (*Envelope) Drop

func (e *Envelope) Drop(scheme, namespace string)

Drop removes the sealed DEK for a single bucket. The Enclave is dropped from the map; memguard cleans up on GC.

func (*Envelope) DropAdminWrapped

func (e *Envelope) DropAdminWrapped(registry map[string]*BucketSecurityPolicy)

DropAdminWrapped removes DEKs for all buckets whose policy is LevelAdminWrapped. LevelPasswordOnly (system/vault://) buckets are intentionally preserved so background jobs keep running after an admin session times out.

func (*Envelope) DropAll

func (e *Envelope) DropAll()

DropAll removes every DEK from the Envelope.

func (*Envelope) DropOld added in v0.0.2

func (e *Envelope) DropOld(scheme, namespace string)

DropOld removes the pre-migration fallback DEK for a single bucket. Called after all records in a bucket have been migrated.

func (*Envelope) HeldKeys

func (e *Envelope) HeldKeys() []string

HeldKeys returns the scheme:namespace keys currently in the Envelope. Intended for status/introspection only.

func (*Envelope) Hold

func (e *Envelope) Hold(scheme, namespace string, buf *memguard.LockedBuffer)

Hold seals a DEK into a memguard Enclave. buf is sealed (and thus destroyed) by this call — do not use it after.

func (*Envelope) HoldBytes

func (e *Envelope) HoldBytes(scheme, namespace string, dek []byte)

HoldBytes seals raw key bytes into the Envelope, then zeros the source slice.

func (*Envelope) HoldEnclave

func (e *Envelope) HoldEnclave(scheme, namespace string, enc *memguard.Enclave) error

HoldEnclave opens a sealed Enclave and places its contents into the Envelope. enc is consumed — the Envelope re-seals the data under its own map entry.

func (*Envelope) HoldOld added in v0.0.2

func (e *Envelope) HoldOld(scheme, namespace string, buf *memguard.LockedBuffer)

HoldOld stores the pre-migration DEK (old master-key-as-DEK) under a secondary key. Used during the background migration window so that records written before the migration can still be decrypted. buf is sealed (and destroyed) by this call.

func (*Envelope) IsHeld

func (e *Envelope) IsHeld(scheme, namespace string) bool

IsHeld reports whether a DEK for this scheme/namespace is currently sealed.

func (*Envelope) Retrieve

func (e *Envelope) Retrieve(scheme, namespace string) (*memguard.LockedBuffer, error)

Retrieve opens the Enclave and returns a LockedBuffer. The caller MUST call buf.Destroy() when done — typically via defer.

buf, err := env.Retrieve(scheme, ns)
if err != nil { return err }
defer buf.Destroy()
useBytes(buf.Bytes())

func (*Envelope) RetrieveOld added in v0.0.2

func (e *Envelope) RetrieveOld(scheme, namespace string) (*memguard.LockedBuffer, error)

RetrieveOld returns the pre-migration DEK for scheme/namespace. Returns ErrBucketLocked when no old key is present (migration complete or not started). The caller MUST call buf.Destroy() when done.

type HSMProvider

type HSMProvider interface {
	WrapDEK(dek []byte) ([]byte, error)
	UnwrapDEK(wrapped []byte) ([]byte, error)
	Ping(ctx context.Context) error
}

HSMProvider abstracts wrap/unwrap operations for LevelHSM and LevelRemote buckets. Implementations must be safe for concurrent use from multiple goroutines. Ping is used by the jack.Doctor health patient to verify provider liveness.

type Hooks

type Hooks struct {
	// Read
	PreGet  func(scheme, namespace, key string) error
	PostGet func(scheme, namespace, key string, value []byte) ([]byte, error)

	// Write
	PreSet  func(scheme, namespace, key string, value []byte) ([]byte, error)
	PostSet func(scheme, namespace, key string, value []byte)

	// Delete
	PreDelete  func(scheme, namespace, key string) error
	PostDelete func(scheme, namespace, key string)

	// Compare-and-swap
	PreCAS  func(scheme, namespace, key string, oldValue, newValue []byte) error
	PostCAS func(scheme, namespace, key string, newValue []byte)

	// Audit — called after every operation regardless of success.
	OnAudit func(action, scheme, namespace, key string, success bool, duration time.Duration)
}

Hooks injects custom logic at key lifecycle points. All Pre* hooks run before the operation and may abort it by returning an error. All Post* hooks run after a successful commit; returning an error from a Post* hook logs the error but does NOT roll back the already-committed operation. Any hook field left nil is a no-op.

type JackConfig

type JackConfig struct {
	Pool     JackPool
	Shutdown JackShutdown
	Doctor   JackDoctor
}

JackConfig carries optional Jack integration handles. All fields are optional; nil means keeper manages its own equivalent.

type JackDoctor

type JackDoctor interface {
	Add(p *jack.Patient) error
	Remove(id string) bool
	StopAll(timeout time.Duration)
}

JackDoctor is the subset of jack.Doctor that keeper uses for health monitoring. Add accepts *jack.Patient to match the concrete jack.Doctor.Add signature so that *jack.Doctor satisfies this interface without an adapter.

type JackPool

type JackPool interface {
	Do(fn func())
	DoCtx(ctx context.Context, fn func(context.Context))
	IsClosed() bool
}

JackPool is the subset of jack.Pool that keeper uses. Keeper never calls Shutdown — the pool lifecycle is owned by the caller.

type JackShutdown

type JackShutdown interface {
	Register(fn any) error
	Done() <-chan struct{}
}

JackShutdown is the subset of jack.Shutdown that keeper uses.

type Keeper

type Keeper struct {
	// contains filtered or unexported fields
}

Keeper is the encrypted secret store.

Security model:

LevelPasswordOnly buckets (vault://):

Master key → Envelope at UnlockDatabase. Available immediately at startup;
background jobs always have access.

LevelAdminWrapped buckets (keeper://):

Random DEK per bucket. DEK wrapped per-admin via
WrapDEK(dek, HKDF(master‖adminPass, salt)).
UnlockBucket(adminID, adminPassword) → Envelope.
Reaper TTL drops these DEKs after inactivity when Jack is configured.

LevelHSM and LevelRemote buckets:

Random DEK generated at CreateBucket time, wrapped by the HSMProvider.
UnlockDatabase automatically calls the provider to unwrap and seed the
Envelope for all registered HSM/Remote buckets. Master key rotation does
not re-encrypt these buckets — the DEK is provider-controlled.

func GlobalGet

func GlobalGet() *Keeper

GlobalGet returns the process-wide default Keeper instance.

func New

func New(config Config, opts ...func(*Config)) (*Keeper, error)

New opens or creates a keeper database.

func Open

func Open(config Config, opts ...func(*Config)) (*Keeper, error)

Open opens an existing database. Returns an error if it does not exist.

func (*Keeper) AddAdminToPolicy

func (s *Keeper) AddAdminToPolicy(scheme, namespace, adminID string, adminPassword []byte) error

AddAdminToPolicy wraps the bucket DEK under a new admin's KEK and persists the updated policy.

func (*Keeper) Backup

func (s *Keeper) Backup(w io.Writer) (BackupInfo, error)

Backup writes to w and returns BackupInfo.

func (*Keeper) BackupTo

func (s *Keeper) BackupTo(w io.Writer) (int64, error)

BackupTo streams a consistent, point-in-time hot backup of the encrypted database directly to w.

func (*Keeper) Close

func (s *Keeper) Close() error

Close locks the store and closes the underlying database.

func (*Keeper) CompareAndSwap

func (s *Keeper) CompareAndSwap(key string, oldValue, newValue []byte) error

func (*Keeper) CompareAndSwapNamespaced

func (s *Keeper) CompareAndSwapNamespaced(namespace, key string, oldValue, newValue []byte) error

func (*Keeper) CompareAndSwapNamespacedFull

func (s *Keeper) CompareAndSwapNamespacedFull(scheme, namespace, key string, oldValue, newValue []byte) error

CompareAndSwapNamespacedFull atomically reads, compares, and replaces a value. The DEK is retrieved inside the transaction to avoid a stale-key window.

func (*Keeper) Copy

func (s *Keeper) Copy(key, fromNS, toNS string) error

func (*Keeper) CopyCrossBucket

func (s *Keeper) CopyCrossBucket(key, fromScheme, fromNS, toScheme, toNS string, confirmDowngrade bool) error

CopyCrossBucket copies a key between two buckets.

func (*Keeper) CreateBucket

func (s *Keeper) CreateBucket(scheme, namespace string, level SecurityLevel, createdBy string) error

CreateBucket registers a new immutable bucket policy.

For LevelPasswordOnly: the bucket is accessible as soon as UnlockDatabase has been called. If the store is already unlocked when CreateBucket is called the bucket is seeded into the Envelope immediately.

For LevelAdminWrapped: the bucket is inaccessible until at least one admin is added via AddAdminToPolicy and then unlocked via UnlockBucket.

For LevelHSM and LevelRemote: the policy must carry a non-nil HSMProvider. The DEK is generated here, wrapped by the provider, and stored in WrappedDEKs.

func (*Keeper) Delete

func (s *Keeper) Delete(key string) error

Delete removes a secret.

func (*Keeper) DeleteBucket

func (s *Keeper) DeleteBucket(scheme, namespace string) error

DeleteBucket removes a namespace bucket and all its contents.

func (*Keeper) DeleteNamespace

func (s *Keeper) DeleteNamespace(namespace string) error

func (*Keeper) DeleteNamespaced

func (s *Keeper) DeleteNamespaced(namespace, key string) error

func (*Keeper) DeleteNamespacedFull

func (s *Keeper) DeleteNamespacedFull(scheme, namespace, key string) error

DeleteNamespacedFull removes a secret with explicit scheme/namespace/key.

func (*Keeper) DeleteScheme

func (s *Keeper) DeleteScheme(scheme string) error

DeleteScheme removes an entire scheme and all its namespaces.

func (*Keeper) DeriveMaster

func (s *Keeper) DeriveMaster(passphrase []byte) (*Master, error)

DeriveMaster derives a Master key from passphrase bytes using the configured KDF. The dummy timing code from the previous version is removed: Argon2 dominates the timing, making the failure path indistinguishable in practice.

func (*Keeper) EnsureBucket added in v0.0.2

func (s *Keeper) EnsureBucket(key string) error

EnsureBucket creates a LevelPasswordOnly bucket for the scheme and namespace parsed from key, if one does not already exist. It is idempotent: ErrPolicyImmutable (bucket already exists) is silently ignored.

This is the correct primitive for CLI and keepcmd operations that should work across any scheme/namespace without requiring an explicit CreateBucket call. It deliberately does not create LevelAdminWrapped or LevelHSM buckets — those require explicit operator intent.

func (*Keeper) Exists

func (s *Keeper) Exists(key string) (bool, error)

Exists reports whether a key is present.

func (*Keeper) ExistsNamespaced

func (s *Keeper) ExistsNamespaced(namespace, key string) (bool, error)

func (*Keeper) ExistsNamespacedFull

func (s *Keeper) ExistsNamespacedFull(scheme, namespace, key string) (bool, error)

ExistsNamespacedFull reports whether a key is present in the given bucket.

func (*Keeper) ExportAuditKey added in v0.0.2

func (s *Keeper) ExportAuditKey() ([]byte, error)

ExportAuditKey derives and returns a fresh copy of the audit encryption key. The caller must zero the returned slice when done. Returns an error when the store is locked.

func (*Keeper) Get

func (s *Keeper) Get(key string) ([]byte, error)

Get retrieves a secret. Returns raw bytes.

func (*Keeper) GetBytes

func (s *Keeper) GetBytes(key string) ([]byte, error)

GetBytes is an alias for Get.

func (*Keeper) GetBytesNamespaced

func (s *Keeper) GetBytesNamespaced(namespace, key string) ([]byte, error)

func (*Keeper) GetNamespaced

func (s *Keeper) GetNamespaced(namespace, key string) ([]byte, error)

func (*Keeper) GetNamespacedFull

func (s *Keeper) GetNamespacedFull(scheme, namespace, key string) ([]byte, error)

GetNamespacedFull retrieves a secret with explicit scheme/namespace/key.

func (*Keeper) GetPolicy

func (s *Keeper) GetPolicy(scheme, namespace string) (*BucketSecurityPolicy, error)

GetPolicy returns a bucket's immutable policy.

func (*Keeper) GetString

func (s *Keeper) GetString(key string) (string, error)

GetString retrieves a secret as a UTF-8 string.

func (*Keeper) GetStringNamespaced

func (s *Keeper) GetStringNamespaced(namespace, key string) (string, error)

func (*Keeper) IsBucketUnlocked

func (s *Keeper) IsBucketUnlocked(scheme, namespace string) bool

IsBucketUnlocked reports whether a bucket's DEK is in the Envelope.

func (*Keeper) IsLocked

func (s *Keeper) IsLocked() bool

IsLocked reports whether the store is currently locked.

func (*Keeper) List

func (s *Keeper) List() ([]string, error)

List returns all keys in the default bucket.

func (*Keeper) ListNamespace

func (s *Keeper) ListNamespace(namespace string) ([]string, error)

func (*Keeper) ListNamespacedFull

func (s *Keeper) ListNamespacedFull(scheme, namespace string) ([]string, error)

ListNamespacedFull returns all keys for the given scheme/namespace.

func (*Keeper) ListNamespaces

func (s *Keeper) ListNamespaces() ([]string, error)

func (*Keeper) ListNamespacesInSchemeFull

func (s *Keeper) ListNamespacesInSchemeFull(scheme string) ([]string, error)

ListNamespacesInSchemeFull returns all namespace names within a scheme.

func (*Keeper) ListPrefix

func (s *Keeper) ListPrefix(prefix string) ([]string, error)

func (*Keeper) ListPrefixNamespaced

func (s *Keeper) ListPrefixNamespaced(namespace, prefix string) ([]string, error)

func (*Keeper) ListPrefixNamespacedFull

func (s *Keeper) ListPrefixNamespacedFull(scheme, namespace, prefix string) ([]string, error)

ListPrefixNamespacedFull returns all keys matching prefix in the given bucket.

func (*Keeper) ListSchemes

func (s *Keeper) ListSchemes() ([]string, error)

ListSchemes returns all scheme names in the database.

func (*Keeper) Lock

func (s *Keeper) Lock() error

Lock locks the store: drops all DEKs, wipes the master key, clears the audit signing key, and stops all background goroutines.

func (*Keeper) LockBucket

func (s *Keeper) LockBucket(scheme, namespace string) error

LockBucket drops the DEK for a single bucket from the Envelope.

func (*Keeper) Metrics

func (s *Keeper) Metrics() core.MetricsSnapshot

Metrics returns a snapshot of operational counters.

func (*Keeper) MigrationStatus added in v0.0.2

func (s *Keeper) MigrationStatus() MigrationState

MigrationStatus reports the current state of the per-bucket DEK derivation migration. Safe to call from any goroutine.

func (*Keeper) Move

func (s *Keeper) Move(key, fromNS, toNS string) error

func (*Keeper) MoveCrossBucket

func (s *Keeper) MoveCrossBucket(key, fromScheme, fromNS, toScheme, toNS string, confirmDowngrade bool) error

func (*Keeper) NeedsAdminRekey

func (s *Keeper) NeedsAdminRekey(scheme, namespace string) (bool, error)

NeedsAdminRekey reports whether a LevelAdminWrapped bucket's wrapped DEKs were last re-keyed before the current master salt was generated. Returns false with no error for LevelPasswordOnly, LevelHSM, and LevelRemote buckets.

func (*Keeper) RegisterBucketHandler added in v0.0.2

func (s *Keeper) RegisterBucketHandler(scheme, namespace string, handler SchemeHandler) error

RegisterBucketHandler attaches a SchemeHandler to an existing bucket policy in the registry. The handler is called for every Pre*/Post* operation on that bucket. It is excluded from serialisation — callers must register it after Open (and after UnlockDatabase for LevelAdminWrapped buckets). Returns ErrPolicyNotFound if the bucket does not exist.

func (*Keeper) RegisterHSMProvider

func (s *Keeper) RegisterHSMProvider(scheme, namespace string, provider HSMProvider) error

RegisterHSMProvider attaches an HSMProvider to an existing LevelHSM or LevelRemote policy in the registry. This must be called after Open and before UnlockDatabase so the provider is available when the bucket is automatically unlocked. It returns an error if the policy is not found or is not HSM/Remote level.

func (*Keeper) RegisterScheme

func (s *Keeper) RegisterScheme(name string, handler SchemeHandler) error

RegisterScheme validates and registers a scheme name with an optional handler.

func (*Keeper) Rename

func (s *Keeper) Rename(key, newKey string) error

Rename moves a key to a new name within the same bucket.

func (*Keeper) RenameNamespaced

func (s *Keeper) RenameNamespaced(namespace, oldKey, newKey string) error

func (*Keeper) RenameNamespacedFull

func (s *Keeper) RenameNamespacedFull(scheme, namespace, oldKey, newKey string) error

RenameNamespacedFull moves a key within an explicit bucket.

func (*Keeper) RevokeAdmin

func (s *Keeper) RevokeAdmin(scheme, namespace, adminID string) error

RevokeAdmin removes an admin's wrapped DEK copy from the policy. The bucket DEK and all secrets remain untouched.

func (*Keeper) Rotate

func (s *Keeper) Rotate(newPassphrase []byte) error

Rotate re-derives the master key with a new passphrase and re-encrypts every LevelPasswordOnly secret. LevelAdminWrapped secrets use per-admin KEKs and are unaffected.

After the master key changes: The audit signing key is re-derived from the new master. A key-rotation checkpoint event is appended to every active audit chain,

signed with the old key as the final event of the old epoch and
verifiable by the new key as the first event of the new epoch.

Passphrase bytes are NOT zeroed by this method — the caller owns them.

func (*Keeper) RotateAdminWrappedDEK

func (s *Keeper) RotateAdminWrappedDEK(scheme, namespace, adminID string, adminPassword []byte) error

RotateAdminWrappedDEK re-wraps the bucket DEK under a fresh per-bucket salt for the given admin, then updates LastRekeyed on the policy. The admin must authenticate with their current password to prove they hold a valid copy of the DEK before it is re-wrapped. After this call, only admins whose credentials were supplied here will have up-to-date wrapped copies. Other admins must call this method with their own credentials to update their copy.

func (*Keeper) RotateSalt

func (s *Keeper) RotateSalt(passphrase []byte) error

RotateSalt generates a new KDF salt, re-derives the master key under it, re-encrypts all LevelPasswordOnly secrets, and updates the verification hash. The old salt is retained in the versioned salt store for audit purposes.

Salt rotation is independent of passphrase rotation. Call this periodically or whenever you want to ensure that a compromised salt cannot be used to accelerate offline attacks against a future passphrase breach.

passphrase must be the current passphrase — it is used to re-derive the master key under the new salt. It is NOT zeroed by this method. LevelAdminWrapped buckets are NOT re-keyed by this call because their DEKs use a per-bucket salt, not the master KDF salt. Call RotateAdminWrappedDEK separately after this method completes.

func (*Keeper) Set

func (s *Keeper) Set(key string, value []byte) error

Set stores a secret.

func (*Keeper) SetAuditFunc

func (s *Keeper) SetAuditFunc(fn func(action, scheme, namespace, key string, success bool, duration time.Duration))

SetAuditFunc registers a callback invoked on every auditable operation.

func (*Keeper) SetBytes

func (s *Keeper) SetBytes(key string, value []byte) error

SetBytes is an alias for Set.

func (*Keeper) SetDefaultNamespace

func (s *Keeper) SetDefaultNamespace(ns string) error

SetDefaultNamespace sets the default namespace used when none is specified.

func (*Keeper) SetDefaultScheme

func (s *Keeper) SetDefaultScheme(scheme string) error

SetDefaultScheme sets the default scheme used when none is specified.

func (*Keeper) SetHooks

func (s *Keeper) SetHooks(hooks Hooks)

SetHooks configures lifecycle hooks for pre/post processing.

func (*Keeper) SetNamespaced

func (s *Keeper) SetNamespaced(namespace, key string, value []byte) error

func (*Keeper) SetNamespacedFull

func (s *Keeper) SetNamespacedFull(scheme, namespace, key string, value []byte) error

SetNamespacedFull stores a secret with explicit scheme/namespace/key.

func (*Keeper) SetString

func (s *Keeper) SetString(key, value string) error

SetString stores a UTF-8 string.

func (*Keeper) SetStringNamespaced

func (s *Keeper) SetStringNamespaced(namespace, key, value string) error

func (*Keeper) Stats

func (s *Keeper) Stats() (*StoreStats, error)

Stats returns aggregate statistics for all schemes and namespaces. Locked buckets contribute key counts and sizes but zero metadata.

func (*Keeper) Unlock

func (s *Keeper) Unlock(passphrase []byte) error

Unlock derives a Master key from passphrase bytes and calls UnlockDatabase. Passphrase bytes are NOT zeroed by this method — the caller owns them.

func (*Keeper) UnlockBucket

func (s *Keeper) UnlockBucket(scheme, namespace, adminID string, adminPassword []byte) error

UnlockBucket unlocks a LevelAdminWrapped bucket. adminPassword is zeroed by this method.

func (*Keeper) UnlockDatabase

func (s *Keeper) UnlockDatabase(master *Master) error

UnlockDatabase unlocks the store with a pre-derived Master key.

All LevelPasswordOnly buckets are unlocked immediately via the Envelope. LevelAdminWrapped buckets require a separate UnlockBucket call. LevelHSM and LevelRemote buckets are unlocked automatically via their registered HSMProvider. The audit HMAC signing key is derived from the master and activated. Background tasks (auto-lock, prune scheduler, health patients) are started after unlock completes.

type Master

type Master struct {
	// contains filtered or unexported fields
}

Master encapsulates the master key for the secret store. It uses memguard for secure memory handling.

func NewMaster

func NewMaster(key []byte) (*Master, error)

NewMaster creates a new Master from a raw key. The raw key is immediately sealed into an enclave and wiped from memory.

func (*Master) Bytes

func (m *Master) Bytes() ([]byte, error)

Bytes retrieves the master key as a byte slice. This creates a temporary copy that should be used immediately and not stored. The caller should call secureZero on the returned bytes when done.

func (*Master) Destroy

func (m *Master) Destroy()

Destroy removes the reference to the enclave.

func (*Master) IsValid

func (m *Master) IsValid() bool

IsValid returns true if the master has a valid enclave.

func (*Master) Open

func (m *Master) Open() (*memguard.LockedBuffer, error)

Open retrieves the master key from the enclave. The returned LockedBuffer must be destroyed after use with defer buf.Destroy()

type MigrationState added in v0.0.2

type MigrationState int

MigrationState reports the per-bucket DEK derivation migration status. Returned by Keeper.MigrationStatus and surfaced in GET /keeper/status.

const (
	// MigrationNotNeeded means the database was created after per-bucket DEK
	// derivation was introduced; no migration is required.
	MigrationNotNeeded MigrationState = iota
	// MigrationInProgress means the background looper is still re-encrypting
	// records from the old master-key-as-DEK format.
	MigrationInProgress
	// MigrationDone means all LevelPasswordOnly records have been re-encrypted
	// under their per-bucket derived DEK.
	MigrationDone
)

func (MigrationState) String added in v0.0.2

func (m MigrationState) String() string

type NamespaceStats

type NamespaceStats struct {
	Scheme            string    `json:"scheme"`
	Name              string    `json:"name"`
	KeyCount          int64     `json:"key_count"`
	TotalSize         int64     `json:"total_size"`
	AvgKeySize        float64   `json:"avg_key_size"`
	OldestKey         time.Time `json:"oldest_key"`
	NewestKey         time.Time `json:"newest_key"`
	TotalReads        int64     `json:"total_reads"`
	TotalWrites       int64     `json:"total_writes"`
	EncryptionVersion int       `json:"encryption_version"`
	SecurityLevel     string    `json:"security_level"`
}

NamespaceStats holds aggregate statistics for one namespace.

type RotationWAL

type RotationWAL struct {
	Status        string    `json:"status"          msgpack:"status"`
	OldKeyHash    []byte    `json:"old_hash"        msgpack:"old_hash"`
	NewKeyHash    []byte    `json:"new_hash"        msgpack:"new_hash"`
	StartedAt     time.Time `json:"started"         msgpack:"started"`
	LastKey       string    `json:"last_key"        msgpack:"last_key"`
	SaltVersion   int       `json:"salt_ver"        msgpack:"salt_ver"`
	WrappedOldKey []byte    `json:"wrapped_old_key" msgpack:"wrapped_old_key"`
}

RotationWAL tracks the state of an in-progress key rotation. Written atomically before the first record is re-encrypted. On crash, UnlockDatabase reads this record and resumes automatically.

WrappedOldKey is the pre-rotation master key encrypted with the new master key using XChaCha20-Poly1305. It is the only safe way to carry the old key across a crash boundary without storing it in plaintext.

type SaltEntry

type SaltEntry struct {
	Version   int       `json:"v"    msgpack:"v"`
	Salt      []byte    `json:"s"    msgpack:"s"`
	CreatedAt time.Time `json:"ca"   msgpack:"ca"`
}

SaltEntry records one generation of the KDF salt.

type SaltStore

type SaltStore struct {
	CurrentVersion int         `json:"current"  msgpack:"current"`
	Entries        []SaltEntry `json:"entries"  msgpack:"entries"`
}

SaltStore is the versioned salt container stored under metaSaltKey. CurrentVersion indexes into Entries. Old entries are retained as an audit trail and for crash-recovery during salt rotation.

type SchemeHandler

type SchemeHandler interface {
	// Read
	PreGet(scheme, namespace, key string) error
	PostGet(scheme, namespace, key string, value []byte) ([]byte, error)

	// Write
	PreSet(scheme, namespace, key string, value []byte) ([]byte, error)
	PostSet(scheme, namespace, key string, value []byte)

	// Delete
	PreDelete(scheme, namespace, key string) error
	PostDelete(scheme, namespace, key string)

	// Compare-and-swap
	PreCAS(scheme, namespace, key string, oldValue, newValue []byte) error
	PostCAS(scheme, namespace, key string, newValue []byte)
}

SchemeHandler allows custom pre/post processing per scheme. All Pre* hooks run before the operation; returning an error aborts it. All Post* hooks run after a successful commit; their errors are logged but do not roll back the already-committed operation.

type SchemeStats

type SchemeStats struct {
	Name       string           `json:"name"`
	Namespaces []NamespaceStats `json:"namespaces"`
	TotalKeys  int64            `json:"total_keys"`
	TotalSize  int64            `json:"total_size"`
}

SchemeStats holds aggregate statistics for one scheme.

type Secret

type Secret struct {
	Ciphertext    []byte `msgpack:"ct"`
	EncryptedMeta []byte `msgpack:"em,omitempty"`
	SchemaVersion int    `msgpack:"sv"`
}

Secret is the on-disk record for a single key, encoded with msgpack. SchemaVersion is always currentSchemaVersion.

type SecurityLevel

type SecurityLevel string

SecurityLevel defines the key-management model for a bucket.

const (
	LevelPasswordOnly SecurityLevel = "password_only"
	LevelAdminWrapped SecurityLevel = "admin_wrapped"
	LevelHSM          SecurityLevel = "hsm"
	LevelRemote       SecurityLevel = "remote"
)

SecurityLevel values for BucketSecurityPolicy.

type StoreStats

type StoreStats struct {
	Schemes           []SchemeStats `json:"schemes"`
	TotalKeys         int64         `json:"total_keys"`
	TotalSize         int64         `json:"total_size"`
	IsLocked          bool          `json:"is_locked"`
	DefaultScheme     string        `json:"default_scheme"`
	DefaultNamespace  string        `json:"default_namespace"`
	AutoLockInterval  time.Duration `json:"auto_lock_interval"`
	TotalReads        int64         `json:"total_reads"`
	TotalWrites       int64         `json:"total_writes"`
	LastActivity      time.Time     `json:"last_activity"`
	DBSize            int64         `json:"db_size_bytes"`
	StorageEfficiency float64       `json:"storage_efficiency"`
	KeyDerivation     string        `json:"key_derivation"`
	SaltVersion       int           `json:"salt_version"`
}

StoreStats is returned by Keeper.Stats.

Directories

Path Synopsis
cmd
keeper command
Command keeper is a standalone secret management CLI backed by keeper.Keeper.
Command keeper is a standalone secret management CLI backed by keeper.Keeper.
pkg
crypt
Package crypt defines pluggable encryption and key-derivation interfaces.
Package crypt defines pluggable encryption and key-derivation interfaces.
hsm
Package hsm provides HSMProvider implementations for use with keeper.
Package hsm provides HSMProvider implementations for use with keeper.
remote
Package remote provides a configurable HTTPS-based HSMProvider that delegates DEK wrap and unwrap operations to any remote KMS service over TLS.
Package remote provides a configurable HTTPS-based HSMProvider that delegates DEK wrap and unwrap operations to any remote KMS service over TLS.
store
Package store defines the storage abstraction used by keeper.
Package store defines the storage abstraction used by keeper.
x
keepcmd
Package keepcmd provides reusable keeper command operations decoupled from any specific CLI framework or application.
Package keepcmd provides reusable keeper command operations decoupled from any specific CLI framework or application.

Jump to

Keyboard shortcuts

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