# SRP-6a client ## Purpose `srp-client.ts` implements the client side of the SRP-6a zero-knowledge password-authenticated key exchange per RFC 5054, using SHA-256 as the one-way hash and the 2048-bit safe-prime group `N`/`g` from Appendix A. The class hands the caller everything needed for an interoperable handshake against any conformant server: salt generation, private key derivation `x = H(s, H(I:p))`, verifier `v = g^x mod N`, ephemeral keypair `(a, A = g^a mod N)`, session key derivation, and constant-time server-proof verification. ## Features - Salt generation via `generateSalt(length?)` honouring an injected RNG. - Private key, verifier, and ephemeral keypair primitives — pure, deterministic given inputs (or RNG). - `deriveSessionKey(...)` produces `K = SHA-256(S)` and the client proof `M1 = H(A || B || K)` per the RFC. - Tamper-resistant `B` validation: `B mod N == 0` aborts with `SRP.INVALID_B`. The `u = H(A || B) == 0` case is excluded via the collision-resistance of SHA-256 and is documented in code rather than re-checked at runtime. - `verifyServerProof` is constant-time (`constantTimeEqual`) and throws on mismatch with `CRYPTOVAULT.SRP.AUTHENTICATION_FAILED`. - Reject groups with `N` shorter than 2048 bits at construction time. - OTel histograms for handshake duration and counters for success / failure. ## Usage: Basic Example ```ts import { SrpClient } from "@suaveplan/crypto-vault/srp/srp-client"; const client = new SrpClient(); const salt = client.generateSalt(); const privateKey = client.derivePrivateKey( "alice@example.com", "correct horse battery staple", salt ); const verifier = client.deriveVerifier(privateKey); console.log(verifier.length); // 256 ``` ## Usage: Advanced Example ```ts import { SrpClient } from "@suaveplan/crypto-vault/srp/srp-client"; import { SRP_G, SRP_N_2048, } from "@suaveplan/crypto-vault/srp/srp-group"; import { pow } from "@noble/curves/abstract/modular"; // Symmetric handshake fixture: same client + a hand-rolled server share. const client = new SrpClient(); const salt = client.generateSalt(); const x = client.derivePrivateKey("alice", "pw", salt); const v = BigInt("0x" + Buffer.from(client.deriveVerifier(x)).toString("hex")); // Server side computes B = (k*v + g^b) mod N (hand-rolled for the example). const k = 3n; // simplified — production: H(N||g) const b = 7n; const B = ((k * v) + pow(SRP_G, b, SRP_N_2048)) % SRP_N_2048; // Client side derives session key and proof. const { a, A } = client.generateEphemeralKeypair(); const result = client.deriveSessionKey(a, A, B, x); console.log(result.K.length); // 32 ``` ## API Reference ### `class SrpClient` #### `new SrpClient(options?)` `SrpClientOptions` accepts: - `clock?: () => number` — defaults to `Date.now`. - `rng?: Rng` — overrides the default CSPRNG for salt and ephemeral generation. - `N?: bigint`, `g?: bigint` — group overrides for tests. Construction rejects any `N` shorter than 2048 bits with `SRP.WEAK_PARAMETERS`. #### `generateSalt(length = 16): Uint8Array` Random salt. Honours `rng?.nextBytes` if provided; otherwise uses `crypto.getRandomValues` via `@suaveplan/crypto-utils`. #### `derivePrivateKey(identity, password, salt): Uint8Array` Computes `x = H(salt, H(identity:password))`. Throws `SRP.INVALID_PRIVATE_KEY` for empty inputs. #### `deriveVerifier(privateKey): Uint8Array` Returns a 256-byte big-endian `v = g^x mod N`. #### `generateEphemeralKeypair(): EphemeralKeypair` `{ a: bigint; A: bigint }` — `a` is the private exponent (kept by the client), `A` the public form sent to the server. Reseeds when the entropy collapses to zero. #### `deriveSessionKey(a, A, B, privateKey): SessionKeyResult` Returns `{ K, S, M1 }` per the RFC. Throws `SRP.INVALID_B` when `B mod N` is zero. #### `verifyServerProof(A, M1, K, serverProof): boolean` Constant-time compare with the recomputed `M2 = H(A || M1 || K)`. Throws `SRP.AUTHENTICATION_FAILED` on mismatch; otherwise returns `true`. #### `computeServerProof(A, M1, K): Uint8Array` Helper exposed for symmetric tests and server-side adapters. ## Implementation Notes - The implementation deliberately keeps every state transition pure — callers thread `(a, A, B, x)` through the methods rather than relying on instance fields. - All `bigint` ↔ byte conversions are big-endian and zero-padded to the group byte length so that hashing is deterministic regardless of the numeric value. - `k = H(N || g)` and `u = H(A || B)` use SHA-256 with both inputs padded to 256 bytes per RFC 5054 §2.5.3 / §2.6. - Constant-time comparison protects against timing-based proof guessing, matching the broader Suaveplan posture of using `constantTimeEqual` for every MAC / proof check.