@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.
@@ -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
- if (ChallengeUtils::isTicketValid($context->clientIp, $powCookie)) {
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
- $suspicionFactor = ($finalScore - $lowThreshold) / (($thresholds['high'] ?? 75) - $lowThreshold);
847
- $suspicionFactor = max(0, min(1.5, $suspicionFactor));
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,