4.7 KiB
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(...)producesK = SHA-256(S)and the client proofM1 = H(A || B || K)per the RFC.- Tamper-resistant
Bvalidation:B mod N == 0aborts withSRP.INVALID_B. Theu = H(A || B) == 0case is excluded via the collision-resistance of SHA-256 and is documented in code rather than re-checked at runtime. verifyServerProofis constant-time (constantTimeEqual) and throws on mismatch withCRYPTOVAULT.SRP.AUTHENTICATION_FAILED.- Reject groups with
Nshorter than 2048 bits at construction time. - OTel histograms for handshake duration and counters for success / failure.
Usage: Basic Example
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
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 toDate.now.rng?: Rng— overrides the default CSPRNG for salt and ephemeral generation.N?: bigint,g?: bigint— group overrides for tests. Construction rejects anyNshorter than 2048 bits withSRP.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)andu = 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
constantTimeEqualfor every MAC / proof check.