@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.
@@ -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
  *