What the iteration count actually does
PBKDF2 chains HMAC-SHA256 c times — U₁ = HMAC(password, salt‖blockIndex), U₂ = HMAC(password, U₁), and so on — but the derived key isn't just the last round's output. Every intermediate Uᵢ is XORed together, so the final block only comes out right if all c rounds actually ran. Each round is cheap on its own; multiplied by hundreds of thousands of rounds, it becomes deliberately, measurably slow.
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode(password),
"PBKDF2", // algorithm name as a bare string — no object needed for importKey here
false,
["deriveBits"]
);
const derivedBits = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt: saltBytes, iterations, hash: "SHA-256" },
keyMaterial,
256 // output length in bits
);The c rounds of HMAC and the XOR-accumulation in the formula above all happen inside this one deriveBits() call — there's no loop in this tool's own code counting iterations; the iterations number is just a parameter passed straight to WebCrypto.
c is the iteration count you picked above; each Uᵢ = HMAC(password, Uᵢ₋₁).
Why the timer matters
The elapsed-time readout after you click "Derive key" isn't a UI flourish — it's the entire point made concrete. An attacker checking a stolen password database against a wordlist pays that exact same per-guess cost, for every single guess. At 1,000 iterations that cost is negligible; at 600,000 it starts to meaningfully slow down large-scale guessing, at the price of also slowing down your own legitimate login checks.
const start = performance.now();
const derivedBits = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt: saltBytes, iterations, hash: "SHA-256" },
keyMaterial,
256
);
const elapsedMs = performance.now() - start;performance.now() is a real, sub-millisecond wall-clock timer — the number shown in this tool's status banner is exactly how long your own browser spent inside that one deriveBits() call, not a simulated or estimated figure.
Salt: public, but not pointless
The salt travels in the clear right alongside the derived key — it isn't a secret. Its job is narrower: it guarantees two users with the same password get completely different derived keys, which defeats precomputed rainbow-table attacks that only work when the same input always produces the same output.
const saltBytes = crypto.getRandomValues(new Uint8Array(16)); // 16 random bytes, shown as hex
// salt is passed as a plain parameter in the algorithm object, right
// alongside iterations and hash — not derived from or hashed with the password
const derivedBits = await crypto.subtle.deriveBits(
{ name: "PBKDF2", salt: saltBytes, iterations, hash: "SHA-256" },
keyMaterial,
256
);Click "New salt" and derive again with the same password: you'll get a completely different key, since salt feeds directly into every Uᵢ round — which is exactly the rainbow-table defense the body text above describes.
Why this tool doesn't offer Argon2
The Web Crypto API implements PBKDF2 natively in every browser; it doesn't implement Argon2, bcrypt, or scrypt at all — those would require a third-party WebAssembly library, which this playground deliberately avoids so that every tool here runs on nothing but your browser's own built-in, audited cryptography. The key derivation functions module covers all four algorithms and explains why Argon2 is the current recommendation for new systems.