@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.d.mts +293 -0
- package/dist/index.d.ts +293 -0
- package/dist/index.js +303 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +274 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +45 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a fresh `(privateKey, publicKey)` pair for the ZKP-auth
|
|
3
|
+
* scheme.
|
|
4
|
+
*
|
|
5
|
+
* The `privateKey` is a uniform 32-byte little-endian encoding of a
|
|
6
|
+
* scalar `n ∈ [1, L)`, drawn via bounded rejection sampling against
|
|
7
|
+
* the CSPRNG chokepoint `randomBytes32()`. The `publicKey` is the
|
|
8
|
+
* canonical 32-byte encoding of `n · G`, where `G` is the Ed25519
|
|
9
|
+
* base point.
|
|
10
|
+
*
|
|
11
|
+
* The two outputs are returned as fresh `Uint8Array` instances —
|
|
12
|
+
* `privateKey` is the accepted CSPRNG draw itself (a copy detached
|
|
13
|
+
* from Node's internal buffer pool, see `rng.ts`'s `Uint8Array.from`
|
|
14
|
+
* step), and `publicKey` is produced by `@noble/curves`'s `toBytes()`
|
|
15
|
+
* which allocates a fresh array. Callers may zero-fill the returned
|
|
16
|
+
* `privateKey` after use without affecting any other observer
|
|
17
|
+
* (Requirement 6.4 hygiene; not enforced by this function but
|
|
18
|
+
* permitted by its allocation contract).
|
|
19
|
+
*
|
|
20
|
+
* Failure modes — both surface as `RandomnessError` with
|
|
21
|
+
* `code === 'RNG_FAILURE'`, never as a partial or zero-padded result
|
|
22
|
+
* (Requirement 1.5):
|
|
23
|
+
*
|
|
24
|
+
* - The underlying `randomBytes32()` throws (CSPRNG anomaly or short
|
|
25
|
+
* read). Any error — whether a `RandomnessError` already produced
|
|
26
|
+
* by `rng.ts`, or a raw `Error` injected by tests via `vi.mock` —
|
|
27
|
+
* is caught and, if not already a `RandomnessError`, re-wrapped as
|
|
28
|
+
* one. This mirrors the defense-in-depth pattern in `compute-proof.ts`.
|
|
29
|
+
* - 256 successive draws all decode to scalars outside `[1, L)`. This
|
|
30
|
+
* is statistically impossible under a healthy CSPRNG (≈ `2^-252`
|
|
31
|
+
* per draw), so exhaustion is reported as an RNG failure rather
|
|
32
|
+
* than as a separate exhaustion-specific error class.
|
|
33
|
+
*
|
|
34
|
+
* @returns An object with `privateKey` (32 bytes, encoding a scalar
|
|
35
|
+
* in `[1, L)`) and `publicKey` (32 bytes, encoding `privateKey · G`).
|
|
36
|
+
* @throws RandomnessError When the CSPRNG fails or rejection sampling
|
|
37
|
+
* exhausts its 256-iteration bound.
|
|
38
|
+
*/
|
|
39
|
+
declare function generateKeyPair(): {
|
|
40
|
+
privateKey: Uint8Array;
|
|
41
|
+
publicKey: Uint8Array;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates a fresh 32-byte challenge for a Schnorr-proof
|
|
46
|
+
* authentication session.
|
|
47
|
+
*
|
|
48
|
+
* The returned bytes are drawn from the OS CSPRNG via the library's
|
|
49
|
+
* single chokepoint (`randomBytes32`) and are statistically
|
|
50
|
+
* independent of `sessionId` — the parameter exists only so the
|
|
51
|
+
* caller's wire protocol can validate session-handle shape at a
|
|
52
|
+
* single, well-defined entry point. See the file-header comment for
|
|
53
|
+
* the security rationale (Requirement 2.5 / Property 4).
|
|
54
|
+
*
|
|
55
|
+
* The returned `Uint8Array` is a fresh allocation, detached from any
|
|
56
|
+
* internal CSPRNG buffer pool (see `rng.ts`'s `Uint8Array.from` step),
|
|
57
|
+
* so the caller may zero-fill it after use without affecting any
|
|
58
|
+
* other observer.
|
|
59
|
+
*
|
|
60
|
+
* Failure modes:
|
|
61
|
+
*
|
|
62
|
+
* - `InvalidInputError` with `code === 'INVALID_SESSION_ID'` — thrown
|
|
63
|
+
* when `sessionId` is not a `Uint8Array`, has length 0, or has
|
|
64
|
+
* length greater than 256 bytes. The 1..256-byte inclusive window
|
|
65
|
+
* is enforced by `assertUint8ArrayLengthBetween`.
|
|
66
|
+
* - `RandomnessError` with `code === 'RNG_FAILURE'` — thrown when
|
|
67
|
+
* `randomBytes32()` throws (any underlying CSPRNG fault), or when
|
|
68
|
+
* the returned buffer is not exactly 32 bytes. In production,
|
|
69
|
+
* `rng.ts` wraps both cases before they reach this function; the
|
|
70
|
+
* extra length guard and try/catch here are defense-in-depth for
|
|
71
|
+
* test-injected mocks (property-13, `vi.mock`). No partial or
|
|
72
|
+
* zero-padded challenge is ever returned (Requirement 2.4).
|
|
73
|
+
*
|
|
74
|
+
* @param sessionId Caller-supplied session handle. Validated for
|
|
75
|
+
* `Uint8Array` shape and a length in the inclusive range
|
|
76
|
+
* `[1, 256]`; never read after validation.
|
|
77
|
+
* @returns A fresh 32-byte CSPRNG-derived `Uint8Array`, independent
|
|
78
|
+
* of `sessionId`.
|
|
79
|
+
* @throws InvalidInputError When `sessionId` fails shape or length
|
|
80
|
+
* validation.
|
|
81
|
+
* @throws RandomnessError When the underlying CSPRNG throws or
|
|
82
|
+
* returns a short read.
|
|
83
|
+
*/
|
|
84
|
+
declare function generateChallenge(sessionId: Uint8Array): Uint8Array;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Computes a 64-byte Schnorr proof of knowledge of `privateKey` over
|
|
88
|
+
* a verifier-chosen `challenge`, with `password` carried as opaque
|
|
89
|
+
* (and currently unused) metadata for forward-compatibility.
|
|
90
|
+
*
|
|
91
|
+
* The returned proof is `R_bytes || s_bytes` (32 bytes each), where
|
|
92
|
+
* `R = r · G` is the commitment to a fresh CSPRNG-drawn nonce
|
|
93
|
+
* `r ∈ [1, L)`, and `s = (r + c · x) mod L` is the response, with
|
|
94
|
+
* `c = int_LE(SHA-512(R || X || challenge)) mod L` and
|
|
95
|
+
* `x = int_LE(privateKey)` (Requirement 11.1: `x` is derived from
|
|
96
|
+
* `privateKey` only; `password` does NOT participate).
|
|
97
|
+
*
|
|
98
|
+
* The proof verifies under `verify-proof.ts`'s
|
|
99
|
+
* `s · G == R + c · publicKey` equation when invoked with the
|
|
100
|
+
* matching `publicKey = x · G` (Property 6 round-trip).
|
|
101
|
+
*
|
|
102
|
+
* `password` is validated for shape (Requirement 3.7) but is then
|
|
103
|
+
* treated as opaque bytes — it is NOT mixed into the scalar `x`, NOT
|
|
104
|
+
* folded into the Fiat-Shamir transcript, and NOT touched in any
|
|
105
|
+
* computation past validation (Requirements 3.3, 11.1; Property 10).
|
|
106
|
+
*
|
|
107
|
+
* Failure modes:
|
|
108
|
+
*
|
|
109
|
+
* - `InvalidInputError` with `code === 'INVALID_PRIVATE_KEY'` —
|
|
110
|
+
* `privateKey` is not a `Uint8Array(32)`, OR its little-endian
|
|
111
|
+
* decoding is `0`, OR its little-endian decoding is `≥ L`
|
|
112
|
+
* (Requirements 3.5, 11.4). The `≥ L` and `=== 0` checks are
|
|
113
|
+
* performed on the RAW decoding, not on `reduceScalar`'s output:
|
|
114
|
+
* `generateKeyPair` always produces in-range keys, so any
|
|
115
|
+
* out-of-range input is an integration error and we surface it
|
|
116
|
+
* verbatim rather than silently reduce.
|
|
117
|
+
* - `InvalidInputError` with `code === 'INVALID_PASSWORD'` —
|
|
118
|
+
* `password` is not a `Uint8Array`, or its length exceeds 4096
|
|
119
|
+
* bytes (Requirement 3.7). The bound is wide enough to admit any
|
|
120
|
+
* reasonable user-supplied password yet rejects payloads large
|
|
121
|
+
* enough to suggest accidental data-passing or a DoS attempt.
|
|
122
|
+
* - `InvalidInputError` with `code === 'INVALID_CHALLENGE'` —
|
|
123
|
+
* `challenge` is not a `Uint8Array(32)` (Requirement 3.6).
|
|
124
|
+
* - `RandomnessError` with `code === 'RNG_FAILURE'` — the underlying
|
|
125
|
+
* `randomBytes32()` threw or short-read, OR rejection sampling
|
|
126
|
+
* exhausted its 256-iteration bound (Requirement 3.10). No
|
|
127
|
+
* partial or zero-padded proof is emitted on this failure path.
|
|
128
|
+
*
|
|
129
|
+
* @param privateKey 32-byte little-endian encoding of a scalar in
|
|
130
|
+
* `[1, L)`. Never read after `x` is derived; the buffer is not
|
|
131
|
+
* wiped by this function (the caller owns its lifecycle).
|
|
132
|
+
* @param password Opaque bytes, length `[0, 4096]`. Validated for
|
|
133
|
+
* shape and then ignored.
|
|
134
|
+
* @param challenge 32-byte verifier-chosen challenge, ideally
|
|
135
|
+
* produced by `generateChallenge`.
|
|
136
|
+
* @returns A fresh 64-byte `Uint8Array` carrying `R_bytes || s_bytes`.
|
|
137
|
+
* @throws InvalidInputError When any input fails shape or range
|
|
138
|
+
* validation.
|
|
139
|
+
* @throws RandomnessError When the CSPRNG throws, returns a short
|
|
140
|
+
* read, or rejection sampling exhausts its iteration bound.
|
|
141
|
+
*/
|
|
142
|
+
declare function computeProof(privateKey: Uint8Array, password: Uint8Array, challenge: Uint8Array): Uint8Array;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Verifies a 64-byte Schnorr proof against the registered `publicKey`
|
|
146
|
+
* and the verifier-chosen `challenge`.
|
|
147
|
+
*
|
|
148
|
+
* Returns `true` iff `proof = R_bytes || s_bytes` satisfies the
|
|
149
|
+
* non-interactive Schnorr equation
|
|
150
|
+
*
|
|
151
|
+
* s · G == R + c · publicKey
|
|
152
|
+
*
|
|
153
|
+
* with `c = int_LE(SHA-512(R_bytes || publicKey || challenge)) mod L`
|
|
154
|
+
* (the Fiat-Shamir scalar pinned in `transcript.ts`, identical to the
|
|
155
|
+
* one used by `compute-proof.ts`).
|
|
156
|
+
*
|
|
157
|
+
* Failure modes:
|
|
158
|
+
*
|
|
159
|
+
* - `InvalidInputError` with `code === 'INVALID_PUBLIC_KEY'` —
|
|
160
|
+
* `publicKey` is not a `Uint8Array(32)` (Requirement 4.5), OR it
|
|
161
|
+
* fails to decode as an Edwards point, OR it decodes to the
|
|
162
|
+
* identity point `O = (0, 1)`. The identity-point rejection is the
|
|
163
|
+
* one Requirement 4.5 specifically calls out: with `publicKey = O`,
|
|
164
|
+
* the verification equation collapses to `s · G == R`, which any
|
|
165
|
+
* forger can satisfy by picking any `s` and setting `R = s · G`.
|
|
166
|
+
* - `InvalidInputError` with `code === 'INVALID_CHALLENGE'` —
|
|
167
|
+
* `challenge` is not a `Uint8Array(32)` (Requirement 4.6).
|
|
168
|
+
* - `InvalidInputError` with `code === 'INVALID_PROOF'` — `proof` is
|
|
169
|
+
* not a `Uint8Array(64)` (Requirement 4.6, applied to the proof
|
|
170
|
+
* shape).
|
|
171
|
+
* - Returns `false` (does NOT throw) when:
|
|
172
|
+
* • `R_bytes` does not decode to a valid Edwards point
|
|
173
|
+
* (Requirement 4.7), OR
|
|
174
|
+
* • `s = int_LE(s_bytes) >= L` (Requirement 4.8), OR
|
|
175
|
+
* • the verification equation `s · G != R + c · publicKey` does
|
|
176
|
+
* not hold (Requirement 4.9, the standard "wrong proof"
|
|
177
|
+
* rejection).
|
|
178
|
+
*
|
|
179
|
+
* The silent-`false` returns are deliberate (design "Key design
|
|
180
|
+
* decisions → 5–6"): the verify path must NOT distinguish between
|
|
181
|
+
* "malformed proof material" and "well-formed but mathematically
|
|
182
|
+
* invalid proof" via thrown errors, since that would expose an
|
|
183
|
+
* oracle to a prover-side adversary.
|
|
184
|
+
*
|
|
185
|
+
* @param publicKey 32-byte Ed25519 point encoding of the registered
|
|
186
|
+
* public key. Must decode to a non-identity point.
|
|
187
|
+
* @param challenge 32-byte verifier-chosen challenge, ideally
|
|
188
|
+
* produced by `generateChallenge`.
|
|
189
|
+
* @param proof 64-byte proof `R_bytes || s_bytes` produced by
|
|
190
|
+
* `compute-proof.ts`.
|
|
191
|
+
* @returns `true` iff the proof satisfies the Schnorr verification
|
|
192
|
+
* equation under `(publicKey, challenge)`; `false` for any
|
|
193
|
+
* well-typed-but-invalid proof or attacker-tampered proof material.
|
|
194
|
+
* @throws InvalidInputError When any caller-supplied input fails
|
|
195
|
+
* shape, length, decoding, or identity-point validation.
|
|
196
|
+
*/
|
|
197
|
+
declare function verifyProof(publicKey: Uint8Array, challenge: Uint8Array, proof: Uint8Array): boolean;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stable, machine-readable identifiers attached to every thrown error.
|
|
201
|
+
*
|
|
202
|
+
* Callers are expected to pattern-match on `.code` rather than inspect
|
|
203
|
+
* `.message`, which is for human readers only. The set is closed: adding a
|
|
204
|
+
* new code is a breaking change to the public API surface.
|
|
205
|
+
*
|
|
206
|
+
* - `INVALID_PRIVATE_KEY` — privateKey shape, length, or scalar range invalid.
|
|
207
|
+
* - `INVALID_PUBLIC_KEY` — publicKey shape, length, decode, or identity-point.
|
|
208
|
+
* - `INVALID_CHALLENGE` — challenge shape or length invalid.
|
|
209
|
+
* - `INVALID_PROOF` — proof shape or length invalid (NOT verification failure).
|
|
210
|
+
* - `INVALID_PASSWORD` — password shape or length invalid.
|
|
211
|
+
* - `INVALID_SESSION_ID` — sessionId shape, empty, or oversize.
|
|
212
|
+
* - `RNG_FAILURE` — CSPRNG threw, returned short, or rejection-sampling exhausted.
|
|
213
|
+
* - `CURVE_ERROR` — `@noble/curves` raised an unexpected internal error.
|
|
214
|
+
*/
|
|
215
|
+
type ErrorCode = 'INVALID_PRIVATE_KEY' | 'INVALID_PUBLIC_KEY' | 'INVALID_CHALLENGE' | 'INVALID_PROOF' | 'INVALID_PASSWORD' | 'INVALID_SESSION_ID' | 'RNG_FAILURE' | 'CURVE_ERROR';
|
|
216
|
+
/**
|
|
217
|
+
* Thrown when a public function receives an input that fails shape, length,
|
|
218
|
+
* encoding, or range validation. The accompanying `.code` indicates which
|
|
219
|
+
* input was invalid.
|
|
220
|
+
*
|
|
221
|
+
* `.name` is set as a readonly class field so it cannot be silently shadowed
|
|
222
|
+
* by user-land subclasses or by `Error`'s default `'Error'` value.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* try {
|
|
226
|
+
* computeProof(privateKey, password, challenge);
|
|
227
|
+
* } catch (e) {
|
|
228
|
+
* if (e instanceof InvalidInputError && e.code === 'INVALID_CHALLENGE') {
|
|
229
|
+
* // handle challenge-shape failure
|
|
230
|
+
* }
|
|
231
|
+
* }
|
|
232
|
+
*/
|
|
233
|
+
declare class InvalidInputError extends Error {
|
|
234
|
+
/** Class name; fixed for all instances. */
|
|
235
|
+
readonly name = "InvalidInputError";
|
|
236
|
+
/** Stable, machine-readable identifier for the failing input. */
|
|
237
|
+
readonly code: ErrorCode;
|
|
238
|
+
/**
|
|
239
|
+
* @param code Stable identifier for the failing input.
|
|
240
|
+
* @param message Human-readable description; not part of the stable API.
|
|
241
|
+
*/
|
|
242
|
+
constructor(code: ErrorCode, message: string);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Thrown when the underlying CSPRNG throws, returns a short read, or when
|
|
246
|
+
* bounded rejection sampling exhausts its iteration cap (treated as an RNG
|
|
247
|
+
* anomaly). The library MUST NOT emit a partial output on this failure path.
|
|
248
|
+
*
|
|
249
|
+
* `.code` is fixed to `'RNG_FAILURE'`. The optional `cause` carries the
|
|
250
|
+
* underlying error (e.g. the `node:crypto.randomBytes` throw) for diagnostics.
|
|
251
|
+
*
|
|
252
|
+
* `cause` is attached via a structural cast so the assignment works under
|
|
253
|
+
* any tsconfig `lib` selection that may or may not include
|
|
254
|
+
* `lib.es2022.error.d.ts` (per design.md).
|
|
255
|
+
*/
|
|
256
|
+
declare class RandomnessError extends Error {
|
|
257
|
+
/** Class name; fixed for all instances. */
|
|
258
|
+
readonly name = "RandomnessError";
|
|
259
|
+
/** Stable, machine-readable identifier; fixed for this class. */
|
|
260
|
+
readonly code: ErrorCode;
|
|
261
|
+
/**
|
|
262
|
+
* @param message Human-readable description; not part of the stable API.
|
|
263
|
+
* @param options Optional bag carrying the underlying `cause`.
|
|
264
|
+
*/
|
|
265
|
+
constructor(message: string, options?: {
|
|
266
|
+
cause?: unknown;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Thrown when an internal `@noble/curves` operation raises an unexpected
|
|
271
|
+
* error (e.g. invalid point encoding) on a code path where the library's
|
|
272
|
+
* contract is to throw rather than return `false`. The verification path's
|
|
273
|
+
* silent-`false` returns for malformed `R` and out-of-range `s` are
|
|
274
|
+
* deliberate exceptions to this rule (see design.md, Requirements 4.7, 4.8).
|
|
275
|
+
*
|
|
276
|
+
* `.code` is fixed to `'CURVE_ERROR'`. The optional `cause` carries the
|
|
277
|
+
* underlying noble error for diagnostics.
|
|
278
|
+
*/
|
|
279
|
+
declare class CryptoError extends Error {
|
|
280
|
+
/** Class name; fixed for all instances. */
|
|
281
|
+
readonly name = "CryptoError";
|
|
282
|
+
/** Stable, machine-readable identifier; fixed for this class. */
|
|
283
|
+
readonly code: ErrorCode;
|
|
284
|
+
/**
|
|
285
|
+
* @param message Human-readable description; not part of the stable API.
|
|
286
|
+
* @param options Optional bag carrying the underlying `cause`.
|
|
287
|
+
*/
|
|
288
|
+
constructor(message: string, options?: {
|
|
289
|
+
cause?: unknown;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export { CryptoError, type ErrorCode, InvalidInputError, RandomnessError, computeProof, generateChallenge, generateKeyPair, verifyProof };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a fresh `(privateKey, publicKey)` pair for the ZKP-auth
|
|
3
|
+
* scheme.
|
|
4
|
+
*
|
|
5
|
+
* The `privateKey` is a uniform 32-byte little-endian encoding of a
|
|
6
|
+
* scalar `n ∈ [1, L)`, drawn via bounded rejection sampling against
|
|
7
|
+
* the CSPRNG chokepoint `randomBytes32()`. The `publicKey` is the
|
|
8
|
+
* canonical 32-byte encoding of `n · G`, where `G` is the Ed25519
|
|
9
|
+
* base point.
|
|
10
|
+
*
|
|
11
|
+
* The two outputs are returned as fresh `Uint8Array` instances —
|
|
12
|
+
* `privateKey` is the accepted CSPRNG draw itself (a copy detached
|
|
13
|
+
* from Node's internal buffer pool, see `rng.ts`'s `Uint8Array.from`
|
|
14
|
+
* step), and `publicKey` is produced by `@noble/curves`'s `toBytes()`
|
|
15
|
+
* which allocates a fresh array. Callers may zero-fill the returned
|
|
16
|
+
* `privateKey` after use without affecting any other observer
|
|
17
|
+
* (Requirement 6.4 hygiene; not enforced by this function but
|
|
18
|
+
* permitted by its allocation contract).
|
|
19
|
+
*
|
|
20
|
+
* Failure modes — both surface as `RandomnessError` with
|
|
21
|
+
* `code === 'RNG_FAILURE'`, never as a partial or zero-padded result
|
|
22
|
+
* (Requirement 1.5):
|
|
23
|
+
*
|
|
24
|
+
* - The underlying `randomBytes32()` throws (CSPRNG anomaly or short
|
|
25
|
+
* read). Any error — whether a `RandomnessError` already produced
|
|
26
|
+
* by `rng.ts`, or a raw `Error` injected by tests via `vi.mock` —
|
|
27
|
+
* is caught and, if not already a `RandomnessError`, re-wrapped as
|
|
28
|
+
* one. This mirrors the defense-in-depth pattern in `compute-proof.ts`.
|
|
29
|
+
* - 256 successive draws all decode to scalars outside `[1, L)`. This
|
|
30
|
+
* is statistically impossible under a healthy CSPRNG (≈ `2^-252`
|
|
31
|
+
* per draw), so exhaustion is reported as an RNG failure rather
|
|
32
|
+
* than as a separate exhaustion-specific error class.
|
|
33
|
+
*
|
|
34
|
+
* @returns An object with `privateKey` (32 bytes, encoding a scalar
|
|
35
|
+
* in `[1, L)`) and `publicKey` (32 bytes, encoding `privateKey · G`).
|
|
36
|
+
* @throws RandomnessError When the CSPRNG fails or rejection sampling
|
|
37
|
+
* exhausts its 256-iteration bound.
|
|
38
|
+
*/
|
|
39
|
+
declare function generateKeyPair(): {
|
|
40
|
+
privateKey: Uint8Array;
|
|
41
|
+
publicKey: Uint8Array;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Generates a fresh 32-byte challenge for a Schnorr-proof
|
|
46
|
+
* authentication session.
|
|
47
|
+
*
|
|
48
|
+
* The returned bytes are drawn from the OS CSPRNG via the library's
|
|
49
|
+
* single chokepoint (`randomBytes32`) and are statistically
|
|
50
|
+
* independent of `sessionId` — the parameter exists only so the
|
|
51
|
+
* caller's wire protocol can validate session-handle shape at a
|
|
52
|
+
* single, well-defined entry point. See the file-header comment for
|
|
53
|
+
* the security rationale (Requirement 2.5 / Property 4).
|
|
54
|
+
*
|
|
55
|
+
* The returned `Uint8Array` is a fresh allocation, detached from any
|
|
56
|
+
* internal CSPRNG buffer pool (see `rng.ts`'s `Uint8Array.from` step),
|
|
57
|
+
* so the caller may zero-fill it after use without affecting any
|
|
58
|
+
* other observer.
|
|
59
|
+
*
|
|
60
|
+
* Failure modes:
|
|
61
|
+
*
|
|
62
|
+
* - `InvalidInputError` with `code === 'INVALID_SESSION_ID'` — thrown
|
|
63
|
+
* when `sessionId` is not a `Uint8Array`, has length 0, or has
|
|
64
|
+
* length greater than 256 bytes. The 1..256-byte inclusive window
|
|
65
|
+
* is enforced by `assertUint8ArrayLengthBetween`.
|
|
66
|
+
* - `RandomnessError` with `code === 'RNG_FAILURE'` — thrown when
|
|
67
|
+
* `randomBytes32()` throws (any underlying CSPRNG fault), or when
|
|
68
|
+
* the returned buffer is not exactly 32 bytes. In production,
|
|
69
|
+
* `rng.ts` wraps both cases before they reach this function; the
|
|
70
|
+
* extra length guard and try/catch here are defense-in-depth for
|
|
71
|
+
* test-injected mocks (property-13, `vi.mock`). No partial or
|
|
72
|
+
* zero-padded challenge is ever returned (Requirement 2.4).
|
|
73
|
+
*
|
|
74
|
+
* @param sessionId Caller-supplied session handle. Validated for
|
|
75
|
+
* `Uint8Array` shape and a length in the inclusive range
|
|
76
|
+
* `[1, 256]`; never read after validation.
|
|
77
|
+
* @returns A fresh 32-byte CSPRNG-derived `Uint8Array`, independent
|
|
78
|
+
* of `sessionId`.
|
|
79
|
+
* @throws InvalidInputError When `sessionId` fails shape or length
|
|
80
|
+
* validation.
|
|
81
|
+
* @throws RandomnessError When the underlying CSPRNG throws or
|
|
82
|
+
* returns a short read.
|
|
83
|
+
*/
|
|
84
|
+
declare function generateChallenge(sessionId: Uint8Array): Uint8Array;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Computes a 64-byte Schnorr proof of knowledge of `privateKey` over
|
|
88
|
+
* a verifier-chosen `challenge`, with `password` carried as opaque
|
|
89
|
+
* (and currently unused) metadata for forward-compatibility.
|
|
90
|
+
*
|
|
91
|
+
* The returned proof is `R_bytes || s_bytes` (32 bytes each), where
|
|
92
|
+
* `R = r · G` is the commitment to a fresh CSPRNG-drawn nonce
|
|
93
|
+
* `r ∈ [1, L)`, and `s = (r + c · x) mod L` is the response, with
|
|
94
|
+
* `c = int_LE(SHA-512(R || X || challenge)) mod L` and
|
|
95
|
+
* `x = int_LE(privateKey)` (Requirement 11.1: `x` is derived from
|
|
96
|
+
* `privateKey` only; `password` does NOT participate).
|
|
97
|
+
*
|
|
98
|
+
* The proof verifies under `verify-proof.ts`'s
|
|
99
|
+
* `s · G == R + c · publicKey` equation when invoked with the
|
|
100
|
+
* matching `publicKey = x · G` (Property 6 round-trip).
|
|
101
|
+
*
|
|
102
|
+
* `password` is validated for shape (Requirement 3.7) but is then
|
|
103
|
+
* treated as opaque bytes — it is NOT mixed into the scalar `x`, NOT
|
|
104
|
+
* folded into the Fiat-Shamir transcript, and NOT touched in any
|
|
105
|
+
* computation past validation (Requirements 3.3, 11.1; Property 10).
|
|
106
|
+
*
|
|
107
|
+
* Failure modes:
|
|
108
|
+
*
|
|
109
|
+
* - `InvalidInputError` with `code === 'INVALID_PRIVATE_KEY'` —
|
|
110
|
+
* `privateKey` is not a `Uint8Array(32)`, OR its little-endian
|
|
111
|
+
* decoding is `0`, OR its little-endian decoding is `≥ L`
|
|
112
|
+
* (Requirements 3.5, 11.4). The `≥ L` and `=== 0` checks are
|
|
113
|
+
* performed on the RAW decoding, not on `reduceScalar`'s output:
|
|
114
|
+
* `generateKeyPair` always produces in-range keys, so any
|
|
115
|
+
* out-of-range input is an integration error and we surface it
|
|
116
|
+
* verbatim rather than silently reduce.
|
|
117
|
+
* - `InvalidInputError` with `code === 'INVALID_PASSWORD'` —
|
|
118
|
+
* `password` is not a `Uint8Array`, or its length exceeds 4096
|
|
119
|
+
* bytes (Requirement 3.7). The bound is wide enough to admit any
|
|
120
|
+
* reasonable user-supplied password yet rejects payloads large
|
|
121
|
+
* enough to suggest accidental data-passing or a DoS attempt.
|
|
122
|
+
* - `InvalidInputError` with `code === 'INVALID_CHALLENGE'` —
|
|
123
|
+
* `challenge` is not a `Uint8Array(32)` (Requirement 3.6).
|
|
124
|
+
* - `RandomnessError` with `code === 'RNG_FAILURE'` — the underlying
|
|
125
|
+
* `randomBytes32()` threw or short-read, OR rejection sampling
|
|
126
|
+
* exhausted its 256-iteration bound (Requirement 3.10). No
|
|
127
|
+
* partial or zero-padded proof is emitted on this failure path.
|
|
128
|
+
*
|
|
129
|
+
* @param privateKey 32-byte little-endian encoding of a scalar in
|
|
130
|
+
* `[1, L)`. Never read after `x` is derived; the buffer is not
|
|
131
|
+
* wiped by this function (the caller owns its lifecycle).
|
|
132
|
+
* @param password Opaque bytes, length `[0, 4096]`. Validated for
|
|
133
|
+
* shape and then ignored.
|
|
134
|
+
* @param challenge 32-byte verifier-chosen challenge, ideally
|
|
135
|
+
* produced by `generateChallenge`.
|
|
136
|
+
* @returns A fresh 64-byte `Uint8Array` carrying `R_bytes || s_bytes`.
|
|
137
|
+
* @throws InvalidInputError When any input fails shape or range
|
|
138
|
+
* validation.
|
|
139
|
+
* @throws RandomnessError When the CSPRNG throws, returns a short
|
|
140
|
+
* read, or rejection sampling exhausts its iteration bound.
|
|
141
|
+
*/
|
|
142
|
+
declare function computeProof(privateKey: Uint8Array, password: Uint8Array, challenge: Uint8Array): Uint8Array;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Verifies a 64-byte Schnorr proof against the registered `publicKey`
|
|
146
|
+
* and the verifier-chosen `challenge`.
|
|
147
|
+
*
|
|
148
|
+
* Returns `true` iff `proof = R_bytes || s_bytes` satisfies the
|
|
149
|
+
* non-interactive Schnorr equation
|
|
150
|
+
*
|
|
151
|
+
* s · G == R + c · publicKey
|
|
152
|
+
*
|
|
153
|
+
* with `c = int_LE(SHA-512(R_bytes || publicKey || challenge)) mod L`
|
|
154
|
+
* (the Fiat-Shamir scalar pinned in `transcript.ts`, identical to the
|
|
155
|
+
* one used by `compute-proof.ts`).
|
|
156
|
+
*
|
|
157
|
+
* Failure modes:
|
|
158
|
+
*
|
|
159
|
+
* - `InvalidInputError` with `code === 'INVALID_PUBLIC_KEY'` —
|
|
160
|
+
* `publicKey` is not a `Uint8Array(32)` (Requirement 4.5), OR it
|
|
161
|
+
* fails to decode as an Edwards point, OR it decodes to the
|
|
162
|
+
* identity point `O = (0, 1)`. The identity-point rejection is the
|
|
163
|
+
* one Requirement 4.5 specifically calls out: with `publicKey = O`,
|
|
164
|
+
* the verification equation collapses to `s · G == R`, which any
|
|
165
|
+
* forger can satisfy by picking any `s` and setting `R = s · G`.
|
|
166
|
+
* - `InvalidInputError` with `code === 'INVALID_CHALLENGE'` —
|
|
167
|
+
* `challenge` is not a `Uint8Array(32)` (Requirement 4.6).
|
|
168
|
+
* - `InvalidInputError` with `code === 'INVALID_PROOF'` — `proof` is
|
|
169
|
+
* not a `Uint8Array(64)` (Requirement 4.6, applied to the proof
|
|
170
|
+
* shape).
|
|
171
|
+
* - Returns `false` (does NOT throw) when:
|
|
172
|
+
* • `R_bytes` does not decode to a valid Edwards point
|
|
173
|
+
* (Requirement 4.7), OR
|
|
174
|
+
* • `s = int_LE(s_bytes) >= L` (Requirement 4.8), OR
|
|
175
|
+
* • the verification equation `s · G != R + c · publicKey` does
|
|
176
|
+
* not hold (Requirement 4.9, the standard "wrong proof"
|
|
177
|
+
* rejection).
|
|
178
|
+
*
|
|
179
|
+
* The silent-`false` returns are deliberate (design "Key design
|
|
180
|
+
* decisions → 5–6"): the verify path must NOT distinguish between
|
|
181
|
+
* "malformed proof material" and "well-formed but mathematically
|
|
182
|
+
* invalid proof" via thrown errors, since that would expose an
|
|
183
|
+
* oracle to a prover-side adversary.
|
|
184
|
+
*
|
|
185
|
+
* @param publicKey 32-byte Ed25519 point encoding of the registered
|
|
186
|
+
* public key. Must decode to a non-identity point.
|
|
187
|
+
* @param challenge 32-byte verifier-chosen challenge, ideally
|
|
188
|
+
* produced by `generateChallenge`.
|
|
189
|
+
* @param proof 64-byte proof `R_bytes || s_bytes` produced by
|
|
190
|
+
* `compute-proof.ts`.
|
|
191
|
+
* @returns `true` iff the proof satisfies the Schnorr verification
|
|
192
|
+
* equation under `(publicKey, challenge)`; `false` for any
|
|
193
|
+
* well-typed-but-invalid proof or attacker-tampered proof material.
|
|
194
|
+
* @throws InvalidInputError When any caller-supplied input fails
|
|
195
|
+
* shape, length, decoding, or identity-point validation.
|
|
196
|
+
*/
|
|
197
|
+
declare function verifyProof(publicKey: Uint8Array, challenge: Uint8Array, proof: Uint8Array): boolean;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Stable, machine-readable identifiers attached to every thrown error.
|
|
201
|
+
*
|
|
202
|
+
* Callers are expected to pattern-match on `.code` rather than inspect
|
|
203
|
+
* `.message`, which is for human readers only. The set is closed: adding a
|
|
204
|
+
* new code is a breaking change to the public API surface.
|
|
205
|
+
*
|
|
206
|
+
* - `INVALID_PRIVATE_KEY` — privateKey shape, length, or scalar range invalid.
|
|
207
|
+
* - `INVALID_PUBLIC_KEY` — publicKey shape, length, decode, or identity-point.
|
|
208
|
+
* - `INVALID_CHALLENGE` — challenge shape or length invalid.
|
|
209
|
+
* - `INVALID_PROOF` — proof shape or length invalid (NOT verification failure).
|
|
210
|
+
* - `INVALID_PASSWORD` — password shape or length invalid.
|
|
211
|
+
* - `INVALID_SESSION_ID` — sessionId shape, empty, or oversize.
|
|
212
|
+
* - `RNG_FAILURE` — CSPRNG threw, returned short, or rejection-sampling exhausted.
|
|
213
|
+
* - `CURVE_ERROR` — `@noble/curves` raised an unexpected internal error.
|
|
214
|
+
*/
|
|
215
|
+
type ErrorCode = 'INVALID_PRIVATE_KEY' | 'INVALID_PUBLIC_KEY' | 'INVALID_CHALLENGE' | 'INVALID_PROOF' | 'INVALID_PASSWORD' | 'INVALID_SESSION_ID' | 'RNG_FAILURE' | 'CURVE_ERROR';
|
|
216
|
+
/**
|
|
217
|
+
* Thrown when a public function receives an input that fails shape, length,
|
|
218
|
+
* encoding, or range validation. The accompanying `.code` indicates which
|
|
219
|
+
* input was invalid.
|
|
220
|
+
*
|
|
221
|
+
* `.name` is set as a readonly class field so it cannot be silently shadowed
|
|
222
|
+
* by user-land subclasses or by `Error`'s default `'Error'` value.
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* try {
|
|
226
|
+
* computeProof(privateKey, password, challenge);
|
|
227
|
+
* } catch (e) {
|
|
228
|
+
* if (e instanceof InvalidInputError && e.code === 'INVALID_CHALLENGE') {
|
|
229
|
+
* // handle challenge-shape failure
|
|
230
|
+
* }
|
|
231
|
+
* }
|
|
232
|
+
*/
|
|
233
|
+
declare class InvalidInputError extends Error {
|
|
234
|
+
/** Class name; fixed for all instances. */
|
|
235
|
+
readonly name = "InvalidInputError";
|
|
236
|
+
/** Stable, machine-readable identifier for the failing input. */
|
|
237
|
+
readonly code: ErrorCode;
|
|
238
|
+
/**
|
|
239
|
+
* @param code Stable identifier for the failing input.
|
|
240
|
+
* @param message Human-readable description; not part of the stable API.
|
|
241
|
+
*/
|
|
242
|
+
constructor(code: ErrorCode, message: string);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Thrown when the underlying CSPRNG throws, returns a short read, or when
|
|
246
|
+
* bounded rejection sampling exhausts its iteration cap (treated as an RNG
|
|
247
|
+
* anomaly). The library MUST NOT emit a partial output on this failure path.
|
|
248
|
+
*
|
|
249
|
+
* `.code` is fixed to `'RNG_FAILURE'`. The optional `cause` carries the
|
|
250
|
+
* underlying error (e.g. the `node:crypto.randomBytes` throw) for diagnostics.
|
|
251
|
+
*
|
|
252
|
+
* `cause` is attached via a structural cast so the assignment works under
|
|
253
|
+
* any tsconfig `lib` selection that may or may not include
|
|
254
|
+
* `lib.es2022.error.d.ts` (per design.md).
|
|
255
|
+
*/
|
|
256
|
+
declare class RandomnessError extends Error {
|
|
257
|
+
/** Class name; fixed for all instances. */
|
|
258
|
+
readonly name = "RandomnessError";
|
|
259
|
+
/** Stable, machine-readable identifier; fixed for this class. */
|
|
260
|
+
readonly code: ErrorCode;
|
|
261
|
+
/**
|
|
262
|
+
* @param message Human-readable description; not part of the stable API.
|
|
263
|
+
* @param options Optional bag carrying the underlying `cause`.
|
|
264
|
+
*/
|
|
265
|
+
constructor(message: string, options?: {
|
|
266
|
+
cause?: unknown;
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Thrown when an internal `@noble/curves` operation raises an unexpected
|
|
271
|
+
* error (e.g. invalid point encoding) on a code path where the library's
|
|
272
|
+
* contract is to throw rather than return `false`. The verification path's
|
|
273
|
+
* silent-`false` returns for malformed `R` and out-of-range `s` are
|
|
274
|
+
* deliberate exceptions to this rule (see design.md, Requirements 4.7, 4.8).
|
|
275
|
+
*
|
|
276
|
+
* `.code` is fixed to `'CURVE_ERROR'`. The optional `cause` carries the
|
|
277
|
+
* underlying noble error for diagnostics.
|
|
278
|
+
*/
|
|
279
|
+
declare class CryptoError extends Error {
|
|
280
|
+
/** Class name; fixed for all instances. */
|
|
281
|
+
readonly name = "CryptoError";
|
|
282
|
+
/** Stable, machine-readable identifier; fixed for this class. */
|
|
283
|
+
readonly code: ErrorCode;
|
|
284
|
+
/**
|
|
285
|
+
* @param message Human-readable description; not part of the stable API.
|
|
286
|
+
* @param options Optional bag carrying the underlying `cause`.
|
|
287
|
+
*/
|
|
288
|
+
constructor(message: string, options?: {
|
|
289
|
+
cause?: unknown;
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export { CryptoError, type ErrorCode, InvalidInputError, RandomnessError, computeProof, generateChallenge, generateKeyPair, verifyProof };
|