cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

The Web Crypto API

Learn Web Crypto for secure random values, SHA-256 hashing, HMAC, PBKDF2, AES-GCM encryption, signatures, and key formats.

By the end, you can
  • 01
    Generate browser-safe randomnessUse crypto.getRandomValues, randomUUID, and rejection sampling instead of Math.random for secrets.
  • 02
    Hash, authenticate, and derive keysTurn text into bytes, digest with SHA-256, verify HMACs, and use PBKDF2 only with production iteration counts.
  • 03
    Encrypt, sign, and manage keysUse AES-GCM with unique IVs, feature-detect signing algorithms, and import/export CryptoKeys in standard formats.

Use platform cryptography, not clever code

Web Crypto is the browser and Node runtime interface to audited cryptographic primitives. It works with typed arrays, bytes from TextEncoder, and promises from Promises and async/await. Use it when a frontend needs secure random values, digests, HMACs, encryption, signatures, or key import/export. Do not design your own cryptography.

Plain definition

The Web Crypto API is the standards-based crypto object: crypto.getRandomValues and crypto.randomUUID create secure random values, while crypto.subtle performs async operations such as hashing, key derivation, encryption, signing, and key import/export.

Real-life analogyA fingerprint identifies a file

A hash answers “did these bytes stay the same?” It identifies data but does not encrypt it.

In real life: A fingerprint identifies a file
In JavaScript: A hash digest identifies data
In real life: Compare two fingerprints
In JavaScript: Compare two digests
In real life: You cannot rebuild the file
In JavaScript: A hash is one-way

Where the analogy stops: A cryptographic hash needs a specific algorithm and careful byte handling. It does not hide data.

Real-life analogyA locked box needs the right key

Encryption hides a message until someone with the right key opens it.

In real life: Put a note in a locked box
In JavaScript: Encrypt plaintext
In real life: Use the right key to open it
In JavaScript: Decrypt ciphertext
In real life: Keep the key private
In JavaScript: Protect the CryptoKey

Where the analogy stops: Encryption needs a secure algorithm, key, and fresh nonce. It is not a hash or a signature.

Real-life analogyA signature on a letter

A signature shows who approved data and reveals whether it changed after signing.

In real life: Sign a letter
In JavaScript: Sign data with a private key
In real life: Check the signature
In JavaScript: Verify with a public key
In real life: A changed letter fails the check
In JavaScript: Changed data does not verify

Where the analogy stops: A digital signature needs the right algorithm and key pair. It does not keep the letter secret.

Encoding, hashing, authentication, encryption, and signing
ToolWhat it doesWhat it does not do
EncodingReversible formatting such as hex, Base64, or UTF-8 bytes.No key; anyone can decode it.
HashingOne-way, fixed-length digest such as SHA-256.No decryption; tiny input changes avalanche the output.
HMACKeyed hash for message authentication.Both sides need the shared secret; it proves integrity, not secrecy.
EncryptionTurns plaintext into ciphertext with a key.AES-GCM also authenticates the ciphertext and tag.
SigningPrivate key creates a signature; public key verifies it.Proves who signed, but the message is still readable unless separately encrypted.

Practical connections: CSRF, cookies & tokens uses random tokens and PKCE hashes; supply-chain security uses Subresource Integrity hashes; IndexedDB can store non-extractable keys; and Web Storage is where keys and long-lived bearer tokens should usually not go.

Secure random values

NO MATH.RANDOM

Randomness is the first place frontend code often goes wrong. Math.random() is fine for animations and sampling demos, but it is not cryptographically secure and must never create tokens, password reset links, CSRF secrets, PKCE verifiers, encryption keys, or IDs attackers can guess. Use crypto.getRandomValues for bytes and crypto.randomUUID() for RFC 4122 version 4 UUIDs.

Generate a hex token from secure bytesPop out in the code editor (opens in a new tab)JavaScript
function bytesToHex(bytes) {  return [...bytes].map((byte) => byte.toString(16).padStart(2, "0")).join("");} function randomToken(byteLength = 16, cryptoImpl = crypto) {  const bytes = new Uint8Array(byteLength);  cryptoImpl.getRandomValues(bytes);  return bytesToHex(bytes);} const fixedCrypto = {  getRandomValues(bytes) {    bytes.set([0xde, 0xad, 0xbe, 0xef].slice(0, bytes.length));    return bytes;  },}; console.log(randomToken(4, fixedCrypto));console.log(randomToken(16).length);console.log("differs every run");

