← Back to the tool

Public-key · Under the hood

ECDH key exchange

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

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.

The actual WebCrypto call
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, independently

The 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.

The actual WebCrypto call
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.privateKey

The 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.

AliceBobpublic key A = aGpublic key B = bG

That's the entire exchange — only the public keys shown above ever cross the wire. Everything after this point happens independently on each side.

Alice: a⋅(bG)Bob: b⋅(aG)\text{Alice: } a \cdot (bG) \qquad \text{Bob: } b \cdot (aG)

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.

What this tool checks afterward
aliceSecretHex === bobSecretHex   // true — both sides landed on (ab)G, from opposite directions

This 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.

No separate call — the 256 above is this step
// 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 directly

This 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.

The actual WebCrypto call
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 secret

Note 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.

Now try it

Back to ECDH key exchange →

Simulate Alice and Bob generating independent key pairs and deriving the exact same shared secret — without ever transmitting it.