@anonympins/fingerprint 0.2.0 → 0.2.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/library.js CHANGED
@@ -1583,29 +1583,104 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
1583
1583
  * @returns {Array<{solution: object, objectives: number[]}>} Le front de Pareto des configurations optimales.
1584
1584
  */
1585
1585
  Optimization.Operators.solveFullSecurityTuning = (context, options = {}) => {
1586
- const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context); // eslint-disable-line
1586
+ const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator(context);
1587
1587
 
1588
1588
  // Un "individu" est un objet de configuration complet
1589
1589
  const createIndividual = () => ({
1590
1590
  thresholds: {
1591
- low: 10 + Math.random() * 30, // 10-40
1592
- medium: 40 + Math.random() * 30, // 40-70
1593
- high: 70 + Math.random() * 25, // 70-95
1591
+ low: 15 + Math.random() * 20, // 15-35
1592
+ medium: 40 + Math.random() * 25, // 40-65
1593
+ high: 70 + Math.random() * 20, // 70-90
1594
1594
  },
1595
1595
  weights: {
1596
1596
  historyScore: Math.random(),
1597
1597
  rotationScore: Math.random(),
1598
1598
  headerAnomalyScore: Math.random(),
1599
- requestPatternScore: Math.random(),
1599
+ requestPatternScore: 0.5 + Math.random(), // Donner plus d'importance aux patterns
1600
1600
  inconsistencyScore: Math.random(),
1601
1601
  honeypotScore: 1.0, // Garder le honeypot à 1.0 est une bonne pratique
1602
+ behaviorScore: Math.random(),
1603
+ crossLayerInconsistencyScore: Math.random(),
1604
+ timeInconsistencyScore: Math.random(),
1602
1605
  },
1603
- // On pourrait aussi faire muter les `patterns` ici
1606
+ patterns: {
1607
+ velocityThreshold: 100 + Math.random() * 400, // 100-500ms
1608
+ velocityWeight: 10 + Math.random() * 40,
1609
+ burstThreshold: 300 + Math.random() * 700, // 300-1000ms
1610
+ burstWeight: 20 + Math.random() * 40,
1611
+ scrapeThreshold: 500 + Math.random() * 1000, // 500-1500ms
1612
+ scrapeWeight: 15 + Math.random() * 35,
1613
+ sequenceLength: 3 + Math.floor(Math.random() * 3), // 3-5
1614
+ sequenceWeight: 20 + Math.random() * 50,
1615
+ regularityThreshold: 50 + Math.random() * 200, // 50-250ms
1616
+ regularityWeight: 20 + Math.random() * 40,
1617
+ decayFactor: 0.85 + Math.random() * 0.14, // 0.85-0.99
1618
+ inactivityReset: 15000 + Math.random() * 45000, // 15s-60s
1619
+ }
1604
1620
  });
1605
1621
 
1606
1622
  // Le crossover et la mutation doivent maintenant opérer sur des objets complexes
