Files
skills/docs/security-stack/crypto/crypto-vault/src/orchestration/orchestration.md
2026-05-29 08:20:56 +01:00

7.0 KiB

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.

(no submodules)

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

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

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.