Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
export type { SrpParams } from "./params.js";
export {
params2048Sha256,
validateSrpParams,
} from "./params.js";

View File

@@ -0,0 +1,71 @@
# `params`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`SrpParams` is the per-handshake parameter bundle every SRP-6a call in `@suaveplan/srp` consumes. It pairs a safe-prime group `(N, g)`, the bit length of `N`, an injected `HashLike` hash function, and a stable algorithm tag (`"SHA256"` or `"SHA1"`). One `SrpParams` value drives every step of a handshake — `computeVerifier`, `clientStartHandshake`, `clientFinishHandshake`, `serverStartHandshake`, `serverFinishHandshake`, `serverComputeM2` — so the same group and hash bind every intermediate value into the proof chain.
## Features
- `params2048Sha256(hash)` convenience factory for the recommended default (RFC 5054 Appendix A 2048-bit group + SHA-256).
- `validateSrpParams(params)` structural validation throwing `SRP.INVALID_PARAMS` on malformed shapes.
- Algorithm gate throwing `SRP.HASH_UNSUPPORTED` for any value outside `{"SHA256", "SHA1"}`.
- `g >= N` rejected at the boundary so subsequent `modPow` calls cannot escape the intended group.
## Usage: Basic Example
```ts
import { params2048Sha256 } from "@suaveplan/srp/params";
import { nobleHashBridge } from "@suaveplan/srp/hash";
const params = params2048Sha256(nobleHashBridge("SHA256"));
console.log(params.bitLength); // 2048
console.log(params.algorithm); // "SHA256"
console.log(params.N.byteLength); // 256
```
## Usage: Advanced Example
```ts
import { validateSrpParams, type SrpParams } from "@suaveplan/srp/params";
import { N_4096, g_4096 } from "@suaveplan/srp/groups/4096";
import { defaultHashBridge } from "@suaveplan/srp/hash";
const params: SrpParams = {
N: N_4096,
g: g_4096,
bitLength: 4096,
hash: defaultHashBridge("SHA256"),
algorithm: "SHA256",
};
validateSrpParams(params);
console.log("4096-bit SHA-256 params validated");
```
## API Reference
### `interface SrpParams`
| Field | Type | Description |
| ----------- | ------------------- | ---------------------------------------------------- |
| `N` | `Uint8Array` | Safe-prime modulus as big-endian bytes. |
| `g` | `Uint8Array` | Generator as big-endian bytes. |
| `bitLength` | `number` | Bit length of `N`; MUST equal `N.byteLength * 8`. |
| `hash` | `HashLike` | Injected hash bridge (sync or async). |
| `algorithm` | `"SHA256" \| "SHA1"`| Stable algorithm tag. |
### `params2048Sha256(hash: HashLike): SrpParams`
Returns a validated `SrpParams` for the 2048-bit group + SHA-256.
### `validateSrpParams(params: SrpParams): void`
Throws `SrpError(SRP.INVALID_PARAMS)` when `N.byteLength` is not a multiple of 4, `bitLength` does not match `N.byteLength * 8`, `g <= 1`, or `g >= N`. Throws `SrpError(SRP.HASH_UNSUPPORTED)` when `algorithm` is not in `{"SHA256", "SHA1"}`.
## Implementation Notes
The 4-byte alignment check on `N.byteLength` reflects the operational shape of every documented RFC 5054 group — 128, 192, 256, 384, 512, 768, 1024 bytes — and serves as a structural smoke-test: a modulus that is not 4-byte aligned almost certainly came from a decoding bug rather than an actual RFC 5054 group.
`validateSrpParams` is called from `params2048Sha256` at construction time so the factory-returned `SrpParams` is already validated when handed to a handshake function. Consumers building `SrpParams` from raw constants (custom groups, test fixtures) MUST call `validateSrpParams` explicitly; the handshake functions DO NOT re-validate on every call to keep the per-handshake overhead minimal.

View File