1607
- const crossover = (c1, c2) => { /* ... logique de croisement pour les objets de config ... */ return c1; }; // eslint-disable-line
1608
- const mutate = (c) => { /* ... logique de mutation pour les objets de config ... */ return c; }; // eslint-disable-line
1623
+ const crossover = (c1, c2) => {
1624
+ const child = JSON.parse(JSON.stringify(c1)); // Deep copy
1625
+ // Croisement pour chaque groupe de paramètres
1626
+ for (const key in child.thresholds) {
1627
+ child.thresholds[key] = (c1.thresholds[key] + c2.thresholds[key]) / 2;
1628
+ }
1629
+ for (const key in child.weights) {
1630
+ if (key !== 'honeypotScore') { // Ne pas croiser le poids du honeypot
1631
+ child.weights[key] = (c1.weights[key] + c2.weights[key]) / 2;
1632
+ }
1633
+ }
1634
+ for (const key in child.patterns) {
1635
+ child.patterns[key] = (c1.patterns[key] + c2.patterns[key]) / 2;
1636
+ }
1637
+ return child;
1638
+ };
1639
+
1640
+ const mutate = (c) => {
1641
+ const newConfig = JSON.parse(JSON.stringify(c));
1642
+
1643
+ // --- NOUVELLE LOGIQUE : Sélection de section pondérée ---
1644
+ // On donne plus de poids à la mutation des 'patterns' et des 'weights',
1645
+ // car ils ont un impact plus direct sur la détection que les seuils.
1646
+ const sections = [
1647
+ { name: 'patterns', weight: 0.5 }, // 50% de chance
1648
+ { name: 'weights', weight: 0.35 }, // 35% de chance
1649
+ { name: 'thresholds', weight: 0.15 } // 15% de chance
1650
+ ];
1651
+ const rand = Math.random();
1652
+ let cumulativeWeight = 0;
1653
+ let sectionToMutate = 'patterns'; // Fallback
1654
+ for (const section of sections) {
1655
+ cumulativeWeight += section.weight;
1656
+ if (rand < cumulativeWeight) {
1657
+ sectionToMutate = section.name;
1658
+ break;
1659
+ }
1660
+ }
1661
+
1662
+ const keys = Object.keys(newConfig[sectionToMutate]);
1663
+ const keyToMutate = keys[Math.floor(Math.random() * keys.length)];
1664
+
1665
+ if (keyToMutate === 'honeypotScore') return newConfig; // Ne pas muter le poids du honeypot
1666
+
1667
+ // Appliquer une mutation avec une amplitude variable
1668
+ const mutationAmount = (Math.random() - 0.5) * 0.4; // +/- 20%
1669
+ newConfig[sectionToMutate][keyToMutate] *= (1 + mutationAmount);
1670
+
1671
+ // S'assurer que les valeurs restent dans des limites raisonnables
1672
+ if (sectionToMutate === 'weights') {
1673
+ newConfig[sectionToMutate][keyToMutate] = Math.max(0, Math.min(1.5, newConfig[sectionToMutate][keyToMutate]));
1674
+ }
1675
+ if (keyToMutate === 'decayFactor') {
1676
+ newConfig.patterns.decayFactor = Math.max(0.8, Math.min(0.999, newConfig.patterns.decayFactor));
1677
+ }
1678
+ if (keyToMutate.includes('Threshold') || keyToMutate.includes('Reset')) {
1679
+ newConfig.patterns[keyToMutate] = Math.max(50, newConfig.patterns[keyToMutate]);
1680
+ }
1681
+
1682
+ return newConfig;
1683
+ };
1609
1684
 
1610
1685
  return Optimization.geneticAlgorithmMultiObjective(
1611
1686
  createIndividual,
package/mongodb-store.js CHANGED
@@ -1,53 +1,53 @@
1
- /**
2
- * @file Creates a store adapter for MongoDB.
3
- * This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
4
- * for automatic expiration of documents.
5
- */
6
-
7
- /**
8
- * Creates a store adapter for a MongoDB collection.
9
- * It's recommended to pass the `db` object and let the adapter handle the collection.
10
- *
11
- * **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
12
- * In the mongo shell, run:
13
- * `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
14
- *
15
- * @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
16
- * @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
17
- * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
18
- */
19
- export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
20
- const collection = db.collection(collectionName);
21
-
22
- return {
23
- async get(key) {
24
- const doc = await collection.findOne({ _id: key });
25
- // The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
26
- return doc ? doc.value : null;
27
- },
28
- async set(key, value, ttl) {
29
- const doc = {
30
- _id: key,
31
- value: value,
32
- };
33
-
34
- if (ttl && ttl > 0) {
35
- // Set the expiration date for the TTL index.
36
- doc.expiresAt = new Date(Date.now() + ttl * 1000);
37
- }
38
-
39
- await collection.updateOne(
40
- { _id: key },
41
- { $set: doc },
42
- { upsert: true }
43
- );
44
- },
45
- async has(key) {
46
- const count = await collection.countDocuments({ _id: key });
47
- return count > 0;
48
- },
49
- async delete(key) {
50
- await collection.deleteOne({ _id: key });
51
- },
52
- };
1
+ /**
2
+ * @file Creates a store adapter for MongoDB.
3
+ * This adapter uses a collection as a key-value store and leverages MongoDB's TTL indexes
4
+ * for automatic expiration of documents.
5
+ */
6
+
7
+ /**
8
+ * Creates a store adapter for a MongoDB collection.
9
+ * It's recommended to pass the `db` object and let the adapter handle the collection.
10
+ *
11
+ * **Note:** For TTL to work, you must create a TTL index on the `expiresAt` field in your collection.
12
+ * In the mongo shell, run:
13
+ * `db.yourCollectionName.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })`
14
+ *
15
+ * @param {import('mongodb').Db} db - An instance of a MongoDB Db object.
16
+ * @param {string} [collectionName='fingerprint_store'] - The name of the collection to use.
17
+ * @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
18
+ */
19
+ export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
20
+ const collection = db.collection(collectionName);
21
+
22
+ return {
23
+ async get(key) {
24
+ const doc = await collection.findOne({ _id: key });
25
+ // The TTL index automatically removes expired documents, so no need to check `expiresAt` here.
26
+ return doc ? doc.value : null;
27
+ },
28
+ async set(key, value, ttl) {
29
+ const doc = {
30
+ _id: key,
31
+ value: value,
32
+ };
33
+
34
+ if (ttl && ttl > 0) {
35
+ // Set the expiration date for the TTL index.
36
+ doc.expiresAt = new Date(Date.now() + ttl * 1000);
37
+ }
38
+
39
+ await collection.updateOne(
40
+ { _id: key },
41
+ { $set: doc },
42
+ { upsert: true }
43
+ );
44
+ },
45
+ async has(key) {
46
+ const count = await collection.countDocuments({ _id: key });
47
+ return count > 0;
48
+ },
49
+ async delete(key) {
50
+ await collection.deleteOne({ _id: key });
51
+ },
52
+ };
53
53
  }
