@anonympins/fingerprint 0.3.2 → 0.3.3
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/CHANGELOG.md +172 -0
- package/README.md +276 -34
- package/composer.json +38 -0
- package/index.js +5 -0
- package/package.json +23 -18
- package/phpunit.xml +20 -0
- package/public/fp.js +2 -0
- package/src/js/build-client.js +69 -0
- package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
- package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/{fingerprint.js → src/js/fingerprint.js} +255 -101
- package/{library.js → src/js/library.js} +1729 -1729
- package/{problem-manager.js → src/js/problem-manager.js} +539 -522
- package/src/php/AutoTuner.php +155 -0
- package/src/php/Challenge/ChallengeUtils.php +306 -0
- package/src/php/Config/SecurityProfiles.php +257 -0
- package/src/php/DirectFingerprint.php +81 -0
- package/src/php/FingerprintBuilder.php +185 -0
- package/src/php/FingerprintClient.php +118 -0
- package/src/php/FingerprintEngine.php +850 -0
- package/src/php/Optimization/FunctionRegistry.php +63 -0
- package/src/php/Optimization/Optimization.php +256 -0
- package/src/php/Optimization/OptimizationOperators.php +305 -0
- package/src/php/Optimization/ProblemInitializers.php +53 -0
- package/src/php/ProblemManager.php +255 -0
- package/src/php/RequestContext.php +87 -0
- package/src/php/Store/IStore.php +42 -0
- package/src/php/Store/InMemoryStore.php +67 -0
- package/src/php/Store/StoreManager.php +26 -0
- package/src/php/Tests/ChallengeUtilsTest.php +82 -0
- package/src/php/Tests/FingerprintBuilderTest.php +58 -0
- package/src/php/Tests/FingerprintEngineTest.php +219 -0
- package/src/php/Tests/PowTest.php +40 -0
- package/src/php/Tests/ProblemManagerTest.php +295 -0
- package/src/php/Tests/RequestUtilsTest.php +81 -0
- package/src/php/Tests/problems.config.json +9 -0
- package/src/php/Utils/BigInt.php +102 -0
- package/src/php/Utils/BlockList.php +100 -0
- package/src/php/Utils/Logger.php +30 -0
- package/src/php/Utils/MaliciousPatterns.php +59 -0
- package/src/php/Utils/RequestUtils.php +673 -0
- package/fingerprint.client.obfuscated.js +0 -1
- /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
- /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
- /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
- /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
- /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
- /package/{redis-store.js → src/js/redis-store.js} +0 -0
- /package/{sql-store.js → src/js/sql-store.js} +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Config;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Définit les profils de sécurité prédéfinis pour la bibliothèque Fingerprint.
|
|
9
|
+
* Ces profils contiennent les poids des scores de suspicion et les seuils de déclenchement.
|
|
10
|
+
*/
|
|
11
|
+
class SecurityProfiles
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @var array<string, array<string, mixed>>
|
|
15
|
+
*/
|
|
16
|
+
public const PROFILES = [
|
|
17
|
+
/**
|
|
18
|
+
* Balanced Profile (Default)
|
|
19
|
+
* A general-purpose configuration suitable for most websites, offering a good mix of security and user experience.
|
|
20
|
+
* It's sensitive enough to catch common bots without being overly aggressive towards legitimate users.
|
|
21
|
+
*/
|
|
22
|
+
'balanced' => [
|
|
23
|
+
'summary' => 'Balanced Profile (Default)',
|
|
24
|
+
'description' => 'A general-purpose configuration suitable for most websites, offering a good mix of security and user experience. It\'s sensitive enough to catch common bots without being overly aggressive towards legitimate users.',
|
|
25
|
+
'weights' => [
|
|
26
|
+
'historyScore' => 0.3,
|
|
27
|
+
'rotationScore' => 0.5,
|
|
28
|
+
'headerAnomalyScore' => 0.1,
|
|
29
|
+
'requestPatternScore' => 0.6,
|
|
30
|
+
'inconsistencyScore' => 0.8,
|
|
31
|
+
'behaviorScore' => 0.7,
|
|
32
|
+
'honeypotScore' => 1.0,
|
|
33
|
+
'crossLayerInconsistencyScore' => 0.4,
|
|
34
|
+
'timeInconsistencyScore' => 0.9,
|
|
35
|
+
'tlsSpoofingScore' => 0.8,
|
|
36
|
+
'botScore' => 1.0, // Poids pour le score de bot explicite
|
|
37
|
+
'cookieDroppingScore' => 0.9, // Pénalité élevée pour la suppression de cookies
|
|
38
|
+
'threatIntelScore' => 0.4, // Poids pour le renseignement sur les menaces (ex: IP de proxy connu)
|
|
39
|
+
'clickVarianceScore' => 0.6, // Poids pour la variance des clics
|
|
40
|
+
],
|
|
41
|
+
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
|
|
42
|
+
'patterns' => [
|
|
43
|
+
'velocityThreshold' => 800,
|
|
44
|
+
'burstThreshold' => 1500,
|
|
45
|
+
'scrapeThreshold' => 1000,
|
|
46
|
+
'historySize' => 10,
|
|
47
|
+
'minSamples' => 5,
|
|
48
|
+
'regularityThreshold' => 50,
|
|
49
|
+
'benfordThreshold' => 0.15,
|
|
50
|
+
'patternWeight' => 80,
|
|
51
|
+
'decayFactor' => 0.9,
|
|
52
|
+
'inactivityReset' => 5000,
|
|
53
|
+
],
|
|
54
|
+
],
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Strict Profile
|
|
58
|
+
* An aggressive configuration for sensitive applications (e.g., financial services, admin panels).
|
|
59
|
+
* It uses lower suspicion thresholds and higher penalties for anomalies, prioritizing security over user convenience.
|
|
60
|
+
* All new devices are challenged by default.
|
|
61
|
+
*/
|
|
62
|
+
'strict' => [
|
|
63
|
+
'summary' => 'Strict Profile',
|
|
64
|
+
'description' => 'An aggressive configuration for sensitive applications (e.g., financial services, admin panels). It uses lower suspicion thresholds and higher penalties for anomalies, prioritizing security over user convenience. All new devices are challenged by default.',
|
|
65
|
+
'weights' => [
|
|
66
|
+
'historyScore' => 0.4,
|
|
67
|
+
'rotationScore' => 0.6,
|
|
68
|
+
'headerAnomalyScore' => 0.2,
|
|
69
|
+
'requestPatternScore' => 0.8,
|
|
70
|
+
'inconsistencyScore' => 1.0,
|
|
71
|
+
'behaviorScore' => 0.8,
|
|
72
|
+
'honeypotScore' => 1.0,
|
|
73
|
+
'crossLayerInconsistencyScore' => 0.6,
|
|
74
|
+
'timeInconsistencyScore' => 1.0,
|
|
75
|
+
'tlsSpoofingScore' => 1.0,
|
|
76
|
+
'botScore' => 1.0,
|
|
77
|
+
'cookieDroppingScore' => 1.0, // Pénalité maximale
|
|
78
|
+
'threatIntelScore' => 0.7, // Poids élevé pour les menaces connues (Tor, etc.)
|
|
79
|
+
'clickVarianceScore' => 0.7, // Poids élevé pour la variance des clics
|
|
80
|
+
],
|
|
81
|
+
'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
|
|
82
|
+
'patterns' => [
|
|
83
|
+
'velocityThreshold' => 1000,
|
|
84
|
+
'burstThreshold' => 1800,
|
|
85
|
+
'scrapeThreshold' => 1200,
|
|
86
|
+
'historySize' => 15,
|
|
87
|
+
'minSamples' => 4,
|
|
88
|
+
'regularityThreshold' => 40,
|
|
89
|
+
'benfordThreshold' => 0.12,
|
|
90
|
+
'patternWeight' => 90,
|
|
91
|
+
'decayFactor' => 0.85,
|
|
92
|
+
'inactivityReset' => 4000,
|
|
93
|
+
],
|
|
94
|
+
'challengeNewDevices' => true, // Challenge all new devices
|
|
95
|
+
],
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* API Profile
|
|
99
|
+
* Optimized for protecting API endpoints. This profile is highly sensitive to request patterns (velocity, bursts)
|
|
100
|
+
* and less reliant on browser-specific behavioral metrics. It's designed to quickly identify and throttle scrapers and automated clients.
|
|
101
|
+
*/
|
|
102
|
+
'api' => [
|
|
103
|
+
'summary' => 'API Profile',
|
|
104
|
+
'description' => 'Optimized for protecting API endpoints. This profile is highly sensitive to request patterns (velocity, bursts) and less reliant on browser-specific behavioral metrics. It\'s designed to quickly identify and throttle scrapers and automated clients.',
|
|
105
|
+
'weights' => [
|
|
106
|
+
'historyScore' => 0.5,
|
|
107
|
+
'rotationScore' => 0.5,
|
|
108
|
+
'headerAnomalyScore' => 0.3,
|
|
109
|
+
'requestPatternScore' => 1.0, // Very high weight for API patterns
|
|
110
|
+
'inconsistencyScore' => 0.7,
|
|
111
|
+
'behaviorScore' => 0.2, // Lower weight, as browser behavior is not applicable
|
|
112
|
+
'honeypotScore' => 1.0,
|
|
113
|
+
'crossLayerInconsistencyScore' => 0.5,
|
|
114
|
+
'timeInconsistencyScore' => 0.8,
|
|
115
|
+
'tlsSpoofingScore' => 0.7,
|
|
116
|
+
'botScore' => 0.5,
|
|
117
|
+
'cookieDroppingScore' => 0.8, // Important pour les clients API qui doivent maintenir un état
|
|
118
|
+
'threatIntelScore' => 0.5, // Les API sont souvent ciblées par des IPs malveillantes
|
|
119
|
+
'clickVarianceScore' => 0.3, // Poids faible car non applicable aux API
|
|
120
|
+
],
|
|
121
|
+
'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
|
|
122
|
+
'patterns' => [
|
|
123
|
+
'velocityThreshold' => 200, // APIs are expected to be fast
|
|
124
|
+
'burstThreshold' => 500,
|
|
125
|
+
'scrapeThreshold' => 400,
|
|
126
|
+
'historySize' => 20,
|
|
127
|
+
'minSamples' => 8,
|
|
128
|
+
'regularityThreshold' => 20,
|
|
129
|
+
'benfordThreshold' => 0.18,
|
|
130
|
+
'patternWeight' => 85,
|
|
131
|
+
'decayFactor' => 0.9,
|
|
132
|
+
'inactivityReset' => 10000,
|
|
133
|
+
],
|
|
134
|
+
// This would be a callable in PHP, but for now, we represent its intent.
|
|
135
|
+
'isApiRequest' => 'req.path.startsWith("/api/") || req.headers.accept?.includes("application/json")',
|
|
136
|
+
],
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Blog Profile
|
|
140
|
+
* Tuned for blogs and content-heavy websites. This profile focuses on detecting content scraping and comment spam
|
|
141
|
+
* by placing a high weight on request patterns and honeypot traps, while being more lenient on behavioral metrics
|
|
142
|
+
* typical of readers.
|
|
143
|
+
*/
|
|
144
|
+
'blog' => [
|
|
145
|
+
'summary' => 'Blog Profile',
|
|
146
|
+
'description' => 'Tuned for blogs and content-heavy websites. This profile focuses on detecting content scraping and comment spam by placing a high weight on request patterns and honeypot traps, while being more lenient on behavioral metrics typical of readers.',
|
|
147
|
+
'weights' => [
|
|
148
|
+
'historyScore' => 0.2,
|
|
149
|
+
'rotationScore' => 0.3,
|
|
150
|
+
'headerAnomalyScore' => 0.1,
|
|
151
|
+
'requestPatternScore' => 0.8, // High weight to detect content scraping
|
|
152
|
+
'inconsistencyScore' => 0.7,
|
|
153
|
+
'behaviorScore' => 0.5, // Less emphasis on complex interactions
|
|
154
|
+
'honeypotScore' => 1.0, // Crucial for comment spam
|
|
155
|
+
'crossLayerInconsistencyScore' => 0.4,
|
|
156
|
+
'timeInconsistencyScore' => 0.8,
|
|
157
|
+
'tlsSpoofingScore' => 0.6,
|
|
158
|
+
'botScore' => 0.8,
|
|
159
|
+
'cookieDroppingScore' => 0.7, // Moins critique, mais toujours un signal
|
|
160
|
+
'threatIntelScore' => 0.3, // Moins prioritaire pour un blog
|
|
161
|
+
'clickVarianceScore' => 0.5, // Poids modéré pour la variance des clics
|
|
162
|
+
],
|
|
163
|
+
'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
|
|
164
|
+
'patterns' => [
|
|
165
|
+
'velocityThreshold' => 1000, // Readers can be fast
|
|
166
|
+
'burstThreshold' => 2000,
|
|
167
|
+
'scrapeThreshold' => 800, // Very sensitive to scraping patterns
|
|
168
|
+
'historySize' => 12,
|
|
169
|
+
'minSamples' => 5,
|
|
170
|
+
'regularityThreshold' => 60,
|
|
171
|
+
'benfordThreshold' => 0.16,
|
|
172
|
+
'patternWeight' => 85,
|
|
173
|
+
'decayFactor' => 0.92,
|
|
174
|
+
'inactivityReset' => 10000,
|
|
175
|
+
],
|
|
176
|
+
],
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* E-commerce Profile
|
|
180
|
+
* A strict profile tailored for e-commerce sites. It's designed to combat inventory scalping,
|
|
181
|
+
* price scraping, and account takeover attempts by using high weights for request patterns and fingerprint inconsistency.
|
|
182
|
+
* It also challenges all new devices to increase the cost for bots.
|
|
183
|
+
*/
|
|
184
|
+
'ecommerce' => [
|
|
185
|
+
'summary' => 'E-commerce Profile',
|
|
186
|
+
'description' => 'A strict profile tailored for e-commerce sites. It\'s designed to combat inventory scalping, price scraping, and account takeover attempts by using high weights for request patterns and fingerprint inconsistency. It also challenges all new devices to increase the cost for bots.',
|
|
187
|
+
'weights' => [
|
|
188
|
+
'historyScore' => 0.4,
|
|
189
|
+
'rotationScore' => 0.6,
|
|
190
|
+
'headerAnomalyScore' => 0.2,
|
|
191
|
+
'inconsistencyScore' => 1.0, // Crucial for preventing account takeover
|
|
192
|
+
'behaviorScore' => 0.8, // Important for checkout/login forms
|
|
193
|
+
'honeypotScore' => 1.0,
|
|
194
|
+
'crossLayerInconsistencyScore' => 0.7,
|
|
195
|
+
// NOUVEAU: Ajout des scores manquants pour une configuration complète
|
|
196
|
+
'requestPatternScore' => 0.9, // Poids unifié pour les patterns, remplace les scores scindés
|
|
197
|
+
'timeInconsistencyScore' => 0.9,
|
|
198
|
+
'tlsSpoofingScore' => 0.9,
|
|
199
|
+
'botScore' => 1.0,
|
|
200
|
+
'cookieDroppingScore' => 1.0, // Crucial pour la détection de bots e-commerce
|
|
201
|
+
'threatIntelScore' => 0.8, // Très important pour l'e-commerce (proxies de scalping)
|
|
202
|
+
'clickVarianceScore' => 0.8, // Poids très élevé pour la variance des clics
|
|
203
|
+
],
|
|
204
|
+
'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
|
|
205
|
+
'patterns' => [
|
|
206
|
+
'velocityThreshold' => 500, // Bots are very fast
|
|
207
|
+
'burstThreshold' => 1000, // Detects rapid retries on the same product/action
|
|
208
|
+
'scrapeThreshold' => 600,
|
|
209
|
+
'historySize' => 15,
|
|
210
|
+
'minSamples' => 6,
|
|
211
|
+
'regularityThreshold' => 30,
|
|
212
|
+
'benfordThreshold' => 0.14,
|
|
213
|
+
'patternWeight' => 95,
|
|
214
|
+
'decayFactor' => 0.88,
|
|
215
|
+
'inactivityReset' => 3000,
|
|
216
|
+
],
|
|
217
|
+
'challengeNewDevices' => true, // New devices are suspicious in e-commerce
|
|
218
|
+
// This would be a callable in PHP, but for now, we represent its intent.
|
|
219
|
+
'isApiRequest' => 'req.path.startsWith("/api/cart") || req.path.startsWith("/api/stock") || req.path.startsWith("/api/checkout")',
|
|
220
|
+
],
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Crée une configuration de sécurité basée sur un profil nommé, avec des surcharges optionnelles.
|
|
225
|
+
*
|
|
226
|
+
* @param string $profileName Le nom du profil à utiliser ('balanced', 'strict', 'api', etc.).
|
|
227
|
+
* @param array<string, mixed> $overrides Un tableau pour fusionner profondément avec le profil, permettant la personnalisation.
|
|
228
|
+
* @return array<string, mixed> L'objet de configuration de sécurité final.
|
|
229
|
+
*/
|
|
230
|
+
public static function createSecurityProfile(string $profileName = 'balanced', array $overrides = []): array
|
|
231
|
+
{
|
|
232
|
+
$baseProfile = self::PROFILES[$profileName] ?? self::PROFILES['balanced'];
|
|
233
|
+
return self::deepMerge($baseProfile, $overrides);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Fusionne profondément deux tableaux. Les propriétés du tableau `$source` écrasent celles du tableau `$target`.
|
|
238
|
+
*
|
|
239
|
+
* @param array<string, mixed> $target Le tableau cible.
|
|
240
|
+
* @param array<string, mixed> $source Le tableau source.
|
|
241
|
+
* @return array<string, mixed> Le tableau fusionné.
|
|
242
|
+
*/
|
|
243
|
+
private static function deepMerge(array $target, array $source): array
|
|
244
|
+
{
|
|
245
|
+
$output = $target;
|
|
246
|
+
|
|
247
|
+
foreach ($source as $key => $value) {
|
|
248
|
+
if (is_array($value) && isset($output[$key]) && is_array($output[$key])) {
|
|
249
|
+
$output[$key] = self::deepMerge($output[$key], $value);
|
|
250
|
+
} else {
|
|
251
|
+
$output[$key] = $value;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return $output;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Intégration directe du moteur de fingerprinting pour les applications PHP sans framework PSR.
|
|
9
|
+
* Cette classe interagit directement avec les superglobales PHP et les fonctions de réponse.
|
|
10
|
+
*/
|
|
11
|
+
class DirectFingerprint
|
|
12
|
+
{
|
|
13
|
+
private FingerprintEngine $engine;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param array $securityConfig La configuration de sécurité pour le moteur.
|
|
17
|
+
*/
|
|
18
|
+
public function __construct(array $securityConfig)
|
|
19
|
+
{
|
|
20
|
+
$this->engine = new FingerprintEngine($securityConfig);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Protège le point d'entrée actuel.
|
|
25
|
+
* Analyse la requête entrante et, si nécessaire, envoie une réponse de challenge/blocage et termine le script.
|
|
26
|
+
* Si la requête est autorisée, la méthode retourne simplement et le reste du script peut s'exécuter.
|
|
27
|
+
*
|
|
28
|
+
* @return array{score: float, vector: array}|null Les données du fingerprint si la requête est autorisée, null sinon.
|
|
29
|
+
*/
|
|
30
|
+
public function protect(): ?array
|
|
31
|
+
{
|
|
32
|
+
// 1. Construire le contexte de la requête à partir des superglobales PHP.
|
|
33
|
+
$body = $_POST ?: json_decode(file_get_contents('php://input'), true);
|
|
34
|
+
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
|
35
|
+
|
|
36
|
+
$context = new RequestContext(
|
|
37
|
+
$_SERVER['REMOTE_ADDR'] ?? '127.0.0.1',
|
|
38
|
+
parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_PATH) ?: '/',
|
|
39
|
+
$headers,
|
|
40
|
+
$_GET,
|
|
41
|
+
$body,
|
|
42
|
+
$_COOKIE,
|
|
43
|
+
$_SERVER['SERVER_PROTOCOL'] ?? '1.1'
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
// 2. Traiter la requête avec le moteur.
|
|
47
|
+
$decision = $this->engine->processRequest($context);
|
|
48
|
+
|
|
49
|
+
// 3. Agir sur la décision.
|
|
50
|
+
if (isset($context->newCookieForResponse)) {
|
|
51
|
+
$cookie = $context->newCookieForResponse;
|
|
52
|
+
setcookie($cookie['name'], $cookie['value'], $cookie['options']);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
switch ($decision['action']) {
|
|
56
|
+
case 'block':
|
|
57
|
+
case 'challenge':
|
|
58
|
+
http_response_code($decision['status'] ?? 403);
|
|
59
|
+
if (is_array($decision['body'])) {
|
|
60
|
+
header('Content-Type: application/json');
|
|
61
|
+
echo json_encode($decision['body']);
|
|
62
|
+
} else {
|
|
63
|
+
header('Content-Type: text/html; charset=utf-8');
|
|
64
|
+
echo $decision['body'];
|
|
65
|
+
}
|
|
66
|
+
exit(); // Termine le script.
|
|
67
|
+
|
|
68
|
+
case 'redirect':
|
|
69
|
+
if (isset($decision['cookie'])) {
|
|
70
|
+
setcookie($decision['cookie']['name'], $decision['cookie']['value'], $decision['cookie']['options']);
|
|
71
|
+
}
|
|
72
|
+
header('Location: ' . $decision['path'], true, 302);
|
|
73
|
+
exit(); // Termine le script.
|
|
74
|
+
|
|
75
|
+
case 'next':
|
|
76
|
+
default:
|
|
77
|
+
// La requête est autorisée, on retourne les informations du fingerprint.
|
|
78
|
+
return ['score' => $decision['score'], 'vector' => $decision['vector']];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
9
|
+
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
10
|
+
*/
|
|
11
|
+
class FingerprintBuilder
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @var array<string, string|int>
|
|
15
|
+
*/
|
|
16
|
+
private array $components = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ajoute un composant à l'empreinte.
|
|
20
|
+
* La valeur est hachée pour l'anonymiser et réduire sa taille.
|
|
21
|
+
*
|
|
22
|
+
* @param string $group Le nom du groupe (ex: 'hw', 'screen', 'geo').
|
|
23
|
+
* @param string|int|bool|null $value La valeur brute à hacher.
|
|
24
|
+
* @return self
|
|
25
|
+
*/
|
|
26
|
+
public function add(string $group, $value): self
|
|
27
|
+
{
|
|
28
|
+
if ($value === null || $value === '') {
|
|
29
|
+
return $this;
|
|
30
|
+
}
|
|
31
|
+
// On hache la valeur individuellement.
|
|
32
|
+
$this->components[$group] = self::cyrb53((string)$value);
|
|
33
|
+
return $this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ajoute un composant brut sans le hacher.
|
|
38
|
+
* Utile pour les métriques qui doivent être lues telles quelles par le serveur.
|
|
39
|
+
*
|
|
40
|
+
* @param string $group Le nom du groupe.
|
|
41
|
+
* @param string|int|null $value La valeur brute.
|
|
42
|
+
* @return self
|
|
43
|
+
*/
|
|
44
|
+
public function addRaw(string $group, $value): self
|
|
45
|
+
{
|
|
46
|
+
if ($value === null) {
|
|
47
|
+
return $this;
|
|
48
|
+
}
|
|
49
|
+
$this->components[$group] = $value;
|
|
50
|
+
return $this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
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.
|
|
56
|
+
*
|
|
57
|
+
* @return string
|
|
58
|
+
*/
|
|
59
|
+
public function __toString(): string
|
|
60
|
+
{
|
|
61
|
+
// ksort trie le tableau par clé.
|
|
62
|
+
ksort($this->components);
|
|
63
|
+
|
|
64
|
+
$parts = [];
|
|
65
|
+
foreach ($this->components as $key => $hash) {
|
|
66
|
+
$parts[] = "{$key}:{$hash}";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return implode('|', $parts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
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).
|
|
75
|
+
*
|
|
76
|
+
* @param string|null $fpString1 Empreinte A.
|
|
77
|
+
* @param string|null $fpString2 Empreinte B.
|
|
78
|
+
* @return float
|
|
79
|
+
*/
|
|
80
|
+
public static function compare(?string $fpString1, ?string $fpString2): float
|
|
81
|
+
{
|
|
82
|
+
if (empty($fpString1) || empty($fpString2)) {
|
|
83
|
+
return 0.0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$parse = function (string $str): array {
|
|
87
|
+
$map = [];
|
|
88
|
+
foreach (explode('|', $str) as $part) {
|
|
89
|
+
$pair = explode(':', $part, 2);
|
|
90
|
+
if (count($pair) === 2 && !empty($pair[0]) && !empty($pair[1])) {
|
|
91
|
+
$map[$pair[0]] = $pair[1];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return $map;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
$map1 = $parse($fpString1);
|
|
98
|
+
$map2 = $parse($fpString2);
|
|
99
|
+
|
|
100
|
+
$volatileKeys = [
|
|
101
|
+
'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
|
|
102
|
+
'cookie_keys', 'upgrade', 'network', 'http_ver',
|
|
103
|
+
'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
$weights = [
|
|
107
|
+
'cvs' => 5.0, 'gpu' => 4.0, 'ja3' => 3.5, 'ja4' => 4.0,
|
|
108
|
+
'h2_settings' => 3.0, 'tcp_fp' => 2.5, 'ua' => 2.0,
|
|
109
|
+
'client_fp_hash' => 3.0, 'browser' => 1.5, 'os_version' => 1.5,
|
|
110
|
+
'device_type' => 1.0, 'hw' => 1.5, 'scr' => 1.0, 'os' => 0.8, 'geo' => 0.5,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
$weightedMatches = 0.0;
|
|
114
|
+
$totalWeight = 0.0;
|
|
115
|
+
|
|
116
|
+
$allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
|
|
117
|
+
|
|
118
|
+
foreach ($allKeys as $key) {
|
|
119
|
+
// On ignore les clés volatiles pour cette comparaison spécifique.
|
|
120
|
+
if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// On ne compare que les clés qui ont un poids défini.
|
|
125
|
+
$weight = $weights[$key] ?? null;
|
|
126
|
+
if ($weight === null) continue;
|
|
127
|
+
|
|
128
|
+
$totalWeight += $weight;
|
|
129
|
+
if (isset($map1[$key]) && isset($map2[$key])) {
|
|
130
|
+
if ($map1[$key] === $map2[$key]) {
|
|
131
|
+
$weightedMatches += $weight;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return $totalWeight === 0.0 ? 0.0 : $weightedMatches / $totalWeight;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
141
|
+
* Porté depuis la version JavaScript.
|
|
142
|
+
*
|
|
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.
|
|
146
|
+
*/
|
|
147
|
+
public static function cyrb53(string $str, int $seed = 0): string
|
|
148
|
+
{
|
|
149
|
+
$h1 = 0xdeadbeef ^ $seed;
|
|
150
|
+
$h2 = 0x41c6ce57 ^ $seed;
|
|
151
|
+
|
|
152
|
+
for ($i = 0, $l = strlen($str); $i < $l; $i++) {
|
|
153
|
+
$ch = ord($str[$i]);
|
|
154
|
+
$h1 = self::imul($h1 ^ $ch, 2654435761);
|
|
155
|
+
$h2 = self::imul($h2 ^ $ch, 1597334677);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
$h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
|
|
159
|
+
$h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
|
|
160
|
+
|
|
161
|
+
// En PHP, les opérations sur les grands nombres peuvent être délicates.
|
|
162
|
+
// On utilise bcmath pour une arithmétique de précision arbitraire, garantissant le même résultat que JS.
|
|
163
|
+
$val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
|
|
164
|
+
return $val_h2;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Émule la multiplication 32-bit `Math.imul` de JavaScript.
|
|
169
|
+
*
|
|
170
|
+
* @param int $a
|
|
171
|
+
* @param int $b
|
|
172
|
+
* @return int Un entier signé 32-bit.
|
|
173
|
+
*/
|
|
174
|
+
private static function imul(int $a, int $b): int
|
|
175
|
+
{
|
|
176
|
+
// Utiliser GMP pour la multiplication pour éviter le dépassement en float sur les systèmes 64-bit.
|
|
177
|
+
$gmp_a = gmp_init($a);
|
|
178
|
+
$gmp_b = gmp_init($b);
|
|
179
|
+
$gmp_result = gmp_mul($gmp_a, $gmp_b);
|
|
180
|
+
|
|
181
|
+
// Tronquer le résultat à 32 bits et le convertir en entier signé.
|
|
182
|
+
$truncated = gmp_and($gmp_result, '0xFFFFFFFF');
|
|
183
|
+
return gmp_intval(gmp_sign($truncated) < 0 ? gmp_sub($truncated, '0x100000000') : $truncated);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
|
|
9
|
+
*
|
|
10
|
+
* Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
|
|
11
|
+
* dans une application PHP. Elle gère l'injection sécurisée du script et la création
|
|
12
|
+
* de "honeypots" (pièges à bots) dans les formulaires.
|
|
13
|
+
*/
|
|
14
|
+
class FingerprintClient
|
|
15
|
+
{
|
|
16
|
+
/**
|
|
17
|
+
* @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
|
|
18
|
+
*/
|
|
19
|
+
private string $clientScriptPath;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
|
|
23
|
+
*/
|
|
24
|
+
private array $clientConfig;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
|
|
28
|
+
*/
|
|
29
|
+
private ?string $nonce;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Constructeur de la classe.
|
|
33
|
+
*
|
|
34
|
+
* @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
|
|
35
|
+
* @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
|
|
36
|
+
*/
|
|
37
|
+
public function __construct(string $clientScriptPath, array $clientConfig = [])
|
|
38
|
+
{
|
|
39
|
+
$this->clientScriptPath = $clientScriptPath;
|
|
40
|
+
|
|
41
|
+
// Configuration par défaut si non fournie
|
|
42
|
+
$this->clientConfig = array_merge([
|
|
43
|
+
'mouse' => true,
|
|
44
|
+
'keystrokes' => true,
|
|
45
|
+
'clicks' => true,
|
|
46
|
+
'honeypots' => [],
|
|
47
|
+
'fetch' => [
|
|
48
|
+
'handleChallenges' => true,
|
|
49
|
+
'probationaryTtl' => 30000, // 30 seconds
|
|
50
|
+
]
|
|
51
|
+
], $clientConfig);
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
// Génère un nonce pour CSP si possible, pour une sécurité renforcée.
|
|
55
|
+
$this->nonce = bin2hex(random_bytes(16));
|
|
56
|
+
} catch (\Exception $e) {
|
|
57
|
+
$this->nonce = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Génère un champ de formulaire "honeypot" caché.
|
|
63
|
+
* Les bots le rempliront, mais il sera invisible pour les humains.
|
|
64
|
+
*
|
|
65
|
+
* @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
|
|
66
|
+
* @return string Le code HTML du champ honeypot.
|
|
67
|
+
*/
|
|
68
|
+
public function generateHoneypotField(string $fieldName): string
|
|
69
|
+
{
|
|
70
|
+
// Ajoute le champ à la configuration pour que le script client le surveille.
|
|
71
|
+
if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
|
|
72
|
+
$this->clientConfig['honeypots'][] = $fieldName;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Styles CSS pour cacher le champ de manière robuste.
|
|
76
|
+
$styles = 'position:absolute; left:-9999px; top:-9999px; opacity:0;';
|
|
77
|
+
|
|
78
|
+
return '<div style="' . $styles . '" aria-hidden="true">'
|
|
79
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">Ne pas remplir ce champ</label>'
|
|
80
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
81
|
+
. '</div>';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
86
|
+
*
|
|
87
|
+
* @return string Le code HTML des balises <script>.
|
|
88
|
+
*/
|
|
89
|
+
public function getScriptTag(): string
|
|
90
|
+
{
|
|
91
|
+
$configJson = json_encode($this->clientConfig);
|
|
92
|
+
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
93
|
+
|
|
94
|
+
// Le script d'initialisation qui sera inclus dans la page.
|
|
95
|
+
$initScript = <<<JS
|
|
96
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
97
|
+
if (window.ClientLibrary) {
|
|
98
|
+
window.ClientLibrary.initializeClient({$configJson});
|
|
99
|
+
} else {
|
|
100
|
+
console.error('Fingerprint client library not loaded.');
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
JS;
|
|
104
|
+
|
|
105
|
+
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
106
|
+
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
107
|
+
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
112
|
+
* @return string|null
|
|
113
|
+
*/
|
|
114
|
+
public function getNonce(): ?string
|
|
115
|
+
{
|
|
116
|
+
return $this->nonce;
|
|
117
|
+
}
|
|
118
|
+
}
|