Crypto
The framework's PQC-first cryptography surface. Every default is post-quantum-aware: ML-KEM-1024 + ECDH P-384 hybrid for key encapsulation (FIPS 203 + classical defense-in-depth), XChaCha20- Poly1305 for authenticated symmetric encryption (24-byte nonce — no nonce-reuse risk under high volume), SHAKE256 as the KDF (FIPS 202 XOF — arbitrary output length), SHA3-512 for hashing, HMAC-SHA3-512 for keyed integrity, and ML-DSA-87 / SLH-DSA-SHAKE- 256f for signatures (auto-detected from the key PEM). Argon2id passphrase stretching lives in b.vaultWrap, not here.
Envelope wire format (length-prefixed, self-describing):
byte 0 : ENVELOPE_MAGIC byte 1 : KEM ID (ML_KEM_1024 / ML_KEM_1024_P384 / ML_KEM_768_X25519) byte 2 : CIPHER ID (XCHACHA20_POLY1305) byte 3 : KDF ID (SHAKE256) ... : KEM ciphertext, ephemeral ECDH pubkey, nonce, AEAD ciphertext
The four-byte header is bound as AEAD AAD so an algorithm- substitution attack (a tampered byte-1 KEM ID, byte-2 cipher ID, etc.) fails Poly1305 verification. Old envelopes decrypt under the IDs written into their header; new writes use the active suite. The KDF additionally absorbs a NIST SP 800-56C r2 §4.1 FixedInfo suite-binding label so a key derived under one suite is not silently usable under another.
Three KEM hybrids ship: ML-KEM-1024 KEM-only (legacy single- component), ML-KEM-1024 + ECDH P-384 (framework default), and ML-KEM-768 + X25519 (IETF / Cloudflare / Chrome TLS 1.3 codepoint 0x11EC — smaller payload, wider browser interop).
SHA-1 / SHA-256 / AES-GCM / classical-only ECDH are intentionally absent from the public surface. Operators who genuinely need them call node:crypto directly so the choice surfaces in their code.
b.crypto.hpke.pq.connolly.seal(opts) #
{
recipientPubKey: string, // ML-KEM-1024 PEM
plaintext: Buffer|string,
info: Buffer|string, // application context
aad: Buffer|string, // additional authenticated data
}
Seal a payload under draft-connolly-cfrg-hpke-mlkem-04 codepoints. Returns { enc, ciphertext }; the framework's existing b.crypto.hpke.seal semantics apply (ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 per project policy). Opens ONLY via b.crypto.hpke.pq.connolly.open — cross-draft substitution into b.crypto.hpke.pq.wg.open refuses by construction.
var pair = b.crypto.hpke.generateKeyPair();
var sealed = b.crypto.hpke.pq.connolly.seal({
recipientPubKey: pair.publicKey,
plaintext: "hello",
info: "app/topic",
});
b.crypto.hpke.pq.connolly.open(opts) #
{
privateKey: string, // ML-KEM-1024 PEM
enc: Buffer,
ciphertext: Buffer,
info: Buffer|string,
aad: Buffer|string,
}
Open a draft-connolly-cfrg-hpke-mlkem-04 envelope produced by connolly.seal. Refuses envelopes sealed under wg.seal (the info-label binding catches cross-draft substitution).
var pt = b.crypto.hpke.pq.connolly.open({
privateKey: pair.privateKey, enc: sealed.enc,
ciphertext: sealed.ciphertext, info: "app/topic",
});
b.crypto.hpke.pq.wg.seal(opts) #
{
recipientPubKey: string, // ML-KEM-1024 PEM
plaintext: Buffer|string,
info: Buffer|string,
aad: Buffer|string,
}
Seal under draft-ietf-hpke-pq-03 codepoints (the WG-adopted PQ-HPKE draft). Otherwise identical contract to b.crypto.hpke.pq.connolly.seal.
var sealed = b.crypto.hpke.pq.wg.seal({
recipientPubKey: pair.publicKey,
plaintext: "hello",
});
b.crypto.hpke.pq.wg.open(opts) #
{
privateKey: string,
enc: Buffer,
ciphertext: Buffer,
info: Buffer|string,
aad: Buffer|string,
}
Open a draft-ietf-hpke-pq-03 envelope produced by wg.seal.
var pt = b.crypto.hpke.pq.wg.open({
privateKey: pair.privateKey, enc: sealed.enc,
ciphertext: sealed.ciphertext,
});
b.crypto.oprf.suite(name) #
Return the RFC 9497 OPRF suite for name — one of "ristretto255-sha512", "p256-sha256", "p384-sha384", or "p521-sha512" (case insensitive). The result is { name, oprf, voprf }; each mode object has the protocol functions:
deriveKeyPair(seed, info)/generateKeyPair()→{ secretKey, publicKey }blind(input)→{ blind, blinded }(client)oprf.blindEvaluate(secretKey, blinded)→ evaluation element;voprf.blindEvaluate(secretKey, publicKey, blinded)→{ evaluated, proof }(server)oprf.finalize(input, blind, evaluation)/voprf.finalize(input, blind, evaluated, blinded, publicKey, proof)→ output bytes (client;voprfverifies the proof and throws if it does not matchpublicKey)evaluate(secretKey, input)→ output bytes (server-side, non-oblivious — equals the client'sfinalizeoutput)
The partially-oblivious poprf mode is intentionally absent (not implemented by the vendored @noble/curves). Throws OprfError for an unknown suite name.
var s = b.crypto.oprf.suite("ristretto255-sha512");
var kp = s.oprf.deriveKeyPair(seed, Buffer.from("my-app"));
var c = s.oprf.blind(Buffer.from("user@example.com")); // client
var ev = s.oprf.blindEvaluate(kp.secretKey, c.blinded); // server
var out = s.oprf.finalize(Buffer.from("user@example.com"), c.blind, ev);
// out === s.oprf.evaluate(kp.secretKey, Buffer.from("user@example.com"))
b.crypto.xwing.combiner(ssM, ssX, ctX, pkX) #
The X-Wing combiner: SHA3-256(ssM ‖ ssX ‖ ctX ‖ pkX ‖ label), where the label is the fixed six bytes the draft defines. Exposed for advanced use and known-answer testing; encapsulate and decapsulate call it internally. Each input must be 32 bytes.
var ss = b.crypto.xwing.combiner(ssMlkem, ssX25519, ephPub, recipientPub);
// → 32-byte shared secret
b.crypto.xwing.keygen(seed?) #
Generate an X-Wing keypair. The decapsulation key is a 32-byte seed (store this); the encapsulation key is the 1216-byte public key to publish. Pass a 32-byte seed for deterministic generation, or omit it for a random key.
var kp = b.crypto.xwing.keygen();
kp.publicKey.length; // → 1216
kp.secretKey.length; // → 32 (the seed — keep it secret)
b.crypto.xwing.encapsulate(publicKey, eseed?) #
Encapsulate to a 1216-byte X-Wing public key. Returns the 1120-byte ciphertext to send and the 32-byte sharedSecret to key a symmetric cipher with. Pass a 64-byte eseed (X25519 ephemeral scalar ‖ ML-KEM coins) for deterministic encapsulation, or omit it for fresh randomness.
var enc = b.crypto.xwing.encapsulate(recipientPublicKey);
enc.ciphertext.length; // → 1120
enc.sharedSecret.length; // → 32
b.crypto.xwing.decapsulate(secretKey, ciphertext) #
Recover the 32-byte shared secret from a 1120-byte X-Wing ciphertext using the 32-byte decapsulation seed. ML-KEM-768's implicit-rejection means a tampered ciphertext yields a different (still 32-byte) secret rather than an error, so never branch on success — derive keys and let the AEAD tag fail.
var ss = b.crypto.xwing.decapsulate(kp.secretKey, enc.ciphertext);
ss.equals(enc.sharedSecret); // → true
b.crypto.hmac(key, data, algorithm?) #
Lowercase-hex HMAC of data keyed by key. The algorithm defaults to the framework's PQC-first SHA3-512 — call b.crypto.hmac(key, data) for keyed integrity (webhook signatures, request-auth tags, audit-chain links) and it is strong by default. Pass an explicit weaker algorithm ONLY to interop with an external scheme that fixes it — e.g. Stripe / Tailscale webhook signatures require "sha256". The algorithm is validated against a SHA-2 / SHA-3 allowlist, so a typo or a broken choice (SHA-1 / MD5) throws at the entry tier rather than silently signing under a surprise hash. key and data accept a Buffer or string. Compare tags with b.crypto.timingSafeEqual, never ==.
var tag = b.crypto.hmac("shared-secret", "POST /webhook|123");
// → SHA3-512 HMAC (128 hex chars) — the PQC-first default
var stripe = b.crypto.hmac(process.env.WHSEC, ts + "." + rawBody, "sha256");
// → HMAC-SHA256 (64 hex chars) — explicit opt-down for external interop
b.crypto.hashStream(readable, algorithm) #
Streams a Readable through createHash(algorithm) and resolves with the raw digest Buffer. Default algorithm is SHA3-512. Algorithm is validated against the allowlist (sha3-256 / sha3-384 / sha3-512 / sha512 / shake256) so a typo or weak choice throws at config time rather than producing a digest under a surprise algorithm. Read- only — no audit emit.
var fs = require("fs");
var stream = fs.createReadStream("/etc/hosts");
b.crypto.hashStream(stream, "sha3-512").then(function (digest) {
digest.toString("hex");
// → "abcd0123...e8f9" (128 hex chars, SHA3-512 = 64 bytes)
});
b.crypto.hashFile(filePath, algorithm) #
Opens filePath as a Readable and streams it through hashStream. Resolves with the raw digest Buffer. Default algorithm is SHA3-512. Read-only — no audit emit; the path is operator-supplied and the digest is the only observable side-effect.
b.crypto.hashFile("/etc/hosts", "sha3-256").then(function (digest) {
digest.toString("hex");
// → "0123abcd...ef89" (64 hex chars, SHA3-256 = 32 bytes)
});
b.crypto.hashFilesParallel(filePaths, opts?) #
{
algorithms?: string[], // default ["sha256", "sha3-512"]; any node:crypto-known digest
concurrency?: number, // default min(8, filePaths.length); 1..256
onProgress?: function (completed, total) // best-effort; thrown errors swallowed
maxBytesPerFile?: number, // default C.BYTES.gib(1) — DoS cap; oversized inputs reject
followSymlinks?: boolean, // default false — refuse symlinks unless explicitly opted in
}
Hash many files in parallel, streaming each one through one or more digest algorithms in a single read pass. Returns an array of { path, byteLength, sha256, sha3_512, ... } records in the same order as filePaths. Concurrency is operator-tunable; the default (min(8, filePaths.length)) matches the framework's hash-while-streaming convention elsewhere without saturating the fs read queue on spinning-disk hosts.
The common consumer-side reason to reach for this primitive is SBOM regeneration / vendor-data integrity sweeps / release-asset bundling — situations where N files each need both SHA-256 (legacy compat) and SHA-3-512 (PQC-first) digests and rolling a worker pool by hand means the same two-loop, capture-N-promises, settle-Q boilerplate every release.
var rows = await b.crypto.hashFilesParallel(
["/var/lib/blamejs/asset-a.bin",
"/var/lib/blamejs/asset-b.bin"],
{ algorithms: ["sha256", "sha3-512"], concurrency: 4 }
);
// rows[0] → { path: "...asset-a.bin", byteLength: 4096,
// sha256: "...", sha3_512: "..." }
b.crypto.randomInt(min, max) #
Cryptographically-secure uniform integer in [min, max). Substrate wrapper that routes every framework integer draw (DNS query-ID, DMARC pct sampling, retry jitter) through one greppable primitive. Both bounds must be safe integers, max > min, and the half-open span must not exceed 2^48 (the underlying runtime's hard cap).
var n = b.crypto.randomInt(0, 100);
// → integer in [0, 100)
b.crypto.timingSafeEqual(a, b) #
Constant-time equality comparison. Accepts only Buffer or string inputs — non-string non-Buffer arguments throw at the entry tier so a Object.prototype.toString-poisoned caller can't redirect the compare through arbitrary attacker-controlled bytes. Returns false immediately when lengths differ (length itself is not a secret), then routes equal-length inputs through crypto.timingSafeEqual. Use when comparing HMAC digests, session tokens, password-reset codes, or any attacker-influenced value where a timing oracle would leak bits.
var expected = b.crypto.hmac("server-key", "payload");
var supplied = "ab12...e9"; // from request header / body
var ok = b.crypto.timingSafeEqual(supplied, expected);
// → true when bytes match, false otherwise (no early exit on mismatch)
b.crypto.sha3Hash(data) #
Returns the lowercase-hex SHA3-512 digest of the input. SHA3-512 is the framework's default hash — collision-resistant, sponge-based, and PQC-aligned (no quantum speedup beyond Grover's). Suitable for content fingerprints, integrity checks, derived-column inputs, and Merkle-tree leaves.
var digest = b.crypto.sha3Hash("hello world");
// → "75d527c368f2efe848ecf6b073a36767800805e9eef2b1857d5f984f036eb6df..."
b.crypto.kdf(input, outputLength) #
SHAKE256-based key derivation. Returns a Buffer of exactly outputLength bytes derived from input. SHAKE256 is an XOF (extendable-output function) — arbitrary output length without the truncation pitfalls of fixed-width SHA3 + slice. Used internally for envelope symmetric-key derivation; operators reach for it when they need application-specific subkeys with explicit length.
var seed = Buffer.from("master-secret|session-42", "utf8");
var subkey = b.crypto.kdf(seed, 32);
subkey.length;
// → 32 (32-byte XChaCha20 key)
b.crypto.namespaceHash(prefix, value, opts) #
{
reserved: object, // accepted but ignored — reserved for a future algorithm-selection knob
}
App-namespaced indexable SHA3-512 hash for derived-hash columns (emailHash, certFpHash, externalIdHash). Returns lowercase hex — stable, indexable column values across every supported database. Centralizes prefix-shape validation: NUL / CR / LF in prefix are refused outright, and prefix is bounded to 64 UTF-8 bytes so an operator can't smuggle log-injection or oversized labels into derived-column inputs. Use when the goal is exact-match lookup, NOT credential storage — for password-style storage use b.credentialHash.hash.
var emailHash = b.crypto.namespaceHash("email", "alice@example.com");
// → "1f3a...c08d" (128 hex chars, SHA3-512 of "email:alice@example.com")
var certFpHash = b.crypto.namespaceHash("cert-fp", Buffer.from([1, 2, 3, 4]));
// Buffer/Uint8Array values are coerced to UTF-8 string before hashing.
b.crypto.generateBytes(byteLength) #
Cryptographically secure random Buffer of length byteLength (default 32). The bytes are SHAKE256(OS-RNG bytes) — defense-in- depth over crypto.randomBytes so a hypothetical OS-RNG weakness is not directly observable downstream. Use for session IDs, KDF salts, AEAD nonces, anything requiring unpredictable bytes.
var sessionId = b.crypto.generateBytes(16).toString("hex");
// → "5b8f2a4c7d1e9f0b3c6a8d2e4f7c1b5d" (32 hex chars, 16 random bytes)
var nonce = b.crypto.generateBytes(24); // XChaCha20-Poly1305 nonce
nonce.length;
// → 24
b.crypto.generateToken(byteLength) #
Hex-encoded random token. Same entropy source as generateBytes (SHAKE256 over OS-RNG bytes) but returned as a lowercase hex string — convenient for HTTP headers, URL parameters, log fields, or any context where a Buffer would need to be encoded anyway. Default byteLength is 32 (64 hex chars, ~256 bits of entropy).
var token = b.crypto.generateToken();
token.length;
// → 64 (32 bytes hex-encoded)
var shortId = b.crypto.generateToken(8);
// → "a3f9...b1" (16 hex chars, 8 random bytes)
b.crypto.toBase64Url(buf) #
RFC 4648 §5 base64url-encode a Buffer / Uint8Array / string. Routes through Node's built-in "base64url" encoding rather than the historical inline .toString("base64").replace(/=+$/, "").replace(/\+/g, "-").replace(/\//g, "_") pattern. Without this helper, every JWS / JWT / DPoP / WebAuthn / DNS-base64url / pagination-cursor / GCS-signed-URL call site reinvented the same three-replace pipeline — and the trailing =+$ regex is polynomial-ReDoS-vulnerable per CodeQL js/polynomial-redos. Node's built-in encoder is linear time, no regex, no backtracking surface.
Input shape: Buffer / Uint8Array → encoded; string → treated as UTF-8 bytes then encoded.
b.crypto.toBase64Url(Buffer.from("hello"));
// → "aGVsbG8"
b.crypto.toBase64Url("hello");
// → "aGVsbG8"
b.crypto.fromBase64Url(s, opts?) #
{
strict: boolean // default: true — refuse non-canonical input
}
RFC 4648 §5 base64url-decode a string into a Buffer. Inverse of toBase64Url. Operators previously reached for Buffer.from(s, "base64url") directly; this wrapper validates the input is a string + provides a single grep-able call site for the round-trip pair.
Strict mode (default) refuses non-canonical input — chars outside the RFC 4648 §5 alphabet, length-mod-4-of-1, mixed +/ from standard base64, trailing garbage. Defends the CWE-347 / CWE-1286 signature-canonicalization footgun where a permissive base64url decoder silently tolerates a tampered JWS / JWT signature (non-canonical bytes decoding to the same buffer). Operators with a documented lossy legacy payload opt out per call via { strict: false }.
var buf = b.crypto.fromBase64Url("aGVsbG8");
buf.toString("utf8");
// → "hello"
b.crypto.makeBase64UrlDecoder(opts) #
{
errorClass: Function, // required — (code, message) error constructor
code: string, // required — error code for both throws
badMessage: string, // required — message on a decode failure
typeMessage: string, // optional — message on a non-string input
}
Bind the strict fromBase64Url decoder to one module's error contract, returning a decode(s) that translates the decoder's TypeError (bad type or non-canonical input) into the caller's typed error. Every base64url-carrying surface — JWT segments, DPoP / OAuth tokens, status lists, pagination cursors — wrapped fromBase64Url in the same if (typeof s !== "string") throw new XError(...); try { return fromBase64Url(s); } catch { throw new XError(...); } shape, differing only in error class, code, and the two messages; this owns that binding.
opts.badMessage is thrown on any decode failure (non-canonical / malformed input — the strict decoder rejects the CWE-347 signature- canonicalization footgun). opts.typeMessage is optional: when set, a non-string input throws it directly (the common case names the field — "cursor must be a string"); when omitted, a non-string falls through to badMessage like any other decode failure.
var b = require("blamejs");
function JwtError(code, message) { this.code = code; this.message = message; }
var decodeSeg = b.crypto.makeBase64UrlDecoder({
errorClass: JwtError,
code: "jwt/malformed",
typeMessage: "expected base64url string",
badMessage: "JWT segment is not valid base64url",
});
decodeSeg("aGVsbG8"); // →
// decodeSeg("!!!") throws JwtError("jwt/malformed", "JWT segment is not valid base64url")
b.crypto.sri(content, opts) #
{
algorithm: string, // "sha256" | "sha384" | "sha512" — default "sha384"
}
Computes a W3C Subresource Integrity 1.0 attribute string — sha###-base64 — that operators paste into or tags. Defends against CDN compromise and ISP MITM injection: the browser refuses to load the resource when its computed hash diverges from the integrity attribute. SRI 1.0 §3.2 supports sha256 / sha384 / sha512; sha384 is the recommended default (collision margin without sha512's 64-byte overhead). Pass an array of contents to emit multiple integrity tokens space- separated per §3.3 (browser picks the strongest it recognizes).
var attr = b.crypto.sri(Buffer.from("alert(1);", "utf8"), { algorithm: "sha384" });
// → "sha384-dnux3uAPxaf+IhCrFG1D/XVNzP1XLDNcn3Pe3jyxouEAoot5kfwC5u8rMwNhE5oi"
var multi = b.crypto.sri(["payload-a", "payload-b"], { algorithm: "sha512" });
// → "sha512-... sha512-..." (two tokens, space-separated)
b.crypto.generateEncryptionKeyPair() #
Generates a hybrid recipient keypair for b.crypto.encrypt: ML-KEM-1024 (FIPS 203 PQC KEM) plus ECDH P-384 (classical defense- in-depth). Returns { publicKey, privateKey, ecPublicKey, ecPrivateKey } — all four PEMs. Persist the private halves in sealed storage; publish the public halves to recipients. The framework default for at-rest envelopes and api-encrypt strategies.
var pair = b.crypto.generateEncryptionKeyPair();
var sealed = b.crypto.encrypt("secret payload", {
publicKey: pair.publicKey,
ecPublicKey: pair.ecPublicKey,
});
var roundTrip = b.crypto.decrypt(sealed, {
privateKey: pair.privateKey,
ecPrivateKey: pair.ecPrivateKey,
});
// → "secret payload"
b.crypto.generateSigningKeyPair(algorithm) #
Generates a PQC signature keypair. Default algorithm is ml-dsa-87 (FIPS 204 — lattice-based, fast verify); pass slh-dsa-shake-256f for hash-based signatures (larger, slower, but minimal cryptographic assumptions — useful for long-lived audit-chain keys). Returns { publicKey, privateKey } PEMs. The signing primitives auto- detect the algorithm from the key PEM, so callers don't need to pass it explicitly to sign / verify.
var pair = b.crypto.generateSigningKeyPair();
var sig = b.crypto.sign("audit:row=42|action=delete", pair.privateKey);
var ok = b.crypto.verify("audit:row=42|action=delete", sig, pair.publicKey);
// → true
// Hash-based alternative:
var slh = b.crypto.generateSigningKeyPair("slh-dsa-shake-256f");
b.crypto.sign(data, privateKeyPem) #
Produces a PQC signature over data. Algorithm is auto-detected from the private-key PEM (ML-DSA-87 lattice / SLH-DSA-SHAKE-256f hash-based). Returns a Buffer. Pair with b.crypto.verify on the recipient side; use for audit-chain links, webhook tags, cross-service request signatures.
var pair = b.crypto.generateSigningKeyPair();
var sig = b.crypto.sign("payload-to-sign", pair.privateKey);
sig.length > 0;
// → true (ML-DSA-87 signature ~ 4627 bytes)
b.crypto.verify(data, signature, publicKeyPem) #
Verifies a signature produced by b.crypto.sign. Returns true on a valid signature, false otherwise — never throws on a malformed signature, so operators don't need to wrap the call. Algorithm is auto-detected from the public-key PEM.
var pair = b.crypto.generateSigningKeyPair();
var sig = b.crypto.sign("hello", pair.privateKey);
var ok = b.crypto.verify("hello", sig, pair.publicKey);
// → true
var tampered = b.crypto.verify("HELLO", sig, pair.publicKey);
// → false (data mismatch)
b.crypto.importPublicJwk(jwk, opts?) #
{
errorClass: Function, // (code, message) error constructor
code: string, // error code passed to errorClass
messagePrefix: string, // text before the Node failure detail. default: ""
}
Import a JWK as a public KeyObject via crypto.createPublicKey({ key, format: "jwk" }), translating the Node import failure into a caller-supplied typed error. Untrusted JWKs reach this from DID documents (publicKeyJwk), DNSKEY records, and COSE_Key structures — each module validates the kty / crv it accepts BEFORE calling, then hands the assembled JWK here for the final import + error translation that was otherwise hand-rolled identically in every one. Centralising it gives one place to harden untrusted-JWK import.
On failure, when opts.errorClass is supplied the thrown error is new opts.errorClass(opts.code, opts.messagePrefix + nodeFailureDetail) so the operator sees the module's own code + a message naming the source; without it the original Node error propagates unchanged.
var b = require("blamejs");
function KeyError(code, message) { this.code = code; this.message = message; }
var key = b.crypto.importPublicJwk(
{ kty: "OKP", crv: "Ed25519", x: "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" },
{ errorClass: KeyError, code: "bad-key", messagePrefix: "bad key: " });
// → (or throws KeyError("bad-key", "bad key: "))
b.crypto.encrypt(plaintext, publicKeys) #
Seals plaintext into a base64 envelope under the recipient's keypair. Default suite is ML-KEM-1024 + ECDH P-384 hybrid (FIPS 203 KEM with classical defense-in-depth) plus SHAKE256 KDF and XChaCha20-Poly1305 AEAD. The 4-byte envelope header (magic + KEM ID + cipher ID + KDF ID) is bound as AEAD AAD so an algorithm- substitution attack on the header fails Poly1305 verification. Pass { publicKey, ecPublicKey } for the hybrid path; passing only an ML-KEM PEM falls back to KEM-only and emits a one-shot system.crypto.hybrid_disabled audit (operators wanting the silent KEM-only path call encryptMlkem768X25519 or seal manually).
var pair = b.crypto.generateEncryptionKeyPair();
var sealed = b.crypto.encrypt("PHI: patient-42 dx=...", {
publicKey: pair.publicKey,
ecPublicKey: pair.ecPublicKey,
});
typeof sealed;
// → "string" (base64 envelope)
var plain = b.crypto.decrypt(sealed, {
privateKey: pair.privateKey,
ecPrivateKey: pair.ecPrivateKey,
});
// → "PHI: patient-42 dx=..."
b.crypto.decrypt(ciphertext, privateKeys, opts?) #
{
allowLegacy: boolean // default false — when true, 0xE1 envelopes
// decrypt via the pre-FixedInfo KDF path
}
Opens a base64 envelope produced by b.crypto.encrypt. The envelope header is parsed first and the decrypt path dispatches by KEM ID — ML-KEM-1024 + P-384, ML-KEM-1024 KEM-only, or ML-KEM-768 + X25519 — so old envelopes continue to decrypt under whichever suite sealed them while new writes use the active suite. Throws on malformed magic, unsupported cipher / KDF, or Poly1305 tag failure. Pass { privateKey, ecPrivateKey } for the default hybrid; the ML-KEM-768 + X25519 KEM ID also requires x25519PrivateKey.
## Legacy 0xE1 envelopes (opts.allowLegacy: true)
The framework's envelope magic byte was bumped from 0xE1 to 0xE2 pre-v1 to enforce a NIST SP 800-56C r2 §4.1 FixedInfo / RFC 9180 §5.1 suite-binding KDF input — SHAKE256 absorbs the suite-id triple (kemId / cipherId / kdfId) plus the literal "blamejs/v1" label alongside the shared secret(s), so the same key cannot be reused across suites without distinct derived material. 0xE1 envelopes lack this binding.
By default 0xE1 envelopes are refused with a hard error directing the operator to re-seal under 0xE2. Operators with at-rest data sealed pre-bump (rare; the bump landed before any operator started depending on the framework) pass opts.allowLegacy: true to read the old envelope, then immediately re-seal via b.crypto.encrypt to migrate. Each legacy decrypt emits a crypto.decrypt.allow_legacy audit event so the migration window is visible in the audit log.
var pair = b.crypto.generateEncryptionKeyPair();
var sealed = b.crypto.encrypt("session-token=abc123", {
publicKey: pair.publicKey,
ecPublicKey: pair.ecPublicKey,
});
var opened = b.crypto.decrypt(sealed, {
privateKey: pair.privateKey,
ecPrivateKey: pair.ecPrivateKey,
});
// → "session-token=abc123"
// Legacy 0xE1 migration:
var plaintext = b.crypto.decrypt(legacyBlob, oldKeys, { allowLegacy: true });
var resealed = b.crypto.encrypt(plaintext, newKeys); // now 0xE2
b.crypto.encryptPacked(buffer, key, aad) #
Symmetric (key-already-known) authenticated encryption. Returns a self-describing Buffer: 1-byte format ID + 24-byte XChaCha20- Poly1305 nonce + ciphertext+tag. Operators who already hold a symmetric key (sealed-storage cell encryption, break-glass row encryption) reach for this instead of the envelope variants. The optional aad (additional authenticated data) is mixed into the Poly1305 tag; encrypt-time and decrypt-time AAD must match exactly or decryption fails. Wire it for context-binding (e.g. (table, rowId, column) so a ciphertext from row A literally cannot decrypt as row B even with the same key).
var key = b.crypto.generateBytes(32);
var data = Buffer.from("row-42 column-ssn", "utf8");
var aad = Buffer.from("patients|42|ssn", "utf8");
var packed = b.crypto.encryptPacked(data, key, aad);
var plain = b.crypto.decryptPacked(packed, key, aad);
plain.toString("utf8");
// → "row-42 column-ssn"
b.crypto.decryptPacked(packed, key, aad) #
Inverse of encryptPacked. Reads the 1-byte format ID, extracts the 24-byte XChaCha20-Poly1305 nonce, and decrypts the trailing ciphertext under key + aad. Throws on unsupported format byte or AAD / tag mismatch — operators wrap when a graceful per-cell fallback is required.
var key = b.crypto.generateBytes(32);
var aad = Buffer.from("audit|2026-05-08", "utf8");
var pkt = b.crypto.encryptPacked(Buffer.from("hello", "utf8"), key, aad);
var open = b.crypto.decryptPacked(pkt, key, aad);
open.toString("utf8");
// → "hello"
b.crypto.generateMlkem768X25519KeyPair() #
Generates the IETF / Cloudflare / Chrome TLS 1.3 hybrid keypair (codepoint 0x11EC): ML-KEM-768 (FIPS 203) + X25519 (RFC 7748). Smaller payload than ML-KEM-1024 + P-384 (~1.1 KB vs ~1.6 KB) and wider interop with peers using the same hybrid (Cloudflare Workers, Chrome, browsers offering hybrid PQ key share). Returns { mlkemPublicKey, mlkemPrivateKey, x25519PublicKey, x25519PrivateKey }.
var pair = b.crypto.generateMlkem768X25519KeyPair();
var sealed = b.crypto.encryptMlkem768X25519("interop payload", {
mlkemPublicKey: pair.mlkemPublicKey,
x25519PublicKey: pair.x25519PublicKey,
});
var plain = b.crypto.decryptMlkem768X25519(sealed, {
privateKey: pair.mlkemPrivateKey,
x25519PrivateKey: pair.x25519PrivateKey,
});
// → "interop payload"
b.crypto.encryptMlkem768X25519(plaintext, recipient) #
Seals plaintext under the IETF / Cloudflare / Chrome TLS 1.3 hybrid (ML-KEM-768 + X25519). Recipient shape is { mlkemPublicKey, x25519PublicKey } — both PEMs. Same envelope wire format as the default hybrid; the KEM ID byte is KEM_IDS.ML_KEM_768_X25519 so b.crypto.decrypt dispatches correctly on the receive side. Reach for this when the recipient publishes ML-KEM-768 + X25519 keys (TLS-1.3 codepoint 0x11EC peers, cross-stack interop with Cloudflare Workers or Chrome-side WebCrypto).
var pair = b.crypto.generateMlkem768X25519KeyPair();
var sealed = b.crypto.encryptMlkem768X25519("cross-stack message", {
mlkemPublicKey: pair.mlkemPublicKey,
x25519PublicKey: pair.x25519PublicKey,
});
typeof sealed;
// → "string" (base64 envelope, ~1.1 KB for short plaintexts)
b.crypto.decryptMlkem768X25519(ciphertext, recipient) #
Symmetric named-pair to encryptMlkem768X25519. Rejects any envelope whose KEM ID byte is not ML_KEM_768_X25519 so an operator who calls this with a ciphertext sealed under a different algorithm gets a clear error rather than the generic dispatch path. Recipient shape is { privateKey, x25519PrivateKey } — privateKey is the ML-KEM-768 PEM, NOT the framework default ML-KEM-1024.
var pair = b.crypto.generateMlkem768X25519KeyPair();
var sealed = b.crypto.encryptMlkem768X25519("interop", {
mlkemPublicKey: pair.mlkemPublicKey,
x25519PublicKey: pair.x25519PublicKey,
});
var plain = b.crypto.decryptMlkem768X25519(sealed, {
privateKey: pair.mlkemPrivateKey,
x25519PrivateKey: pair.x25519PrivateKey,
});
// → "interop"
b.crypto.encryptEnvelopeAsCertPeer(plaintext, opts) #
{
peerCertDer: Buffer, // peer's TLS cert as DER bytes (Buffer or Uint8Array)
peerKemPubkey: string, // peer's ML-KEM-1024 pubkey PEM (non-empty string)
}
Produces an envelope sealed to a peer identified by their TLS cert (P-384 ECDH half) plus a peer-supplied ML-KEM-1024 pubkey. The wire format is identical to b.crypto.encrypt — only the input keys differ. Use for sealed-storage records with peer recipients, cross-service messages between cert-identified peers without a shared framework keypair, or audit-log entries tagged with peer recipients. The cert must carry an ECDH P-384 SubjectPublicKeyInfo — anything else throws crypto/cert-key-not-ecdh-p384.
var fs = require("fs");
var peerCertDer = fs.readFileSync("/etc/ssl/peer.cert.der");
var peerKemPubkey = fs.readFileSync("/etc/ssl/peer.mlkem.pem", "utf8");
var sealed = b.crypto.encryptEnvelopeAsCertPeer("cross-peer payload", {
peerCertDer: peerCertDer,
peerKemPubkey: peerKemPubkey,
});
typeof sealed;
// → "string" (base64 envelope)
b.crypto.decryptEnvelopeAsCertPeer(envelope, opts) #
{
certPrivateKey: object, // KeyObject or PEM string — ECDH P-384 priv (secp384r1)
kemSecret: string, // operator's ML-KEM-1024 priv PEM (non-empty)
}
Decrypts an envelope sealed to this operator's TLS cert ECDH-pubkey + ML-KEM-1024 pubkey. certPrivateKey accepts either a node:crypto KeyObject (ECDH P-384, namedCurve secp384r1) or its PEM-encoded pkcs8 string; kemSecret is always the ML-KEM-1024 PEM. A non- P-384 cert key throws crypto/cert-key-not-ecdh-p384. Mirror of encryptEnvelopeAsCertPeer for the receive side.
var fs = require("fs");
var ourCertPriv = fs.readFileSync("/etc/ssl/our.cert.key.pem", "utf8");
var ourKemSecret = fs.readFileSync("/etc/ssl/our.mlkem.priv.pem", "utf8");
var sealed = "AaECA..."; // base64 envelope received from peer
var plain = b.crypto.decryptEnvelopeAsCertPeer(sealed, {
certPrivateKey: ourCertPriv,
kemSecret: ourKemSecret,
});
typeof plain;
// → "string"
b.crypto.hashCertFingerprint(pemOrDer) #
Computes a stable SHA3-512 fingerprint of an X.509 certificate. Accepts either DER bytes (Buffer) or a PEM string (BEGIN/END envelope is stripped, base64 body decoded). Returns { hex, colon } so callers can compare against either rendering style — lowercase hex (concise, log-friendly) or uppercase colon-separated hex (matches openssl x509 -fingerprint output shape). Use for peer-cert pinning, mTLS bootstrap allowlists, webhook verification, certificate-transparency cross-checks.
var fs = require("fs");
var pem = fs.readFileSync("/etc/ssl/peer.cert.pem", "utf8");
var fp = b.crypto.hashCertFingerprint(pem);
fp.hex.length;
// → 128 (SHA3-512 = 64 bytes hex-encoded)
fp.colon.split(":").length;
// → 64 (one byte per group)
b.crypto.isCertRevoked(pemOrDer, denyList) #
Returns true when the cert's SHA3-512 fingerprint matches any entry in denyList. denyList entries may be the colon-separated uppercase hex form, the lowercase hex form, or both — every comparison runs through crypto.timingSafeEqual so the answer doesn't leak which entry matched. Use for cert-transparency-style deny lists, revoked-peer sweeps, or compromised-CA blocking.
var fs = require("fs");
var pem = fs.readFileSync("/etc/ssl/peer.cert.pem", "utf8");
var deny = ["DEADBEEF:CAFEBABE:1234:5678:..."];
var revoked = b.crypto.isCertRevoked(pem, deny);
// → false (when the fingerprint is not in the deny list)
b.crypto.spkiPin(pemOrDer) #
Computes the RFC 7469 (HPKP §2.4) public-key pin of an X.509 certificate: base64(SHA-256(SubjectPublicKeyInfo DER)). Accepts DER bytes (Buffer) or a PEM string (BEGIN/END envelope stripped, base64 body decoded — same 64 KiB ReDoS cap as hashCertFingerprint). Returns { sha256, b64, hex }: sha256 is the sha256/ wire form browsers and curl --pinnedpubkey render, b64 the bare base64 body, hex the lowercase-hex digest.
Unlike hashCertFingerprint (SHA3-512 over the WHOLE certificate), an SPKI pin binds only the public key, so it survives certificate reissue on the same key pair — the property RFC 7469 pinning relies on.
This is the one place SHA-256 appears on the b.crypto surface: it is an RFC 7469 interop wire constant (browsers / curl / OpenSSL pin stores), not a framework hashing default. The pin binds the peer's long-term SPKI, not the ephemeral PQC key-exchange group negotiated per TLS handshake.
var fs = require("fs");
var pem = fs.readFileSync("/etc/ssl/peer.cert.pem", "utf8");
var pin = b.crypto.spkiPin(pem);
pin.sha256;
// → "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="
b.crypto.spkiPinVerifier(opts) #
{
pins: string[], // required — >= 2 'sha256/' pins (RFC 7469 backup-pin rule)
hostname: string, // optional — verify cert identity against this name instead of the host arg
}
Builds a tls.checkServerIdentity-compatible (host, cert) => Error | undefined that enforces RFC 7469 public-key pinning on top of RFC 9525 strict hostname verification. Pass the returned function as tls.connect({ checkServerIdentity }).
The verifier runs hostname/SAN identity FIRST (via b.network.tls.checkServerIdentity9525 — SAN-required, no Common Name fallback), so a pin match on a certificate issued for the wrong name is still refused. Only when identity passes does it derive the peer's SPKI pin from the presented DER and constant-time-compare (crypto.timingSafeEqual) it against every configured pin. Returns the identity Error on a hostname/SAN failure, an Error with code crypto/spki-pin-mismatch when no pin matches, or undefined when both identity and pin check out.
opts.pins MUST be an array of at least two sha256/ pins: RFC 7469 §4.3 requires a backup pin corresponding to a key not in the current chain so key rotation does not brick the pinned endpoint. When opts.hostname is set, the certificate identity is checked against it instead of the host argument Node supplies — pin the expected name explicitly. Produce pin strings with b.crypto.spkiPin(...).sha256.
var tls = require("node:tls");
var verify = b.crypto.spkiPinVerifier({
pins: [
"sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // backup key
],
});
var sock = tls.connect({
host: "api.example.com",
port: 443,
checkServerIdentity: verify,
});
b.crypto.selfTest(opts?) #
{
{
throwOnFailure?: boolean, // default true — throw if any check fails
}
}
Run a power-on self-test over the framework's cryptographic primitives — the integrity check FIPS 140-3 requires of a validated module. The hash / XOF checks are known-answer tests against NIST FIPS 202 vectors (SHA3-256 / SHA3-512 / SHAKE256); the AEAD check round-trips XChaCha20-Poly1305 and confirms a tampered ciphertext is rejected; the post-quantum checks run a pairwise-consistency + negative test for ML-KEM-1024, ML-DSA-87, and SLH-DSA-SHAKE-256f. Returns a structured report and, by default, throws on any failure so a broken crypto stack fails closed at boot rather than silently producing bad output.
var report = b.crypto.selfTest();
// -> { ok: true, results: [ { name, ok }, ... ], failures: [], ranAt }
Last updated 2026-08-08T16:39:49.652Z by seeder.