← Back to the tool

Foundations · Under the hood

HMAC sign & verify

What each button in the tool actually computes, step by step — with the real formulas.

Turning one secret into two padded keys

HMAC first pads (or, if it's longer than one block, hashes down) your secret to SHA-256's 64-byte block size, producing K′. That padded key is then XORed with two fixed constants — ipad (0x36 repeated) and opad (0x5c repeated) — producing two different derived keys from the one secret you typed.

The actual WebCrypto call
const key = await crypto.subtle.importKey(
  "raw",
  new TextEncoder().encode(secret),
  { name: "HMAC", hash: "SHA-256" },  // tells WebCrypto which hash HMAC should wrap
  false,                               // extractable — false, since it's only ever used to sign
  ["sign"]                             // this demo never verifies via a separate "verify" usage
);

The K′ padding and the ipad/opad XOR described above aren't things this code does — they happen inside crypto.subtle the moment you import the key with { name: "HMAC", hash: "SHA-256" }.

Two nested hashes

The inner hash covers the padded-and-XORed key plus your actual message. The outer hash covers the padded-and-XORed key (with the other constant) plus that inner result. The hex string labeled "Tag" is the output of the outer hash — a single 32-byte value.

The actual WebCrypto call
const tag = await crypto.subtle.sign(
  "HMAC",
  key,                                    // the imported key from the step above
  new TextEncoder().encode(message)
);
// tag is a 32-byte ArrayBuffer — hex-encoded above as "Tag"

sign() runs both nested hashes in the formula above and returns only the final 32-byte result — there's no intermediate access to the inner hash from JavaScript.

HMAC(K,m)=H((K′⊕opad) ∥ H((K′⊕ipad) ∥ m))\mathrm{HMAC}(K, m) = H\big((K' \oplus \mathrm{opad}) \,\|\, H((K' \oplus \mathrm{ipad}) \,\|\, m)\big)

Why not just H(secret ‖ message)?

The simpler-looking H(secret ‖ message) is vulnerable to length-extension attacks, covered in detail in the Hash functions module — an attacker who knows H(secret ‖ message) can compute H(secret ‖ message ‖ extra) without ever learning the secret. HMAC's nested double-hash construction closes that gap; it's not a stylistic choice.

What the Verify button actually checks

It recomputes HMAC(secret, message) from scratch, using whatever is currently in the message and secret fields, and compares the result to the tag you provided. Production systems use a constant-time comparison for this exact step — comparing byte by byte and stopping at the first mismatch leaks timing information about how many leading bytes matched, the same class of issue covered in the side-channel module. This demo uses a simple equality check for clarity.

The actual verification code
const recomputed = await hmacSha256Hex(verifySecret, verifyMessage);
// hmacSha256Hex() runs the same importKey + sign calls shown above

if (recomputed === verifyTag.trim().toLowerCase()) {
  // valid — but this is a plain string comparison, not constant-time
} else {
  // invalid
}

WebCrypto does have a purpose-built, constant-time check — crypto.subtle.verify("HMAC", key, tagBytes, messageBytes) — which recomputes the tag internally and never leaks timing information. This tool deliberately bypasses it and compares strings by hand instead, precisely to make the constant-time point above concrete rather than hidden inside a built-in.

Now try it

Back to HMAC sign & verify →

Compute a real HMAC-SHA256 over a message and secret, then see why changing even one bit of the message makes verification fail.