3.3 KiB
client-start
Purpose
Kick off the SRP-6a handshake on the client side. Draws a private exponent a ≥ 256 bits from an injectable Rng and emits the public value A = g^a mod N. Per RFC 5054 §2.5.4 the function rejects (re-draws) any a whose resulting A ≡ 0 mod N; in practice a real CSPRNG over a safe-prime group never trips this gate, but the gate is wired so synthetic / adversarial RNGs cannot escape the protocol invariant.
Features
- 256-bit private exponent — equal to the RFC 5054 §2.5.4 minimum.
- Re-draw on
A ≡ 0 mod Nup to 16 attempts before surrendering withSRP.PUBLIC_KEY_INVALID. - Deterministic from a seeded
Rng— same seed yields identical(a, A). - Returns
Azero-padded tobyteLength(N). - Emits
srp.handshake_duration_ms{role=client,phase=start}and on the abort pathsrp.outcome_total{role=client,outcome=public_key_invalid}+srp.errors_total{code=SRP.PUBLIC_KEY_INVALID}.
Usage: Basic Example
import { clientStartHandshake } from "@suaveplan/srp/client";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
const params = params2048Sha256(nobleHashBridge("SHA256"));
const rng = {
nextUint32: () => 0x12345678,
nextFloat: () => 0.5,
nextBytes: (n: number) => crypto.getRandomValues(new Uint8Array(n)),
};
const { a, A } = clientStartHandshake(rng, params);
console.log(a.byteLength, A.byteLength); // 32 256
Usage: Advanced Example
import { clientStartHandshake } from "@suaveplan/srp/client";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
import type { Rng } from "@suaveplan/types";
const params = params2048Sha256(nobleHashBridge("SHA256"));
// Seeded deterministic RNG — useful for replaying a known handshake in tests.
function seededRng(seed: number): Rng {
let state = seed >>> 0;
return {
nextUint32: () => {
state = (state * 1664525 + 1013904223) >>> 0;
return state;
},
nextFloat: () => 0.5,
};
}
const { a: a1, A: A1 } = clientStartHandshake(seededRng(1), params);
const { a: a2, A: A2 } = clientStartHandshake(seededRng(1), params);
console.log(Buffer.from(A1).equals(Buffer.from(A2))); // true
API Reference
clientStartHandshake(rng: Rng, params: SrpParams): { a: Uint8Array; A: Uint8Array }
Returns { a, A }. a is exactly 32 bytes (256 bits) of fresh entropy from rng. A is params.N.byteLength bytes, zero-padded on the left.
Implementation Notes
The 16-attempt re-draw cap exists so a pathological RNG cannot trap the function in an unbounded loop. With a real CSPRNG the probability of A ≡ 0 mod N per attempt is ≤ 1/N, which for the bundled 2048-bit group is ≤ 2^-2048; the 16-attempt cap is purely a synthetic-test safety net.
a.byteLength === 32 is the RFC 5054 §2.5.4 floor ("at least 256 bits"). Drawing more bits gains no security headroom because a is implicitly reduced modulo N inside modPow; we draw exactly the minimum to keep allocations small.
The function is CPU-bound — one modPow over a 2048-bit safe prime takes ~5–20 ms on contemporary hardware. It is intentionally synchronous; consumers that want to run it off the main thread should schedule it via a Web Worker or similar.