@anonympins/fingerprint 0.7.0 → 0.7.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/src/js/build-client.js +8 -10
  7. package/src/js/fingerprint.builder.js +37 -38
  8. package/src/js/fingerprint.client.js +1531 -1463
  9. package/src/js/fingerprint.client.obfuscated.js +1 -0
  10. package/src/js/fingerprint.js +190 -16
  11. package/src/js/fingerprint.utils.js +213 -213
  12. package/src/js/library.js +340 -376
  13. package/src/js/mongodb-store.js +1 -1
  14. package/src/js/optimization.worker.js +27 -27
  15. package/src/js/pow.solver.inline.js +154 -56
  16. package/src/js/pow.solver.js +180 -92
  17. package/src/js/pow.worker.js +48 -48
  18. package/src/js/redis-store.js +1 -1
  19. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  20. package/src/js/tests/fingerprint.test.js +124 -10
  21. package/src/js/tests/ip-reputation.test.js +1 -0
  22. package/src/js/upow-model-task.js +14 -15
  23. package/src/php/AutoTuner.php +34 -34
  24. package/src/php/Challenge/ChallengeUtils.php +57 -11
  25. package/src/php/Config/SecurityProfiles.php +20 -0
  26. package/src/php/DirectFingerprint.php +190 -145
  27. package/src/php/FingerprintBuilder.php +29 -30
  28. package/src/php/FingerprintClient.php +147 -148
  29. package/src/php/FingerprintEngine.php +44 -17
  30. package/src/php/Ja3AnomalyDetector.php +33 -32
  31. package/src/php/Optimization/FunctionRegistry.php +7 -7
  32. package/src/php/Optimization/Optimization.php +24 -24
  33. package/src/php/Optimization/OptimizationOperators.php +26 -25
  34. package/src/php/Optimization/ProblemInitializers.php +52 -52
  35. package/src/php/ProblemManager.php +31 -34
  36. package/src/php/RequestContext.php +8 -1
  37. package/src/php/Store/IStore.php +7 -8
  38. package/src/php/Store/InMemoryStore.php +66 -66
  39. package/src/php/Store/MongoDbStore.php +5 -5
  40. package/src/php/Store/RedisStore.php +5 -5
  41. package/src/php/Store/StoreManager.php +35 -35
  42. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  43. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  44. package/src/php/Tests/FingerprintEngineTest.php +38 -0
  45. package/src/php/Tests/IpReputationTest.php +10 -0
  46. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  47. package/src/php/Tests/MetricsTest.php +97 -97
  48. package/src/php/Tests/ProblemManagerTest.php +376 -376
  49. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  50. package/src/php/Tests/RequestUtilsTest.php +471 -471
  51. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  52. package/src/php/Tests/config/ed25519_key.json +3 -3
  53. package/src/php/Utils/BlockList.php +99 -99
  54. package/src/php/Utils/Env.php +49 -49
  55. package/src/php/Utils/MaliciousPatterns.php +74 -74
  56. package/src/php/Utils/MetricsManager.php +209 -209
  57. package/src/php/Utils/RequestUtils.php +129 -7
  58. package/src/php/Utils/TLSClientHelloParser.php +369 -369
  59. package/src/php/WordPress/WpDbStore.php +155 -0
  60. package/src/php/WordPress/fingerprint-wordpress.php +554 -0
  61. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  62. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
  63. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  64. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
  65. package/src/php/WordPress/package.php +218 -0
  66. package/src/php/bin/auto-tune.php +118 -118
