# `server-finish` ## Purpose Complete the SRP-6a handshake on the server side after receiving the client's public value `A` and proof `M1`. Validates `A mod N != 0` (RFC 5054 §2.5.4 abort condition), derives the server-side shared secret `S = (A * v^u)^b mod N`, hashes it into the session key `K = H(S)`, recomputes the expected `M1`, and constant-time-compares against the client's `M1`. On match emits `M2 = H(A | M1 | K)` per RFC 2945 §3. On mismatch throws `SrpError(SRP.AUTH_FAILED)`. ## Features - `A mod N == 0` gate per RFC 5054 §2.5.4. - `u ≡ 0 mod N` gate inherited from `computeU`. - Constant-time `M1` comparison via `@suaveplan/crypto-utils.constantTimeEqual`. - Returns `K`, `M2`, and `S` so consumers can chain into a KDF on `S` directly. - Emits `srp.handshake_duration_ms{role=server,phase=finish}` plus the appropriate outcome counter (`ok` / `auth_failed` / `public_key_invalid`). ## Usage: Basic Example ```ts import { serverFinishHandshake } from "@suaveplan/srp/server"; import { params2048Sha256 } from "@suaveplan/srp/params"; import { nobleHashBridge } from "@suaveplan/srp/hash"; const params = params2048Sha256(nobleHashBridge("SHA256")); const salt = new Uint8Array(16); const A = new Uint8Array(256); A[255] = 1; const B = new Uint8Array(256); B[255] = 2; const b = new Uint8Array(32); b[31] = 1; const v = new Uint8Array(256); v[255] = 1; const M1Client = new Uint8Array(32); // server expects to recompute this and compare try { serverFinishHandshake("alice", salt, A, B, b, v, M1Client, params); } catch (err) { console.log("auth failed:", (err as Error).message); } ``` ## Usage: Advanced Example ```ts import { serverFinishHandshake, serverStartHandshake } from "@suaveplan/srp/server"; import { clientFinishHandshake, clientStartHandshake } from "@suaveplan/srp/client"; import { computeVerifier } from "@suaveplan/srp/verifier"; import { params2048Sha256 } from "@suaveplan/srp/params"; import { nobleHashBridge } from "@suaveplan/srp/hash"; const params = params2048Sha256(nobleHashBridge("SHA256")); const salt = new Uint8Array(16); const rng = { nextUint32: () => 0, nextFloat: () => 0, nextBytes: (n: number) => crypto.getRandomValues(new Uint8Array(n)), }; const { v } = computeVerifier("alice", "password123", salt, params); const { a, A } = clientStartHandshake(rng, params); const { b, B } = serverStartHandshake(v, rng, params); const client = clientFinishHandshake("alice", "password123", salt, A, B, a, params); const server = serverFinishHandshake("alice", salt, A, B, b, v, client.M1, params); console.log(Buffer.from(client.K).equals(Buffer.from(server.K))); // true ``` ## API Reference ### `serverFinishHandshake(I, salt, A, B, b, v, M1Client, params): { K, M2, S }` Throws `SrpError(SRP.PUBLIC_KEY_INVALID)` when `A mod N == 0` or `u ≡ 0 mod N`. Throws `SrpError(SRP.AUTH_FAILED)` when the recomputed `M1` does not constant-time-match `M1Client`. On success returns the session key `K`, the server proof `M2`, and the raw shared secret `S`. ## Implementation Notes The order `validate A mod N → compute u → compute S → compute K → compute expectedM1 → compare M1Client` is intentional: the cheap modular check (`A mod N == 0`) happens first so an adversary cannot force the server to spend a `modPow` on garbage; `computeU` runs next because it has its own abort gate (`u ≡ 0`); only then do we pay the full `modPow` chain. This ordering minimises wasted server CPU on adversarial inputs without leaking any new timing channel (every legitimate handshake takes the same path). The constant-time `M1` compare is the last gate before emitting `M2`. We use `expectedM1` (the server's locally-computed proof) inside `computeM2` rather than `M1Client` — even though they are byte-identical when the compare succeeds — because reusing the server-side computation eliminates a class of subtle bugs where a malicious client substitutes a near-miss `M1` that happens to match by the time the compare runs.