Files
skills/docs/security-stack/crypto/crypto-vault/src/srp/srp-client.md
2026-05-29 08:20:56 +01:00

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(...) 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

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 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.