How to use the Web Crypto API in the browser: hashing, signing, and encryption
The Web Crypto API gives every browser a built-in cryptography engine. Learn how to hash data, sign and verify messages, and encrypt or decrypt content using SubtleCrypto — with practical code examples and security notes.
On this page
- What the Web Crypto API is
- Key concepts
- Keys are objects, not strings
- Symmetric vs asymmetric
- Timing-safe comparison
- Hashing data
- Signing and verifying messages
- Encrypting and decrypting data
- Key derivation
- Common mistakes to avoid
- FAQ
- What is the difference between hashing and encryption?
- Can I use Web Crypto in a Web Worker?
- Is Web Crypto secure for production use?
- How does Web Crypto compare to Node.js crypto?
- Do I need a library for client-side encryption?
The Web Crypto API is a browser built-in that gives JavaScript access to cryptographic primitives. You do not need a library to hash a password, sign a message, or encrypt data. Every modern browser ships with it, accessible via window.crypto.subtle. It runs in the main thread and in Web Workers, and since 2015 it has been Baseline Widely Available across all major browsers.
This guide covers what the API actually does, how its key concepts work, and the code patterns you need to hash data, create and verify digital signatures, and encrypt or decrypt content safely.
What the Web Crypto API is
The Web Crypto API surfaces low-level cryptographic operations through the SubtleCrypto interface. Its entry point is window.crypto.subtle (or self.crypto.subtle in a Worker). The API is deliberately low-level — it does not hold your hand. You choose the algorithm, manage the key material, and handle the raw bytes. This is both its power and its risk: it is easy to use correctly for standard patterns and easy to misuse in subtle ways.
The API requires a secure context — it only works on pages served over HTTPS (or localhost). Any page loaded over plain HTTP throws a DOMException.
The operations it provides fall into three groups:
- Hashing — produce a fixed-length digest from arbitrary data (
digest()). - Signing — create or verify a digital signature with a key pair (
sign(),verify()). - Encryption — encrypt or decrypt with a symmetric key (
encrypt(),decrypt()). - Key management — generate, derive, import, export, and wrap keys (
generateKey(),deriveKey(),importKey(),exportKey(),wrapKey(),unwrapKey()).
All methods return Promises and operate on ArrayBuffer or TypedArray data, not strings. You encode strings with TextEncoder and decode responses with TextDecoder.
Key concepts
Keys are objects, not strings
Cryptographic keys in the Web Crypto API are CryptoKey objects. They carry metadata — the algorithm, the key type (secret, private, or public), and whether they are extractable. You never pass raw bytes as a key; you always create or import a CryptoKey first.
Symmetric vs asymmetric
- Symmetric algorithms use the same key to encrypt and decrypt (AES). The key must be kept secret.
- Asymmetric algorithms use a key pair: a public key for encryption or signature verification, and a private key that stays on your side. RSA and ECDSA are asymmetric.
For most browser-based tasks — encrypting local data, signing a token, hashing a password — symmetric (AES) is simpler and faster. Use asymmetric when you need to share the encryption key or when the other party needs a public key for verification.
Timing-safe comparison
Never use === to compare two strings or byte sequences for equality in security-sensitive code. An attacker can measure how long the comparison takes and use the timing difference to deduce the secret. The Web Crypto API provides crypto.timingSafeEqual() for exactly this — always use it when comparing MACs, signatures, or any secret byte sequence.
Hashing data
Hashing turns any input into a fixed-length "fingerprint" in a one-way direction. You cannot recover the original from the hash. SHA-256 (256-bit output) is the most widely used browser-safe hash. SHA-384 and SHA-512 are also available. MD5 and SHA-1 are considered broken for security purposes.
A common use is deriving a key from a password, or creating a quick integrity check for a payload.
const encoder = new TextEncoder();
const data = encoder.encode("The quick brown fox");
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join("");
console.log(hashHex);
// 4d156c9271fd4f5e99e27fc2e84d3f8f3c0e5b8f7d2a6e1c9b3d...
sha-256 is the string identifier for SHA-256. The output is an ArrayBuffer of 32 bytes (256 bits). Convert it to hex for readability, or keep it as bytes for further processing.
For hashing passwords before storing them, consider using PBKDF2 (via deriveKey) rather than a plain hash — it applies multiple iterations to slow down brute-force guesses. For a deeper look at hashing algorithms and their properties, see the SHA-2 article on Wikipedia.
Signing and verifying messages
Digital signatures let you prove that a message was created by the holder of a private key. The recipient uses the matching public key to verify. This is asymmetric cryptography: you sign with the private key, anyone can verify with the public key.
The Web Crypto API supports ECDSA (Elliptic Curve Digital Signature Algorithm) and RSASSA-PKCS1-v1_5. ECDSA is faster and produces smaller signatures — use it for new work.
// Generate a key pair
const keyPair = await crypto.subtle.generateKey(
{ name: "ECDSA", namedCurve: "P-256" },
true, // extractable
["sign", "verify"]
);
const encoder = new TextEncoder();
const message = encoder.encode("I authorize this transfer");
// Sign
const sigBuffer = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" },
keyPair.privateKey,
message
);
// Export public key to share
const pubKeyBuffer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
// Verify
const valid = await crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" },
keyPair.publicKey,
sigBuffer,
message
);
console.log("Signature valid:", valid); // true
The signature is a fixed-length byte sequence tied to the message and the private key. If the message or the signature changes, verification fails. This pattern is used for signing JWTs, API tokens, and document attestations. For a practical guide on JWT signing in a Node.js API, see the JWT authentication guide.
Encrypting and decrypting data
Encryption turns readable data (plaintext) into unreadable data (ciphertext) using a key. The Web Crypto API supports three AES modes:
- AES-GCM — authenticated encryption (AEAD). The recommended mode. It detects tampering automatically. Requires a 12-byte initialization vector (IV) that must be unique per encryption (but does not need to be secret).
- AES-CBC — block cipher chaining. Requires a 16-byte IV. No built-in authentication — you must not rely on it alone for security-critical data.
- AES-CTR — counter mode. Requires an IV and a counter. Rarely needed for new work.
Always prefer AES-GCM. It is the only AES mode in Web Crypto that provides authenticated encryption out of the box, which protects against chosen-ciphertext attacks where an attacker asks the system to decrypt arbitrary messages and uses the result to learn about the key.
const encoder = new TextEncoder();
const plaintext = encoder.encode("Secret message");
// Generate a random 256-bit key
const key = await crypto.subtle.generateKey(
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
// IV must be unique per encryption — use random bytes
const iv = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv },
key,
plaintext
);
// Store or transmit: key (kept secret), iv (can be stored alongside ciphertext), ciphertext
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv },
key,
ciphertext
);
const decoder = new TextDecoder();
console.log(decoder.decode(decrypted)); // "Secret message"
Store the IV alongside the ciphertext — it is not secret, only unique. For a client-side application that encrypts local data (say, a note-taking app that stores data in IndexedDB), this pattern is all you need: generate a key, encrypt, store the key in localStorage or derive it from a password.
For storing encryption keys securely in a server-side application, see the environment variables guide — never hardcode secrets. For secret management at scale, HashiCorp Vault is purpose-built for this. See the Vault alternative page for a comparison.
Key derivation
Deriving a key lets you produce a cryptographic key from a password or another key material. The Web Crypto API supports PBKDF2 (Password-Based Key Derivation Function 2) and HKDF (HMAC-based Key Derivation Function). PBKDF2 is the right choice when you have a password and want a key.
const encoder = new TextEncoder();
const password = encoder.encode("user-password-123");
// Salt must be unique per password — store it alongside the derived key
const salt = crypto.getRandomValues(new Uint8Array(16));
const derivedKey = await crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 310000, hash: "SHA-256" },
await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"]),
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
310,000 iterations is the current OWASP minimum for PBKDF2 with SHA-256 (as of 2023). The salt must be unique per user and stored in plaintext alongside the encrypted data — it is not secret, only makes precomputed attacks impossible.
Common mistakes to avoid
The Web Crypto API is low-level and easy to misuse. These are the patterns that cause real security incidents:
- Reusing an IV (nonce) with AES-GCM or AES-CTR — if the same IV is used twice with the same key, an attacker can recover both ciphertexts. Always generate a fresh random IV with
crypto.getRandomValues()for every encryption operation. - Using AES-CBC or AES-CTR without message authentication — without authentication, an attacker can flip bits in the ciphertext and the decryption will produce modified output with no error. Use AES-GCM to get authentication automatically, or implement HMAC over the ciphertext if you must use CBC/CTR.
- Hardcoding a key in source code — keys in code are in version control and logs. Derive them from a password, store them in
localStorage(client-side), or retrieve them from a secret manager. For server-side patterns, see the environment variables guide. - Using MD5 or SHA-1 for anything security-related — both are considered broken for collision resistance. SHA-256 is the minimum standard.
- Ignoring secure context requirements — the API throws in non-HTTPS contexts. Do not build features that silently degrade without noticing.
- Using
===for secret comparison — always usecrypto.timingSafeEqual()to compare MACs, signatures, and derived keys.
For a broader view of the surrounding ecosystem — API design, testing, monitoring — see the API explained guide.
FAQ
What is the difference between hashing and encryption?
Hashing is one-way: given data, you produce a fixed digest. You cannot recover the original from the digest. Encryption is two-way: given ciphertext and the correct key, you recover the plaintext. Use hashing for integrity checks and password storage; use encryption when the original data must be readable again.
Can I use Web Crypto in a Web Worker?
Yes. crypto.subtle is available in Workers via self.crypto.subtle. Running crypto operations in a Worker keeps them off the main thread and prevents the UI from stuttering during heavy operations like key derivation or large-file encryption.
Is Web Crypto secure for production use?
Yes, when used correctly. The underlying algorithms (AES-GCM, ECDSA, SHA-256) are well-vetted and implemented in browser-native code that is generally faster than JavaScript polyfills. The risk is in the usage — wrong algorithm choices, IV reuse, missing authentication, or hardcoded keys. The W3C specification itself carries a warning that the primitives are easy to misuse.
How does Web Crypto compare to Node.js crypto?
Node.js crypto module (and the node:crypto API since v15) provides roughly the same algorithms and often more. The Web Crypto API is the browser subset of those primitives, designed for the browser's threat model. Both use the same underlying native implementations on the system — Web Crypto in browsers, Node.js directly in server-side code. If you write code that works with window.crypto.subtle, the same patterns work with crypto.subtle in Node.js v15+.
Do I need a library for client-side encryption?
For the operations in this guide, no — the built-in API covers hashing, signing, and AES-GCM encryption without any dependency. Libraries like libsodium.js provide higher-level constructs (sealed boxes, password hashing with Argon2) that are harder to misuse, but for standard AES-GCM + ECDSA the Web Crypto API is sufficient and has no supply-chain risk.
Sources
- MDN Web Crypto API (developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API
- CC-BY-SA 2.5+)
- MDN SubtleCrypto (developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto
- CC-BY-SA 2.5+)
- W3C Web Cryptography Level 2 (w3.org/TR/webcrypto-2/
- W3C Recommendation)
- Wikipedia — SHA-2 (en.wikipedia.org/wiki/SHA-2
- CC BY-SA 4.0)
Share this guide
Related guides
- TLS versions explained: from SSL 3.0 to TLS 1.3
Every TLS version from SSL 3.0 to TLS 1.3 explained — what changed, what broke, and why old versions are now disabled by default.
- SSH explained: how Secure Shell works
SSH explained: what Secure Shell is, how the protocol works end to end, and why it replaced telnet and rsh.
- What is HSTS (HTTP Strict Transport Security)? How it works and why it matters
HTTP Strict Transport Security (HSTS) is a web security policy that forces browsers to only use HTTPS. Learn how the Strict-Transport-Security header works, what it protects against, and how HSTS preload lists eliminate the SSL-stripping window.