@anonympins/fingerprint 0.4.3 → 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) {
@@ -1,91 +1,91 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint;
6
-
7
- /**
8
- * Représente le contexte d'une requête HTTP, fournissant un accès unifié
9
- * aux informations nécessaires pour l'analyse de l'empreinte.
10
- * Cette classe est conçue pour être créée à partir d'un objet de requête
11
- * standard (ex: PSR-7, Symfony, Laravel).
12
- */
13
- class RequestContext
14
- {
15
- public string $clientIp;
16
- public string $path;
17
- /** @var array<string, string> */
18
- public array $headers;
19
- /** @var array<string, mixed> */
20
- public array $query;
21
- /** @var array<string, mixed>|object|null */
22
- public $body;
23
- /** @var array<string, string> */
24
- public array $cookies;
25
- public ?string $httpVersion;
26
- public int $requestTimestamp;
27
-
28
- /** @var ?array{type: string, name: string} */
29
- public ?array $graphqlOperation = null;
30
-
31
- /** @var ?array<string, mixed> */
32
- public ?array $newCookieForResponse = null;
33
-
34
- // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
35
- public ?string $ja3;
36
- public ?string $ja4;
37
- public ?string $ja4s;
38
- public ?string $ja4h;
39
- public ?string $http2Fingerprint;
40
- public ?string $tcpFingerprint;
41
-
42
- /**
43
- * @param string $clientIp
44
- * @param string $path
45
- * @param array<string, string> $headers
46
- * @param array<string, mixed> $query
47
- * @param array<string, mixed>|object|null $body
48
- * @param array<string, string> $cookies
49
- * @param string|null $httpVersion
50
- * @param int|null $requestTimestamp
51
- */
52
- public function __construct(
53
- string $clientIp,
54
- string $path,
55
- array $headers,
56
- array $query,
57
- $body,
58
- array $cookies,
59
- ?string $httpVersion,
60
- ?int $requestTimestamp = null
61
- ) {
62
- $this->clientIp = $clientIp;
63
- $this->path = $path;
64
- // Normaliser les en-têtes en minuscules pour un accès cohérent
65
- $this->headers = array_change_key_case($headers, CASE_LOWER);
66
- $this->query = $query;
67
- $this->body = $body;
68
- $this->cookies = $cookies;
69
- $this->httpVersion = $httpVersion;
70
- $this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
71
-
72
- // Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
73
- $this->ja3 = $this->headers['x-ja3-hash'] ?? null;
74
- $this->ja4 = $this->headers['x-ja4-hash'] ?? null;
75
- $this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
76
- $this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
77
- $this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
78
- $this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
79
- }
80
- /**
81
- * Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
82
- *
83
- * @param string $name Le nom de l'en-tête.
84
- * @return string|null La valeur de l'en-tête ou null si non trouvé.
85
- */
86
-
87
- public function getHeader(string $name): ?string
88
- {
89
- return $this->headers[strtolower($name)] ?? null;
90
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint;
6
+
7
+ /**
8
+ * Représente le contexte d'une requête HTTP, fournissant un accès unifié
9
+ * aux informations nécessaires pour l'analyse de l'empreinte.
10
+ * Cette classe est conçue pour être créée à partir d'un objet de requête
11
+ * standard (ex: PSR-7, Symfony, Laravel).
12
+ */
13
+ class RequestContext
14
+ {
15
+ public string $clientIp = '';
16
+ public string $path = '';
17
+ /** @var array<string, string> */
18
+ public array $headers = [];
19
+ /** @var array<string, mixed> */
20
+ public array $query = [];
21
+ /** @var array<string, mixed>|object|null */
22
+ public $body = null;
23
+ /** @var array<string, string> */
24
+ public array $cookies = [];
25
+ public ?string $httpVersion = null;
26
+ public int $requestTimestamp = 0;
27
+
28
+ /** @var ?array{type: string, name: string} */
29
+ public ?array $graphqlOperation = null;
30
+
31
+ /** @var ?array<string, mixed> */
32
+ public ?array $newCookieForResponse = null;
33
+
34
+ // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
35
+ public ?string $ja3 = null;
36
+ public ?string $ja4 = null;
37
+ public ?string $ja4s = null;
38
+ public ?string $ja4h = null;
39
+ public ?string $http2Fingerprint = null;
40
+ public ?string $tcpFingerprint = null;
41
+
42
+ /**
43
+ * @param string $clientIp
44
+ * @param string $path
45
+ * @param array<string, string> $headers
46
+ * @param array<string, mixed> $query
47
+ * @param array<string, mixed>|object|null $body
48
+ * @param array<string, string> $cookies
49
+ * @param string|null $httpVersion
50
+ * @param int|null $requestTimestamp
51
+ */
52
+ public function __construct(
53
+ string $clientIp,
54
+ string $path,
55
+ array $headers,
56
+ array $query,
57
+ $body,
58
+ array $cookies,
59
+ ?string $httpVersion,
60
+ ?int $requestTimestamp = null
61
+ ) {
62
+ $this->clientIp = $clientIp;
63
+ $this->path = $path;
64
+ // Normaliser les en-têtes en minuscules pour un accès cohérent
65
+ $this->headers = array_change_key_case($headers, CASE_LOWER);
66
+ $this->query = $query;
67
+ $this->body = $body;
68
+ $this->cookies = $cookies;
69
+ $this->httpVersion = $httpVersion;
70
+ $this->requestTimestamp = $requestTimestamp ?? (int)(microtime(true) * 1000);
71
+
72
+ // Extraire les empreintes TLS/HTTP2/TCP si elles sont fournies par les en-têtes
73
+ $this->ja3 = $this->headers['x-ja3-hash'] ?? null;
74
+ $this->ja4 = $this->headers['x-ja4-hash'] ?? null;
75
+ $this->ja4s = $this->headers['x-ja4s-hash'] ?? null;
76
+ $this->ja4h = $this->headers['x-ja4h-hash'] ?? null;
77
+ $this->http2Fingerprint = $this->headers['x-http2-fingerprint'] ?? null;
78
+ $this->tcpFingerprint = $this->headers['x-tcp-fingerprint'] ?? null;
79
+ }
80
+ /**
81
+ * Récupère la valeur d'un en-tête HTTP de manière insensible à la casse.
82
+ *
83
+ * @param string $name Le nom de l'en-tête.
84
+ * @return string|null La valeur de l'en-tête ou null si non trouvé.
85
+ */
86
+
87
+ public function getHeader(string $name): ?string
88
+ {
89
+ return $this->headers[strtolower($name)] ?? null;
90
+ }
91
91
  }
@@ -1,42 +1,42 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Store;
6
-
7
- /**
8
- * Interface pour un système de stockage persistant.
9
- * Utilisé pour stocker les données des appareils, les secrets des challenges, etc.
10
- */
11
- interface IStore
12
- {
13
- /**
14
- * Récupère une valeur associée à une clé.
15
- * @param string $key
16
- * @return mixed|null
17
- */
18
- public function get(string $key);
19
-
20
- /**
21
- * Stocke une valeur associée à une clé, avec une durée de vie optionnelle.
22
- * @param string $key
23
- * @param mixed $value
24
- * @param int|null $ttl Durée de vie en secondes.
25
- * @return void
26
- */
27
- public function set(string $key, $value, ?int $ttl = null): void;
28
-
29
- /**
30
- * Vérifie si une clé existe dans le stockage.
31
- * @param string $key
32
- * @return bool
33
- */
34
- public function has(string $key): bool;
35
-
36
- /**
37
- * Supprime une clé du stockage.
38
- * @param string $key
39
- * @return void
40
- */
41
- public function delete(string $key): void;
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Store;
6
+
7
+ /**
8
+ * Interface pour un système de stockage persistant.
9
+ * Utilisé pour stocker les données des appareils, les secrets des challenges, etc.
10
+ */
11
+ interface IStore
12
+ {
13
+ /**
14
+ * Récupère une valeur associée à une clé.
15
+ * @param string $key
16
+ * @return mixed|null
17
+ */
18
+ public function get(string $key);
19
+
20
+ /**
21
+ * Stocke une valeur associée à une clé, avec une durée de vie optionnelle.
22
+ * @param string $key
23
+ * @param mixed $value
24
+ * @param int|null $ttl Durée de vie en secondes.
25
+ * @return void
26
+ */
27
+ public function set(string $key, $value, ?int $ttl = null): void;
28
+
29
+ /**
30
+ * Vérifie si une clé existe dans le stockage.
31
+ * @param string $key
32
+ * @return bool
33
+ */
34
+ public function has(string $key): bool;
35
+
36
+ /**
37
+ * Supprime une clé du stockage.
38
+ * @param string $key
39
+ * @return void
40
+ */
41
+ public function delete(string $key): void;
42
42
  }