@anonympins/fingerprint 0.4.2 → 0.4.4

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.
@@ -1,132 +1,132 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint;
6
- use Anonympins\Fingerprint\Config\SecurityProfiles;
7
-
8
- /**
9
- * FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
10
- *
11
- * Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
12
- * dans une application PHP. Elle gère l'injection sécurisée du script et la création
13
- * de "honeypots" (pièges à bots) dans les formulaires.
14
- */
15
- class FingerprintClient
16
- {
17
- /**
18
- * @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
19
- */
20
- private string $clientScriptPath;
21
-
22
- /**
23
- * @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
24
- */
25
- private array $clientConfig;
26
-
27
- /**
28
- * @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
29
- */
30
- private ?string $nonce;
31
-
32
- /**
33
- * Constructeur de la classe.
34
- *
35
- * @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
36
- * @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
37
- */
38
- public function __construct(string $clientScriptPath, array $clientConfig = [])
39
- {
40
- $this->clientScriptPath = $clientScriptPath;
41
-
42
- $defaultConfig = [
43
- 'mouse' => true,
44
- 'keystrokes' => true,
45
- 'clicks' => true,
46
- 'honeypots' => [],
47
- 'fetch' => [
48
- 'handleChallenges' => true,
49
- 'probationaryTtl' => 30000, // 30 seconds
50
- ],
51
- 'wasm' => true, // Activer la tentative de chargement du module WASM
52
- 'wasmPath' => '/fp.js' // Chemin vers le script de chargement WASM
53
- ];
54
-
55
- // Utiliser une fusion profonde pour permettre de surcharger des sous-clés
56
- $this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
57
-
58
- try {
59
- // Génère un nonce pour CSP si possible, pour une sécurité renforcée.
60
- $this->nonce = bin2hex(random_bytes(16));
61
- } catch (\Exception $e) {
62
- $this->nonce = null;
63
- }
64
- }
65
-
66
- /**
67
- * Génère un champ de formulaire "honeypot" caché.
68
- * Les bots le rempliront, mais il sera invisible pour les humains.
69
- *
70
- * @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
71
- * @return string Le code HTML du champ honeypot.
72
- */
73
- public function generateHoneypotField(string $fieldName): string
74
- {
75
- // Ajoute le champ à la configuration pour que le script client le surveille.
76
- if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
77
- $this->clientConfig['honeypots'][] = $fieldName;
78
- }
79
-
80
- // Styles CSS pour cacher le champ de manière robuste.
81
- $styles = 'position:absolute; left:-9999px; top:-9999px; opacity:0;';
82
-
83
- return '<div style="' . $styles . '" aria-hidden="true">'
84
- . '<label for="' . htmlspecialchars($fieldName) . '">Ne pas remplir ce champ</label>'
85
- . '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
86
- . '</div>';
87
- }
88
-
89
- /**
90
- * Génère le bloc de script complet à inclure dans une page HTML.
91
- *
92
- * @return string Le code HTML des balises <script>.
93
- */
94
- public function getScriptTag(): string
95
- {
96
- $configJson = json_encode($this->clientConfig);
97
- $nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
98
-
99
- // Le script d'initialisation qui sera inclus dans la page.
100
- $initScript = <<<JS
101
- document.addEventListener('DOMContentLoaded', function() {
102
- const config = {$configJson};
103
- if (window.ClientLibrary) {
104
- if (config.wasmPath) {
105
- const wasmScript = document.createElement('script');
106
- wasmScript.src = config.wasmPath;
107
- wasmScript.async = true;
108
- wasmScript.nonce = '{$this->nonce}';
109
- document.head.appendChild(wasmScript);
110
- }
111
-
112
- window.ClientLibrary.initializeClient(config);
113
- } else {
114
- console.error('Fingerprint client library not loaded.');
115
- }
116
- });
117
- JS;
118
-
119
- // On combine le chargement de la bibliothèque et le script d'initialisation.
120
- return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
121
- . '<script' . $nonceAttr . '>' . $initScript . '</script>';
122
- }
123
-
124
- /**
125
- * Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
126
- * @return string|null
127
- */
128
- public function getNonce(): ?string
129
- {
130
- return $this->nonce;
131
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint;
6
+ use Anonympins\Fingerprint\Config\SecurityProfiles;
7
+
8
+ /**
9
+ * FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
10
+ *
11
+ * Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
12
+ * dans une application PHP. Elle gère l'injection sécurisée du script et la création
13
+ * de "honeypots" (pièges à bots) dans les formulaires.
14
+ */
15
+ class FingerprintClient
16
+ {
17
+ /**
18
+ * @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
19
+ */
20
+ private string $clientScriptPath;
21
+
22
+ /**
23
+ * @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
24
+ */
25
+ private array $clientConfig;
26
+
27
+ /**
28
+ * @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
29
+ */
30
+ private ?string $nonce;
31
+
32
+ /**
33
+ * Constructeur de la classe.
34
+ *
35
+ * @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
36
+ * @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
37
+ */
38
+ public function __construct(string $clientScriptPath, array $clientConfig = [])
39
+ {
40
+ $this->clientScriptPath = $clientScriptPath;
41
+
42
+ $defaultConfig = [
43
+ 'mouse' => true,
44
+ 'keystrokes' => true,
45
+ 'clicks' => true,
46
+ 'honeypots' => [],
47
+ 'fetch' => [
48
+ 'handleChallenges' => true,
49
+ 'probationaryTtl' => 30000, // 30 seconds
50
+ ],
51
+ 'wasm' => true, // Activer la tentative de chargement du module WASM
52
+ 'wasmPath' => '/fp.js' // Chemin vers le script de chargement WASM
53
+ ];
54
+
55
+ // Utiliser une fusion profonde pour permettre de surcharger des sous-clés
56
+ $this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
57
+
58
+ try {
59
+ // Génère un nonce pour CSP si possible, pour une sécurité renforcée.
60
+ $this->nonce = bin2hex(random_bytes(16));
61
+ } catch (\Exception $e) {
62
+ $this->nonce = null;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Génère un champ de formulaire "honeypot" caché.
68
+ * Les bots le rempliront, mais il sera invisible pour les humains.
69
+ *
70
+ * @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
71
+ * @return string Le code HTML du champ honeypot.
72
+ */
73
+ public function generateHoneypotField(string $fieldName): string
74
+ {
75
+ // Ajoute le champ à la configuration pour que le script client le surveille.
76
+ if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
77
+ $this->clientConfig['honeypots'][] = $fieldName;
78
+ }
79
+
80
+ // Styles CSS pour cacher le champ de manière robuste.
81
+ $styles = 'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;';
82
+
83
+ return '<div style="' . $styles . '" aria-hidden="true">'
84
+ . '<label for="' . htmlspecialchars($fieldName) . '">' . $fieldName . '</label>'
85
+ . '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
86
+ . '</div>';
87
+ }
88
+
89
+ /**
90
+ * Génère le bloc de script complet à inclure dans une page HTML.
91
+ *
92
+ * @return string Le code HTML des balises <script>.
93
+ */
94
+ public function getScriptTag(): string
95
+ {
96
+ $configJson = json_encode($this->clientConfig);
97
+ $nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
98
+
99
+ // Le script d'initialisation qui sera inclus dans la page.
100
+ $initScript = <<<JS
101
+ document.addEventListener('DOMContentLoaded', function() {
102
+ const config = {$configJson};
103
+ if (window.ClientLibrary) {
104
+ if (config.wasmPath) {
105
+ const wasmScript = document.createElement('script');
106
+ wasmScript.src = config.wasmPath;
107
+ wasmScript.async = true;
108
+ wasmScript.nonce = '{$this->nonce}';
109
+ document.head.appendChild(wasmScript);
110
+ }
111
+
112
+ window.ClientLibrary.initializeClient(config);
113
+ } else {
114
+ console.error('Fingerprint client library not loaded.');
115
+ }
116
+ });
117
+ JS;
118
+
119
+ // On combine le chargement de la bibliothèque et le script d'initialisation.
120
+ return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
121
+ . '<script' . $nonceAttr . '>' . $initScript . '</script>';
122
+ }
123
+
124
+ /**
125
+ * Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
126
+ * @return string|null
127
+ */
128
+ public function getNonce(): ?string
129
+ {
130
+ return $this->nonce;
131
+ }
132
132
  }
@@ -417,6 +417,11 @@
417
417
  // Score d'incohérence des Client-Hints
418
418
  $clientHintsInconsistency = RequestUtils::getClientHintsInconsistencyScore($context);
419
419
 
420
+ // Score de similarité globale de l'empreinte (Clustering Botnet)
421
+ $stableFp = RequestUtils::extractStablePart($currentDeviceHash);
422
+ $stableFpHash = FingerprintBuilder::cyrb53($stableFp);
423
+ $botnetCluster = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
424
+
420
425
  // NOUVEAU: Score de réputation du sous-réseau IP
421
426
  $subnetScore = RequestUtils::getSubnetScore($context, $deviceId);
422
427
 
@@ -437,6 +442,7 @@
437
442
  'threatIntelScore' => $threatIntel['threatIntelScore'],
438
443
  'clientHintsInconsistencyScore' => $clientHintsInconsistency['clientHintsInconsistencyScore'],
439
444
  'subnetScore' => $subnetScore['subnetScore'],
445
+ 'botnetClusterScore' => $botnetCluster['botnetClusterScore'],
440
446
  ]);
