first commit
This commit is contained in:
52
docs/security-stack/security/srp/src/math/k.md
Normal file
52
docs/security-stack/security/srp/src/math/k.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# `k`
|
||||
|
||||
## Purpose
|
||||
|
||||
Compute the SRP-6a multiplier parameter `k = H(N | PAD(g))` per RFC 5054 §2.5.3. The multiplier is the structural change that distinguishes SRP-6a from SRP-6 (the latter set `k = 3`, which broke under a generator-substitution attack). Mixing both `N` and the padded generator into a hash binds `k` to the chosen group so the multiplier cannot be transplanted between groups by an attacker who controls one side of the handshake.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure function of `(N, g, hash)`.
|
||||
- Returns the hash's native digest size (32 bytes for SHA-256, 20 for SHA-1).
|
||||
- Uses `PAD` to zero-extend `g` to `byteLength(N)` before hashing, per RFC 5054 §2.5.3.
|
||||
- Caller-side memoization safe — `params` is treated as immutable.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { computeK } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const k = computeK(params);
|
||||
console.log(k.byteLength); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { computeK } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const kCached = computeK(params);
|
||||
// Reuse k across many handshakes — k is a pure function of (N, g, hash).
|
||||
function getCachedK(): Uint8Array {
|
||||
return kCached;
|
||||
}
|
||||
console.log(getCachedK().byteLength);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `computeK(params: SrpParams): Uint8Array`
|
||||
|
||||
Returns `H(N | PAD(g))` using `params.hash`. Result length equals `params.hash.digestSize`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`k` is a pure function of `(N, g, hash)` and is therefore safe to memoize across handshakes; consumers that mount a long-lived `SrpParams` value can cache `k` outside the per-request critical path. This package does NOT cache automatically because the `SrpParams.hash` field MAY be a stateful subtle bridge that the consumer rotates per session; deferring the memoization decision to the caller keeps the contract simple.
|
||||
|
||||
The digest call goes through `digestSync` (from `../hash/hash-bridge.js`), which throws `SRP.HASH_UNSUPPORTED` if the consumer accidentally wires an async `HashLike` into a sync handshake path. Without that gate, an awaited Promise would silently coerce to `[object Promise]` bytes downstream.
|
||||
63
docs/security-stack/security/srp/src/math/modpow.md
Normal file
63
docs/security-stack/security/srp/src/math/modpow.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# `modpow`
|
||||
|
||||
## Purpose
|
||||
|
||||
Modular exponentiation primitive for `@suaveplan/srp`. This is a thin wrapper over `@noble/curves`'s hardened `pow(num, power, modulo)` from `@noble/curves/abstract/modular`. Every SRP-6a path that needs `base^exp mod N` routes through this one entry point so the codebase never inlines TypeScript's native `BigInt ** BigInt % N` operator — a pattern explicitly forbidden by RFC 5054 §2.5.4's timing-attack guidance because the standard-library BigInt power-and-mod does NOT execute in constant time.
|
||||
|
||||
This module also publishes two utility encoders, `bytesToBigInt` and `bigIntToBytes`, that convert between big-endian `Uint8Array` (the on-wire shape for SRP group elements) and `bigint` (the shape `modPow` accepts). Co-locating the encoders here means every group-element conversion in `src/` flows through one audited helper.
|
||||
|
||||
## Features
|
||||
|
||||
- `modPow(base, exp, modulus): bigint` — single entry point for `base^exp mod modulus`.
|
||||
- `bytesToBigInt(bytes): bigint` — big-endian decode.
|
||||
- `bigIntToBytes(value, length?): Uint8Array` — big-endian encode with optional zero-padded fixed width.
|
||||
- Backed by `@noble/curves/abstract/modular.pow` — vetted, hardened impl.
|
||||
- Banned-pattern source scan in `groups.spec-conformance.test.ts` enforces zero `BigInt ** BigInt %` patterns under `src/`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { modPow, bytesToBigInt, bigIntToBytes } from "@suaveplan/srp";
|
||||
|
||||
const base = bytesToBigInt(new Uint8Array([2]));
|
||||
const exp = bytesToBigInt(new Uint8Array([3]));
|
||||
const mod = bytesToBigInt(new Uint8Array([11]));
|
||||
const result = modPow(base, exp, mod); // 2^3 mod 11 = 8n
|
||||
const back = bigIntToBytes(result, 1); // Uint8Array([8])
|
||||
console.log(result, back);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { modPow, bytesToBigInt, bigIntToBytes } from "@suaveplan/srp";
|
||||
import { N_2048 } from "@suaveplan/srp/groups";
|
||||
|
||||
// Verifier-side: v = g^x mod N where g=2 and x is derived from H(salt|H(I:P)).
|
||||
const N = bytesToBigInt(N_2048);
|
||||
const g = 2n;
|
||||
const x = bytesToBigInt(new Uint8Array(32)); // pretend x is a 32-byte hash
|
||||
const v = modPow(g, x, N);
|
||||
const vBytes = bigIntToBytes(v, N_2048.byteLength);
|
||||
console.log(vBytes.byteLength); // 256
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `modPow(base: bigint, exp: bigint, modulus: bigint): bigint`
|
||||
|
||||
Returns `base^exp mod modulus`. Delegates to `@noble/curves/abstract/modular.pow`. Throws whatever the noble impl throws on bad input (e.g. negative modulus).
|
||||
|
||||
### `bytesToBigInt(bytes: Uint8Array): bigint`
|
||||
|
||||
Decode a big-endian `Uint8Array` to a non-negative `bigint`. Empty input returns `0n`.
|
||||
|
||||
### `bigIntToBytes(value: bigint, length?: number): Uint8Array`
|
||||
|
||||
Encode a non-negative `bigint` to a big-endian `Uint8Array`. When `length` is supplied, the output is zero-padded on the left to that width; when `length` is shorter than the natural byte width of `value`, throws `RangeError`. Without `length`, returns the smallest non-empty representation.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`bigIntToBytes` zero-pads on the left because SRP's wire format treats every group element as a fixed-width big-endian buffer matching `byteLength(N)`. Calling `bigIntToBytes(v, N.byteLength)` is the canonical "encode a group element for transmission or hashing" call; the `PAD` helper in `pad.ts` performs the same left-zero-extension on values whose width is already known to fit, which is the contract RFC 5054 §2.5.3 specifies.
|
||||
|
||||
The `pow` import from `@noble/curves/abstract/modular.js` is the single dependency line in this file. Updating the noble library version updates every SRP `modPow` call site simultaneously — there is no `base ** exp % modulus` fallback to fall out of sync.
|
||||
51
docs/security-stack/security/srp/src/math/pad.md
Normal file
51
docs/security-stack/security/srp/src/math/pad.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# `pad`
|
||||
|
||||
## Purpose
|
||||
|
||||
The `PAD(x, N)` helper from RFC 5054 §2.5.3. Returns a left-zero-extended copy of `x` whose length equals `N.byteLength`. SRP-6a mixes group elements (`A`, `B`, `g`) into hashes only AFTER they have been padded to the full byte-length of the modulus; without this padding the hash output would depend on whether the integer-decoding step happened to strip leading zeros, which would let an attacker malleate the hash by manipulating an otherwise-equivalent group element.
|
||||
|
||||
## Features
|
||||
|
||||
- Left-zero-extension to `N.byteLength`.
|
||||
- `Uint8Array.slice()`-based defensive copy when input already matches the target width (so the caller cannot mutate the shared modulus buffer through the result).
|
||||
- Throws `SrpError(SRP.INVALID_PARAMS)` on oversize input.
|
||||
- Allocation-free fast path for the equal-length case (still returns a fresh `Uint8Array` for ownership clarity).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { PAD } from "@suaveplan/srp";
|
||||
import { N_2048 } from "@suaveplan/srp/groups";
|
||||
|
||||
const short = new Uint8Array([0xab, 0xcd]);
|
||||
const padded = PAD(short, N_2048);
|
||||
console.log(padded.byteLength); // 256
|
||||
console.log(padded.slice(254)); // Uint8Array([0xab, 0xcd])
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { PAD } from "@suaveplan/srp";
|
||||
import { N_2048, g_2048 } from "@suaveplan/srp/groups";
|
||||
import { sha256 } from "@noble/hashes/sha2";
|
||||
|
||||
// k = H(N | PAD(g)) per RFC 5054 §2.5.3.
|
||||
const concatenated = new Uint8Array(N_2048.byteLength + N_2048.byteLength);
|
||||
concatenated.set(N_2048, 0);
|
||||
concatenated.set(PAD(g_2048, N_2048), N_2048.byteLength);
|
||||
const k = sha256(concatenated);
|
||||
console.log(k.byteLength); // 32
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `PAD(x: Uint8Array, N: Uint8Array): Uint8Array`
|
||||
|
||||
Returns a fresh `Uint8Array` of length `N.byteLength` whose trailing `x.byteLength` bytes equal `x` and whose preceding bytes are `0`. Throws `SrpError(SRP.INVALID_PARAMS)` when `x.byteLength > N.byteLength`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The defensive-copy on the equal-length path matters because callers MAY pass a constant reference (e.g. a frozen modulus from `@suaveplan/srp/groups`); returning the same reference would let downstream code accidentally mutate the shared buffer. The cost is one allocation per call, which is negligible compared to a `modPow` over a 2048-bit modulus.
|
||||
|
||||
`PAD` never validates that `x` decodes to a non-zero `Z_N` element — that check lives in the higher-level handshake functions, which reject `A ≡ 0 mod N`, `B ≡ 0 mod N`, and `u ≡ 0 mod N` per RFC 5054 §2.5.4 with `SRP.PUBLIC_KEY_INVALID`. The pad helper's only job is byte-shape normalisation.
|
||||
64
docs/security-stack/security/srp/src/math/proof.md
Normal file
64
docs/security-stack/security/srp/src/math/proof.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# `proof`
|
||||
|
||||
## Purpose
|
||||
|
||||
Internal helpers for computing the SRP-6a / RFC 2945 §3 mutual-authentication proofs `M1` and `M2`. Both client and server compute these values: the client emits `M1 = H(H(N) XOR H(g) | H(I) | salt | A | B | K)` to prove it knows the password, and the server emits `M2 = H(A | M1 | K)` to prove it knows the verifier. Co-locating the math in one module keeps the formula in exactly one place so any future correction (e.g. a hash-binding change) lands consistently across both roles.
|
||||
|
||||
## Features
|
||||
|
||||
- `computeM1(I, salt, A, B, K, params)` — RFC 2945 §3 client proof.
|
||||
- `computeM2(A, M1, K, params)` — RFC 2945 §3 server proof.
|
||||
- Equal-length XOR helper for the `H(N) XOR H(g)` term — bound to the chosen `params.hash` digest size.
|
||||
- UTF-8 encode `I` once per call.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { computeM1, computeM2 } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const salt = new Uint8Array(16);
|
||||
const A = new Uint8Array(256);
|
||||
const B = new Uint8Array(256);
|
||||
const K = new Uint8Array(32);
|
||||
const M1 = computeM1("alice", salt, A, B, K, params);
|
||||
const M2 = computeM2(A, M1, K, params);
|
||||
console.log(M1.byteLength, M2.byteLength); // 32 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { computeM1, computeM2 } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
import { constantTimeEqual } from "@suaveplan/crypto-utils";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const salt = new Uint8Array(16);
|
||||
const A = new Uint8Array(256);
|
||||
const B = new Uint8Array(256);
|
||||
const K = new Uint8Array(32);
|
||||
|
||||
const clientM1 = computeM1("alice", salt, A, B, K, params);
|
||||
const serverExpectedM1 = computeM1("alice", salt, A, B, K, params);
|
||||
console.log(constantTimeEqual(clientM1, serverExpectedM1));
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `computeM1(I, salt, A, B, K, params): Uint8Array`
|
||||
|
||||
Returns the client proof `H(H(N) XOR H(g) | H(I) | salt | A | B | K)`. Result length equals `params.hash.digestSize`.
|
||||
|
||||
### `computeM2(A, M1, K, params): Uint8Array`
|
||||
|
||||
Returns the server proof `H(A | M1 | K)`. Result length equals `params.hash.digestSize`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `H(N) XOR H(g)` term is a deliberate design choice in RFC 2945 — it commits both ends to the chosen group AND ties the proof to the hash function. An attacker who substituted a weakened group during transport would produce a different `H(N) XOR H(g)` value than the legitimate counterpart computed, causing the proof to fail.
|
||||
|
||||
`computeM1` deliberately accepts `A` and `B` as-is rather than re-padding them. RFC 2945's formula is over the un-padded public values, AND the values supplied to `computeM1` from the handshake functions are already produced by `bigIntToBytes(_, params.N.byteLength)` and so are already zero-padded to the modulus width. Double-padding would produce off-by-N-byte digests.
|
||||
58
docs/security-stack/security/srp/src/math/rng-bytes.md
Normal file
58
docs/security-stack/security/srp/src/math/rng-bytes.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# `rng-bytes`
|
||||
|
||||
## Purpose
|
||||
|
||||
Adapter helper that draws `n` bytes from any `Rng` implementing the `@suaveplan/types` contract. The `Rng` interface mandates `nextUint32` but `nextBytes` is optional; SRP's client/server start functions need a `≥ 256`-bit random exponent, so this helper unifies the two paths. When the supplied `Rng` exposes `nextBytes`, that path is used directly; otherwise the helper packs `nextUint32` calls into a byte buffer with little-endian field extraction. Both paths produce uniform output across the full byte range.
|
||||
|
||||
## Features
|
||||
|
||||
- Single entry point regardless of whether the `Rng` provides `nextBytes`.
|
||||
- Little-endian packing of `nextUint32` for portability across deterministic seeded RNGs.
|
||||
- No allocation overhead on the `nextBytes` fast path.
|
||||
- Pure function — no global state.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { rngBytes } from "@suaveplan/srp";
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
|
||||
const rng: Rng = {
|
||||
nextUint32: () => 0x12345678,
|
||||
nextFloat: () => 0.5,
|
||||
};
|
||||
const bytes = rngBytes(rng, 32);
|
||||
console.log(bytes.byteLength); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { rngBytes } from "@suaveplan/srp";
|
||||
import type { Rng } from "@suaveplan/types";
|
||||
|
||||
// Web-Crypto-backed RNG with native nextBytes — the fast path.
|
||||
const cryptoRng: Rng = {
|
||||
nextUint32: () => {
|
||||
const buf = new Uint32Array(1);
|
||||
crypto.getRandomValues(buf);
|
||||
return buf[0];
|
||||
},
|
||||
nextFloat: () => crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32,
|
||||
nextBytes: (n: number) => crypto.getRandomValues(new Uint8Array(n)),
|
||||
};
|
||||
const a = rngBytes(cryptoRng, 32);
|
||||
console.log(a.byteLength);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `rngBytes(rng: Rng, n: number): Uint8Array`
|
||||
|
||||
Returns an `n`-byte `Uint8Array` of uniformly distributed bytes. When `rng.nextBytes` exists, delegates directly. Otherwise packs `nextUint32` outputs into the buffer in little-endian byte order.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The little-endian pack order on the `nextUint32` fallback is arbitrary — the resulting bytes feed straight into a `bigInt` decode that does NOT preserve endianness of the source 32-bit words, so any consistent order is correct. We pick little-endian because it matches the layout of `Uint32Array` views over `ArrayBuffer` on every modern platform, so an implementation that wants to optimize further can swap to a typed-array view without changing behaviour.
|
||||
|
||||
The helper is deliberately not memoized: each call draws fresh entropy. Callers that need a deterministic exponent for testing supply a seeded `Rng` instance whose `nextUint32` reproduces the same word stream; `rngBytes` then yields a deterministic byte sequence from that stream.
|
||||
61
docs/security-stack/security/srp/src/math/u.md
Normal file
61
docs/security-stack/security/srp/src/math/u.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# `u`
|
||||
|
||||
## Purpose
|
||||
|
||||
Compute the SRP-6a scrambling value `u = H(PAD(A) | PAD(B))` per RFC 5054 §2.5.4. `u` mixes the two ephemeral public values into a single nonce that binds both sides of the handshake into the shared-secret derivation. Without it, an active attacker who observes one handshake could replay parts of it; with it, every handshake's shared secret depends on a fresh pair of `(A, B)` values.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure function of `(A, B, params)`.
|
||||
- `PAD` both `A` and `B` to `byteLength(N)` before hashing, per RFC 5054 §2.5.3.
|
||||
- Returns the hash's native digest size.
|
||||
- Throws `SrpError(SRP.PUBLIC_KEY_INVALID)` on `u ≡ 0 mod N` — RFC 5054 §2.5.4 mandates abort because `u = 0` lets the client recover the shared secret without the password.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { computeU } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const A = new Uint8Array(256);
|
||||
A[255] = 1;
|
||||
const B = new Uint8Array(256);
|
||||
B[255] = 2;
|
||||
const u = computeU(A, B, params);
|
||||
console.log(u.byteLength); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { computeU } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
import { SRP_CODES, SrpError } from "@suaveplan/srp/errors";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
try {
|
||||
// Constructed inputs whose hash happens to be ≡ 0 mod N is vanishingly
|
||||
// unlikely with a real hash, but the gate fires deterministically when
|
||||
// it does.
|
||||
computeU(new Uint8Array(256), new Uint8Array(256), params);
|
||||
} catch (err) {
|
||||
if (err instanceof SrpError && err.code === SRP_CODES.PUBLIC_KEY_INVALID) {
|
||||
console.log("u ≡ 0; aborted per RFC 5054 §2.5.4");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `computeU(A: Uint8Array, B: Uint8Array, params: SrpParams): Uint8Array`
|
||||
|
||||
Returns `H(PAD(A) | PAD(B))`. Throws `SrpError(SRP.PUBLIC_KEY_INVALID)` when the decoded `u mod N` is zero.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `u ≡ 0` check is performed AFTER the hash so consumers cannot bypass it by crafting `A` or `B` whose untreated values would produce zero — every input that decodes to zero goes through the same path. The probability of `H(PAD(A) | PAD(B)) ≡ 0 mod N` is approximately `2^-256` for SHA-256; the gate exists for protocol correctness rather than for any expected runtime hit.
|
||||
|
||||
`PAD` is applied to both inputs before concatenation because hashing raw `A` and `B` (potentially leading-zero-stripped from `bigIntToBytes` representations) would let an attacker malleate either value's representation without changing its semantic content. RFC 5054 §2.5.3 mandates the pad for exactly this reason.
|
||||
53
docs/security-stack/security/srp/src/math/x.md
Normal file
53
docs/security-stack/security/srp/src/math/x.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# `x`
|
||||
|
||||
## Purpose
|
||||
|
||||
Compute the SRP-6a private exponent `x = H(salt | H(I | ":" | P))` per RFC 5054 §2.6. `x` ties username + password + salt to a deterministic exponent in `Z_N`, producing the verifier `v = g^x mod N` that the server stores. Both `I` (username) and `P` (password) are encoded as UTF-8 bytes before hashing. The colon separator is fixed by RFC 5054 and is consumed verbatim — consumers MUST NOT pre-quote or escape colons inside `I` or `P`; that is the consumer's responsibility upstream.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure function of `(I, P, salt, params)`.
|
||||
- UTF-8 encoding of `I` and `P` so non-ASCII usernames work.
|
||||
- Two-stage hash: inner `H(I | ":" | P)` then outer `H(salt | innerHash)`.
|
||||
- Deterministic — same inputs always produce the same `x`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { computeX } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const salt = new Uint8Array(16);
|
||||
const x = computeX("alice@example.com", "correct horse battery staple", salt, params);
|
||||
console.log(x.byteLength); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { computeX } from "@suaveplan/srp";
|
||||
import { params2048Sha256 } from "@suaveplan/srp/params";
|
||||
import { nobleHashBridge } from "@suaveplan/srp/hash";
|
||||
|
||||
const params = params2048Sha256(nobleHashBridge("SHA256"));
|
||||
const salt = new Uint8Array([1, 2, 3, 4]);
|
||||
|
||||
// Determinism — both calls return byte-identical x for identical inputs.
|
||||
const x1 = computeX("alice", "secret", salt, params);
|
||||
const x2 = computeX("alice", "secret", salt, params);
|
||||
console.log(x1.toString() === x2.toString());
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `computeX(I: string, P: string, salt: Uint8Array, params: SrpParams): Uint8Array`
|
||||
|
||||
Returns `H(salt | H(I | ":" | P))`. Result length equals `params.hash.digestSize`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The colon separator in `I | ":" | P` is part of the SRP-6a definition; without it, an attacker could pivot a credential with username `"ab"` + password `"cd"` against a credential with username `"abc"` + password `"d"` (same `I | P` byte stream). RFC 5054 §2.6 fixes the separator to `0x3a` (`":"`); we encode it as UTF-8 once at module load and reuse the cached buffer across calls.
|
||||
|
||||
`x` is only ever derived on the client side — the server stores the resulting verifier `v = g^x mod N` and never sees `x` directly. The function therefore lives in `math/` rather than `client/` because it is a primitive used by both `computeVerifier` and `clientFinishHandshake`; co-locating it under either of those modules would obscure the dependency relationship.
|
||||
Reference in New Issue
Block a user