first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user