@zkp-auth/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,274 @@
1
+ // src/rng.ts
2
+ import { randomBytes } from "crypto";
3
+
4
+ // src/errors.ts
5
+ var InvalidInputError = class extends Error {
6
+ /** Class name; fixed for all instances. */
7
+ name = "InvalidInputError";
8
+ /** Stable, machine-readable identifier for the failing input. */
9
+ code;
10
+ /**
11
+ * @param code Stable identifier for the failing input.
12
+ * @param message Human-readable description; not part of the stable API.
13
+ */
14
+ constructor(code, message) {
15
+ super(message);
16
+ this.code = code;
17
+ }
18
+ };
19
+ var RandomnessError = class extends Error {
20
+ /** Class name; fixed for all instances. */
21
+ name = "RandomnessError";
22
+ /** Stable, machine-readable identifier; fixed for this class. */
23
+ code = "RNG_FAILURE";
24
+ /**
25
+ * @param message Human-readable description; not part of the stable API.
26
+ * @param options Optional bag carrying the underlying `cause`.
27
+ */
28
+ constructor(message, options) {
29
+ super(message);
30
+ if (options?.cause !== void 0) {
31
+ this.cause = options.cause;
32
+ }
33
+ }
34
+ };
35
+ var CryptoError = class extends Error {
36
+ /** Class name; fixed for all instances. */
37
+ name = "CryptoError";
38
+ /** Stable, machine-readable identifier; fixed for this class. */
39
+ code = "CURVE_ERROR";
40
+ /**
41
+ * @param message Human-readable description; not part of the stable API.
42
+ * @param options Optional bag carrying the underlying `cause`.
43
+ */
44
+ constructor(message, options) {
45
+ super(message);
46
+ if (options?.cause !== void 0) {
47
+ this.cause = options.cause;
48
+ }
49
+ }
50
+ };
51
+
52
+ // src/rng.ts
53
+ function randomBytes32() {
54
+ let buf;
55
+ try {
56
+ buf = randomBytes(32);
57
+ } catch (e) {
58
+ throw new RandomnessError("CSPRNG failure", { cause: e });
59
+ }
60
+ if (buf.length !== 32) {
61
+ throw new RandomnessError("CSPRNG returned short read");
62
+ }
63
+ return Uint8Array.from(buf);
64
+ }
65
+
66
+ // src/encoding.ts
67
+ import { ed25519 } from "@noble/curves/ed25519.js";
68
+ import {
69
+ bytesToNumberLE,
70
+ numberToBytesLE,
71
+ concatBytes
72
+ } from "@noble/curves/utils.js";
73
+ var L = ed25519.Point.Fn.ORDER;
74
+ var BASE = ed25519.Point.BASE;
75
+ function reduceScalar(n) {
76
+ return ed25519.Point.Fn.create(n);
77
+ }
78
+ function scalarFromBytesLE(bytes) {
79
+ return bytesToNumberLE(bytes);
80
+ }
81
+ function scalarToBytesLE(n) {
82
+ return numberToBytesLE(n, 32);
83
+ }
84
+ function pointFromBytesStrict(bytes) {
85
+ try {
86
+ return ed25519.Point.fromBytes(bytes);
87
+ } catch (e) {
88
+ throw new CryptoError("point decoding failed", { cause: e });
89
+ }
90
+ }
91
+ function pointFromBytesSoft(bytes) {
92
+ try {
93
+ return ed25519.Point.fromBytes(bytes);
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ function pointToBytes(p) {
99
+ return p.toBytes();
100
+ }
101
+
102
+ // src/keypair.ts
103
+ var MAX_REJECTION_ITERATIONS = 256;
104
+ function generateKeyPair() {
105
+ for (let i = 0; i < MAX_REJECTION_ITERATIONS; i += 1) {
106
+ let candidate;
107
+ try {
108
+ candidate = randomBytes32();
109
+ } catch (e) {
110
+ if (e instanceof RandomnessError) throw e;
111
+ throw new RandomnessError("CSPRNG failure", { cause: e });
112
+ }
113
+ const n = scalarFromBytesLE(candidate);
114
+ if (n >= 1n && n < L) {
115
+ const publicKey = pointToBytes(BASE.multiply(n));
116
+ return { privateKey: candidate, publicKey };
117
+ }
118
+ }
119
+ throw new RandomnessError("rejection sampling exhausted");
120
+ }
121
+
122
+ // src/validate.ts
123
+ function describeKind(value) {
124
+ if (value === null) return "null";
125
+ if (Array.isArray(value)) return "array";
126
+ return typeof value;
127
+ }
128
+ function assertUint8Array(value, code, paramName) {
129
+ if (!(value instanceof Uint8Array)) {
130
+ throw new InvalidInputError(
131
+ code,
132
+ `${paramName} must be a Uint8Array (received ${describeKind(value)})`
133
+ );
134
+ }
135
+ }
136
+ function assertUint8ArrayLength(value, expectedLen, code, paramName) {
137
+ assertUint8Array(value, code, paramName);
138
+ if (value.length !== expectedLen) {
139
+ throw new InvalidInputError(
140
+ code,
141
+ `${paramName} must be a Uint8Array of length ${expectedLen} (received length ${value.length})`
142
+ );
143
+ }
144
+ }
145
+ function assertUint8ArrayLengthBetween(value, minLen, maxLen, code, paramName) {
146
+ assertUint8Array(value, code, paramName);
147
+ if (value.length < minLen || value.length > maxLen) {
148
+ throw new InvalidInputError(
149
+ code,
150
+ `${paramName} must be a Uint8Array of length between ${minLen} and ${maxLen} inclusive (received length ${value.length})`
151
+ );
152
+ }
153
+ }
154
+
155
+ // src/challenge.ts
156
+ function generateChallenge(sessionId) {
157
+ assertUint8ArrayLengthBetween(sessionId, 1, 256, "INVALID_SESSION_ID", "sessionId");
158
+ let result;
159
+ try {
160
+ result = randomBytes32();
161
+ } catch (e) {
162
+ if (e instanceof RandomnessError) throw e;
163
+ throw new RandomnessError("CSPRNG failure", { cause: e });
164
+ }
165
+ if (result.length !== 32) {
166
+ throw new RandomnessError("CSPRNG returned short read");
167
+ }
168
+ return result;
169
+ }
170
+
171
+ // src/transcript.ts
172
+ import { sha512 } from "@noble/hashes/sha512.js";
173
+ function computeFiatShamirScalar(R_bytes, publicKey_bytes, challenge_bytes) {
174
+ const input = concatBytes(R_bytes, publicKey_bytes, challenge_bytes);
175
+ const digest = sha512(input);
176
+ const c_unreduced = scalarFromBytesLE(digest);
177
+ return reduceScalar(c_unreduced);
178
+ }
179
+
180
+ // src/compute-proof.ts
181
+ var MAX_REJECTION_ITERATIONS2 = 256;
182
+ function computeProofCore(r_bytes, r, x, publicKey_bytes, challenge) {
183
+ const R = BASE.multiply(r);
184
+ const R_bytes = pointToBytes(R);
185
+ const c = computeFiatShamirScalar(R_bytes, publicKey_bytes, challenge);
186
+ const s = reduceScalar(r + c * x);
187
+ const s_bytes = scalarToBytesLE(s);
188
+ r_bytes.fill(0);
189
+ return concatBytes(R_bytes, s_bytes);
190
+ }
191
+ function computeProof(privateKey, password, challenge) {
192
+ assertUint8ArrayLength(privateKey, 32, "INVALID_PRIVATE_KEY", "privateKey");
193
+ assertUint8Array(password, "INVALID_PASSWORD", "password");
194
+ assertUint8ArrayLengthBetween(password, 0, 4096, "INVALID_PASSWORD", "password");
195
+ assertUint8ArrayLength(challenge, 32, "INVALID_CHALLENGE", "challenge");
196
+ const n_raw = scalarFromBytesLE(privateKey);
197
+ if (n_raw === 0n || n_raw >= L) {
198
+ throw new InvalidInputError(
199
+ "INVALID_PRIVATE_KEY",
200
+ "privateKey decodes to a scalar outside [1, L)"
201
+ );
202
+ }
203
+ const x = n_raw;
204
+ const publicKey_bytes = pointToBytes(BASE.multiply(x));
205
+ for (let i = 0; i < MAX_REJECTION_ITERATIONS2; i += 1) {
206
+ let r_bytes;
207
+ try {
208
+ r_bytes = randomBytes32();
209
+ } catch (e) {
210
+ if (e instanceof RandomnessError) throw e;
211
+ throw new RandomnessError("CSPRNG failure", { cause: e });
212
+ }
213
+ const r = reduceScalar(scalarFromBytesLE(r_bytes));
214
+ if (r !== 0n) {
215
+ return computeProofCore(r_bytes, r, x, publicKey_bytes, challenge);
216
+ }
217
+ }
218
+ throw new RandomnessError("rejection sampling exhausted");
219
+ }
220
+
221
+ // src/compare.ts
222
+ import { timingSafeEqual } from "crypto";
223
+ function timingSafeEqualBytes(a, b) {
224
+ if (a.length !== b.length) {
225
+ return false;
226
+ }
227
+ return timingSafeEqual(a, b);
228
+ }
229
+
230
+ // src/verify-proof.ts
231
+ function verifyProof(publicKey, challenge, proof) {
232
+ assertUint8ArrayLength(publicKey, 32, "INVALID_PUBLIC_KEY", "publicKey");
233
+ assertUint8ArrayLength(challenge, 32, "INVALID_CHALLENGE", "challenge");
234
+ assertUint8ArrayLength(proof, 64, "INVALID_PROOF", "proof");
235
+ let PK;
236
+ try {
237
+ PK = pointFromBytesStrict(publicKey);
238
+ } catch (e) {
239
+ throw new InvalidInputError(
240
+ "INVALID_PUBLIC_KEY",
241
+ `publicKey: failed to decode as Edwards point (${e.message})`
242
+ );
243
+ }
244
+ if (PK.is0()) {
245
+ throw new InvalidInputError(
246
+ "INVALID_PUBLIC_KEY",
247
+ "publicKey decodes to the identity point"
248
+ );
249
+ }
250
+ const R_bytes = proof.subarray(0, 32);
251
+ const s_bytes = proof.subarray(32, 64);
252
+ const R = pointFromBytesSoft(R_bytes);
253
+ if (R === null) {
254
+ return false;
255
+ }
256
+ const s = scalarFromBytesLE(s_bytes);
257
+ if (s >= L) {
258
+ return false;
259
+ }
260
+ const c = computeFiatShamirScalar(R_bytes, publicKey, challenge);
261
+ const lhs = BASE.multiply(s);
262
+ const rhs = R.add(PK.multiply(c));
263
+ return timingSafeEqualBytes(pointToBytes(lhs), pointToBytes(rhs));
264
+ }
265
+ export {
266
+ CryptoError,
267
+ InvalidInputError,
268
+ RandomnessError,
269
+ computeProof,
270
+ generateChallenge,
271
+ generateKeyPair,
272
+ verifyProof
273
+ };
274
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/rng.ts","../src/errors.ts","../src/encoding.ts","../src/keypair.ts","../src/validate.ts","../src/challenge.ts","../src/transcript.ts","../src/compute-proof.ts","../src/compare.ts","../src/verify-proof.ts"],"sourcesContent":["// @zkp-auth/core — single CSPRNG chokepoint\r\n//\r\n// This module is the SOLE call site of `crypto.randomBytes` in\r\n// `packages/zkp-auth-core/src/**/*.ts`. Every public function that needs\r\n// fresh entropy (`generateKeyPair`, `generateChallenge`, `computeProof`)\r\n// funnels through `randomBytes32` so that:\r\n//\r\n// 1. the CSPRNG-failure branch can be audited at a single import site,\r\n// 2. unit tests can mock entropy by mocking exactly this module,\r\n// 3. the audit guard in task 13.1 can verify by string-match that\r\n// `node:crypto.randomBytes` is imported in exactly one source file.\r\n//\r\n// The wrapper \"fails closed\" on any RNG anomaly: a throw inside Node's\r\n// CSPRNG, or a buffer of unexpected length, surfaces as `RandomnessError`\r\n// with stable code `'RNG_FAILURE'`. There is no partial-output path, no\r\n// zero-padded fallback, and no silent retry. Callers either receive 32\r\n// fresh bytes or an exception.\r\n//\r\n// Validates: Requirements 1.5, 2.4, 3.10, 6.1\r\n// See design.md → \"Components and Interfaces\" → \"rng.ts — RNG wrapper\".\r\n\r\nimport { randomBytes } from 'node:crypto';\r\n\r\nimport { RandomnessError } from './errors.js';\r\n\r\n/**\r\n * Returns 32 fresh bytes drawn from the OS CSPRNG.\r\n *\r\n * Internally calls `crypto.randomBytes(32)` from `node:crypto` (synchronous\r\n * overload). The result is normalized to a plain `Uint8Array` — Node's\r\n * `randomBytes` returns a `Buffer`, which IS a `Uint8Array` subclass, but\r\n * the public contract of this library narrows the type to `Uint8Array` so\r\n * downstream consumers do not need to depend on Node's `Buffer` API.\r\n *\r\n * The returned array is a fresh copy (via `Uint8Array.from`), detached\r\n * from Node's internal allocator memory. This matters for callers that\r\n * zero-fill the buffer after use (see `compute-proof.ts`, Requirement 6.4):\r\n * mutating the returned array MUST NOT affect any other observer of the\r\n * original buffer. `Uint8Array.from` is preferred over the equivalent\r\n * `new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength).slice()`\r\n * idiom because it is shorter and equally explicit about copying — the\r\n * single-buffer performance difference is irrelevant at 32 bytes per call.\r\n *\r\n * Failure modes — both surface as `RandomnessError` with code\r\n * `'RNG_FAILURE'`, never as a partial or zero-padded result:\r\n *\r\n * - The underlying `randomBytes` call throws (e.g. the OS entropy source\r\n * is unavailable). The original error is attached as `.cause`.\r\n * - The returned buffer has a length other than exactly 32 bytes. This\r\n * should never occur with the synchronous overload of `randomBytes`,\r\n * but Requirements 1.5, 2.4, and 3.10 explicitly contemplate the\r\n * \"fewer than 32 bytes\" case so we check defensively.\r\n *\r\n * @returns A fresh 32-byte `Uint8Array` of CSPRNG output.\r\n * @throws RandomnessError When the OS CSPRNG throws, or returns a buffer\r\n * whose length is not exactly 32 bytes.\r\n */\r\nexport function randomBytes32(): Uint8Array {\r\n let buf: Buffer;\r\n try {\r\n buf = randomBytes(32);\r\n } catch (e) {\r\n throw new RandomnessError('CSPRNG failure', { cause: e });\r\n }\r\n if (buf.length !== 32) {\r\n throw new RandomnessError('CSPRNG returned short read');\r\n }\r\n return Uint8Array.from(buf);\r\n}\r\n","// @zkp-auth/core — typed error classes\r\n//\r\n// This module is the single source of truth for the error taxonomy of\r\n// `@zkp-auth/core`. Every fault path in the library throws one of the three\r\n// classes declared here, each tagged with a stable `.code` from `ErrorCode`\r\n// so callers can pattern-match on `.code` instead of parsing messages.\r\n//\r\n// Validates: Requirements 7.1, 7.2, 7.4, 7.5\r\n// See design.md → \"Components and Interfaces\" → \"errors.ts — Typed error classes\".\r\n\r\n/**\r\n * Stable, machine-readable identifiers attached to every thrown error.\r\n *\r\n * Callers are expected to pattern-match on `.code` rather than inspect\r\n * `.message`, which is for human readers only. The set is closed: adding a\r\n * new code is a breaking change to the public API surface.\r\n *\r\n * - `INVALID_PRIVATE_KEY` — privateKey shape, length, or scalar range invalid.\r\n * - `INVALID_PUBLIC_KEY` — publicKey shape, length, decode, or identity-point.\r\n * - `INVALID_CHALLENGE` — challenge shape or length invalid.\r\n * - `INVALID_PROOF` — proof shape or length invalid (NOT verification failure).\r\n * - `INVALID_PASSWORD` — password shape or length invalid.\r\n * - `INVALID_SESSION_ID` — sessionId shape, empty, or oversize.\r\n * - `RNG_FAILURE` — CSPRNG threw, returned short, or rejection-sampling exhausted.\r\n * - `CURVE_ERROR` — `@noble/curves` raised an unexpected internal error.\r\n */\r\nexport type ErrorCode =\r\n | 'INVALID_PRIVATE_KEY'\r\n | 'INVALID_PUBLIC_KEY'\r\n | 'INVALID_CHALLENGE'\r\n | 'INVALID_PROOF'\r\n | 'INVALID_PASSWORD'\r\n | 'INVALID_SESSION_ID'\r\n | 'RNG_FAILURE'\r\n | 'CURVE_ERROR';\r\n\r\n/**\r\n * Thrown when a public function receives an input that fails shape, length,\r\n * encoding, or range validation. The accompanying `.code` indicates which\r\n * input was invalid.\r\n *\r\n * `.name` is set as a readonly class field so it cannot be silently shadowed\r\n * by user-land subclasses or by `Error`'s default `'Error'` value.\r\n *\r\n * @example\r\n * try {\r\n * computeProof(privateKey, password, challenge);\r\n * } catch (e) {\r\n * if (e instanceof InvalidInputError && e.code === 'INVALID_CHALLENGE') {\r\n * // handle challenge-shape failure\r\n * }\r\n * }\r\n */\r\nexport class InvalidInputError extends Error {\r\n /** Class name; fixed for all instances. */\r\n readonly name = 'InvalidInputError';\r\n /** Stable, machine-readable identifier for the failing input. */\r\n readonly code: ErrorCode;\r\n\r\n /**\r\n * @param code Stable identifier for the failing input.\r\n * @param message Human-readable description; not part of the stable API.\r\n */\r\n constructor(code: ErrorCode, message: string) {\r\n super(message);\r\n this.code = code;\r\n }\r\n}\r\n\r\n/**\r\n * Thrown when the underlying CSPRNG throws, returns a short read, or when\r\n * bounded rejection sampling exhausts its iteration cap (treated as an RNG\r\n * anomaly). The library MUST NOT emit a partial output on this failure path.\r\n *\r\n * `.code` is fixed to `'RNG_FAILURE'`. The optional `cause` carries the\r\n * underlying error (e.g. the `node:crypto.randomBytes` throw) for diagnostics.\r\n *\r\n * `cause` is attached via a structural cast so the assignment works under\r\n * any tsconfig `lib` selection that may or may not include\r\n * `lib.es2022.error.d.ts` (per design.md).\r\n */\r\nexport class RandomnessError extends Error {\r\n /** Class name; fixed for all instances. */\r\n readonly name = 'RandomnessError';\r\n /** Stable, machine-readable identifier; fixed for this class. */\r\n readonly code: ErrorCode = 'RNG_FAILURE';\r\n\r\n /**\r\n * @param message Human-readable description; not part of the stable API.\r\n * @param options Optional bag carrying the underlying `cause`.\r\n */\r\n constructor(message: string, options?: { cause?: unknown }) {\r\n super(message);\r\n if (options?.cause !== undefined) {\r\n (this as { cause?: unknown }).cause = options.cause;\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Thrown when an internal `@noble/curves` operation raises an unexpected\r\n * error (e.g. invalid point encoding) on a code path where the library's\r\n * contract is to throw rather than return `false`. The verification path's\r\n * silent-`false` returns for malformed `R` and out-of-range `s` are\r\n * deliberate exceptions to this rule (see design.md, Requirements 4.7, 4.8).\r\n *\r\n * `.code` is fixed to `'CURVE_ERROR'`. The optional `cause` carries the\r\n * underlying noble error for diagnostics.\r\n */\r\nexport class CryptoError extends Error {\r\n /** Class name; fixed for all instances. */\r\n readonly name = 'CryptoError';\r\n /** Stable, machine-readable identifier; fixed for this class. */\r\n readonly code: ErrorCode = 'CURVE_ERROR';\r\n\r\n /**\r\n * @param message Human-readable description; not part of the stable API.\r\n * @param options Optional bag carrying the underlying `cause`.\r\n */\r\n constructor(message: string, options?: { cause?: unknown }) {\r\n super(message);\r\n if (options?.cause !== undefined) {\r\n (this as { cause?: unknown }).cause = options.cause;\r\n }\r\n }\r\n}\r\n","// @zkp-auth/core — scalar and point encoding wrappers\r\n//\r\n// This module is the SOLE call site of `@noble/curves`'s scalar and point\r\n// encoding primitives in `packages/zkp-auth-core/src/**/*.ts`. Every\r\n// downstream caller — `keypair.ts`, `compute-proof.ts`, `verify-proof.ts`,\r\n// `transcript.ts` — funnels through these wrappers so that:\r\n//\r\n// 1. the group order `L` is read at module load from\r\n// `ed25519.Point.Fn.ORDER` and is NEVER hardcoded as a literal — a\r\n// future `@noble/curves` bump that adjusts the internal field\r\n// representation continues to work without code change;\r\n// 2. any error raised by `@noble/curves` decoding is re-thrown as\r\n// `CryptoError` with a stable `.code` and the original error\r\n// attached as `.cause`, keeping the public error taxonomy closed\r\n// (Requirement 7.5);\r\n// 3. the verification path can ask for a \"soft\" parse that returns\r\n// `null` rather than throwing — required by Requirement 4.7's\r\n// contract that a malformed `R` MUST cause `verifyProof` to return\r\n// `false` silently rather than emit an oracle via a thrown error.\r\n//\r\n// `concatBytes` is re-exported from this module so that `transcript.ts`\r\n// and `compute-proof.ts` can `import { concatBytes } from './encoding.js'`\r\n// rather than introducing another `@noble/curves` import site. This keeps\r\n// the audit surface tight: only this file and `transcript.ts` (for\r\n// `sha512` from `@noble/hashes`) need to import directly from the noble\r\n// libraries.\r\n//\r\n// Validates: Requirements 7.5, 8.4 (curve-math source of truth)\r\n// See design.md → \"Components and Interfaces\" → \"encoding.ts\" and\r\n// \"External API Surface §B–§C\".\r\n\r\nimport { ed25519 } from '@noble/curves/ed25519.js';\r\nimport {\r\n bytesToNumberLE,\r\n numberToBytesLE,\r\n concatBytes,\r\n} from '@noble/curves/utils.js';\r\n\r\nimport { CryptoError } from './errors.js';\r\n\r\n/**\r\n * The runtime shape of a decoded Ed25519 point, as returned by\r\n * `ed25519.Point.fromBytes`. Exposed so callers can type their locals\r\n * against the same shape without re-deriving it from `@noble/curves`.\r\n */\r\nexport type EdwardsPoint = ReturnType<typeof ed25519.Point.fromBytes>;\r\n\r\n/**\r\n * The Ed25519 group order\r\n * `L = 2^252 + 27742317777372353535851937790883648493`.\r\n *\r\n * Read at module load from `ed25519.Point.Fn.ORDER`. The literal value\r\n * is intentionally NOT hardcoded anywhere in this codebase — `L` is the\r\n * single source of truth for scalar-range checks\r\n * (`keypair.ts` rejection sampling, `compute-proof.ts` Requirement 3.5,\r\n * `verify-proof.ts` Requirement 4.8) and for modular reduction\r\n * (see {@link reduceScalar}). Anchoring on\r\n * `@noble/curves` honors the \"curve math source of truth\" rule\r\n * (Requirement 8.4; design \"External API Surface §B\").\r\n */\r\nexport const L: bigint = ed25519.Point.Fn.ORDER;\r\n\r\n/**\r\n * The Ed25519 base point (generator) `G`, as defined in RFC 8032 §5.1.\r\n *\r\n * Used by every scalar multiplication in the library:\r\n *\r\n * - `keypair.ts` computes `publicKey = pointToBytes(BASE.multiply(x))`;\r\n * - `compute-proof.ts` computes `R = BASE.multiply(r)`;\r\n * - `verify-proof.ts` computes `lhs = BASE.multiply(s)` for the\r\n * verification equation `s · G == R + c · publicKey`.\r\n *\r\n * The exposed value is the noble `EdwardsPoint` instance, so callers\r\n * can invoke the constant-time `multiply` method on it. `multiplyUnsafe`\r\n * is forbidden anywhere a secret scalar is involved (design \"Key design\r\n * decisions → 4\").\r\n */\r\nexport const BASE: EdwardsPoint = ed25519.Point.BASE;\r\n\r\n/**\r\n * Reduces an integer modulo the Ed25519 group order `L`.\r\n *\r\n * Delegates to `ed25519.Point.Fn.create(n)`. The result is the canonical\r\n * representative in `[0, L)`. Callers use this for:\r\n *\r\n * - the Fiat-Shamir scalar `c = int_LE(SHA-512(...)) mod L` (see\r\n * `transcript.ts`),\r\n * - the response scalar `s = (r + c · x) mod L` (see `compute-proof.ts`).\r\n *\r\n * @param n A `bigint` of any sign or magnitude.\r\n * @returns `n mod L`, in `[0, L)`.\r\n */\r\nexport function reduceScalar(n: bigint): bigint {\r\n return ed25519.Point.Fn.create(n);\r\n}\r\n\r\n/**\r\n * Decodes a byte sequence as a non-negative little-endian `bigint`.\r\n *\r\n * Performs NO range reduction — the caller decides whether to reduce\r\n * `mod L`, or whether to reject values outside `[1, L)`. This split is\r\n * intentional: `keypair.ts` rejects via rejection sampling,\r\n * `compute-proof.ts` rejects on `>= L` per Requirement 3.5, and\r\n * `verify-proof.ts` returns `false` on `s >= L` per Requirement 4.8 —\r\n * three different policies that all share this single decode step.\r\n *\r\n * Length is not validated here; callers validate length up-front via\r\n * helpers from `validate.ts`.\r\n *\r\n * @param bytes A `Uint8Array` carrying a little-endian-encoded integer.\r\n * @returns The non-negative `bigint` decoding of `bytes`.\r\n */\r\nexport function scalarFromBytesLE(bytes: Uint8Array): bigint {\r\n return bytesToNumberLE(bytes);\r\n}\r\n\r\n/**\r\n * Encodes a non-negative `bigint` as exactly 32 little-endian bytes.\r\n *\r\n * Delegates to `numberToBytesLE(n, 32)`. Callers are responsible for\r\n * ensuring `n` fits in 32 bytes (i.e. `0 <= n < 2^256`); in this\r\n * library, the only callers feed in scalars already reduced mod `L`\r\n * via {@link reduceScalar}, so the constraint is satisfied by\r\n * construction.\r\n *\r\n * @param n A non-negative `bigint`, expected to lie in `[0, 2^256)`.\r\n * @returns A 32-byte `Uint8Array` carrying the little-endian encoding\r\n * of `n`.\r\n */\r\nexport function scalarToBytesLE(n: bigint): Uint8Array {\r\n return numberToBytesLE(n, 32);\r\n}\r\n\r\n/**\r\n * Decodes a 32-byte sequence into an Ed25519 point, throwing on failure.\r\n *\r\n * Used by `verify-proof.ts` for the registered `publicKey`: a public\r\n * key that fails to decode indicates either a misconfigured\r\n * registration or an integration error, and the contract is to surface\r\n * a typed error rather than a silent `false`. The caller in\r\n * `verify-proof.ts` re-wraps the `CryptoError` thrown here as the\r\n * more-specific `InvalidInputError('INVALID_PUBLIC_KEY', ...)` per\r\n * Requirement 4.5.\r\n *\r\n * Any error raised by `ed25519.Point.fromBytes` — invalid encoding,\r\n * non-canonical point, off-curve coordinates, etc. — is caught and\r\n * re-thrown as `new CryptoError('point decoding failed', { cause: e })`,\r\n * so the original noble error is preserved for diagnostics while the\r\n * exposed type stays inside the library's stable error taxonomy\r\n * (Requirement 7.5).\r\n *\r\n * @param bytes A 32-byte Ed25519 point encoding (RFC 8032 §5.1.2).\r\n * @returns The decoded `EdwardsPoint`.\r\n * @throws CryptoError When `ed25519.Point.fromBytes` rejects the encoding.\r\n */\r\nexport function pointFromBytesStrict(bytes: Uint8Array): EdwardsPoint {\r\n try {\r\n return ed25519.Point.fromBytes(bytes);\r\n } catch (e: unknown) {\r\n throw new CryptoError('point decoding failed', { cause: e });\r\n }\r\n}\r\n\r\n/**\r\n * Decodes a 32-byte sequence into an Ed25519 point, returning `null`\r\n * on failure instead of throwing.\r\n *\r\n * Used by `verify-proof.ts` exclusively for the `R` component of an\r\n * incoming `proof`. Per Requirement 4.7, a malformed `R` MUST cause\r\n * `verifyProof` to return `false` silently — throwing would expose an\r\n * oracle distinguishing \"malformed `R`\" from \"well-formed but invalid\r\n * proof\". Returning `null` lets the caller convert the failure into a\r\n * boolean return without ever materializing an exception.\r\n *\r\n * Any error raised by `ed25519.Point.fromBytes` is swallowed; the\r\n * underlying noble error is intentionally not surfaced here, as this\r\n * code path is the one place the library is designed to be silent.\r\n *\r\n * @param bytes A 32-byte sequence to decode.\r\n * @returns The decoded `EdwardsPoint`, or `null` if\r\n * `ed25519.Point.fromBytes` rejected the encoding for any reason.\r\n */\r\nexport function pointFromBytesSoft(bytes: Uint8Array): EdwardsPoint | null {\r\n try {\r\n return ed25519.Point.fromBytes(bytes);\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Encodes an Ed25519 point as 32 bytes per RFC 8032 §5.1.2.\r\n *\r\n * Delegates to the instance method `p.toBytes()`. The deprecated\r\n * `p.toRawBytes()` is intentionally NOT used (External API Surface §B).\r\n *\r\n * @param p An `EdwardsPoint` produced by curve math or by one of the\r\n * decoding helpers in this module.\r\n * @returns The 32-byte canonical encoding of `p`.\r\n */\r\nexport function pointToBytes(p: EdwardsPoint): Uint8Array {\r\n return p.toBytes();\r\n}\r\n\r\n/**\r\n * Concatenates an arbitrary number of `Uint8Array` segments into a single\r\n * `Uint8Array`. Re-exported from `@noble/curves/utils.js`.\r\n *\r\n * Surfaced here so that `transcript.ts` and `compute-proof.ts` can\r\n * import `concatBytes` from `./encoding.js` rather than adding another\r\n * direct `@noble/curves` import site. Keeping the noble import surface\r\n * narrow makes the audit guard in task 13.1 a single-file string-match.\r\n */\r\nexport { concatBytes };\r\n","// @zkp-auth/core — `generateKeyPair` with bounded rejection sampling\r\n//\r\n// This module implements the sole key-generation entry point of\r\n// `@zkp-auth/core`. It draws a 32-byte candidate from the CSPRNG\r\n// chokepoint (`./rng.js`), accepts it iff its little-endian decoding is\r\n// a scalar `n` with `1 <= n < L`, and emits the public key as the\r\n// canonical encoding of `n · G` where `G = BASE` is the Ed25519\r\n// generator. The acceptance test is implemented as bounded rejection\r\n// sampling — never as modular reduction — so the emitted `privateKey`\r\n// is uniform over `[1, L)` (Requirement 1.2), with no skew toward the\r\n// low end of the scalar range that a `mod L` strategy would introduce\r\n// because `2^256` is not a multiple of `L`.\r\n//\r\n// Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5, 11.2\r\n// See design.md → \"Components and Interfaces → keypair.ts\" and\r\n// design.md → \"Key design decisions → 2\" (rejection-sampling bound).\r\n//\r\n// Implementation notes:\r\n//\r\n// - The bound `MAX_REJECTION_ITERATIONS = 256` is the value locked by\r\n// the design. Under a healthy CSPRNG the probability that a single\r\n// 32-byte draw lies outside `[1, L)` is ≈ `1 - L / 2^256 ≈ 2^-252`,\r\n// so 256 successive rejections is statistically indistinguishable\r\n// from impossible — exhausting the loop is treated as an RNG anomaly\r\n// and surfaces as `RandomnessError` with stable code `'RNG_FAILURE'`,\r\n// matching the failure taxonomy of the CSPRNG wrapper itself\r\n// (Requirement 1.5).\r\n//\r\n// - We use the constant-time `BASE.multiply(n)` from `@noble/curves`,\r\n// never `multiplyUnsafe`. `multiplyUnsafe` skips the constant-time\r\n// ladder and would leak the secret scalar via timing — forbidden by\r\n// the project's audit rules and the design's \"Key design decisions →\r\n// 4\". `multiply` is invoked exactly once, on the accepted candidate.\r\n//\r\n// - The accepted `candidate` is returned AS-IS as `privateKey`. We do\r\n// NOT clamp (Ed25519 EdDSA-style bit clamping) and do NOT hash. This\r\n// is what distinguishes our key shape from `ed25519.keygen()`'s\r\n// RFC-8032 seed → expanded scalar pipeline; see \"External API\r\n// Surface §F\" in design.md. Downstream `computeProof` consumes the\r\n// raw 32-byte little-endian scalar, so any transform here would\r\n// break the round-trip property (Requirement 4 / Property 6).\r\n//\r\n// - The lower bound `n >= 1n` rules out the all-zero key explicitly.\r\n// `n = 0` would yield `publicKey = encode(0 · G) = encode(O)` (the\r\n// identity / neutral element), which is the privacy-critical\r\n// degenerate case Requirement 11.4 calls out: a zero key would make\r\n// the proof trivially verify against any challenge, breaking the\r\n// soundness contract of the scheme.\r\n//\r\n// - There are no `===` / `!==` comparisons or `Buffer.equals` calls on\r\n// secret byte arrays in this file. The accept test is performed in\r\n// bigint domain (`n >= 1n && n < L`), which does not invoke any\r\n// byte-level equality on `candidate` and is not a path the audit\r\n// guard (task 13.1) needs to flag.\r\n\r\nimport { randomBytes32 } from './rng.js';\r\nimport { scalarFromBytesLE, L, BASE, pointToBytes } from './encoding.js';\r\nimport { RandomnessError } from './errors.js';\r\n\r\n/**\r\n * Maximum number of rejection-sampling iterations before treating the\r\n * loop as an RNG anomaly. Locked at 256 by design.md \"Key design\r\n * decisions → 2\"; see the file-header comment above for the\r\n * statistical justification.\r\n */\r\nconst MAX_REJECTION_ITERATIONS = 256;\r\n\r\n/**\r\n * Generates a fresh `(privateKey, publicKey)` pair for the ZKP-auth\r\n * scheme.\r\n *\r\n * The `privateKey` is a uniform 32-byte little-endian encoding of a\r\n * scalar `n ∈ [1, L)`, drawn via bounded rejection sampling against\r\n * the CSPRNG chokepoint `randomBytes32()`. The `publicKey` is the\r\n * canonical 32-byte encoding of `n · G`, where `G` is the Ed25519\r\n * base point.\r\n *\r\n * The two outputs are returned as fresh `Uint8Array` instances —\r\n * `privateKey` is the accepted CSPRNG draw itself (a copy detached\r\n * from Node's internal buffer pool, see `rng.ts`'s `Uint8Array.from`\r\n * step), and `publicKey` is produced by `@noble/curves`'s `toBytes()`\r\n * which allocates a fresh array. Callers may zero-fill the returned\r\n * `privateKey` after use without affecting any other observer\r\n * (Requirement 6.4 hygiene; not enforced by this function but\r\n * permitted by its allocation contract).\r\n *\r\n * Failure modes — both surface as `RandomnessError` with\r\n * `code === 'RNG_FAILURE'`, never as a partial or zero-padded result\r\n * (Requirement 1.5):\r\n *\r\n * - The underlying `randomBytes32()` throws (CSPRNG anomaly or short\r\n * read). Any error — whether a `RandomnessError` already produced\r\n * by `rng.ts`, or a raw `Error` injected by tests via `vi.mock` —\r\n * is caught and, if not already a `RandomnessError`, re-wrapped as\r\n * one. This mirrors the defense-in-depth pattern in `compute-proof.ts`.\r\n * - 256 successive draws all decode to scalars outside `[1, L)`. This\r\n * is statistically impossible under a healthy CSPRNG (≈ `2^-252`\r\n * per draw), so exhaustion is reported as an RNG failure rather\r\n * than as a separate exhaustion-specific error class.\r\n *\r\n * @returns An object with `privateKey` (32 bytes, encoding a scalar\r\n * in `[1, L)`) and `publicKey` (32 bytes, encoding `privateKey · G`).\r\n * @throws RandomnessError When the CSPRNG fails or rejection sampling\r\n * exhausts its 256-iteration bound.\r\n */\r\nexport function generateKeyPair(): {\r\n privateKey: Uint8Array;\r\n publicKey: Uint8Array;\r\n} {\r\n for (let i = 0; i < MAX_REJECTION_ITERATIONS; i += 1) {\r\n // `randomBytes32()` normally throws `RandomnessError` (CSPRNG fault\r\n // or short read) — already wrapped by `rng.ts`. In tests, `rng.ts`\r\n // is mocked via `vi.mock` and may throw a raw `Error`. We re-wrap\r\n // any non-`RandomnessError` here so the public-API contract\r\n // \"throw only `InvalidInputError` or `RandomnessError`\" holds at\r\n // this module boundary regardless of what the mock injects.\r\n let candidate: Uint8Array;\r\n try {\r\n candidate = randomBytes32();\r\n } catch (e) {\r\n if (e instanceof RandomnessError) throw e;\r\n throw new RandomnessError('CSPRNG failure', { cause: e });\r\n }\r\n\r\n // Raw little-endian decoding, no reduction. We REJECT out-of-range\r\n // candidates (per Requirement 1.2's \"rejection sampling\") rather\r\n // than reduce mod `L`, because reduction would skew the\r\n // distribution of accepted scalars toward the low end of `[0, L)`\r\n // — `2^256` is not an integer multiple of `L`.\r\n const n = scalarFromBytesLE(candidate);\r\n\r\n if (n >= 1n && n < L) {\r\n // Accepted. Derive the public key with the constant-time\r\n // scalar-multiply. `BASE.multiply` (not `multiplyUnsafe`) is\r\n // mandatory: this is the single point in this function where\r\n // the secret scalar `n` enters curve math, and any timing\r\n // variation here would directly leak `n`.\r\n const publicKey = pointToBytes(BASE.multiply(n));\r\n return { privateKey: candidate, publicKey };\r\n }\r\n // Rejected: continue the loop and draw a fresh candidate. The\r\n // rejected `candidate` is left to the GC; we do not zero-fill\r\n // here because the rejected bytes are not a secret — they were\r\n // never accepted as a private key and the CSPRNG-state info they\r\n // carry is no more sensitive than any other discarded RNG output.\r\n }\r\n\r\n // Loop exhausted without acceptance. Treated as an RNG anomaly per\r\n // design.md \"Key design decisions → 2\"; surfaces with the same\r\n // stable `.code` (`'RNG_FAILURE'`) as a CSPRNG throw or short read,\r\n // so callers can pattern-match on a single error code for all\r\n // randomness-related failures.\r\n throw new RandomnessError('rejection sampling exhausted');\r\n}\r\n","// @zkp-auth/core — input shape validation helpers\r\n//\r\n// All four public functions validate inputs at their entry point. Validation\r\n// is the only place we compute on attacker-supplied data before any\r\n// constant-time considerations apply (length checks are not secret).\r\n//\r\n// Each helper is declared as an `asserts value is Uint8Array` function so the\r\n// TypeScript flow analysis in callers can rely on the narrowed type after\r\n// the call site without an additional cast.\r\n//\r\n// On failure each helper throws `InvalidInputError(code, message)`. Messages\r\n// are human-readable and name the offending `paramName` and the violated\r\n// constraint. Callers MUST pattern-match on `.code` rather than parse\r\n// messages (see Requirement 7.4).\r\n//\r\n// Validates: Requirements 7.1, 7.2, 7.4\r\n// See design.md → \"Components and Interfaces\" → \"validate.ts — Input shape validation\".\r\n//\r\n// Implementation note: we deliberately do NOT depend on `@noble/curves`'s\r\n// `abytes` / `ensureBytes` so that the thrown error class is always our\r\n// `InvalidInputError` with a stable `.code`, not noble's internal error\r\n// shape. (design.md, \"External API Surface §C\".)\r\n\r\nimport { InvalidInputError, type ErrorCode } from './errors.js';\r\n\r\n/**\r\n * Produces a short, human-readable description of a value's runtime shape,\r\n * suitable for inclusion in an error message. Never throws; never reveals\r\n * the value itself (only its kind, to keep secret-bearing inputs out of\r\n * thrown messages).\r\n *\r\n * @param value Arbitrary unknown value.\r\n * @returns A short kind label such as `\"null\"`, `\"undefined\"`, `\"string\"`,\r\n * `\"number\"`, `\"bigint\"`, `\"boolean\"`, `\"function\"`, `\"symbol\"`,\r\n * `\"array\"`, or `\"object\"`.\r\n */\r\nfunction describeKind(value: unknown): string {\r\n if (value === null) return 'null';\r\n if (Array.isArray(value)) return 'array';\r\n return typeof value;\r\n}\r\n\r\n/**\r\n * Asserts that `value` is a `Uint8Array` instance.\r\n *\r\n * `Buffer` extends `Uint8Array`, so callers may pass a `Buffer` and the\r\n * assertion succeeds — this is intentional. Plain objects with a numeric\r\n * `.length` property, typed arrays of other element types (e.g.\r\n * `Uint16Array`), `ArrayBuffer`s, regular arrays, `null`, and `undefined`\r\n * all fail the check.\r\n *\r\n * @param value Value of unknown shape to validate.\r\n * @param code Stable {@link ErrorCode} attached to the thrown error\r\n * so callers can pattern-match on `.code`.\r\n * @param paramName Name of the parameter being validated, included in the\r\n * human-readable message for diagnostics.\r\n * @throws InvalidInputError When `value` is not a `Uint8Array` instance.\r\n */\r\nexport function assertUint8Array(\r\n value: unknown,\r\n code: ErrorCode,\r\n paramName: string,\r\n): asserts value is Uint8Array {\r\n if (!(value instanceof Uint8Array)) {\r\n throw new InvalidInputError(\r\n code,\r\n `${paramName} must be a Uint8Array (received ${describeKind(value)})`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Asserts that `value` is a `Uint8Array` instance with exactly\r\n * `expectedLen` bytes.\r\n *\r\n * Performs the `Uint8Array` shape check first (delegating to\r\n * {@link assertUint8Array}) so callers always see the more-specific\r\n * shape error before any length error.\r\n *\r\n * @param value Value of unknown shape to validate.\r\n * @param expectedLen Required byte length (exact match).\r\n * @param code Stable {@link ErrorCode} attached to the thrown error\r\n * so callers can pattern-match on `.code`.\r\n * @param paramName Name of the parameter being validated, included in\r\n * the human-readable message for diagnostics.\r\n * @throws InvalidInputError When `value` is not a `Uint8Array`, or its\r\n * length differs from `expectedLen`.\r\n */\r\nexport function assertUint8ArrayLength(\r\n value: unknown,\r\n expectedLen: number,\r\n code: ErrorCode,\r\n paramName: string,\r\n): asserts value is Uint8Array {\r\n assertUint8Array(value, code, paramName);\r\n if (value.length !== expectedLen) {\r\n throw new InvalidInputError(\r\n code,\r\n `${paramName} must be a Uint8Array of length ${expectedLen} (received length ${value.length})`,\r\n );\r\n }\r\n}\r\n\r\n/**\r\n * Asserts that `value` is a `Uint8Array` instance whose length lies in\r\n * the inclusive range `[minLen, maxLen]`.\r\n *\r\n * Performs the `Uint8Array` shape check first (delegating to\r\n * {@link assertUint8Array}) so callers always see the more-specific\r\n * shape error before any length error. Both bounds are inclusive — a\r\n * length equal to `minLen` or `maxLen` is accepted.\r\n *\r\n * @param value Value of unknown shape to validate.\r\n * @param minLen Minimum allowed byte length, inclusive.\r\n * @param maxLen Maximum allowed byte length, inclusive.\r\n * @param code Stable {@link ErrorCode} attached to the thrown error\r\n * so callers can pattern-match on `.code`.\r\n * @param paramName Name of the parameter being validated, included in the\r\n * human-readable message for diagnostics.\r\n * @throws InvalidInputError When `value` is not a `Uint8Array`, or its\r\n * length falls outside the inclusive `[minLen, maxLen]` range.\r\n */\r\nexport function assertUint8ArrayLengthBetween(\r\n value: unknown,\r\n minLen: number,\r\n maxLen: number,\r\n code: ErrorCode,\r\n paramName: string,\r\n): asserts value is Uint8Array {\r\n assertUint8Array(value, code, paramName);\r\n if (value.length < minLen || value.length > maxLen) {\r\n throw new InvalidInputError(\r\n code,\r\n `${paramName} must be a Uint8Array of length between ${minLen} and ${maxLen} inclusive (received length ${value.length})`,\r\n );\r\n }\r\n}\r\n","// @zkp-auth/core — `generateChallenge` for verifier-chosen per-session nonces\r\n//\r\n// This module implements the sole challenge-generation entry point of\r\n// `@zkp-auth/core`. It validates the caller-supplied `sessionId` for\r\n// shape (Requirement 2.2) and then returns 32 fresh CSPRNG bytes drawn\r\n// from the chokepoint `randomBytes32()`. The returned challenge is\r\n// the verifier's contribution to the Schnorr-proof transcript; binding\r\n// each authentication attempt to a unique value is what makes proofs\r\n// non-replayable across sessions (Requirement 2 user-story).\r\n//\r\n// Validates: Requirements 2.1, 2.2, 2.3, 2.4, 2.5\r\n// See design.md → \"Components and Interfaces → challenge.ts\" and\r\n// requirements.md → \"Requirement 2: Challenge Generation\".\r\n//\r\n// SECURITY-CRITICAL CONTRACT — Requirement 2.5 / Property 4:\r\n//\r\n// The returned 32 bytes MUST NOT be a function of `sessionId`. After\r\n// the validation call, `sessionId` is never read, never hashed, never\r\n// mixed into the RNG state, and never folded into the output in any\r\n// way. The body is exactly two statements: validation, then a fresh\r\n// `randomBytes32()` call whose result is returned unchanged.\r\n//\r\n// The reason for this strict separation is the threat model in\r\n// requirements.md OQ-3: `sessionId` is the caller's server-side\r\n// bookkeeping handle (e.g. a database row id) and may be predictable\r\n// or adversary-influenced. Mixing it into the challenge would\r\n// downgrade the challenge's unpredictability to that of `sessionId`,\r\n// defeating the very property the verifier needs from this function.\r\n// The `sessionId → challenge` association is maintained by the\r\n// caller's protocol layer, not by this library.\r\n//\r\n// Implementation notes:\r\n//\r\n// - The 1..256-byte length window for `sessionId` (Requirement 2.2) is\r\n// wide enough to accept any reasonable session-handle encoding —\r\n// UUIDs (16 bytes), 256-bit hex strings (64 bytes), opaque tokens —\r\n// while rejecting empty inputs (which suggest a caller bug) and\r\n// absurdly large inputs (which suggest accidental payload-passing\r\n// or a DoS attempt against any caller-side bookkeeping). The bound\r\n// is enforced by `assertUint8ArrayLengthBetween` with both bounds\r\n// inclusive.\r\n//\r\n// - The validation step's `'INVALID_SESSION_ID'` error code is fixed\r\n// in the `ErrorCode` taxonomy (`./errors.ts`); callers pattern-match\r\n// on `.code` to distinguish session-id failures from any other\r\n// `InvalidInputError`. Any non-`Uint8Array` shape, zero-length, or\r\n// over-256-byte input flows through this single code.\r\n//\r\n// - The RNG-failure path propagates `RandomnessError` from\r\n// `randomBytes32()` unchanged when the underlying CSPRNG throws.\r\n// Because tests mock `rng.ts` directly via `vi.mock` and may inject\r\n// a raw `Error` (property-13's `generateChallenge` portion does this),\r\n// the call is wrapped in a try/catch that re-wraps any non-\r\n// `RandomnessError` as one. This is a defense-in-depth no-op in\r\n// production — `rng.ts` already wraps all faults at the chokepoint —\r\n// but ensures the public-API contract holds at this module's boundary\r\n// regardless of what the mock injects.\r\n// A post-call length check (`result.length !== 32`) defends against\r\n// the \"short-read\" mock case (`new Uint8Array(31)`) which bypasses\r\n// `rng.ts`'s own length guard. Both failure modes surface as\r\n// `RandomnessError` with stable code `'RNG_FAILURE'`, no partial\r\n// output (Requirement 2.4).\r\n//\r\n// - There are no byte-array equality comparisons in this file. The\r\n// audit guard (task 13.1) will see only the validation call and the\r\n// `randomBytes32()` return — no `===`, no `!==`, no `Buffer.equals`.\r\n\r\nimport { RandomnessError } from './errors.js';\r\nimport { assertUint8ArrayLengthBetween } from './validate.js';\r\nimport { randomBytes32 } from './rng.js';\r\n\r\n/**\r\n * Generates a fresh 32-byte challenge for a Schnorr-proof\r\n * authentication session.\r\n *\r\n * The returned bytes are drawn from the OS CSPRNG via the library's\r\n * single chokepoint (`randomBytes32`) and are statistically\r\n * independent of `sessionId` — the parameter exists only so the\r\n * caller's wire protocol can validate session-handle shape at a\r\n * single, well-defined entry point. See the file-header comment for\r\n * the security rationale (Requirement 2.5 / Property 4).\r\n *\r\n * The returned `Uint8Array` is a fresh allocation, detached from any\r\n * internal CSPRNG buffer pool (see `rng.ts`'s `Uint8Array.from` step),\r\n * so the caller may zero-fill it after use without affecting any\r\n * other observer.\r\n *\r\n * Failure modes:\r\n *\r\n * - `InvalidInputError` with `code === 'INVALID_SESSION_ID'` — thrown\r\n * when `sessionId` is not a `Uint8Array`, has length 0, or has\r\n * length greater than 256 bytes. The 1..256-byte inclusive window\r\n * is enforced by `assertUint8ArrayLengthBetween`.\r\n * - `RandomnessError` with `code === 'RNG_FAILURE'` — thrown when\r\n * `randomBytes32()` throws (any underlying CSPRNG fault), or when\r\n * the returned buffer is not exactly 32 bytes. In production,\r\n * `rng.ts` wraps both cases before they reach this function; the\r\n * extra length guard and try/catch here are defense-in-depth for\r\n * test-injected mocks (property-13, `vi.mock`). No partial or\r\n * zero-padded challenge is ever returned (Requirement 2.4).\r\n *\r\n * @param sessionId Caller-supplied session handle. Validated for\r\n * `Uint8Array` shape and a length in the inclusive range\r\n * `[1, 256]`; never read after validation.\r\n * @returns A fresh 32-byte CSPRNG-derived `Uint8Array`, independent\r\n * of `sessionId`.\r\n * @throws InvalidInputError When `sessionId` fails shape or length\r\n * validation.\r\n * @throws RandomnessError When the underlying CSPRNG throws or\r\n * returns a short read.\r\n */\r\nexport function generateChallenge(sessionId: Uint8Array): Uint8Array {\r\n assertUint8ArrayLengthBetween(sessionId, 1, 256, 'INVALID_SESSION_ID', 'sessionId');\r\n\r\n // Defense-in-depth: wrap the randomBytes32() call so that any error —\r\n // whether a `RandomnessError` already produced by `rng.ts`, or a raw\r\n // `Error` injected by a `vi.mock` in tests — surfaces as a\r\n // `RandomnessError`. In production this is a no-op; `rng.ts` wraps\r\n // every CSPRNG fault at the chokepoint. The short-read length check\r\n // covers the test case where the mock returns a 31-byte buffer that\r\n // bypasses `rng.ts`'s own length guard entirely (property-13,\r\n // generateChallenge portion, task 6.4).\r\n let result: Uint8Array;\r\n try {\r\n result = randomBytes32();\r\n } catch (e) {\r\n if (e instanceof RandomnessError) throw e;\r\n throw new RandomnessError('CSPRNG failure', { cause: e });\r\n }\r\n if (result.length !== 32) {\r\n throw new RandomnessError('CSPRNG returned short read');\r\n }\r\n return result;\r\n}\r\n","// @zkp-auth/core — Fiat-Shamir scalar derivation\r\n//\r\n// This module is the SOLE definition of the Fiat-Shamir hash construction\r\n// used by `@zkp-auth/core`. Both the prover (`compute-proof.ts`) and the\r\n// verifier (`verify-proof.ts`) call into the single function exported here\r\n// — `computeFiatShamirScalar` — so the construction can never drift between\r\n// the two sides. Any change to the transcript is a change in exactly one\r\n// file, gated by the audit guard described below.\r\n//\r\n// The construction is:\r\n//\r\n// c = int_LE(SHA-512(R_bytes || publicKey_bytes || challenge_bytes)) mod L\r\n//\r\n// where `||` is raw byte concatenation with NO domain separator, NO length\r\n// prefix, and NO padding (Requirement 8.2). The 64-byte SHA-512 digest is\r\n// interpreted as one little-endian integer in `[0, 2^512)` and reduced\r\n// modulo the Ed25519 group order `L`. The resulting scalar `c` is\r\n// statistically indistinguishable from uniform on `[0, L)` (the standard\r\n// \"wide-reduction\" argument used in EdDSA-family hash-to-scalar\r\n// constructions). We deliberately do NOT truncate the digest to 32 bytes\r\n// before reducing — design \"Components and Interfaces → transcript.ts\"\r\n// pins the wide-reduction form as the canonical construction so this file\r\n// can be cross-checked against an independent oracle bit-for-bit.\r\n//\r\n// `password` is intentionally absent from the signature, body, and\r\n// imports of this file (Requirements 3.3, 8.1, 11.1). Per Requirement 11\r\n// `password` is reserved-but-unused metadata in v1 and never participates\r\n// in scalar derivation; locking it out at the file level — rather than\r\n// at the call site — makes that contract impossible to violate by\r\n// accident.\r\n//\r\n// This file is the ONLY location under `packages/zkp-auth-core/src/**/*.ts`\r\n// that imports from `@noble/hashes`. The audit guard in task 13.1 enforces\r\n// that invariant by string-matching the import source. Every other module\r\n// that needs `concatBytes` re-imports it from `./encoding.js`, which\r\n// re-exports the symbol exactly so `transcript.ts` and `compute-proof.ts`\r\n// can pull it from a single in-package source.\r\n//\r\n// Validates: Requirements 3.3, 4.3, 8.1, 8.2, 8.3, 8.4, 11.1\r\n// See design.md → \"Components and Interfaces\" → \"transcript.ts\".\r\n\r\nimport { sha512 } from '@noble/hashes/sha512.js';\r\n\r\nimport { concatBytes, reduceScalar, scalarFromBytesLE } from './encoding.js';\r\n\r\n/**\r\n * Computes the Fiat-Shamir challenge scalar\r\n * `c = int_LE(SHA-512(R_bytes || publicKey_bytes || challenge_bytes)) mod L`.\r\n *\r\n * This is the single point in the codebase where the Fiat-Shamir\r\n * transcript is defined. `compute-proof.ts` calls it to bind the proof\r\n * to `(R, publicKey, challenge)`; `verify-proof.ts` calls it with the\r\n * verbatim same inputs to re-derive the same `c`. Because both sides\r\n * share this exact function, the construction cannot drift — any change\r\n * here changes both prover and verifier in lockstep.\r\n *\r\n * Algorithm (per design \"Components and Interfaces → transcript.ts\"):\r\n *\r\n * 1. `input = concatBytes(R_bytes, publicKey_bytes, challenge_bytes)`\r\n * — 96 bytes; raw concatenation with no separator, no length prefix,\r\n * no padding (Requirement 8.2).\r\n * 2. `digest = sha512(input)` — exactly 64 bytes (Requirement 8.4).\r\n * 3. `c_unreduced = scalarFromBytesLE(digest)` — interprets the full\r\n * 64 bytes as one little-endian integer in `[0, 2^512)`. We do NOT\r\n * truncate to 32 bytes before reducing; the wide-reduction form is\r\n * what makes `c` statistically indistinguishable from uniform on\r\n * `[0, L)`, and matches the canonical construction pinned in the\r\n * design document.\r\n * 4. `return reduceScalar(c_unreduced)` — canonical representative in\r\n * `[0, L)`.\r\n *\r\n * `password` MUST NOT participate in this computation: per Requirement\r\n * 11.1 it is opaque metadata in v1, and per Requirement 8.1 it is NOT\r\n * part of the Fiat-Shamir transcript. The function signature is closed\r\n * over only `(R_bytes, publicKey_bytes, challenge_bytes)` so that no\r\n * future refactor can quietly add `password` to the hash input.\r\n *\r\n * The function performs NO byte-array equality on its inputs; it is a\r\n * pure hash-and-reduce pipeline. Length validation is the caller's\r\n * responsibility — `compute-proof.ts` and `verify-proof.ts` validate\r\n * input shapes via helpers from `validate.ts` before reaching here.\r\n * Callers always pass exactly 32-byte segments.\r\n *\r\n * @param R_bytes The 32-byte commitment encoding `R = r · G`.\r\n * @param publicKey_bytes The 32-byte encoding of the prover's public key\r\n * `X = x · G`.\r\n * @param challenge_bytes The 32-byte verifier-chosen challenge.\r\n * @returns The Fiat-Shamir challenge scalar `c` as a `bigint` in\r\n * `[0, L)`.\r\n */\r\nexport function computeFiatShamirScalar(\r\n R_bytes: Uint8Array,\r\n publicKey_bytes: Uint8Array,\r\n challenge_bytes: Uint8Array,\r\n): bigint {\r\n const input = concatBytes(R_bytes, publicKey_bytes, challenge_bytes);\r\n const digest = sha512(input);\r\n const c_unreduced = scalarFromBytesLE(digest);\r\n return reduceScalar(c_unreduced);\r\n}\r\n","// @zkp-auth/core — Schnorr proof construction with Fiat-Shamir transform\r\n//\r\n// This module implements the sole proof-construction entry point of\r\n// `@zkp-auth/core`. Given a `privateKey`, a (reserved-but-unused)\r\n// `password`, and a verifier-chosen `challenge`, it produces the\r\n// 64-byte Schnorr proof `R || s` whose verification equation\r\n// `s · G == R + c · publicKey` is exercised on the verifier side\r\n// by `verify-proof.ts` (Requirement 4.3, round-trip Property 6).\r\n//\r\n// The construction follows the textbook non-interactive Schnorr\r\n// identification scheme with the Fiat-Shamir transform pinned in\r\n// `transcript.ts`:\r\n//\r\n// x = int_LE(privateKey) -- secret scalar\r\n// r ←$ [1, L) -- fresh CSPRNG nonce\r\n// R = r · G -- commitment\r\n// c = int_LE(SHA-512(R || X || challenge)) mod L\r\n// s = (r + c · x) mod L -- response\r\n// proof = R_bytes || s_bytes -- 64 bytes\r\n//\r\n// where `X = x · G` is the prover's public key and `||` is raw byte\r\n// concatenation. `password` is intentionally absent from every step\r\n// of this computation past the entry-level shape validation\r\n// (Requirements 3.3, 8.1, 11.1) — see \"SECURITY-CRITICAL CONTRACTS\"\r\n// below.\r\n//\r\n// Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8,\r\n// 3.10, 6.1, 6.2, 6.3, 6.4, 11.1, 11.4\r\n// See design.md → \"Components and Interfaces → compute-proof.ts\" and\r\n// design.md → \"Key design decisions → 2\" (rejection-sampling bound)\r\n// → 4 (constant-time `multiply`)\r\n// → \"Mocking strategy\" (the\r\n// `__forTesting__` hook) and\r\n// requirements.md → \"Requirement 3: Proof Computation\",\r\n// \"Requirement 6: Non-Functional — Fresh Nonces\",\r\n// \"Requirement 11: Password-to-Scalar Derivation\".\r\n//\r\n// SECURITY-CRITICAL CONTRACTS:\r\n//\r\n// 1. `password` is reserved-but-unused (Requirement 11.1, 11.3). After\r\n// the entry-level shape validation it is NEVER read. It is NOT\r\n// mixed into the scalar `x`, NOT folded into the Fiat-Shamir\r\n// transcript (the call to `computeFiatShamirScalar` does not even\r\n// accept a `password` parameter — see `transcript.ts`'s file-level\r\n// closure of the construction), and NOT touched in any computation.\r\n// Property 10 (test/property-10-password-no-op.test.ts) locks this\r\n// invariant: two distinct `password` values with the same fixed\r\n// nonce, `privateKey`, and `challenge` MUST produce byte-identical\r\n// proofs.\r\n//\r\n// 2. The constant-time scalar multiplication `BASE.multiply(scalar)`\r\n// is mandatory (design \"Key design decisions → 4\"). `multiplyUnsafe`\r\n// is FORBIDDEN anywhere a secret scalar is involved. Two call sites\r\n// feed secret scalars into curve math here: `BASE.multiply(x)` to\r\n// derive `publicKey_bytes` for the transcript, and `BASE.multiply(r)`\r\n// to derive the commitment `R`. Any timing variation in those\r\n// calls would directly leak `x` or `r` to a side-channel observer.\r\n//\r\n// 3. `r_bytes.fill(0)` is called inside the shared core helper after\r\n// the proof has been assembled, before returning (Requirement 6.4,\r\n// best-effort). This is memory hygiene only — JavaScript provides\r\n// no hard zeroization guarantee (GC may have relocated the buffer,\r\n// JIT may have spilled to a register), and the bigint `r` itself\r\n// cannot be wiped from the JS runtime. Property 15\r\n// (test/property-15-nonce-zero-fill.test.ts) verifies the wipe by\r\n// capturing a reference to the exact `Uint8Array` returned by\r\n// `randomBytes32()` and checking its contents post-call.\r\n//\r\n// 4. NO `===` / `!==` / `Buffer.equals` / short-circuit array compare\r\n// on any byte array derived from `privateKey`, `password`, or\r\n// `r_bytes` (Requirement 3.8). The only `===` / `!==` operators in\r\n// this file are on `bigint` values (`n_raw === 0n`, `r === 0n`,\r\n// `r !== 0n`) and on numeric loop counters — none of which is a\r\n// byte-array comparison. The audit guard in task 13.1 enforces\r\n// this constraint by string-matching against the forbidden-data\r\n// identifier set.\r\n//\r\n// 5. The `__forTesting__` namespace exposes a `computeProofWithFixedNonce`\r\n// hook that bypasses the live CSPRNG. It is annotated with the\r\n// audit-marker comment `// __forTesting__ — DO NOT IMPORT FROM\r\n// PRODUCTION CODE` immediately above the export. The audit script\r\n// in task 13.1 grep-asserts that this exact marker appears EXACTLY\r\n// ONCE in `src/`, locking the contract that no production module\r\n// can pull in the test-only nonce hook. The hook is NOT part of\r\n// the `@zkp-auth/core` public API surface and `index.ts` does NOT\r\n// re-export it.\r\n//\r\n// Implementation notes:\r\n//\r\n// - The bound `MAX_REJECTION_ITERATIONS = 256` matches `keypair.ts`'s\r\n// constant by design (design \"Key design decisions → 2\"). The\r\n// probability of `r === 0n` after a single CSPRNG draw and a\r\n// `mod L` reduction is `≈ 2^-252` (the only multiples of `L`\r\n// inside `[0, 2^256)` are `0`, `L`, `2L`, ...; very few fit), so\r\n// exhausting 256 successive rejections is statistically\r\n// indistinguishable from impossible. Exhaustion is treated as an\r\n// RNG anomaly and surfaced as `RandomnessError` with stable code\r\n// `'RNG_FAILURE'`, the same code used for an underlying CSPRNG\r\n// throw or short read (Requirement 3.10).\r\n//\r\n// - The `randomBytes32()` call site is wrapped in a try/catch that\r\n// re-wraps any non-`RandomnessError` into a `RandomnessError`. In\r\n// production this is a defense-in-depth no-op — `rng.ts` already\r\n// wraps every CSPRNG fault into `RandomnessError` at the chokepoint\r\n// — but the test suite mocks `rng.ts` directly via `vi.mock` and\r\n// may inject a raw `Error` (see property-13's `computeProof`\r\n// portion). Re-wrapping ensures the public-API contract \"throw\r\n// only `InvalidInputError` and `RandomnessError`\" holds at this\r\n// module's boundary regardless of what the mocked-or-real `rng.ts`\r\n// chooses to throw.\r\n//\r\n// - The shared helper `computeProofCore` takes `r` and `x` as bigints\r\n// already (not as bytes), so callers handle the bigint derivation\r\n// at the entry point and the helper concerns itself only with the\r\n// commitment/transcript/response/wipe/assemble pipeline. The task\r\n// text mentions a six-argument helper signature\r\n// `(privateKey, password, challenge, r_bytes, x, publicKey_bytes)`,\r\n// but the actual computation only NEEDS\r\n// `(r_bytes, r, x, publicKey_bytes, challenge)`: `privateKey` is\r\n// only used to derive `x`, and `password` MUST NOT participate\r\n// anywhere past validation (Property 10). Dropping the unused\r\n// parameters from the helper makes the \"password is not touched\r\n// here\" contract impossible to violate by accident, and matches\r\n// the same file-level-absence pattern that `transcript.ts` uses\r\n// for the same reason.\r\n//\r\n// - The `__forTesting__` hook makes a defensive COPY of the\r\n// caller-supplied `r_bytes` before passing it into the shared core\r\n// helper. The core helper zero-fills its `r_bytes` argument as part\r\n// of the production wipe path; if the helper wiped the test's own\r\n// buffer, a single test invocation that calls the hook twice with\r\n// the same `r_bytes` (e.g. property-10's two-call pattern, where\r\n// `(p1, p2)` are tested against the same nonce) would observe the\r\n// second call's `r` reduce to `0n` and throw. The copy keeps the\r\n// shared-code-path design — the helper still does the real wipe —\r\n// while leaving the test caller's buffer intact for re-use across\r\n// the property body.\r\n\r\nimport { InvalidInputError, RandomnessError } from './errors.js';\r\nimport {\r\n assertUint8Array,\r\n assertUint8ArrayLength,\r\n assertUint8ArrayLengthBetween,\r\n} from './validate.js';\r\nimport { randomBytes32 } from './rng.js';\r\nimport {\r\n L,\r\n BASE,\r\n scalarFromBytesLE,\r\n scalarToBytesLE,\r\n reduceScalar,\r\n pointToBytes,\r\n concatBytes,\r\n} from './encoding.js';\r\nimport { computeFiatShamirScalar } from './transcript.js';\r\n\r\n/**\r\n * Maximum number of rejection-sampling iterations before treating the\r\n * loop as an RNG anomaly. Locked at 256 by design.md \"Key design\r\n * decisions → 2\" — the same constant `keypair.ts` uses for its\r\n * private-key acceptance loop. See the file-header comment for the\r\n * statistical justification.\r\n */\r\nconst MAX_REJECTION_ITERATIONS = 256;\r\n\r\n/**\r\n * Shared post-derivation core of the Schnorr proof construction.\r\n *\r\n * Given the bigint scalars `r ∈ [1, L)` (the nonce) and `x ∈ [1, L)`\r\n * (the secret), the 32-byte encoding `publicKey_bytes` of `x · G`,\r\n * the 32-byte `challenge`, and the 32-byte buffer `r_bytes` from\r\n * which `r` was derived, this helper executes design steps 5–9:\r\n *\r\n * 5. `R = BASE.multiply(r)`, `R_bytes = pointToBytes(R)` (commitment)\r\n * 6. `c = computeFiatShamirScalar(R_bytes, publicKey_bytes, challenge)`\r\n * 7. `s = reduceScalar(r + c * x)`, `s_bytes = scalarToBytesLE(s)`\r\n * 8. `r_bytes.fill(0)` (zero-fill)\r\n * 9. `return concatBytes(R_bytes, s_bytes)` (assembly)\r\n *\r\n * Both the public `computeProof` and the test-only\r\n * `__forTesting__.computeProofWithFixedNonce` route through this\r\n * single helper so the construction is byte-identical between the\r\n * two entry points. That is the property property-10's two-call\r\n * pattern relies on, and it is what lets property-15's RNG-mocked\r\n * harness observe the production wipe path through the test seam.\r\n *\r\n * `privateKey` and `password` are deliberately NOT parameters here.\r\n * `privateKey` was already used at the entry point to derive `x`,\r\n * and `password` MUST NOT participate in proof construction at all\r\n * (Requirement 11.1, Property 10). Closing them out at the file/function\r\n * level — rather than at the call site — makes the contract impossible\r\n * to violate by accident, mirroring the same file-level-absence\r\n * pattern `transcript.ts` uses to lock its own password-free contract.\r\n *\r\n * The helper performs NO byte-array equality on its inputs; it is a\r\n * pure curve-math + hash + concatenate pipeline. The `r_bytes.fill(0)`\r\n * call mutates the caller-supplied buffer in place — this is by\r\n * design (Requirement 6.4) and is what gives property-15 something\r\n * concrete to observe. Callers that must preserve their `r_bytes`\r\n * across the call (only the `__forTesting__` hook fits this profile;\r\n * the production path's `r_bytes` is a fresh CSPRNG draw with no\r\n * other observers) are responsible for passing in a copy.\r\n */\r\nfunction computeProofCore(\r\n r_bytes: Uint8Array,\r\n r: bigint,\r\n x: bigint,\r\n publicKey_bytes: Uint8Array,\r\n challenge: Uint8Array,\r\n): Uint8Array {\r\n // Commitment (Requirement 3.3). `BASE.multiply(r)` is the\r\n // constant-time ladder; `multiplyUnsafe` is forbidden here because\r\n // `r` is a secret nonce whose timing exposure would degrade\r\n // soundness toward the classical `s1 - s2 = (c1 - c2) · x` recovery\r\n // attack documented in `unit-nonce-reuse-attack.test.ts`.\r\n const R = BASE.multiply(r);\r\n const R_bytes = pointToBytes(R);\r\n\r\n // Fiat-Shamir scalar (Requirement 3.3, 8.1, 8.2). The transcript is\r\n // pinned in `transcript.ts`'s single function: this is the ONLY way\r\n // either prover or verifier can produce `c`, so prover/verifier\r\n // construction can never drift. `password` is structurally absent\r\n // from `computeFiatShamirScalar`'s signature.\r\n const c = computeFiatShamirScalar(R_bytes, publicKey_bytes, challenge);\r\n\r\n // Response (Requirement 3.4). `reduceScalar` returns the canonical\r\n // representative in `[0, L)`, so `s_bytes` is well-formed\r\n // (Requirement 4.8 admits `s = 0` as in-range but cryptographically\r\n // degenerate — it is on the verifier to decide correctness via the\r\n // verification equation, not on us to reject the encoding).\r\n const s = reduceScalar(r + c * x);\r\n const s_bytes = scalarToBytesLE(s);\r\n\r\n // Zero-fill the nonce buffer (Requirement 6.4, best-effort). See\r\n // file-header SECURITY-CRITICAL CONTRACT 3 for what this does and\r\n // does NOT prove. The bigint `r` and `x` cannot be wiped from JS;\r\n // we accept that residual exposure and document it in `SELF_REVIEW.md`\r\n // (Requirement 10.2).\r\n r_bytes.fill(0);\r\n\r\n // Output assembly (Requirement 3.1). `concatBytes` is re-exported\r\n // from `encoding.ts` so this module does not need a direct\r\n // `@noble/curves/utils.js` import — the audit guard in task 13.1\r\n // requires that the noble import surface be confined to\r\n // `encoding.ts` and `transcript.ts`.\r\n return concatBytes(R_bytes, s_bytes);\r\n}\r\n\r\n/**\r\n * Computes a 64-byte Schnorr proof of knowledge of `privateKey` over\r\n * a verifier-chosen `challenge`, with `password` carried as opaque\r\n * (and currently unused) metadata for forward-compatibility.\r\n *\r\n * The returned proof is `R_bytes || s_bytes` (32 bytes each), where\r\n * `R = r · G` is the commitment to a fresh CSPRNG-drawn nonce\r\n * `r ∈ [1, L)`, and `s = (r + c · x) mod L` is the response, with\r\n * `c = int_LE(SHA-512(R || X || challenge)) mod L` and\r\n * `x = int_LE(privateKey)` (Requirement 11.1: `x` is derived from\r\n * `privateKey` only; `password` does NOT participate).\r\n *\r\n * The proof verifies under `verify-proof.ts`'s\r\n * `s · G == R + c · publicKey` equation when invoked with the\r\n * matching `publicKey = x · G` (Property 6 round-trip).\r\n *\r\n * `password` is validated for shape (Requirement 3.7) but is then\r\n * treated as opaque bytes — it is NOT mixed into the scalar `x`, NOT\r\n * folded into the Fiat-Shamir transcript, and NOT touched in any\r\n * computation past validation (Requirements 3.3, 11.1; Property 10).\r\n *\r\n * Failure modes:\r\n *\r\n * - `InvalidInputError` with `code === 'INVALID_PRIVATE_KEY'` —\r\n * `privateKey` is not a `Uint8Array(32)`, OR its little-endian\r\n * decoding is `0`, OR its little-endian decoding is `≥ L`\r\n * (Requirements 3.5, 11.4). The `≥ L` and `=== 0` checks are\r\n * performed on the RAW decoding, not on `reduceScalar`'s output:\r\n * `generateKeyPair` always produces in-range keys, so any\r\n * out-of-range input is an integration error and we surface it\r\n * verbatim rather than silently reduce.\r\n * - `InvalidInputError` with `code === 'INVALID_PASSWORD'` —\r\n * `password` is not a `Uint8Array`, or its length exceeds 4096\r\n * bytes (Requirement 3.7). The bound is wide enough to admit any\r\n * reasonable user-supplied password yet rejects payloads large\r\n * enough to suggest accidental data-passing or a DoS attempt.\r\n * - `InvalidInputError` with `code === 'INVALID_CHALLENGE'` —\r\n * `challenge` is not a `Uint8Array(32)` (Requirement 3.6).\r\n * - `RandomnessError` with `code === 'RNG_FAILURE'` — the underlying\r\n * `randomBytes32()` threw or short-read, OR rejection sampling\r\n * exhausted its 256-iteration bound (Requirement 3.10). No\r\n * partial or zero-padded proof is emitted on this failure path.\r\n *\r\n * @param privateKey 32-byte little-endian encoding of a scalar in\r\n * `[1, L)`. Never read after `x` is derived; the buffer is not\r\n * wiped by this function (the caller owns its lifecycle).\r\n * @param password Opaque bytes, length `[0, 4096]`. Validated for\r\n * shape and then ignored.\r\n * @param challenge 32-byte verifier-chosen challenge, ideally\r\n * produced by `generateChallenge`.\r\n * @returns A fresh 64-byte `Uint8Array` carrying `R_bytes || s_bytes`.\r\n * @throws InvalidInputError When any input fails shape or range\r\n * validation.\r\n * @throws RandomnessError When the CSPRNG throws, returns a short\r\n * read, or rejection sampling exhausts its iteration bound.\r\n */\r\nexport function computeProof(\r\n privateKey: Uint8Array,\r\n password: Uint8Array,\r\n challenge: Uint8Array,\r\n): Uint8Array {\r\n // Step 1 — input validation (Requirements 3.5, 3.6, 3.7).\r\n // `assertUint8ArrayLength` first checks the `Uint8Array` shape and\r\n // then the exact length, throwing `InvalidInputError` with the\r\n // supplied error code on either failure. The `password` validation\r\n // is split into a shape assertion and a length-range assertion so\r\n // that both bounds (`0 ≤ length ≤ 4096`) are enforced atomically\r\n // with a single `INVALID_PASSWORD` code.\r\n assertUint8ArrayLength(privateKey, 32, 'INVALID_PRIVATE_KEY', 'privateKey');\r\n assertUint8Array(password, 'INVALID_PASSWORD', 'password');\r\n assertUint8ArrayLengthBetween(password, 0, 4096, 'INVALID_PASSWORD', 'password');\r\n assertUint8ArrayLength(challenge, 32, 'INVALID_CHALLENGE', 'challenge');\r\n\r\n // Step 2 — scalar derivation (Requirements 3.5, 11.1, 11.4).\r\n // Decode `privateKey` as a little-endian bigint with NO reduction.\r\n // Reject the all-zero key (Requirement 11.4: `x = 0` would make the\r\n // proof trivially `R = r·G`, `s = r` and leak the nonce as `s`),\r\n // and reject any value `≥ L` (Requirement 3.5: a key outside\r\n // `[1, L)` is an integration error against `generateKeyPair`'s\r\n // contract that all produced keys are in-range). Any value in\r\n // `[1, L)` is already its own `reduceScalar` representative, so\r\n // assigning `x = n_raw` is exact — no information is lost by\r\n // skipping the explicit `reduceScalar` call.\r\n const n_raw = scalarFromBytesLE(privateKey);\r\n if (n_raw === 0n || n_raw >= L) {\r\n throw new InvalidInputError(\r\n 'INVALID_PRIVATE_KEY',\r\n 'privateKey decodes to a scalar outside [1, L)',\r\n );\r\n }\r\n const x = n_raw;\r\n\r\n // Step 3 — public key for transcript (Requirements 3.3, 11.2).\r\n // Constant-time scalar multiply — `multiplyUnsafe` is forbidden\r\n // here because `x` is the secret. `pointToBytes` produces the\r\n // canonical 32-byte Ed25519 encoding (RFC 8032 §5.1.2) used as the\r\n // middle segment of the Fiat-Shamir transcript.\r\n const publicKey_bytes = pointToBytes(BASE.multiply(x));\r\n\r\n // Step 4 — bounded rejection sampling for the nonce `r`\r\n // (Requirements 3.2, 6.1, 6.2, 6.3, 3.10).\r\n //\r\n // Each iteration draws a fresh 32-byte CSPRNG buffer, decodes it as\r\n // a little-endian bigint, reduces `mod L`, and accepts the draw iff\r\n // the reduced scalar is non-zero. On acceptance we hand the buffer\r\n // and bigint pair to the shared core helper, which builds the\r\n // proof, wipes the buffer, and returns. On `r === 0n` we redraw —\r\n // the rejected `r_bytes` is left to the GC without an explicit\r\n // wipe, matching the same hygiene policy `keypair.ts` documents:\r\n // a rejected candidate was never used to construct any secret-bearing\r\n // material, so its residual presence in memory carries no proof\r\n // material to leak.\r\n for (let i = 0; i < MAX_REJECTION_ITERATIONS; i += 1) {\r\n let r_bytes: Uint8Array;\r\n try {\r\n r_bytes = randomBytes32();\r\n } catch (e) {\r\n // In production, `rng.ts` already wraps every CSPRNG fault into\r\n // `RandomnessError` with stable code `'RNG_FAILURE'`, so this\r\n // re-wrap is a defense-in-depth no-op. Tests mock `rng.ts`\r\n // directly via `vi.mock` and may inject a raw `Error`\r\n // (property-13's `computeProof` portion does exactly this);\r\n // the re-wrap ensures the public-API contract \"throw only\r\n // `InvalidInputError` and `RandomnessError`\" holds at this\r\n // module's boundary regardless. We avoid double-wrapping by\r\n // letting an already-`RandomnessError` propagate unchanged so\r\n // the original `.cause` chain stays intact.\r\n if (e instanceof RandomnessError) throw e;\r\n throw new RandomnessError('CSPRNG failure', { cause: e });\r\n }\r\n\r\n // `reduceScalar(scalarFromBytesLE(r_bytes))` is the canonical\r\n // nonce derivation per design step 4. We reject `r === 0n` and\r\n // redraw — the only way `r === 0n` arises under a healthy CSPRNG\r\n // is when `r_bytes` decodes to a multiple of `L` inside\r\n // `[0, 2^256)`, which has probability `≈ 2^-252`. The bigint\r\n // comparison `r !== 0n` is on a `bigint` value, NOT on a byte\r\n // array, so it is permitted under Requirement 3.8.\r\n const r = reduceScalar(scalarFromBytesLE(r_bytes));\r\n if (r !== 0n) {\r\n return computeProofCore(r_bytes, r, x, publicKey_bytes, challenge);\r\n }\r\n // r === 0n: continue the loop and draw a fresh candidate. We do\r\n // not zero-fill the rejected `r_bytes` here for the same reason\r\n // `keypair.ts` does not zero-fill rejected candidates — rejected\r\n // bytes were never accepted as nonce material and the CSPRNG-state\r\n // information they carry is no more sensitive than any other\r\n // discarded RNG output.\r\n }\r\n\r\n // Loop exhausted without acceptance. Treated as an RNG anomaly per\r\n // design \"Key design decisions → 2\"; surfaces with the same stable\r\n // `.code` (`'RNG_FAILURE'`) as a CSPRNG throw or short read, so\r\n // callers can pattern-match on a single error code for all\r\n // randomness-related failures (Requirement 3.10).\r\n throw new RandomnessError('rejection sampling exhausted');\r\n}\r\n\r\n// __forTesting__ — DO NOT IMPORT FROM PRODUCTION CODE\r\n/**\r\n * Test-only escape hatch that bypasses the live CSPRNG.\r\n *\r\n * `computeProofWithFixedNonce` performs the same input validation as\r\n * `computeProof`, derives `x` and `publicKey_bytes` identically, and\r\n * then routes through the SAME shared `computeProofCore` helper —\r\n * but with a caller-supplied `r_bytes` instead of a fresh CSPRNG\r\n * draw. This is the seam Property 10\r\n * (test/property-10-password-no-op.test.ts) and the adversarial\r\n * documentation test `unit-nonce-reuse-attack.test.ts` (task 7.7)\r\n * rely on: pinning the nonce removes CSPRNG variability so the only\r\n * remaining variable across two calls is whatever input the property\r\n * is varying.\r\n *\r\n * This export is NOT part of `@zkp-auth/core`'s public API surface\r\n * and `index.ts` does NOT re-export it. The audit-marker single-line\r\n * comment immediately above this declaration is grep-asserted by the\r\n * audit script in task 13.1 to appear EXACTLY ONCE in `src/`, locking\r\n * the contract that no production module pulls in this hook.\r\n *\r\n * Hook contract (per design.md ~line 1085 and tasks.md task 7.6):\r\n * the test MUST supply a well-formed `r_bytes` of exactly 32 bytes\r\n * whose `mod L` reduction is non-zero. The hook validates both\r\n * conditions and throws `InvalidInputError('INVALID_PROOF', ...)` on\r\n * violation — the `'INVALID_PROOF'` code is the closest fit in the\r\n * `ErrorCode` taxonomy (`r_bytes` is a piece of proof material) and\r\n * the misuse is a test-author bug rather than an end-user input\r\n * error, so the specific code does not need to land in the public\r\n * stable-code surface.\r\n *\r\n * `r_bytes` is COPIED before being passed to `computeProofCore`. The\r\n * core helper zero-fills its `r_bytes` argument as part of the\r\n * production wipe path; if it wiped the test caller's buffer, a\r\n * single test invocation that calls the hook twice with the same\r\n * `r_bytes` (e.g. property-10's `(p1, p2)` pair) would observe the\r\n * second call's `r` reduce to `0n` and throw. The defensive copy\r\n * preserves the shared-code-path design — the helper still does the\r\n * real wipe — while leaving the test caller's buffer intact for\r\n * re-use across the property body.\r\n */\r\nexport const __forTesting__ = {\r\n computeProofWithFixedNonce(\r\n privateKey: Uint8Array,\r\n password: Uint8Array,\r\n challenge: Uint8Array,\r\n r_bytes: Uint8Array,\r\n ): Uint8Array {\r\n // Same input validation as `computeProof` — Requirements 3.5,\r\n // 3.6, 3.7 — reused verbatim so the hook cannot accidentally\r\n // accept malformed inputs the production path would reject.\r\n assertUint8ArrayLength(privateKey, 32, 'INVALID_PRIVATE_KEY', 'privateKey');\r\n assertUint8Array(password, 'INVALID_PASSWORD', 'password');\r\n assertUint8ArrayLengthBetween(password, 0, 4096, 'INVALID_PASSWORD', 'password');\r\n assertUint8ArrayLength(challenge, 32, 'INVALID_CHALLENGE', 'challenge');\r\n\r\n // `r_bytes` shape check. The test contract requires a 32-byte\r\n // buffer; using `INVALID_PROOF` as the error code reflects that\r\n // `r_bytes` is proof-material-adjacent (it is the nonce buffer\r\n // from which `R` is derived) and that this surface is not part\r\n // of the public API.\r\n assertUint8ArrayLength(r_bytes, 32, 'INVALID_PROOF', 'r_bytes');\r\n\r\n // Same scalar derivation as `computeProof`. See the production\r\n // path's step-2 comment for the rationale of rejecting\r\n // `n_raw === 0n` and `n_raw >= L` on the raw decoding.\r\n const n_raw = scalarFromBytesLE(privateKey);\r\n if (n_raw === 0n || n_raw >= L) {\r\n throw new InvalidInputError(\r\n 'INVALID_PRIVATE_KEY',\r\n 'privateKey decodes to a scalar outside [1, L)',\r\n );\r\n }\r\n const x = n_raw;\r\n\r\n // Same publicKey derivation. Constant-time `BASE.multiply(x)`,\r\n // `multiplyUnsafe` forbidden.\r\n const publicKey_bytes = pointToBytes(BASE.multiply(x));\r\n\r\n // Caller-supplied nonce, mirroring the production reduction. We\r\n // reject `r === 0n` rather than redrawing — there is no live RNG\r\n // to redraw from in this code path, and the test's contract is\r\n // to supply a valid `r_bytes` in the first place.\r\n const r = reduceScalar(scalarFromBytesLE(r_bytes));\r\n if (r === 0n) {\r\n throw new InvalidInputError(\r\n 'INVALID_PROOF',\r\n 'r_bytes reduces to zero modulo L',\r\n );\r\n }\r\n\r\n // Defensive copy — see the JSDoc above for why this is necessary\r\n // for property-10's two-call pattern. `Uint8Array.from` allocates\r\n // a fresh backing buffer that the core helper will zero-fill in\r\n // place; the original `r_bytes` the caller supplied remains\r\n // untouched.\r\n const r_bytes_owned = Uint8Array.from(r_bytes);\r\n\r\n return computeProofCore(r_bytes_owned, r, x, publicKey_bytes, challenge);\r\n },\r\n} as const;\r\n","// @zkp-auth/core — constant-time byte equality\r\n//\r\n// This module is the SOLE call site of `crypto.timingSafeEqual` in\r\n// `packages/zkp-auth-core/src/**/*.ts`. Every byte-array equality over\r\n// secret or attacker-chosen data in the library funnels through\r\n// `timingSafeEqualBytes` so that side-channel discipline can be audited\r\n// statically (audit task 13.1 enforces this with a string-match guard).\r\n//\r\n// Validates: Requirements 5.1, 5.2, 5.3, 5.4\r\n// See design.md → \"Components and Interfaces\" → \"compare.ts — Constant-time byte equality\".\r\n\r\nimport { timingSafeEqual } from 'node:crypto';\r\n\r\n/**\r\n * Constant-time byte-array equality with length tolerance.\r\n *\r\n * Wraps Node's `crypto.timingSafeEqual` to provide two guarantees on top of\r\n * the underlying primitive:\r\n *\r\n * 1. If `a.length !== b.length`, returns `false` without throwing.\r\n * Node's `crypto.timingSafeEqual` throws `RangeError` on unequal-length\r\n * inputs; that throw would itself be a side channel and would force every\r\n * caller to wrap the call in `try`/`catch`. Length is public information\r\n * in this protocol (it is part of the encoding contract), so a synchronous\r\n * `false` return is safe and ergonomic.\r\n * 2. Otherwise delegates to `crypto.timingSafeEqual`, which performs a\r\n * constant-time comparison over the two equal-length buffers.\r\n *\r\n * This is the ONLY function in every TypeScript file under src/ allowed\r\n * to compare bytes derived from a private key, nonce, password, proof, or\r\n * challenge. Callers that need byte equality on secret or attacker-chosen\r\n * data MUST route through this helper; direct use of `===`, `==`,\r\n * `Buffer.equals`, or `Uint8Array`-iterating short-circuit comparisons is\r\n * forbidden by the library's audit guard.\r\n *\r\n * @param a First byte array.\r\n * @param b Second byte array.\r\n * @returns `true` iff `a` and `b` have the same length and every byte\r\n * matches; `false` otherwise. Never throws.\r\n */\r\nexport function timingSafeEqualBytes(a: Uint8Array, b: Uint8Array): boolean {\r\n if (a.length !== b.length) {\r\n return false;\r\n }\r\n return timingSafeEqual(a, b);\r\n}\r\n","// @zkp-auth/core — Schnorr proof verification with Fiat-Shamir transform\r\n//\r\n// This module implements the sole proof-verification entry point of\r\n// `@zkp-auth/core`. Given a registered `publicKey`, a verifier-chosen\r\n// `challenge`, and a candidate 64-byte `proof` produced by\r\n// `compute-proof.ts`, it returns `true` iff the proof satisfies the\r\n// non-interactive Schnorr verification equation\r\n//\r\n// s · G == R + c · publicKey\r\n//\r\n// where `R || s = proof` (32 bytes each), `G = BASE`, and `c` is the\r\n// Fiat-Shamir scalar pinned in `transcript.ts`:\r\n//\r\n// c = int_LE(SHA-512(R_bytes || publicKey_bytes || challenge_bytes)) mod L\r\n//\r\n// The verification equation is symmetric to the construction in\r\n// `compute-proof.ts`: substituting `s = r + c · x` and `R = r · G` and\r\n// `publicKey = x · G` gives `(r + c·x) · G == r·G + c · (x·G)`, which\r\n// holds in the Edwards group. Round-trip correctness against\r\n// `compute-proof.ts` is locked by Property 6, and soundness against a\r\n// matching-key forger is locked by Property 8 (cross-key rejection).\r\n//\r\n// Validates: Requirements 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9,\r\n// 4.10, 4.11\r\n// See design.md → \"Components and Interfaces → verify-proof.ts\" and\r\n// design.md → \"Key design decisions → 4\" (constant-time multiply\r\n// on the verify side too)\r\n// → 5 (asymmetric error model)\r\n// → 6 (oracle avoidance via\r\n// `timingSafeEqualBytes`)\r\n// → \"Verification equation\r\n// choice\" and\r\n// requirements.md → \"Requirement 4: Proof Verification\".\r\n//\r\n// SECURITY-CRITICAL CONTRACTS:\r\n//\r\n// 1. Asymmetric error model (Requirement 4.5–4.8, design \"Key design\r\n// decisions → 5\"). The verify path distinguishes two classes of\r\n// fault:\r\n//\r\n// • Caller-side faults — `publicKey` shape/decode/identity-point,\r\n// `challenge` shape, `proof` shape. These represent integration\r\n// errors against the verifier's own state (an unregistered\r\n// public key, a challenge that the verifier itself did not\r\n// produce, a proof whose length is structurally wrong) and\r\n// MUST surface as `InvalidInputError` with a stable `.code`\r\n// so the caller can pattern-match and remediate.\r\n//\r\n// • Attacker-controlled proof material — malformed `R_bytes`\r\n// (cannot decode to an Edwards point, Requirement 4.7),\r\n// out-of-range `s` with `s >= L` (Requirement 4.8). These\r\n// represent a tampered or hostile proof submitted by a\r\n// prover-side adversary. The verify path MUST silently return\r\n// `false` here, NOT throw — throwing would expose an oracle\r\n// that distinguishes \"malformed proof\" from \"well-formed but\r\n// mathematically invalid proof\", giving an attacker a free\r\n// bit of information per submission. Property 9\r\n// (test/property-09-malformed-r-out-of-range-s.test.ts) locks\r\n// the silent-`false` half of this contract.\r\n//\r\n// 2. `timingSafeEqualBytes` over `point.equals` (Requirement 4.4,\r\n// design \"Key design decisions → 6\"). The final equation check\r\n// `lhs == rhs` MUST be performed by encoding both points to bytes\r\n// via `pointToBytes` and comparing those bytes through\r\n// `timingSafeEqualBytes`. The `EdwardsPoint.equals` method works\r\n// in extended projective coordinates (it cross-multiplies the `Z`\r\n// components), and its timing profile is data-dependent on the\r\n// field arithmetic involved. A timing oracle on the verify\r\n// equation could let an adversary distinguish \"almost-correct\"\r\n// proofs from totally-wrong ones, weakening soundness in\r\n// multi-attempt scenarios. Encoding to canonical RFC 8032 bytes\r\n// and constant-time-comparing is the standard mitigation; it\r\n// also locks the comparison to the same canonical encoding the\r\n// Fiat-Shamir transcript uses, removing any mismatch between\r\n// \"equal as projective points\" and \"equal as wire bytes\".\r\n//\r\n// 3. `pointFromBytesStrict` for `publicKey` (caller-side throw) vs.\r\n// `pointFromBytesSoft` for `R_bytes` (silent false). These two\r\n// decode helpers in `encoding.ts` are NOT interchangeable on this\r\n// code path. The strict variant re-throws as `CryptoError`, which\r\n// we re-wrap into `InvalidInputError('INVALID_PUBLIC_KEY', ...)`\r\n// per Requirement 4.5; the soft variant returns `null`, which we\r\n// convert into `false` per Requirement 4.7. Mixing them up would\r\n// invert the error model for that input — a `pointFromBytesSoft`\r\n// on `publicKey` would silently return `false` for an\r\n// un-decodable public key (denying the caller the typed error),\r\n// and a `pointFromBytesStrict` on `R_bytes` would throw on any\r\n// tampered proof (creating exactly the oracle Requirement 4.7\r\n// forbids).\r\n//\r\n// 4. `is0()` rejection of identity `publicKey` (Requirement 4.5).\r\n// The Edwards identity point `O = (0, 1)` is a valid encoding —\r\n// `pointFromBytesStrict` returns it without complaint — but\r\n// accepting `publicKey == O` would let any forger trivially win:\r\n// with `publicKey = O`, the verification equation collapses to\r\n// `s · G == R + c · O = R`, so any `(R, s)` pair satisfying\r\n// `s · G == R` verifies regardless of `c`. The forger can pick\r\n// any scalar `s`, set `R = s · G`, and submit `(R, s)` for any\r\n// challenge. We close this attack at the verifier by rejecting\r\n// `publicKey = O` outright via `PK.is0()` after a successful\r\n// decode. The check uses the `is0()` method that\r\n// `EdwardsPoint extends CurvePoint<bigint, EdwardsPoint>`\r\n// inherits from the `CurvePoint` interface in\r\n// `@noble/curves/abstract/curve.d.ts` — confirmed available on\r\n// every concrete Edwards point in noble v1.9.x.\r\n//\r\n// 5. NO `===` / `!==` / `Buffer.equals` on byte arrays anywhere in\r\n// this file. The only `===` / `!==` operators present are on\r\n// sentinel and bigint values:\r\n//\r\n// • `R === null` — sentinel check on the soft-decode result;\r\n// `null` is not a byte array, so this is permitted.\r\n// • `s >= L` and `s === 0n` — bigint range checks; bigint\r\n// comparisons are permitted under Requirement 3.8 (the\r\n// forbidden-data identifier set is byte arrays only).\r\n//\r\n// Every byte-array equality in the verify path runs through\r\n// `timingSafeEqualBytes` from `compare.ts`. The audit guard in\r\n// task 13.1 enforces this constraint by string-matching against\r\n// the forbidden-data identifier set across `src/**/*.ts`.\r\n\r\nimport { InvalidInputError } from './errors.js';\r\nimport { assertUint8ArrayLength } from './validate.js';\r\nimport {\r\n L,\r\n BASE,\r\n scalarFromBytesLE,\r\n pointFromBytesStrict,\r\n pointFromBytesSoft,\r\n pointToBytes,\r\n} from './encoding.js';\r\nimport { computeFiatShamirScalar } from './transcript.js';\r\nimport { timingSafeEqualBytes } from './compare.js';\r\n\r\n/**\r\n * Verifies a 64-byte Schnorr proof against the registered `publicKey`\r\n * and the verifier-chosen `challenge`.\r\n *\r\n * Returns `true` iff `proof = R_bytes || s_bytes` satisfies the\r\n * non-interactive Schnorr equation\r\n *\r\n * s · G == R + c · publicKey\r\n *\r\n * with `c = int_LE(SHA-512(R_bytes || publicKey || challenge)) mod L`\r\n * (the Fiat-Shamir scalar pinned in `transcript.ts`, identical to the\r\n * one used by `compute-proof.ts`).\r\n *\r\n * Failure modes:\r\n *\r\n * - `InvalidInputError` with `code === 'INVALID_PUBLIC_KEY'` —\r\n * `publicKey` is not a `Uint8Array(32)` (Requirement 4.5), OR it\r\n * fails to decode as an Edwards point, OR it decodes to the\r\n * identity point `O = (0, 1)`. The identity-point rejection is the\r\n * one Requirement 4.5 specifically calls out: with `publicKey = O`,\r\n * the verification equation collapses to `s · G == R`, which any\r\n * forger can satisfy by picking any `s` and setting `R = s · G`.\r\n * - `InvalidInputError` with `code === 'INVALID_CHALLENGE'` —\r\n * `challenge` is not a `Uint8Array(32)` (Requirement 4.6).\r\n * - `InvalidInputError` with `code === 'INVALID_PROOF'` — `proof` is\r\n * not a `Uint8Array(64)` (Requirement 4.6, applied to the proof\r\n * shape).\r\n * - Returns `false` (does NOT throw) when:\r\n * • `R_bytes` does not decode to a valid Edwards point\r\n * (Requirement 4.7), OR\r\n * • `s = int_LE(s_bytes) >= L` (Requirement 4.8), OR\r\n * • the verification equation `s · G != R + c · publicKey` does\r\n * not hold (Requirement 4.9, the standard \"wrong proof\"\r\n * rejection).\r\n *\r\n * The silent-`false` returns are deliberate (design \"Key design\r\n * decisions → 5–6\"): the verify path must NOT distinguish between\r\n * \"malformed proof material\" and \"well-formed but mathematically\r\n * invalid proof\" via thrown errors, since that would expose an\r\n * oracle to a prover-side adversary.\r\n *\r\n * @param publicKey 32-byte Ed25519 point encoding of the registered\r\n * public key. Must decode to a non-identity point.\r\n * @param challenge 32-byte verifier-chosen challenge, ideally\r\n * produced by `generateChallenge`.\r\n * @param proof 64-byte proof `R_bytes || s_bytes` produced by\r\n * `compute-proof.ts`.\r\n * @returns `true` iff the proof satisfies the Schnorr verification\r\n * equation under `(publicKey, challenge)`; `false` for any\r\n * well-typed-but-invalid proof or attacker-tampered proof material.\r\n * @throws InvalidInputError When any caller-supplied input fails\r\n * shape, length, decoding, or identity-point validation.\r\n */\r\nexport function verifyProof(\r\n publicKey: Uint8Array,\r\n challenge: Uint8Array,\r\n proof: Uint8Array,\r\n): boolean {\r\n // Step 1 — input shape validation (Requirements 4.5, 4.6).\r\n // Length checks come first so every subsequent step can rely on\r\n // the inputs being byte arrays of the right size. The error codes\r\n // here are the public, stable identifiers callers are expected to\r\n // pattern-match on (Requirement 7.4).\r\n assertUint8ArrayLength(publicKey, 32, 'INVALID_PUBLIC_KEY', 'publicKey');\r\n assertUint8ArrayLength(challenge, 32, 'INVALID_CHALLENGE', 'challenge');\r\n assertUint8ArrayLength(proof, 64, 'INVALID_PROOF', 'proof');\r\n\r\n // Step 2 — strict publicKey decode (Requirement 4.5).\r\n // `pointFromBytesStrict` throws `CryptoError` on any decode\r\n // failure (invalid encoding, off-curve y-coordinate, non-canonical\r\n // representation, etc.). We catch and re-throw as the more-\r\n // specific `InvalidInputError('INVALID_PUBLIC_KEY', ...)` so the\r\n // caller-facing error taxonomy stays closed at the public boundary\r\n // (Requirement 7.5). The original `CryptoError` is attached as\r\n // `cause` via the standard `Error` `{ cause }` mechanism — the\r\n // `InvalidInputError` constructor in `errors.ts` does NOT accept\r\n // an options bag (it is `(code, message)` only), but `Error` itself\r\n // honors `.cause` if assigned post-construction. Rather than rely\r\n // on that less-portable assignment, we fold the underlying error's\r\n // message into the human-readable `message` text — the message is\r\n // for diagnostics only (Requirement 7.4 explicitly tells callers\r\n // not to parse it), so embedding the cause is safe.\r\n let PK;\r\n try {\r\n PK = pointFromBytesStrict(publicKey);\r\n } catch (e) {\r\n throw new InvalidInputError(\r\n 'INVALID_PUBLIC_KEY',\r\n `publicKey: failed to decode as Edwards point (${(e as Error).message})`,\r\n );\r\n }\r\n\r\n // Identity-point rejection (Requirement 4.5, SECURITY-CRITICAL\r\n // CONTRACT 4 above). `is0()` is the `CurvePoint`-interface method\r\n // that returns `true` for the Edwards identity `O = (0, 1)` and\r\n // `false` for every other point. Without this check, a registered\r\n // `publicKey == O` would let any forger satisfy the verification\r\n // equation by picking any `s` and setting `R = s · G`.\r\n if (PK.is0()) {\r\n throw new InvalidInputError(\r\n 'INVALID_PUBLIC_KEY',\r\n 'publicKey decodes to the identity point',\r\n );\r\n }\r\n\r\n // Step 3 — slice the proof into its `R` and `s` components per\r\n // the encoding contract (Requirement 3.1, mirrored on the verify\r\n // side as Requirement 4.6). `subarray` returns views into the\r\n // caller's `proof` buffer — no copy is made — which is fine for\r\n // every downstream operation here: `pointFromBytesSoft` and\r\n // `scalarFromBytesLE` both read-only-consume their argument,\r\n // and `computeFiatShamirScalar` likewise only reads `R_bytes`.\r\n const R_bytes = proof.subarray(0, 32);\r\n const s_bytes = proof.subarray(32, 64);\r\n\r\n // Step 4 — soft `R` decode (Requirement 4.7).\r\n // `pointFromBytesSoft` returns `null` instead of throwing when the\r\n // bytes cannot decode to a valid Edwards point. Per Requirement\r\n // 4.7, the verify path MUST surface this as a silent `false` — a\r\n // throw here would create a timing/exception oracle that\r\n // distinguishes \"malformed `R`\" from \"well-formed but invalid\r\n // proof\", giving an adversary a free bit per submission.\r\n //\r\n // The `R === null` comparison is on a sentinel value, not on a\r\n // byte array, so it is permitted under the Requirement 3.8\r\n // forbidden-data-identifier set.\r\n const R = pointFromBytesSoft(R_bytes);\r\n if (R === null) {\r\n return false;\r\n }\r\n\r\n // Step 5 — out-of-range `s` rejection (Requirement 4.8).\r\n // `scalarFromBytesLE` decodes the 32-byte little-endian buffer as\r\n // a non-negative bigint in `[0, 2^256)`. Per Requirement 4.8, any\r\n // `s >= L` MUST cause `verifyProof` to return `false` silently —\r\n // again to deny an oracle distinguishing \"out-of-range `s`\" from\r\n // \"in-range but mathematically invalid `s`\". `s === 0n` is in\r\n // range and is NOT rejected here (a degenerate but well-formed\r\n // value); the verification equation will reject it via the\r\n // mathematical check downstream if it does not happen to satisfy\r\n // `0 == R + c · publicKey`.\r\n //\r\n // The `s >= L` and `s === 0n`-not-rejected comparisons are on\r\n // bigint values, not byte arrays, so they are permitted.\r\n const s = scalarFromBytesLE(s_bytes);\r\n if (s >= L) {\r\n return false;\r\n }\r\n\r\n // Step 6 — Fiat-Shamir scalar (Requirement 4.3, 8.1, 8.2).\r\n // Uses the SAME `computeFiatShamirScalar` function the prover\r\n // calls in `compute-proof.ts`. Sharing the single transcript\r\n // implementation across prover and verifier — pinned in\r\n // `transcript.ts` — is what guarantees the construction cannot\r\n // drift between the two sides; any change to the hash input,\r\n // ordering, or reduction strategy lands in both halves\r\n // simultaneously.\r\n const c = computeFiatShamirScalar(R_bytes, publicKey, challenge);\r\n\r\n // Step 7 — assemble the two sides of the verification equation\r\n // `s · G == R + c · publicKey` (Requirement 4.3, 4.4).\r\n //\r\n // `BASE.multiply(s)` uses the constant-time scalar-multiply ladder.\r\n // `s` here is NOT a secret — it travels over the wire — but\r\n // Requirement 4.4 mandates that the verify equation use the same\r\n // primitive throughout, and `multiply` is also the only ladder\r\n // available on the public `EdwardsPoint` API in noble v1.9.x that\r\n // we use uniformly across the codebase. There is no compensating\r\n // performance argument for `multiplyUnsafe` here on the verifier\r\n // side — keeping the call uniform with `compute-proof.ts` makes\r\n // the audit surface a single rule rather than two.\r\n //\r\n // `PK.multiply(c)` likewise uses the constant-time ladder; `c` is\r\n // derived from a public hash and is also non-secret, but the\r\n // uniformity argument applies equally.\r\n //\r\n // `R.add(...)` is the standard Edwards group addition.\r\n const lhs = BASE.multiply(s);\r\n const rhs = R.add(PK.multiply(c));\r\n\r\n // Step 8 — constant-time equation check (Requirement 4.4,\r\n // SECURITY-CRITICAL CONTRACT 2 above). Encode both points to their\r\n // canonical 32-byte RFC 8032 representations and compare via\r\n // `timingSafeEqualBytes`. We MUST NOT use `lhs.equals(rhs)` — that\r\n // method works in extended projective coordinates and its timing\r\n // profile is data-dependent on the field arithmetic involved.\r\n //\r\n // The byte-level comparison also pins the equality semantics to\r\n // \"equal as wire bytes\", which is the same equality the\r\n // Fiat-Shamir transcript already commits to via `pointToBytes(R)`.\r\n // No mismatch between \"equal as projective points\" and \"equal as\r\n // canonical encodings\" is possible at this seam.\r\n return timingSafeEqualBytes(pointToBytes(lhs), pointToBytes(rhs));\r\n}\r\n"],"mappings":"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","names":["MAX_REJECTION_ITERATIONS"]}
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@zkp-auth/core",
3
+ "version": "0.1.0",
4
+ "description": "Zero-Knowledge Proof core crypto — Schnorr PoK on Ed25519",
5
+ "keywords": ["zkp", "zero-knowledge", "authentication", "schnorr", "ed25519", "crypto", "typescript"],
6
+ "author": "Vedad Kovačević",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/vedadkovacevic/zkp-auth.git",
10
+ "directory": "packages/zkp-auth-core"
11
+ },
12
+ "homepage": "https://zkp-auth.dev",
13
+ "bugs": {
14
+ "url": "https://github.com/vedadkovacevic/zkp-auth/issues"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "module": "./dist/index.mjs",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.mjs",
23
+ "require": "./dist/index.js"
24
+ }
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "test": "vitest run",
29
+ "typecheck": "tsc --noEmit",
30
+ "clean": "rm -rf dist"
31
+ },
32
+ "dependencies": {
33
+ "@noble/curves": "^1.9.7",
34
+ "@noble/hashes": "^1.8.0"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20",
38
+ "fast-check": "^3",
39
+ "tsup": "^8.0.0",
40
+ "typescript": "^5.4.0",
41
+ "vitest": "^1.4.0"
42
+ },
43
+ "files": ["dist"],
44
+ "license": "MIT"
45
+ }