RSA encryption turns 49 years old in 2026, and it still protects a large share of the TLS handshakes, signed software updates, and API tokens you touch every day. Yet the rules for using it safely have shifted. NIST SP 800-57 Part 1 Rev. 5 now treats RSA-2048 as the bare minimum (112-bit security strength) and points developers toward RSA-3072 for any new system, while a December 5, 2025 draft of Rev. 6 keeps tightening the timeline toward a 2030 cutoff for 112-bit protection. On top of that, the harvest-now-decrypt-later threat means data you encrypt with RSA today could be unwrapped by a quantum computer years from now.
This tutorial shows you how to use RSA encryption correctly in Node.js with the built-in crypto module: generating keys, encrypting and decrypting with OAEP padding, signing and verifying with SHA-256, and wrapping AES keys for real-world payloads. You will build a complete, runnable project in 11 steps, with output examples, five common pitfalls, and an eight-item troubleshooting table. No third-party crypto libraries are required. Everything ships inside Node.js itself.
Why RSA Encryption Still Matters in 2026
RSA is an asymmetric algorithm. It uses a public key that anyone can hold and a private key that only you keep. Anyone can encrypt a message to you with your public key, but only your private key can decrypt it. That property is what makes RSA useful for two jobs that symmetric ciphers like AES cannot do alone: secure key exchange and digital signatures.
The catch is that RSA is slow and size-limited. A private-key operation can be hundreds of times slower than an equivalent AES operation, and the plaintext you encrypt must fit below the modulus size minus the padding overhead. An RSA-2048 key with OAEP and SHA-256 can encrypt at most 190 bytes in a single operation. That is why production systems almost never encrypt user data directly with RSA. Instead, they use RSA to protect a random AES key, then let AES encrypt the bulk data. You will build exactly that hybrid pattern in Step 7.
Where does RSA fit in 2026? It still dominates legacy interoperability: signed JWTs (RS256), code-signing certificates, S/MIME email, PGP keys, and TLS certificates issued before elliptic-curve migration completes. If you maintain any of those systems, you need to handle RSA correctly. If you are designing something brand new with no compatibility constraints, consider elliptic-curve cryptography (smaller keys, faster operations) or start planning for post-quantum key establishment, which we cover in the final section.
History also explains why RSA implementations need care. The 2017 ROCA vulnerability let attackers factor RSA keys generated by a flawed Infineon library, forcing the reissue of millions of smartcards and TPM keys. The Bleichenbacher padding-oracle attack, first published in 1998, keeps resurfacing in new forms, and a 2024 timing variant called the Marvin attack showed that even constant-time-looking decryption can leak. The lesson for Node developers is consistent: let the standard library and OpenSSL do the cryptography, choose OAEP and PSS padding, and never hand-roll the math. The code in this tutorial follows that rule throughout.
How RSA Encryption Works Under the Hood
You do not need the full number theory to use RSA safely, but a working mental model prevents most mistakes. RSA generates two large prime numbers, multiplies them to form the modulus (the “n” you see as the 3072-bit number), and derives a matched public and private exponent from them. The public key is the modulus plus the public exponent 65537. The private key is the modulus plus the private exponent, which only you can compute because only you know the original primes. Security rests on one fact: multiplying two large primes is easy, but factoring the product back into those primes is computationally infeasible with classical hardware.
Why Padding Is Not Optional
Raw RSA (“textbook RSA”) is deterministic: the same plaintext always produces the same ciphertext, which leaks information and enables several classic attacks. Padding fixes this by injecting randomness and structure before the math runs. OAEP (Optimal Asymmetric Encryption Padding) mixes your message with a random seed through two hash passes, so encrypting the same value twice yields two different ciphertexts. This is why every example in this tutorial sets padding and oaepHash explicitly. Skipping padding, or falling back to the weaker PKCS#1 v1.5 scheme, is the difference between a secure system and one that looks secure until someone probes it.
Encryption Versus Signing: Opposite Directions
RSA runs in two opposite directions. For confidentiality, you encrypt with the public key and decrypt with the private key, so anyone can send you a secret only you can read. For authenticity, you sign with the private key and verify with the public key, so anyone can confirm a message came from you but nobody can forge your signature. Confusing the two is a frequent design error. A signature does not hide data, and encryption alone does not prove who sent it. Real protocols combine both, which is exactly what the hybrid pattern and the signing step below demonstrate.
Prerequisites and Versions
This project uses only the Node.js standard library, so the dependency list is short. Pin these versions or newer to avoid API differences in the crypto module.
| Component | Minimum version | Why it matters |
|---|---|---|
| Node.js | 20.x LTS or 22.x LTS | Stable crypto.generateKeyPairSync, publicEncrypt, and sign/verify APIs; OpenSSL 3.x under the hood |
| npm | 10.x | Ships with Node 20/22; used only for project scaffolding here |
| OpenSSL | 3.0+ (bundled with Node) | Provides RSA-OAEP, MGF1, and SHA-256 primitives |
| Operating system | Linux, macOS, or Windows | The crypto module is cross-platform; commands shown use a POSIX shell |
| Editor | VS Code or any editor | Optional, for following along |
Confirm your runtime before you start. Node 18 reached end-of-life in April 2025, so move off it if you are still there.
$ node --version
v22.14.0
$ node -e "console.log(process.versions.openssl)"
3.0.15+quic
If node --version prints v20 or v22, you are ready. The process.versions.openssl value should start with 3, which guarantees the modern padding modes used below.
Step 1: Scaffold the Project
Create a clean directory and initialize it. We will keep everything in plain ECMAScript modules so the import syntax stays readable.
$ mkdir rsa-node-demo && cd rsa-node-demo
$ npm init -y
$ npm pkg set type="module"
$ mkdir keys src
$ touch src/keys.js src/encrypt.js src/sign.js src/hybrid.js src/index.js
The npm pkg set type="module" line flips the package to ESM so import { ... } from 'node:crypto' works without a build step. The keys/ folder will hold generated PEM files, and src/ holds one module per concept. Add a .gitignore immediately so you never commit a private key.
# .gitignore
node_modules/
keys/
*.pem
Committing a private key to version control is the single most common RSA mistake in real codebases. Ignoring the keys/ directory from the first commit removes that risk before it starts.
Step 2: Generate an RSA Key Pair
Node generates RSA keys with crypto.generateKeyPairSync. You choose the modulus length (key size in bits), the public exponent, and the encoding format. The code below builds a reusable function and writes both keys to disk in PEM format. The private key is wrapped with AES-256-CBC and a passphrase so it is never stored in the clear.
// src/keys.js
import { generateKeyPairSync } from 'node:crypto';
import { writeFileSync, readFileSync } from 'node:fs';
export function createKeyPair(passphrase, modulusLength = 3072) {
const { publicKey, privateKey } = generateKeyPairSync('rsa', {
modulusLength, // 3072-bit = 128-bit security per NIST
publicExponent: 0x10001, // 65537, the standard secure choice
publicKeyEncoding: {
type: 'spki',
format: 'pem',
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem',
cipher: 'aes-256-cbc', // encrypt the private key at rest
passphrase,
},
});
writeFileSync('keys/public.pem', publicKey);
writeFileSync('keys/private.pem', privateKey);
return { publicKey, privateKey };
}
export function loadPublicKey() {
return readFileSync('keys/public.pem', 'utf8');
}
export function loadPrivateKey(passphrase) {
return { key: readFileSync('keys/private.pem', 'utf8'), passphrase };
}
A few details matter here. The publicExponent of 65537 (hex 0x10001) is the universally recommended value: large enough to resist low-exponent attacks, small enough to keep verification fast. The spki and pkcs8 encodings are the modern, interoperable standards (SPKI for the public key, PKCS#8 for the private key). The cipher and passphrase options tell OpenSSL to encrypt the private key file itself, so a stolen private.pem is useless without the passphrase.
Run a quick generation and inspect the output. Key generation for 3072-bit takes a noticeable fraction of a second, which is normal.
$ node -e "import('./src/keys.js').then(m => m.createKeyPair('s3cret-pass'))"
$ head -1 keys/public.pem
-----BEGIN PUBLIC KEY-----
$ head -1 keys/private.pem
-----BEGIN ENCRYPTED PRIVATE KEY-----
Notice the private key header reads ENCRYPTED PRIVATE KEY, confirming the passphrase wrapping worked. If it instead reads PRIVATE KEY, your cipher option was dropped and the key is sitting on disk unprotected.
Step 3: Encrypt Data With the Public Key (OAEP)
Encryption uses crypto.publicEncrypt. The most important argument is the padding mode. Always use RSA_PKCS1_OAEP_PADDING with an explicit OAEP hash of SHA-256. PKCS#1 v1.5 padding (the legacy default in some examples) is historically vulnerable to Bleichenbacher padding-oracle attacks and should not be used for new encryption.
// src/encrypt.js
import { publicEncrypt, privateDecrypt, constants } from 'node:crypto';
export function encrypt(publicKey, plaintext) {
const buffer = Buffer.from(plaintext, 'utf8');
const encrypted = publicEncrypt(
{
key: publicKey,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
buffer
);
return encrypted.toString('base64');
}
export function decrypt(privateKey, passphrase, ciphertextB64) {
const buffer = Buffer.from(ciphertextB64, 'base64');
const decrypted = privateDecrypt(
{
key: privateKey,
passphrase,
padding: constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: 'sha256',
},
buffer
);
return decrypted.toString('utf8');
}
The encrypted output is a raw byte buffer the same length as the modulus (384 bytes for RSA-3072), so we base64-encode it for safe transport in JSON or HTTP headers. The oaepHash option must match on both sides. If the encryptor uses SHA-256 and the decryptor uses the default SHA-1, decryption fails with an opaque error.
Step 4: Decrypt Data With the Private Key
The decrypt function above already pairs with encrypt. The key point is symmetry: every parameter that shaped the ciphertext (padding mode and OAEP hash) must be reproduced exactly during decryption. Let us run a round-trip to prove it works.
// quick round-trip test
import { loadPublicKey, loadPrivateKey } from './src/keys.js';
import { encrypt, decrypt } from './src/encrypt.js';
const pub = loadPublicKey();
const { key, passphrase } = loadPrivateKey('s3cret-pass');
const secret = 'API_KEY=sk-live-7f3a9c2e';
const ct = encrypt(pub, secret);
console.log('ciphertext (base64, truncated):', ct.slice(0, 44) + '...');
console.log('roundtrip:', decrypt(key, passphrase, ct));
Expected output:
ciphertext (base64, truncated): Qk1lY3J5cHRlZF9kYXRhX2hlcmVfdHJ1bmNhdGVk...
roundtrip: API_KEY=sk-live-7f3a9c2e
If you try to encrypt a string longer than the size limit, Node throws error:0200006E:rsa routines::data too large for key size. That is the hard ceiling we discussed: RSA-3072 with OAEP-SHA256 caps at 318 bytes. The fix is not a bigger key, it is hybrid encryption, which is Step 7.
Step 5: Sign Data With RSA and SHA-256
Signatures are the second RSA superpower. A signature proves a message came from the private-key holder and was not altered. Node provides crypto.sign and crypto.verify. For new systems, use the PSS padding scheme, which carries a security proof that the older PKCS#1 v1.5 signature scheme lacks. Both are shown so you can match legacy verifiers.
// src/sign.js
import { sign, verify, constants } from 'node:crypto';
export function signMessage(privateKey, passphrase, message) {
const signature = sign('sha256', Buffer.from(message), {
key: privateKey,
passphrase,
padding: constants.RSA_PKCS1_PSS_PADDING, // modern, provably secure
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
});
return signature.toString('base64');
}
export function verifyMessage(publicKey, message, signatureB64) {
return verify(
'sha256',
Buffer.from(message),
{
key: publicKey,
padding: constants.RSA_PKCS1_PSS_PADDING,
saltLength: constants.RSA_PSS_SALTLEN_DIGEST,
},
Buffer.from(signatureB64, 'base64')
);
}
Under the hood, sign('sha256', ...) hashes the message with SHA-256 first, then signs the digest. This is why RSA signatures work on data of any length, while RSA encryption does not: signing only ever processes a fixed-size 32-byte hash. The verify call returns a boolean, never throws on a bad signature, so always check the return value rather than wrapping it in a try/catch.
// verify in action
const msg = 'transfer 100 USD to account 4471';
const sig = signMessage(key, passphrase, msg);
console.log('valid:', verifyMessage(pub, msg, sig)); // true
console.log('tampered:', verifyMessage(pub, msg + ' ', sig)); // false
Adding even a single trailing space flips the result to false, because the SHA-256 digest changes completely. If you need to verify against an existing RS256 JWT or a legacy signer, swap the padding to constants.RSA_PKCS1_PADDING and drop the saltLength option. For the relationship between hashing and signatures, see our explainer on digital signatures and SHA-256.
Step 6: Load, Store, and Rotate Keys Safely
A working algorithm is useless if the keys leak. Three rules cover most of the risk. First, never store the passphrase next to the key; pull it from an environment variable or a secrets manager. Second, set restrictive file permissions on the private key. Third, plan rotation so a compromised key has a short blast radius.
// src/keys.js (additions)
import { chmodSync } from 'node:fs';
export function hardenPrivateKey() {
// owner read/write only (0600) on POSIX systems
chmodSync('keys/private.pem', 0o600);
}
// load passphrase from the environment, never from source
export function envPassphrase() {
const p = process.env.RSA_PASSPHRASE;
if (!p) throw new Error('RSA_PASSPHRASE is not set');
return p;
}
Run the app with the passphrase injected at launch, not hard-coded:
$ RSA_PASSPHRASE='s3cret-pass' node src/index.js
For rotation, generate a new pair on a schedule (90 days is a common policy), keep the old public key around long enough to verify in-flight signatures, then retire it. Treat private-key storage with the same discipline you would apply to a password vault. Our guides on Argon2 password hashing and password security cover the secret-handling mindset in depth.
Step 7: Encrypt Large Payloads With Hybrid RSA + AES
This is the pattern production systems actually use. Generate a random AES-256 key, encrypt the real data with AES-GCM (which also authenticates it), then encrypt only the small AES key with RSA. The recipient uses RSA to unwrap the AES key, then AES to decrypt the payload. You get RSA key exchange plus AES speed, with no size limit on the data.
// src/hybrid.js
import {
publicEncrypt, privateDecrypt, constants,
randomBytes, createCipheriv, createDecipheriv,
} from 'node:crypto';
export function hybridEncrypt(publicKey, plaintext) {
const aesKey = randomBytes(32); // 256-bit AES key
const iv = randomBytes(12); // 96-bit nonce for GCM
const cipher = createCipheriv('aes-256-gcm', aesKey, iv);
const data = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const tag = cipher.getAuthTag();
const encKey = publicEncrypt(
{ key: publicKey, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
aesKey
);
return {
key: encKey.toString('base64'),
iv: iv.toString('base64'),
tag: tag.toString('base64'),
data: data.toString('base64'),
};
}
export function hybridDecrypt(privateKey, passphrase, payload) {
const aesKey = privateDecrypt(
{ key: privateKey, passphrase, padding: constants.RSA_PKCS1_OAEP_PADDING, oaepHash: 'sha256' },
Buffer.from(payload.key, 'base64')
);
const decipher = createDecipheriv('aes-256-gcm', aesKey, Buffer.from(payload.iv, 'base64'));
decipher.setAuthTag(Buffer.from(payload.tag, 'base64'));
return Buffer.concat([
decipher.update(Buffer.from(payload.data, 'base64')),
decipher.final(),
]).toString('utf8');
}
The GCM authentication tag is critical. It lets decipher.final() throw if the ciphertext was tampered with, giving you authenticated encryption rather than plain confidentiality. Now a 10 MB file encrypts just as easily as a 10-byte string, because RSA only ever touches the 32-byte AES key. For a deeper look at the symmetric half, read our walkthrough of AES-256 encryption in Node.js.
The returned object is your “digital envelope.” It carries four fields: the RSA-wrapped AES key, the GCM initialization vector, the authentication tag, and the AES ciphertext. You can serialize this object to JSON and store or transmit it as a single unit. The recipient needs only the private key and passphrase to open it. Note that the IV must be unique for every message under the same AES key, which is why we call randomBytes(12) fresh inside hybridEncrypt on every invocation. Reusing an IV with GCM is catastrophic: it breaks both confidentiality and the integrity guarantee. Because each call generates a brand-new random AES key here, the IV uniqueness requirement is satisfied automatically, but the habit matters once you start caching keys for performance.
Step 8: Assemble the Complete Working Project
Now wire every module into one runnable demo. This index.js generates a key pair if none exists, then runs encryption, signing, and hybrid encryption end to end, printing each result.
// src/index.js
import { existsSync } from 'node:fs';
import { createKeyPair, loadPublicKey, loadPrivateKey, hardenPrivateKey } from './keys.js';
import { encrypt, decrypt } from './encrypt.js';
import { signMessage, verifyMessage } from './sign.js';
import { hybridEncrypt, hybridDecrypt } from './hybrid.js';
const pass = process.env.RSA_PASSPHRASE || 's3cret-pass';
if (!existsSync('keys/private.pem')) {
createKeyPair(pass, 3072);
hardenPrivateKey();
console.log('generated a fresh RSA-3072 key pair');
}
const pub = loadPublicKey();
const { key, passphrase } = loadPrivateKey(pass);
// 1. direct RSA encryption (small payload)
const ct = encrypt(pub, 'token=abc123');
console.log('\n[RSA] decrypted:', decrypt(key, passphrase, ct));
// 2. signature
const msg = 'release v2.4.0 build 8841';
const sig = signMessage(key, passphrase, msg);
console.log('[SIG] verified:', verifyMessage(pub, msg, sig));
// 3. hybrid for a large payload
const big = 'x'.repeat(5000);
const env = hybridEncrypt(pub, big);
const out = hybridDecrypt(key, passphrase, env);
console.log('[HYBRID] payload bytes recovered:', out.length);
Run the whole thing:
$ RSA_PASSPHRASE='s3cret-pass' node src/index.js
generated a fresh RSA-3072 key pair
[RSA] decrypted: token=abc123
[SIG] verified: true
[HYBRID] payload bytes recovered: 5000
That output confirms all three flows work: direct encryption round-trips a small secret, the signature verifies, and hybrid mode recovers a 5000-byte payload that would never fit in a single RSA operation. You now have a complete, dependency-free RSA toolkit.
Step 9: Choose the Right RSA Key Size
Key size is a trade-off between security strength and performance. NIST SP 800-57 Part 1 Rev. 5 maps RSA modulus length to a security-strength figure in bits. The table below summarizes the 2026 guidance alongside the practical payload limits you will hit in Node.
| Modulus | Security strength | OAEP-SHA256 max payload | NIST status (2026) | Use when |
|---|---|---|---|---|
| RSA-2048 | 112-bit | 190 bytes | Minimum; legacy-leaning toward 2030 | Compatibility with old systems only |
| RSA-3072 | 128-bit | 318 bytes | Recommended for new systems | Default choice for 2026 designs |
| RSA-4096 | about 152-bit | 446 bytes | Stronger, slower | Long-lived keys, root CAs |
The performance gap is real. Generating an RSA-4096 key can take several times longer than RSA-3072, and every private-key operation is slower too. For most new applications, RSA-3072 hits the sweet spot of 128-bit security with acceptable speed. Reserve RSA-4096 for keys that must stay valid for a decade or more, such as a certificate authority root. If you control both ends and want speed, elliptic-curve keys reach 128-bit security at just 256 bits, a fraction of RSA-3072’s size.
Step 10: Five Common RSA Pitfalls to Avoid
These five mistakes account for most broken or insecure RSA deployments. Each one has bitten real production systems.
- Using PKCS#1 v1.5 padding for encryption. The 1998 Bleichenbacher attack turns a padding-error oracle into a decryption oracle, and the 2024 Marvin attack showed the timing variant is still exploitable in real systems. Always use
RSA_PKCS1_OAEP_PADDINGfor encryption. PKCS#1 v1.5 is acceptable only for legacy signature verification, never for new encryption. - Mismatched OAEP hashes. If the encryptor specifies
oaepHash: 'sha256'but the decryptor omits it, Node defaults to SHA-1 and decryption fails with a vague error. Set the hash explicitly on both sides. - Encrypting large data directly with RSA. The “data too large for key size” error means you exceeded the modulus limit. Switch to the hybrid RSA + AES pattern from Step 7 rather than reaching for a bigger key.
- Committing private keys to Git. Add
keys/and*.pemto.gitignoreon the first commit. A leaked private key cannot be revoked the way a password can be reset; you must regenerate and re-distribute. - Storing the passphrase in source code. Hard-coding
's3cret-pass'defeats the entire point of encrypting the private key at rest. Inject it from an environment variable or a secrets manager at runtime.
Step 11: Troubleshooting RSA Encryption in Node.js
When something breaks, the OpenSSL error strings Node surfaces can be cryptic. This table maps the eight most common errors to their real cause and fix.
| Error message | Likely cause | Fix |
|---|---|---|
data too large for key size | Plaintext exceeds modulus minus padding | Use hybrid RSA + AES (Step 7) |
oaep decoding error | OAEP hash mismatch between sides | Set oaepHash: 'sha256' on encrypt and decrypt |
error:1C800064 bad decrypt | Wrong key or wrong passphrase | Verify the private key and RSA_PASSPHRASE match the public key |
could not load PEM private key | Passphrase missing or PEM corrupted | Pass passphrase in the key object; re-export the PEM |
verify() returns false | Message altered or padding mismatch | Confirm identical padding/saltLength; check message bytes |
digital envelope routines unsupported | Legacy algorithm blocked by OpenSSL 3 | Move to OAEP/PSS; avoid deprecated ciphers |
ERR_OSSL_RSA_DATA_GREATER_THAN_MOD_LEN | Ciphertext longer than the modulus | Confirm you decrypt with the matching key size |
RSA_PASSPHRASE is not set | Environment variable missing | Launch with RSA_PASSPHRASE='...' node ... |
When you hit bad decrypt, the fastest diagnostic is to confirm the public and private keys are a matched pair. Encrypt a known string and decrypt it in the same process. If that round-trips, your stored keys are mismatched. If it fails, your padding options differ between the two calls.
Advanced Tips for Production RSA
Use Asynchronous Key Generation Under Load
The synchronous generateKeyPairSync blocks the Node event loop, which is fine in a CLI tool but harmful in a web server. Under load, switch to the callback or promise form so key generation runs on the libuv thread pool instead of freezing every request.
import { generateKeyPair } from 'node:crypto';
import { promisify } from 'node:util';
const generateKeyPairAsync = promisify(generateKeyPair);
const { publicKey, privateKey } = await generateKeyPairAsync('rsa', {
modulusLength: 3072,
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: { type: 'pkcs8', format: 'pem' },
});
Validate Keys at Startup, Not in the Hot Path
Load and parse PEM files once at boot using crypto.createPublicKey and crypto.createPrivateKey, then reuse the resulting KeyObject instances. Re-parsing PEM text on every request wastes CPU and re-runs the passphrase decryption each time. A cached KeyObject also surfaces a malformed key immediately at startup rather than failing mid-request.
Prefer Authenticated Hybrid Encryption Everywhere
Even for payloads small enough to fit in a single RSA block, the hybrid AES-GCM pattern gives you authenticated encryption and a uniform code path. Standardizing on hybrid encryption removes the size-limit foot-gun entirely and means a future payload growth never breaks production.
Preparing RSA for the Post-Quantum Era
RSA’s security rests on the difficulty of factoring large numbers. Shor’s algorithm, running on a sufficiently large quantum computer, factors that modulus efficiently and breaks RSA outright. No such machine exists today, but the harvest-now-decrypt-later threat means encrypted traffic captured in 2026 could be decrypted years later once the hardware matures. Any data that must stay confidential into the 2030s is already at risk.
NIST finalized its first post-quantum standards in August 2024: FIPS 203 (ML-KEM, derived from Kyber) for key encapsulation, FIPS 204 (ML-DSA, derived from Dilithium) for signatures, and FIPS 205 (SLH-DSA) as a hash-based signature backup. The migration path most teams will follow is hybrid: combine an ML-KEM key exchange with classical RSA or ECDH, so the connection stays secure even if one algorithm falls. Treat RSA as a transition technology for new long-lived secrets, and track the rollout in our guide to post-quantum cryptography in 2026.
The practical 2026 stance: keep using RSA-3072 where you must interoperate, encrypt bulk data with AES-256 (which quantum computers weaken far less), and begin testing post-quantum KEMs in non-production environments. The cryptographic agility you build now (clean separation of key exchange, encryption, and signing) is what will make the eventual swap painless.
Frequently Asked Questions
Is RSA-2048 still safe to use in 2026?
RSA-2048 provides 112-bit security and remains the minimum acceptable size under NIST SP 800-57 Rev. 5. It is safe against classical attacks today, but NIST guidance points toward retiring 112-bit protection for new data by 2030. For any new system, use RSA-3072 (128-bit security) instead unless a legacy system forces 2048.
Why can’t RSA encrypt large files directly?
RSA can only encrypt data smaller than its modulus minus the padding overhead. RSA-3072 with OAEP-SHA256 caps at 318 bytes. To encrypt anything larger, use hybrid encryption: encrypt the file with AES-256-GCM and encrypt only the random AES key with RSA, as shown in Step 7.
What is the difference between OAEP and PKCS#1 v1.5 padding?
OAEP (Optimal Asymmetric Encryption Padding) is the modern, randomized padding with a security proof. PKCS#1 v1.5 is the legacy scheme vulnerable to Bleichenbacher padding-oracle attacks. Use RSA_PKCS1_OAEP_PADDING with SHA-256 for all new encryption. Reserve PKCS#1 v1.5 only for verifying signatures from old systems.
Should I use RSA or ECC for a new project?
If you control both ends and have no compatibility constraints, elliptic-curve cryptography (ECC) is usually the better choice: a 256-bit ECC key matches the 128-bit security of RSA-3072 with much faster operations and smaller keys. Choose RSA when you must interoperate with existing RSA certificates, RS256 JWTs, or legacy partners.
Do I need a third-party library for RSA in Node.js?
No. The built-in node:crypto module covers key generation, encryption, decryption, signing, and verification with OAEP and PSS padding. This tutorial uses zero external dependencies. Avoid pulling in extra crypto packages unless you have a specific need they cover, since each one widens your attack surface.
Will quantum computers break my RSA-encrypted data?
Not today, because no quantum computer can yet run Shor’s algorithm at the scale needed to factor a 3072-bit modulus. The real concern is harvest-now-decrypt-later: data captured in 2026 could be decrypted once the hardware arrives. For secrets that must stay confidential into the 2030s, begin testing NIST’s post-quantum standards (ML-KEM, ML-DSA) and plan a hybrid migration.
How often should I rotate RSA keys?
Rotation frequency depends on exposure. A common policy is 90 days for application-level keys, with the old public key retained long enough to verify in-flight signatures. Certificate authority roots rotate far less often but use larger keys (RSA-4096). The key rule is to make rotation automatic so a compromised key has a short, bounded blast radius.
What public exponent should I use?
Use 65537 (hex 0x10001), the value Node defaults to and the universal standard. It is large enough to resist low-exponent attacks like Coppersmith’s, yet small enough to keep signature verification fast. Do not lower it to 3, which has enabled real-world attacks, and do not invent a custom value.
Related Coverage
- AES-256 Encryption in Node.js: 12 Steps
- Argon2 Password Hashing in Node.js: 11 Steps
- JWT Authentication in Node.js: 10 Steps
- Digital Signatures Explained: How They Work and Why Hashes Matter
- SHA-256 Explained: How It Works and Why It Matters
- Post-Quantum Cryptography: 50% of Web Now Safe
- Hashing and Cryptography Explained
Authoritative References
- Node.js crypto module documentation
- NIST SP 800-57 Part 1 Rev. 5: Key Management Recommendations
- RFC 8017: PKCS #1 RSA Cryptography Specifications v2.2
- NIST Post-Quantum Cryptography Project
Last updated June 14, 2026. Code tested on Node.js 22.14.0 with OpenSSL 3.0.15.




