Initial Commit
This commit is contained in:
5
packages/security-stack/security/srp/src/params/index.ts
Normal file
5
packages/security-stack/security/srp/src/params/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export type { SrpParams } from "./params.js";
|
||||
export {
|
||||
params2048Sha256,
|
||||
validateSrpParams,
|
||||
} from "./params.js";
|
||||
71
packages/security-stack/security/srp/src/params/params.md
Normal file
71
packages/security-stack/security/srp/src/params/params.md
Normal 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.
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
92
packages/security-stack/security/srp/src/params/params.ts
Normal file
92
packages/security-stack/security/srp/src/params/params.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user