@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.
- package/CHANGELOG.md +38 -0
- package/README.md +70 -60
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +831 -635
- package/src/js/fingerprint.js +249 -31
- package/src/js/pow.solver.js +531 -497
- package/src/js/problem-manager.js +24 -0
- package/src/js/tests/fingerprint.client.init.test.js +140 -119
- package/src/js/tests/fingerprint.test.js +2451 -2319
- package/src/js/tests/pow.solver.test.js +233 -197
- package/src/js/tests/problem-manager.test.js +358 -322
- package/src/php/Challenge/ChallengeUtils.php +415 -361
- package/src/php/Config/SecurityProfiles.php +276 -271
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +36 -2
- package/src/php/Optimization/FunctionRegistry.php +63 -62
- package/src/php/Optimization/OptimizationOperators.php +401 -304
- package/src/php/ProblemManager.php +29 -0
- package/src/php/RequestContext.php +90 -90
- package/src/php/Store/IStore.php +41 -41
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/FingerprintEngineTest.php +330 -299
- package/src/php/Tests/ProblemManagerTest.php +376 -296
- package/src/php/Tests/RequestUtilsTest.php +386 -253
- package/src/php/Tests/problems.config.json +3 -3
- package/src/php/Utils/RequestUtils.php +201 -17
|
@@ -289,7 +289,105 @@ class RequestUtils
|
|
|
289
289
|
|
|
290
290
|
return ['avgSpeed' => $avgSpeed, 'avgAcceleration' => $avgAcceleration, 'straightness' => $straightness, 'pauses' => $pauses, 'segments' => array_column($segments, 'distance')];
|
|
291
291
|
}
|
|
292
|
+
/**
|
|
293
|
+
* Analyse une série d'événements tactiles mobiles pour en extraire des indicateurs comportementaux.
|
|
294
|
+
* @param array|null $history
|
|
295
|
+
* @return array
|
|
296
|
+
*/
|
|
297
|
+
private static function analyzeTouchMovements(?array $history): array
|
|
298
|
+
{
|
|
299
|
+
if (empty($history) || count($history) < 3) {
|
|
300
|
+
return [
|
|
301
|
+
'avgSpeed' => 0.0, 'avgAcceleration' => 0.0, 'straightness' => 1.0, 'pauses' => 0, 'segments' => [],
|
|
302
|
+
'avgPressure' => 0.0, 'avgRadius' => 0.0, 'pressureVariance' => 0.0, 'radiusVariance' => 0.0, 'maxTouches' => 1
|
|
303
|
+
];
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
$segments = [];
|
|
307
|
+
$totalDistance = 0.0;
|
|
308
|
+
$pauses = 0;
|
|
309
|
+
$totalPressure = 0.0;
|
|
310
|
+
$totalRadius = 0.0;
|
|
311
|
+
$maxTouches = 1;
|
|
312
|
+
|
|
313
|
+
for ($i = 1; $i < count($history); $i++) {
|
|
314
|
+
$p1 = $history[$i - 1];
|
|
315
|
+
$p2 = $history[$i];
|
|
316
|
+
$dx = $p2['x'] - $p1['x'];
|
|
317
|
+
$dy = $p2['y'] - $p1['y'];
|
|
318
|
+
$dt = $p2['t'] - $p1['t'];
|
|
319
|
+
$distance = sqrt($dx * $dx + $dy * $dy);
|
|
320
|
+
|
|
321
|
+
$totalPressure += (float)($p2['p'] ?? 0.0);
|
|
322
|
+
$totalRadius += (float)($p2['r'] ?? 0.0);
|
|
323
|
+
if (($p2['num'] ?? 1) > $maxTouches) {
|
|
324
|
+
$maxTouches = (int)$p2['num'];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if ($dt > 0) {
|
|
328
|
+
$speed = $distance / $dt;
|
|
329
|
+
$segments[] = ['distance' => $distance, 'dt' => $dt, 'speed' => $speed];
|
|
330
|
+
$totalDistance += $distance;
|
|
331
|
+
}
|
|
332
|
+
if ($dt > 100 && $distance < 5) {
|
|
333
|
+
$pauses++;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
$totalPressure += (float)($history[0]['p'] ?? 0.0);
|
|
338
|
+
$totalRadius += (float)($history[0]['r'] ?? 0.0);
|
|
339
|
+
|
|
340
|
+
$avgPressure = $totalPressure / count($history);
|
|
341
|
+
$avgRadius = $totalRadius / count($history);
|
|
342
|
+
|
|
343
|
+
$sqDiffPressureSum = 0.0;
|
|
344
|
+
$sqDiffRadiusSum = 0.0;
|
|
345
|
+
foreach ($history as $pt) {
|
|
346
|
+
$sqDiffPressureSum += pow((float)($pt['p'] ?? 0.0) - $avgPressure, 2);
|
|
347
|
+
$sqDiffRadiusSum += pow((float)($pt['r'] ?? 0.0) - $avgRadius, 2);
|
|
348
|
+
}
|
|
349
|
+
$pressureVariance = $sqDiffPressureSum / count($history);
|
|
350
|
+
$radiusVariance = $sqDiffRadiusSum / count($history);
|
|
351
|
+
|
|
352
|
+
if (count($segments) < 2) {
|
|
353
|
+
return [
|
|
354
|
+
'avgSpeed' => 0.0, 'avgAcceleration' => 0.0, 'straightness' => 1.0, 'pauses' => $pauses, 'segments' => [],
|
|
355
|
+
'avgPressure' => $avgPressure, 'avgRadius' => $avgRadius, 'pressureVariance' => $pressureVariance, 'radiusVariance' => $radiusVariance, 'maxTouches' => $maxTouches
|
|
356
|
+
];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
$totalTime = $history[count($history) - 1]['t'] - $history[0]['t'];
|
|
360
|
+
$avgSpeed = $totalTime > 0 ? array_sum(array_column($segments, 'speed')) / count($segments) : 0.0;
|
|
361
|
+
|
|
362
|
+
$totalAbsAcceleration = 0.0;
|
|
363
|
+
for ($i = 1; $i < count($segments); $i++) {
|
|
364
|
+
$s1 = $segments[$i - 1];
|
|
365
|
+
$s2 = $segments[$i];
|
|
366
|
+
if ($s2['dt'] > 0) {
|
|
367
|
+
$acceleration = ($s2['speed'] - $s1['speed']) / $s2['dt'];
|
|
368
|
+
$totalAbsAcceleration += abs($acceleration);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
$avgAcceleration = $totalAbsAcceleration / (count($segments) - 1);
|
|
292
372
|
|
|
373
|
+
$startPoint = $history[0];
|
|
374
|
+
$endPoint = $history[count($history) - 1];
|
|
375
|
+
$straightDistance = sqrt(pow($endPoint['x'] - $startPoint['x'], 2) + pow($endPoint['y'] - $startPoint['y'], 2));
|
|
376
|
+
$straightness = $totalDistance > 0 ? $straightDistance / $totalDistance : 1.0;
|
|
377
|
+
|
|
378
|
+
return [
|
|
379
|
+
'avgSpeed' => $avgSpeed,
|
|
380
|
+
'avgAcceleration' => $avgAcceleration,
|
|
381
|
+
'straightness' => $straightness,
|
|
382
|
+
'pauses' => $pauses,
|
|
383
|
+
'segments' => array_column($segments, 'distance'),
|
|
384
|
+
'avgPressure' => $avgPressure,
|
|
385
|
+
'avgRadius' => $avgRadius,
|
|
386
|
+
'pressureVariance' => $pressureVariance,
|
|
387
|
+
'radiusVariance' => $radiusVariance,
|
|
388
|
+
'maxTouches' => $maxTouches
|
|
389
|
+
];
|
|
390
|
+
}
|
|
293
391
|
|
|
294
392
|
/**
|
|
295
393
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
@@ -314,6 +412,7 @@ class RequestUtils
|
|
|
314
412
|
$score = 0.0;
|
|
315
413
|
|
|
316
414
|
$mouseAnalysis = self::analyzeMouseMovements($metrics['mouseMovementsHistory'] ?? null);
|
|
415
|
+
$touch = self::analyzeTouchMovements($metrics['touchMovementsHistory'] ?? null);
|
|
317
416
|
|
|
318
417
|
if (isset($metrics['historyLength'])) {
|
|
319
418
|
if ($metrics['historyLength'] === 1) $score += 15;
|
|
@@ -321,7 +420,7 @@ class RequestUtils
|
|
|
321
420
|
elseif ($metrics['historyLength'] >= 2) $score -= 10;
|
|
322
421
|
} else {
|
|
323
422
|
// Pénalité pour absence totale d'interaction si l'historique n'est pas dispo
|
|
324
|
-
if ($mouseAnalysis['avgSpeed'] == 0 && ($metrics['keystrokeLatency'] ?? 0) == 0) {
|
|
423
|
+
if ($mouseAnalysis['avgSpeed'] == 0 && $touch['avgSpeed'] == 0 && ($metrics['keystrokeLatency'] ?? 0) == 0) {
|
|
325
424
|
$score += 40;
|
|
326
425
|
}
|
|
327
426
|
}
|
|
@@ -343,6 +442,30 @@ class RequestUtils
|
|
|
343
442
|
$score += 35;
|
|
344
443
|
}
|
|
345
444
|
}
|
|
445
|
+
// Analyse comportementale des événements tactiles (Touch Move)
|
|
446
|
+
$touchHistory = $metrics['touchMovementsHistory'] ?? null;
|
|
447
|
+
if (!empty($touchHistory)) {
|
|
448
|
+
if ($touch['avgSpeed'] > 0) {
|
|
449
|
+
if ($touch['avgSpeed'] > 5) $score += 30;
|
|
450
|
+
if ($touch['avgAcceleration'] > 0.8) $score += 20;
|
|
451
|
+
if ($touch['straightness'] > 0.98) $score += 35;
|
|
452
|
+
if ($touch['pauses'] === 0 && count($touch['segments']) > 25) $score += 15;
|
|
453
|
+
|
|
454
|
+
// Détection de l'émulation (pression et rayon de contact constants)
|
|
455
|
+
if ($touch['avgPressure'] > 0 && $touch['pressureVariance'] == 0) {
|
|
456
|
+
$score += 30;
|
|
457
|
+
}
|
|
458
|
+
if ($touch['avgRadius'] > 0 && $touch['radiusVariance'] == 0) {
|
|
459
|
+
$score += 30;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (count($touch['segments']) > 10) {
|
|
463
|
+
$benfordDev = Optimization::benfordTest($touch['segments']);
|
|
464
|
+
if ($benfordDev > 0.18) {
|
|
465
|
+
$score += 35;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
346
469
|
|
|
347
470
|
return ['behaviorScore' => min(100.0, $score)];
|
|
348
471
|
}
|
|
@@ -553,9 +676,9 @@ class RequestUtils
|
|
|
553
676
|
* @param string $fpString La chaîne d'empreinte complète.
|
|
554
677
|
* @return string La sous-chaîne de l'empreinte contenant uniquement les parties stables.
|
|
555
678
|
*/
|
|
556
|
-
|
|
679
|
+
public static function extractStablePart(string $fpString): string
|
|
557
680
|
{
|
|
558
|
-
$stableKeys = ['
|
|
681
|
+
$stableKeys = ['ua', 'ja3', 'ja4', 'h2', 'tcp'];
|
|
559
682
|
$parts = explode('|', $fpString);
|
|
560
683
|
$stableParts = [];
|
|
561
684
|
foreach ($parts as $part) {
|
|
@@ -584,6 +707,9 @@ class RequestUtils
|
|
|
584
707
|
$patternWeight = $patternConfig['patternWeight'] ?? 80;
|
|
585
708
|
$decayFactor = $patternConfig['decayFactor'] ?? 0.95;
|
|
586
709
|
$inactivityReset = $patternConfig['inactivityReset'] ?? 180000;
|
|
710
|
+
$regularityRatio = $patternConfig['regularityRatio'] ?? 0.4;
|
|
711
|
+
$benfordRatio = $patternConfig['benfordRatio'] ?? 0.3;
|
|
712
|
+
$enumerationRatio = $patternConfig['enumerationRatio'] ?? 0.3;
|
|
587
713
|
|
|
588
714
|
$now = time() * 1000;
|
|
589
715
|
$history = $deviceData['requestHistory'] ?? [];
|
|
@@ -606,7 +732,8 @@ class RequestUtils
|
|
|
606
732
|
}
|
|
607
733
|
$deviceData['requestHistory'] = $history;
|
|
608
734
|
|
|
609
|
-
$
|
|
735
|
+
$regularityScore = 0.0;
|
|
736
|
+
$benfordScore = 0.0;
|
|
610
737
|
$timings = $deviceData['timingHistory'];
|
|
611
738
|
|
|
612
739
|
// Analyse statistique si nous avons assez de données
|
|
@@ -621,18 +748,16 @@ class RequestUtils
|
|
|
621
748
|
$stdDev = sqrt($variance);
|
|
622
749
|
$benfordDeviation = Optimization::benfordTest($timings);
|
|
623
750
|
|
|
624
|
-
// Détection de régularité (bots de type "cron")
|
|
625
751
|
if ($stdDev < $regularityThreshold) {
|
|
626
|
-
$
|
|
752
|
+
$regularityScore = 1.0 - ($stdDev / $regularityThreshold);
|
|
627
753
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
$instantScore = $patternWeight;
|
|
754
|
+
if ($benfordDeviation > $benfordThreshold) {
|
|
755
|
+
$benfordScore = min(1.0, ($benfordDeviation - $benfordThreshold) / (0.5 - $benfordThreshold));
|
|
631
756
|
}
|
|
632
757
|
}
|
|
633
758
|
|
|
634
|
-
//
|
|
635
|
-
$enumerationScore = 0;
|
|
759
|
+
// Path enumeration progressif
|
|
760
|
+
$enumerationScore = 0.0;
|
|
636
761
|
if (count($history) >= 3) {
|
|
637
762
|
$templates = array_map(function($h) {
|
|
638
763
|
return preg_replace('/\d+/', '{num}', $h['path']);
|
|
@@ -646,10 +771,15 @@ class RequestUtils
|
|
|
646
771
|
$maxTemplateRepetition = !empty($templateCounts) ? max($templateCounts) : 0;
|
|
647
772
|
|
|
648
773
|
if ($maxTemplateRepetition >= 3 && count($uniquePaths) === count($history)) {
|
|
649
|
-
$enumerationScore = $
|
|
774
|
+
$enumerationScore = min(1.0, ($maxTemplateRepetition - 2) / 5.0);
|
|
650
775
|
}
|
|
651
776
|
}
|
|
652
777
|
|
|
778
|
+
$weightedScore = ($regularityScore * $regularityRatio) +
|
|
779
|
+
($benfordScore * $benfordRatio) +
|
|
780
|
+
($enumerationScore * $enumerationRatio);
|
|
781
|
+
$instantScore = $weightedScore * $patternWeight;
|
|
782
|
+
|
|
653
783
|
// Logique de décroissance et de score final
|
|
654
784
|
$newPatternScore = $deviceData['lastPatternScore'] ?? 0;
|
|
655
785
|
|
|
@@ -660,7 +790,7 @@ class RequestUtils
|
|
|
660
790
|
}
|
|
661
791
|
$newPatternScore = max(0, $newPatternScore);
|
|
662
792
|
|
|
663
|
-
$deviceData['lastPatternScore'] = $
|
|
793
|
+
$deviceData['lastPatternScore'] = max((float)$instantScore, (float)$newPatternScore);
|
|
664
794
|
|
|
665
795
|
return ['requestPatternScore' => min(100.0, $deviceData['lastPatternScore'])];
|
|
666
796
|
}
|
|
@@ -914,14 +1044,18 @@ class RequestUtils
|
|
|
914
1044
|
$subnetData['highScoreDevices'] = [];
|
|
915
1045
|
}
|
|
916
1046
|
|
|
917
|
-
|
|
1047
|
+
// Utilisation de la partie stable du fingerprint matériel plutôt que l'ID de cookie volatil
|
|
1048
|
+
$currentDeviceHash = self::getCompositeDeviceHash($context);
|
|
1049
|
+
$stableFpId = FingerprintBuilder::cyrb53(self::extractStablePart($currentDeviceHash));
|
|
1050
|
+
|
|
1051
|
+
$currentDeviceContributions = $subnetData['highScoreDevices'][$stableFpId] ?? 0;
|
|
918
1052
|
if ($currentDeviceContributions < 5 && $finalScore < 95) {
|
|
919
|
-
$subnetData['highScoreDevices'][$
|
|
1053
|
+
$subnetData['highScoreDevices'][$stableFpId] = $currentDeviceContributions + 1;
|
|
920
1054
|
$subnetData['highScoreCount']++;
|
|
921
1055
|
}
|
|
922
1056
|
|
|
923
|
-
if (!in_array($
|
|
924
|
-
$subnetData['deviceIds'][] = $
|
|
1057
|
+
if (!in_array($stableFpId, $subnetData['deviceIds'], true)) {
|
|
1058
|
+
$subnetData['deviceIds'][] = $stableFpId;
|
|
925
1059
|
}
|
|
926
1060
|
$subnetData['lastActivity'] = time();
|
|
927
1061
|
|
|
@@ -986,6 +1120,56 @@ class RequestUtils
|
|
|
986
1120
|
return ['subnetScore' => min(100.0, $score)];
|
|
987
1121
|
}
|
|
988
1122
|
|
|
1123
|
+
/**
|
|
1124
|
+
* Calcule le score d'anomalie de similarité réseau (Botnet Clustering).
|
|
1125
|
+
* @param RequestContext $context
|
|
1126
|
+
* @param string $stableFpHash
|
|
1127
|
+
* @return array{'botnetClusterScore': float}
|
|
1128
|
+
*/
|
|
1129
|
+
public static function getBotnetClusterScore(RequestContext $context, string $stableFpHash): array
|
|
1130
|
+
{
|
|
1131
|
+
if (empty($stableFpHash)) {
|
|
1132
|
+
return ['botnetClusterScore' => 0.0];
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
$store = StoreManager::getStore();
|
|
1136
|
+
$key = "botnet-cluster:{$stableFpHash}";
|
|
1137
|
+
$now = time();
|
|
1138
|
+
$tenMinutesAgo = $now - 600;
|
|
1139
|
+
|
|
1140
|
+
$clusterData = $store->get($key) ?? [];
|
|
1141
|
+
if (!is_array($clusterData)) {
|
|
1142
|
+
$clusterData = [];
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
$clusterData = array_filter($clusterData, function ($entry) use ($tenMinutesAgo) {
|
|
1146
|
+
return isset($entry['timestamp']) && $entry['timestamp'] > $tenMinutesAgo;
|
|
1147
|
+
});
|
|
1148
|
+
$clusterData = array_values($clusterData);
|
|
1149
|
+
|
|
1150
|
+
$found = false;
|
|
1151
|
+
foreach ($clusterData as &$entry) {
|
|
1152
|
+
if (isset($entry['ip']) && $entry['ip'] === $context->clientIp) {
|
|
1153
|
+
$entry['timestamp'] = $now;
|
|
1154
|
+
$found = true;
|
|
1155
|
+
break;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
unset($entry);
|
|
1159
|
+
|
|
1160
|
+
if (!$found) {
|
|
1161
|
+
$clusterData[] = ['ip' => $context->clientIp, 'timestamp' => $now];
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
$store->set($key, $clusterData, 600);
|
|
1165
|
+
$uniqueIpsCount = count($clusterData);
|
|
1166
|
+
$botnetClusterScore = 0.0;
|
|
1167
|
+
if ($uniqueIpsCount >= 2) {
|
|
1168
|
+
$botnetClusterScore = min(100.0, round(100.0 * (1.0 - exp(-0.35 * ($uniqueIpsCount - 1))), 1));
|
|
1169
|
+
}
|
|
1170
|
+
return ['botnetClusterScore' => $botnetClusterScore];
|
|
1171
|
+
}
|
|
1172
|
+
|
|
989
1173
|
/**
|
|
990
1174
|
* Calcule le score de réputation d'une IP en appliquant la décroissance temporelle.
|
|
991
1175
|
*/
|