Initial Commit
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<!-- markdownlint-disable MD024 -->
|
||||
|
||||
# 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 }` 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<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 === 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<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 `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<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 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<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 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"`
|
||||
|
||||
<!-- markdownlint-enable MD024 -->
|
||||
Reference in New Issue
Block a user