/** * T1 — isomorphic crypto provider: entropy + constant-time compare + WebCrypto handle. * * Uses ONLY the WHATWG `globalThis.crypto` surface (present in browsers and Node >=20), so the * package stays isomorphic with zero `node:crypto` import. `timingSafeEqual` is hand-rolled * (no `Buffer`) so it runs unchanged in the browser bundle. */ import { CryptoUnavailableError } from './errors.js' function globalCrypto(): Crypto { const c = (globalThis as { crypto?: Crypto }).crypto if (!c || typeof c.getRandomValues !== 'function') { throw new CryptoUnavailableError('globalThis.crypto.getRandomValues is unavailable') } return c } /** Return the WebCrypto SubtleCrypto (browser `crypto.subtle` / Node `webcrypto.subtle`). */ export function getWebCrypto(): SubtleCrypto { const subtle = globalCrypto().subtle if (!subtle) { throw new CryptoUnavailableError('WebCrypto SubtleCrypto is unavailable') } return subtle } /** CSPRNG bytes via `crypto.getRandomValues`. */ export function randomBytes(len: number): Uint8Array { if (!Number.isInteger(len) || len < 0) { throw new CryptoUnavailableError(`randomBytes length must be a non-negative integer; got ${len}`) } const out = new Uint8Array(len) globalCrypto().getRandomValues(out) return out } /** * Constant-time, length-safe byte equality. Returns `false` on any length mismatch WITHOUT a * content-dependent early exit: both branches XOR-accumulate over a fixed number of iterations. */ export function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean { const lenDiff = a.length ^ b.length const n = Math.max(a.length, b.length) let acc = lenDiff for (let i = 0; i < n; i++) { const av = i < a.length ? a[i]! : 0 const bv = i < b.length ? b[i]! : 0 acc |= av ^ bv } return acc === 0 }