The Web Crypto API is a browser-native API for performing cryptographic operations in JavaScript. It supports RSA key generation, encryption, decryption, signing, and verification, all without any third-party libraries. If you need to do RSA in the browser, this is the right tool for the job.
Let's walk through the practical steps of using the Web Crypto API for RSA operations.
Why Use the Web Crypto API?
Before the Web Crypto API, JavaScript crypto relied on pure-JavaScript libraries like Forge or jsencrypt. These worked but had drawbacks: they were slower than native implementations, they had larger bundle sizes, and they were harder to audit for correctness.
The Web Crypto API provides: - **Native performance**: Cryptographic operations run in native code, not JavaScript. - **No dependencies**: Built into every modern browser. No npm install needed. - **Secure random**: Uses the operating system's CSPRNG for key generation. - **Well-tested implementations**: Maintained by browser vendors, not third parties.
The API is available as window.crypto.subtle (or just crypto.subtle in modern contexts). All operations are asynchronous and return Promises.
Generating an RSA Key Pair
The first step is generating an RSA key pair. Here is how to generate a 2048-bit RSA-OAEP key pair:
javascript async function generateRSAKeyPair() { const keyPair = await crypto.subtle.generateKey( { name: 'RSA-OAEP', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), // 65537 hash: 'SHA-256' }, true, // extractable ['encrypt', 'decrypt'] // usages ); return keyPair; }
The parameters: - **name**: The algorithm. "RSA-OAEP" for encryption, "RSA-PSS" for signing. - **modulusLength**: The key size in bits. 2048 is the standard. - **publicExponent**: Typically 65537, encoded as [1, 0, 1] in big-endian bytes. - **hash**: The hash function used with OAEP or PSS. SHA-256 is recommended. - **extractable**: Whether the key can be exported. Set to true if you need to export the key to PEM. - **usages**: What operations the key can be used for.
For signing keys, use "RSA-PSS" and ['sign', 'verify']:
javascript async function generateRSASigningKeyPair() { return await crypto.subtle.generateKey( { name: 'RSA-PSS', modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: 'SHA-256' }, true, ['sign', 'verify'] ); }
## Exporting Keys to PEM
The Web Crypto API exports keys in binary formats (SPKI for public keys, PKCS8 for private keys). To convert these to PEM format (the text format used by OpenSSL and most tools), you need to Base64-encode them and add headers.
javascript async function exportPublicKeyToPEM(publicKey) { const spki = await crypto.subtle.exportKey('spki', publicKey); const base64 = arrayBufferToBase64(spki); return -----BEGIN PUBLIC KEY-----\n${formatPEM(base64)}\n-----END PUBLIC KEY-----; }
async function exportPrivateKeyToPEM(privateKey) { const pkcs8 = await crypto.subtle.exportKey('pkcs8', privateKey); const base64 = arrayBufferToBase64(pkcs8); return -----BEGIN PRIVATE KEY-----\n${formatPEM(base64)}\n-----END PRIVATE KEY-----; }
function formatPEM(base64) { // Split into 64-character lines return base64.match(/.{1,64}/g).join('\n'); }
function arrayBufferToBase64(buffer) { const bytes = new Uint8Array(buffer); let binary = ''; for (const byte of bytes) { binary += String.fromCharCode(byte); } return btoa(binary); }
## Importing Keys from PEM
To import a PEM-formatted key, reverse the process:
javascript async function importPublicKeyFromPEM(pem, usages = ['encrypt']) { const base64 = pem .replace(/-----BEGIN PUBLIC KEY-----/, '') .replace(/-----END PUBLIC KEY-----/, '') .replace(/\n/g, ''); const buffer = base64ToArrayBuffer(base64); return await crypto.subtle.importKey( 'spki', buffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, usages ); }
function base64ToArrayBuffer(base64) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } return bytes.buffer; }
## Encrypting and Decrypting
Once you have keys, you can encrypt and decrypt data:
javascript async function encrypt(publicKey, data) { const encoded = new TextEncoder().encode(data); const ciphertext = await crypto.subtle.encrypt( { name: 'RSA-OAEP' }, publicKey, encoded ); return ciphertext; }
async function decrypt(privateKey, ciphertext) { const decrypted = await crypto.subtle.decrypt( { name: 'RSA-OAEP' }, privateKey, ciphertext ); return new TextDecoder().decode(decrypted); }
Remember: RSA can only encrypt messages up to about 245 bytes with a 2048-bit key and OAEP padding. For larger data, use hybrid encryption (RSA encrypts an AES key, AES encrypts the data). See our hybrid encryption guide for details.
Signing and Verifying
For signing, use RSA-PSS:
javascript async function sign(privateKey, data) { const encoded = new TextEncoder().encode(data); const signature = await crypto.subtle.sign( { name: 'RSA-PSS', saltLength: 32 }, privateKey, encoded ); return signature; }
async function verify(publicKey, signature, data) { const encoded = new TextEncoder().encode(data); return await crypto.subtle.verify( { name: 'RSA-PSS', saltLength: 32 }, publicKey, signature, encoded ); }
The saltLength parameter specifies the length of the random salt used by PSS. 32 bytes (256 bits) is standard with SHA-256.
Important Considerations
**Key storage**: The Web Crypto API does not provide key storage. Keys exist in memory and are lost when the page is unloaded. For persistent keys, export to PEM and store them securely (in a backend, in IndexedDB with encryption, or in a dedicated key management system).
**Never expose private keys**: If you export a private key to PEM in the browser, it is accessible to any JavaScript on the page. For production applications, keep private keys on the server and only use the Web Crypto API for public key operations in the browser.
**Algorithm consistency**: When importing keys, the algorithm parameters must match what was used to generate the key. If you generated a key with RSA-OAEP and SHA-256, you must import it with RSA-OAEP and SHA-256.
**Performance**: RSA key generation takes 100ms to 1000ms. Do it once and reuse the key. Do not generate keys in hot paths.
The Bottom Line
The Web Crypto API is the right way to do RSA in the browser. It is native, fast, secure, and has no dependencies. For most use cases, you will generate keys on the server, send the public key to the browser, and use the Web Crypto API for encryption or signature verification in the client.
For RSA operations on the server side, use Node.js's built-in crypto module, which provides similar functionality with a more comprehensive API. And for a deeper understanding of RSA, read our RSA encryption explained guide. You can also experiment with RSA operations using our RSA Encrypt/Decrypt tool.