Cryptography

Using the Web Crypto API for RSA: A Practical Guide

9 min read
By
Using the Web Crypto API for RSA: A Practical Guide

Photo by Francesco Ungaro from Pexels

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.

Frequently Asked Questions

Is the Web Crypto API secure for RSA operations?

Yes. The Web Crypto API uses the browser’s native cryptographic implementation, which is typically backed by operating system-level crypto libraries. It uses cryptographically secure random number generators and well-tested implementations of RSA. It is more secure than JavaScript-based crypto libraries because the actual cryptographic operations happen in native code, not in JavaScript.

Can I use the Web Crypto API in Node.js?

Node.js has its own crypto module (require("crypto")) which provides similar functionality. As of Node.js 19+, the global crypto.subtle API is also available, providing the same interface as the browser. For cross-platform code, you can use the Web Crypto API in both environments, but for Node.js-specific projects, the native crypto module is more feature-rich.

How do I export an RSA key to PEM format using the Web Crypto API?

Use crypto.subtle.exportKey("spki", publicKey) for public keys or crypto.subtle.exportKey("pkcs8", privateKey) for private keys. This returns an ArrayBuffer containing the DER-encoded key. Convert it to PEM by Base64-encoding the buffer and wrapping it with "-----BEGIN PUBLIC KEY-----" and "-----END PUBLIC KEY-----" headers.

Why are Web Crypto API operations asynchronous?

Web Crypto API operations return Promises because cryptographic operations can take significant time (especially RSA key generation, which can take seconds). Making them asynchronous prevents blocking the main thread, keeping the UI responsive. The actual cryptographic work happens in a separate thread within the browser implementation.

Try NovelCrypt Tools

Experience military-grade encryption for your sensitive data. Create self-destructing messages, encrypt files, or explore our experimental lab tools.

Explore NovelCrypt