Files
kms/versioning.go
Hanzo AI ca66adc00a purge: rip remaining Infisical residue — docs, env examples, repo boilerplate
Following the earlier AdminShell+CollectionCRUD rewrite (PR #14) which
already deleted backend/, backend-go/, cloudformation/, helm-charts/,
nginx/, sink/, wasm/, upgrade-impact/, e2e/, migration/, package.json
and switched the frontend to a 184K Hanzo-first build, complete the
cleanup by deleting the remaining Infisical-era residue:

  - docs/                                   (1.0G Infisical mintlify docs)
  - .env.example, .env.dev.example,
    .env.migration.example, .env.test.example
                                            (legacy Postgres/Redis envs;
                                             no Go code references them)
  - .eslintignore, .husky/                  (TS toolchain leftovers)
  - CODE_OF_CONDUCT.md, CONTRIBUTING.md,
    SECURITY.md                             (Infisical repo boilerplate)

Working tree drops from ~1.0G to <600K of content (1.5G total is .git/
which `git gc --aggressive --prune=now` shrinks locally; the remote
keeps its history shape — anyone pulling gets the lean checkout).

Build + test verification:
  go build ./...                            (4 cmd binaries: green)
  go test ./... -count=1 -short             (all packages: ok)

Kept: cmd/, pkg/, sdk/, frontend/ (AdminShell), schema/, examples/,
root Go (audit.go, auth.go, consensus.go, embed.go, jwks.go, mount.go,
versioning.go + tests), Dockerfile, Dockerfile.kms-fetch, Makefile,
LLM.md, CLAUDE.md, DEPRECATED.md, SOVEREIGN-KMS-ARCHITECTURE.md,
TFHE-KMS-ARCHITECTURE.md, LICENSE, README.md, VERSION, .github/
(8 Hanzo workflows: build, build-kms-fetch, check-fe-ts-and-lint,
ci, pr-preview, release, validate-pr-title, workflow-sanity).
2026-06-07 14:23:12 -07:00

110 lines
3.8 KiB
Go

// Package main — secret version tracking for replay protection (R-3).
//
// The upstream luxfi/kms store.Secret has no version field. We store a
// monotonic int64 version alongside each secret in ZapDB under a sibling
// key prefix `kms/versions/{path}/{env}/{name}` so upstream schema stays
// untouched — no fork, no PR, no schema migration.
//
// Threat model (R-3): a captured Update envelope replayed after a
// subsequent rotation would overwrite a live secret with a stale value
// (revert-on-replay). Create is safe to replay because POST is an upsert
// of the new value; an adversary cannot use it to revert.
//
// Fix: Update (PATCH) requires the caller to supply the secret's current
// version (If-Match header or JSON body `version`). Mismatch → 409. Each
// successful write increments the stored version. Create (POST) seeds
// version = 1 on insert and bumps version on upsert — idempotent semantics
// preserved (no information leak; adversary cannot cause revert because
// PATCH is the only mutate-with-prior path and it requires version).
package kms
import (
"encoding/binary"
"errors"
"fmt"
badger "github.com/luxfi/zapdb"
)
// ErrVersionMismatch is returned from storePutWithVersion when the caller
// supplies a version that does not match the current on-disk version.
var ErrVersionMismatch = errors.New("kms: version mismatch")
// versionKey returns the ZapDB key used to store the monotonic version
// counter for a (path, name, env) secret. Kept parallel to the store's
// own layout so a list/scan by prefix still works.
func versionKey(path, name, env string) []byte {
return []byte(fmt.Sprintf("kms/versions/%s/%s/%s", path, env, name))
}
// readVersion returns the current version of a secret, or 0 if no version
// has been recorded (equivalent to "secret does not exist yet").
func readVersion(db *badger.DB, path, name, env string) (int64, error) {
var v int64
err := db.View(func(txn *badger.Txn) error {
item, err := txn.Get(versionKey(path, name, env))
if errors.Is(err, badger.ErrKeyNotFound) {
return nil // v stays 0
}
if err != nil {
return err
}
return item.Value(func(buf []byte) error {
if len(buf) != 8 {
return fmt.Errorf("kms: malformed version record (len=%d)", len(buf))
}
v = int64(binary.BigEndian.Uint64(buf))
return nil
})
})
return v, err
}
// bumpVersion atomically reads-then-writes the version counter, returning
// the NEW version written. Requires the caller-supplied expected version
// to match the current version. If expected is -1, skip the CAS check
// (used by POST/upsert; POST does not claim to know the prior version).
func bumpVersion(db *badger.DB, path, name, env string, expected int64) (int64, error) {
var newVer int64
err := db.Update(func(txn *badger.Txn) error {
var cur int64
item, err := txn.Get(versionKey(path, name, env))
switch {
case errors.Is(err, badger.ErrKeyNotFound):
cur = 0
case err != nil:
return err
default:
if valErr := item.Value(func(buf []byte) error {
if len(buf) != 8 {
return fmt.Errorf("kms: malformed version record (len=%d)", len(buf))
}
cur = int64(binary.BigEndian.Uint64(buf))
return nil
}); valErr != nil {
return valErr
}
}
if expected >= 0 && expected != cur {
return ErrVersionMismatch
}
newVer = cur + 1
buf := make([]byte, 8)
binary.BigEndian.PutUint64(buf, uint64(newVer))
return txn.Set(versionKey(path, name, env), buf)
})
return newVer, err
}
// deleteVersion removes the version record when a secret is deleted, so
// that a later re-create starts from version 1 again (not version N+1).
func deleteVersion(db *badger.DB, path, name, env string) error {
return db.Update(func(txn *badger.Txn) error {
err := txn.Delete(versionKey(path, name, env))
if errors.Is(err, badger.ErrKeyNotFound) {
return nil
}
return err
})
}