@anonympins/fingerprint 0.4.5 → 0.4.6
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 +18 -0
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.js +442 -59
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +381 -285
- package/src/js/pow.solver.js +97 -1
- package/src/js/tests/fingerprint.test.js +59 -0
- package/src/php/AutoTuner.php +1 -1
- package/src/php/Challenge/ChallengeUtils.php +97 -0
- package/src/php/FingerprintEngine.php +136 -8
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/RequestUtils.php +45 -0
|
@@ -10,6 +10,7 @@ use Anonympins\Fingerprint\FingerprintEngine;
|
|
|
10
10
|
use Anonympins\Fingerprint\RequestContext;
|
|
11
11
|
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
12
12
|
use Anonympins\Fingerprint\Store\StoreManager;
|
|
13
|
+
use Anonympins\Fingerprint\Challenge\ChallengeUtils;
|
|
13
14
|
use PHPUnit\Framework\TestCase;
|
|
14
15
|
|
|
15
16
|
class FingerprintEngineTest extends TestCase
|
|
@@ -90,6 +91,35 @@ class FingerprintEngineTest extends TestCase
|
|
|
90
91
|
$this->assertLessThan($this->securityConfig['thresholds']['low'], $secondDecision['score']);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
public function testTlsSessionResumptionCookielessTracking(): void
|
|
95
|
+
{
|
|
96
|
+
// 1. Première requête d'un client avec un ID de session TLS, sans cookie
|
|
97
|
+
$tlsSessionId = 'test-tls-1.3-session-resumption-id-12345';
|
|
98
|
+
$context1 = $this->createRequestContext([
|
|
99
|
+
'headers' => [
|
|
100
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
|
101
|
+
'x-tls-session-id' => $tlsSessionId,
|
|
102
|
+
]
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
$firstDecision = $this->engine->processRequest($context1);
|
|
106
|
+
$this->assertArrayHasKey('newCookieForResponse', $firstDecision, "Un cookie d'identité d'appareil doit être généré.");
|
|
107
|
+
$deviceId = $firstDecision['newCookieForResponse']['value'];
|
|
108
|
+
|
|
109
|
+
// 2. Deuxième requête : le client supprime ses cookies, mais garde la même session TLS (Session Resumption / 0-RTT)
|
|
110
|
+
$context2 = $this->createRequestContext([
|
|
111
|
+
'headers' => [
|
|
112
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
|
113
|
+
'x-tls-session-id' => $tlsSessionId,
|
|
114
|
+
],
|
|
115
|
+
'cookies' => [] // Cookies supprimés !
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
$secondDecision = $this->engine->processRequest($context2);
|
|
119
|
+
$this->assertEquals('next', $secondDecision['action']);
|
|
120
|
+
$this->assertArrayNotHasKey('newCookieForResponse', $secondDecision, "Aucun nouveau cookie d'identité ne doit être généré car la session TLS a permis de restaurer l'identité.");
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
public function testIssuesChallengeForSuspiciousRequest(): void
|
|
94
124
|
{
|
|
95
125
|
// 1. First, establish a device identity to avoid the cookie-dropping penalty
|
|
@@ -354,4 +384,79 @@ class FingerprintEngineTest extends TestCase
|
|
|
354
384
|
// Le poids de tcpAnomalyScore dans le profil balanced est de 0.8 (80 * 0.8 = 64)
|
|
355
385
|
$this->assertGreaterThanOrEqual(64.0, $decision['score']);
|
|
356
386
|
}
|
|
387
|
+
|
|
388
|
+
public function testDetectsAndVerifiesSpaceChallenge(): void
|
|
389
|
+
{
|
|
390
|
+
$store = StoreManager::getStore();
|
|
391
|
+
|
|
392
|
+
$config = SecurityProfiles::createSecurityProfile('balanced', [
|
|
393
|
+
'enableProofOfSpace' => true,
|
|
394
|
+
'pospace' => [
|
|
395
|
+
'sizeMb' => 1,
|
|
396
|
+
'numQueries' => 5
|
|
397
|
+
],
|
|
398
|
+
'challengeNewDevices' => false,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
$engine = $this->getMockBuilder(FingerprintEngine::class)
|
|
402
|
+
->setConstructorArgs([$config])
|
|
403
|
+
->onlyMethods(['calculateFinalScore'])
|
|
404
|
+
->getMock();
|
|
405
|
+
$engine->method('calculateFinalScore')->willReturn(30.0);
|
|
406
|
+
|
|
407
|
+
$headers = [
|
|
408
|
+
'user-agent' => 'A normal browser',
|
|
409
|
+
'accept-language' => 'en-US,en;q=0.9',
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
// 1. Première requête provoquant un challenge
|
|
413
|
+
$context = $this->createRequestContext([
|
|
414
|
+
'headers' => $headers
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
$decision = $engine->processRequest($context);
|
|
418
|
+
$this->assertEquals('challenge', $decision['action']);
|
|
419
|
+
|
|
420
|
+
$html = $decision['body'];
|
|
421
|
+
$this->assertStringContainsString('initializeSpace', $html);
|
|
422
|
+
|
|
423
|
+
preg_match('/const nonce = "(.*?)";/', $html, $matchesNonce);
|
|
424
|
+
preg_match('/const clientSecret = "(.*?)";/', $html, $matchesSecret);
|
|
425
|
+
preg_match('/const queries = (\[.*?\]);/', $html, $matchesQueries);
|
|
426
|
+
|
|
427
|
+
$nonce = $matchesNonce[1];
|
|
428
|
+
$clientSecret = $matchesSecret[1];
|
|
429
|
+
$queries = json_decode($matchesQueries[1], true);
|
|
430
|
+
|
|
431
|
+
// 2. Résoudre le challenge
|
|
432
|
+
$combined = '';
|
|
433
|
+
foreach ($queries as $idx) {
|
|
434
|
+
$combined .= $this->callPrivateGenerateBlock($nonce . ":" . $clientSecret, (int)$idx);
|
|
435
|
+
}
|
|
436
|
+
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
437
|
+
$solution = hash('sha256', $finalBlock);
|
|
438
|
+
|
|
439
|
+
// 3. Soumettre le challenge
|
|
440
|
+
$submitContext = $this->createRequestContext([
|
|
441
|
+
'headers' => $headers,
|
|
442
|
+
'query' => [
|
|
443
|
+
'pow_type' => 'pospace',
|
|
444
|
+
'pow_nonce' => $nonce,
|
|
445
|
+
'pow_solution_space' => $solution
|
|
446
|
+
]
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
$engineReal = new FingerprintEngine($config);
|
|
450
|
+
$submitDecision = $engineReal->processRequest($submitContext);
|
|
451
|
+
$this->assertEquals('redirect', $submitDecision['action']);
|
|
452
|
+
$this->assertArrayHasKey('cookie', $submitDecision);
|
|
453
|
+
$this->assertEquals('pow_clearance', $submitDecision['cookie']['name']);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private function callPrivateGenerateBlock(string $seed, int $blockIndex): string
|
|
457
|
+
{
|
|
458
|
+
$ref = new \ReflectionClass(ChallengeUtils::class);
|
|
459
|
+
$method = $ref->getMethod('generateBlock');
|
|
460
|
+
return $method->invoke(null, $seed, $blockIndex);
|
|
461
|
+
}
|
|
357
462
|
}
|
|
@@ -35,6 +35,19 @@ class RequestUtilsTest extends TestCase
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
public function testGetCrossLayerInconsistencyViewportExceedsScreen(): void
|
|
39
|
+
{
|
|
40
|
+
$scrHash = FingerprintBuilder::cyrb53("1920x1080_24");
|
|
41
|
+
$context = $this->createRequestContext([
|
|
42
|
+
'headers' => [
|
|
43
|
+
'x-device-fingerprint' => "scr:{$scrHash}",
|
|
44
|
+
'sec-ch-viewport-width' => '2560'
|
|
45
|
+
]
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
49
|
+
$this->assertEquals(20.0, $score['crossLayerInconsistencyScore']);
|
|
50
|
+
}
|
|
38
51
|
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
39
52
|
{
|
|
40
53
|
$context = $this->createRequestContext([
|
|
@@ -552,6 +552,51 @@ class RequestUtils
|
|
|
552
552
|
}
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
// 2. Incohérence de l'écran (si les Client Hints sont disponibles)
|
|
556
|
+
$clientScreenHash = $clientFpMap['scr'] ?? null;
|
|
557
|
+
$viewportWidth = $context->getHeader('sec-ch-viewport-width');
|
|
558
|
+
if ($clientScreenHash && $viewportWidth) {
|
|
559
|
+
$viewportWidthInt = (int)$viewportWidth;
|
|
560
|
+
$matchedScreenWidth = null;
|
|
561
|
+
$commonWidths = [320, 360, 375, 390, 412, 414, 768, 1024, 1280, 1366, 1440, 1536, 1600, 1920, 2560, 3840];
|
|
562
|
+
$commonHeights = [480, 568, 640, 667, 736, 800, 812, 844, 896, 900, 1024, 1080, 1200, 1440, 1600, 2160];
|
|
563
|
+
$commonDepths = [24, 30, 32];
|
|
564
|
+
|
|
565
|
+
foreach ($commonWidths as $w) {
|
|
566
|
+
foreach ($commonHeights as $h) {
|
|
567
|
+
foreach ($commonDepths as $d) {
|
|
568
|
+
$candidate = "{$w}x{$h}_{$d}";
|
|
569
|
+
$candidateHash = (string)FingerprintBuilder::cyrb53($candidate);
|
|
570
|
+
if ($clientScreenHash === $candidateHash) {
|
|
571
|
+
$matchedScreenWidth = $w;
|
|
572
|
+
break 3;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if ($matchedScreenWidth !== null && $viewportWidthInt > $matchedScreenWidth) {
|
|
579
|
+
$score += 20;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 3. Incohérence du GPU/Canvas et JA3
|
|
584
|
+
$clientGpuHash = $clientFpMap['gpu'] ?? null;
|
|
585
|
+
$ja3 = $context->ja3;
|
|
586
|
+
if ($clientGpuHash && $ja3) {
|
|
587
|
+
$expectedClients = self::TLS_FINGERPRINT_DB[$ja3] ?? null;
|
|
588
|
+
if ($expectedClients) {
|
|
589
|
+
if (!is_array($expectedClients)) {
|
|
590
|
+
$expectedClients = [$expectedClients];
|
|
591
|
+
}
|
|
592
|
+
$nonBrowserLibraries = ['Python', 'Go', 'Java', 'curl'];
|
|
593
|
+
$isLibrary = !empty(array_intersect($expectedClients, $nonBrowserLibraries));
|
|
594
|
+
if ($isLibrary) {
|
|
595
|
+
$score += 30;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
555
600
|
return ['crossLayerInconsistencyScore' => min(100.0, $score)];
|
|
556
601
|
}
|
|
557
602
|
|