M← Home

SubtleCrypto playground

An interactive page for the browser's Web Crypto API

Every modern browser ships a full cryptography toolkit at crypto.subtle: hashing, symmetric and public-key encryption, signatures, key derivation and key wrapping, all behind one promise-based API. It is called subtle because it is easy to misuse, so this page lets you poke at it and see what each call takes and returns.

Generate keys first (or add the starter set), then use them on the other tabs. Each result comes with the exact call that produced it and how long it took. Inputs and outputs are bytes, shown as text, hex or Base64: switch the encoding of any field to see the same bytes another way. Everything runs locally in your browser and nothing leaves the page; keys only live in memory. Below the playground there is a reference: the algorithms and their usages, why crypto.subtle can be undefined, short examples and common pitfalls.

Checking for Web Crypto…

Algorithms in the Web Crypto API

crypto.subtle supports twelve key algorithms, plus SHA-1, SHA-256, SHA-384 and SHA-512 for digest and PBKDF2 and HKDF for key derivation. There is no MD5 and no SHA-3. A key carries its algorithm and a fixed list of usages; a call that needs any other usage fails with InvalidAccessError.

AlgorithmUsed forKey usages
AES-GCMAuthenticated encryption. The usual choice for encrypting data.encrypt, decrypt, wrapKey, unwrapKey
AES-CBCBlock cipher with PKCS#7 padding. No integrity check.encrypt, decrypt, wrapKey, unwrapKey
AES-CTRStream mode from a counter block. No padding, no integrity check.encrypt, decrypt, wrapKey, unwrapKey
AES-KWRFC 3394 key wrap. Only wraps and unwraps other keys.wrapKey, unwrapKey
HMACKeyed hash for message authentication codes.sign, verify
RSA-OAEPPublic-key encryption of short messages and keys.public: encrypt, wrapKey; private: decrypt, unwrapKey
RSA-PSSRSA signatures with randomised PSS padding.public: verify; private: sign
RSASSA-PKCS1-v1_5Classic deterministic RSA signatures (as in RS256).public: verify; private: sign
ECDSAElliptic curve signatures on the NIST curves (as in ES256).public: verify; private: sign
ECDHElliptic curve key agreement on the NIST curves.public: none; private: deriveKey, deriveBits
Ed25519Edwards-curve signatures. Fast, deterministic, fixed parameters.public: verify; private: sign
X25519Curve25519 key agreement.public: none; private: deriveKey, deriveBits

Why is crypto.subtle undefined?

Browsers only expose crypto.subtle in a secure context: a page served over HTTPS, or from localhost or 127.0.0.1. On any other http:// page it is undefined, while crypto.getRandomValues still works. The common case is opening a dev server from a phone at an address like http://192.168.1.10:3000. Serve the dev server over HTTPS (for Next.js, next dev --experimental-https), use a tunnel with an HTTPS URL, or test on localhost.

if (!globalThis.crypto?.subtle) {
  // false on http:// pages other than localhost
  console.log("secure context:", window.isSecureContext);
}

Examples

Encrypt and decrypt with AES-GCM

const key = await crypto.subtle.generateKey(
  { name: "AES-GCM", length: 256 },
  true,
  ["encrypt", "decrypt"],
);
const iv = crypto.getRandomValues(new Uint8Array(12)); // new IV every time
const data = new TextEncoder().encode("Attack at dawn");

const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, data);
const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
new TextDecoder().decode(plaintext); // "Attack at dawn"

Sign and verify with ECDSA

const { publicKey, privateKey } = await crypto.subtle.generateKey(
  { name: "ECDSA", namedCurve: "P-256" },
  true,
  ["sign", "verify"],
);
const message = new TextEncoder().encode("I owe you one coffee.");
const params = { name: "ECDSA", hash: "SHA-256" };

const signature = await crypto.subtle.sign(params, privateKey, message);
await crypto.subtle.verify(params, publicKey, signature, message); // true

Derive an AES key from a password with PBKDF2

const password = new TextEncoder().encode("correct horse battery staple");
const salt = crypto.getRandomValues(new Uint8Array(16)); // store it with the data

const base = await crypto.subtle.importKey("raw", password, "PBKDF2", false, ["deriveKey"]);
const key = await crypto.subtle.deriveKey(
  { name: "PBKDF2", hash: "SHA-256", salt, iterations: 600_000 },
  base,
  { name: "AES-GCM", length: 256 },
  false,
  ["encrypt", "decrypt"],
);

Hash a string with SHA-256

const bytes = new TextEncoder().encode("hello world");
const digest = await crypto.subtle.digest("SHA-256", bytes);
const hex = [...new Uint8Array(digest)]
  .map((b) => b.toString(16).padStart(2, "0"))
  .join(""); // "b94d27b9…ace2efcde9"

Pitfalls

  • Never reuse an AES-GCM IV with the same key. Two messages under one key and IV leak their XOR and let an attacker forge tags. Use 12 random bytes per message and send the IV with the ciphertext.
  • AES-CBC and AES-CTR do not detect tampering. A changed byte decrypts to changed plaintext without an error. Use AES-GCM, or add an HMAC over the ciphertext.
  • ECDSA signatures are raw, not DER. Web Crypto returns r and s concatenated (64 bytes for P-256). OpenSSL, Java and most libraries expect ASN.1 DER, so convert at the boundary.
  • RSA-OAEP only encrypts short messages. A 2048-bit key with SHA-256 takes at most 190 bytes. Encrypt the data with AES and the AES key with RSA.
  • Leave optional parameters out instead of passing undefined. Chrome rejects additionalData: undefined in AES-GCM with a TypeError.
  • Non-extractable keys can still be stored. A CryptoKey with extractable: false cannot be exported, but it can be saved in IndexedDB and used after a reload.
  • Errors carry little detail. A failed decrypt is just OperationError, often with an empty message, by design: it does not say whether the key, the IV or the data was wrong.