The Web Crypto API gives browsers native access to cryptographic operations: hashing, symmetric encryption, asymmetric key pairs, digital signatures, and random number generation. It is fast, secure, and available in every modern browser. It is also easy to misuse in ways that create a false sense of security.
This post covers what the Web Crypto API does well, the common mistakes developers make with client-side cryptography, and practical patterns for using it correctly.
What the Web Crypto API Provides
The API lives at window.crypto.subtle (the "subtle" name is a reminder that cryptography is easy to get wrong). All operations are asynchronous and return Promises. The key operations:
- Hashing -- SHA-1, SHA-256, SHA-384, SHA-512
- Symmetric encryption -- AES-CBC, AES-GCM, AES-CTR
- Asymmetric encryption -- RSA-OAEP
- Signing -- HMAC, RSA-PSS, ECDSA
- Key derivation -- PBKDF2, HKDF
- Random values --
crypto.getRandomValues()
Hashing: The Safe Use Case
Hashing is the most straightforward use of client-side crypto. Computing a SHA-256 hash of user input or a file has no security gotchas -- the hash is deterministic, there are no keys to manage, and the result is the same whether computed on the client or server.
async function sha256(message) {
var encoder = new TextEncoder();
var data = encoder.encode(message);
var hash = await crypto.subtle.digest("SHA-256", data);
var bytes = new Uint8Array(hash);
return Array.from(bytes)
.map(function(b) { return b.toString(16).padStart(2, "0"); })
.join("");
}
// Usage
sha256("hello world").then(function(h) {
console.log(h);
// "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
});
This is useful for file integrity checks (let users verify a download hash), generating deterministic IDs, or building tools like the hash generator on this site.
Encryption: Where It Gets Tricky
Client-side encryption is where most developers make mistakes. The API makes it easy to encrypt data, but the hard part is not the encryption -- it is the key management.
AES-GCM is the recommended algorithm for symmetric encryption. It provides both confidentiality and authenticity (it detects tampering). Here is a correct implementation:
async function encrypt(plaintext, password) {
var encoder = new TextEncoder();
var salt = crypto.getRandomValues(new Uint8Array(16));
var iv = crypto.getRandomValues(new Uint8Array(12));
// Derive a key from the password
var keyMaterial = await crypto.subtle.importKey(
"raw", encoder.encode(password), "PBKDF2", false,
["deriveKey"]
);
var key = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt: salt, iterations: 600000,
hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false, ["encrypt"]
);
var ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: iv },
key,
encoder.encode(plaintext)
);
// Concatenate salt + iv + ciphertext for storage
var result = new Uint8Array(
salt.length + iv.length + ciphertext.byteLength
);
result.set(salt, 0);
result.set(iv, salt.length);
result.set(new Uint8Array(ciphertext), salt.length + iv.length);
return result;
}
The critical details that are easy to get wrong:
- Never reuse an IV (initialization vector). Generate a fresh random IV for every encryption operation. Reusing an IV with the same key completely breaks AES-GCM security.
- Use PBKDF2 with high iterations for password-derived keys. 600,000 iterations is the current OWASP recommendation for SHA-256. This makes brute-force attacks on the password slow.
- Store the salt and IV alongside the ciphertext. They are not secret -- they just need to be unique. The decryption side needs them to reconstruct the key and decrypt.
The Fundamental Limitation
Here is the uncomfortable truth about client-side encryption: the code that does the encryption is served by the same server the user might be trying to protect their data from.
If you build a web app that encrypts data client-side before sending it to your server, a malicious server operator (or an attacker who compromises the server) can simply serve modified JavaScript that skips the encryption or exfiltrates the key. The user has no way to verify that the JavaScript they received is the same JavaScript you wrote.
This does not mean client-side encryption is useless. It is valuable for:
- Local-only tools. A password generator or hash calculator that never sends data to a server. The threat model is different -- you are protecting against network eavesdropping, not a malicious server.
- Defense in depth. Encrypting data client-side before upload adds a layer even if the server is compromised. It is not a guarantee, but it raises the bar.
- End-to-end encrypted apps where the client code is distributed as a native app or browser extension, which users can audit and which does not change on every page load.
Random Number Generation
crypto.getRandomValues() is the one function you should always use instead of Math.random() for anything security-related. Math.random() is not cryptographically secure -- its output is predictable if you know the seed.
// Generate a random UUID (v4)
function randomUUID() {
if (crypto.randomUUID) return crypto.randomUUID();
var bytes = crypto.getRandomValues(new Uint8Array(16));
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
var hex = Array.from(bytes)
.map(function(b) { return b.toString(16).padStart(2, "0"); })
.join("");
return hex.slice(0,8) + "-" + hex.slice(8,12) + "-" +
hex.slice(12,16) + "-" + hex.slice(16,20) + "-" +
hex.slice(20);
}
Note that crypto.randomUUID() is now available natively in all modern browsers and is the simplest option for generating UUIDs.
Practical Guidelines
- Use Web Crypto API, never roll your own crypto. No custom XOR encryption, no hand-built hash functions, no "clever" obfuscation schemes.
- Use AES-GCM for symmetric encryption. It is the most robust option the API provides. Avoid AES-CBC unless you have a specific reason and understand the padding oracle risks.
- Generate random values with
crypto.getRandomValues(), never withMath.random(). - Wrap all crypto operations in try/catch. The API throws on invalid parameters, unsupported algorithms, and in non-secure contexts (HTTP). Handle failures gracefully.
- Be honest about your threat model. Client-side crypto protects data in transit and at rest on the client. It does not protect against a compromised server serving malicious code.
The Web Crypto API is a powerful tool that every web developer should know. But crypto is only as strong as the weakest link in the system, and in a web application, that weakest link is usually not the algorithm -- it is the key management, the threat model assumptions, or the JavaScript delivery mechanism. Use it where it genuinely helps, and be clear-eyed about where it does not.