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) #

experimental0.10.10
{
  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) #

experimental0.10.10
{
  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) #

experimental0.10.10
{
  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) #

experimental0.10.10
{
  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) #

stable0.13.0

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:

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) #

experimental0.13.3soc2

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?) #

experimental0.13.3soc2

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?) #

experimental0.13.3soc2

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) #

experimental0.13.3soc2

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?) #

stable0.18.8

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) #

0.5.0

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) #

0.5.0

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?) #

stable0.9.14
{
  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) #

stable0.10.7

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) #

0.1.0

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) #

0.1.0

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) #

0.1.0

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) #

0.6.0
{
  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) #

0.1.0

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) #

0.1.0

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) #

stable0.9.45

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?) #

stable0.9.45
{
  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) #

stable0.15.13
{
  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) #

0.5.0
{
  algorithm: string,   // "sha256" | "sha384" | "sha512" — default "sha384"
}

Computes a W3C Subresource Integrity 1.0 attribute string — sha###-base64 — that operators paste into