441
447
 
442
448
  // Sauvegarder l'état mis à jour de l'appareil dans le store
@@ -745,6 +751,26 @@
745
751
  $this->log('High suspicion score detected - overriding valid ticket to re-issue challenge', ['finalScore' => $finalScore, 'deviceId' => $deviceId]);
746
752
  }
747
753
 
754
+ // --- AJOUT: Limiteur de débit (Token Bucket) ---
755
+ $rateLimitPassed = ChallengeUtils::checkChallengeRateLimit($context->clientIp);
756
+ if (!$rateLimitPassed) {
757
+ $this->log('Challenge rate limit exceeded - blocking with 429', ['clientIp' => $context->clientIp]);
758
+ $decision = [
759
+ 'action' => 'block',
760
+ 'status' => 429,
761
+ 'body' => 'Too Many Requests',
762
+ 'score' => $finalScore,
763
+ 'vector' => $suspicionVector
764
+ ];
765
+ if ($this->dryRun) {
766
+ $this->log("[Dry Run] Intended action: {$decision['action']}", ['score' => $decision['score']]);
767
+ $decision['intendedAction'] = $decision['action'];
768
+ $decision['action'] = 'next';
769
+ unset($decision['status'], $decision['body']);
770
+ }
771
+ return $decision;
772
+ }
773
+
748
774
  $decision = ['action' => 'challenge', 'score' => $finalScore, 'vector' => $suspicionVector, 'status' => 403];
