# 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` 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 }` and `openVault(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` 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` 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 === 4` and 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` and `async packVault(keychain: Keychain, password: string, options?: PackVaultOptions): Promise`. `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 `encodePublicKeychain` output equals `k`'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` 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 by `signerPrivateKey` - **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, key: string, vaultBytes: Uint8Array): Promise`, `async savePublicKeysToStorage(storage: AbstractStorage, key: string, publicKeyBytes: Uint8Array): Promise`, and `async getDataFromStorage(storage: AbstractStorage, key: string): Promise`. `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 then `getDataFromStorage(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-vault` runs - **THEN** the run exits 0 #### Scenario: dependent sweep is clean - **WHEN** `bunx turbo test --filter=...@suaveplan/crypto-vault` runs - **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"`