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

3.3 KiB

server-start

Purpose

Begin the SRP-6a handshake on the server side. Loads the stored verifier v (alongside the user's (I, salt)), draws a ≥ 256-bit private exponent b, and emits the server public value B = (k * v + g^b) mod N per RFC 5054 §2.5.4. The augmented form (multiplying k into the verifier) is what distinguishes SRP-6a from SRP-6. The function re-draws b if the resulting B mod N == 0, up to 16 attempts.

Features

  • 256-bit private exponent — matches RFC 5054 §2.5.4 minimum.
  • k = H(N | PAD(g)) precomputed once per call (could be memoized externally for further savings).
  • B ≡ 0 mod N re-draw with 16-attempt safety cap.
  • Returns B zero-padded to byteLength(N).
  • Emits srp.handshake_duration_ms{role=server,phase=start} and on abort srp.outcome_total{role=server,outcome=public_key_invalid} + srp.errors_total{code=SRP.PUBLIC_KEY_INVALID}.

Usage: Basic Example

import { serverStartHandshake } from "@suaveplan/srp/server";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
import { computeVerifier } from "@suaveplan/srp/verifier";

const params = params2048Sha256(nobleHashBridge("SHA256"));
const salt = new Uint8Array(16);
const { v } = computeVerifier("alice", "password123", salt, params);
const rng = {
    nextUint32: () => 0,
    nextFloat: () => 0,
    nextBytes: (n: number) => crypto.getRandomValues(new Uint8Array(n)),
};
const { b, B } = serverStartHandshake(v, rng, params);
console.log(b.byteLength, B.byteLength); // 32 256

Usage: Advanced Example

import { serverStartHandshake } from "@suaveplan/srp/server";
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
import { computeVerifier } from "@suaveplan/srp/verifier";
import type { Rng } from "@suaveplan/types";

const params = params2048Sha256(nobleHashBridge("SHA256"));
const salt = new Uint8Array(16);
const { v } = computeVerifier("alice", "password123", salt, params);

// Synthetic deterministic RNG to demonstrate reproducibility.
function seededRng(seed: number): Rng {
    let state = seed >>> 0;
    return {
        nextUint32: () => {
            state = (state * 1664525 + 1013904223) >>> 0;
            return state;
        },
        nextFloat: () => 0.5,
    };
}
const r1 = serverStartHandshake(v, seededRng(42), params);
const r2 = serverStartHandshake(v, seededRng(42), params);
console.log(Buffer.from(r1.B).equals(Buffer.from(r2.B))); // true

API Reference

serverStartHandshake(v, rng, params): { b, B }

Returns { b, B }. b is 32 bytes of fresh entropy from rng; B is params.N.byteLength bytes, zero-padded.

Implementation Notes

kvModN = (k * v) mod N is computed once at the top of the function and reused inside the re-draw loop because it does not depend on b. The only loop-variant work is g^b mod N (the modPow call) and the final ((kvModN + gb) mod N). This matters because the dominant cost is modPow; pulling k*v out of the loop saves wall time on the (vanishingly unlikely) re-draw path.

The re-draw cap is the same 16 as the client side. Per RFC 5054 §2.5.4 the requirement is "MUST abort if B ≡ 0 mod N"; the cap converts that into a finite-bound failure rather than an unbounded loop on a pathological RNG.