When I first started building encrypted client-side applications, my first instinct was to reach for a third-party library like CryptoJS or Forge. They are feature-rich, but they come with a cost: bundle size, slower execution, and the inherent risk of introducing a vulnerability through a dependency. This led me to ask: can I use Web Crypto API for production, or is it too experimental?

The short answer is yes. In fact, for most modern applications, you should use the Web Crypto API. It is a native browser implementation that provides hardware-accelerated cryptographic operations, making it significantly faster and more secure than any JavaScript-based alternative. However, ‘production-ready’ doesn’t mean ‘foolproof.’ There are specific constraints regarding secure contexts and key management that you must understand before shipping.

The Fundamentals of Web Crypto API

The Web Crypto API provides a low-level interface for performing cryptographic operations. Unlike legacy libraries, it doesn’t run in the main JS execution thread in the same way; it leverages the browser’s internal C++ implementation, which is optimized for the underlying CPU architecture.

Before diving into the code, it’s important to understand cryptography basics for developers. The Web Crypto API focuses on three main areas: Key Generation, Encryption/Decryption, and Hashing/Signing.

The ‘Secure Context’ Requirement

One critical caveat for production: the Web Crypto API is only available in Secure Contexts. This means your site must be served over HTTPS or localhost. If you try to access window.crypto.subtle on an insecure HTTP connection, it will be undefined. This is a non-negotiable security measure to prevent man-in-the-middle attacks from injecting malicious crypto logic.

Deep Dive: Production Implementation Strategies

1. Generating and Managing Keys

In my experience, the biggest mistake developers make is handling raw key material in JavaScript strings. The Web Crypto API solves this by using CryptoKey objects. These objects can be marked as extractable: false, meaning the private key never actually touches your JavaScript variables in a way that can be easily leaked via an XSS attack.

async function generateProductionKey() {
  const keyPair = await window.crypto.subtle.generateKey(
    {
      name: "RSA-OAEP",
      modulusLength: 4096,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: "SHA-256",
    },
    false, // Not extractable! The private key stays in the browser's secure storage
    ["encrypt", "decrypt"]
  );
  return keyPair;
}

2. Symmetric vs. Asymmetric Encryption

For production, you need to choose the right tool for the job. I typically use AES-GCM for bulk data encryption because it provides authenticated encryption, ensuring the data hasn’t been tampered with. For key exchange, I stick to RSA-OAEP or ECDH.

3. Handling Hashing and Passwords

While crypto.subtle.digest('SHA-256', data) is great for integrity checks, it is not suitable for password hashing. Remember, SHA-256 is too fast, making it vulnerable to GPU brute-forcing. If you are handling passwords on the client side before sending them to a server, I highly recommend using Argon2 in web apps via WebAssembly for a memory-hard alternative.

Comparison of JS-based crypto libraries vs native Web Crypto API performance
Comparison of JS-based crypto libraries vs native Web Crypto API performance

Implementing a Secure Workflow

To move from a prototype to a production-grade system, follow this architectural flow: generate a temporary session key (AES), encrypt the data, and then encrypt that session key using the recipient’s public RSA key. This hybrid approach gives you the speed of symmetric encryption with the security of asymmetric key distribution.

As shown in the architecture diagram above, the key is to minimize the time sensitive data exists in a decrypted state within the browser’s memory.

Secure Key Storage

Where do you store the keys? localStorage is a disaster for crypto keys because it’s accessible by any script on your origin. Instead, I use IndexedDB to store CryptoKey objects. Since IndexedDB can store the structured CryptoKey object directly, the browser can keep the key non-extractable while still persisting it across sessions. For more on this, check out my guide on secure key storage.

Principles for Production Crypto

Tools for Testing and Debugging

Debugging crypto is notoriously difficult because the output is binary (ArrayBuffers). I recommend using a small helper function to convert these to Base64 for logging during development:

const bufferToBase64 = buf => btoa(String.fromCharCode(...new Uint8Array(buf)));

For auditing your implementation, I suggest using the browser’s Application Tab in Chrome DevTools to inspect IndexedDB and ensure your keys aren’t accidentally stored as plain text strings.

Case Study: End-to-End Encrypted Chat

I recently implemented a small E2EE chat prototype using this API. By leveraging window.crypto.subtle, I reduced the initial bundle size by 120KB (by removing a heavy JS library) and saw a 4x increase in encryption speed on mobile devices. The biggest hurdle was handling the ArrayBuffer conversions, but the performance gains in production were undeniable.

If you’re building something similar, start by defining your key rotation policy early. Production crypto isn’t just about the algorithm; it’s about how you handle keys over time.