Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
# orchestration
High-level orchestration helpers that bundle keychain generation, password-AEAD
encryption, signed public-key file production, and storage I/O behind a single
call surface.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`@suaveplan/crypto-vault` already ships rich low-level primitives — BIP39
mnemonics, Argon2id KDF, hierarchical key derivation, SecureBuffer, SRP — but
no opinionated "create a root keychain plus encrypted vault plus signed
public-key file" entrypoint. The orchestration module fills that gap.
It composes `@suaveplan/crypto`'s `generateKeychain` / `encodeKeychain` /
`encodePublicKeychain` primitives with this package's
`deriveKeyWithArgon2id` KEK derivation and the Ed25519 signing surface from
`@suaveplan/crypto-curve`, producing the single-call surface used by
`@suaveplan/server-keys` operator bins (`create-root-keys`, `create-server-keys`,
`roll-server-keys`, `view-keys`, `view-vault`, `view-public-key-file`, etc.).
## Features
- One-call `createRootKeys` / `createServerKeys` / `rollServerKeys` producing
keychain + encrypted vault + signed public-key file in one trip.
- Deterministic server-key derivation: `(rootMaster, version)` reproduces the
same private material across hosts so rotation never loses key continuity.
- Versioned rotation via `rollServerKeys` with strict `version ≥ 0` validation.
- Round-trip `openVault` / `packVault` with constant-time wrong-password
rejection (no information leak about which of password / integrity failed).
- `viewPublicKeys` with optional Ed25519 signature verification.
- Idempotent storage helpers (`saveVaultToStorage`,
`savePublicKeysToStorage`, `getDataFromStorage`) that translate "missing key"
outcomes from any backend into a typed `CryptoVaultError`.
- suaveplan-native wire formats — explicitly NOT byte-compatible with the
upstream `@suaveplan/keys` BitPacker format; rotation is an offline migration.
## Usage: Basic
```ts
import {
createRootKeys,
openVault,
saveVaultToStorage,
} from "@suaveplan/crypto-vault";
import { MemoryStorage } from "@suaveplan/storage";
const storage = new MemoryStorage({ cleanupIntervalMs: 0 });
const root = await createRootKeys("correct horse battery staple");
await saveVaultToStorage(storage, "root.vault", root.vaultBytes);
const recovered = await openVault(root.vaultBytes, "correct horse battery staple");
// Eight default-purpose slots: "mast" (master), "sign", "encr", "kx ",
// "salt", "hash", "strm", "dev " — keyed by the 4-byte purpose tag.
console.log(recovered["mast"].publicKey.length); // 32
```
## Usage: Advanced
```ts
import {
createRootKeys,
createServerKeys,
rollServerKeys,
viewPublicKeys,
} from "@suaveplan/crypto-vault";
const root = await createRootKeys("root-pw");
// Provision a server keychain at version 1 (default)
const server1 = await createServerKeys(root, "server-pw");
// Roll the server keychain forward — deterministically derives a fresh master.
const server2 = await rollServerKeys(root, server1.version, "server-pw");
// Verify the server's signed public-key file using the root sign pubkey.
const decoded = await viewPublicKeys(
server2.publicKeyBytes,
root.keychain["sign"].publicKey
);
console.log(decoded["sign"].publicKey === server2.publicKeys["sign"].publicKey);
```
## API Reference
- `createRootKeys(password, options?)` → `Promise<RootKeysResult>`. Generates a
fresh root keychain via `generateKeychain`. `options.masterKey` lets callers
supply a pre-existing 32-byte seed; `options.layoutFields` overrides the
default v1 layout `{ masterVersion: 1, userId: 0, depth: 0, slotVersion: 1 }`;
`options.argon2id` overrides KDF parameters.
- `createServerKeys(rootKeys, serverPassword, options?)` →
`Promise<ServerKeysResult>`. Deterministically derives a child master via
`HMAC-SHA512(rootMaster, u32be(version))` truncated to 32 bytes. Throws
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.INVALID_VERSION)` on negative or
non-integer `version`. Public-key file is signed by the *root* sign slot.
- `rollServerKeys(rootKeys, currentVersion, serverPassword, options?)` →
`Promise<ServerKeysResult>`. Returns a result at `version = currentVersion + 1`.
Throws the same `INVALID_VERSION` error on negative `currentVersion`.
- `openVault(vaultBytes, password)` → `Promise<Keychain>`. Splits the wire
layout (`salt(16) || nonce(24) || ciphertext`), re-derives the KEK with
Argon2id, and decrypts. Throws
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED)` on wrong
password, short input, or tampered ciphertext.
- `packVault(keychain, password, options?)` → `Promise<Uint8Array>`. Inverse of
`openVault`; reusable by callers that already hold a `Keychain`.
- `viewPublicKeys(publicKeyBytes, optionalSignKey?)` → `Promise<PublicKeychain>`.
Splits the trailing 64-byte Ed25519 signature, decodes the body, and (when
`optionalSignKey` is provided) verifies the signature. Throws
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.PUBLIC_KEY_SIGNATURE_INVALID)` on
short input or signature mismatch.
- `saveVaultToStorage(storage, key, vaultBytes)` /
`savePublicKeysToStorage(storage, key, publicKeyBytes)` → `Promise<void>`.
Thin idempotent wrappers over `AbstractStorage.set`.
- `getDataFromStorage(storage, key)` → `Promise<Uint8Array>`. Translates a
missing key (either `undefined` return or a thrown backend error) into
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.STORAGE_KEY_NOT_FOUND)`.
## Implementation Notes
- The KEK is derived with `deriveKeyWithArgon2id` using
`DEFAULT_ARGON2ID_PARAMS` (OWASP-compliant: 64 MiB memory, 3 iterations,
parallelism 4) unless callers override via `options.argon2id`.
- Vault layout: 16-byte Argon2id salt, then a 24-byte XChaCha20-Poly1305
nonce, then ciphertext. Both header values are drawn from
`crypto.getRandomValues` so two encryptions of the same plaintext under the
same password share no bytes.
- Public-key file layout: `encodePublicKeychain(extractPublicKeys(keychain))`
followed by a 64-byte Ed25519 signature. For root keychains the keychain
self-signs; for server keychains the root's sign slot signs the child's
public bytes so a verifier holding only the root sign pubkey can attest the
full chain.
- `openVault` always uses `DEFAULT_ARGON2ID_PARAMS` to re-derive the KEK; the
salt is embedded in the vault header but the params themselves are not — the
trade-off keeps the format flat at the cost of locking the on-disk profile to
the package default. Future overrides will append a versioned header.
- `viewPublicKeys` collapses every signature failure into a single error code
to avoid leaking which check failed first; tests rely on that property when
asserting the conformance behaviour.
- Storage helpers do not introspect the value — they pass `Uint8Array` straight
through. Backends with their own size limits (e.g. `MemoryStorage`'s
`maxValueSize`) surface as their own typed error, which is intentional: the
vault layer should not silently mutate the backend's contract.