Full rewrite of the v1 library in TypeScript with the same public API. The
node bundle now routes RSA primitives through node:crypto whenever
possible, and the browser bundle defaults to native BigInt.
- Keygen uses
crypto.generateKeyPairSync. 2048-bit drops from ~2.3 s to ~50 ms (~45× faster) on modern hardware; 1024-bit from ~240 ms to ~10 ms. - PKCS#1 v1.5 and PSS sign/verify use
crypto.sign/crypto.verify. PSS-SHA256 sign on 2048-bit drops from ~17 ms to sub-millisecond. - OAEP encrypt / PKCS#1 v1.5 encrypt route through
NodeNativeEngine— alsonode:crypto-backed.
A drop-in BigInteger implementation lives at
src/bigint/big-integer-native.ts and
uses ES2020's native BigInt. The browser bundle picks it at load time;
the node bundle stays on the audited jsbn implementation. Round-trips
identically through every API; switch back to jsbn with
new NodeRSA(key, { bigIntImpl: 'jsbn' }) if you ever need to.
| 2048-bit, JS path | jsbn | native | speedup |
|---|---|---|---|
| PSS-SHA256 sign | ~16 ms | ~4 ms | ~4× |
| PSS-SHA256 verify | ~0.4 ms | ~0.08 ms | ~5× |
The bigIntImpl option (also accepted by setOptions) must be set
BEFORE the key is imported or generated; switching it on an instance
that already has key components throws, since the two implementations
produce incompatible BigInteger instances.
The browser bundle silently falls back to jsbn on runtimes without
globalThis.BigInt (i.e. pre-2020 environments). No user action needed.
-
Min Node.js is now 20. v1 worked back to Node 8.11; v2 requires Node 20+ for
node:crypto,globalThis.crypto, and modern ESM features. -
Module shape: ESM-first.
package.json#exportsprovides a dual ESM/CJS layout —import NodeRSA from 'node-rsa'for ESM,require('node-rsa').defaultfor CommonJS. -
Browser default return type is
Uint8Array(wasBuffervia polyfill). Node return type staysBuffer(which extendsUint8Array, so most existing consumers continue to work). Internal byte handling isUint8Arrayend-to-end; the Node entry wraps results asBufferat the API boundary. -
No more
Bufferorcryptoshims for browsers. The browser bundle contains zero Node-builtin imports — verified in CI by agrepoverdist/index.browser.js. Bundlers (Vite, Webpack 5, Rollup, esbuild, Parcel) resolve the browser entry via package.json conditional exports. -
setOptions({environment})is a deprecated no-op. Build-time platform conditions decide the runtime now. The option still forces the pure-JS engine path when set to'browser', preserving the v1 semantic that the 61-case test suite relies on. A one-timeconsole.warnis emitted on use. -
MD4 is Node-only and provider-gated. OpenSSL 3 (Node 17+) doesn't load the legacy provider by default, so
crypto.createHash('md4')throws. v2 probes at module load and reports md4 as unsupported when the provider is absent. The browser bundle never supports MD4. -
asn1npm dependency removed. PKCS#1, PKCS#8, and OpenSSH formats now use a small in-tree DER reader/writer (~150 lines, undersrc/asn1/). Byte-identical to v1 output for every fixture key. -
Native PKCS#1 v1.5
privateDecryptis routed through the JS engine on modern Node. Node has security-deprecated raw PKCS#1 v1.5 decryption (CVE response); v2 transparently falls back to the pure-JS implementation so the call still succeeds. The byte-for-byte plaintext is identical. -
Default signing scheme switched from
pkcs1(PKCS#1 v1.5) topss(RSASSA-PSS). PSS is the modern best-practice signing scheme — it has a tighter security reduction and is preferred by RFC 8017 / NIST for new code. Existing signatures produced under the v1 default remain verifiable by passingsigningScheme: 'pkcs1'explicitly.// To keep v1's PKCS#1 v1.5 default explicit: const key = new NodeRSA(null, { signingScheme: 'pkcs1' }); const sig = key.sign('msg');
The bare-hash shorthand
setOptions({ signingScheme: 'sha256' })also resolves topss-sha256(waspkcs1-sha256in v1). SetsigningScheme: 'pkcs1-sha256'explicitly to keep v1 behaviour. -
Custom MGF for PSS now throws on the node bundle.
node:cryptoonly supports MGF1 with hash equal to the signing hash. If you need a non-default MGF, force the pure-JS path withsetOptions({ environment: 'browser' }). -
Hash algorithms unsupported by the local OpenSSL build now throw at sign/verify time on the node bundle. Functionally equivalent to v1 (the JS scheme delegated to
nodeBackend.digestwhich also threw) — only the error wording and call-site changed.
- OAEP decode is now constant-time (RFC 8017 §7.1.2). Closes a Manger-
style padding-oracle (~10⁵ queries to recover plaintext given a timing
oracle). Includes a missing
Y == 0x00check on the leading byte and a post-decode message-length bound. - PKCS#1 v1.5 decode is now constant-time internally (RFC 8017 §7.2.2, Bleichenbacher / ROBOT). Closes the internal differential timing oracle; the valid/invalid binary oracle inherent to PKCS#1 v1.5 remains — use OAEP for untrusted ciphertexts (the README has a security note).
- PSS verify is now constant-time (RFC 8017 §9.1.2 step 11).
- Private-key operations are blinded (Kocher 1996 / Brumley-Boneh
2003 defence). Fresh
r ← random coprime to nmasks the variable-timemodPowfrom any timing leak ond,dmp1, ordmq1. - Miller-Rabin uses CSPRNG witnesses in [2, n-2] (was
Math.random()over a 168-element fixed table — adversarial-pseudoprime risk) and now honours the caller's full round count (was silently halved). Keygen picks adaptive rounds by bit length per FIPS 186-4 Table C.3. - Public exponent validated on import:
1 < ewith e odd (RFC 8017 §3.1). - RSA primitive bounds-check:
0 ≤ x < nenforced in both$doPrivateand$doPublic(RFC 8017 §3.2).verify()translates the resulting out-of-range error to "invalid signature" per §8.x. - Imported private keys are CRT-consistency-checked:
n = p·q,dp ≡ d mod (p−1),dq ≡ d mod (q−1),q·coeff ≡ 1 mod p,e·dp ≡ 1 mod (p−1),e·dq ≡ 1 mod (q−1). Closes a Boneh-DeMillo- Lipton fault-injection vector on crafted PEM/PKCS#8/OpenSSH files. generate(B)refusesB < 512(cryptographically broken) and emits a one-shotconsole.warnforB < 2048(below NIST SP 800-56B §6.1.6.2 minimum).- Fermat-distance defence: keygen rejects p, q pairs with
|p − q| < 2^(B/2 − 100)(FIPS 186-4 §B.3.6). - CRT recombination is branch-free: removed the data-dependent
while (xp < xq) xp += ploop. - OpenSSH parser hardening:
SshReader.readStringbounds-checks beforesubarray; the two private-section checkints (checkint1,checkint2) are now validated for equality. - PKCS#8 parser hardening: outer version validated against {0, 1} (RFC 5958 §2); inner PKCS#1 version restricted to two-prime (RFC 8017 §A.1.2); algorithm OID whitelist with clear diagnostics for PSS-only (1.2.840.113549.1.1.10) and OAEP-only (.1.1.7) misuse.
- TypeScript types for every public surface (
NodeRSAOptions,EncryptionSchemeOptions,SigningSchemeOptions,HashAlg, format string union types). @noble/hashesruntime dependency for synchronous SHA/MD/RIPEMD digests in the browser bundle. ~6 KB gzipped, audited, zero-dep.- Bundle size budget (CI-enforced):
dist/index.browser.js: <100 KB raw / <30 KB gzipped (currently 90/21)dist/index.node.{js,cjs}: <120 KB raw / <35 KB gzipped (currently 94/22)
- Modern tooling:
tsupfor build (esbuild),vitestfor tests (with a workspace running every spec in two projects —nodeandbrowser-emulated),biomefor lint+format, strict TypeScript withnoUncheckedIndexedAccess/exactOptionalPropertyTypes/noImplicitOverrideetc. - 1006 test cases across 27 files. The v1 mocha suite of 61
it()blocks is ported verbatim and runs in both vitest projects.
See git history.