capjs-core 0.1.0 → 0.1.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capjs-core",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Stateless server-side challenge generator and verifier for Cap, a lightweight, modern open-source CAPTCHA alternative. No storage, no filesystem - bring your own.",
5
5
  "keywords": [
6
6
  "anti-abuse",
@@ -42,6 +42,7 @@
42
42
  "src/instrumentation.js",
43
43
  "src/prng.js",
44
44
  "src/crypto.js",
45
+ "src/rsw.js",
45
46
  "README.md",
46
47
  "LICENSE"
47
48
  ],
package/src/index.d.ts CHANGED
@@ -19,7 +19,36 @@ export interface GeneratedInstrumentation {
19
19
  instrumentation: string;
20
20
  }
21
21
 
22
+ export type ProtocolName = "sha256-pow" | "rsw" | "instrumentation";
23
+
24
+ export interface RswKeypair {
25
+ N: bigint;
26
+ p: bigint;
27
+ q: bigint;
28
+ bits?: number;
29
+ }
30
+
31
+ export interface SerializedRswKeypair {
32
+ N: string;
33
+ p: string;
34
+ q: string;
35
+ bits?: number | null;
36
+ }
37
+
38
+ export interface RswMinter {
39
+ N: bigint;
40
+ t: number;
41
+ g: bigint;
42
+ h: bigint;
43
+ modulusBytes: number;
44
+ N_hex: string;
45
+ g_hex: string;
46
+ h_hex: string;
47
+ mint(): { x_hex: string; y_hex: string };
48
+ }
49
+
22
50
  export interface GenerateChallengeOptions {
51
+ // ── format-1 (default) ──
23
52
  challengeCount?: number;
24
53
  challengeSize?: number;
25
54
  challengeDifficulty?: number;
@@ -30,24 +59,64 @@ export interface GenerateChallengeOptions {
30
59
  instrumentationGenerator?: (
31
60
  opts: InstrumentationOptions,
32
61
  ) => Promise<GeneratedInstrumentation>;
62
+
63
+ // ── format-2 opt-in ──
64
+ format?: 2;
65
+ protocols?: ProtocolName[];
66
+ /** RSW keypair, either as raw BigInts (from `generateRswKeypair`) or as a
67
+ * serialized JSON-friendly form (from `serializeRswKeypair`). cap-core
68
+ * builds the minter on first call and memoizes it on this object. */
69
+ keypair?: RswKeypair | SerializedRswKeypair;
70
+ /** Number of sequential squarings the client must perform. Default 75_000. */
71
+ t?: number;
72
+ /** Alternative to `keypair`: pass a pre-built minter (advanced). */
73
+ rsw?: RswMinter;
33
74
  }
34
75
 
35
- export interface ChallengeResult {
76
+ export interface Format2ChallengeEntry {
77
+ protocol: ProtocolName;
78
+ payload: Record<string, unknown>;
79
+ }
80
+
81
+ export interface Format1ChallengeResult {
36
82
  challenge: ChallengeSpec;
37
83
  token: string;
38
84
  expires: number;
39
85
  instrumentation?: string;
40
86
  }
41
87
 
88
+ export interface Format2ChallengeResult {
89
+ token: string;
90
+ format: 2;
91
+ challenges: Format2ChallengeEntry[];
92
+ expires: number;
93
+ }
94
+
95
+ export type ChallengeResult = Format1ChallengeResult | Format2ChallengeResult;
96
+
42
97
  export interface InstrumentationPayload {
43
98
  i: string;
44
99
  state: Record<string, number>;
45
100
  ts?: number;
46
101
  }
47
102
 
103
+ export interface Format2SolutionShaPow { nonce: number | string }
104
+ export interface Format2SolutionRsw { y: string }
105
+ export interface Format2SolutionInstrumentation {
106
+ instr?: InstrumentationPayload;
107
+ blocked?: boolean;
108
+ timeout?: boolean;
109
+ }
110
+ export type Format2Solution =
111
+ | Format2SolutionShaPow
112
+ | Format2SolutionRsw
113
+ | Format2SolutionInstrumentation;
114
+
48
115
  export interface ValidateChallengeBody {
49
116
  token: string;
50
- solutions: number[];
117
+ /** Format-1: an array of nonces. Format-2: an array of protocol-specific
118
+ * solution objects, in the same order as `challenges`. */
119
+ solutions: number[] | Format2Solution[];
51
120
  instr?: InstrumentationPayload;
52
121
  instr_blocked?: boolean;
53
122
  instr_timeout?: boolean;
@@ -97,3 +166,21 @@ export function validateChallenge(
97
166
  body: ValidateChallengeBody,
98
167
  opts?: ValidateChallengeOptions,
99
168
  ): Promise<ValidateChallengeResult>;
169
+
170
+ // ─── RSW (format-2) helpers ────────────────────────────────────────────────
171
+ /** Generates a fresh 2048-bit-by-default RSA-style modulus + factors. Expensive
172
+ * (~700 ms to several seconds depending on prime-roll luck). Generate once
173
+ * at boot and persist the result. */
174
+ export function generateRswKeypair(bits?: number): RswKeypair;
175
+
176
+ /** JSON-friendly form, safe to write to disk or env. */
177
+ export function serializeRswKeypair(kp: RswKeypair): SerializedRswKeypair;
178
+ export function deserializeRswKeypair(s: SerializedRswKeypair): RswKeypair;
179
+
180
+ /** Build a minter for a specific squaring count `t`. Memoized when used via
181
+ * `generateChallenge({ keypair, t })`; this direct API is for callers who
182
+ * want full control. */
183
+ export function buildRswMinter(
184
+ args: { N: bigint; p: bigint; q: bigint; t: number; g?: bigint },
185
+ opts?: { bits?: number },
186
+ ): RswMinter;
package/src/index.js CHANGED
@@ -10,11 +10,58 @@ import {
10
10
  sha256Bytes,
11
11
  sha256Hex,
12
12
  } from "./crypto.js";
13
- import {
14
- generateInstrumentation,
15
- verifyInstrumentationResult,
16
- } from "./instrumentation.js";
13
+ import { generateInstrumentation, verifyInstrumentationResult } from "./instrumentation.js";
17
14
  import { fnv1a, fnv1aResume, prngFromHash } from "./prng.js";
15
+ import {
16
+ buildRswMinter as _buildRswMinter,
17
+ deserializeRswKeypair,
18
+ verifyRswSolution,
19
+ } from "./rsw.js";
20
+
21
+ export {
22
+ buildRswMinter,
23
+ generateRswKeypair,
24
+ serializeRswKeypair,
25
+ deserializeRswKeypair,
26
+ } from "./rsw.js";
27
+
28
+ const DEFAULT_RSW_T = 75_000;
29
+
30
+ // Cache the minter on the keypair object identity, keyed by t. Callers who
31
+ // pass the same `keypair` object back across requests get zero per-call
32
+ // overhead beyond the mint itself.
33
+ const _minterCache = new WeakMap(); // keypair → Map<t, minter>
34
+
35
+ function resolveRswMinter(opts) {
36
+ // explicit minter takes precedence (existing API)
37
+ if (opts.rsw && typeof opts.rsw.mint === "function") return opts.rsw;
38
+
39
+ if (!opts.keypair) {
40
+ throw new Error(
41
+ "[capjs-core] format-2 with 'rsw' requires opts.keypair (from generateRswKeypair) or opts.rsw (from buildRswMinter).",
42
+ );
43
+ }
44
+
45
+ // Normalize: serialized (strings) → BigInt
46
+ const kp = typeof opts.keypair.N === "string"
47
+ ? deserializeRswKeypair(opts.keypair)
48
+ : opts.keypair;
49
+
50
+ // Memoize {keypair, t} → minter. Reuses the precomputed h across calls so
51
+ // the first request after boot is the only one that pays the ~5 ms setup.
52
+ const t = opts.t ?? DEFAULT_RSW_T;
53
+ let byT = _minterCache.get(opts.keypair);
54
+ if (!byT) {
55
+ byT = new Map();
56
+ _minterCache.set(opts.keypair, byT);
57
+ }
58
+ let minter = byT.get(t);
59
+ if (!minter) {
60
+ minter = _buildRswMinter({ ...kp, t });
61
+ byT.set(t, minter);
62
+ }
63
+ return minter;
64
+ }
18
65
 
19
66
  const DEFAULT_CHALLENGE_COUNT = 50;
20
67
  const DEFAULT_CHALLENGE_SIZE = 32;
@@ -35,10 +82,7 @@ function assertSecret(secret) {
35
82
  if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
36
83
  throw new Error("[capjs-core] secret must be a string or Buffer");
37
84
  }
38
- const len =
39
- typeof secret === "string"
40
- ? Buffer.byteLength(secret, "utf8")
41
- : secret.length;
85
+ const len = typeof secret === "string" ? Buffer.byteLength(secret, "utf8") : secret.length;
42
86
  if (len < 16) {
43
87
  throw new Error("[capjs-core] secret must be at least 16 bytes for security");
44
88
  }
@@ -47,6 +91,10 @@ function assertSecret(secret) {
47
91
  export async function generateChallenge(secret, opts = {}) {
48
92
  assertSecret(secret);
49
93
 
94
+ if (opts.format === 2) {
95
+ return generateChallengeV2(secret, opts);
96
+ }
97
+
50
98
  const c = opts.challengeCount ?? DEFAULT_CHALLENGE_COUNT;
51
99
  const s = opts.challengeSize ?? DEFAULT_CHALLENGE_SIZE;
52
100
  const d = opts.challengeDifficulty ?? DEFAULT_CHALLENGE_DIFFICULTY;
@@ -82,8 +130,7 @@ export async function generateChallenge(secret, opts = {}) {
82
130
 
83
131
  let instrumentation;
84
132
  if (opts.instrumentation) {
85
- const instrOpts =
86
- typeof opts.instrumentation === "object" ? opts.instrumentation : {};
133
+ const instrOpts = typeof opts.instrumentation === "object" ? opts.instrumentation : {};
87
134
  const generator = opts.instrumentationGenerator || generateInstrumentation;
88
135
  const instr = await generator({ ...instrOpts, ttlMs });
89
136
  payload.ei = encryptGcm(
@@ -113,14 +160,17 @@ export async function validateChallenge(secret, body, opts = {}) {
113
160
  assertSecret(secret);
114
161
 
115
162
  if (!body || typeof body !== "object") return fail("invalid_body");
116
- if (!body.token || typeof body.token !== "string")
117
- return fail("missing_token");
163
+ if (!body.token || typeof body.token !== "string") return fail("missing_token");
118
164
  const solutions = body.solutions;
119
165
  if (!Array.isArray(solutions)) return fail("missing_solutions");
120
166
 
121
167
  const payload = jwtVerify(body.token, secret);
122
168
  if (!payload) return fail("invalid_token");
123
169
 
170
+ if (payload.f === 2) {
171
+ return validateChallengeV2(secret, body, payload, opts);
172
+ }
173
+
124
174
  if (opts.scope && payload.sk !== opts.scope) return fail("scope_mismatch");
125
175
 
126
176
  if (!payload.exp || payload.exp < Date.now()) return fail("expired");
@@ -177,8 +227,7 @@ export async function validateChallenge(secret, body, opts = {}) {
177
227
  return fail("instr_timeout", { instr_error: true });
178
228
  } else if (body.instr) {
179
229
  const r = verifyInstrumentationResult(instrMeta, body.instr);
180
- if (!r.valid)
181
- return fail(r.reason || "instr_failed", { instr_error: true });
230
+ if (!r.valid) return fail(r.reason || "instr_failed", { instr_error: true });
182
231
  } else {
183
232
  return fail("instr_missing", { instr_error: true });
184
233
  }
@@ -227,3 +276,174 @@ export async function validateChallenge(secret, body, opts = {}) {
227
276
  iat: payload.iat,
228
277
  };
229
278
  }
279
+
280
+ async function generateChallengeV2(secret, opts) {
281
+ const protocols =
282
+ Array.isArray(opts.protocols) && opts.protocols.length ? opts.protocols : ["rsw"];
283
+ const ttlMs = opts.expiresMs ?? DEFAULT_CHALLENGE_TTL_MS;
284
+ const now = Date.now();
285
+ const exp = now + ttlMs;
286
+
287
+ const challenges = [];
288
+ const expected = [];
289
+
290
+ for (const proto of protocols) {
291
+ if (proto === "sha256-pow") {
292
+ const c = opts.challengeCount ?? DEFAULT_CHALLENGE_COUNT;
293
+ const s = opts.challengeSize ?? DEFAULT_CHALLENGE_SIZE;
294
+ const d = opts.challengeDifficulty ?? DEFAULT_CHALLENGE_DIFFICULTY;
295
+ if (
296
+ !Number.isInteger(c) ||
297
+ c < 1 ||
298
+ c > MAX_CHALLENGE_COUNT ||
299
+ !Number.isInteger(s) ||
300
+ s < 1 ||
301
+ s > MAX_CHALLENGE_SIZE ||
302
+ !Number.isInteger(d) ||
303
+ d < 1 ||
304
+ d > MAX_CHALLENGE_DIFFICULTY
305
+ ) {
306
+ throw new Error(
307
+ `[capjs-core] invalid params: c≤${MAX_CHALLENGE_COUNT}, s≤${MAX_CHALLENGE_SIZE}, d∈[1,${MAX_CHALLENGE_DIFFICULTY}]`,
308
+ );
309
+ }
310
+ const target = "0".repeat(d);
311
+ for (let i = 0; i < c; i++) {
312
+ const salt = randomHex(s);
313
+ challenges.push({ protocol: "sha256-pow", payload: { salt, target } });
314
+ expected.push({ protocol: "sha256-pow", salt, target });
315
+ }
316
+ } else if (proto === "rsw") {
317
+ const minter = resolveRswMinter(opts);
318
+ const { x_hex, y_hex } = minter.mint();
319
+ challenges.push({
320
+ protocol: "rsw",
321
+ payload: { N: minter.N_hex, x: x_hex, t: minter.t },
322
+ });
323
+ expected.push({ protocol: "rsw", y: y_hex });
324
+ } else if (proto === "instrumentation") {
325
+ const instrOpts =
326
+ typeof opts.instrumentation === "object" && opts.instrumentation !== null
327
+ ? opts.instrumentation
328
+ : {};
329
+ const generator = opts.instrumentationGenerator || generateInstrumentation;
330
+ const instr = await generator({ ...instrOpts, ttlMs });
331
+ challenges.push({ protocol: "instrumentation", payload: { blob: instr.instrumentation } });
332
+ expected.push({
333
+ protocol: "instrumentation",
334
+ instrMeta: {
335
+ id: instr.id,
336
+ expectedVals: instr.expectedVals,
337
+ vars: instr.vars,
338
+ blockAutomatedBrowsers: instr.blockAutomatedBrowsers,
339
+ expires: exp,
340
+ },
341
+ });
342
+ } else {
343
+ throw new Error(`[capjs-core] unknown protocol '${proto}'`);
344
+ }
345
+ }
346
+
347
+ const tokenPayload = {
348
+ f: 2,
349
+ n: randomHex(16),
350
+ exp,
351
+ iat: now,
352
+ ev: encryptGcm({ expected }, secret, "cap:fmt2-v1"),
353
+ };
354
+ if (opts.scope) tokenPayload.sk = String(opts.scope);
355
+ if (opts.extra && typeof opts.extra === "object") tokenPayload.x = opts.extra;
356
+
357
+ const token = jwtSign(tokenPayload, secret);
358
+ return { token, format: 2, challenges, expires: exp };
359
+ }
360
+
361
+ async function validateChallengeV2(secret, body, payload, opts = {}) {
362
+ if (opts.scope && payload.sk !== opts.scope) return fail("scope_mismatch");
363
+ if (!payload.exp || payload.exp < Date.now()) return fail("expired");
364
+
365
+ const decrypted = decryptGcm(payload.ev, secret, "cap:fmt2-v1");
366
+ if (!decrypted || !Array.isArray(decrypted.expected)) return fail("invalid_token");
367
+ const expected = decrypted.expected;
368
+ const solutions = body.solutions;
369
+
370
+ if (solutions.length !== expected.length) return fail("invalid_solutions");
371
+
372
+ for (let i = 0; i < expected.length; i++) {
373
+ const e = expected[i];
374
+ const s = solutions[i];
375
+ if (!s || typeof s !== "object") return fail("invalid_solution");
376
+
377
+ if (e.protocol === "sha256-pow") {
378
+ const nonce = s.nonce;
379
+ if (typeof nonce !== "number" && typeof nonce !== "string") return fail("invalid_solution");
380
+ const hash = sha256Bytes(e.salt + nonce);
381
+ if (!powMatchesPrefix(hash, parseHexPrefix(e.target))) return fail("invalid_solution");
382
+ } else if (e.protocol === "rsw") {
383
+ if (!verifyRswSolution(e.y, s.y)) return fail("invalid_solution");
384
+ } else if (e.protocol === "instrumentation") {
385
+ const meta = e.instrMeta;
386
+ if (!meta) return fail("instr_corrupted", { instr_error: true });
387
+ if (meta.expires && Date.now() > meta.expires) {
388
+ return fail("instr_expired", { instr_error: true });
389
+ }
390
+ if (s.blocked === true) {
391
+ if (meta.blockAutomatedBrowsers) {
392
+ return fail("instr_automated_browser", { instr_error: true });
393
+ }
394
+ } else if (s.timeout === true) {
395
+ return fail("instr_timeout", { instr_error: true });
396
+ } else if (s.instr) {
397
+ const r = verifyInstrumentationResult(meta, s.instr);
398
+ if (!r.valid) return fail(r.reason || "instr_failed", { instr_error: true });
399
+ } else {
400
+ return fail("instr_missing", { instr_error: true });
401
+ }
402
+ } else {
403
+ return fail("invalid_solution");
404
+ }
405
+ }
406
+
407
+ if (typeof opts.consumeNonce === "function") {
408
+ const sig = jwtSigHex(body.token);
409
+ if (!sig) return fail("invalid_token");
410
+ const ttlMs = Math.max(1, payload.exp - Date.now());
411
+ let claimed;
412
+ try {
413
+ claimed = await opts.consumeNonce(sig, ttlMs);
414
+ } catch (err) {
415
+ return fail("nonce_store_error", { error: String(err?.message || err) });
416
+ }
417
+ if (!claimed) return fail("already_redeemed");
418
+ }
419
+
420
+ const tokenTtlMs = opts.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
421
+ const tokenExpires = Date.now() + tokenTtlMs;
422
+
423
+ if (typeof opts.signToken === "function") {
424
+ const signed = await opts.signToken({
425
+ scope: payload.sk ?? null,
426
+ expires: tokenExpires,
427
+ iat: payload.iat,
428
+ });
429
+ return {
430
+ success: true,
431
+ token: signed,
432
+ expires: tokenExpires,
433
+ scope: payload.sk ?? null,
434
+ iat: payload.iat,
435
+ };
436
+ }
437
+
438
+ const id = randomHex(8);
439
+ const verToken = randomHex(15);
440
+ const tokenHash = sha256Hex(verToken);
441
+ return {
442
+ success: true,
443
+ token: `${id}:${verToken}`,
444
+ tokenKey: `${id}:${tokenHash}`,
445
+ expires: tokenExpires,
446
+ scope: payload.sk ?? null,
447
+ iat: payload.iat,
448
+ };
449
+ }
package/src/rsw.js ADDED
@@ -0,0 +1,167 @@
1
+ import { randomBytes } from "node:crypto";
2
+
3
+ function bigintFromBuf(buf) {
4
+ let x = 0n;
5
+ for (const b of buf) x = (x << 8n) | BigInt(b);
6
+ return x;
7
+ }
8
+
9
+ function modpow(b, e, m) {
10
+ let r = 1n;
11
+ b %= m;
12
+ if (b < 0n) b += m;
13
+ while (e > 0n) {
14
+ if (e & 1n) r = (r * b) % m;
15
+ e >>= 1n;
16
+ b = (b * b) % m;
17
+ }
18
+ return r;
19
+ }
20
+
21
+ function modinv(a, m) {
22
+ let [old_r, r] = [a % m, m];
23
+ let [old_s, s] = [1n, 0n];
24
+ if (old_r < 0n) old_r += m;
25
+ while (r !== 0n) {
26
+ const q = old_r / r;
27
+ [old_r, r] = [r, old_r - q * r];
28
+ [old_s, s] = [s, old_s - q * s];
29
+ }
30
+ return ((old_s % m) + m) % m;
31
+ }
32
+
33
+ function isProbablePrime(n, k = 4) {
34
+ if (n < 2n) return false;
35
+ for (const p of [2n, 3n, 5n, 7n, 11n, 13n, 17n, 19n, 23n, 29n, 31n]) {
36
+ if (n === p) return true;
37
+ if (n % p === 0n) return false;
38
+ }
39
+ let d = n - 1n;
40
+ let r = 0n;
41
+ while ((d & 1n) === 0n) {
42
+ d >>= 1n;
43
+ r++;
44
+ }
45
+ const bits = n.toString(2).length;
46
+ const byteLen = (bits + 7) >> 3;
47
+ outer: for (let i = 0; i < k; i++) {
48
+ let a = 0n;
49
+ do {
50
+ a = bigintFromBuf(randomBytes(byteLen)) % (n - 3n);
51
+ } while (a < 2n);
52
+ let x = modpow(a, d, n);
53
+ if (x === 1n || x === n - 1n) continue;
54
+ for (let j = 0n; j < r - 1n; j++) {
55
+ x = (x * x) % n;
56
+ if (x === n - 1n) continue outer;
57
+ }
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+
63
+ function randomPrime(bits) {
64
+ const byteLen = (bits + 7) >> 3;
65
+ for (;;) {
66
+ let x = bigintFromBuf(randomBytes(byteLen));
67
+ x |= 1n;
68
+ x |= 1n << BigInt(bits - 1);
69
+ x |= 1n << BigInt(bits - 2);
70
+ x &= (1n << BigInt(bits)) - 1n;
71
+ if (isProbablePrime(x, 4)) return x;
72
+ }
73
+ }
74
+
75
+ function hexOf(bi, byteLen) {
76
+ let s = bi.toString(16);
77
+ if (byteLen != null) {
78
+ const target = byteLen * 2;
79
+ if (s.length < target) s = "0".repeat(target - s.length) + s;
80
+ }
81
+ return s;
82
+ }
83
+
84
+ // generation is expensive (~700 ms for 2048-bit on a modern CPU); persist the result.
85
+ export function generateRswKeypair(bits = 2048) {
86
+ if (bits % 2 !== 0) throw new Error("rsw bits must be even");
87
+ const p = randomPrime(bits >> 1);
88
+ let q = randomPrime(bits >> 1);
89
+ while (q === p) q = randomPrime(bits >> 1);
90
+ const N = p * q;
91
+ return { N, p, q, bits };
92
+ }
93
+
94
+ export function serializeRswKeypair(kp) {
95
+ return { N: kp.N.toString(), p: kp.p.toString(), q: kp.q.toString(), bits: kp.bits ?? null };
96
+ }
97
+ export function deserializeRswKeypair(s) {
98
+ if (!s || typeof s !== "object") throw new Error("invalid serialized rsw keypair");
99
+ const N = typeof s.N === "string" ? BigInt(s.N) : s.N;
100
+ const p = typeof s.p === "string" ? BigInt(s.p) : s.p;
101
+ const q = typeof s.q === "string" ? BigInt(s.q) : s.q;
102
+ if (typeof N !== "bigint" || typeof p !== "bigint" || typeof q !== "bigint") {
103
+ throw new Error("rsw keypair fields must be bigint or string-encoded bigint");
104
+ }
105
+ return { N, p, q, bits: s.bits ?? N.toString(2).length };
106
+ }
107
+
108
+ export function buildRswMinter({ N, p, q, t, g }, opts = {}) {
109
+ if (!N || !p || !q || !t) throw new Error("rsw minter needs {N, p, q, t}");
110
+ const bits = opts.bits ?? N.toString(2).length;
111
+ const modulusBytes = Math.ceil(bits / 8);
112
+ const G = g ?? (bigintFromBuf(randomBytes(modulusBytes)) % (N - 3n)) + 2n;
113
+
114
+ const pm1 = p - 1n;
115
+ const qm1 = q - 1n;
116
+ const e_p = modpow(2n, BigInt(t), pm1);
117
+ const e_q = modpow(2n, BigInt(t), qm1);
118
+
119
+ const hp = modpow(G % p, e_p, p);
120
+ const hq = modpow(G % q, e_q, q);
121
+ const qinv_p = modinv(q % p, p);
122
+ const crtCombine = (ap, aq) => {
123
+ let s = (((ap - aq) % p) + p) % p;
124
+ s = (s * qinv_p) % p;
125
+ return aq + q * s;
126
+ };
127
+ const h = crtCombine(hp, hq);
128
+
129
+ const gp = G % p;
130
+ const gq = G % q;
131
+
132
+ function mintFromR(r) {
133
+ const rp = r % pm1;
134
+ const rq = r % qm1;
135
+ const xp = modpow(gp, rp, p);
136
+ const xq = modpow(gq, rq, q);
137
+ const yp = modpow(hp, rp, p);
138
+ const yq = modpow(hq, rq, q);
139
+ return { x: crtCombine(xp, xq), y: crtCombine(yp, yq) };
140
+ }
141
+
142
+ return {
143
+ N,
144
+ t,
145
+ g: G,
146
+ h,
147
+ modulusBytes,
148
+ N_hex: hexOf(N, modulusBytes),
149
+ g_hex: hexOf(G, modulusBytes),
150
+ h_hex: hexOf(h, modulusBytes),
151
+
152
+ mint() {
153
+ const r = bigintFromBuf(randomBytes(32));
154
+ const { x, y } = mintFromR(r);
155
+ return {
156
+ x_hex: hexOf(x, modulusBytes),
157
+ y_hex: hexOf(y, modulusBytes),
158
+ };
159
+ },
160
+ };
161
+ }
162
+
163
+ export function verifyRswSolution(expectedYHex, claimedYHex) {
164
+ if (typeof claimedYHex !== "string" || typeof expectedYHex !== "string") return false;
165
+ const norm = (s) => s.replace(/^0x/, "").toLowerCase().replace(/^0+/, "");
166
+ return norm(expectedYHex) === norm(claimedYHex);
167
+ }