capjs-core 0.1.0 → 0.1.2
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 +2 -1
- package/src/index.d.ts +89 -2
- package/src/index.js +227 -14
- package/src/instrumentation.js +27 -4
- package/src/rsw.js +167 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capjs-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
|
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
|
-
|
|
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,51 @@ 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
|
+
const _minterCache = new WeakMap();
|
|
31
|
+
|
|
32
|
+
function resolveRswMinter(opts) {
|
|
33
|
+
if (opts.rsw && typeof opts.rsw.mint === "function") return opts.rsw;
|
|
34
|
+
|
|
35
|
+
if (!opts.keypair) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"[capjs-core] format-2 with 'rsw' requires opts.keypair (from generateRswKeypair) or opts.rsw (from buildRswMinter).",
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const kp = typeof opts.keypair.N === "string"
|
|
42
|
+
? deserializeRswKeypair(opts.keypair)
|
|
43
|
+
: opts.keypair;
|
|
44
|
+
|
|
45
|
+
const t = opts.t ?? DEFAULT_RSW_T;
|
|
46
|
+
let byT = _minterCache.get(opts.keypair);
|
|
47
|
+
if (!byT) {
|
|
48
|
+
byT = new Map();
|
|
49
|
+
_minterCache.set(opts.keypair, byT);
|
|
50
|
+
}
|
|
51
|
+
let minter = byT.get(t);
|
|
52
|
+
if (!minter) {
|
|
53
|
+
minter = _buildRswMinter({ ...kp, t });
|
|
54
|
+
byT.set(t, minter);
|
|
55
|
+
}
|
|
56
|
+
return minter;
|
|
57
|
+
}
|
|
18
58
|
|
|
19
59
|
const DEFAULT_CHALLENGE_COUNT = 50;
|
|
20
60
|
const DEFAULT_CHALLENGE_SIZE = 32;
|
|
@@ -35,10 +75,7 @@ function assertSecret(secret) {
|
|
|
35
75
|
if (typeof secret !== "string" && !Buffer.isBuffer(secret)) {
|
|
36
76
|
throw new Error("[capjs-core] secret must be a string or Buffer");
|
|
37
77
|
}
|
|
38
|
-
const len =
|
|
39
|
-
typeof secret === "string"
|
|
40
|
-
? Buffer.byteLength(secret, "utf8")
|
|
41
|
-
: secret.length;
|
|
78
|
+
const len = typeof secret === "string" ? Buffer.byteLength(secret, "utf8") : secret.length;
|
|
42
79
|
if (len < 16) {
|
|
43
80
|
throw new Error("[capjs-core] secret must be at least 16 bytes for security");
|
|
44
81
|
}
|
|
@@ -47,6 +84,10 @@ function assertSecret(secret) {
|
|
|
47
84
|
export async function generateChallenge(secret, opts = {}) {
|
|
48
85
|
assertSecret(secret);
|
|
49
86
|
|
|
87
|
+
if (opts.format === 2) {
|
|
88
|
+
return generateChallengeV2(secret, opts);
|
|
89
|
+
}
|
|
90
|
+
|
|
50
91
|
const c = opts.challengeCount ?? DEFAULT_CHALLENGE_COUNT;
|
|
51
92
|
const s = opts.challengeSize ?? DEFAULT_CHALLENGE_SIZE;
|
|
52
93
|
const d = opts.challengeDifficulty ?? DEFAULT_CHALLENGE_DIFFICULTY;
|
|
@@ -82,8 +123,7 @@ export async function generateChallenge(secret, opts = {}) {
|
|
|
82
123
|
|
|
83
124
|
let instrumentation;
|
|
84
125
|
if (opts.instrumentation) {
|
|
85
|
-
const instrOpts =
|
|
86
|
-
typeof opts.instrumentation === "object" ? opts.instrumentation : {};
|
|
126
|
+
const instrOpts = typeof opts.instrumentation === "object" ? opts.instrumentation : {};
|
|
87
127
|
const generator = opts.instrumentationGenerator || generateInstrumentation;
|
|
88
128
|
const instr = await generator({ ...instrOpts, ttlMs });
|
|
89
129
|
payload.ei = encryptGcm(
|
|
@@ -113,14 +153,17 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
113
153
|
assertSecret(secret);
|
|
114
154
|
|
|
115
155
|
if (!body || typeof body !== "object") return fail("invalid_body");
|
|
116
|
-
if (!body.token || typeof body.token !== "string")
|
|
117
|
-
return fail("missing_token");
|
|
156
|
+
if (!body.token || typeof body.token !== "string") return fail("missing_token");
|
|
118
157
|
const solutions = body.solutions;
|
|
119
158
|
if (!Array.isArray(solutions)) return fail("missing_solutions");
|
|
120
159
|
|
|
121
160
|
const payload = jwtVerify(body.token, secret);
|
|
122
161
|
if (!payload) return fail("invalid_token");
|
|
123
162
|
|
|
163
|
+
if (payload.f === 2) {
|
|
164
|
+
return validateChallengeV2(secret, body, payload, opts);
|
|
165
|
+
}
|
|
166
|
+
|
|
124
167
|
if (opts.scope && payload.sk !== opts.scope) return fail("scope_mismatch");
|
|
125
168
|
|
|
126
169
|
if (!payload.exp || payload.exp < Date.now()) return fail("expired");
|
|
@@ -177,8 +220,7 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
177
220
|
return fail("instr_timeout", { instr_error: true });
|
|
178
221
|
} else if (body.instr) {
|
|
179
222
|
const r = verifyInstrumentationResult(instrMeta, body.instr);
|
|
180
|
-
if (!r.valid)
|
|
181
|
-
return fail(r.reason || "instr_failed", { instr_error: true });
|
|
223
|
+
if (!r.valid) return fail(r.reason || "instr_failed", { instr_error: true });
|
|
182
224
|
} else {
|
|
183
225
|
return fail("instr_missing", { instr_error: true });
|
|
184
226
|
}
|
|
@@ -227,3 +269,174 @@ export async function validateChallenge(secret, body, opts = {}) {
|
|
|
227
269
|
iat: payload.iat,
|
|
228
270
|
};
|
|
229
271
|
}
|
|
272
|
+
|
|
273
|
+
async function generateChallengeV2(secret, opts) {
|
|
274
|
+
const protocols =
|
|
275
|
+
Array.isArray(opts.protocols) && opts.protocols.length ? opts.protocols : ["rsw"];
|
|
276
|
+
const ttlMs = opts.expiresMs ?? DEFAULT_CHALLENGE_TTL_MS;
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
const exp = now + ttlMs;
|
|
279
|
+
|
|
280
|
+
const challenges = [];
|
|
281
|
+
const expected = [];
|
|
282
|
+
|
|
283
|
+
for (const proto of protocols) {
|
|
284
|
+
if (proto === "sha256-pow") {
|
|
285
|
+
const c = opts.challengeCount ?? DEFAULT_CHALLENGE_COUNT;
|
|
286
|
+
const s = opts.challengeSize ?? DEFAULT_CHALLENGE_SIZE;
|
|
287
|
+
const d = opts.challengeDifficulty ?? DEFAULT_CHALLENGE_DIFFICULTY;
|
|
288
|
+
if (
|
|
289
|
+
!Number.isInteger(c) ||
|
|
290
|
+
c < 1 ||
|
|
291
|
+
c > MAX_CHALLENGE_COUNT ||
|
|
292
|
+
!Number.isInteger(s) ||
|
|
293
|
+
s < 1 ||
|
|
294
|
+
s > MAX_CHALLENGE_SIZE ||
|
|
295
|
+
!Number.isInteger(d) ||
|
|
296
|
+
d < 1 ||
|
|
297
|
+
d > MAX_CHALLENGE_DIFFICULTY
|
|
298
|
+
) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`[capjs-core] invalid params: c≤${MAX_CHALLENGE_COUNT}, s≤${MAX_CHALLENGE_SIZE}, d∈[1,${MAX_CHALLENGE_DIFFICULTY}]`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const target = "0".repeat(d);
|
|
304
|
+
for (let i = 0; i < c; i++) {
|
|
305
|
+
const salt = randomHex(s);
|
|
306
|
+
challenges.push({ protocol: "sha256-pow", payload: { salt, target } });
|
|
307
|
+
expected.push({ protocol: "sha256-pow", salt, target });
|
|
308
|
+
}
|
|
309
|
+
} else if (proto === "rsw") {
|
|
310
|
+
const minter = resolveRswMinter(opts);
|
|
311
|
+
const { x_hex, y_hex } = minter.mint();
|
|
312
|
+
challenges.push({
|
|
313
|
+
protocol: "rsw",
|
|
314
|
+
payload: { N: minter.N_hex, x: x_hex, t: minter.t },
|
|
315
|
+
});
|
|
316
|
+
expected.push({ protocol: "rsw", y: y_hex });
|
|
317
|
+
} else if (proto === "instrumentation") {
|
|
318
|
+
const instrOpts =
|
|
319
|
+
typeof opts.instrumentation === "object" && opts.instrumentation !== null
|
|
320
|
+
? opts.instrumentation
|
|
321
|
+
: {};
|
|
322
|
+
const generator = opts.instrumentationGenerator || generateInstrumentation;
|
|
323
|
+
const instr = await generator({ ...instrOpts, ttlMs });
|
|
324
|
+
challenges.push({ protocol: "instrumentation", payload: { blob: instr.instrumentation } });
|
|
325
|
+
expected.push({
|
|
326
|
+
protocol: "instrumentation",
|
|
327
|
+
instrMeta: {
|
|
328
|
+
id: instr.id,
|
|
329
|
+
expectedVals: instr.expectedVals,
|
|
330
|
+
vars: instr.vars,
|
|
331
|
+
blockAutomatedBrowsers: instr.blockAutomatedBrowsers,
|
|
332
|
+
expires: exp,
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
} else {
|
|
336
|
+
throw new Error(`[capjs-core] unknown protocol '${proto}'`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const tokenPayload = {
|
|
341
|
+
f: 2,
|
|
342
|
+
n: randomHex(16),
|
|
343
|
+
exp,
|
|
344
|
+
iat: now,
|
|
345
|
+
ev: encryptGcm({ expected }, secret, "cap:fmt2-v1"),
|
|
346
|
+
};
|
|
347
|
+
if (opts.scope) tokenPayload.sk = String(opts.scope);
|
|
348
|
+
if (opts.extra && typeof opts.extra === "object") tokenPayload.x = opts.extra;
|
|
349
|
+
|
|
350
|
+
const token = jwtSign(tokenPayload, secret);
|
|
351
|
+
return { token, format: 2, challenges, expires: exp };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function validateChallengeV2(secret, body, payload, opts = {}) {
|
|
355
|
+
if (opts.scope && payload.sk !== opts.scope) return fail("scope_mismatch");
|
|
356
|
+
if (!payload.exp || payload.exp < Date.now()) return fail("expired");
|
|
357
|
+
|
|
358
|
+
const decrypted = decryptGcm(payload.ev, secret, "cap:fmt2-v1");
|
|
359
|
+
if (!decrypted || !Array.isArray(decrypted.expected)) return fail("invalid_token");
|
|
360
|
+
const expected = decrypted.expected;
|
|
361
|
+
const solutions = body.solutions;
|
|
362
|
+
|
|
363
|
+
if (solutions.length !== expected.length) return fail("invalid_solutions");
|
|
364
|
+
|
|
365
|
+
for (let i = 0; i < expected.length; i++) {
|
|
366
|
+
const e = expected[i];
|
|
367
|
+
const s = solutions[i];
|
|
368
|
+
if (!s || typeof s !== "object") return fail("invalid_solution");
|
|
369
|
+
|
|
370
|
+
if (e.protocol === "sha256-pow") {
|
|
371
|
+
const nonce = s.nonce;
|
|
372
|
+
if (typeof nonce !== "number" && typeof nonce !== "string") return fail("invalid_solution");
|
|
373
|
+
const hash = sha256Bytes(e.salt + nonce);
|
|
374
|
+
if (!powMatchesPrefix(hash, parseHexPrefix(e.target))) return fail("invalid_solution");
|
|
375
|
+
} else if (e.protocol === "rsw") {
|
|
376
|
+
if (!verifyRswSolution(e.y, s.y)) return fail("invalid_solution");
|
|
377
|
+
} else if (e.protocol === "instrumentation") {
|
|
378
|
+
const meta = e.instrMeta;
|
|
379
|
+
if (!meta) return fail("instr_corrupted", { instr_error: true });
|
|
380
|
+
if (meta.expires && Date.now() > meta.expires) {
|
|
381
|
+
return fail("instr_expired", { instr_error: true });
|
|
382
|
+
}
|
|
383
|
+
if (s.blocked === true) {
|
|
384
|
+
if (meta.blockAutomatedBrowsers) {
|
|
385
|
+
return fail("instr_automated_browser", { instr_error: true });
|
|
386
|
+
}
|
|
387
|
+
} else if (s.timeout === true) {
|
|
388
|
+
return fail("instr_timeout", { instr_error: true });
|
|
389
|
+
} else if (s.instr) {
|
|
390
|
+
const r = verifyInstrumentationResult(meta, s.instr);
|
|
391
|
+
if (!r.valid) return fail(r.reason || "instr_failed", { instr_error: true });
|
|
392
|
+
} else {
|
|
393
|
+
return fail("instr_missing", { instr_error: true });
|
|
394
|
+
}
|
|
395
|
+
} else {
|
|
396
|
+
return fail("invalid_solution");
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (typeof opts.consumeNonce === "function") {
|
|
401
|
+
const sig = jwtSigHex(body.token);
|
|
402
|
+
if (!sig) return fail("invalid_token");
|
|
403
|
+
const ttlMs = Math.max(1, payload.exp - Date.now());
|
|
404
|
+
let claimed;
|
|
405
|
+
try {
|
|
406
|
+
claimed = await opts.consumeNonce(sig, ttlMs);
|
|
407
|
+
} catch (err) {
|
|
408
|
+
return fail("nonce_store_error", { error: String(err?.message || err) });
|
|
409
|
+
}
|
|
410
|
+
if (!claimed) return fail("already_redeemed");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const tokenTtlMs = opts.tokenTtlMs ?? DEFAULT_TOKEN_TTL_MS;
|
|
414
|
+
const tokenExpires = Date.now() + tokenTtlMs;
|
|
415
|
+
|
|
416
|
+
if (typeof opts.signToken === "function") {
|
|
417
|
+
const signed = await opts.signToken({
|
|
418
|
+
scope: payload.sk ?? null,
|
|
419
|
+
expires: tokenExpires,
|
|
420
|
+
iat: payload.iat,
|
|
421
|
+
});
|
|
422
|
+
return {
|
|
423
|
+
success: true,
|
|
424
|
+
token: signed,
|
|
425
|
+
expires: tokenExpires,
|
|
426
|
+
scope: payload.sk ?? null,
|
|
427
|
+
iat: payload.iat,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const id = randomHex(8);
|
|
432
|
+
const verToken = randomHex(15);
|
|
433
|
+
const tokenHash = sha256Hex(verToken);
|
|
434
|
+
return {
|
|
435
|
+
success: true,
|
|
436
|
+
token: `${id}:${verToken}`,
|
|
437
|
+
tokenKey: `${id}:${tokenHash}`,
|
|
438
|
+
expires: tokenExpires,
|
|
439
|
+
scope: payload.sk ?? null,
|
|
440
|
+
iat: payload.iat,
|
|
441
|
+
};
|
|
442
|
+
}
|
package/src/instrumentation.js
CHANGED
|
@@ -151,6 +151,25 @@ const DOC_PROP_MARKERS = [
|
|
|
151
151
|
"__webdriver_script_func",
|
|
152
152
|
"__webdriver_script_function",
|
|
153
153
|
];
|
|
154
|
+
const NAV_OWN_PROP_MARKERS = [
|
|
155
|
+
"webdriver",
|
|
156
|
+
"userAgent",
|
|
157
|
+
"appVersion",
|
|
158
|
+
"userAgentData",
|
|
159
|
+
"platform",
|
|
160
|
+
"vendor",
|
|
161
|
+
"product",
|
|
162
|
+
"productSub",
|
|
163
|
+
"languages",
|
|
164
|
+
"language",
|
|
165
|
+
"plugins",
|
|
166
|
+
"mimeTypes",
|
|
167
|
+
"hardwareConcurrency",
|
|
168
|
+
"deviceMemory",
|
|
169
|
+
"maxTouchPoints",
|
|
170
|
+
"permissions",
|
|
171
|
+
"connection",
|
|
172
|
+
];
|
|
154
173
|
const ATTR_SUBSTRING_MARKERS = ["selenium", "webdriver", "driver"];
|
|
155
174
|
const STACK_SUBSTRING_MARKERS = ["pptr:", "UtilityScript.", "PhantomJS"];
|
|
156
175
|
const WINDOW_PREFIX_MARKERS = ["puppeteer_", "cdc_", "$cdc_"];
|
|
@@ -179,6 +198,7 @@ function hashWith(seed) {
|
|
|
179
198
|
function buildBlockChecks(b, id, hF, hSet, h) {
|
|
180
199
|
const winHashes = WINDOW_PROP_MARKERS.map(h);
|
|
181
200
|
const docHashes = DOC_PROP_MARKERS.map(h);
|
|
201
|
+
const navOwnHashes = NAV_OWN_PROP_MARKERS.map(h);
|
|
182
202
|
const attrSubHashes = ATTR_SUBSTRING_MARKERS.map(h);
|
|
183
203
|
const stackSubHashes = STACK_SUBSTRING_MARKERS.map(h);
|
|
184
204
|
const winPrefixHashes = WINDOW_PREFIX_MARKERS.map(h);
|
|
@@ -187,16 +207,19 @@ function buildBlockChecks(b, id, hF, hSet, h) {
|
|
|
187
207
|
const webglRendererHash = h(WEBGL_RENDERER_MARKER);
|
|
188
208
|
const productSubGeckoHash = h(PRODUCTSUB_GECKO);
|
|
189
209
|
const sequentumHash = h(SEQUENTUM_MARKER);
|
|
190
|
-
|
|
210
|
+
|
|
191
211
|
const checks = [];
|
|
192
212
|
|
|
193
213
|
checks.push(
|
|
194
214
|
`if (!${b}) { try { var d = Object.getOwnPropertyDescriptors(navigator); var __wh = ${h("webdriver")}; for (const k in d) { if (${hF}(k) === __wh) { ${b} = true; break; } } if (!${b}) { var p = Object.getPrototypeOf(navigator); while (p && !${b}) { for (const k of Object.getOwnPropertyNames(p)) { if (${hF}(k) === __wh) { try { if (navigator[k]) ${b} = true; } catch {} break; } } p = Object.getPrototypeOf(p); } } } catch { ${b} = true; } }`,
|
|
195
215
|
);
|
|
196
216
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
217
|
+
{
|
|
218
|
+
const a = rVar();
|
|
219
|
+
checks.push(
|
|
220
|
+
`if (!${b}) { try { var ${a} = ${JSON.stringify(navOwnHashes)}; for (const k of Object.getOwnPropertyNames(navigator)) { if (${hSet}(${a}, ${hF}(k))) { ${b} = true; break; } } } catch { ${b} = true; } }`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
200
223
|
|
|
201
224
|
{
|
|
202
225
|
const a = rVar();
|
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
|
+
}
|