UNPKG

run402-mcp

Version:

MCP server for Run402 — full-stack backend infrastructure for AI agents: Postgres, auth, storage, serverless functions and atomic deploys. Paid with x402/MPP. Includes $0.03 image generation.

177 lines • 8.13 kB
/** * The Node-native bulk-AEAD backend (vault-native-bulk-crypto, design * D1/D2/D4). * * Every WAL pack, checkpoint pack, `ref_state` and `retention_roots` frame * passes through `sealFrame`/`openFrame`, and on the benchmark vault the 67 MB * checkpoint pack's decrypt is a measured share of a cold clone's local * segment. `@noble/ciphers` is correct everywhere and stays the default; Node * simply has a faster implementation of the SAME construction sitting in * OpenSSL, so this module installs it through the core's backend seam. * * `node:crypto` has no XChaCha20-Poly1305, but the construction is defined in * terms of primitives it does have (D2): * * subkey = HChaCha20(key, nonce[0..16]) * out = ChaCha20-Poly1305-IETF(subkey, iv = 0x00000000 ‖ nonce[16..24]) * * The subkey step is the ONLY missing piece, and `@noble/ciphers` exports * `hchacha` for exactly it — so the key-derivation half stays on the audited * implementation and only the bulk stream+tag work moves to OpenSSL. Output * layout is `ct‖tag`, byte-identical to `@noble`'s * `xchacha20poly1305(...).encrypt` (verified across sizes spanning 0 bytes to * multiple megabytes, and every AAD length, by the byte-equality suite). * * Failure posture (D4): registration is guarded by cipher availability and * try/caught, so an OpenSSL build without `chacha20-poly1305` — or any * construction error — silently leaves the `@noble` path in place. Correct, * just slower. `open` returns `null` on tag failure; the core renders the * SAME `VAULT_AEAD_AUTH_FAILURE` it always did. */ import { createCipheriv, createDecipheriv, createHash, getCiphers } from "node:crypto"; import { hchacha } from "@noble/ciphers/chacha.js"; import { _setVaultAeadBackend, _setVaultHashBackend } from "../namespaces/vault.crypto.js"; const NODE_CIPHER = "chacha20-poly1305"; const TAG_BYTES = 16; /** ChaCha's `"expand 32-byte k"` constant as little-endian u32 words. */ const SIGMA = new Uint32Array([0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]); /** * Little-endian byte↔word conversion done EXPLICITLY rather than through a * `Uint32Array` view of the same buffer: a view is platform-endian, so it * would silently produce a different subkey on a big-endian host — a * wire-format fork by architecture, the one unacceptable outcome (D5). */ function leWords(bytes) { const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); const words = new Uint32Array(bytes.byteLength / 4); for (let i = 0; i < words.length; i += 1) words[i] = view.getUint32(i * 4, true); return words; } function leBytes(words) { const bytes = new Uint8Array(words.length * 4); const view = new DataView(bytes.buffer); for (let i = 0; i < words.length; i += 1) view.setUint32(i * 4, words[i], true); return bytes; } /** HChaCha20(key, nonce[0..16]) → the 32-byte IETF subkey. */ function xchachaSubkey(key32, nonce24) { const out = new Uint32Array(8); hchacha(SIGMA, leWords(key32), leWords(nonce24.subarray(0, 16)), out); return leBytes(out); } /** The IETF 12-byte nonce: four zero bytes, then the extended nonce's tail. */ function ietfIv(nonce24) { const iv = new Uint8Array(12); iv.set(nonce24.subarray(16, 24), 4); return iv; } export const nodeVaultAeadBackend = { seal(key32, nonce24, aad, plaintext) { const cipher = createCipheriv(NODE_CIPHER, xchachaSubkey(key32, nonce24), ietfIv(nonce24), { authTagLength: TAG_BYTES }); // OpenSSL's ChaCha20-Poly1305 needs the plaintext length up front when an // AAD is set; an empty AAD must not be fed at all (it is not the same as // "no AAD" to every OpenSSL build). if (aad.length > 0) cipher.setAAD(aad, { plaintextLength: plaintext.length }); const head = cipher.update(plaintext); const tail = cipher.final(); const tag = cipher.getAuthTag(); const out = new Uint8Array(head.length + tail.length + tag.length); out.set(head, 0); out.set(tail, head.length); out.set(tag, head.length + tail.length); return out; }, open(key32, nonce24, aad, ciphertextAndTag) { if (ciphertextAndTag.length < TAG_BYTES) return null; const split = ciphertextAndTag.length - TAG_BYTES; const decipher = createDecipheriv(NODE_CIPHER, xchachaSubkey(key32, nonce24), ietfIv(nonce24), { authTagLength: TAG_BYTES }); if (aad.length > 0) decipher.setAAD(aad, { plaintextLength: split }); decipher.setAuthTag(ciphertextAndTag.subarray(split)); try { const head = decipher.update(ciphertextAndTag.subarray(0, split)); const tail = decipher.final(); const out = new Uint8Array(head.length + tail.length); out.set(head, 0); out.set(tail, head.length); return out; } catch { // Tag mismatch — indistinguishable from any other authentication // failure, which is exactly the contract. return null; } }, }; /** * Install the native backend when this OpenSSL has the cipher. Returns whether * it took, so a test can assert the guard rather than infer it. Any failure * leaves the `@noble` default untouched — the native path is an optimization, * never a correctness dependency. */ export function installNodeVaultAeadBackend() { try { if (!getCiphers().includes(NODE_CIPHER)) return false; // Prove the construction works on THIS build before letting the bulk path // depend on it: a cipher that is listed but rejects our parameters must // not become a runtime failure on the first real frame. const probe = nodeVaultAeadBackend.seal(new Uint8Array(32), new Uint8Array(24), new Uint8Array([1]), new Uint8Array([2, 3])); const opened = nodeVaultAeadBackend.open(new Uint8Array(32), new Uint8Array(24), new Uint8Array([1]), probe); if (!opened || opened.length !== 2 || opened[0] !== 2 || opened[1] !== 3) return false; _setVaultAeadBackend(nodeVaultAeadBackend); return true; } catch { return false; } } /** * The Node-native protocol-hash backend (vault-native-hash). Same seam * pattern, second slot: `sha256Hex` is the chokepoint every protocol hash * flows through, and on the receipt-verified open/seal paths it hashes whole * multi-megabyte frames on pure-JS `@noble/hashes` — the measured leftover * after the AEAD moved to OpenSSL. `createHash` is sync (the vault crypto * surface is sync public API), byte-compatible, and already hashes packs in * `vault-recover.ts`/`vault-mirror.ts`. */ export const nodeVaultHashBackend = { sha256(bytes) { // A digest Buffer can live in Node's shared pool — respect its offset. const d = createHash("sha256").update(bytes).digest(); return new Uint8Array(d.buffer, d.byteOffset, d.byteLength); }, }; /** * SHA-256 of the empty input — the fixed probe vector. A build whose * `createHash` does not reproduce it must not become the protocol hash. */ const SHA256_EMPTY_HEX = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; /** * Install the native hash backend after a live probe (vault-native-hash * design D3): a backend that does not reproduce the known digest — or throws — * registers nothing and the `@noble` default keeps serving. The `backend` * parameter exists only so the byte-equality suite can drive the refusing * branch; production callers pass nothing. */ export function installNodeVaultHashBackend(backend = nodeVaultHashBackend) { try { const probe = backend.sha256(new Uint8Array(0)); let hex = ""; for (const b of probe) hex += b.toString(16).padStart(2, "0"); if (hex !== SHA256_EMPTY_HEX) return false; _setVaultHashBackend(backend); return true; } catch { return false; } } //# sourceMappingURL=vault-native-crypto.js.map