@anonympins/fingerprint 0.2.0 → 0.2.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/README.md CHANGED
@@ -98,6 +98,10 @@ const securityConfig = {
98
98
  high: 75, // Score for a very difficult challenge
99
99
  block: 95, // Score above which the request is blocked outright (HTTP 404)
100
100
  },
101
+ cpu: {
102
+ minDifficultyBits: 8,
103
+ maxDifficultyBits: 24,
104
+ },
101
105
  // (Optional) Configure the duration (in milliseconds) for various temporary data.
102
106
  ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
103
107
  challengeTtl: 300000, // 5 minutes. Time during which a challenge nonce is valid.
@@ -1,104 +1,161 @@
1
- /**
2
- * Algorithme de hachage cyrb53 (rapide et faible taux de collision).
3
- */
4
- export const cyrb53 = (str, seed = 0) => {
5
- let h1 = 0xdeadbeef ^ seed,
6
- h2 = 0x41c6ce57 ^ seed;
7
- for (let i = 0, ch; i < str.length; i++) {
8
- ch = str.charCodeAt(i);
9
- h1 = Math.imul(h1 ^ ch, 2654435761);
10
- h2 = Math.imul(h2 ^ ch, 1597334677);
11
- }
12
- h1 =
13
- Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
14
- Math.imul(h2 ^ (h2 >>> 13), 3266489909);
15
- h2 =
16
- Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
17
- Math.imul(h1 ^ (h1 >>> 13), 3266489909);
18
- return 4294967296 * (2097151 & h2) + (h1 >>> 0);
19
- };
20
-
21
- /**
22
- * Classe pour construire une empreinte composite (Multi-Hash).
23
- * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
24
- */
25
- export class FingerprintBuilder {
26
- constructor() {
27
- this.components = new Map();
28
- }
29
-
30
- /**
31
- * Ajoute un composant au hash global.
32
- * @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
33
- * @param {string|number|boolean} value - La valeur brute à hasher
34
- */
35
- add(group, value) {
36
- if (value === undefined || value === null) return this;
37
- // On hash la valeur individuellement pour l'anonymiser et réduire sa taille
38
- this.components.set(group, cyrb53(String(value)));
39
- return this;
40
- }
41
-
42
- /**
43
- * Adds a raw component without hashing it.
44
- * Useful for metrics that need to be read on the server.
45
- * @param {string} group - The name of the group.
46
- * @param {string|number} value - The raw value.
47
- */
48
- addRaw(group, value) {
49
- if (value === undefined || value === null) return this;
50
- this.components.set(group, value);
51
- return this;
52
- }
53
- /**
54
- * Génère la chaîne de signature finale.
55
- * Trie les clés pour garantir un ordre déterministe.
56
- */
57
- toString() {
58
- return Array.from(this.components.entries())
59
- .sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
60
- .map(([key, hash]) => `${key}:${hash}`)
61
- .join("|");
62
- }
63
-
64
- /**
65
- * Compares two fingerprints and returns a similarity score (0 to 1).
66
- * Uses weights to give more importance to strong invariants (Canvas, GPU).
67
- * @param {string} fpString1 - Fingerprint A
68
- * @param {string} fpString2 - Fingerprint B
69
- */
70
- static compare(fpString1, fpString2) {
71
- if (!fpString1 || !fpString2) return 0;
72
-
73
- const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
74
-
75
- const map1 = parse(fpString1);
76
- const map2 = parse(fpString2);
77
-
78
- // Poids de "véracité" (Entropie/Stabilité)
79
- const weights = {
80
- cvs: 4.0, // Canvas: Très haute entropie (Rendu unique)
81
- gpu: 3.0, // GPU: Haute entropie (Matériel spécifique)
82
- hw: 1.5, // Hardware: Moyenne entropie
83
- scr: 1.0, // Screen: Moyenne
84
- geo: 0.5, // Geo: Faible (VPN/Voyage)
85
- os: 0.5, // OS: Faible (Générique)
86
- bot: 0.0, // Bot: Informatif
87
- };
88
-
89
- let weightedMatches = 0;
90
- let totalWeight = 0;
91
-
92
- const allKeys = new Set([...map1.keys(), ...map2.keys()]);
93
-
94
- allKeys.forEach((key) => {
95
- const weight = weights[key] || 1.0;
96
- totalWeight += weight;
97
- if (map1.get(key) === map2.get(key)) {
98
- weightedMatches += weight;
99
- }
100
- });
101
-
102
- return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
103
- }
1
+ /**
2
+ * Algorithme de hachage cyrb53 (rapide et faible taux de collision).
3
+ */
4
+ export const cyrb53 = (str, seed = 0) => {
5
+ let h1 = 0xdeadbeef ^ seed,
6
+ h2 = 0x41c6ce57 ^ seed;
7
+ for (let i = 0, ch; i < str.length; i++) {
8
+ ch = str.charCodeAt(i);
9
+ h1 = Math.imul(h1 ^ ch, 2654435761);
10
+ h2 = Math.imul(h2 ^ ch, 1597334677);
11
+ }
12
+ h1 =
13
+ Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^
14
+ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
15
+ h2 =
16
+ Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^
17
+ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
18
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
19
+ };
20
+
21
+ /**
22
+ * Classe pour construire une empreinte composite (Multi-Hash).
23
+ * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
24
+ */
25
+ export class FingerprintBuilder {
26
+ constructor() {
27
+ this.components = new Map();
28
+ }
29
+
30
+ /**
31
+ * Ajoute un composant au hash global.
32
+ * @param {string} group - Le nom du groupe (ex: 'hw', 'screen', 'geo')
33
+ * @param {string|number|boolean} value - La valeur brute à hasher
34
+ */
35
+ add(group, value) {
36
+ if (value === undefined || value === null) return this;
37
+ // On hash la valeur individuellement pour l'anonymiser et réduire sa taille
38
+ this.components.set(group, cyrb53(String(value)));
39
+ return this;
40
+ }
41
+
42
+ /**
43
+ * Adds a raw component without hashing it.
44
+ * Useful for metrics that need to be read on the server.
45
+ * @param {string} group - The name of the group.
46
+ * @param {string|number} value - The raw value.
47
+ */
48
+ addRaw(group, value) {
49
+ if (value === undefined || value === null) return this;
50
+ this.components.set(group, value);
51
+ return this;
52
+ }
53
+
54
+ /**
55
+ * Affiche les composants actuels dans la console.
56
+ * @param {string} [title='FingerprintBuilder Components'] - Un titre pour le log.
57
+ */
58
+ log(title = 'FingerprintBuilder Components') {
59
+ console.log(`--- ${title} ---`);
60
+ const sortedComponents = Array.from(this.components.entries())
61
+ .sort((a, b) => a[0].localeCompare(b[0]));
62
+
63
+ console.table(Object.fromEntries(sortedComponents));
64
+ console.log(`Final string: ${this.toString()}`);
65
+ console.log(`---------------------------------${'-'.repeat(title.length)}`);
66
+ }
67
+
68
+ /**
69
+ * Génère la chaîne de signature finale.
70
+ * Trie les clés pour garantir un ordre déterministe.
71
+ */
72
+ toString() {
73
+ return Array.from(this.components.entries())
74
+ .sort((a, b) => a[0].localeCompare(b[0])) // Tri alphabétique des clés
75
+ .map(([key, hash]) => `${key}:${hash}`)
76
+ .join("|");
77
+ }
78
+ /**
79
+ * Adds a raw component without hashing it.
80
+ * Useful for metrics that need to be read on the server.
81
+ * @param {string} group - The name of the group.
82
+ * @param {string|number} value - The raw value.
83
+ */
84
+ addRaw(group, value) {
85
+ if (value === undefined || value === null) return this;
86
+ this.components.set(group, value);
87
+ return this;
88
+ }
89
+
90
+ /**
91
+ * Compares two fingerprints and returns a similarity score (0 to 1).
92
+ * Uses weights to give more importance to strong invariants (Canvas, GPU).
93
+ * @param {string} fpString1 - Fingerprint A
94
+ * @param {string} fpString2 - Fingerprint B
95
+ */
96
+ static compare(fpString1, fpString2) {
97
+ if (!fpString1 || !fpString2) return 0;
98
+
99
+ const parse = (str) => new Map(str.split("|").map(part => part.split(":")).filter(([k,v]) => k && v));
100
+
101
+ const map1 = parse(fpString1);
102
+ const map2 = parse(fpString2);
103
+
104
+ // Keys to ignore when comparing the initial request fingerprint with the challenge solver's fingerprint.
105
+ // Headers like Client-Hints (ch_*), cookie presence (cookie_keys), and upgrade-insecure-requests
106
+ // can vary or be absent on the subsequent request that submits the solution, especially after a redirect.
107
+ // By ignoring them, we focus the comparison on more stable identifiers like UA, JA3, GPU, etc.
108
+ const volatileKeys = new Set([
109
+ 'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
110
+ 'cookie_keys', 'upgrade',
111
+ // Also ignore network and http version as they can change between requests (e.g., proxy, protocol upgrade)
112
+ 'network', 'http_ver',
113
+ // Ignore proxy-related headers as they are not stable client identifiers
114
+ 'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
115
+ ]);
116
+
117
+ // Poids de "véracité" (Entropie/Stabilité)
118
+ // Les poids sont augmentés pour donner plus d'importance aux signaux forts.
119
+ const weights = {
120
+ // --- Signaux très forts (difficiles à usurper) ---
121
+ cvs: 5.0, // Canvas: Très haute entropie (Rendu unique du GPU/driver)
122
+ gpu: 4.0, // GPU: Haute entropie (Matériel spécifique)
123
+ ja3: 3.5, // JA3: Identifie la librairie TLS (très stable pour un client donné)
124
+ ua: 2.0, // User-Agent: Signal fort, bien que modifiable
125
+
126
+ // --- Signaux composites et dérivés ---
127
+ client_fp_hash: 3.0, // Le hash de l'empreinte client est un signal très fort.
128
+ browser: 1.5, // Le navigateur extrait du UA.
129
+ os_version: 1.5, // L'OS extrait du UA.
130
+ device_type: 1.0, // Le type d'appareil extrait du UA.
131
+
132
+ // --- Signaux moyens ---
133
+ hw: 1.5, // Hardware (CPU, RAM): Stabilité moyenne
134
+ scr: 1.0, // Screen: Stabilité moyenne
135
+ // 'os' est souvent la même chose que 'ch_platform', on peut le déprécier ou lui donner un poids faible.
136
+ os: 0.8, // OS (nav.platform): Assez stable
137
+ geo: 0.5, // Geo/Langue: Peut changer (VPN, voyage)
138
+ };
139
+
140
+ let weightedMatches = 0;
141
+ let totalWeight = 0;
142
+
143
+ const allKeys = new Set([...map1.keys(), ...map2.keys()]);
144
+
145
+ allKeys.forEach((key) => {
146
+ // On ignore les clés volatiles pour cette comparaison spécifique.
147
+ if (volatileKeys.has(key)) return;
148
+
149
+ // On ne compare que les clés qui ont un poids défini.
150
+ const weight = weights[key];
151
+ if (!weight) return;
152
+
153
+ totalWeight += weight; // N'incrémenter que si la clé est pertinente.
154
+ if (map1.get(key) === map2.get(key)) {
155
+ weightedMatches += weight;
156
+ }
157
+ });
158
+
159
+ return totalWeight === 0 ? 0 : weightedMatches / totalWeight;
160
+ }
104
161
  }
@@ -335,7 +335,7 @@ const ClientLibrary = {
335
335
  * @private
336
336
  */
337
337
  async solveChallengeAndRetry(response, resource, options) {
338
- if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json')) {
338
+ if (response.status !== 404 || !response.headers.get('content-type')?.includes('application/json') || response.bodyUsed) {
339
339
  return response;
340
340
  }
341
341
 
@@ -346,7 +346,9 @@ const ClientLibrary = {
346
346
  }
347
347
 
348
348
  console.log(`[Fingerprint] Received a '${challengeData.challenge.type}' challenge. Solving...`);
349
- const solution = await solveChallenge(challengeData.challenge);
349
+ // L'empreinte de l'appareil qui résout le challenge est cruciale.
350
+ const solverFp = this.getDeviceFingerprint();
351
+ const solution = await solveChallenge(challengeData.challenge, solverFp);
350
352
  console.log('[Fingerprint] Challenge solved. Retrying original request.');
351
353
 
352
354
  // Ajouter la solution aux paramètres de la requête pour le nouvel essai
@@ -360,17 +362,15 @@ const ClientLibrary = {
360
362
  url.searchParams.set(`pow_solution_${key}`, String(value));
361
363
  });
362
364
 
363
- // Pour le challenge d'optimisation, la solution est un tableau d'objets
364
- if (solution.population) {
365
- url.searchParams.set('pow_solution_population', JSON.stringify(solution.population));
366
- }
367
-
368
365
  // Pour le challenge de travail utile
369
366
  if (solution.work_result) {
370
367
  url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
371
368
  url.searchParams.set('pow_problem_id', solution.problem_id);
372
369
  }
373
370
 
371
+ // On ajoute l'empreinte du solveur à la requête de réessai.
372
+ url.searchParams.set('pow_fp', solverFp);
373
+
374
374
  // On utilise la chaîne d'intercepteurs pour la requête réessayée,
375
375
  // ce qui garantit que le fetch original est appelé avec le bon contexte.
376
376
  // Cela évite de réintroduire l'erreur "Illegal invocation".
@@ -418,8 +418,9 @@ const ClientLibrary = {
418
418
  // Ajoute l'intercepteur pour la résolution de challenge
419
419
  if (fetchConfig.handleChallenges !== false) {
420
420
  this.addFetchInterceptor(async (resource, options, next) => {
421
- const response = await next(resource, options);
422
- return this.solveChallengeAndRetry(response, resource, options);
421
+ const originalResponse = await next(resource, options);
422
+ // On clone la réponse pour que la lecture du corps par solveChallengeAndRetry ne la consomme pas pour l'appelant original.
423
+ return this.solveChallengeAndRetry(originalResponse.clone(), resource, options);
423
424
  });
424
425
  }
425
426
  }
package/fingerprint.js CHANGED
@@ -27,111 +27,12 @@ const getPowSecret = () => {
27
27
  * @returns {string} The solver JavaScript code.
28
28
  */
29
29
  const getPowSolverCode = () => {
30
- try {
31
- const __filename = fileURLToPath(import.meta.url);
32
- const __dirname = dirname(__filename);
33
- const solverPath = join(__dirname, 'pow.solver.inline.js'); // Use the inline version
34
- return readFileSync(solverPath, 'utf-8');
35
- } catch (error) {
36
- console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
37
- // Fallback inline code if file cannot be loaded
38
- return `(function(global){
39
- async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
40
- const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
41
- let cpuSolution = 0;
42
- const ipPart = clientIp || '';
43
- while(true){
44
- // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
45
- const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
46
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
47
- const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
48
- if(BigInt('0x'+hashHex) < cpuTarget) break;
49
- cpuSolution++;
50
- if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
51
- }
52
- return cpuSolution;
53
- }
54
- async function solveMemory(seed, difficulty){
55
- const size = difficulty * 1024 * 1024;
56
- const buffer = new Uint32Array(size / 4);
57
- let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
58
- for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
59
- let solution = 0;
60
- const iterations = size / 16;
61
- let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
62
- for(let i=0;i<iterations;i++){
63
- addr = buffer[addr] % buffer.length;
64
- solution ^= addr;
65
- }
66
- return solution;
67
- }
68
- async function solveTsp(cities, targetMaxDistance){
69
- function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
70
- function evaluatePathDistance(cities,path){
71
- let total=0;
72
- for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
73
- total+=distance(cities[path[path.length-1]],cities[path[0]]);
74
- return total;
75
- }
76
- function solveTspNearestNeighbor(cities){
77
- const n=cities.length;
78
- if(n===0)return[];
79
- let path=[0];
80
- let visited=new Array(n).fill(false);
81
- visited[0]=true;
82
- for(let i=1;i<n;i++){
83
- let nearest=-1, minDist=Infinity;
84
- for(let j=0;j<n;j++){
85
- if(!visited[j]){
86
- const d=distance(cities[path[i-1]],cities[j]);
87
- if(d<minDist){minDist=d;nearest=j;}
88
- }
89
- }
90
- path.push(nearest);
91
- visited[nearest]=true;
92
- }
93
- return path;
94
- }
95
- await new Promise(r=>setTimeout(r,10));
96
- const solutionPath=solveTspNearestNeighbor(cities);
97
- const solutionDistance=evaluatePathDistance(cities,solutionPath);
98
- return{path:solutionPath,distance:solutionDistance};
99
- }
100
- async function solveChallenge(challenge) {
101
- const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
102
- const solutions = {};
103
-
104
- switch (type) {
105
- case 'cpu_target':
106
- solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
107
- break;
108
- case 'cpu_mem':
109
- case 'cpu_mem_inline':
110
- const memSeed = nonce + ":" + clientSecret;
111
- const [cpuSol, memSol] = await Promise.all([
112
- solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
113
- solveMemory(memSeed, memDifficulty)
114
- ]);
115
- solutions.cpu = cpuSol;
116
- solutions.mem = memSol;
117
- break;
118
- case 'tsp':
119
- const tspResult = await solveTsp(cities, targetMaxDistance);
120
- solutions.tsp = tspResult.path;
121
- solutions.distance = tspResult.distance;
122
- break;
123
- default:
124
- throw new Error(\`Unknown challenge type: \${type}\`);
125
- }
126
-
127
- return solutions;
128
- }
129
- global.solveCpuChallengeInline=solveCpuTargetInline;
130
- global.solveMemoryChallenge=solveMemory;
131
- global.solveTspChallenge=solveTsp;
132
- global.solveChallenge=solveChallenge;
133
- })(typeof window!=='undefined'?window:global);`;
134
- }
30
+ // On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
31
+ // ce qui est préférable à servir un code de secours potentiellement désynchronisé.
32
+ const __filename = fileURLToPath(import.meta.url);
33
+ const __dirname = dirname(__filename);
34
+ const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
35
+ return readFileSync(solverPath, 'utf-8');
135
36
  };
136
37
 
137
38
  /**
@@ -190,7 +91,7 @@ function getHeaderSignature(context) {
190
91
  for (let i = 0; i < context.rawHeaders.length; i += 2) {
191
92
  headerKeys.push(context.rawHeaders[i]);
192
93
  }
193
- return cyrb53(headerKeys.join(','));
94
+ return cyrb53(headerKeys.sort().join(','));
194
95
  }
195
96
 
196
97
  /**
@@ -227,11 +128,6 @@ export function getCompositeDeviceHash(context) {
227
128
  const ua = context.headers["user-agent"];
228
129
  if (ua) {
229
130
  srv.add("ua", ua);
230
- // Extraire des infos supplémentaires du UA
231
- const uaParts = parseUserAgent(ua);
232
- if (uaParts.browser) srv.add("browser", uaParts.browser);
233
- if (uaParts.os) srv.add("os_version", uaParts.os);
234
- if (uaParts.device) srv.add("device_type", uaParts.device);
235
131
  }
236
132
 
237
133
  // 2. SIGNAL FORT: JA3 TLS Fingerprint
@@ -266,9 +162,6 @@ export function getCompositeDeviceHash(context) {
266
162
  srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
267
163
  }
268
164
 
269
- // 10. SIGNAL FORT: Ordonnancement des headers
270
- srv.add("h_ord", getHeaderSignature(context));
271
-
272
165
  // 11. SIGNAL AVANCÉ: Cookies (si disponible)
273
166
  if (context.cookies) {
274
167
  const cookieKeys = Object.keys(context.cookies).sort().join(',');
@@ -614,11 +507,11 @@ export const verifyPoWAndGenerateTicket = (
614
507
  * Verifies a memory PoW solution.
615
508
  * The server performs the same calculation to validate.
616
509
  */
617
- export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
510
+ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
618
511
  const size = difficulty * 1024 * 1024;
619
512
  const iterations = size / 16;
620
513
  const buffer = new Uint32Array(size / 4);
621
- const seed = clientSecret ? `${nonce}:${clientSecret}` : nonce;
514
+ const seed = `:${nonce}:${clientSecret}`;
622
515
  let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
623
516
 
624
517
  for (let i = 0; i < buffer.length; i++) {
@@ -946,8 +839,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
946
839
  const now = Date.now();
947
840
  const currentPath = context.path;
948
841
  // Make the function robust to handle both URLSearchParams and plain objects for query.
949
- // Ensure query parameters are consistently handled, whether they come from a URLSearchParams object or a plain object.
950
- const params = context.query instanceof URLSearchParams ? context.query : new URLSearchParams(context.query);
842
+ const params =
843
+ context.query instanceof URLSearchParams
844
+ ? new URLSearchParams(context.query.toString()) // Clone to avoid modifying the original
845
+ : new URLSearchParams(context.query || {});
951
846
  params.sort(); // Sort for deterministic order
952
847
  const currentQueryString = params.toString();
953
848
 
@@ -1365,20 +1260,44 @@ const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
1365
1260
  * @param {number} suspicionFactor - A number from 0 to 1.
1366
1261
  * @returns {BigInt} The target number.
1367
1262
  */
1368
- function calculateTarget(suspicionFactor) {
1263
+ function calculateTarget(suspicionFactor, securityConfig = {}) {
1369
1264
  // Difficulty range adjusted to be realistic.
1370
1265
  // MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
1371
1266
  // MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
1372
- const MIN_DIFFICULTY_BITS = 18; // Default value, should be configurable
1373
- const MAX_DIFFICULTY_BITS = 26; // Default value, should be configurable
1267
+ // NOUVEAU: La difficulté est maintenant configurable.
1268
+ const { cpu: cpuConfig = {} } = securityConfig;
1269
+ const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
1270
+ const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
1374
1271
 
1375
1272
  // Use linear interpolation between min and max difficulty.
1376
1273
  const totalDifficultyBits =
1377
1274
  MIN_DIFFICULTY_BITS +
1378
1275
  suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
1276
+
1277
+ if (totalDifficultyBits <= 0) return 2n ** 256n - 1n; // Si la difficulté est nulle ou négative, la cible est maximale (aucun challenge).
1278
+
1279
+ // The correct way to calculate the target is to define the number of leading zero bits required.
1280
+ // A target for N bits of difficulty is 2^(256-N).
1281
+ // We can calculate this with a left-shift on 1.
1282
+ const shift = 256n - BigInt(Math.floor(totalDifficultyBits));
1283
+ return 1n << shift;
1284
+ }
1379
1285
 
1380
- // The target is max / 2^bits
1381
- return MAX_DIFFICULTY_TARGET >> BigInt(Math.floor(totalDifficultyBits));
1286
+ /**
1287
+ * @private
1288
+ * Crée le bloc de base pour le challenge CPU.
1289
+ * Ce buffer contient toutes les données sauf la solution.
1290
+ * @param {string} nonce
1291
+ * @param {string} clientSecret
1292
+ * @param {string} fingerprint
1293
+ * @returns {Buffer}
1294
+ */
1295
+ function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
1296
+ const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
1297
+ // On concatène les chaînes, puis on les convertit en buffer une seule fois.
1298
+ // Cela garantit que le client et le serveur travaillent sur la même base binaire.
1299
+ const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
1300
+ return Buffer.from(messageBase, 'utf8');
1382
1301
  }
1383
1302
 
1384
1303
  /**
@@ -1389,12 +1308,15 @@ export function generateCpuTargetChallenge(
1389
1308
  nonce,
1390
1309
  suspicionFactor,
1391
1310
  originalUrl,
1311
+ securityConfig,
1392
1312
  ) {
1393
- const target = calculateTarget(suspicionFactor);
1313
+ const target = calculateTarget(suspicionFactor, securityConfig);
1314
+ // Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
1315
+ const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
1394
1316
  return {
1395
1317
  type: "cpu_target",
1396
1318
  nonce: nonce,
1397
- target: target.toString(16), // Send the target in hexadecimal
1319
+ target: target.toString(16),
1398
1320
  path: originalUrl,
1399
1321
  };
1400
1322
  }
@@ -1419,10 +1341,11 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1419
1341
  async function solve() {
1420
1342
  const clientIp = ${JSON.stringify(clientIp)};
1421
1343
  const nonce = ${JSON.stringify(nonce)};
1422
- const cpuTarget = BigInt("0x${target}");
1423
- const solution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, null, (progress) => {
1424
- // Optional progress callback
1425
- });
1344
+ const cpuTarget = BigInt("0x" + "${target}");
1345
+ // La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
1346
+ // car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
1347
+ const baseBlockBytes = new TextEncoder().encode(nonce + ":");
1348
+ const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
1426
1349
  window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
1427
1350
  }
1428
1351
  solve();
@@ -1437,9 +1360,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1437
1360
  * @param {string} clientIp - The client's IP address.
1438
1361
  * @returns {string} HTML content.
1439
1362
  */
1440
- function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
1363
+ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
1441
1364
  const { nonce, target, path } = cpuChallengeDetails;
1442
1365
  const solverCode = getPowSolverCode();
1366
+ // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
1367
+ // Le fingerprint est maintenant passé directement en paramètre.
1368
+ const fingerprint = originalFingerprint;
1369
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
1370
+ const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
1443
1371
 
1444
1372
  const challengeScript = `
1445
1373
  async function solve() {
@@ -1447,22 +1375,18 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1447
1375
  const path = ${JSON.stringify(path)};
1448
1376
  const clientSecret = ${JSON.stringify(clientSecret)};
1449
1377
  const clientIp = ${JSON.stringify(clientIp)};
1450
- const cpuTarget = BigInt("0x${target}");
1378
+ const cpuTarget = BigInt("0x" + "${target}");
1451
1379
  const memDifficulty = ${memoryDifficulty};
1452
- // The client-side fingerprint library must be available to generate the fingerprint
1453
- // of the machine solving the challenge. This assumes a client library is loaded.
1454
- // We need a function to get the client fingerprint. Let's assume it's available on window.
1455
- const getClientFingerprint = () => (window.ClientLibrary && typeof window.ClientLibrary.getDeviceFingerprint === 'function') ? window.ClientLibrary.getDeviceFingerprint() : '';
1456
- const fingerprint = getClientFingerprint();
1380
+ // Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
1381
+ // Il n'a plus besoin de construire le message lui-même.
1382
+ const baseBlock = new Uint8Array(${baseBlockBytes});
1457
1383
 
1458
1384
  // --- CPU Challenge ---
1459
- document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1460
- const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, fingerprint, (progress) => {
1461
- // Optional progress callback
1462
- });
1385
+ document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...'; const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
1386
+
1463
1387
  // --- Memory Challenge ---
1464
1388
  document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1465
- await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1389
+ await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1466
1390
  let memSolution = 0;
1467
1391
  try {
1468
1392
  const memSeed = nonce + ":" + clientSecret;
@@ -1471,7 +1395,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1471
1395
  document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1472
1396
  return;
1473
1397
  }
1474
- window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution + "&pow_fp=" + encodeURIComponent(fingerprint);
1398
+
1399
+ // Redirect with both solutions and the fingerprint used to solve.
1400
+ const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
1401
+ window.location.href = finalUrl;
1475
1402
  }
1476
1403
  solve();
1477
1404
  `;
@@ -1502,23 +1429,56 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
1502
1429
  */
1503
1430
  export function verifyCpuTargetPoWAndGenerateTicket(
1504
1431
  clientIp, // This parameter is crucial and must be the actual client IP
1505
- ticketTtl, // NOUVEAU: Durée de validité du ticket (TTL en ms) configurable
1432
+ ticketTtl,
1506
1433
  nonce,
1507
1434
  solution,
1508
- clientSecret, // Le secret est maintenant requis
1509
- target, // La cible est maintenant passée directement en hexadécimal,
1510
- fingerprint, // Le fingerprint du SOLVER, soumis par le client
1435
+ challengeContext = {}, // Le contexte complet du challenge est maintenant passé
1511
1436
  ) {
1512
- const message = clientSecret
1513
- ? `${nonce}:${solution}:${clientSecret}:${fingerprint}`
1514
- : `${clientIp}:${nonce}:${solution}`; // L'IP est utilisée uniquement pour les challenges sans secret (plus anciens/simples)
1437
+ const { cpuTarget, baseBlock } = challengeContext;
1438
+ if (!cpuTarget || !baseBlock) {
1439
+ console.error('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
1440
+ return null;
1441
+ }
1442
+
1443
+ // Le baseBlock est déjà un Buffer ou un tableau d'octets.
1444
+ // On s'assure que c'est un Buffer pour la concaténation.
1445
+ const baseBlockBuffer = Buffer.isBuffer(baseBlock) ? baseBlock : Buffer.from(baseBlock);
1446
+ const solutionBuffer = Buffer.from(String(solution), 'utf8');
1447
+
1448
+ // Concaténation binaire directe. C'est la garantie de cohérence.
1449
+ const finalBlock = Buffer.concat([baseBlockBuffer, solutionBuffer]);
1450
+
1515
1451
  const hash = crypto
1516
1452
  .createHash("sha256")
1517
- .update(message)
1453
+ .update(finalBlock)
1518
1454
  .digest("hex");
1519
1455
  const hashAsInt = BigInt("0x" + hash);
1456
+ const targetAsInt = BigInt("0x" + cpuTarget);
1457
+
1458
+ // --- NOUVEAUX LOGS POUR LE DÉBOGAGE ---
1459
+ console.log('[FP Server Verify] Intermediate values:', {
1460
+ hashCalculated: `0x${hash}`,
1461
+ hashAsInt: hashAsInt.toString(), // Log as string to see full value
1462
+ target: `0x${cpuTarget}`,
1463
+ targetAsInt: targetAsInt.toString(), // Log as string to see full value
1464
+ });
1465
+ // --- FIN DES NOUVEAUX LOGS ---
1466
+
1467
+ const isValid = hashAsInt < targetAsInt;
1520
1468
 
1521
- if (hashAsInt < BigInt("0x" + target)) {
1469
+ // --- AJOUT DE LOGS POUR LE DÉBOGAGE ---
1470
+ if (!isValid) {
1471
+ console.log('[FP Server Verify] CPU PoW verification FAILED. Details:', {
1472
+ hashCalculated: `0x${hash}`,
1473
+ target: `0x${cpuTarget}`,
1474
+
1475
+ });
1476
+ }
1477
+ // --- FIN DES LOGS ---
1478
+
1479
+ if (isValid) {
1480
+ console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
1481
+ });
1522
1482
  // The comparison is direct with native BigInts
1523
1483
  // The proof is valid, generate the ticket
1524
1484
  const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
@@ -1706,7 +1666,7 @@ export class FingerprintEngine {
1706
1666
  if (deviceData?.condemned) {
1707
1667
  this._log('Device condemned - blocking request', { deviceId });
1708
1668
  if (onDeviceCompromised) {
1709
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1669
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1710
1670
  }
1711
1671
  return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1712
1672
  }
@@ -1768,7 +1728,7 @@ export class FingerprintEngine {
1768
1728
  // Si une solution de challenge est soumise, on la traite en priorité absolue,
1769
1729
  // avant même de recalculer le score de suspicion.
1770
1730
  const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
1771
- if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
1731
+ if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
1772
1732
  this._log('Challenge solution submitted', { pow_type, pow_nonce });
1773
1733
 
1774
1734
  // On doit calculer le score de suspicion *avant* de valider le ticket,
@@ -1795,14 +1755,33 @@ export class FingerprintEngine {
1795
1755
  if (challengeContext) {
1796
1756
  // *** NOUVELLE VÉRIFICATION CRUCIALE ***
1797
1757
  // On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
1798
- // lors de l'émission du challenge (`challengeContext.fingerprint`).
1799
- const solverFingerprint = pow_fp;
1800
- const originalFingerprint = challengeContext.fingerprint;
1758
+ // lors de l'émission du challenge.
1759
+ // --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
1760
+ // This handles API clients that might not use the full client-side library but still solve the challenge.
1761
+ const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
1762
+ const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
1763
+
1764
+ let similarity;
1765
+ const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
1766
+
1767
+ // If fingerprints are simple strings (like test placeholders 'fp-probation')
1768
+ // and don't contain the typical structure, fall back to a strict equality check.
1769
+ if (!originalFingerprint?.includes(':') || !solverFingerprint?.includes(':')) {
1770
+ similarity = (originalFingerprint === solverFingerprint) ? 1.0 : 0.0;
1771
+ } else {
1772
+ // Use the weighted comparison for structured fingerprints.
1773
+ // We compare the fingerprint of the request that triggered the challenge
1774
+ // with the fingerprint of the request that is submitting the solution.
1775
+ // They should be very similar.
1776
+ similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
1777
+ }
1801
1778
 
1802
- if (solverFingerprint !== originalFingerprint) {
1779
+ if (similarity < similarityThreshold) {
1803
1780
  this._log('Fingerprint mismatch - challenge solved on a different machine!', {
1804
1781
  original: originalFingerprint,
1805
1782
  solver: solverFingerprint,
1783
+ similarity: similarity.toFixed(4),
1784
+ threshold: similarityThreshold
1806
1785
  });
1807
1786
  isValid = false;
1808
1787
  } else {
@@ -1810,13 +1789,13 @@ export class FingerprintEngine {
1810
1789
  finalTtl = isProbationary ? probationaryTtl : optimalTtl;
1811
1790
  this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
1812
1791
 
1813
- if (pow_type === "cpu_target" && pow_solution) {
1814
- ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget, solverFingerprint);
1792
+ if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
1793
+ const cpuSolution = pow_solution_cpu || pow_solution;
1794
+ ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext);
1815
1795
  isValid = ticket !== null;
1816
1796
  this._log('CPU target challenge verification', { isValid });
1817
- } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
1818
- const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget, solverFingerprint);
1819
- const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
1797
+ } else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) { const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext);
1798
+ const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
1820
1799
  isValid = cpuTicket !== null && isMemValid;
1821
1800
  if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
1822
1801
  this._log('Combined CPU+Memory challenge verification', {
@@ -1840,7 +1819,7 @@ export class FingerprintEngine {
1840
1819
 
1841
1820
  // NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
1842
1821
  // 1. On part du chemin original stocké, qui peut contenir des query params.
1843
- const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
1822
+ const originalUrl = new URL(challengeContext?.originalPath || requestContext.path, `http://${requestContext.headers.host || 'localhost'}`);
1844
1823
  // 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
1845
1824
  const finalSearchParams = new URLSearchParams(requestContext.query);
1846
1825
 
@@ -1851,6 +1830,10 @@ export class FingerprintEngine {
1851
1830
  finalSearchParams.delete('pow_solution_cpu');
1852
1831
  finalSearchParams.delete('pow_solution_mem');
1853
1832
  finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
1833
+ // NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
1834
+ finalSearchParams.delete('pow_solution_population');
1835
+ finalSearchParams.delete('pow_solution_work_result');
1836
+ finalSearchParams.delete('pow_problem_id');
1854
1837
 
1855
1838
  // 4. On reconstruit le chemin final.
1856
1839
  const finalQueryString = finalSearchParams.toString();
@@ -1873,6 +1856,13 @@ export class FingerprintEngine {
1873
1856
  this._log('Challenge solution invalid', { pow_nonce });
1874
1857
  suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
1875
1858
  finalScore = this.calculateFinalScore(suspicionVector);
1859
+ // --- FIX: After invalidating a solution, immediately check if the new score triggers a block ---
1860
+ const newBlockThreshold = thresholds.block ?? 95;
1861
+ if (finalScore >= newBlockThreshold) {
1862
+ this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
1863
+ return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1864
+ }
1865
+ // If not blocked, the request will proceed to be re-challenged.
1876
1866
  }
1877
1867
  } else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
1878
1868
  this._log('Optimization task solution submitted', { pow_nonce });
@@ -1935,7 +1925,7 @@ export class FingerprintEngine {
1935
1925
  if (isBlocked) {
1936
1926
  this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
1937
1927
  if (onDeviceCompromised) {
1938
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1928
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1939
1929
  }
1940
1930
  return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1941
1931
  }
@@ -1947,7 +1937,7 @@ export class FingerprintEngine {
1947
1937
  this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
1948
1938
  deviceData.condemned = true; // This device is a bot. Condemn it.
1949
1939
  if (onDeviceCompromised) {
1950
- onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1940
+ onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
1951
1941
  }
1952
1942
  if (logger) {
1953
1943
  logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
@@ -2002,8 +1992,8 @@ export class FingerprintEngine {
2002
1992
  const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
2003
1993
  const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
2004
1994
 
2005
-
2006
- const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
1995
+ // On passe la configuration pour que la difficulté soit calculée correctement.
1996
+ const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
2007
1997
 
2008
1998
  // La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
2009
1999
  // Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
@@ -2019,16 +2009,18 @@ export class FingerprintEngine {
2019
2009
  memDifficulty,
2020
2010
  cpuTarget: cpuChallengeDetails.target
2021
2011
  });
2022
- // (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge.
2023
- const originalFingerprint = requestContext.headers['x-device-fingerprint'] || getCompositeDeviceHash(requestContext);
2012
+ // (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge. We call it via __internal to allow mocking.
2013
+ const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
2024
2014
 
2025
2015
  // Store the entire challenge context with a short TTL (e.g., 5 minutes)
2016
+ const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
2026
2017
  await store.set(`secret:${nonce}`, {
2027
2018
  clientSecret,
2028
2019
  cpuTarget: cpuChallengeDetails.target,
2029
2020
  suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
2030
2021
  fingerprint: originalFingerprint, // *** NOUVEAU ***
2031
2022
  memDifficulty: memDifficulty,
2023
+ baseBlock: baseBlock, // *** NOUVEAU: Le bloc de base est stocké pour la vérification ***
2032
2024
  originalPath: path, // *** FIX: Store the original path ***
2033
2025
  }, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
2034
2026
 
@@ -2060,13 +2052,14 @@ export class FingerprintEngine {
2060
2052
  clientSecret: clientSecret, // The client needs this to solve the challenge
2061
2053
  cpuTarget: cpuChallengeDetails.target,
2062
2054
  memDifficulty: memDifficulty,
2055
+ baseBlock: [...baseBlock], // Envoyer le buffer comme un tableau d'octets
2063
2056
  }
2064
2057
  };
2065
2058
  this._log('API challenge response generated', { challengePayload });
2066
2059
  return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
2067
2060
  } else {
2068
2061
  // For browsers, send the HTML page.
2069
- const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
2062
+ const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer, originalFingerprint);
2070
2063
  this._log('Browser challenge page generated', {
2071
2064
  pageLength: page.length,
2072
2065
  hasTrapContainer: true
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @file @/optimization.worker.js
3
+ * @description Web Worker générique pour exécuter les algorithmes d'optimisation de la bibliothèque `Optimization`.
4
+ * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
+ */
6
+
7
+ import { parentPort, workerData } from 'worker_threads';
8
+ import { Optimization } from './library.js'; // Assurez-vous que le chemin est correct
9
+
10
+ if (parentPort) {
11
+ parentPort.on('message', async () => { // Le message est vide, on utilise workerData
12
+ const { solverName, solverArgs } = workerData;
13
+
14
+ // Gérer les solveurs imbriqués (ex: 'Operators.solvePortfolio')
15
+ const solverFunction = solverName.split('.').reduce((obj, prop) => obj && obj[prop], Optimization);
16
+
17
+ if (typeof solverFunction === 'function') {
18
+ try {
19
+ const result = await solverFunction(...solverArgs);
20
+ parentPort.postMessage(result);
21
+ } catch (error) {
22
+ parentPort.postMessage({ error: error.message, stack: error.stack });
23
+ }
24
+ } else {
25
+ parentPort.postMessage({ error: `Solver '${solverName}' not found or is not a function in Optimization library.` });
26
+ }
27
+ });
28
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anonympins/fingerprint",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
5
5
  "main": "fingerprint.js",
6
6
  "type": "module",
@@ -15,6 +15,9 @@
15
15
  "fingerprint.client.js",
16
16
  "fingerprint.builder.js",
17
17
  "pow.solver.js",
18
+ "pow.worker.js",
19
+ "problem-manager.js",
20
+ "optimization.worker.js",
18
21
  "library.js",
19
22
  "redis-store.js",
20
23
  "mongodb-store.js",
package/pow.solver.js CHANGED
@@ -10,26 +10,42 @@
10
10
  'use strict';
11
11
 
12
12
  /**
13
- * Résout un challenge CPU basé sur une cible (version inline pour compatibilité HTML).
14
- * @param {string} clientIp - L'adresse IP du client.
15
- * @param {string} nonce - Le nonce du challenge.
13
+ * Résout un challenge CPU basé sur une cible en utilisant un bloc de base binaire.
14
+ * @param {Uint8Array} baseBlock - Le bloc de données initial (nonce, secret, fp) fourni par le serveur.
16
15
  * @param {bigint} target - La cible à atteindre.
17
- * @param {string} clientSecret - Le secret client (optionnel).
18
16
  * @param {Function} progressCallback - Callback pour les mises à jour de progression.
19
17
  * @returns {Promise<number>} La solution (un nombre entier).
20
18
  */
21
- export async function solveCpuTargetInline(clientIp, nonce, target, clientSecret = null, progressCallback) {
22
- // Le 'target' est déjà un BigInt lorsqu'il est appelé depuis la page de challenge.
23
- // On s'assure juste qu'il est bien de ce type.
19
+ export async function solveCpuTargetInline(baseBlock, target, progressCallback) {
20
+ // --- FIX: Add validation for the target to prevent BigInt conversion errors ---
21
+ if (typeof target !== 'bigint' && (typeof target !== 'string' || !/^[0-9a-fA-F]+$/.test(target))) {
22
+ throw new TypeError(`Invalid target type: expected a BigInt or a hex string, but got ${typeof target} with value ${target}`);
23
+ }
24
+
24
25
  const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
26
+ // --- END FIX ---
27
+ const encoder = new TextEncoder();
25
28
  let cpuSolution = 0;
26
- const ipPart = clientIp || ''; // Use empty string if IP is null/undefined
27
- while (true) { // When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
28
- const msg = clientSecret ?
29
- `${nonce}:${cpuSolution}:${clientSecret}` :
30
- `${ipPart}:${nonce}:${cpuSolution}`;
31
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
29
+
30
+ while (true) {
31
+ const solutionBytes = encoder.encode(String(cpuSolution));
32
+
33
+ // Concaténation binaire directe : c'est plus rapide et plus sûr.
34
+ const finalBlock = new Uint8Array(baseBlock.length + solutionBytes.length);
35
+ finalBlock.set(baseBlock);
36
+ finalBlock.set(solutionBytes, baseBlock.length);
37
+
38
+ if (cpuSolution === 0) {
39
+ const reconstructedMsg = new TextDecoder().decode(finalBlock);
40
+ console.log('[FP Solve Debug] Client will hash message:', reconstructedMsg);
41
+ }
42
+ const buf = await crypto.subtle.digest("SHA-256", finalBlock);
32
43
  const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
44
+ // --- AJOUT DE LOGS POUR LE DÉBOGAGE CÔTÉ CLIENT ---
45
+ if (cpuSolution === 0) { // Log only the first attempt
46
+ console.log(`[FP Client Solve] Attempt 0 hash: "0x${hashHex}"`);
47
+ }
48
+ // --- FIN DES LOGS ---
33
49
  if (BigInt('0x' + hashHex) < cpuTarget) break;
34
50
  cpuSolution++;
35
51
  if (cpuSolution % 100000 === 0) {
@@ -328,27 +344,30 @@ export async function solveOptimizationTask(initialPopulation, generations) {
328
344
  * @param {object} challenge - L'objet challenge reçu du serveur.
329
345
  * @returns {Promise<object>} Un objet contenant la ou les solutions.
330
346
  */
331
- export async function solveChallenge(challenge) {
347
+ export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
332
348
  const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
333
349
  const solutions = {};
334
350
 
335
351
  switch (type) {
336
352
  case 'cpu_target':
337
- const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
353
+ // Note: This case is not fully exercised by tests as it relies on Web Workers.
338
354
  if (!cpuTarget) {
339
355
  throw new Error("Challenge data is missing 'cpuTarget' property.");
340
356
  }
341
357
  const target = cpuTarget; // Keep variable name for consistency below
342
- solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
358
+ // Pour ce challenge simple, le baseBlock est juste le nonce.
359
+ const baseBlockBytes = new TextEncoder().encode(nonce + ":");
360
+ solutions.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
343
361
  break;
344
362
  case 'cpu_mem':
345
363
  // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
346
- const baseMessageCombined = `${nonce}:${clientSecret}`;
364
+ const baseMessageCombined = `:${nonce}:${clientSecret}`;
347
365
  const memSeed = `:${nonce}:${clientSecret}`;
348
366
  const [cpuSol, memSol] = await Promise.all([
349
367
  (async () => {
350
- if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
351
- return solveCpuTargetInline(null, nonce, cpuTarget, clientSecret);
368
+ if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property."); // Pass fingerprint to solver
369
+ const baseBlock = new Uint8Array(challenge.baseBlock);
370
+ return solveCpuTargetInline(baseBlock, cpuTarget, null);
352
371
  })(),
353
372
  solveMemory(memSeed, memDifficulty)
354
373
  ]);
@@ -357,11 +376,12 @@ export async function solveChallenge(challenge) {
357
376
  break;
358
377
  case 'cpu_mem_inline':
359
378
  // Version inline pour compatibilité HTML avec IP incluse
360
- const memSeedInline = `${nonce}:${clientSecret}`;
379
+ const memSeedInline = `:${nonce}:${clientSecret}`;
361
380
  const [cpuSolInline, memSolInline] = await Promise.all([
362
381
  (async () => {
363
382
  if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
364
- return solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
383
+ const baseBlock = new Uint8Array(challenge.baseBlock);
384
+ return solveCpuTargetInline(baseBlock, cpuTarget, null);
365
385
  })(),
366
386
  solveMemory(memSeedInline, memDifficulty)
367
387
  ]);
package/pow.worker.js ADDED
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @file @/pow.worker.js
3
+ * @description Web Worker dédié à la résolution du challenge CPU.
4
+ * Ce script s'exécute sur un thread séparé pour ne pas bloquer l'interface utilisateur.
5
+ */
6
+
7
+ self.onmessage = async (event) => {
8
+ const { message, target } = event.data;
9
+ let solution = 0;
10
+ const encoder = new TextEncoder();
11
+
12
+ // La boucle de calcul intensive est isolée dans ce worker.
13
+ while (true) {
14
+ const currentMessage = `${message}:${solution}`;
15
+ const data = encoder.encode(currentMessage);
16
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
17
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
18
+ const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
19
+
20
+ if (BigInt('0x' + hashHex) < target) {
21
+ // Une fois la solution trouvée, on la renvoie au thread principal.
22
+ self.postMessage({ solution });
23
+ return;
24
+ }
25
+ solution++;
26
+ }
27
+ };
@@ -0,0 +1,175 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { Optimization } from './library.js';
3
+
4
+ /**
5
+ * @namespace ProblemInitializers
6
+ * @description Fonctions pour générer dynamiquement les données d'un problème.
7
+ */
8
+ const ProblemInitializers = {
9
+ /**
10
+ * Génère un ensemble de points aléatoires pour un problème de TSP.
11
+ * @param {object} params - Les paramètres de génération.
12
+ * @param {number} params.count - Le nombre de points à générer.
13
+ * @param {{x: number, y: number}} [params.bounds={x: 1000, y: 1000}] - Les limites spatiales.
14
+ * @returns {Array<{x: number, y: number}>}
15
+ */
16
+ 'generate:randomPoints': (params) => {
17
+ const { count, bounds = { x: 1000, y: 1000 } } = params;
18
+ if (isNaN(count)) return [];
19
+ return Array.from({ length: count }, () => ({ x: Math.random() * bounds.x, y: Math.random() * bounds.y }));
20
+ },
21
+
22
+ /**
23
+ * Génère un ensemble d'actifs financiers aléatoires pour un problème de portefeuille.
24
+ * @param {object} params - Les paramètres de génération.
25
+ * @param {number} params.count - Le nombre d'actifs à générer.
26
+ * @returns {Array<{expectedReturn: number, volatility: number}>}
27
+ */
28
+ 'generate:randomAssets': (params) => {
29
+ const { count } = params;
30
+ if (isNaN(count)) return [];
31
+ return Array.from({ length: count }, () => ({
32
+ expectedReturn: Math.random() * 0.2,
33
+ volatility: 0.1 + Math.random() * 0.3
34
+ }));
35
+ },
36
+
37
+ /**
38
+ * Crée une fonction qui génère des arguments pour chaque worker de `runMultipleParallel`.
39
+ * Permet de faire varier les paramètres (ex: solution initiale) pour chaque cycle.
40
+ * @param {object} params - Les paramètres de configuration.
41
+ * @param {Array<any>} params.baseArgs - Les arguments de base, communs à tous les workers.
42
+ * @param {object} params.variations - Décrit comment faire varier un argument.
43
+ * @returns {function(number): Array<any>} La fonction `workerDataGenerator`.
44
+ */
45
+ 'generate:parallelArgs': (params) => {
46
+ const { baseArgs, variations } = params;
47
+ return (cycleIndex) => {
48
+ const cycleArgs = [...baseArgs];
49
+ // Pour l'instant, on gère la variation de la solution initiale pour le TSP
50
+ if (variations?.initialSolution === 'random') {
51
+ cycleArgs[0] = cycleArgs[0].sort(() => Math.random() - 0.5);
52
+ }
53
+ return cycleArgs;
54
+ };
55
+ }
56
+ };
57
+
58
+ class ProblemManager {
59
+ constructor(configPath) {
60
+ this.configPath = configPath;
61
+ this.problems = this.loadProblems();
62
+ this.currentProblemIndex = 0;
63
+ }
64
+
65
+ loadProblems() {
66
+ try {
67
+ const data = readFileSync(this.configPath, 'utf-8');
68
+ const problems = JSON.parse(data);
69
+ // Initialisation dynamique des problèmes
70
+ for (const problem of problems) { // eslint-disable-line no-unused-vars
71
+ for (const key in problem.payload) {
72
+ const value = problem.payload[key];
73
+ // On cherche une instruction d'initialisation (ex: { "$init": "generate:randomPoints", ... })
74
+ if (typeof value === 'object' && value !== null && value.$init) {
75
+ const initializer = ProblemInitializers[value.$init];
76
+ if (initializer) {
77
+ // On remplace l'objet d'instruction par les données générées.
78
+ problem.payload[key] = initializer(value.params || {});
79
+ }
80
+ }
81
+ }
82
+ }
83
+ return problems;
84
+ } catch (error) {
85
+ console.error(`[ProblemManager] Erreur lors du chargement du fichier de problèmes: ${error.message}`);
86
+ return []; // Retourne un tableau vide en cas d'erreur pour éviter un crash
87
+ }
88
+ }
89
+
90
+ saveProblems() {
91
+ // Note: Dans un vrai scénario, utilisez une base de données pour éviter les race conditions.
92
+ try {
93
+ writeFileSync(this.configPath, JSON.stringify(this.problems, null, 2));
94
+ } catch (error) {
95
+ console.error(`[ProblemManager] Erreur lors de la sauvegarde du fichier de problèmes: ${error.message}`);
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Sélectionne un problème et génère une unité de travail.
101
+ * @param {number} suspicionFactor - Le facteur de suspicion pour ajuster la difficulté.
102
+ * @returns {{problemId: string, task: object}|null}
103
+ */
104
+ dispatchWork(suspicionFactor) {
105
+ if (this.problems.length === 0) return null;
106
+
107
+ const problem = this.problems[this.currentProblemIndex];
108
+ this.currentProblemIndex = (this.currentProblemIndex + 1) % this.problems.length;
109
+
110
+ const task = { type: problem.workUnit.type };
111
+ const { scalingFactor } = problem.workUnit;
112
+
113
+ switch (problem.workUnit.type) {
114
+ case 'simulated_annealing_iterations':
115
+ // Assurer une difficulté minimale pour que le challenge soit significatif
116
+ const baseIterations = Math.max(15000, problem.workUnit.baseIterations || 0);
117
+ task.iterations = scalingFactor
118
+ ? Math.floor(baseIterations * Math.pow(scalingFactor, suspicionFactor))
119
+ : Math.floor(baseIterations * (0.5 + suspicionFactor));
120
+ task.payload = problem.payload;
121
+ task.initialSolution = problem.state.bestSolution;
122
+ break;
123
+
124
+ case 'genetic_algorithm_generations':
125
+ // Assurer une difficulté minimale pour que le challenge soit significatif
126
+ const baseGenerations = Math.max(50, problem.workUnit.baseGenerations || 0);
127
+ task.generations = scalingFactor
128
+ ? Math.floor(baseGenerations * Math.pow(scalingFactor, suspicionFactor))
129
+ : Math.floor(baseGenerations * (0.5 + suspicionFactor));
130
+ task.payload = problem.payload;
131
+ task.initialPopulation = problem.state.population;
132
+ break;
133
+
134
+ case 'run_multiple_parallel':
135
+ task.solverName = problem.workUnit.solverName;
136
+ task.numCycles = problem.workUnit.numCycles;
137
+ // Les arguments et le générateur sont dans le payload pour plus de flexibilité
138
+ task.baseSolverArgs = problem.payload.baseSolverArgs;
139
+ task.workerDataGenerator = problem.payload.workerDataGenerator;
140
+ task.logProgress = problem.payload.logProgress || false;
141
+ task.concurrency = problem.payload.concurrency;
142
+ break;
143
+ }
144
+
145
+ return { problemId: problem.id, task };
146
+ }
147
+
148
+ /**
149
+ * Intègre la solution d'un client dans l'état du problème.
150
+ * @param {string} problemId - L'ID du problème.
151
+ * @param {object} solutionData - La solution renvoyée par le client.
152
+ */
153
+ integrateSolution(problemId, solutionData) {
154
+ const problem = this.problems.find(p => p.id === problemId);
155
+ if (!problem) return;
156
+
157
+ switch (problem.workUnit.type) {
158
+ case 'simulated_annealing_iterations':
159
+ if (solutionData.energy < (parseFloat(problem.state.bestEnergy) || Infinity)) {
160
+ problem.state.bestSolution = solutionData.solution;
161
+ problem.state.bestEnergy = solutionData.energy;
162
+ console.log(`[ProblemManager] Nouvelle meilleure solution pour ${problemId}: ${solutionData.energy.toFixed(2)}`);
163
+ }
164
+ break;
165
+ case 'genetic_algorithm_generations':
166
+ problem.state.population = solutionData.population;
167
+ console.log(`[ProblemManager] Population mise à jour pour ${problemId}.`);
168
+ break;
169
+ }
170
+ this.saveProblems();
171
+ }
172
+ }
173
+
174
+ export { ProblemManager }; // Export the class for testing
175
+ export const problemManager = new ProblemManager('./problems.config.json');