The first printed token uses an injected fixed source so tests can prove the formatter. The 16-byte token length and real token value differ every run when the default crypto source fills the array. The important property is unpredictability, not a pretty format.

Validate randomUUID as UUID v4Pop out in the code editor (opens in a new tab)JavaScript
const uuid = crypto.randomUUID();const v4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;console.log(v4.test(uuid));console.log(uuid.slice(14, 15));console.log("differs every run");

A UUID v4 has a 4 in the version position and 8, 9, a, or b in the variant position. The value itself differs every run, so tests validate it with a regex instead of comparing one literal string.

getRandomValues size limitPop out in the code editor (opens in a new tab)JavaScript
console.log("65536 bytes", crypto.getRandomValues(new Uint8Array(65536)).length);try {  crypto.getRandomValues(new Uint8Array(65537));} catch (error) {  console.log(error.name);  console.log(error.message);}
Runtime facts verified

crypto.subtle and crypto.randomUUID require a secure context such as HTTPS or localhost. In Chrome on http://example.com, crypto.subtle and randomUUID were unavailable, but getRandomValues still worked. The CompleteFrontend Edit & run iframe is sandboxed with an opaque origin, but it is created from localhost/HTTPS, so crypto.subtle is available there.

The 65,536-byte limit is real. Node 22.23.1 throws QuotaExceededError with message The requested length exceeds 65,536 bytes. Chrome 154 throws the same name and says the ArrayBufferView byte length exceeds the entropy available via this API.

Random integers need rejection sampling

STEP THROUGH

A common mistake is sample % max. If the random range is not evenly divisible by max, some results get one extra source value. For a 32-bit sample and max = 10, residues 0 through 5 would happen one extra time. Rejection sampling discards the tiny tail so every accepted result has the same count.

Rejection sampling for a random range
Step 0 of 8Ready
Your turn: follow the blue line

Step through rejection sampling. The first fixed draw is rejected because modulo would be biased; the second draw is accepted.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  const range = 0x100000000;  const limit = Math.floor(range / max) * max;  const sample = new Uint32Array(1);  do {    source.getRandomValues(sample);  } while (sample[0] >= limit);  return sample[0] % max;} const fake = {  getRandomValues(array) {    array[0] = array[0] === 0 ? 4294967294 : 8;    return array;  },}; console.log(randomInt(10, fake));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Modulo without rejection

2^32 % 10 is 6, so six residues get 429,496,730 source values while the other four get 429,496,729.

Rejection sampling

Accept 4,294,967,290 values and reject the last six. Each result from 0 to 9 now has exactly 429,496,729 accepted samples.

Hash bytes with SubtleCrypto.digest

REAL VECTOR

Hash functions are deterministic, one-way, and fixed length. SHA-256 returns 32 bytes no matter how large the input is. To hash text, encode it to bytes, await crypto.subtle.digest, then choose a display format such as hex. You already saw those byte-to-text display formats in Text encoding & Base64.

SHA-256 digest as hexPop out in the code editor (opens in a new tab)JavaScript
async function sha256Hex(message) {  const bytes = new TextEncoder().encode(message);  const digest = await crypto.subtle.digest("SHA-256", bytes);  return [...new Uint8Array(digest)]    .map((byte) => byte.toString(16).padStart(2, "0"))    .join("");} sha256Hex("abc").then(console.log);

The known SHA-256 test vector for "abc" is ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad. If your helper prints anything else, the bytes or hex conversion are wrong.

