Files
2026-05-27 23:29:53 +01:00

6.9 KiB

Extend @suaveplan/crypto-vault — vault orchestration layer

Package: @suaveplan/crypto-vault Status: PROPOSED Type: feature (additive — no breaking changes)

  • Spec delta: ./specs/crypto-vault/spec.md

Why

@suaveplan/crypto-vault ships rich primitives (BIP39 / Argon2id / 2SKD / HKDF / SecureBuffer / OpaqueKeyHandle / SRP) but no high-level orchestration helpers — the layer that bundles "generate a root keychain + encrypt it into a vault file + write the signed public-key file" into one call. Upstream ../suaveplan/packages/universal/keys/src/lib/keys-common.ts shipped that orchestration as part of the now-split @suaveplan/keys umbrella; on genesis it has no home yet.

@suaveplan/server-keys (in-flight; full-parity port of 18 operator CLI bins) is blocked on this layer: 7 of its bins (create-root-keys, create-server-keys, roll-server-keys, view-keys, view-vault, view-public-key-file, plus the orchestration half of the chain bins) call createRootKeys / createServerKeys / rollServerKeys / openVault / packVault / viewPublicKeys / saveVaultToStorage / savePublicKeysToStorage / getDataFromStorage. The 3 *WithChain variants already shipped in @suaveplan/crypto-keys (factory layer) demonstrate the pattern; this proposal adds the non-chain siblings to crypto-vault so server-keys can ship at parity.

The wire format is genesis-native, not byte-compatible with upstream vaults (upstream used the deprecated BitPacker(schema) layer; genesis uses the v1 derivation grammar encodePublicKeychain + AEAD regime-2 secret blob + regime-3 application data payload). Cross-version interop is explicitly out of scope — operators rotating from upstream to genesis use a one-shot migration command (in a separate follow-up package), not in-place vault parsing.

What changes

All additive — no existing crypto-vault export changes shape. New module src/orchestration/ exports 9 functions + supporting types.

9 new functions

  • createRootKeys(password, options?): Promise<RootKeysResult> — generates a fresh root Keychain via @suaveplan/crypto generateKeychain, derives an Argon2id KEK from password, encrypts the full keychain into vault bytes (AEAD regime-2 secret blob), extracts public keys + canonical bytes via encodePublicKeychain. Returns { keychain, vaultBytes, publicKeyBytes, publicKeys }.
  • createServerKeys(rootKeys, serverPassword, options?): Promise<ServerKeysResult> — generates a child keychain derived from rootKeys.keychain.master.private (HD-key path), encrypts it under serverPassword. Returns { keychain, vaultBytes, publicKeyBytes, publicKeys, version }.
  • rollServerKeys(rootKeys, currentVersion, serverPassword, options?): Promise<ServerKeysResult> — deterministic re-derivation at version = currentVersion + 1 using the same root master key + serverPassword. Re-encryption produces a fresh vault; the underlying private material is reproducible from (rootMasterKey, version, serverPassword).
  • openVault(vaultBytes, password): Promise<Keychain> — Argon2id-derive KEK from password, AEAD-decrypt vaultBytes to recover the full Keychain. Wrong password raises CryptoVaultError(VAULT_DECRYPT_FAILED); tampered bytes raise the same.
  • packVault(keychain, password, options?): Promise<Uint8Array> — inverse of openVault. Used by callers that already have a Keychain instance (e.g. after a rotation) and need to re-encrypt it.
  • viewPublicKeys(publicKeyBytes, optionalSignKey?): Promise<PublicKeychain> — decode the signed public-key file. If optionalSignKey is provided, verify the embedded signature; mismatch raises CryptoVaultError(PUBLIC_KEY_SIGNATURE_INVALID).
  • saveVaultToStorage(storage, key, vaultBytes): Promise<void> — convenience wrapper over @suaveplan/storage's AbstractStorage.put(key, vaultBytes). Idempotent.
  • savePublicKeysToStorage(storage, key, publicKeyBytes): Promise<void> — same shape, for public-key files.
  • getDataFromStorage(storage, key): Promise<Uint8Array> — convenience wrapper over AbstractStorage.get(key). Raises CryptoVaultError(STORAGE_KEY_NOT_FOUND) when absent (translates whatever the storage backend throws).

Supporting types

  • RootKeysResult, ServerKeysResult (interfaces declaring the return shapes)
  • CreateRootKeysOptions, CreateServerKeysOptions, RollServerKeysOptions, PackVaultOptions (option bags — all optional; defaults for KDF parameters, algorithm choice, derivation path)

New error codes (additive — registered alongside existing CRYPTO_VAULT_* codes)

  • CRYPTO_VAULT_DECRYPT_FAILED — vault decryption failed (wrong password or tampered bytes)
  • CRYPTO_VAULT_PUBLIC_KEY_SIGNATURE_INVALID — viewPublicKeys signature check failed
  • CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND — getDataFromStorage key absent
  • CRYPTO_VAULT_INVALID_VERSION — rollServerKeys received currentVersion < 0

Subpath export

./orchestration/orchestration — new entry in package.json's exports map.

Dependency additions

  • @suaveplan/crypto: workspace:^ (already used; ensure declared) — generateKeychain, encodePublicKeychain, decodePublicKeychain, extractPublicKeys, regime-2 seal/open helpers
  • @suaveplan/storage: workspace:^ (new dep) — AbstractStorage interface for the storage-aware helpers

Out of scope

  • Byte-compat with upstream @suaveplan/keys vault format. Upstream used BitPacker(fullKeychainSchema); genesis uses the v1 derivation grammar. A migration command from old → new format belongs in a separate @suaveplan/keys-migration package.
  • VaultManager event-emitter class (upstream vault/vault.ts 451-LOC orchestrator with open/unlock/lock FSM). Belongs in a follow-up extension; the 9 functions here are the primitives VaultManager would compose if/when it lands.
  • VaultStreams (chunked streaming vault decryption). Belongs in a separate package once a real consumer exists.
  • Hardware-fingerprint binding for keychains. Out of scope (application policy).

Source mapping

Capability Upstream source Notes
Public-API surface (9 functions) ../suaveplan/packages/universal/keys/src/lib/keys-common.ts L1-L469 re-implemented on top of genesis v1 grammar (NOT byte-compat)
Argon2id KDF parameter defaults ../suaveplan/enterprise/keys.md uses existing DEFAULT_ARGON2ID_PARAMS from crypto-vault
CRYPTO_VAULT_* code additions genesis-original matches existing crypto-vault error registry shape

Impact

Affected dependents

@suaveplan/server-keys (in-flight) — this proposal unblocks 7 of its 18 bins. No other current consumers.

Genesis crypto-vault is foundation-tier: changes ripple through its existing consumers (@suaveplan/vault, @suaveplan/crypto-vault itself). The orchestration module is additive — no existing exports change — so dependent tests should not regress. The pre-archive bunx turbo test --filter=...@suaveplan/crypto-vault sweep will confirm.