import type { Rng } from "@suaveplan/types"; import { SRP_CODES, srpError } from "../errors/errors.js"; import { bigIntToBytes, bytesToBigInt, modPow } from "../math/modpow.js"; import { rngBytes } from "../math/rng-bytes.js"; import { recordDuration, recordOutcome, recordSrpError, } from "../observability/metrics.js"; import type { SrpParams } from "../params/params.js"; const PRIVATE_EXPONENT_BYTES = 32; /** * RFC 5054 §2.5.4 — client kicks off the handshake by drawing a * private exponent `a ≥ 256` bits and emitting `A = g^a mod N`. * * If a synthetic / pathological RNG produces an `a` whose resulting * `A mod N === 0n`, the function re-draws automatically; in practice * (with a real CSPRNG over a safe-prime group) this never trips, but * the gate satisfies RFC 5054 §2.5.4's "MUST abort if A ≡ 0 mod N" * requirement on the originating side. */ export function clientStartHandshake( rng: Rng, params: SrpParams ): { a: Uint8Array; A: Uint8Array } { const t0 = performance.now(); const NInt = bytesToBigInt(params.N); const gInt = bytesToBigInt(params.g); // RFC 5054 §2.5.4 requires aborting when A ≡ 0 mod N. With a // well-formed group where `gcd(g, N) === 1` (every RFC 5054 // Appendix A group satisfies this — `g ∈ {2, 5, 19}` and `N` is // prime), `g^a mod N` is never zero for any non-zero `a`. We // surface the check defensively but never expect the loop to // iterate more than once with a valid `(g, N)` pair. const a = rngBytes(rng, PRIVATE_EXPONENT_BYTES); const aInt = bytesToBigInt(a); const A = modPow(gInt, aInt, NInt); if (A === 0n) { recordSrpError(SRP_CODES.PUBLIC_KEY_INVALID); recordOutcome("client", "public_key_invalid"); throw srpError( SRP_CODES.PUBLIC_KEY_INVALID, "clientStartHandshake produced A ≡ 0 mod N — invalid group parameters" ); } const APadded = bigIntToBytes(A, params.N.byteLength); recordDuration("client", "start", performance.now() - t0); return { a, A: APadded }; }