@@ -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.2",
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) {
@@ -323,70 +339,110 @@ export async function solveOptimizationTask(initialPopulation, generations) {
323
339
  return population;
324
340
  }
325
341
 
342
+ /**
343
+ * @class ChallengeSolution
344
+ * @description Encapsule une solution de challenge et fournit des méthodes pour la manipuler.
345
+ * @private
346
+ */
347
+ class ChallengeSolution {
348
+ constructor(type, nonce, rawSolution) {
349
+ this.type = type;
350
+ this.nonce = nonce;
351
+ this.rawSolution = rawSolution;
352
+ }
353
+
354
+ /**
355
+ * Applique les paramètres de la solution à un objet URL.
356
+ * @param {URL} url - L'objet URL à modifier.
357
+ */
358
+ applyToUrl(url) {
359
+ url.searchParams.set('pow_type', this.type);
360
+ url.searchParams.set('pow_nonce', this.nonce);
361
+
362
+ // Logique de formatage spécifique à chaque type de challenge
363
+ if (this.type === 'cpu_mem' || this.type === 'cpu_mem_inline' || this.type === 'cpu_target') {
364
+ Object.entries(this.rawSolution).forEach(([key, value]) => {
365
+ url.searchParams.set(`pow_solution_${key}`, String(value));
366
+ });
367
+ } else if (this.type === 'useful_work_task') {
368
+ url.searchParams.set('pow_solution_work_result', JSON.stringify(this.rawSolution.work_result));
369
+ url.searchParams.set('pow_problem_id', this.rawSolution.problem_id);
370
+ } else {
371
+ // Pour les cas simples comme 'tsp' où la solution est une seule valeur
372
+ url.searchParams.set('pow_solution', JSON.stringify(this.rawSolution));
373
+ }
374
+ }
375
+ }
376
+
326
377
  /**
327
378
  * Fonction principale qui reçoit un objet challenge et le résout.
328
379
  * @param {object} challenge - L'objet challenge reçu du serveur.
329
- * @returns {Promise<object>} Un objet contenant la ou les solutions.
380
+ * @param {string} [fingerprint=''] - L'empreinte de l'appareil qui résout le challenge.
381
+ * @returns {Promise<ChallengeSolution>} Un objet `ChallengeSolution` encapsulant le résultat.
330
382
  */
331
- export async function solveChallenge(challenge) {
383
+ export async function solveChallenge(challenge, fingerprint = '') { // The fingerprint is now passed from the client library
332
384
  const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
333
- const solutions = {};
385
+ let rawSolution = {};
334
386
 
335
387
  switch (type) {
336
388
  case 'cpu_target':
337
- const baseMessageCpu = `:${nonce}`; // L'IP est gérée côté serveur
389
+ // Note: This case is not fully exercised by tests as it relies on Web Workers.
338
390
  if (!cpuTarget) {
339
391
  throw new Error("Challenge data is missing 'cpuTarget' property.");
340
392
  }
341
393
  const target = cpuTarget; // Keep variable name for consistency below
342
- solutions.cpu = await solveCpuTarget(baseMessageCpu, BigInt('0x' + target));
394
+ // Pour ce challenge simple, le baseBlock est juste le nonce.
395
+ const baseBlockBytes = new TextEncoder().encode(nonce + ":");
396
+ rawSolution.cpu = await solveCpuTargetInline(baseBlockBytes, target, null);
343
397
  break;
344
398
  case 'cpu_mem':
345
399
  // Pour les appels API, le client IP n'est pas connu, on ne le met pas dans le message
346
- const baseMessageCombined = `${nonce}:${clientSecret}`;
400
+ const baseMessageCombined = `:${nonce}:${clientSecret}`;
347
401
  const memSeed = `:${nonce}:${clientSecret}`;
348
402
  const [cpuSol, memSol] = await Promise.all([
349
403
  (async () => {
350
- if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
351
- return solveCpuTargetInline(null, nonce, cpuTarget, clientSecret);
404
+ if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property."); // Pass fingerprint to solver
405
+ const baseBlock = new Uint8Array(challenge.baseBlock);
406
+ return solveCpuTargetInline(baseBlock, cpuTarget, null);
352
407
  })(),
353
408
  solveMemory(memSeed, memDifficulty)
354
409
  ]);
355
- solutions.cpu = cpuSol;
356
- solutions.mem = memSol;
410
+ rawSolution.cpu = cpuSol;
411
+ rawSolution.mem = memSol;
357
412
  break;
358
413
  case 'cpu_mem_inline':
359
414
  // Version inline pour compatibilité HTML avec IP incluse
360
- const memSeedInline = `${nonce}:${clientSecret}`;
415
+ const memSeedInline = `:${nonce}:${clientSecret}`;
361
416
  const [cpuSolInline, memSolInline] = await Promise.all([
362
417
  (async () => {
363
418
  if (!cpuTarget) throw new Error("Challenge data is missing 'cpuTarget' property.");
364
- return solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
419
+ const baseBlock = new Uint8Array(challenge.baseBlock);
420
+ return solveCpuTargetInline(baseBlock, cpuTarget, null);
365
421
  })(),
366
422
  solveMemory(memSeedInline, memDifficulty)
367
423
  ]);
368
- solutions.cpu = cpuSolInline;
369
- solutions.mem = memSolInline;
424
+ rawSolution.cpu = cpuSolInline;
425
+ rawSolution.mem = memSolInline;
370
426
  break;
371
427
  case 'tsp':
372
428
  const tspResult = await solveTsp(cities, targetMaxDistance);
373
- solutions.tsp = tspResult.path;
374
- solutions.distance = tspResult.distance;
429
+ // Pour ce challenge, la solution est juste le chemin.
430
+ rawSolution = tspResult.path;
375
431
  break;
376
432
  case 'optimization_task':
377
433
  const finalPopulation = await solveOptimizationTask(optimizationTask.population, optimizationTask.generations);
378
- solutions.population = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
434
+ rawSolution = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
379
435
  break;
380
436
  case 'useful_work_task':
381
437
  const workResult = await solveUsefulWorkTask(usefulWorkTask.task);
382
- solutions.work_result = workResult;
383
- solutions.problem_id = usefulWorkTask.problemId;
438
+ rawSolution.work_result = workResult;
439
+ rawSolution.problem_id = usefulWorkTask.problemId;
384
440
  break;
385
441
  default:
386
442
  throw new Error(`Unknown challenge type: ${type}`);
387
443
  }
388
444
 
389
- return solutions;
445
+ return new ChallengeSolution(type, nonce, rawSolution);
390
446
  }
391
447
 
392
448
  // --- Compatibilité pour l'injection directe dans le HTML ---
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
+ };