Web Crypto API Client-Side Encryption (2026 Guide)
February 24, 2026 · Web Development, Security, JavaScript
Client-side encryption is no longer just for password managers and messaging apps. In 2026, more web applications encrypt sensitive data before it leaves the browser to reduce breach impact, comply with privacy requirements, and keep user trust. The Web Crypto API makes this possible with native, performant cryptography in modern browsers.
This guide walks through practical client-side encryption using Crypto.subtle with real-world patterns, key management considerations, and copy‑pasteable code. You’ll learn how to encrypt JSON payloads with AES‑GCM, derive keys from passwords via PBKDF2, and safely serialize binary data. It’s the kind of reference you can bookmark and return to when implementing encryption in your own app.
What is the Web Crypto API (and why use it)?
The Web Crypto API is a browser-native cryptography interface that exposes secure primitives like AES, HMAC, PBKDF2, and elliptic curve operations. It runs in optimized, vetted native code rather than slow, error-prone JavaScript crypto libraries.
Key benefits:
- Security: Uses hardened implementations provided by the browser.
- Performance: Faster than JS-based crypto libraries.
- Standardized: Supported in all modern browsers (Chrome, Firefox, Safari, Edge).
Client-side encryption: realistic use cases
Here are common patterns where client-side encryption makes sense:
- Encrypting notes, files, or secrets before syncing to cloud storage.
- End-to-end encryption for messaging or collaboration apps.
- Protecting backups stored in third-party services.
- Reducing data exposure for compliance (HIPAA, GDPR, SOC 2).
Remember: encryption only helps if keys are protected. The browser can be secure, but a compromised device or malicious extension can still access data. The goal is to limit the server’s ability to read sensitive content.
Core primitives you’ll use
- AES-GCM: Authenticated encryption for confidentiality and integrity.
- PBKDF2: Password-based key derivation.
- HKDF: Derive multiple keys from a single master key (optional).
- Random values: Use crypto.getRandomValues for IVs and salts.
Encrypting data with AES-GCM
AES-GCM is the go-to algorithm for client-side encryption. It provides both encryption and integrity checks. If the ciphertext is altered, decryption fails.
Step 1: Generate a key
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
Step 2: Encrypt JSON data
const encoder = new TextEncoder();
const data = { message: "Hello, encrypted world!", ts: Date.now() };
const plaintext = encoder.encode(JSON.stringify(data));
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
Step 3: Serialize for storage or transport
Web Crypto returns an ArrayBuffer. For storage, convert to Base64 or JSON-friendly formats.
const toBase64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
const encryptedPayload = {
iv: toBase64(iv),
ciphertext: toBase64(ciphertext)
};
Want to inspect output? Use the Base64 Encoder/Decoder to validate your serialized values. If you want to view the JSON cleanly, open it in the JSON Formatter.
Step 4: Decrypt
const fromBase64 = (b64) => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const ivBytes = fromBase64(encryptedPayload.iv);
const ctBytes = fromBase64(encryptedPayload.ciphertext);
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: ivBytes },
key,
ctBytes
);
const decoded = new TextDecoder().decode(decrypted);
const result = JSON.parse(decoded);
Deriving keys from passwords with PBKDF2
Many applications need a password to unlock data. Instead of using the password directly, derive a cryptographic key with PBKDF2. This adds computational cost and mitigates brute-force attacks.
Key derivation example
const getKeyFromPassword = async (password, salt) => {
const enc = new TextEncoder();
const baseKey = await crypto.subtle.importKey(
"raw",
enc.encode(password),
{ name: "PBKDF2" },
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt,
iterations: 250000,
hash: "SHA-256"
},
baseKey,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
};
Putting it together
const salt = crypto.getRandomValues(new Uint8Array(16));
const password = "correct horse battery staple";
const key = await getKeyFromPassword(password, salt);
// Use the key with AES-GCM as shown earlier
Store the salt alongside the ciphertext. It’s not secret. The salt prevents identical passwords from producing the same key.
Key storage: export vs. non-exportable
You can generate keys as exportable or non-exportable. Exportable keys can be serialized and stored, but they’re easier to leak. For security, prefer non-exportable keys and re-derive from the user’s password when needed.
If you must store a key (for performance or offline use), store it in IndexedDB using CryptoKey objects (supported in most modern browsers). Avoid localStorage for keys; it’s too easy to exfiltrate.
Working with binary data
Files and blobs can be encrypted too. You’ll typically read them as ArrayBuffer and pass to crypto.subtle.encrypt.
const arrayBuffer = await file.arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
arrayBuffer
);
Store the file name and metadata separately (or inside encrypted JSON).
Security gotchas to avoid
- Never reuse IVs with the same key in AES-GCM. Always generate a new random IV.
- Don’t invent your own crypto. Use AES-GCM, PBKDF2, and supported primitives.
- Be careful with password strength. Weak passwords are easy to brute-force even with PBKDF2. Add UI guidance and optional password strength checks (the Regex Tester can help design validation rules).
- Verify your JSON format for storage or transport. It’s easy to corrupt JSON; use the JSON Formatter to check.
Client-side encryption design patterns
1) Zero-knowledge storage
Encrypt data in the browser and store it on the server. The server never sees plaintext. When the user logs in, you derive the key from their password and decrypt locally.
2) Encrypted sync + device keys
Generate a random master key (exportable), encrypt it with a password-derived key, and store the encrypted master key on the server. This allows password changes without re-encrypting all data.
3) Sharing encrypted content
Use asymmetric cryptography (ECDH or RSA-OAEP) to encrypt a symmetric key for multiple recipients. The Web Crypto API supports ECDH and RSA-OAEP, though the workflow is more advanced than AES-GCM.
Practical example: encrypting a JSON note
This example combines everything into a compact, reusable set of functions.
const enc = new TextEncoder();
const dec = new TextDecoder();
const toBase64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf)));
const fromBase64 = (b64) => Uint8Array.from(atob(b64), c => c.charCodeAt(0));
const deriveKey = async (password, salt) => {
const baseKey = await crypto.subtle.importKey(
"raw",
enc.encode(password),
{ name: "PBKDF2" },
false,
["deriveKey"]
);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 250000, hash: "SHA-256" },
baseKey,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
};
const encryptJson = async (obj, password) => {
const salt = crypto.getRandomValues(new Uint8Array(16));
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await deriveKey(password, salt);
const plaintext = enc.encode(JSON.stringify(obj));
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, plaintext);
return {
salt: toBase64(salt),
iv: toBase64(iv),
ciphertext: toBase64(ciphertext)
};
};
const decryptJson = async (payload, password) => {
const salt = fromBase64(payload.salt);
const iv = fromBase64(payload.iv);
const ciphertext = fromBase64(payload.ciphertext);
const key = await deriveKey(password, salt);
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
return JSON.parse(dec.decode(decrypted));
};
You can test the payload and make sure it’s valid JSON using the JSON Formatter. If you need to safely store the payload in a query string (not recommended for secrets), use the URL Encoder/Decoder to avoid encoding issues.
Performance considerations
- PBKDF2 iterations add latency. Tune iterations based on target devices (e.g., 250k for desktop, 150k for mobile).
- AES-GCM is fast and should handle large payloads well.
- For large files, consider chunked encryption to avoid memory spikes.
Testing and debugging tips
- Always test decryption after encryption in the same session.
- Log base64-encoded IV and ciphertext when debugging (never in production).
- Use the Base64 Encoder/Decoder to verify serialized binary data.
- Validate JSON structures with the JSON Formatter.
FAQ
Is the Web Crypto API secure enough for production?
Yes. It uses native cryptography implementations from the browser and is the recommended standard for client-side crypto in modern web apps. Security depends on correct usage and strong key management.
Can I use Web Crypto API in Node.js?
Yes. Node’s webcrypto module offers compatibility with the browser API. The patterns in this article transfer with minor adjustments.
Should I store encryption keys in localStorage?
No. localStorage is too easy to read from XSS attacks. Prefer non-exportable keys and re-derive from a user password, or store CryptoKey objects in IndexedDB.
Does AES-GCM provide integrity checking?
Yes. AES-GCM is authenticated encryption. If data is modified, decryption fails. This is why AES-GCM is generally preferred for client-side encryption.
Category: Web Development — Date: February 24, 2026
Recommended Tools & Resources
Level up your security knowledge:
Cloudflare Workers → Web Application Security by Andrew Hoffman → Clean Code by Robert C. Martin →