SHA-256 avalanche workbench
Live SHA-256 hasherPop out in the code editor (opens in a new tab)JavaScript
async function sha256Hex(message) {  const bytes = new TextEncoder().encode(message);  const digest = await crypto.subtle.digest("SHA-256", bytes);  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");}
Digest comparison58 changed hex positions
abcba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad
abda52d159f262b2c6ddb724a61840befc36eb30c88877a4030b65cbe86298449c9
Try it yourself

Both hashes are 64 hex characters. 58 positions differ right now, even when the messages are almost the same.

The playground calls the same sha256Hex function as the lesson examples. Hashes are deterministic, not random.

Hashes are useful for integrity checks such as Subresource Integrity, cache keys, and comparing content without storing the content itself. They are not encryption and cannot be reversed.

HMAC authenticates; PBKDF2 slows guesses

KEYED HASHES

A plain hash says “these bytes match.” It does not say who created the digest. HMAC adds a shared secret key, so a receiver can verify the message came from someone who knows the secret and that the message was not changed. Webhooks often use this pattern.

HMAC sign and verifyPop out in the code editor (opens in a new tab)JavaScript
async function hmacHex(message, secret) {  const encoder = new TextEncoder();  const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);  const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));  return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");} async function verifyHmac(message, secret, hex) {  const encoder = new TextEncoder();  const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);  const signature = Uint8Array.from(hex.match(/../g), (pair) => parseInt(pair, 16));  return crypto.subtle.verify("HMAC", key, signature, encoder.encode(message));} (async () => {  const message = "The quick brown fox jumps over the lazy dog";  const signature = await hmacHex(message, "key");  console.log(signature);  console.log(await verifyHmac(message, "key", signature));  console.log(await verifyHmac(message + "!", "key", signature));})();

Passwords are different. Do not store SHA-256(password). SHA-256 is intentionally fast, which helps attackers guess billions of candidates. Prefer server-side password hashing libraries using Argon2id, bcrypt, or scrypt. Web Crypto has PBKDF2 and HKDF; when PBKDF2-HMAC-SHA-256 is required, current OWASP guidance is about 600,000 iterations or more for new applications. The next snippet uses only 1,000 iterations so the browser demo finishes quickly.

PBKDF2 demo key derivationPop out in the code editor (opens in a new tab)JavaScript
async function deriveDemoKey(password, salt, iterations = 1000) {  const encoder = new TextEncoder();  const baseKey = await crypto.subtle.importKey("raw", encoder.encode(password), "PBKDF2", false, ["deriveBits"]);  const bits = await crypto.subtle.deriveBits(    { name: "PBKDF2", hash: "SHA-256", salt: encoder.encode(salt), iterations },    baseKey,    256,  );  return [...new Uint8Array(bits)].map((byte) => byte.toString(16).padStart(2, "0")).join("");} deriveDemoKey("correct horse battery staple", "demo-salt", 1000).then((hex) => {  console.log(hex);  console.log("demo iterations only");});
Password storage belongs on the server

Browser PBKDF2 is useful for protocols and local encryption designs, but a login system should hash passwords server-side with a unique salt, a slow work factor, rate limits, monitoring, and upgrade paths. Never treat a frontend-only hash as a password storage strategy.

Encrypt with AES-GCM and never reuse an IV

AUTHENTICATED

AES-GCM is the Web Crypto workhorse for symmetric encryption. It uses one shared key to encrypt and decrypt, a 12-byte IV for each message, and an authentication tag that travels with the ciphertext. Reusing an IV with the same key can reveal data and break authentication. Store the IV beside the ciphertext; it is not secret, it just must be unique for that key.

AES-GCM round trip with a fixed teaching vector
Step 0 of 7Ready
Your turn: follow the blue line

Step through an AES-GCM round trip. The values are deterministic for teaching; production IVs must be random and never reused with the same key.

Running in
  1. script
