Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
# 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.

View File

@@ -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 -->

View File

@@ -0,0 +1,56 @@
# Tasks — extend-crypto-vault-orchestration
## 1. Scaffold
- [ ] 1.1 Add `src/orchestration/` directory; new files `orchestration.ts`, `orchestration.test.ts`, `orchestration.md`, `orchestration.types.ts`, `orchestration.spec-conformance.test.ts`, `index.ts`
- [ ] 1.2 Update `package.json` `exports`: add `./orchestration/orchestration` subpath
- [ ] 1.3 Add `@suaveplan/storage: workspace:^` to dependencies (if not already declared)
- [ ] 1.4 Re-run `bun install`
## 2. Error codes (additive — extend existing registry)
- [ ] 2.1 Add `CRYPTO_VAULT_DECRYPT_FAILED`, `CRYPTO_VAULT_PUBLIC_KEY_SIGNATURE_INVALID`, `CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND`, `CRYPTO_VAULT_INVALID_VERSION` to `src/errors/`
- [ ] 2.2 Update the existing `errors/index.ts` to re-export new codes
- [ ] 2.3 Update `src/errors/errors.test.ts` (or equivalent) to cover new code shapes
## 3. Implementation
- [ ] 3.1 `src/orchestration/orchestration.types.ts` — `RootKeysResult`, `ServerKeysResult`, `CreateRootKeysOptions`, `CreateServerKeysOptions`, `RollServerKeysOptions`, `PackVaultOptions`
- [ ] 3.2 `src/orchestration/orchestration.ts` — 9 exported async functions:
- `createRootKeys(password, options?)`
- `createServerKeys(rootKeys, serverPassword, options?)`
- `rollServerKeys(rootKeys, currentVersion, serverPassword, options?)`
- `openVault(vaultBytes, password)`
- `packVault(keychain, password, options?)`
- `viewPublicKeys(publicKeyBytes, optionalSignKey?)`
- `saveVaultToStorage(storage, key, vaultBytes)`
- `savePublicKeysToStorage(storage, key, publicKeyBytes)`
- `getDataFromStorage(storage, key)`
- [ ] 3.3 `src/orchestration/orchestration.md` — ≥200 words, Purpose / Features / Usage:Basic / Usage:Advanced / API Reference / Implementation Notes
- [ ] 3.4 `src/orchestration/index.ts` — named re-exports only
- [ ] 3.5 Update root `src/index.ts` to re-export the orchestration surface by name
## 4. Tests
- [ ] 4.1 `src/orchestration/orchestration.test.ts` covers every scenario in the spec delta (use `@suaveplan/storage`'s in-memory store for the storage-helper tests; real Argon2id for the KDF)
- [ ] 4.2 `src/orchestration/orchestration.spec-conformance.test.ts` pins (a) every new error code thrown from a production path, (b) `expectTypeOf` on every exported function signature
- [ ] 4.3 `tests/dep-graph.test.ts` (or extend existing) asserts the new subpath export is reachable and `orchestration.ts` has a sibling `.md`
- [ ] 4.4 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for Argon2id-heavy tests; use the low-iteration test profile from `DEFAULT_ARGON2ID_PARAMS` for unit tests)
- [ ] 4.5 All tests import runner from `@suaveplan/testing/runner`
## 5. Gates
- [ ] 5.1 `bunx turbo lint typecheck test --filter=@suaveplan/crypto-vault` — green
- [ ] 5.2 `bunx turbo test:coverage --filter=@suaveplan/crypto-vault` — 100/100/100/100
- [ ] 5.3 `bun scripts/openspec-conformance.ts @suaveplan/crypto-vault` — exit 0
- [ ] 5.4 `bunx turbo build --filter=@suaveplan/crypto-vault` — green
- [ ] 5.5 `bunx turbo test --filter=...@suaveplan/crypto-vault` — dependent sweep green (foundation package)
- [ ] 5.6 Banned-pattern scan (0 hits, JSDoc-prose `any` ok)
- [ ] 5.7 No `export *` in `orchestration/index.ts`
- [ ] 5.8 No direct `vitest`/`bun:test` imports
## 6. Archive
- [ ] 6.1 Fold `specs/crypto-vault/spec.md` ADDED Requirements into the canonical `openspec/specs/crypto-vault/spec.md` (append; don't replace)
- [ ] 6.2 Move `openspec/changes/extend-crypto-vault-orchestration/` → `openspec/archive/2026-05-17-extend-crypto-vault-orchestration/`
- [ ] 6.3 Refresh `MANIFEST.md`