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).
110 lines
3.8 KiB
Go
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
|
|
})
|
|
}
|