@anonympins/fingerprint 0.5.0 → 0.5.1
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 +22 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.js +221 -23
- package/src/js/fingerprint.js +148 -9
- package/src/js/gpu_pow.solver.js +217 -0
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.test.js +63 -0
- package/src/js/tests/gpu_pow.test.js +118 -0
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/Challenge/ChallengeUtils.php +42 -0
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +21 -0
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +77 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/RequestUtils.php +120 -0
|
@@ -10,6 +10,83 @@ use PHPUnit\Framework\TestCase;
|
|
|
10
10
|
|
|
11
11
|
class ChallengeUtilsTest extends TestCase
|
|
12
12
|
{
|
|
13
|
+
public function testGpuPowDeterministicHashing(): void
|
|
14
|
+
{
|
|
15
|
+
$seed = 'test-seed-determinism';
|
|
16
|
+
$f1 = ChallengeUtils::hashSeedToFloat($seed);
|
|
17
|
+
$f2 = ChallengeUtils::hashSeedToFloat($seed);
|
|
18
|
+
$f3 = ChallengeUtils::hashSeedToFloat('different-seed');
|
|
19
|
+
|
|
20
|
+
$this->assertEquals($f1, $f2, 'Seed hashing must be deterministic.');
|
|
21
|
+
$this->assertGreaterThanOrEqual(0, $f1);
|
|
22
|
+
$this->assertLessThan(1, $f1);
|
|
23
|
+
$this->assertNotEquals($f1, $f3);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public function testGpuPowVerificationRejectsTamperedSolution(): void
|
|
27
|
+
{
|
|
28
|
+
$seed = 'php-verification-seed';
|
|
29
|
+
$iterations = 100;
|
|
30
|
+
|
|
31
|
+
// Generate correct solutions mimicking the GPU solver trajectory
|
|
32
|
+
$numericSeed = ChallengeUtils::hashSeedToFloat($seed);
|
|
33
|
+
$r = 3.9999;
|
|
34
|
+
$solutions = [];
|
|
35
|
+
$fround = function (float $value): float {
|
|
36
|
+
return unpack('f', pack('f', $value))[1];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for ($idx = 0; $idx < 64; $idx++) {
|
|
40
|
+
$x = $fround($numericSeed + $idx * 0.015);
|
|
41
|
+
$rFloat = $fround($r);
|
|
42
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
43
|
+
$x = $fround($rFloat * $x * $fround(1.0 - $x));
|
|
44
|
+
}
|
|
45
|
+
$solutions[] = number_format($x, 6, '.', '');
|
|
46
|
+
}
|
|
47
|
+
$validSolution = implode(',', $solutions);
|
|
48
|
+
|
|
49
|
+
$this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $validSolution));
|
|
50
|
+
|
|
51
|
+
// Tamper with value on verified sample index 12
|
|
52
|
+
$tampered = $solutions;
|
|
53
|
+
$tampered[12] = (string)((float)$tampered[12] + 0.0002); // Out of tolerance
|
|
54
|
+
$this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tampered)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public function testGpuPowVerification(): void
|
|
58
|
+
{
|
|
59
|
+
$seed = 'php-gpu-test-seed';
|
|
60
|
+
$iterations = 50;
|
|
61
|
+
|
|
62
|
+
$hash = 0;
|
|
63
|
+
for ($i = 0; $i < strlen($seed); $i++) {
|
|
64
|
+
$hash = (($hash << 5) - $hash + ord($seed[$i])) & 0xffffffff;
|
|
65
|
+
if ($hash & 0x80000000) {
|
|
66
|
+
$hash = $hash - 0x100000000;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
$numericSeed = abs($hash % 1000000) / 1000000;
|
|
70
|
+
$r = 3.9999;
|
|
71
|
+
$solutions = [];
|
|
72
|
+
$fround = function (float $value): float {
|
|
73
|
+
return unpack('f', pack('f', $value))[1];
|
|
74
|
+
};
|
|
75
|
+
for ($idx = 0; $idx < 64; $idx++) {
|
|
76
|
+
$x = $fround($numericSeed + $idx * 0.015);
|
|
77
|
+
$rFloat = $fround($r);
|
|
78
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
79
|
+
$x = $fround($rFloat * $x * $fround(1.0 - $x));
|
|
80
|
+
}
|
|
81
|
+
$solutions[] = number_format($x, 6, '.', '');
|
|
82
|
+
}
|
|
83
|
+
$solutionString = implode(',', $solutions);
|
|
84
|
+
$this->assertTrue(ChallengeUtils::verifyGpuPow($seed, $iterations, $solutionString));
|
|
85
|
+
$tamperedSolutions = $solutions;
|
|
86
|
+
$tamperedSolutions[0] = (string)((float)$tamperedSolutions[0] + 0.1);
|
|
87
|
+
$this->assertFalse(ChallengeUtils::verifyGpuPow($seed, $iterations, implode(',', $tamperedSolutions)));
|
|
88
|
+
}
|
|
89
|
+
|
|
13
90
|
protected function setUp(): void
|
|
14
91
|
{
|
|
15
92
|
// Définir une clé secrète pour les tests
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
8
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
9
|
+
use PHPUnit\Framework\TestCase;
|
|
10
|
+
|
|
11
|
+
class QuicFingerprintTest extends TestCase
|
|
12
|
+
{
|
|
13
|
+
public function testGetQuicAnomalyScoreReturnsZeroIfNoFp(): void
|
|
14
|
+
{
|
|
15
|
+
$context = $this->createMock(RequestContext::class);
|
|
16
|
+
$context->method('getHeader')->willReturn(null);
|
|
17
|
+
$res = RequestUtils::getQuicAnomalyScore($context);
|
|
18
|
+
$this->assertEquals(0.0, $res['quicAnomalyScore']);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function testGetQuicAnomalyScoreDetectsSpoofedChrome(): void
|
|
22
|
+
{
|
|
23
|
+
$context = $this->createMock(RequestContext::class);
|
|
24
|
+
$context->method('getHeader')->willReturnCallback(function($name) {
|
|
25
|
+
if ($name === 'user-agent') {
|
|
26
|
+
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
27
|
+
}
|
|
28
|
+
if ($name === 'x-quic-fp') {
|
|
29
|
+
return '1;1=65536,4=50;i=0';
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
$res = RequestUtils::getQuicAnomalyScore($context);
|
|
35
|
+
$this->assertEquals(100.0, $res['quicAnomalyScore']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function testGetQuicAnomalyScoreAllowsLegitChrome(): void
|
|
39
|
+
{
|
|
40
|
+
$context = $this->createMock(RequestContext::class);
|
|
41
|
+
$context->method('getHeader')->willReturnCallback(function($name) {
|
|
42
|
+
if ($name === 'user-agent') {
|
|
43
|
+
return 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
|
|
44
|
+
}
|
|
45
|
+
if ($name === 'x-quic-fp') {
|
|
46
|
+
return '1;1=1572864,4=100;u=2,i';
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
$res = RequestUtils::getQuicAnomalyScore($context);
|
|
52
|
+
$this->assertEquals(0.0, $res['quicAnomalyScore']);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -48,6 +48,28 @@ class RequestUtilsTest extends TestCase
|
|
|
48
48
|
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
49
49
|
$this->assertEquals(20.0, $score['crossLayerInconsistencyScore']);
|
|
50
50
|
}
|
|
51
|
+
public function testGetRenderingAnomalyScoreEmpty(): void
|
|
52
|
+
{
|
|
53
|
+
$context = $this->createRequestContext();
|
|
54
|
+
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
55
|
+
$this->assertEquals(0.0, $res['renderingAnomalyScore']);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public function testGetRenderingAnomalyScoreSpoofed(): void
|
|
59
|
+
{
|
|
60
|
+
$metrics = [
|
|
61
|
+
'rendering' => [
|
|
62
|
+
'fps' => 60,
|
|
63
|
+
'jitter' => 12.5,
|
|
64
|
+
'offscreenAnom' => false
|
|
65
|
+
]
|
|
66
|
+
];
|
|
67
|
+
$context = $this->createRequestContext([
|
|
68
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
69
|
+
]);
|
|
70
|
+
$res = RequestUtils::getRenderingAnomalyScore($context);
|
|
71
|
+
$this->assertEquals(65.0, $res['renderingAnomalyScore']);
|
|
72
|
+
}
|
|
51
73
|
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
52
74
|
{
|
|
53
75
|
$context = $this->createRequestContext([
|
|
@@ -185,6 +207,27 @@ class RequestUtilsTest extends TestCase
|
|
|
185
207
|
$this->assertEquals(40.0, $scoreNoActivity['behaviorScore']);
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
public function testGetBehaviorScoreWithBotLikeKeystrokeDynamics(): void
|
|
211
|
+
{
|
|
212
|
+
$metrics = [
|
|
213
|
+
'honeypotInteraction' => false,
|
|
214
|
+
'keystrokeLatency' => 100.0,
|
|
215
|
+
'keystrokeDwellTimes' => [50, 50, 50, 50, 50],
|
|
216
|
+
'keystrokeFlightTimes' => [
|
|
217
|
+
['digraph' => 'ab', 'time' => 100],
|
|
218
|
+
['digraph' => 'bc', 'time' => 100],
|
|
219
|
+
['digraph' => 'cd', 'time' => 100],
|
|
220
|
+
['digraph' => 'de', 'time' => 100],
|
|
221
|
+
['digraph' => 'ef', 'time' => 100]
|
|
222
|
+
]
|
|
223
|
+
];
|
|
224
|
+
$context = $this->createRequestContext([
|
|
225
|
+
'headers' => ['x-behavior-metrics' => json_encode($metrics)]
|
|
226
|
+
]);
|
|
227
|
+
$score = RequestUtils::getBehaviorScore($context);
|
|
228
|
+
$this->assertGreaterThanOrEqual(70.0, $score['behaviorScore']);
|
|
229
|
+
}
|
|
230
|
+
|
|
188
231
|
public function testGetBehaviorScoreWithBotLikeTouchMovements(): void
|
|
189
232
|
{
|
|
190
233
|
$metrics = [
|
|
@@ -436,6 +436,41 @@ class RequestUtils
|
|
|
436
436
|
if (($metrics['keystrokeLatency'] ?? 0) > 0 && $metrics['keystrokeLatency'] < 40) $score += 25;
|
|
437
437
|
if (($metrics['keystrokeLatency'] ?? 0) > 1000) $score += 15;
|
|
438
438
|
|
|
439
|
+
// NOUVEAU: Analyse de digraphie/trigraphie (dwell & flight times)
|
|
440
|
+
$dwellTimes = $metrics['keystrokeDwellTimes'] ?? [];
|
|
441
|
+
$flightTimes = $metrics['keystrokeFlightTimes'] ?? [];
|
|
442
|
+
|
|
443
|
+
if (count($dwellTimes) >= 5) {
|
|
444
|
+
$meanDwell = array_sum($dwellTimes) / count($dwellTimes);
|
|
445
|
+
$varDwell = array_reduce($dwellTimes, fn($carry, $item) => $carry + pow($item - $meanDwell, 2), 0) / count($dwellTimes);
|
|
446
|
+
$stdDevDwell = sqrt($varDwell);
|
|
447
|
+
|
|
448
|
+
if ($stdDevDwell < 2.0) {
|
|
449
|
+
$score += 35.0;
|
|
450
|
+
}
|
|
451
|
+
if ($meanDwell < 15.0) {
|
|
452
|
+
$score += 25.0;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (count($flightTimes) >= 5) {
|
|
457
|
+
$times = array_column($flightTimes, 'time');
|
|
458
|
+
$meanFlight = array_sum($times) / count($times);
|
|
459
|
+
$varFlight = array_reduce($times, fn($carry, $item) => $carry + pow($item - $meanFlight, 2), 0) / count($times);
|
|
460
|
+
$stdDevFlight = sqrt($varFlight);
|
|
461
|
+
|
|
462
|
+
if ($stdDevFlight < 3.0) {
|
|
463
|
+
$score += 35.0;
|
|
464
|
+
}
|
|
465
|
+
if ($meanFlight < 25.0) {
|
|
466
|
+
$score += 25.0;
|
|
467
|
+
}
|
|
468
|
+
$benfordDev = Optimization::benfordTest($times);
|
|
469
|
+
if ($benfordDev > 0.18) {
|
|
470
|
+
$score += 30.0;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
439
474
|
// Analyse de Benford sur les segments de mouvement de la souris
|
|
440
475
|
if (count($mouseAnalysis['segments']) > 10) {
|
|
441
476
|
$benfordDeviation = Optimization::benfordTest($mouseAnalysis['segments']);
|
|
@@ -471,6 +506,42 @@ class RequestUtils
|
|
|
471
506
|
return ['behaviorScore' => min(100.0, $score)];
|
|
472
507
|
}
|
|
473
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Calcule un score basé sur la régularité d'affichage (V-Sync) et l'utilisation suspecte d'OffscreenCanvas.
|
|
511
|
+
* @return array{'renderingAnomalyScore': float}
|
|
512
|
+
*/
|
|
513
|
+
public static function getRenderingAnomalyScore(RequestContext $context): array
|
|
514
|
+
{
|
|
515
|
+
$behaviorHeader = $context->getHeader('x-behavior-metrics');
|
|
516
|
+
if (!$behaviorHeader) {
|
|
517
|
+
return ['renderingAnomalyScore' => 0.0];
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
$metrics = json_decode($behaviorHeader, true);
|
|
521
|
+
if (json_last_error() !== JSON_ERROR_NONE || !isset($metrics['rendering'])) {
|
|
522
|
+
return ['renderingAnomalyScore' => 0.0];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
$rendering = $metrics['rendering'];
|
|
526
|
+
$score = 0.0;
|
|
527
|
+
|
|
528
|
+
if (!empty($rendering['offscreenAnom'])) {
|
|
529
|
+
$score += 100.0;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
$fps = (float)($rendering['fps'] ?? 0.0);
|
|
533
|
+
$jitter = (float)($rendering['jitter'] ?? 0.0);
|
|
534
|
+
|
|
535
|
+
if ($fps > 250.0 || ($fps > 0.0 && $fps < 15.0)) {
|
|
536
|
+
$score += 50.0;
|
|
537
|
+
}
|
|
538
|
+
if ($jitter > 6.0) {
|
|
539
|
+
$score += min(80.0, ($jitter - 6.0) * 10.0);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return ['renderingAnomalyScore' => min(100.0, $score)];
|
|
543
|
+
}
|
|
544
|
+
|
|
474
545
|
/**
|
|
475
546
|
* Analyse basique d'un User-Agent.
|
|
476
547
|
* @return array{browser?: string, os?: string, device?: string}
|
|
@@ -1587,6 +1658,55 @@ class RequestUtils
|
|
|
1587
1658
|
return ['tcpAnomalyScore' => $tcpAnomalyScore];
|
|
1588
1659
|
}
|
|
1589
1660
|
|
|
1661
|
+
/**
|
|
1662
|
+
* Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
|
|
1663
|
+
* @param RequestContext $context
|
|
1664
|
+
* @return array{'quicAnomalyScore': float}
|
|
1665
|
+
*/
|
|
1666
|
+
public static function getQuicAnomalyScore(RequestContext $context): array
|
|
1667
|
+
{
|
|
1668
|
+
$quicFp = $context->getHeader('x-quic-fp') ?? $context->quicFingerprint ?? null;
|
|
1669
|
+
if (empty($quicFp) || !is_string($quicFp)) {
|
|
1670
|
+
return ['quicAnomalyScore' => 0.0];
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
$parts = explode(';', $quicFp);
|
|
1674
|
+
if (count($parts) < 2) {
|
|
1675
|
+
return ['quicAnomalyScore' => 0.0];
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
$params = [];
|
|
1679
|
+
foreach (explode(',', $parts[1]) as $p) {
|
|
1680
|
+
$kv = explode('=', $p, 2);
|
|
1681
|
+
if (count($kv) === 2) {
|
|
1682
|
+
$params[$kv[0]] = $kv[1];
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
$priorityOrder = $parts[2] ?? '';
|
|
1686
|
+
|
|
1687
|
+
$ua = $context->getHeader('user-agent') ?? '';
|
|
1688
|
+
$uaParts = self::parseUserAgent($ua);
|
|
1689
|
+
$browser = $uaParts['browser'] ?? null;
|
|
1690
|
+
|
|
1691
|
+
if (empty($browser)) {
|
|
1692
|
+
return ['quicAnomalyScore' => 0.0];
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
$anomaly = 0.0;
|
|
1696
|
+
if (str_starts_with($browser, 'Chrome') || str_starts_with($browser, 'Edge')) {
|
|
1697
|
+
$maxData = isset($params['1']) ? (int)$params['1'] : 0;
|
|
1698
|
+
$maxStreams = isset($params['4']) ? (int)$params['4'] : 0;
|
|
1699
|
+
if ($maxData > 0 && $maxData < 1048576) $anomaly += 40.0;
|
|
1700
|
+
if ($maxStreams > 0 && $maxStreams !== 100) $anomaly += 30.0;
|
|
1701
|
+
if (!empty($priorityOrder) && !str_contains($priorityOrder, 'u=')) $anomaly += 30.0;
|
|
1702
|
+
} elseif (str_starts_with($browser, 'Firefox')) {
|
|
1703
|
+
$maxData = isset($params['1']) ? (int)$params['1'] : 0;
|
|
1704
|
+
if ($maxData > 0 && $maxData > 5000000) $anomaly += 40.0;
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
return ['quicAnomalyScore' => max(0.0, min(100.0, $anomaly))];
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1590
1710
|
/**
|
|
1591
1711
|
* Vérifie si un ticket de clearance (PoW) est valide, en supportant la tolérance au roaming.
|
|
1592
1712
|
*
|