Generating two independent key pairs
"Generate Alice's & Bob's keys" runs the same generateKey() call twice, completely independently — there's no coordination between the two, which is the whole point of Diffie–Hellman: neither side needs to know anything about the other's key before this step.
const aliceKeys = await crypto.subtle.generateKey(
{ name: "ECDH", namedCurve: "P-256" },
true,
["deriveBits"] // not ["sign", "verify"] — this key can only be used to derive a shared secret
);
// bobKeys generated the exact same way, independentlyThe key usage is deriveBits, not deriveKey — this tool wants the raw shared bytes to display as hex, rather than wrapping them directly into a CryptoKey the way an application normally would.
Each side does one scalar multiplication
"Derive shared secret" runs one elliptic-curve scalar multiplication per side, using the same double-and-add computation covered in the ECC module: Alice multiplies Bob's public point by her own private scalar; Bob multiplies Alice's public point by his.
const aliceSecret = await crypto.subtle.deriveBits(
{ name: "ECDH", public: bobKeys.publicKey }, // Bob's public key
aliceKeys.privateKey, // Alice's own private key
256 // how many bits to return
);
// Bob runs the mirror image: { name: "ECDH", public: aliceKeys.publicKey }, bobKeys.privateKeyThe public key of the party you're deriving against goes inside the algorithm object, not as a separate argument — an easy detail to get backwards the first time you write this.
That's the entire exchange — only the public keys shown above ever cross the wire. Everything after this point happens independently on each side.
Landing on the same point, from different directions
Scalar multiplication on an elliptic curve group is associative and commutative in exactly the way that matters here: a·(bG) and b·(aG) are both equal to (ab)G. Neither side ever computes or transmits ab directly — each only ever touches their own private scalar and the other side's public point.
aliceSecretHex === bobSecretHex // true — both sides landed on (ab)G, from opposite directionsThis equality is the entire proof the exchange worked. Nothing about the comparison is cryptographic — it's a plain string check the tool runs purely to show you the two independently-computed values actually match.
From a curve point to 256 raw bits
The value handed back by deriveBits() isn't hashed or processed further — the Web Crypto spec defines it as the raw x-coordinate of that shared point, encoded as a fixed-length big-endian byte string. That's exactly the hex value labeled "Secret" in this tool.
Raw curve coordinates like this aren't perfectly uniformly distributed as random bits — some x-coordinates are reachable from more private keys than others — so real protocols always run this value through a proper key derivation function (HKDF, covered in the key derivation module) before using it as an encryption key. This tool shows the raw value specifically so you can see what ECDH actually outputs, before that extra step most applications add on top.
// The third argument to deriveBits() above, 256, is exactly this:
// P-256's field size in bits, with no hashing or KDF applied.
// A real application would instead do something like:
const rawSecret = await crypto.subtle.deriveBits({ name: "ECDH", public: bobKeys.publicKey }, aliceKeys.privateKey, 256);
const encryptionKey = await crypto.subtle.importKey("raw", rawSecret, "HKDF", false, ["deriveKey"]);
// ...then deriveKey() an actual AES-GCM key from that, rather than using rawSecret directlyThis tool intentionally stops at the raw bits so you can see them — production code should never use deriveBits()'s output as a key directly.
Why Mallory's attempt lands somewhere else entirely
Mallory has her own, completely unrelated private scalar m. Multiplying it by Alice's public point gives m·(aG) — a real, valid point on the curve, but with no algebraic relationship to Bob's private key b. There's no shortcut from "any point on the curve" back to "the specific point only someone holding Bob's exact private key could reach" — that shortcut not existing is the elliptic curve discrete logarithm problem itself, the same hard problem the ECC module is built around.
const mallory = await crypto.subtle.generateKey({ name: "ECDH", namedCurve: "P-256" }, true, ["deriveBits"]);
const malloryAttempt = await crypto.subtle.deriveBits(
{ name: "ECDH", public: aliceKeys.publicKey }, // Alice's real public key
mallory.privateKey, // Mallory's own, unrelated private key
256
);
// malloryAttempt = m·(aG) — a valid point, but not (ab)G, so it matches neither Alice's nor Bob's secretNote what this does and doesn't demonstrate: Mallory's call succeeds — WebCrypto has no way to know her key isn't "supposed to" be part of this exchange. The security comes entirely from the math producing a different point, not from any check WebCrypto performs.