749
775
 
750
776
  if ($this->dryRun) {
@@ -793,8 +819,16 @@
793
819
  ]
794
820
  ]
795
821
  ];
796
- $decision['body'] = $challengePayload;
797
- return $decision;
822
+ if ($isApiRequest) {
823
+ $decision['body'] = $challengePayload;
824
+ return $decision;
825
+ } else {
826
+ $html = '<html><body><script>';
827
+ $html .= 'window.location.href = "' . $context->path . '?pow_type=useful_work_task&pow_nonce=' . $nonce . '&pow_problem_id=' . $work['problemId'] . '&pow_solution_work_result=" + encodeURIComponent(JSON.stringify({"solution": [], "energy": 0}));';
828
+ $html .= '</script></body></html>';
829
+ $decision['body'] = $html;
830
+ return $decision;
831
+ }
798
832
  } else {
799
833
  // This case handles when uPoW is enabled but dispatching a task fails (e.g., config not found).
800
834
  // We log it and fall through to the standard PoW challenge.
@@ -1,63 +1,64 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Optimization;
6
-
7
- /**
8
- * Registre pour exposer de manière contrôlée les fonctions de la bibliothèque d'optimisation.
9
- */
10
- class FunctionRegistry
11
- {
12
- /** @var array<string, callable> */
13
- private static array $functions = [];
14
-
15
- /**
16
- * Initialise le registre avec les fonctions disponibles.
17
- */
18
- private static function initialize(): void
19
- {
20
- if (empty(self::$functions)) {
21
- // Fonctions de "Scoring"
22
- self::$functions['tsp.calculateEnergy'] = [OptimizationUtils::class, 'evaluatePathDistance'];
23
- self::$functions['portfolio.calculateMetrics'] = [OptimizationOperators::class, 'createPortfolioAllocator'];
24
-
25
- // Fonctions de "Résolution"
26
- self::$functions['tsp.solve'] = [OptimizationOperators::class, 'solveTSP'];
27
- self::$functions['portfolio.solve'] = [OptimizationOperators::class, 'solvePortfolio'];
28
- self::$functions['fraud.solve'] = [OptimizationOperators::class, 'solveFraudDetection'];
29
- self::$functions['facility.solve'] = [OptimizationOperators::class, 'solveFacilityLocation'];
30
- self::$functions['security.tune'] = [OptimizationOperators::class, 'solveFullSecurityTuning'];
31
- }
32
- }
33
-
34
- /**
35
- * Récupère une fonction depuis le registre.
36
- */
37
- public static function get(string $name): ?callable
38
- {
39
- self::initialize();
40
- return self::$functions[$name] ?? null;
41
- }
42
- /**
43
- * Enregistre une nouvelle fonction. Principalement pour les tests.
44
- * @internal
45
- * @param string $name
46
- * @param callable $function
47
- * @return void
48
- */
49
- public static function register(string $name, callable $function): void
50
- {
51
- self::initialize();
52
- self::$functions[$name] = $function;
53
- }
54
-
55
- /**
56
- * Réinitialise le registre. Uniquement pour les tests.
57
- * @internal
58
- */
59
- public static function __internal_resetRegistry(): void
60
- {
61
- self::$functions = [];
62
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Optimization;
6
+
7
+ /**
8
+ * Registre pour exposer de manière contrôlée les fonctions de la bibliothèque d'optimisation.
9
+ */
10
+ class FunctionRegistry
11
+ {
12
+ /** @var array<string, callable> */
13
+ private static array $functions = [];
14
+
15
+ /**
16
+ * Initialise le registre avec les fonctions disponibles.
17
+ */
18
+ private static function initialize(): void
19
+ {
20
+ if (empty(self::$functions)) {
21
+ // Fonctions de "Scoring"
22
+ self::$functions['tsp.calculateEnergy'] = [OptimizationUtils::class, 'evaluatePathDistance'];
23
+ self::$functions['portfolio.calculateMetrics'] = [OptimizationOperators::class, 'createPortfolioAllocator'];
24
+ self::$functions['facility.calculateEnergy'] = [OptimizationOperators::class, 'evaluateFacilityLocation'];
25
+
26
+ // Fonctions de "Résolution"
27
+ self::$functions['tsp.solve'] = [OptimizationOperators::class, 'solveTSP'];
28
+ self::$functions['portfolio.solve'] = [OptimizationOperators::class, 'solvePortfolio'];
29
+ self::$functions['fraud.solve'] = [OptimizationOperators::class, 'solveFraudDetection'];
30
+ self::$functions['facility.solve'] = [OptimizationOperators::class, 'solveFacilityLocation'];
31
+ self::$functions['security.tune'] = [OptimizationOperators::class, 'solveFullSecurityTuning'];
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Récupère une fonction depuis le registre.
37
+ */
38
+ public static function get(string $name): ?callable
39
+ {
40
+ self::initialize();
41
+ return self::$functions[$name] ?? null;
42
+ }
43
+ /**
44
+ * Enregistre une nouvelle fonction. Principalement pour les tests.
45
+ * @internal
46
+ * @param string $name
47
+ * @param callable $function
48
+ * @return void
49
+ */
50
+ public static function register(string $name, callable $function): void
51
+ {
52
+ self::initialize();
53
+ self::$functions[$name] = $function;
54
+ }
55
+
56
+ /**
57
+ * Réinitialise le registre. Uniquement pour les tests.
58
+ * @internal
59
+ */
60
+ public static function __internal_resetRegistry(): void
61
+ {
62
+ self::$functions = [];
63
+ }
63
64
  }