@anonympins/fingerprint 0.4.5 → 0.5.0
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 +39 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.client.js +139 -0
- package/src/js/fingerprint.js +767 -215
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +389 -285
- package/src/js/pow.solver.js +105 -1
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +191 -1
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/php/AutoTuner.php +72 -1
- package/src/php/Challenge/ChallengeUtils.php +395 -5
- package/src/php/FingerprintEngine.php +176 -10
- 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/ChallengeUtilsTest.php +208 -81
- package/src/php/Tests/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
- package/src/php/Utils/RequestUtils.php +45 -0
|
@@ -30,6 +30,29 @@
|
|
|
30
30
|
public function __construct(array $securityConfig)
|
|
31
31
|
{
|
|
32
32
|
$this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
|
|
33
|
+
|
|
34
|
+
// Dynamically bind Ed25519 keys if passed via config
|
|
35
|
+
if (isset($securityConfig['ed25519_private_key'])) {
|
|
36
|
+
$_ENV['ED25519_PRIVATE_KEY'] = $securityConfig['ed25519_private_key'];
|
|
37
|
+
}
|
|
38
|
+
if (isset($securityConfig['ed25519_public_key'])) {
|
|
39
|
+
$_ENV['ED25519_PUBLIC_KEY'] = $securityConfig['ed25519_public_key'];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Auto-load optimized config if autotuning savePath is specified
|
|
43
|
+
if (isset($securityConfig['autotuning']['savePath'])) {
|
|
44
|
+
$savePath = $securityConfig['autotuning']['savePath'];
|
|
45
|
+
if (file_exists($savePath)) {
|
|
46
|
+
try {
|
|
47
|
+
$savedConfig = json_decode(file_get_contents($savePath), true);
|
|
48
|
+
if (json_last_error() === JSON_ERROR_NONE && is_array($savedConfig)) {
|
|
49
|
+
$securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
|
|
50
|
+
}
|
|
51
|
+
} catch (\Throwable $e) {
|
|
52
|
+
error_log("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
33
56
|
$this->securityConfig = $securityConfig;
|
|
34
57
|
$this->verbose = $securityConfig['verbose'] ?? false;
|
|
35
58
|
$this->allowlist = $this->buildAllowlist();
|
|
@@ -50,7 +73,8 @@
|
|
|
50
73
|
'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
|
|
51
74
|
'honeypot', 'threatIntel', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger', 'probationaryTtl',
|
|
52
75
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
53
|
-
'similarityThreshold', 'summary', 'description'
|
|
76
|
+
'similarityThreshold', 'summary', 'description',
|
|
77
|
+
'ed25519_private_key', 'ed25519_public_key'
|
|
54
78
|
];
|
|
55
79
|
|
|
56
80
|
if (empty($config['weights'])) {
|
|
@@ -225,6 +249,16 @@
|
|
|
225
249
|
$deviceData = null;
|
|
226
250
|
$newCookie = null;
|
|
227
251
|
|
|
252
|
+
// Cookieless Identity Tracking: Attempt to restore device ID using TLS session resume ID
|
|
253
|
+
$tlsSessionId = $context->tlsSessionId;
|
|
254
|
+
if (!$deviceId && $tlsSessionId) {
|
|
255
|
+
$resumedDeviceId = $store->get("tls-session:{$tlsSessionId}");
|
|
256
|
+
if ($resumedDeviceId) {
|
|
257
|
+
$deviceId = $resumedDeviceId;
|
|
258
|
+
$this->log('Identity resumed via TLS Session ID', ['deviceId' => $deviceId, 'tlsSessionId' => $tlsSessionId]);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
228
262
|
if ($deviceId) {
|
|
229
263
|
$deviceData = $store->get("device:{$deviceId}");
|
|
230
264
|
}
|
|
@@ -275,6 +309,11 @@
|
|
|
275
309
|
}
|
|
276
310
|
}
|
|
277
311
|
|
|
312
|
+
// Bind the current TLS session ID to the device ID
|
|
313
|
+
if ($deviceId && $tlsSessionId) {
|
|
314
|
+
$store->set("tls-session:{$tlsSessionId}", $deviceId, 3600); // 1h cache duration
|
|
315
|
+
}
|
|
316
|
+
|
|
278
317
|
return [
|
|
279
318
|
'deviceId' => $deviceId,
|
|
280
319
|
'deviceData' => $deviceData,
|
|
@@ -515,6 +554,53 @@
|
|
|
515
554
|
];
|
|
516
555
|
}
|
|
517
556
|
|
|
557
|
+
|
|
558
|
+
private function decodePolymorphicFingerprint(string $fpString, array $mapping): string
|
|
559
|
+
{
|
|
560
|
+
if (empty($mapping['keys'])) {
|
|
561
|
+
return $fpString;
|
|
562
|
+
}
|
|
563
|
+
$reverseKeys = array_flip($mapping['keys']);
|
|
564
|
+
$parts = explode('|', $fpString);
|
|
565
|
+
$mappedParts = [];
|
|
566
|
+
foreach ($parts as $part) {
|
|
567
|
+
$pair = explode(':', $part, 2);
|
|
568
|
+
if (count($pair) === 2) {
|
|
569
|
+
$origKey = $reverseKeys[$pair[0]] ?? $pair[0];
|
|
570
|
+
$mappedParts[] = "{$origKey}:{$pair[1]}";
|
|
571
|
+
} else {
|
|
572
|
+
$mappedParts[] = $part;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return implode('|', $mappedParts);
|
|
576
|
+
}
|
|
577
|
+
private function translatePolymorphicHeaders(RequestContext $context): void
|
|
578
|
+
{
|
|
579
|
+
$store = StoreManager::getStore();
|
|
580
|
+
$activeMappings = $store->get('active-polymorphic-mappings') ?: [];
|
|
581
|
+
if (!is_array($activeMappings)) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
foreach ($activeMappings as $mapping) {
|
|
586
|
+
$devFpHeader = strtolower($mapping['headers']['x-device-fingerprint'] ?? '');
|
|
587
|
+
$behaviorHeader = strtolower($mapping['headers']['x-behavior-metrics'] ?? '');
|
|
588
|
+
|
|
589
|
+
if (!empty($devFpHeader) && isset($context->headers[$devFpHeader])) {
|
|
590
|
+
$context->headers['x-device-fingerprint'] = $context->headers[$devFpHeader];
|
|
591
|
+
if (!empty($behaviorHeader) && isset($context->headers[$behaviorHeader])) {
|
|
592
|
+
$context->headers['x-behavior-metrics'] = $context->headers[$behaviorHeader];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
$clientFp = $context->headers['x-device-fingerprint'];
|
|
596
|
+
if ($clientFp && is_string($clientFp)) {
|
|
597
|
+
$context->headers['x-device-fingerprint'] = $this->decodePolymorphicFingerprint($clientFp, $mapping);
|
|
598
|
+
}
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
518
604
|
/**
|
|
519
605
|
* Traite une requête entrante et retourne une décision.
|
|
520
606
|
* @param RequestContext $context Le contexte de la requête.
|
|
@@ -522,8 +608,31 @@
|
|
|
522
608
|
*/
|
|
523
609
|
public function processRequest(RequestContext $context): array
|
|
524
610
|
{
|
|
611
|
+
$this->translatePolymorphicHeaders($context);
|
|
612
|
+
|
|
525
613
|
// Initialiser le vecteur de suspicion pour éviter les erreurs de type.
|
|
526
614
|
$suspicionVector = [];
|
|
615
|
+
|
|
616
|
+
$coopOp = $context->query['coop_op'] ?? null;
|
|
617
|
+
if ($coopOp) {
|
|
618
|
+
$result = ChallengeUtils::handleCooperativeRequest($context->query);
|
|
619
|
+
return [
|
|
620
|
+
'action' => 'challenge',
|
|
621
|
+
'status' => 200,
|
|
622
|
+
'body' => $result
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
$this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
627
|
+
|
|
628
|
+
// Parse GraphQL query if applicable
|
|
629
|
+
if ($context->path === '/graphql' && !empty($context->body)) {
|
|
630
|
+
$gqlInfo = RequestUtils::parseGraphQLQuery(is_array($context->body) ? $context->body : []);
|
|
631
|
+
if ($gqlInfo) {
|
|
632
|
+
$context->graphqlOperation = $gqlInfo;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
527
636
|
|
|
528
637
|
$this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
|
|
529
638
|
|
|
@@ -549,6 +658,7 @@
|
|
|
549
658
|
$isChallengeSubmission = $powNonce && (
|
|
550
659
|
isset($context->query['pow_solution']) ||
|
|
551
660
|
isset($context->query['pow_solution_cpu']) ||
|
|
661
|
+
isset($context->query['pow_solution_space']) ||
|
|
552
662
|
(isset($context->query['pow_type']) && $context->query['pow_type'] === 'useful_work_task')
|
|
553
663
|
);
|
|
554
664
|
if ($isChallengeSubmission) {
|
|
@@ -587,6 +697,28 @@
|
|
|
587
697
|
$isValid = $isValid && $isMemValid;
|
|
588
698
|
}
|
|
589
699
|
}
|
|
700
|
+
} elseif ($powType === 'pospace') {
|
|
701
|
+
$powSolutionSpace = $context->query['pow_solution_space'] ?? null;
|
|
702
|
+
if ($powSolutionSpace && isset($challengeContext['queries'])) {
|
|
703
|
+
$isSpaceValid = ChallengeUtils::verifySpacePoW(
|
|
704
|
+
$powNonce,
|
|
705
|
+
$powSolutionSpace,
|
|
706
|
+
$challengeContext['queries'],
|
|
707
|
+
$powNonce . ":" . $challengeContext['clientSecret'],
|
|
708
|
+
$challengeContext['clientSecret']
|
|
709
|
+
);
|
|
710
|
+
$isValid = $isSpaceValid;
|
|
711
|
+
if ($isValid) {
|
|
712
|
+
$ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000;
|
|
713
|
+
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
714
|
+
$ticket = ChallengeUtils::generateStatelessTicket([
|
|
715
|
+
'expiry' => $expiry,
|
|
716
|
+
'originalIp' => $context->clientIp,
|
|
717
|
+
'deviceId' => '',
|
|
718
|
+
'deviceHash' => ''
|
|
719
|
+
]);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
590
722
|
} elseif ($powType === 'useful_work_task') {
|
|
591
723
|
$problemId = $context->query['pow_problem_id'] ?? null;
|
|
592
724
|
$workResultJson = $context->query['pow_solution_work_result'] ?? null;
|
|
@@ -622,10 +754,6 @@
|
|
|
622
754
|
$this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
|
|
623
755
|
|
|
624
756
|
return [
|
|
625
|
-
// ... (le reste de la logique de redirection)
|
|
626
|
-
'action' => 'redirect',
|
|
627
|
-
'path' => RequestUtils::cleanUrlFromPowParams($challengeContext['originalPath'] ?? '/', $context->query),
|
|
628
|
-
'score' => 0.0,
|
|
629
757
|
'action' => 'redirect',
|
|
630
758
|
'path' => RequestUtils::cleanUrlFromPowParams($challengeContext['originalPath'] ?? '/', $context->query),
|
|
631
759
|
'score' => 0.0,
|
|
@@ -643,7 +771,11 @@
|
|
|
643
771
|
// 3. Vérifier un ticket existant
|
|
644
772
|
$hasValidTicket = false;
|
|
645
773
|
$powCookie = $context->cookies['pow_clearance'] ?? null;
|
|
646
|
-
|
|
774
|
+
$zkpProof = $context->getHeader('x-zkp-proof') ?? $context->query['pow_zkp'] ?? '';
|
|
775
|
+
$deviceId = $context->cookies['device_id'] ?? '';
|
|
776
|
+
$currentDeviceHash = RequestUtils::getCompositeDeviceHash($context);
|
|
777
|
+
$allowRoaming = $this->securityConfig['allowCrossNetworkRoaming'] ?? false;
|
|
778
|
+
if (ChallengeUtils::isTicketValid($context->clientIp, $powCookie, $deviceId, $currentDeviceHash, $allowRoaming, $zkpProof)) {
|
|
647
779
|
$hasValidTicket = true;
|
|
648
780
|
MetricsManager::incrementCounter('tickets_valid_total');
|
|
649
781
|
// On ne retourne pas tout de suite pour permettre le re-challenge
|
|
@@ -791,7 +923,11 @@
|
|
|
791
923
|
|
|
792
924
|
$nonce = bin2hex(random_bytes(16));
|
|
793
925
|
$clientSecret = bin2hex(random_bytes(16));
|
|
794
|
-
|
|
926
|
+
$highThreshold = $thresholds['high'] ?? 75;
|
|
927
|
+
|
|
928
|
+
$suspicionFactor = ($finalScore - $lowThreshold) / (($thresholds['high'] ?? 75) - $lowThreshold);
|
|
929
|
+
$suspicionFactor = max(0, min(1.5, $suspicionFactor));
|
|
930
|
+
|
|
795
931
|
// --- NOUVELLE LOGIQUE uPoW ---
|
|
796
932
|
$shouldUseUsefulWork = ($this->securityConfig['enableUsefulWork'] ?? false) && (
|
|
797
933
|
($this->securityConfig['forceUsefulWork'] ?? false) || (random_int(0, 255) / 255) > 0.5
|
|
@@ -843,9 +979,39 @@
|
|
|
843
979
|
|
|
844
980
|
// --- FIN DE LA LOGIQUE uPoW (le reste est le fallback) ---
|
|
845
981
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
982
|
+
if ($this->securityConfig['enableProofOfSpace'] ?? false) {
|
|
983
|
+
$spaceChallenge = ChallengeUtils::generateSpaceChallenge($context->clientIp, $nonce, $suspicionFactor, $context->path, $this->securityConfig);
|
|
984
|
+
$store->set("secret:{$nonce}", [
|
|
985
|
+
'clientSecret' => $clientSecret,
|
|
986
|
+
'suspicionScore' => $finalScore,
|
|
987
|
+
'queries' => $spaceChallenge['queries'],
|
|
988
|
+
'sizeMb' => $spaceChallenge['sizeMb'],
|
|
989
|
+
'fingerprint' => RequestUtils::getCompositeDeviceHash($context),
|
|
990
|
+
'originalPath' => $context->path,
|
|
991
|
+
], $this->securityConfig['challengeTtl'] ?? 300);
|
|
992
|
+
|
|
993
|
+
if ($deviceData) {
|
|
994
|
+
$deviceData['lastChallengeNonce'] = $nonce;
|
|
995
|
+
$store->set("device:{$deviceId}", $deviceData); // @phpstan-ignore-line
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
if ($isApiRequest) {
|
|
999
|
+
$decision['body'] = [
|
|
1000
|
+
'challenge' => [
|
|
1001
|
+
'type' => 'pospace',
|
|
1002
|
+
'nonce' => $nonce,
|
|
1003
|
+
'clientSecret' => $clientSecret,
|
|
1004
|
+
'queries' => $spaceChallenge['queries'],
|
|
1005
|
+
'sizeMb' => $spaceChallenge['sizeMb'],
|
|
1006
|
+
]
|
|
1007
|
+
];
|
|
1008
|
+
} else {
|
|
1009
|
+
$page = ChallengeUtils::generateSpaceChallengePage($spaceChallenge, $clientSecret, $this->securityConfig);
|
|
1010
|
+
$decision['body'] = $page;
|
|
1011
|
+
}
|
|
1012
|
+
return $decision;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
849
1015
|
$cpuChallengeDetails = [
|
|
850
1016
|
'type' => 'cpu_target',
|
|
851
1017
|
'nonce' => $nonce,
|