Web Crypto

crypto.getRandomValues() works anywhere, but crypto.randomUUID() and crypto.subtle (Baseline Widely available) are undefined outside secure contexts. subtle provides encrypt() (AES-GCM, AES-CBC, RSA-OAEP), sign() (Ed25519, ECDSA, RSA-PSS, HMAC), digest() (SHA-256/384/512), deriveKey() (PBKDF2, HKDF, ECDH, X25519) and key import, export and wrapping.

Password-based AES-GCM encryption and an Ed25519 signatureJavaScript
const { subtle } = crypto;
const bytes = (text) => new TextEncoder().encode(text);
const salt = crypto.getRandomValues(new Uint8Array(16));  // store salt and iv with the data
const iv = crypto.getRandomValues(new Uint8Array(12));
const pw = await subtle.importKey('raw', bytes('correct horse'), 'PBKDF2', false, ['deriveKey']);
const key = await subtle.deriveKey({ name: 'PBKDF2', salt, iterations: 600_000, hash: 'SHA-256' },
  pw, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
const sealed = await subtle.encrypt({ name: 'AES-GCM', iv }, key, bytes('Meet at 9'));
const opened = await subtle.decrypt({ name: 'AES-GCM', iv }, key, sealed);
console.log(sealed.byteLength, 'bytes sealed ->', new TextDecoder().decode(opened));
const { publicKey, privateKey } = await subtle.generateKey('Ed25519', false, ['sign', 'verify']);
const signature = await subtle.sign('Ed25519', privateKey, bytes('Invoice #42'));
console.log(signature.byteLength, 'byte signature valid:',
  await subtle.verify('Ed25519', publicKey, signature, bytes('Invoice #42')));
Output
25 bytes sealed -> Meet at 9
64 byte signature valid: true

The extra 16 bytes are AES-GCM's tag, which makes decrypt() reject tampering. Ed25519 reached all engines with Chrome 137 1 (May 2025). Use OWASP's 600,000 PBKDF2 iterations and never reuse an IV with the same key.