Next: line 2
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
async function aesGcmDemo() {  const iv = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);  const key = await crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["encrypt", "decrypt"]);  const encoded = new TextEncoder().encode("meet at noon");  const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);  const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);  const hex = [...new Uint8Array(ciphertext)].map((byte) => byte.toString(16).padStart(2, "0")).join("");  console.log(hex);  console.log(new TextDecoder().decode(plaintext));} aesGcmDemo();
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Encrypt, tamper, decrypt
Random-IV AES-GCM functionsPop out in the code editor (opens in a new tab)JavaScript
async function encryptMessage(text) {  const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"]);  const iv = crypto.getRandomValues(new Uint8Array(12));  const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(text));  return { key, iv, ciphertext: new Uint8Array(ciphertext) };} async function decryptMessage(record) {  const bytes = await crypto.subtle.decrypt({ name: "AES-GCM", iv: record.iv }, record.key, record.ciphertext);  return new TextDecoder().decode(bytes);}
Current recordauthentic
IVnot generated yet
Ciphertext + tagnot generated yet
StatusEncrypt a message to create a random IV and ciphertext.
Try it yourself

Encrypt a message to create a random IV and ciphertext.

This demo generates a fresh in-memory AES-GCM key and a 12-byte random IV for each Encrypt click. It does not store the key.

When the last byte is tampered, Web Crypto rejects decrypt with OperationError. That rejection is the security feature: do not catch it and continue as if you had trustworthy text. For asymmetric encryption, RSA-OAEP can encrypt small secrets such as a random AES key, while ECDH derives a shared secret that you feed into a KDF. In modern systems, asymmetric crypto usually protects or agrees on symmetric keys; AES-GCM protects the bulk data.

Sign when readers must verify the author

FEATURE DETECT

Encryption hides content. Signing proves a holder of the private key approved the message. The message can still be public. Web Crypto widely supports ECDSA with P-256. Chrome 154 and Node 22.23.1 also support Ed25519, but production code should feature-detect because support is newer across the platform.

Feature-detect Ed25519, then verify ECDSAPop out in the code editor (opens in a new tab)JavaScript
async function supportsEd25519() {  try {    const pair = await crypto.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);    const message = new TextEncoder().encode("hello");    const signature = await crypto.subtle.sign({ name: "Ed25519" }, pair.privateKey, message);    const ok = await crypto.subtle.verify({ name: "Ed25519" }, pair.publicKey, signature, message);    return ok && signature.byteLength === 64;  } catch {    return false;  }} async function ecdsaDemo() {  const message = new TextEncoder().encode("ship release 42");  const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);  const signature = await crypto.subtle.sign({ name: "ECDSA", hash: "SHA-256" }, pair.privateKey, message);  console.log(await supportsEd25519());  console.log(await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, pair.publicKey, signature, message));  console.log(await crypto.subtle.verify({ name: "ECDSA", hash: "SHA-256" }, pair.publicKey, signature, new TextEncoder().encode("ship release 43")));} ecdsaDemo();

Signatures are how package manifests, update feeds, and authorization statements can be checked without sharing a symmetric secret. They do not hide the message. If the message is confidential, encrypt it separately and verify both confidentiality and authenticity.

CryptoKey objects and key formats

KEY MANAGEMENT

Web Crypto does not hand you raw bytes by default. It hands you CryptoKey objects with type, extractable, algorithm, and usages. Those properties are part of the security boundary: a key meant only for verify cannot sign, and a non-extractable key cannot be exported as raw bytes.

Inspect extractable keys and formatsPop out in the code editor (opens in a new tab)JavaScript
async function inspectKeys() {  const aesKey = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);  console.log(aesKey.type + " " + aesKey.extractable + " " + aesKey.usages.join(","));  console.log((await crypto.subtle.exportKey("raw", aesKey)).byteLength);  console.log((await crypto.subtle.exportKey("jwk", aesKey)).kty);   const pair = await crypto.subtle.generateKey({ name: "ECDSA", namedCurve: "P-256" }, true, ["sign", "verify"]);  console.log((await crypto.subtle.exportKey("spki", pair.publicKey)).byteLength > 0);  console.log((await crypto.subtle.exportKey("pkcs8", pair.privateKey)).byteLength > 0);} inspectKeys();
Key containers and export formats
FormatShapeUse
CryptoKeyRuntime objectCarries type, extractable, algorithm, and usages; may be non-extractable.
rawSymmetric key bytesAES or HMAC bytes. Protect them like secrets when extractable.
jwkJSON Web KeyPortable JSON for web protocols; can represent symmetric, public, or private keys.
spkiPublic keyDER SubjectPublicKeyInfo, commonly PEM-wrapped for public keys.
pkcs8Private keyDER PrivateKeyInfo, commonly PEM-wrapped; protect at rest and in transit.

