node-rsa
node-rsa is a high-level RSA key, encryption, and signature library for Node.js 20+ and modern browser bundles. It generates keys, imports and exports PKCS#1, PKCS#8, OpenSSH, DER, PEM, and raw components, encrypts with OAEP or legacy PKCS#1 v1.5 padding, and signs with PSS or PKCS#1 v1.5. Version 2 is a TypeScript rewrite with Node crypto fast paths, a separate pure-JavaScript browser build, built-in types, and hardened padding and key validation. It is an RSA toolbox, not a complete envelope format, certificate stack, JWT implementation, or key-management service.
node-rsa 2.0 is a credible convenience layer when RSA interoperability and format conversion are the actual requirements. For new application protocols, prefer a defined format such as JOSE and keep encryption hybrid, settings explicit, and private keys out of browser code.
Use it if
- You must interoperate with an existing RSA key or protocol and want one API for key formats, encryption, decryption, signing, and verification
- You need the same RSA operations in Node.js and a modern browser bundle without Buffer or crypto polyfills
- You are migrating node-rsa 1.x code and can review the version 2 signing-default and module-shape changes
- You need to convert among PEM, DER, OpenSSH, and raw RSA components as part of application code
- You are designing a new token or encrypted-message protocol: jose or a defined envelope format handles algorithm identifiers, headers, serialization, and key selection that node-rsa leaves to you
- You need bulk encryption: the README offers long-message support, but RSA ciphertext expands per block and a hybrid design with an authenticated symmetric cipher is the better fit
- You need Node.js 18 or older: version 2 raises the engine requirement to Node 20, while staying on 1.x misses the version 2 security audit fixes
- You rely on the old defaults: version 2 changed signatures from PKCS#1 v1.5 to PSS, while OAEP still defaults to SHA-1 unless you choose a hash explicitly
- You must decrypt attacker-controlled PKCS#1 v1.5 ciphertext: the security notes say the valid-versus-invalid padding oracle is inherent even after internal timing was hardened
Setup reality
Install with npm install node-rsa. There are no native builds, credentials, peer dependencies, or config files, but version 2 requires Node.js 20 and its migration is not a blind package bump. ESM uses import NodeRSA from 'node-rsa'; CommonJS uses require('node-rsa').default. Remove @types/node-rsa because version 2 includes its own declarations and the old DefinitelyTyped package exposes the wrong namespace shape. Browser builds depend on a bundler that honors conditional exports and get ESM plus Uint8Array results; Node gets ESM or CommonJS and Buffer-compatible results. Old browser Buffer, crypto, and process shims should be removed. Make padding choices explicit at the call site: use OAEP with SHA-256 for new encryption unless an external protocol requires something else, and use PSS with SHA-256 for new signatures. The library's OAEP default is SHA-1, while the version 2 signing default changed from PKCS#1 v1.5 to PSS, so implicit settings create interoperability surprises. RSA has a scheme-dependent plaintext limit exposed by getMaxMessageSize(); for files or large payloads, encrypt data with an authenticated symmetric cipher and use RSA only to wrap its key. Key generation is synchronous, so do not put it on a latency-sensitive request path. Store private PEM outside source control, restrict filesystem or secret-manager access, never send it to the browser, and define key IDs and rotation outside this package because node-rsa does not manage either.
Patterns
Generate a 2048-bit RSA key pairgenerate-key-pair
import NodeRSA from 'node-rsa';
const key = new NodeRSA({ b: 2048 });
console.log(key.getKeySize());Version 2 refuses keys below 512 bits and warns below 2048; key generation is synchronous, so keep it off hot request paths.
Export private and public PEMexport-pem-keys
const privatePem = key.exportKey('pkcs8-private-pem');
const publicPem = key.exportKey('pkcs8-public-pem');Store privatePem in a secret manager or permission-restricted file; only the public key is safe to distribute.
Import a PEM key with automatic detectionimport-pem-key
import NodeRSA from 'node-rsa';
const key = new NodeRSA(process.env.RSA_PRIVATE_KEY);
if (!key.isPrivate()) throw new Error('private RSA key required');PEM input can omit the format, but raw DER must include an exact format such as pkcs8-private-der.
Encrypt with explicit OAEP and SHA-256encrypt-oaep-sha256
const publicKey = new NodeRSA(publicPem);
publicKey.setOptions({
encryptionScheme: { scheme: 'pkcs1_oaep', hash: 'sha256' },
});
const ciphertext = publicKey.encrypt('small secret', 'base64');OAEP is the default scheme but its default hash is SHA-1, so set SHA-256 explicitly and match it during decryption.
Decrypt matching OAEP ciphertextdecrypt-oaep-sha256
const privateKey = new NodeRSA(privatePem);
privateKey.setOptions({
encryptionScheme: { scheme: 'pkcs1_oaep', hash: 'sha256' },
});
const plaintext = privateKey.decrypt(ciphertext, 'utf8');The private key must use the same OAEP hash and label as the encrypting side or decryption fails.
Reject plaintext that exceeds one RSA blockcheck-message-limit
const bytes = new TextEncoder().encode(message);
const max = publicKey.getMaxMessageSize();
if (bytes.byteLength > max) {
throw new Error(`use hybrid encryption; RSA limit is ${max} bytes`);
}
const encrypted = publicKey.encrypt(bytes, 'base64');The limit changes with modulus size and padding; use RSA to wrap a symmetric key instead of splitting a large payload into RSA blocks.
Sign with explicit RSA-PSS and SHA-256sign-pss-sha256
const signer = new NodeRSA(privatePem);
signer.setOptions({
signingScheme: { scheme: 'pss', hash: 'sha256', saltLength: 32 },
});
const signature = signer.sign(payload, 'base64');PSS is version 2's default, but spelling out the scheme, hash, and salt length avoids cross-version and cross-language ambiguity.
Verify a signature with the public keyverify-pss-signature
const verifier = new NodeRSA(publicPem);
verifier.setOptions({
signingScheme: { scheme: 'pss', hash: 'sha256', saltLength: 32 },
});
const valid = verifier.verify(payload, signature, 'utf8', 'base64');verify returns false for an invalid signature; the payload encoding and all PSS parameters must match the signer.
Load version 2 from CommonJSuse-commonjs
const NodeRSA = require('node-rsa').default;
const key = new NodeRSA(publicPem);Version 2 uses the standard ESM-to-CommonJS default export shape; require('node-rsa') by itself returns a module object.
Encrypt in a modern browser bundleuse-browser-bundle
import NodeRSA from 'node-rsa';
const publicKey = new NodeRSA(publicPem);
publicKey.setOptions({
encryptionScheme: { scheme: 'pkcs1_oaep', hash: 'sha256' },
});
const ciphertext = publicKey.encrypt(formValue, 'base64');Use a bundler with conditional-exports support. Do not put a private key in browser code, and remove old Buffer or crypto polyfills.
Convert PKCS#1 private PEM to PKCS#8convert-key-format
const key = new NodeRSA(pkcs1Pem, 'pkcs1-private-pem');
const pkcs8Pem = key.exportKey('pkcs8-private-pem');Converting representation does not encrypt the private key or add password protection; secure storage remains your responsibility.
Export an OpenSSH public keyexport-openssh-public
const key = new NodeRSA(privateOrPublicPem);
const authorizedKey = key.exportKey('openssh-public');The OpenSSH public format is suitable for authorized_keys-style interchange; it is not an X.509 certificate.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| jose | npm | Choose it for JWS, JWT, JWE, JWK, and standards-defined algorithm and serialization behavior |
| node-forge | npm | Choose it when RSA is only one part of a browser-capable PKI task involving certificates, CSRs, ASN.1, or TLS structures |
| openpgp | npm | Choose it for OpenPGP messages, identities, armored keys, and multi-recipient encryption rather than raw RSA operations |
| @noble/curves | npm | Choose it for audited modern elliptic-curve primitives when no RSA interoperability requirement exists |