@@ -1,146 +1,191 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint;
6
-
7
- use Anonympins\Fingerprint\Utils\MetricsManager;
8
-
9
- /**
10
- * Intégration directe du moteur de fingerprinting pour les applications PHP sans framework PSR.
11
- * Cette classe interagit directement avec les superglobales PHP et les fonctions de réponse.
12
- */
13
- class DirectFingerprint
14
- {
15
- private array $securityConfig;
16
- private FingerprintEngine $engine;
17
-
18
- /**
19
- * @param array $securityConfig La configuration de sécurité pour le moteur.
20
- */
21
- public function __construct(array $securityConfig)
22
- {
23
- $this->engine = new FingerprintEngine($securityConfig);
24
- $this->securityConfig = $securityConfig;
25
- }
26
-
27
- /**
28
- * Protège le point d'entrée actuel.
29
- * Analyse la requête entrante et, si nécessaire, envoie une réponse de challenge/blocage et termine le script.
30
- * Si la requête est autorisée, la méthode retourne simplement et le reste du script peut s'exécuter.
31
- *
32
- * @return array{score: float, vector: array}|null Les données du fingerprint si la requête est autorisée, null sinon.
33
- */
34
- public function protect(): ?array
35
- {
36
- // 1. Construire le contexte de la requête à partir des superglobales PHP.
37
- $body = $_POST ?: json_decode(file_get_contents('php://input'), true);
38
- $headers = function_exists('getallheaders') ? getallheaders() : [];
39
-
40
- $context = new RequestContext(
41
- $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
42
- parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
43
- $headers,
44
- $_GET,
45
- $body,
46
- $_COOKIE,
47
- $_SERVER['SERVER_PROTOCOL'] ?? '1.1'
48
- );
49
-
50
- // 2. Traiter la requête avec le moteur.
51
- $decision = $this->engine->processRequest($context);
52
-
53
- // 3. Agir sur la décision.
54
- if (isset($context->newCookieForResponse)) {
55
- $cookie = $context->newCookieForResponse;
56
- setcookie($cookie['name'], $cookie['value'], $cookie['options']);
57
- }
58
-
59
- switch ($decision['action']) {
60
- case 'block':
61
- case 'challenge':
62
- http_response_code($decision['status'] ?? 403);
63
- if (is_array($decision['body'])) {
64
- header('Content-Type: application/json');
65
- echo json_encode($decision['body']);
66
- } else {
67
- header('Content-Type: text/html; charset=utf-8');
68
- echo $decision['body'];
69
- }
70
- exit(); // Termine le script.
71
-
72
- case 'redirect':
73
- if (isset($decision['cookie'])) {
74
- setcookie($decision['cookie']['name'], $decision['cookie']['value'], $decision['cookie']['options']);
75
- }
76
- header('Location: ' . $decision['path'], true, 302);
77
- exit(); // Termine le script.
78
-
79
- case 'next':
80
- default:
81
- // La requête est autorisée, on retourne les informations du fingerprint.
82
- return ['score' => $decision['score'], 'vector' => $decision['vector']];
83
- }
84
- }
85
-
86
- /**
87
- * Handles a request to the /metrics endpoint, applying authorization rules.
88
- * If metrics are enabled and authorized, it outputs Prometheus formatted metrics and exits.
89
- * Otherwise, it handles unauthorized access or returns a 404 if metrics are not enabled.
90
- *
91
- * @param RequestContext $context The current request context.
92
- */
93
- public function handleMetricsRequest(RequestContext $context): void
94
- {
95
- // 2. Appliquer le callback d'autorisation personnalisé si défini.
96
- $authorizationCallback = $this->securityConfig['metricsAuthorizationCallback'] ?? null;
97
- if (is_callable($authorizationCallback)) {
98
- $decision = call_user_func($authorizationCallback, $context);
99
-
100
- if (is_bool($decision)) {
101
- if (!$decision) {
102
- http_response_code(403); // Forbidden
103
- echo "Access to metrics denied.";
104
- exit();
105
- }
106
- } elseif (is_array($decision) && isset($decision['action'])) {
107
- switch ($decision['action']) {
108
- case 'block':
109
- http_response_code($decision['status'] ?? 403);
110
- echo $decision['body'] ?? "Access denied.";
111
- exit();
112
- case 'redirect':
113
- header('Location: ' . $decision['path'], true, $decision['status'] ?? 302);
114
- exit();
115
- case 'next':
116
- // Autorisé, continuer pour servir les métriques
117
- break;
118
- default:
119
- // Action inconnue, refuser par défaut
120
- http_response_code(403);
121
- echo "Invalid authorization decision.";
122
- exit();
123
- }
124
- } else {
125
- // Retour inattendu du callback, refuser par défaut
126
- http_response_code(403);
127
- echo "Invalid authorization callback response.";
128
- exit();
129
- }
130
- }
131
-
132
- // 3. Si autorisé, servir les métriques.
133
- header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
134
- echo MetricsManager::getPrometheusMetrics();
135
- exit();
136
- }
137
-
138
- /**
139
- * Returns Prometheus formatted metrics if enabled in the security configuration.
140
- * @return string|null
141
- */
142
- public function getPrometheusMetrics(): ?string
143
- {
144
- return MetricsManager::getPrometheusMetrics();
145
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint;
6
+
7
+ use Anonympins\Fingerprint\Utils\MetricsManager;
8
+
9
+ /**
10
+ * Direct integration of the fingerprint engine for PHP applications without a PSR framework.
11
+ * Interacts directly with PHP superglobals and response headers.
12
+ */
13
+ class DirectFingerprint
14
+ {
15
+ private array $securityConfig;
16
+ private FingerprintEngine $engine;
17
+
18
+ /**
19
+ * @param array $securityConfig Security configuration for the engine.
20
+ */
21
+ public function __construct(array $securityConfig)
22
+ {
23
+ $this->engine = new FingerprintEngine($securityConfig);
24
+ $this->securityConfig = $securityConfig;
25
+ }
26
+
27
+ /**
28
+ * Protects the current entry point.
29
+ * Analyzes the incoming request and issues challenges or block responses, exiting the script if necessary.
30
+ * If the request is allowed, returns the fingerprint data.
31
+ *
32
+ * @return array{score: float, vector: array}|null Fingerprint data if allowed, null otherwise.
33
+ */
34
+ public function protect(): ?array
35
+ {
36
+ // 1. Build request context from PHP superglobals
37
+ $body = $_POST ?: json_decode(file_get_contents('php://input'), true);
38
+ $headers = function_exists('getallheaders') ? getallheaders() : [];
39
+
40
+ $context = new RequestContext(
41
+ $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
42
+ parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
43
+ $headers,
44
+ $_GET,
45
+ $body,
46
+ $_COOKIE,
47
+ $_SERVER['SERVER_PROTOCOL'] ?? '1.1'
48
+ );
49
+
50
+ // 2. Process request with engine
51
+ $decision = $this->engine->processRequest($context);
52
+
53
+ // 3. Act on decision
54
+ if (isset($context->newCookieForResponse)) {
55
+ $cookie = $context->newCookieForResponse;
56
+ $this->sendCookie($cookie['name'], $cookie['value'], $cookie['options'] ?? []);
57
+ }
58
+
59
+ switch ($decision['action']) {
60
+ case 'block':
61
+ case 'challenge':
62
+ http_response_code($decision['status'] ?? 403);
63
+ if (is_array($decision['body'])) {
64
+ header('Content-Type: application/json');
65
+ echo json_encode($decision['body']);
66
+ } else {
67
+ header('Content-Type: text/html; charset=utf-8');
68
+ echo $decision['body'];
69
+ }
70
+ exit();
71
+
72
+ case 'redirect':
73
+ if (isset($decision['cookie'])) {
74
+ $this->sendCookie($decision['cookie']['name'], $decision['cookie']['value'], $decision['cookie']['options'] ?? []);
75
+ }
76
+ header('Location: ' . $decision['path'], true, 302);
77
+ exit();
78
+
79
+ case 'next':
80
+ default:
81
+ // Request allowed, return fingerprint metrics
82
+ return ['score' => $decision['score'], 'vector' => $decision['vector']];
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Sends an HTTP cookie while handling modern flags like 'partitioned'.
88
+ *
89
+ * @param string $name
90
+ * @param string $value
91
+ * @param array<string, mixed> $options
92
+ */
93
+ private function sendCookie(string $name, string $value, array $options): void
94
+ {
95
+ $isPartitioned = !empty($options['partitioned']);
96
+ unset($options['partitioned']);
97
+
98
+ // PHP setcookie() does not natively support 'partitioned'.
99
+ // If the cookie is secure and partitioned, emit raw header manually.
100
+ if ($isPartitioned && !empty($options['secure'])) {
101
+ $header = rawurlencode($name) . '=' . rawurlencode($value);
102
+ if (!empty($options['expires'])) {
103
+ $header .= '; Expires=' . gmdate('D, d M Y H:i:s T', (int)$options['expires']);
104
+ $header .= '; Max-Age=' . max(0, (int)$options['expires'] - time());
105
+ }
106
+ if (!empty($options['path'])) {
107
+ $header .= '; Path=' . $options['path'];
108
+ }
109
+ if (!empty($options['domain'])) {
110
+ $header .= '; Domain=' . $options['domain'];
111
+ }
112
+ if (!empty($options['secure'])) {
113
+ $header .= '; Secure';
114
+ }
115
+ if (!empty($options['httponly'])) {
116
+ $header .= '; HttpOnly';
117
+ }
118
+ if (!empty($options['samesite'])) {
119
+ $header .= '; SameSite=' . $options['samesite'];
120
+ }
121
+ $header .= '; Partitioned';
122
+ header('Set-Cookie: ' . $header, false);
123
+ return;
124
+ }
125
+
126
+ $allowedKeys = ['expires', 'path', 'domain', 'secure', 'httponly', 'samesite'];
127
+ $cleanOptions = array_intersect_key($options, array_flip($allowedKeys));
128
+ setcookie($name, $value, $cleanOptions);
129
+ }
130
+
131
+ /**
132
+ * Handles a request to the /metrics endpoint, applying authorization rules.
133
+ * If metrics are enabled and authorized, it outputs Prometheus formatted metrics and exits.
134
+ * Otherwise, it handles unauthorized access or returns a 404 if metrics are not enabled.
135
+ *
136
+ * @param RequestContext $context The current request context.
137
+ */
138
+ public function handleMetricsRequest(RequestContext $context): void
139
+ {
140
+ // 2. Apply custom authorization callback if configured
141
+ $authorizationCallback = $this->securityConfig['metricsAuthorizationCallback'] ?? null;
142
+ if (is_callable($authorizationCallback)) {
143
+ $decision = call_user_func($authorizationCallback, $context);
144
+
145
+ if (is_bool($decision)) {
146
+ if (!$decision) {
147
+ http_response_code(403); // Forbidden
148
+ echo "Access to metrics denied.";
149
+ exit();
150
+ }
151
+ } elseif (is_array($decision) && isset($decision['action'])) {
152
+ switch ($decision['action']) {
153
+ case 'block':
154
+ http_response_code($decision['status'] ?? 403);
155
+ echo $decision['body'] ?? "Access denied.";
156
+ exit();
157
+ case 'redirect':
158
+ header('Location: ' . $decision['path'], true, $decision['status'] ?? 302);
159
+ exit();
160
+ case 'next':
161
+ // Authorized; proceed to serve metrics
162
+ break;
163
+ default:
164
+ // Unknown action; deny by default
165
+ http_response_code(403);
166
+ echo "Invalid authorization decision.";
167
+ exit();
168
+ }
169
+ } else {
170
+ // Unexpected return type; deny by default
171
+ http_response_code(403);
172
+ echo "Invalid authorization callback response.";
173
+ exit();
174
+ }
175
+ }
176
+
177
+ // 3. If authorized, stream metrics output
178
+ header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
179
+ echo MetricsManager::getPrometheusMetrics();
180
+ exit();
181
+ }
182
+
183
+ /**
184
+ * Returns Prometheus formatted metrics if enabled in the security configuration.
185
+ * @return string|null
186
+ */
187
+ public function getPrometheusMetrics(): ?string
188
+ {
189
+ return MetricsManager::getPrometheusMetrics();
190
+ }
146
191
  }
@@ -5,8 +5,8 @@ declare(strict_types=1);
5
5
  namespace Anonympins\Fingerprint;
6
6
 
7
7
  /**
8
- * Classe pour construire une empreinte composite (Multi-Hash).
9
- * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
8
+ * Class to build a composite fingerprint (Multi-Hash).
9
+ * Output format: "grp1:hash1|grp2:hash2|grp3:hash3"
10
10
  */
11
11
  class FingerprintBuilder
12
12
  {
@@ -16,11 +16,10 @@ class FingerprintBuilder
16
16
  private array $components = [];
17
17
 
18
18
  /**
19
- * Ajoute un composant à l'empreinte.
20
- * La valeur est hachée pour l'anonymiser et réduire sa taille.
19
+ * Adds a component to the fingerprint.
21
20
  *
22
- * @param string $group Le nom du groupe (ex: 'hw', 'screen', 'geo').
23
- * @param string|int|bool|null $value La valeur brute à hacher.
21
+ * @param string $group Group name (e.g. 'hw', 'screen', 'geo').
22
+ * @param string|int|bool|null $value Raw value to hash.
24
23
  * @return self
25
24
  */
26
25
  public function add(string $group, $value): self
@@ -28,17 +27,17 @@ class FingerprintBuilder
28
27
  if ($value === null || $value === '') {
29
28
  return $this;
30
29
  }
31
- // On hache la valeur individuellement.
30
+ // Hash the value individually
32
31
  $this->components[$group] = self::cyrb53((string)$value);
33
32
  return $this;
34
33
  }
35
34
 
36
35
  /**
37
- * Ajoute un composant brut sans le hacher.
38
- * Utile pour les métriques qui doivent être lues telles quelles par le serveur.
36
+ * Adds a raw component without hashing it.
37
+ * Useful for metrics that need to be read as-is on the server.
39
38
  *
40
- * @param string $group Le nom du groupe.
41
- * @param string|int|null $value La valeur brute.
39
+ * @param string $group Group name.
40
+ * @param string|int|null $value Raw value.
42
41
  * @return self
43
42
  */
44
43
  public function addRaw(string $group, $value): self
@@ -51,14 +50,14 @@ class FingerprintBuilder
51
50
  }
52
51
 
53
52
  /**
54
- * Génère la chaîne de l'empreinte finale.
55
- * Les composants sont triés par clé pour garantir un ordre déterministe.
53
+ * Generates the final fingerprint string.
54
+ * Components are sorted by key to guarantee deterministic ordering.
56
55
  *
57
56
  * @return string
58
57
  */
59
58
  public function __toString(): string
60
59
  {
61
- // ksort trie le tableau par clé.
60
+ // Sort array by key
62
61
  ksort($this->components);
63
62
 
64
63
  $parts = [];
@@ -70,11 +69,11 @@ class FingerprintBuilder
70
69
  }
71
70
 
72
71
  /**
73
- * Compare deux empreintes et retourne un score de similarité (de 0 à 1).
74
- * Utilise une pondération pour donner plus d'importance aux invariants forts (Canvas, GPU, JA3).
72
+ * Compares two fingerprints and returns a similarity score (0 to 1).
73
+ * Uses weights to emphasize strong invariants (Canvas, GPU, JA3).
75
74
  *
76
- * @param string|null $fpString1 Empreinte A.
77
- * @param string|null $fpString2 Empreinte B.
75
+ * @param string|null $fpString1 Fingerprint A.
76
+ * @param string|null $fpString2 Fingerprint B.
78
77
  * @return float
79
78
  */
80
79
  public static function compare(?string $fpString1, ?string $fpString2): float
@@ -116,12 +115,12 @@ class FingerprintBuilder
116
115
  $allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
117
116
 
118
117
  foreach ($allKeys as $key) {
119
- // On ignore les clés volatiles pour cette comparaison spécifique.
118
+ // Ignore volatile keys for this comparison
120
119
  if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
121
120
  continue;
122
121
  }
123
122
 
124
- // On ne compare que les clés qui ont un poids défini.
123
+ // Only compare keys that have an assigned weight
125
124
  $weight = $weights[$key] ?? null;
126
125
  if ($weight === null) continue;
127
126
 
@@ -137,12 +136,12 @@ class FingerprintBuilder
137
136
  }
138
137
 
139
138
  /**
140
- * Algorithme de hachage cyrb53 (rapide et faible taux de collision).
141
- * Porté depuis la version JavaScript.
139
+ * cyrb53 hashing algorithm (fast and low collision rate).
140
+ * Ported from the JavaScript version.
142
141
  *
143
- * @param string $str La chaîne à hacher.
144
- * @param int $seed Une graine optionnelle.
145
- * @return string Le hash sous forme de chaîne de caractères.
142
+ * @param string $str String to hash.
143
+ * @param int $seed Optional seed.
144
+ * @return string Hash as string.
146
145
  */
147
146
  public static function cyrb53(string $str, int $seed = 0): string
148
147
  {
@@ -158,8 +157,8 @@ class FingerprintBuilder
158
157
  $h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
159
158
  $h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
160
159
 
161
- // Sur les architectures 64 bits (standard en production), PHP gère nativement les entiers 64 bits signés.
162
- // Nous pouvons éviter bcmath en effectuant des décalages binaires natifs pour un gain drastique de performances.
160
+ // On 64-bit platforms, PHP handles 64-bit signed ints natively.
161
+ // Use native shifts instead of bcmath for better performance.
163
162
  if (PHP_INT_SIZE === 8) {
164
163
  $h1_u = $h1 & 0xffffffff;
165
164
  $h2_u = $h2 & 0xffffffff;
@@ -167,17 +166,17 @@ class FingerprintBuilder
167
166
  return (string)$val_h2;
168
167
  }
169
168
 
170
- // Fallback bcmath uniquement sur l'architecture obsolète 32 bits.
169
+ // Fallback to bcmath on 32-bit platforms
171
170
  $val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
172
171
  return $val_h2;
173
172
  }
174
173
 
175
174
  /**
176
- * Émule la multiplication 32-bit `Math.imul` de JavaScript.
175
+ * Emulates JavaScript's 32-bit `Math.imul` multiplication.
177
176
  *
178
177
  * @param int $a
179
178
  * @param int $b
180
- * @return int Un entier signé 32-bit.
179
+ * @return int Signed 32-bit integer.
181
180
  */
182
181
  private static function imul(int $a, int $b): int
183
182
  {