Files
suaveplan/packages/security-stack/security/srp/src/client/client-start.ts
2026-05-27 23:47:53 +01:00

53 lines
2.0 KiB
TypeScript

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 };
}