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/rollServerKeysproducing 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
rollServerKeyswith strictversion ≥ 0validation. - Round-trip
openVault/packVaultwith constant-time wrong-password rejection (no information leak about which of password / integrity failed). viewPublicKeyswith optional Ed25519 signature verification.- Idempotent storage helpers (
saveVaultToStorage,savePublicKeysToStorage,getDataFromStorage) that translate "missing key" outcomes from any backend into a typedCryptoVaultError. - suaveplan-native wire formats — explicitly NOT byte-compatible with the
upstream
@suaveplan/keysBitPacker 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 viagenerateKeychain.options.masterKeylets callers supply a pre-existing 32-byte seed;options.layoutFieldsoverrides the default v1 layout{ masterVersion: 1, userId: 0, depth: 0, slotVersion: 1 };options.argon2idoverrides KDF parameters.createServerKeys(rootKeys, serverPassword, options?)→Promise<ServerKeysResult>. Deterministically derives a child master viaHMAC-SHA512(rootMaster, u32be(version))truncated to 32 bytes. ThrowsCryptoVaultError(CRYPTOVAULT.ORCHESTRATION.INVALID_VERSION)on negative or non-integerversion. Public-key file is signed by the root sign slot.rollServerKeys(rootKeys, currentVersion, serverPassword, options?)→Promise<ServerKeysResult>. Returns a result atversion = currentVersion + 1. Throws the sameINVALID_VERSIONerror on negativecurrentVersion.openVault(vaultBytes, password)→Promise<Keychain>. Splits the wire layout (salt(16) || nonce(24) || ciphertext), re-derives the KEK with Argon2id, and decrypts. ThrowsCryptoVaultError(CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED)on wrong password, short input, or tampered ciphertext.packVault(keychain, password, options?)→Promise<Uint8Array>. Inverse ofopenVault; reusable by callers that already hold aKeychain.viewPublicKeys(publicKeyBytes, optionalSignKey?)→Promise<PublicKeychain>. Splits the trailing 64-byte Ed25519 signature, decodes the body, and (whenoptionalSignKeyis provided) verifies the signature. ThrowsCryptoVaultError(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 overAbstractStorage.set.getDataFromStorage(storage, key)→Promise<Uint8Array>. Translates a missing key (eitherundefinedreturn or a thrown backend error) intoCryptoVaultError(CRYPTOVAULT.ORCHESTRATION.STORAGE_KEY_NOT_FOUND).
Implementation Notes
- The KEK is derived with
deriveKeyWithArgon2idusingDEFAULT_ARGON2ID_PARAMS(OWASP-compliant: 64 MiB memory, 3 iterations, parallelism 4) unless callers override viaoptions.argon2id. - Vault layout: 16-byte Argon2id salt, then a 24-byte XChaCha20-Poly1305
nonce, then ciphertext. Both header values are drawn from
crypto.getRandomValuesso 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. openVaultalways usesDEFAULT_ARGON2ID_PARAMSto 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.viewPublicKeyscollapses 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
Uint8Arraystraight through. Backends with their own size limits (e.g.MemoryStorage'smaxValueSize) surface as their own typed error, which is intentional: the vault layer should not silently mutate the backend's contract.