#!/usr/bin/env bun /** * Deterministic SRP test-vector generator. * * Runs the full SRP-6a handshake (computeVerifier → clientStartHandshake → * serverStartHandshake → clientFinishHandshake → serverFinishHandshake) * for the bundled 2048-bit group with SHA-256, using a fixed-seed * deterministic RNG, and writes the resulting `(x, v, A, B, K, M1, M2)` * tuple to `src/__fixtures__/vectors-2048-sha256.json`. * * Re-running the script with the same seed produces a byte-identical * JSON fixture. The conformance test replays the same handshake against * the committed fixture to anchor the modern 2048/SHA-256 default. * * Usage: * bun scripts/generate-srp-vectors.ts [--seed=42] */ import { mkdirSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import type { Rng } from "@suaveplan/types"; import { clientFinishHandshake } from "../src/client/client-finish.js"; import { clientStartHandshake } from "../src/client/client-start.js"; import { nobleHashBridge } from "../src/hash/hash-bridge.js"; import { params2048Sha256 } from "../src/params/params.js"; import { serverFinishHandshake } from "../src/server/server-finish.js"; import { serverStartHandshake } from "../src/server/server-start.js"; import { computeVerifier } from "../src/verifier/verifier.js"; function seededRng(seed: number): Rng { let state = seed >>> 0; return { nextUint32: () => { state = (state * 1664525 + 1013904223) >>> 0; return state; }, nextFloat: () => 0.5, nextBytes: (n: number) => { const out = new Uint8Array(n); let i = 0; while (i < n) { state = (state * 1664525 + 1013904223) >>> 0; out[i++] = state & 0xff; if (i < n) out[i++] = (state >>> 8) & 0xff; if (i < n) out[i++] = (state >>> 16) & 0xff; if (i < n) out[i++] = (state >>> 24) & 0xff; } return out; }, }; } function toHex(bytes: Uint8Array): string { return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); } function parseSeed(): number { const arg = process.argv.find((a) => a.startsWith("--seed=")); if (arg) return Number.parseInt(arg.slice("--seed=".length), 10); return 42; } async function main(): Promise { const seed = parseSeed(); const params = params2048Sha256(nobleHashBridge("SHA256")); const I = "alice@example.com"; const P = "correct horse battery staple"; const salt = seededRng(seed + 1).nextBytes!(16); const { x, v } = computeVerifier(I, P, salt, params); const { a, A } = clientStartHandshake(seededRng(seed + 2), params); const { b, B } = serverStartHandshake(v, seededRng(seed + 3), params); const client = clientFinishHandshake(I, P, salt, A, B, a, params); const server = serverFinishHandshake(I, salt, A, B, b, v, client.M1, params); const fixture = { meta: { group: "RFC 5054 Appendix A 2048-bit", hash: "SHA-256", seed, generator: "scripts/generate-srp-vectors.ts", }, inputs: { I, P, salt_hex: toHex(salt), a_hex: toHex(a), b_hex: toHex(b), }, outputs: { x_hex: toHex(x), v_hex: toHex(v), A_hex: toHex(A), B_hex: toHex(B), K_hex: toHex(client.K), M1_hex: toHex(client.M1), M2_hex: toHex(server.M2), S_hex: toHex(client.S), }, }; const outPath = join( dirname(new URL(import.meta.url).pathname), "..", "src", "__fixtures__", "vectors-2048-sha256.json" ); mkdirSync(dirname(outPath), { recursive: true }); writeFileSync(outPath, `${JSON.stringify(fixture, null, 2)}\n`); console.log(`wrote ${outPath}`); } await main();