46. The Web Crypto API: hashing, AES-GCM, key derivation, asymmetric crypto
crypto.subtle (SubtleCrypto) is the browser's native, hardware-backed
cryptography API — always prefer it over any pure-JS crypto library for
primitives it supports, both for correctness (no re-implemented, easy-to-
get-wrong primitives) and speed (native code, sometimes hardware-accelerated).
Hashing (one-way, for integrity checks and password-adjacent use — never for storing passwords directly, see PBKDF2 below):
async function sha256(text) {
const data = new TextEncoder().encode(text);
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0")).join("");
}
await sha256("hello"); // "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
Symmetric encryption (AES-GCM) — the standard choice for encrypting data you'll decrypt yourself later (client-side vault, encrypted notes, §48):
async function generateKey() {
return crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
}
async function encrypt(key, plaintext) {
const iv = crypto.getRandomValues(new Uint8Array(12)); // MUST be unique per encryption
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext)
);
return { iv, ciphertext }; // store BOTH — decryption needs the exact same iv
}
async function decrypt(key, { iv, ciphertext }) {
const plaintextBuffer = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
return new TextDecoder().decode(plaintextBuffer);
}
Deriving a key from a user password (PBKDF2) — never use a password directly as an encryption key; derive a proper key through many hashing iterations to make brute-forcing expensive:
async function deriveKeyFromPassword(password, salt) {
const keyMaterial = await crypto.subtle.importKey(
"raw", new TextEncoder().encode(password), "PBKDF2", false, ["deriveKey"]
);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: 600_000, hash: "SHA-256" }, // OWASP 2023+ minimum
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
}
Asymmetric crypto (ECDSA for signing, RSA-OAEP/ECDH for key exchange) — for verifying authenticity or exchanging a symmetric key without ever transmitting it:
async function signMessage(privateKey, message) {
const signature = await crypto.subtle.sign(
{ name: "ECDSA", hash: "SHA-256" }, privateKey, new TextEncoder().encode(message)
);
return signature;
}
async function verifySignature(publicKey, signature, message) {
return crypto.subtle.verify(
{ name: "ECDSA", hash: "SHA-256" }, publicKey, signature, new TextEncoder().encode(message)
);
}