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 rootKeychainvia@suaveplan/cryptogenerateKeychain, derives an Argon2id KEK frompassword, encrypts the full keychain into vault bytes (AEAD regime-2 secret blob), extracts public keys + canonical bytes viaencodePublicKeychain. Returns{ keychain, vaultBytes, publicKeyBytes, publicKeys }.createServerKeys(rootKeys, serverPassword, options?): Promise<ServerKeysResult>— generates a child keychain derived fromrootKeys.keychain.master.private(HD-key path), encrypts it underserverPassword. Returns{ keychain, vaultBytes, publicKeyBytes, publicKeys, version }.rollServerKeys(rootKeys, currentVersion, serverPassword, options?): Promise<ServerKeysResult>— deterministic re-derivation atversion = currentVersion + 1using 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 frompassword, AEAD-decryptvaultBytesto recover the fullKeychain. Wrong password raisesCryptoVaultError(VAULT_DECRYPT_FAILED); tampered bytes raise the same.packVault(keychain, password, options?): Promise<Uint8Array>— inverse ofopenVault. Used by callers that already have aKeychaininstance (e.g. after a rotation) and need to re-encrypt it.viewPublicKeys(publicKeyBytes, optionalSignKey?): Promise<PublicKeychain>— decode the signed public-key file. IfoptionalSignKeyis provided, verify the embedded signature; mismatch raisesCryptoVaultError(PUBLIC_KEY_SIGNATURE_INVALID).saveVaultToStorage(storage, key, vaultBytes): Promise<void>— convenience wrapper over@suaveplan/storage'sAbstractStorage.put(key, vaultBytes). Idempotent.savePublicKeysToStorage(storage, key, publicKeyBytes): Promise<void>— same shape, for public-key files.getDataFromStorage(storage, key): Promise<Uint8Array>— convenience wrapper overAbstractStorage.get(key). RaisesCryptoVaultError(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—viewPublicKeyssignature check failedCRYPTO_VAULT_STORAGE_KEY_NOT_FOUND—getDataFromStoragekey absentCRYPTO_VAULT_INVALID_VERSION—rollServerKeysreceivedcurrentVersion < 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) —AbstractStorageinterface for the storage-aware helpers
Out of scope
- Byte-compat with upstream
@suaveplan/keysvault format. Upstream usedBitPacker(fullKeychainSchema); genesis uses the v1 derivation grammar. A migration command from old → new format belongs in a separate@suaveplan/keys-migrationpackage. VaultManagerevent-emitter class (upstreamvault/vault.ts451-LOC orchestrator withopen/unlock/lockFSM). Belongs in a follow-up extension; the 9 functions here are the primitivesVaultManagerwould 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.