@@ -0,0 +1,85 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SRP_CODES, SrpError } from "../errors/errors.js";
import { nobleHashBridge } from "../hash/hash-bridge.js";
import type { SrpParams } from "./params.js";
import { params2048Sha256, validateSrpParams } from "./params.js";
describe("params2048Sha256", () => {
it("emits SHA-256 algorithm tag", { timeout: 5000 }, () => {
const params = params2048Sha256(nobleHashBridge("SHA256"));
expect(params.algorithm).toBe("SHA256");
expect(params.bitLength).toBe(2048);
expect(params.N.byteLength).toBe(256);
expect(params.g.byteLength).toBe(1);
});
});
describe("validateSrpParams", () => {
function baseParams(): SrpParams {
return params2048Sha256(nobleHashBridge("SHA256"));
}
it("rejects N.byteLength not multiple of 4", { timeout: 5000 }, () => {
const bad: SrpParams = {
...baseParams(),
N: new Uint8Array(17),
bitLength: 17 * 8,
};
try {
validateSrpParams(bad);
throw new Error("validateSrpParams did not throw");
} catch (err) {
expect(err).toBeInstanceOf(SrpError);
expect((err as SrpError).code).toBe(SRP_CODES.INVALID_PARAMS);
}
});
it("rejects bitLength mismatch", { timeout: 5000 }, () => {
const bad: SrpParams = {
...baseParams(),
bitLength: 1024,
};
expect(() => validateSrpParams(bad)).toThrow(SrpError);
});
it("rejects algorithm outside SHA256/SHA1", { timeout: 5000 }, () => {
const bad = {
...baseParams(),
algorithm: "SHA512" as never,
};
try {
validateSrpParams(bad);
throw new Error("did not throw");
} catch (err) {
expect(err).toBeInstanceOf(SrpError);
expect((err as SrpError).code).toBe(SRP_CODES.HASH_UNSUPPORTED);
}
});
it("rejects g <= 1", { timeout: 5000 }, () => {
const bad: SrpParams = {
...baseParams(),
g: new Uint8Array([1]),
};
try {
validateSrpParams(bad);
throw new Error("did not throw");
} catch (err) {
expect((err as SrpError).code).toBe(SRP_CODES.INVALID_PARAMS);
}
});
it("rejects g >= N", { timeout: 5000 }, () => {
const params = baseParams();
const bad: SrpParams = {
...params,
g: params.N.slice(), // g === N
};
try {
validateSrpParams(bad);
throw new Error("did not throw");
} catch (err) {
expect((err as SrpError).code).toBe(SRP_CODES.INVALID_PARAMS);
}
});
});

View File

@@ -0,0 +1,92 @@
import type { HashLike } from "@suaveplan/types";
import { SRP_CODES, srpError } from "../errors/errors.js";
import { g_2048, N_2048 } from "../groups/groups-2048.js";
import type { SrpHashAlgorithm } from "../hash/hash-bridge.js";
/**
* Per-handshake SRP parameter bundle. Pairs a safe-prime group
* `(N, g)` with a `HashLike` hash function and a stable algorithm tag.
*/
export interface SrpParams {
/** Safe-prime modulus `N` as a big-endian `Uint8Array`. */
readonly N: Uint8Array;
/** Generator `g` as a big-endian `Uint8Array`. */
readonly g: Uint8Array;
/** Bit length of `N` (must equal `N.byteLength * 8`). */
readonly bitLength: number;
/** Injected hash bridge (sync or async). */
readonly hash: HashLike;
/** Stable algorithm tag — `"SHA256"` (default) or `"SHA1"` (legacy). */
readonly algorithm: SrpHashAlgorithm;
}
/**
* Structurally validate an `SrpParams` candidate. Throws
* `SrpError(SRP.INVALID_PARAMS)` on:
*
* - `N.byteLength` not a multiple of 4,
* - `bitLength !== N.byteLength * 8`,
* - `g`'s decoded big-int `<= 1`,
* - `g`'s decoded big-int `>= N`'s decoded big-int.
*
* Throws `SrpError(SRP.HASH_UNSUPPORTED)` when `algorithm` is not in
* `{"SHA256", "SHA1"}`.
*/
export function validateSrpParams(params: SrpParams): void {
if (params.N.byteLength % 4 !== 0) {
throw srpError(
SRP_CODES.INVALID_PARAMS,
`N.byteLength must be a multiple of 4, got ${params.N.byteLength}`
);
}
if (params.bitLength !== params.N.byteLength * 8) {
throw srpError(
SRP_CODES.INVALID_PARAMS,
`bitLength must equal N.byteLength * 8, got ${params.bitLength} for ${params.N.byteLength}-byte N`
);
}
if (params.algorithm !== "SHA256" && params.algorithm !== "SHA1") {
throw srpError(
SRP_CODES.HASH_UNSUPPORTED,
`unsupported algorithm: ${String(params.algorithm)}`
);
}
const gInt = decodeBigEndian(params.g);
if (gInt <= 1n) {
throw srpError(
SRP_CODES.INVALID_PARAMS,
`g must decode to a big-int > 1, got ${gInt}`
);
}
const NInt = decodeBigEndian(params.N);
if (gInt >= NInt) {
throw srpError(
SRP_CODES.INVALID_PARAMS,
"g must decode to a big-int strictly less than N"
);
}
}
function decodeBigEndian(bytes: Uint8Array): bigint {
let acc = 0n;
for (const b of bytes) {
acc = (acc << 8n) | BigInt(b);
}
return acc;
}
/**
* Convenience factory for the bundled 2048-bit safe-prime group with
* SHA-256 hashing — the recommended default for new deployments.
*/
export function params2048Sha256(hash: HashLike): SrpParams {
const params: SrpParams = {
N: N_2048,
g: g_2048,
bitLength: 2048,
hash,
algorithm: "SHA256",
};
validateSrpParams(params);
return params;
}