first commit
This commit is contained in:
133
docs/security-stack/crypto/crypto-vault/src/srp/srp-client.md
Normal file
133
docs/security-stack/crypto/crypto-vault/src/srp/srp-client.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# SRP-6a client
|
||||
|
||||
## Purpose
|
||||
|
||||
`srp-client.ts` implements the client side of the SRP-6a zero-knowledge
|
||||
password-authenticated key exchange per RFC 5054, using SHA-256 as the
|
||||
one-way hash and the 2048-bit safe-prime group `N`/`g` from Appendix A.
|
||||
The class hands the caller everything needed for an interoperable
|
||||
handshake against any conformant server: salt generation, private key
|
||||
derivation `x = H(s, H(I:p))`, verifier `v = g^x mod N`, ephemeral
|
||||
keypair `(a, A = g^a mod N)`, session key derivation, and constant-time
|
||||
server-proof verification.
|
||||
|
||||
## Features
|
||||
|
||||
- Salt generation via `generateSalt(length?)` honouring an injected RNG.
|
||||
- Private key, verifier, and ephemeral keypair primitives — pure,
|
||||
deterministic given inputs (or RNG).
|
||||
- `deriveSessionKey(...)` produces `K = SHA-256(S)` and the client proof
|
||||
`M1 = H(A || B || K)` per the RFC.
|
||||
- Tamper-resistant `B` validation: `B mod N == 0` aborts with
|
||||
`SRP.INVALID_B`. The `u = H(A || B) == 0` case is excluded via the
|
||||
collision-resistance of SHA-256 and is documented in code rather than
|
||||
re-checked at runtime.
|
||||
- `verifyServerProof` is constant-time (`constantTimeEqual`) and throws on
|
||||
mismatch with `CRYPTOVAULT.SRP.AUTHENTICATION_FAILED`.
|
||||
- Reject groups with `N` shorter than 2048 bits at construction time.
|
||||
- OTel histograms for handshake duration and counters for success / failure.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SrpClient } from "@suaveplan/crypto-vault/srp/srp-client";
|
||||
|
||||
const client = new SrpClient();
|
||||
const salt = client.generateSalt();
|
||||
const privateKey = client.derivePrivateKey(
|
||||
"alice@example.com",
|
||||
"correct horse battery staple",
|
||||
salt
|
||||
);
|
||||
const verifier = client.deriveVerifier(privateKey);
|
||||
console.log(verifier.length); // 256
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SrpClient } from "@suaveplan/crypto-vault/srp/srp-client";
|
||||
import {
|
||||
SRP_G,
|
||||
SRP_N_2048,
|
||||
} from "@suaveplan/crypto-vault/srp/srp-group";
|
||||
import { pow } from "@noble/curves/abstract/modular";
|
||||
|
||||
// Symmetric handshake fixture: same client + a hand-rolled server share.
|
||||
const client = new SrpClient();
|
||||
const salt = client.generateSalt();
|
||||
const x = client.derivePrivateKey("alice", "pw", salt);
|
||||
const v = BigInt("0x" + Buffer.from(client.deriveVerifier(x)).toString("hex"));
|
||||
|
||||
// Server side computes B = (k*v + g^b) mod N (hand-rolled for the example).
|
||||
const k = 3n; // simplified — production: H(N||g)
|
||||
const b = 7n;
|
||||
const B = ((k * v) + pow(SRP_G, b, SRP_N_2048)) % SRP_N_2048;
|
||||
|
||||
// Client side derives session key and proof.
|
||||
const { a, A } = client.generateEphemeralKeypair();
|
||||
const result = client.deriveSessionKey(a, A, B, x);
|
||||
console.log(result.K.length); // 32
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class SrpClient`
|
||||
|
||||
#### `new SrpClient(options?)`
|
||||
|
||||
`SrpClientOptions` accepts:
|
||||
|
||||
- `clock?: () => number` — defaults to `Date.now`.
|
||||
- `rng?: Rng` — overrides the default CSPRNG for salt and ephemeral
|
||||
generation.
|
||||
- `N?: bigint`, `g?: bigint` — group overrides for tests. Construction
|
||||
rejects any `N` shorter than 2048 bits with `SRP.WEAK_PARAMETERS`.
|
||||
|
||||
#### `generateSalt(length = 16): Uint8Array`
|
||||
|
||||
Random salt. Honours `rng?.nextBytes` if provided; otherwise uses
|
||||
`crypto.getRandomValues` via `@suaveplan/crypto-utils`.
|
||||
|
||||
#### `derivePrivateKey(identity, password, salt): Uint8Array`
|
||||
|
||||
Computes `x = H(salt, H(identity:password))`. Throws
|
||||
`SRP.INVALID_PRIVATE_KEY` for empty inputs.
|
||||
|
||||
#### `deriveVerifier(privateKey): Uint8Array`
|
||||
|
||||
Returns a 256-byte big-endian `v = g^x mod N`.
|
||||
|
||||
#### `generateEphemeralKeypair(): EphemeralKeypair`
|
||||
|
||||
`{ a: bigint; A: bigint }` — `a` is the private exponent (kept by the
|
||||
client), `A` the public form sent to the server. Reseeds when the entropy
|
||||
collapses to zero.
|
||||
|
||||
#### `deriveSessionKey(a, A, B, privateKey): SessionKeyResult`
|
||||
|
||||
Returns `{ K, S, M1 }` per the RFC. Throws `SRP.INVALID_B` when `B mod N`
|
||||
is zero.
|
||||
|
||||
#### `verifyServerProof(A, M1, K, serverProof): boolean`
|
||||
|
||||
Constant-time compare with the recomputed `M2 = H(A || M1 || K)`. Throws
|
||||
`SRP.AUTHENTICATION_FAILED` on mismatch; otherwise returns `true`.
|
||||
|
||||
#### `computeServerProof(A, M1, K): Uint8Array`
|
||||
|
||||
Helper exposed for symmetric tests and server-side adapters.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The implementation deliberately keeps every state transition pure —
|
||||
callers thread `(a, A, B, x)` through the methods rather than relying
|
||||
on instance fields.
|
||||
- All `bigint` ↔ byte conversions are big-endian and zero-padded to the
|
||||
group byte length so that hashing is deterministic regardless of the
|
||||
numeric value.
|
||||
- `k = H(N || g)` and `u = H(A || B)` use SHA-256 with both inputs padded
|
||||
to 256 bytes per RFC 5054 §2.5.3 / §2.6.
|
||||
- Constant-time comparison protects against timing-based proof guessing,
|
||||
matching the broader Suaveplan posture of using `constantTimeEqual` for
|
||||
every MAC / proof check.
|
||||
71
docs/security-stack/crypto/crypto-vault/src/srp/srp-group.md
Normal file
71
docs/security-stack/crypto/crypto-vault/src/srp/srp-group.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# SRP-6a 2048-bit group
|
||||
|
||||
## Purpose
|
||||
|
||||
`srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054
|
||||
Appendix A as `bigint` constants. The SRP-6a client uses this group to
|
||||
exchange ephemeral keys, derive the shared secret, and produce mutual
|
||||
proofs. The constants are pinned because they are part of the protocol
|
||||
contract — switching groups silently would break interoperability with
|
||||
every server using the same RFC.
|
||||
|
||||
## Features
|
||||
|
||||
- `SRP_N_2048` — the 2048-bit safe prime `N` as a positive `bigint`.
|
||||
- `SRP_G` — the generator `g = 2`.
|
||||
- `SRP_N_BITLEN = 2048`, `SRP_N_BYTELEN = 256` — convenience constants for
|
||||
byte-padding and validation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SRP_G, SRP_N_2048 } from "@suaveplan/crypto-vault/srp/srp-group";
|
||||
console.log(SRP_N_2048.toString(2).length); // 2048
|
||||
console.log(SRP_G); // 2n
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SRP_G,
|
||||
SRP_N_2048,
|
||||
SRP_N_BITLEN,
|
||||
SRP_N_BYTELEN,
|
||||
} from "@suaveplan/crypto-vault/srp/srp-group";
|
||||
import { pow } from "@noble/curves/abstract/modular";
|
||||
|
||||
// Compute v = g^x mod N for an arbitrary private key x.
|
||||
function makeVerifier(x: bigint): bigint {
|
||||
return pow(SRP_G, x, SRP_N_2048);
|
||||
}
|
||||
|
||||
console.log(SRP_N_BITLEN); // 2048
|
||||
console.log(SRP_N_BYTELEN); // 256
|
||||
console.log(typeof makeVerifier(123n)); // bigint
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SRP_N_2048: bigint`
|
||||
|
||||
The exact 2048-bit safe prime defined in RFC 5054 Appendix A, parsed from
|
||||
the canonical hex constant.
|
||||
|
||||
### `SRP_G: bigint`
|
||||
|
||||
The generator: `2n`.
|
||||
|
||||
### `SRP_N_BITLEN: number`, `SRP_N_BYTELEN: number`
|
||||
|
||||
Bit length (`2048`) and byte length (`256`) of the prime.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The hex string is laid out as a TypeScript string concatenation matching
|
||||
the line breaks in the RFC for diffability against the source.
|
||||
- The constant is parsed once at module load; subsequent imports share the
|
||||
resulting `bigint`.
|
||||
- The 1024-bit and 1536-bit groups from the same RFC are intentionally
|
||||
omitted. RFC 5054 §1 states they should be deprecated for new
|
||||
deployments, and the proposal pins us to ≥ 2048 bits.
|
||||
Reference in New Issue
Block a user