9.0 KiB
Capability Spec Delta: crypto-vault (extend-crypto-vault-orchestration)
Package: @suaveplan/crypto-vault
Status: PROPOSED
ADDED Requirements
Requirement: createRootKeys produces a keychain + encrypted vault + signed public-key bytes
The package SHALL export async createRootKeys(password: string, options?: CreateRootKeysOptions): Promise<RootKeysResult> that generates a fresh root Keychain via @suaveplan/crypto generateKeychain, derives an Argon2id KEK from password (defaults from DEFAULT_ARGON2ID_PARAMS), AEAD-encrypts the canonical-encoded keychain bytes into vault bytes, extracts the public keychain via extractPublicKeys, and returns { keychain, vaultBytes, publicKeyBytes, publicKeys }. The publicKeyBytes SHALL be the output of encodePublicKeychain (which carries the master public key + per-slot algorithm tags). On success, openVault(vaultBytes, password) SHALL recover an equivalent keychain.
Scenario: result round-trips through openVault
- WHEN
createRootKeys("hunter2")returns{ vaultBytes }andopenVault(vaultBytes, "hunter2")is then awaited - THEN the returned keychain has the same master public key as
result.keychain.master.public
Verified by: src/orchestration/orchestration.test.ts::"createRootKeys output round-trips through openVault"
Requirement: createServerKeys derives a versioned child keychain from root master
The package SHALL export async createServerKeys(rootKeys: RootKeysResult, serverPassword: string, options?: CreateServerKeysOptions): Promise<ServerKeysResult> that derives a server Keychain from rootKeys.keychain.master.private (deterministic HD-key derivation at the default path), encrypts under serverPassword, and returns { keychain, vaultBytes, publicKeyBytes, publicKeys, version }. Default version SHALL be 1. Re-running with the same (rootKeys, password, version) SHALL produce keychains with identical public keys (deterministic derivation).
Scenario: deterministic re-derivation
- WHEN
createServerKeys(rootKeys, "pw")is called twice with the same root + password - THEN both results have identical
publicKeys(private material differs only by nonce in the encrypted vault layer)
Verified by: src/orchestration/orchestration.test.ts::"createServerKeys deterministically derives from root master"
Requirement: rollServerKeys bumps the version deterministically
The package SHALL export async rollServerKeys(rootKeys: RootKeysResult, currentVersion: number, serverPassword: string, options?: RollServerKeysOptions): Promise<ServerKeysResult> returning a ServerKeysResult at version = currentVersion + 1. currentVersion < 0 SHALL raise CryptoVaultError(CRYPTO_VAULT_INVALID_VERSION). The returned keychain SHALL be reproducible from (rootKeys, currentVersion, password).
Scenario: version increments
- WHEN
rollServerKeys(root, 3, "pw")is awaited - THEN
result.version === 4and the underlying keychain differs from one derived at version 3
Scenario: negative version rejected
- WHEN
rollServerKeys(root, -1, "pw")is awaited - THEN the promise rejects with
CryptoVaultError(CRYPTO_VAULT_INVALID_VERSION)
Verified by: src/orchestration/orchestration.test.ts::"rollServerKeys increments version" AND src/orchestration/orchestration.test.ts::"rollServerKeys rejects negative currentVersion"
Requirement: openVault and packVault are inverses
The package SHALL export async openVault(vaultBytes: Uint8Array, password: string): Promise<Keychain> and async packVault(keychain: Keychain, password: string, options?: PackVaultOptions): Promise<Uint8Array>. openVault(packVault(k, p), p) SHALL produce a keychain with the same canonical-encoded public bytes as k. Wrong password or tampered bytes SHALL raise CryptoVaultError(CRYPTO_VAULT_DECRYPT_FAILED).
Scenario: round-trip
- WHEN
openVault(await packVault(k, "pw"), "pw")is awaited - THEN the result's
encodePublicKeychainoutput equalsk's
Scenario: wrong password rejected
- WHEN
openVault(await packVault(k, "right"), "wrong")is awaited - THEN the promise rejects with
CryptoVaultError(CRYPTO_VAULT_DECRYPT_FAILED)
Scenario: tampered bytes rejected
- WHEN any byte of a valid vault is flipped and
openVault(tampered, password)is awaited - THEN the promise rejects with
CryptoVaultError(CRYPTO_VAULT_DECRYPT_FAILED)
Verified by: src/orchestration/orchestration.test.ts::"openVault and packVault round-trip" AND src/orchestration/orchestration.test.ts::"openVault rejects wrong password" AND src/orchestration/orchestration.test.ts::"openVault rejects tampered bytes"
Requirement: viewPublicKeys decodes a signed public-key file with optional verification
The package SHALL export async viewPublicKeys(publicKeyBytes: Uint8Array, optionalSignKey?: PublicKey): Promise<PublicKeychain> that decodes via decodePublicKeychain. When optionalSignKey is provided, the embedded signature SHALL be verified against it; verification failure SHALL raise CryptoVaultError(CRYPTO_VAULT_PUBLIC_KEY_SIGNATURE_INVALID). Without optionalSignKey, decoding succeeds without verification.
Scenario: verification succeeds with matching key
- WHEN
viewPublicKeys(bytes, signerPublicKey)is awaited for bytes signed bysignerPrivateKey - THEN the call resolves with the decoded
PublicKeychain
Scenario: verification fails with wrong key
- WHEN
viewPublicKeys(bytes, wrongPublicKey)is awaited - THEN the promise rejects with
CryptoVaultError(CRYPTO_VAULT_PUBLIC_KEY_SIGNATURE_INVALID)
Verified by: src/orchestration/orchestration.test.ts::"viewPublicKeys verifies signature when sign key supplied" AND src/orchestration/orchestration.test.ts::"viewPublicKeys rejects wrong sign key"
Requirement: Storage helpers wrap AbstractStorage idempotently
The package SHALL export async saveVaultToStorage(storage: AbstractStorage<Uint8Array>, key: string, vaultBytes: Uint8Array): Promise<void>, async savePublicKeysToStorage(storage: AbstractStorage<Uint8Array>, key: string, publicKeyBytes: Uint8Array): Promise<void>, and async getDataFromStorage(storage: AbstractStorage<Uint8Array>, key: string): Promise<Uint8Array>. getDataFromStorage SHALL raise CryptoVaultError(CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND) when the key is absent (translating whatever the backend throws). Save helpers SHALL be idempotent — calling with the same key + bytes twice SHALL not raise.
Scenario: save then get returns the same bytes
- WHEN
saveVaultToStorage(memStorage, "k", bytes)is awaited thengetDataFromStorage(memStorage, "k")is awaited - THEN the result equals
bytes
Scenario: missing key raises typed error
- WHEN
getDataFromStorage(memStorage, "absent")is awaited - THEN the promise rejects with
CryptoVaultError(CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND)
Scenario: double-save is idempotent
- WHEN
saveVaultToStorage(memStorage, "k", bytes)is awaited twice - THEN both awaits resolve without error
Verified by: src/orchestration/orchestration.test.ts::"saveVaultToStorage + getDataFromStorage round-trip" AND src/orchestration/orchestration.test.ts::"getDataFromStorage raises CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND on missing key" AND src/orchestration/orchestration.test.ts::"saveVaultToStorage is idempotent"
Requirement: New error codes registered without breaking existing ones
The package SHALL extend CRYPTO_VAULT_CODES with CRYPTO_VAULT_DECRYPT_FAILED, CRYPTO_VAULT_PUBLIC_KEY_SIGNATURE_INVALID, CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND, CRYPTO_VAULT_INVALID_VERSION. Every existing code SHALL remain registered with identical value. Every new code SHALL be raised by at least one production call site in src/orchestration/.
Scenario: new codes thrown from production paths
- WHEN the conformance test exercises each new code's production path
- THEN every new
CRYPTO_VAULT_*code is raised at least once
Verified by: src/orchestration/orchestration.spec-conformance.test.ts::"every new orchestration code is thrown from a production path"
Requirement: Subpath export, sibling docs, no breaking change to existing surface
The package SHALL declare a new subpath export ./orchestration/orchestration in package.json. The new src/orchestration/orchestration.ts SHALL have a sibling .md of ≥200 words. Existing exports SHALL be unchanged. Existing consumers (@suaveplan/vault, @suaveplan/crypto-vault's own tests) SHALL pass without modification.
Scenario: existing crypto-vault tests still pass
- WHEN
bunx turbo test --filter=@suaveplan/crypto-vaultruns - THEN the run exits 0
Scenario: dependent sweep is clean
- WHEN
bunx turbo test --filter=...@suaveplan/crypto-vaultruns - THEN the run exits 0
Verified by: tests/dep-graph.test.ts::"orchestration subpath export is reachable" AND tests/dep-graph.test.ts::"orchestration.ts has a sibling .md"