Non-extractable keys can still be stored by the browser. IndexedDB can persist a CryptoKey object without exposing its raw bytes, which is safer than putting key material in localStorage. The fragment below is intentionally not runnable in the lesson editor because real IndexedDB setup needs a database wrapper and application-specific lifecycle decisions.

Store a non-extractable key objectJavaScript
async function makeStoredKey() {  const key = await crypto.subtle.generateKey(    { name: "AES-GCM", length: 256 },    false,    ["encrypt", "decrypt"],  );  // Store the non-extractable CryptoKey object itself, not raw key bytes.  const db = await openIndexedDbSomewhere();  await db.put("keys", key, "message-key");  return key.extractable;}

If you do export key material, know the format: raw for symmetric bytes, jwk for JSON Web Keys, spki for public keys, and pkcs8 for private keys. Private key export should be rare, encrypted at rest, and handled outside ordinary page scripts whenever possible.

What not to do

  • Do not roll your own crypto. Compose audited primitives and protocols instead.
  • Do not use ECB mode. Web Crypto does not expose AES-ECB; use authenticated modes such as AES-GCM.
  • Do not reuse AES-GCM IVs with the same key. Generate a fresh 12-byte IV per message.
  • Do not store keys in localStorage. XSS can read it. Prefer non-extractable keys in IndexedDB or server-side keys.
  • Do not confuse encoding, hashing, encryption, and signing. Each answers a different security question.
Common Web Crypto mistakes
MistakeWhy it is wrongSafer habit
Math.random() for tokensNot cryptographically secureUse crypto.getRandomValues or crypto.randomUUID.
sample % maxBiased unless the range divides evenlyUse rejection sampling for bounded random integers.
Plain SHA-256 passwordsFast hashes are cheap to brute-forceUse Argon2id/bcrypt/scrypt server-side, or PBKDF2 with current high iterations when required.
AES-GCM IV reuseReusing an IV with the same key can reveal data and break authenticationGenerate a fresh 12-byte random IV per message and store it beside the ciphertext.
localStorage for keysScript-readable storage is exposed to XSSPrefer non-extractable CryptoKeys in IndexedDB or server-side keys.
Encoding equals encryptionBase64, UTF-8, and hex are reversible without a keyName the operation: encode, hash, authenticate, encrypt, or sign.
Hashing, encryption, signing, or encoding?
  • Turn bytes into base64url for a JWK field.
  • Compute a SHA-256 digest for a Subresource Integrity value.
  • Verify a webhook body with a shared secret.
  • Hide a note with AES-GCM and a fresh IV.
  • Prove a release manifest came from the private key holder.
  • Hash a PKCE code verifier into a challenge.
Try it yourself
0 of 6 correct

Sort each real-world task by the primary operation it needs. Some tasks combine operations, but choose the one doing the security work described.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upRead a UUID v4

Which version nibble should the UUID regex require?

Starter codePop out in the code editor (opens in a new tab)JavaScript
const uuid = crypto.randomUUID();
const v4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
console.log(v4.test(uuid));
console.log(uuid.slice(14, 15));
console.log("differs every run");

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeReject modulo bias

    Predict the printed number from the deterministic random source.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function randomInt(max, source = crypto) {
      const range = 0x100000000;
      const limit = Math.floor(range / max) * max;
      const sample = new Uint32Array(1);
      do {
        source.getRandomValues(sample);
      } while (sample[0] >= limit);
      return sample[0] % max;
    }
    
    const fake = {
      getRandomValues(array) {
        array[0] = array[0] === 0 ? 4294967294 : 8;
        return array;
      },
    };
    
    console.log(randomInt(10, fake));

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCheck a SHA-256 test vector

      Type the first eight hex characters of the digest.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      async function sha256Hex(message) {
        const bytes = new TextEncoder().encode(message);
        const digest = await crypto.subtle.digest("SHA-256", bytes);
        return [...new Uint8Array(digest)]
          .map((byte) => byte.toString(16).padStart(2, "0"))
          .join("");
      }
      
      sha256Hex("abc").then(console.log);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeSpot HMAC tampering

        Does the tampered verification print true or false?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        async function hmacHex(message, secret) {
          const encoder = new TextEncoder();
          const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign", "verify"]);
          const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(message));
          return [...new Uint8Array(signature)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
        }
        
        async function verifyHmac(message, secret, hex) {
          const encoder = new TextEncoder();
          const key = await crypto.subtle.importKey("raw", encoder.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
          const signature = Uint8Array.from(hex.match(/../g), (pair) => parseInt(pair, 16));
          return crypto.subtle.verify("HMAC", key, signature, encoder.encode(message));
        }
        
        (async () => {
          const message = "The quick brown fox jumps over the lazy dog";
          const signature = await hmacHex(message, "key");
          console.log(signature);
          console.log(await verifyHmac(message, "key", signature));
          console.log(await verifyHmac(message + "!", "key", signature));
        })();

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeName the AES-GCM tamper failure

          When a ciphertext byte or authentication tag changes, what exception name should decrypt reject with?

          Answer, then press Check. Spacing and letter case don’t matter.

            Check your understanding

            8 QUESTIONS
            Web Crypto quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Which API should create session tokens or CSRF secrets in browser code?

              Choose an answer to see the explanation.

            2. Question 2 of 8Why does a bounded random integer helper reject some 32-bit samples?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              function randomInt(max, source = crypto) {
                const range = 0x100000000;
                const limit = Math.floor(range / max) * max;
                const sample = new Uint32Array(1);
                do {
                  source.getRandomValues(sample);
                } while (sample[0] >= limit);
                return sample[0] % max;
              }
              
              const fake = {
                getRandomValues(array) {
                  array[0] = array[0] === 0 ? 4294967294 : 8;
                  return array;
                },
              };
              
              console.log(randomInt(10, fake));

              Choose an answer to see the explanation.

            3. Question 3 of 8What does SHA-256 return for any input size?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this known SHA-256 test vector print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              async function sha256Hex(message) {
                const bytes = new TextEncoder().encode(message);
                const digest = await crypto.subtle.digest("SHA-256", bytes);
                return [...new Uint8Array(digest)]
                  .map((byte) => byte.toString(16).padStart(2, "0"))
                  .join("");
              }
              
              sha256Hex("abc").then(console.log);

              Choose an answer to see the explanation.

            5. Question 5 of 8What does HMAC add to a plain hash?

              Choose an answer to see the explanation.

            6. Question 6 of 8What must be unique for every AES-GCM encryption with the same key?

              Choose an answer to see the explanation.

            7. Question 7 of 8What does AES-GCM decryption do when the ciphertext or tag is changed?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              async function aesGcmDemo() {
                const keyBytes = new Uint8Array([...Array(32).keys()]);
                const iv = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
                const key = await crypto.subtle.importKey("raw", keyBytes, "AES-GCM", false, ["encrypt", "decrypt"]);
                const encoded = new TextEncoder().encode("meet at noon");
                const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, encoded);
                const plaintext = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
                const hex = [...new Uint8Array(ciphertext)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
                console.log(hex);
                console.log(new TextDecoder().decode(plaintext));
              }
              
              aesGcmDemo();

              Choose an answer to see the explanation.

            8. Question 8 of 8Which key format normally exports a public key, not a private key?

              Choose an answer to see the explanation.

            Key takeaways

            • Use crypto.getRandomValues and crypto.randomUUID for security-sensitive randomness; never use Math.random for secrets.
            • crypto.subtle is promise-based and works on bytes, so encode text before hashing, signing, or encrypting.
            • SHA-256 is deterministic, one-way, and fixed length. HMAC adds a shared secret; PBKDF2 slows derivation but frontend demos use low iterations only for speed.
            • AES-GCM needs a fresh 12-byte IV for each message with the same key, and tampering must reject.
            • CryptoKey metadata, extractability, usages, and import/export formats are part of key management.
            • Keep keys out of script-readable storage; consider non-extractable keys in IndexedDB or server-side key management.

            Final definition: Web Crypto gives JavaScript audited primitives for random bytes, hashes, authentication, encryption, signatures, derivation, and keys. Your job is to choose the right primitive, feed it bytes, and respect the key and nonce rules.

            Up next: Measuring performance, where the focus shifts from keeping users safe to proving which code is actually slow.

            CompleteFrontend Clear concepts. Working examples.