Web Crypto API
The Web Crypto API provides a standardized interface for performing cryptographic operations in JavaScript. It is accessible via window.crypto.subtle (the SubtleCrypto interface) and supports hashing, encryption, decryption, key generation, digital signatures, and key derivation. The API is designed to be secure, performant, and portable across browsers and Node.js.
Unlike third-party crypto libraries, the Web Crypto API is built into the browser and backed by native implementations (often hardware-accelerated). All operations are asynchronous and return Promises, making them compatible with async/await patterns. The API is available in secure contexts (HTTPS) and in Web Workers.
| 1 | // Web Crypto API overview |
| 2 | const crypto = window.crypto || globalThis.crypto; |
| 3 | const subtle = crypto.subtle; |
| 4 | |
| 5 | // Available operations: |
| 6 | // - digest: hash functions (SHA-1, SHA-256, SHA-384, SHA-512) |
| 7 | // - encrypt/decrypt: symmetric encryption (AES-GCM, AES-CBC) |
| 8 | // - generateKey: create cryptographic keys |
| 9 | // - sign/verify: digital signatures (HMAC, RSASSA-PKCS1-v1_5) |
| 10 | // - deriveKey/deriveBits: key derivation (PBKDF2, ECDH) |
| 11 | // - importKey/exportKey: serialize keys |
| 12 | |
| 13 | // All methods return Promises |
| 14 | const hash = await subtle.digest("SHA-256", new TextEncoder().encode("hello")); |
| 15 | console.log(new Uint8Array(hash)); |
The digest() method computes a cryptographic hash of the input data. It supports SHA-1 (deprecated for security), SHA-256, SHA-384, and SHA-512. The output is an ArrayBuffer containing the raw hash bytes. Hashing is one-way — you cannot reverse a hash to recover the original input.
| 1 | // SHA-256 hash |
| 2 | async function sha256(message) { |
| 3 | const encoder = new TextEncoder(); |
| 4 | const data = encoder.encode(message); |
| 5 | const hashBuffer = await crypto.subtle.digest("SHA-256", data); |
| 6 | const hashArray = Array.from(new Uint8Array(hashBuffer)); |
| 7 | const hashHex = hashArray.map(b => b.toString(16).padStart(2, "0")).join(""); |
| 8 | return hashHex; |
| 9 | } |
| 10 | |
| 11 | const hash = await sha256("Hello, World!"); |
| 12 | console.log(hash); |
| 13 | // "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f" |
| 14 | |
| 15 | // SHA-512 hash |
| 16 | async function sha512(message) { |
| 17 | const data = new TextEncoder().encode(message); |
| 18 | const hashBuffer = await crypto.subtle.digest("SHA-512", data); |
| 19 | return Array.from(new Uint8Array(hashBuffer)) |
| 20 | .map(b => b.toString(16).padStart(2, "0")) |
| 21 | .join(""); |
| 22 | } |
| 23 | |
| 24 | // Hash binary data |
| 25 | async function hashFile(file) { |
| 26 | const buffer = await file.arrayBuffer(); |
| 27 | const hashBuffer = await crypto.subtle.digest("SHA-256", buffer); |
| 28 | return Array.from(new Uint8Array(hashBuffer)) |
| 29 | .map(b => b.toString(16).padStart(2, "0")) |
| 30 | .join(""); |
| 31 | } |
info
AES-GCM (Advanced Encryption Standard in Galois/Counter Mode) is an authenticated encryption algorithm. It provides both confidentiality (encryption) and integrity (authentication tag). This means if the ciphertext is tampered with, decryption will fail — providing tamper detection.
| 1 | // AES-GCM encryption and decryption |
| 2 | async function encryptAESGCM(key, plaintext, iv) { |
| 3 | const encoder = new TextEncoder(); |
| 4 | const data = encoder.encode(plaintext); |
| 5 | |
| 6 | const encrypted = await crypto.subtle.encrypt( |
| 7 | { name: "AES-GCM", iv }, |
| 8 | key, |
| 9 | data |
| 10 | ); |
| 11 | |
| 12 | return new Uint8Array(encrypted); |
| 13 | } |
| 14 | |
| 15 | async function decryptAESGCM(key, ciphertext, iv) { |
| 16 | const decrypted = await crypto.subtle.decrypt( |
| 17 | { name: "AES-GCM", iv }, |
| 18 | key, |
| 19 | ciphertext |
| 20 | ); |
| 21 | |
| 22 | return new TextDecoder().decode(decrypted); |
| 23 | } |
| 24 | |
| 25 | // Generate a random AES-GCM key |
| 26 | async function generateAESKey() { |
| 27 | return await crypto.subtle.generateKey( |
| 28 | { name: "AES-GCM", length: 256 }, |
| 29 | true, // extractable |
| 30 | ["encrypt", "decrypt"] |
| 31 | ); |
| 32 | } |
| 33 | |
| 34 | // Generate random IV (12 bytes recommended for AES-GCM) |
| 35 | function generateIV() { |
| 36 | return crypto.getRandomValues(new Uint8Array(12)); |
| 37 | } |
| 38 | |
| 39 | // Full example |
| 40 | const key = await generateAESKey(); |
| 41 | const iv = generateIV(); |
| 42 | |
| 43 | const ciphertext = await encryptAESGCM(key, "Secret message!", iv); |
| 44 | console.log("Encrypted:", ciphertext); |
| 45 | |
| 46 | const plaintext = await decryptAESGCM(key, ciphertext, iv); |
| 47 | console.log("Decrypted:", plaintext); // "Secret message!" |
warning
The Web Crypto API supports generating cryptographic keys for various algorithms. Keys can be symmetric (AES, HMAC) or asymmetric (RSA, ECDSA). Generated keys can be used directly for encryption/signing or exported for storage.
| 1 | // Generate AES key |
| 2 | const aesKey = await crypto.subtle.generateKey( |
| 3 | { name: "AES-GCM", length: 256 }, |
| 4 | true, // extractable — can be exported |
| 5 | ["encrypt", "decrypt", "wrapKey", "unwrapKey"] |
| 6 | ); |
| 7 | |
| 8 | // Generate HMAC key |
| 9 | const hmacKey = await crypto.subtle.generateKey( |
| 10 | { name: "HMAC", hash: "SHA-256" }, |
| 11 | true, |
| 12 | ["sign", "verify"] |
| 13 | ); |
| 14 | |
| 15 | // Generate ECDSA key pair (asymmetric) |
| 16 | const ecKeyPair = await crypto.subtle.generateKey( |
| 17 | { |
| 18 | name: "ECDSA", |
| 19 | namedCurve: "P-256" |
| 20 | }, |
| 21 | true, // extractable |
| 22 | ["sign", "verify"] |
| 23 | ); |
| 24 | |
| 25 | // Export key to raw format |
| 26 | const rawKey = await crypto.subtle.exportKey("raw", aesKey); |
| 27 | console.log("Raw key:", new Uint8Array(rawKey)); |
| 28 | |
| 29 | // Export key as JWK (JSON Web Key) |
| 30 | const jwk = await crypto.subtle.exportKey("jwk", aesKey); |
| 31 | console.log("JWK:", jwk); |
| 32 | |
| 33 | // Import key from raw format |
| 34 | const importedKey = await crypto.subtle.importKey( |
| 35 | "raw", |
| 36 | rawKey, |
| 37 | { name: "AES-GCM", length: 256 }, |
| 38 | true, |
| 39 | ["encrypt", "decrypt"] |
| 40 | ); |
HMAC (Hash-based Message Authentication Code) provides both integrity and authenticity verification. It uses a secret key combined with a hash function to produce a signature. The signature can be verified by anyone with the key, but cannot be forged without it.
| 1 | // HMAC signing and verification |
| 2 | async function signHMAC(key, message) { |
| 3 | const encoder = new TextEncoder(); |
| 4 | const data = encoder.encode(message); |
| 5 | |
| 6 | const signature = await crypto.subtle.sign( |
| 7 | "HMAC", |
| 8 | key, |
| 9 | data |
| 10 | ); |
| 11 | |
| 12 | return new Uint8Array(signature); |
| 13 | } |
| 14 | |
| 15 | async function verifyHMAC(key, message, signature) { |
| 16 | const encoder = new TextEncoder(); |
| 17 | const data = encoder.encode(message); |
| 18 | |
| 19 | const valid = await crypto.subtle.verify( |
| 20 | "HMAC", |
| 21 | key, |
| 22 | signature, |
| 23 | data |
| 24 | ); |
| 25 | |
| 26 | return valid; |
| 27 | } |
| 28 | |
| 29 | // Generate HMAC key |
| 30 | const hmacKey = await crypto.subtle.generateKey( |
| 31 | { name: "HMAC", hash: "SHA-256" }, |
| 32 | false, // not extractable (more secure) |
| 33 | ["sign", "verify"] |
| 34 | ); |
| 35 | |
| 36 | // Sign a message |
| 37 | const message = "Important: Transfer $1000"; |
| 38 | const signature = await signHMAC(hmacKey, message); |
| 39 | console.log("Signature:", Array.from(signature).map(b => |
| 40 | b.toString(16).padStart(2, "0") |
| 41 | ).join("")); |
| 42 | |
| 43 | // Verify the message |
| 44 | const isValid = await verifyHMAC(hmacKey, message, signature); |
| 45 | console.log("Valid:", isValid); // true |
| 46 | |
| 47 | // Tampered message fails verification |
| 48 | const tampered = await verifyHMAC(hmacKey, "Important: Transfer $9999", signature); |
| 49 | console.log("Tampered:", tampered); // false |
info
PBKDF2 (Password-Based Key Derivation Function 2) derives a cryptographic key from a password. It applies a pseudorandom function (like HMAC) combined with a salt and iterates thousands of times to slow down brute-force attacks. It is the standard way to derive encryption keys from user passwords.
| 1 | // PBKDF2 key derivation |
| 2 | async function deriveKeyFromPassword(password, salt) { |
| 3 | const encoder = new TextEncoder(); |
| 4 | |
| 5 | // Import password as raw key material |
| 6 | const passwordKey = await crypto.subtle.importKey( |
| 7 | "raw", |
| 8 | encoder.encode(password), |
| 9 | "PBKDF2", |
| 10 | false, |
| 11 | ["deriveBits", "deriveKey"] |
| 12 | ); |
| 13 | |
| 14 | // Derive a 256-bit AES-GCM key |
| 15 | const derivedKey = await crypto.subtle.deriveKey( |
| 16 | { |
| 17 | name: "PBKDF2", |
| 18 | salt, |
| 19 | iterations: 600000, // OWASP recommendation (2023+) |
| 20 | hash: "SHA-256" |
| 21 | }, |
| 22 | passwordKey, |
| 23 | { name: "AES-GCM", length: 256 }, |
| 24 | false, // not extractable |
| 25 | ["encrypt", "decrypt"] |
| 26 | ); |
| 27 | |
| 28 | return derivedKey; |
| 29 | } |
| 30 | |
| 31 | // Usage: encrypt data with password |
| 32 | const password = "user-password-123"; |
| 33 | const salt = crypto.getRandomValues(new Uint8Array(16)); |
| 34 | |
| 35 | const key = await deriveKeyFromPassword(password, salt); |
| 36 | |
| 37 | // Encrypt |
| 38 | const iv = crypto.getRandomValues(new Uint8Array(12)); |
| 39 | const ciphertext = await crypto.subtle.encrypt( |
| 40 | { name: "AES-GCM", iv }, |
| 41 | key, |
| 42 | new TextEncoder().encode("Sensitive data") |
| 43 | ); |
| 44 | |
| 45 | // Decrypt (with same password and salt) |
| 46 | const key2 = await deriveKeyFromPassword(password, salt); |
| 47 | const plaintext = await crypto.subtle.decrypt( |
| 48 | { name: "AES-GCM", iv }, |
| 49 | key2, |
| 50 | ciphertext |
| 51 | ); |
| 52 | |
| 53 | console.log(new TextDecoder().decode(plaintext)); // "Sensitive data" |
danger