Diffie-Hellman: A Secret over a Public Wire
in reviewThis lesson is complete and being validated before release - details may still change.
in reviewThis lesson is complete and being validated before release - details may still change.
diffie-hellman.ts
import { ModularArithmetic } from "../number-theory/modular-arithmetic";import { Mulberry32 } from "../random";import type { Tracer } from "../trace";/** The largest prime below 2^16: the group the handshake lesson uses untraced. */export const MAX_PRIME = 65521;/** Eve's brute force gives up after this many powers, so a trace stays short. */export const MAX_LOG_TRIALS = 128;/** Primes small enough for Eve to finish on the stage, each with a generator. */export const GROUPS: readonly { p: number; g: number }[] = [ { p: 23, g: 5 }, { p: 47, g: 5 }, { p: 97, g: 5 }, { p: 127, g: 3 },];export interface Exchange { p: number; g: number; a: number; aliceSecret: number; b: number; bobSecret: number; shareA: number; shareB: number; sharedA: number; sharedB: number; agree: boolean;}export interface LogResult { target: number; /** The exponent found, or null when the trials ran out. */ exponent: number | null; trials: number; found: boolean;}export interface DhRun { exchange: Exchange; eve: LogResult; /** What Eve computes from the secret she recovered, or null. */ eveShared: number | null; broken: boolean;}/** * Diffie-Hellman key exchange: everyone agrees on a prime p and a * generator g in public; Alice picks a secret a and sends g to the a, * Bob picks b and sends g to the b, and each raises what they got to * their own secret, so both hold g to the ab while the wire carried * only g to the a and g to the b. Eve, who saw both, needs a or b, * and finding the exponent that gives a known power of g is the * discrete logarithm, for which nothing much better than trying every * exponent is known when p is large. Here Eve tries them, which works * at the stage's sizes and is why real groups have 2048-bit primes. * * Time: O(log p) multiplications per party; O(p) for Eve. Space: O(1). */export class DiffieHellman { constructor( private readonly tracer?: Tracer, private readonly modular: ModularArithmetic = new ModularArithmetic(), ) {} /** The public parameters: a prime and a generator of its multiplicative group. */ publish(p: number, g: number): void { if (!ModularArithmetic.isPrime(p) || p > MAX_PRIME) throw new RangeError(`p must be a prime up to ${MAX_PRIME}, got ${p}`); if (!ModularArithmetic.isGenerator(g, p)) throw new RangeError(`${g} does not generate the group mod ${p}`); // ▸ the prime and the generator are agreed in the open this.tracer?.emit("publish", { p, g }); } /** A party's secret and the share it publishes, g to the secret. */ keyPair(party: string, p: number, g: number, secret: number): number { if (!Number.isInteger(secret) || secret < 1 || secret > p - 2) throw new RangeError(`a secret is a whole number from 1 to p - 2, got ${secret}`); // ▸ each party picks an exponent and tells nobody this.tracer?.emit("secret", { party, secret }); const share = this.modular.pow(g, secret, p); // ▸ g to the secret, mod p: safe to show, hard to undo this.tracer?.emit("share", { party, secret, share }); return share; } /** A share crosses the wire, where anyone can read it. */ send(from: string, to: string, value: number): number { // ▸ the share crosses the wire, where eve reads it this.tracer?.emit("send", { from, to, value }); return value; } /** The received share raised to the party's own secret. */ combine(party: string, p: number, received: number, secret: number): number { const shared = this.modular.pow(received, secret, p); // ▸ the other share to your own secret: g to the ab either way this.tracer?.emit("combine", { party, received, secret, shared }); return shared; } /** The whole exchange between Alice with secret a and Bob with secret b. */ exchange(p: number, g: number, a: number, b: number): Exchange { exchange(p: number, g: number, aliceSecret: number, bobSecret: number): Exchange { this.publish(p, g); const shareA = this.keyPair("alice", p, g, a); const shareA = this.keyPair("alice", p, g, aliceSecret); const shareB = this.keyPair("bob", p, g, b); const shareB = this.keyPair("bob", p, g, bobSecret); const toBob = this.send("alice", "bob", shareA); const toAlice = this.send("bob", "alice", shareB); const sharedA = this.combine("alice", p, toAlice, a); const sharedA = this.combine("alice", p, toAlice, aliceSecret); const sharedB = this.combine("bob", p, toBob, b); const sharedB = this.combine("bob", p, toBob, bobSecret); const agree = sharedA === sharedB; // ▸ both parties hold the same key, and never sent it this.tracer?.emit("agree", { sharedA, sharedB, same: agree }); return { p, g, a, b, shareA, shareB, sharedA, sharedB, agree }; return { p, g, aliceSecret, bobSecret, shareA, shareB, sharedA, sharedB, agree }; } /** Eve's attack: multiply by g until the running power equals the target, or the trials run out. */ discreteLog(p: number, g: number, target: number): LogResult { let value = 1; for (let x = 1; x <= Math.min(MAX_LOG_TRIALS, p - 1); x++) { value = this.modular.mul(value, g, p); const hit = value === target; // ▸ try the next exponent: is g to the x the share? this.tracer?.emit("trial", { x, value, target, hit }); if (hit) return { target, exponent: x, trials: x, found: true }; } return { target, exponent: null, trials: Math.min(MAX_LOG_TRIALS, p - 1), found: false }; } /** Secrets from the seed, the exchange, then Eve reads the wire and tries to recover Alice's secret from her share. */ run(p: number, g: number, seed: number): DhRun { const random = new Mulberry32(seed); const a = random.intBetween(2, p - 2); const aliceSecret = random.intBetween(2, p - 2); const b = random.intBetween(2, p - 2); const bobSecret = random.intBetween(2, p - 2); const exchange = this.exchange(p, g, a, b); const exchange = this.exchange(p, g, aliceSecret, bobSecret); // ▸ eve has p, g, and both shares, and needs a or b this.tracer?.emit("eavesdrop", { p, g, shareA: exchange.shareA, shareB: exchange.shareB }); const eve = this.discreteLog(p, g, exchange.shareA); const eveShared = eve.exponent === null ? null : this.modular.pow(exchange.shareB, eve.exponent, p); const broken = eveShared === exchange.sharedA; // ▸ with the exponent, eve computes the key too this.tracer?.emit("broken", { exponent: eve.exponent, trials: eve.trials, shared: eveShared, found: broken }); return { exchange, eve, eveShared, broken }; }}Shared key 12, agreed on both ends; Eve recovered it after 15 trials, which is why real primes are far larger.