53 lines
2.0 KiB
TypeScript
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 };
|
|
}
|