@anonympins/fingerprint 0.6.1 → 0.6.4

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.
@@ -11,6 +11,7 @@
11
11
  use Anonympins\Fingerprint\Utils\Logger;
12
12
  use Anonympins\Fingerprint\Utils\MetricsManager;
13
13
  use Anonympins\Fingerprint\Utils\RequestUtils;
14
+ use Anonympins\Fingerprint\Utils\Env;
14
15
 
15
16
  // Correction de l'import
16
17
 
@@ -164,14 +165,56 @@
164
165
 
165
166
  public function __construct(array $securityConfig)
166
167
  {
167
- $this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
168
+ $this->isProduction = Env::get('APP_ENV') === 'production';
168
169
 
169
170
  // Dynamically bind Ed25519 keys if passed via config
170
171
  if (isset($securityConfig['ed25519_private_key'])) {
171
- $_ENV['ED25519_PRIVATE_KEY'] = $securityConfig['ed25519_private_key'];
172
+ Env::set('ED25519_PRIVATE_KEY', $securityConfig['ed25519_private_key']);
172
173
  }
173
174
  if (isset($securityConfig['ed25519_public_key'])) {
174
- $_ENV['ED25519_PUBLIC_KEY'] = $securityConfig['ed25519_public_key'];
175
+ Env::set('ED25519_PUBLIC_KEY', $securityConfig['ed25519_public_key']);
176
+ }
177
+
178
+ // Génération et persistance automatique de la paire de clés Ed25519
179
+ $useAsymmetric = ($securityConfig['useAsymmetricTickets'] ?? false) === true || ($securityConfig['ed25519'] ?? '') === 'auto';
180
+ $envPrivate = Env::get('ED25519_PRIVATE_KEY');
181
+ if ($useAsymmetric && ($envPrivate === null || $envPrivate === '')) {
182
+ $configDir = dirname(__DIR__, 1) . '/config';
183
+ $persistentKeyPath = $configDir . '/ed25519_key.json';
184
+
185
+ if (file_exists($persistentKeyPath)) {
186
+ try {
187
+ $keys = json_decode(file_get_contents($persistentKeyPath), true);
188
+ if (isset($keys['privateKey'], $keys['publicKey'])) {
189
+ Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
190
+ Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
191
+ }
192
+ } catch (\Throwable $e) {
193
+ error_log('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
194
+ }
195
+ } else {
196
+ try {
197
+ if (defined('OPENSSL_KEYTYPE_ED25519')) {
198
+ $pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
199
+ if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
200
+ $details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
201
+ $publicKeyPem = $details['key']; // @phpstan-ignore-line
202
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
203
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
204
+ // Also update $_ENV and $_SERVER for consistency
205
+ if (!is_dir($configDir)) {
206
+ mkdir($configDir, 0777, true);
207
+ }
208
+ file_put_contents($persistentKeyPath, json_encode([
209
+ 'privateKey' => $privateKeyPem,
210
+ 'publicKey' => $publicKeyPem
211
+ ], JSON_PRETTY_PRINT));
212
+ }
213
+ }
214
+ } catch (\Throwable $e) {
215
+ error_log('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
216
+ }
217
+ }
175
218
  }
176
219
 
177
220
  // Auto-load optimized config if autotuning savePath is specified
@@ -194,6 +237,9 @@
194
237
  $this->logger = isset($securityConfig['logger']) && is_callable($securityConfig['logger']) ? new Logger($securityConfig['logger']) : null;
195
238
  $this->dryRun = $securityConfig['dryRun'] ?? false;
196
239
  $this->validateConfig($securityConfig);
240
+ if ($this->securityConfig['reset'] ?? false) {
241
+ $this->resetStore();
242
+ }
197
243
  }
198
244
 
199
245
  /**
@@ -209,6 +255,18 @@
209
255
  $this->log('Configuration mise à jour à chaud (Hot-Reloaded)', $this->securityConfig);
210
256
  }
211
257
 
258
+ /**
259
+ * Réinitialise le store de persistance actif.
260
+ */
261
+ public function resetStore(): void
262
+ {
263
+ $store = StoreManager::getStore();
264
+ if (method_exists($store, 'clear')) {
265
+ $store->clear();
266
+ }
267
+ $this->log('Store has been reset/cleared.');
268
+ }
269
+
212
270
  private function validateConfig(array $config): void
213
271
  {
214
272
  if (empty($config)) {
@@ -222,7 +280,8 @@
222
280
  'honeypot', 'threatIntel', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger', 'probationaryTtl',
223
281
  'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
224
282
  'similarityThreshold', 'summary', 'description',
225
- 'ed25519_private_key', 'ed25519_public_key'
283
+ 'ed25519_private_key', 'ed25519_public_key',
284
+ 'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
226
285
  ];
227
286
 
228
287
  if (empty($config['weights'])) {
@@ -453,28 +512,50 @@
453
512
  */
454
513
  private function checkAllowlists(RequestContext $context): bool
455
514
  {
515
+ $whitelisted = false;
516
+ $type = '';
517
+
456
518
  if ($this->isIpInAllowlist($context->clientIp)) {
457
- $this->log('IP in allowlist - allowing request', ['clientIp' => $context->clientIp]);
458
- return true;
459
- }
460
- if ($this->isPathInAllowlist($context->path)) {
461
- $this->log('Path in allowlist - allowing request', ['path' => $context->path]);
462
- return true;
463
- }
464
- if ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
465
- $this->log('Host and path in allowlist - allowing request', ['host' => $context->getHeader('host'), 'path' => $context->path]);
466
- return true;
467
- }
468
- // NOUVEAU: Vérifier la liste blanche GraphQL
469
- if ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
470
- $this->log('GraphQL operation in allowlist - allowing request', ['operation' => "{$context->graphqlOperation['type']}:{$context->graphqlOperation['name']}"]);
471
- return true;
472
- }
473
- if ($this->verifyWhitelistedBot($context)) {
474
- $this->log('Whitelisted bot verified - allowing request', ['clientIp' => $context->clientIp]);
519
+ $whitelisted = true;
520
+ $type = 'allowlist';
521
+ } elseif ($this->isPathInAllowlist($context->path)) {
522
+ $whitelisted = true;
523
+ $type = 'path_allowlist';
524
+ } elseif ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
525
+ $whitelisted = true;
526
+ $type = 'host_path_allowlist';
527
+ } elseif ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
528
+ $whitelisted = true;
529
+ $type = 'graphql_operation_allowlist';
530
+ } elseif ($this->verifyWhitelistedBot($context)) {
531
+ $whitelisted = true;
532
+ $type = 'bot';
533
+ }
534
+
535
+ if ($whitelisted) {
536
+ $filterWhitelist = $this->securityConfig['filterWhitelist'] ?? false;
537
+ $bypassWhitelist = false;
538
+ if ($filterWhitelist === true) {
539
+ $bypassWhitelist = $this->hasCertainAttack($context);
540
+ } elseif (is_numeric($filterWhitelist)) {
541
+ if ($context->preCalculatedScore === null) {
542
+ $suspicionVector = [];
543
+ $context->preCalculatedVector = $this->getSuspicionVector($context, $suspicionVector);
544
+ $context->preCalculatedScore = $this->calculateFinalScore($context->preCalculatedVector);
545
+ }
546
+ if ($context->preCalculatedScore > $filterWhitelist) {
547
+ $bypassWhitelist = true;
548
+ }
549
+ }
550
+
551
+ if ($bypassWhitelist) {
552
+ $this->log('Whitelisted request exceeds filter threshold - bypassing whitelist bypass', ['clientIp' => $context->clientIp, 'path' => $context->path]);
553
+ return false;
554
+ }
555
+ $this->log("IP/Path in allowlist ({$type}) - allowing request", ['clientIp' => $context->clientIp, 'path' => $context->path]);
475
556
  return true;
476
557
  }
477
-
558
+
478
559
  return false;
479
560
  }
480
561
 
@@ -483,6 +564,9 @@
483
564
  */
484
565
  private function resolveRequestIdentity(RequestContext $context, array &$suspicionVector): array
485
566
  {
567
+ if ($context->resolvedIdentity !== null) {
568
+ return $context->resolvedIdentity;
569
+ }
486
570
  $this->log('Resolving request identity', ['clientIp' => $context->clientIp, 'cookies' => $context->cookies]);
487
571
  $store = StoreManager::getStore();
488
572
  $existingDeviceId = $context->cookies['device_id'] ?? null; // @phpstan-ignore-line
@@ -564,11 +648,12 @@
564
648
  $store->set("tls-session:{$tlsSessionId}", $deviceId, 3600); // 1h cache duration
565
649
  }
566
650
 
567
- return [
651
+ $context->resolvedIdentity = [
568
652
  'deviceId' => $deviceId,
569
653
  'deviceData' => $deviceData,
570
654
  'newCookie' => $newCookie,
571
655
  ];
656
+ return $context->resolvedIdentity;
572
657
  }
573
658
 
574
659
  /**
@@ -860,6 +945,23 @@
860
945
  }
861
946
  }
862
947
 
948
+ /**
949
+ * Évalue si la requête présente des caractéristiques d'attaque flagrantes ou a déclenché un honeypot.
950
+ */
951
+ private function hasCertainAttack(RequestContext $context): bool
952
+ {
953
+ $honeypotConfig = $this->securityConfig['honeypot'] ?? [];
954
+ $honeypot = RequestUtils::getHoneypotScore($context, $honeypotConfig);
955
+ if (($honeypot['honeypotScore'] ?? 0.0) >= 100.0) {
956
+ return true;
957
+ }
958
+ $bot = RequestUtils::getBotScore($context);
959
+ if (($bot['botScore'] ?? 0.0) >= 100.0) {
960
+ return true;
961
+ }
962
+ return false;
963
+ }
964
+
863
965
  /**
864
966
  * Traite une requête entrante et retourne une décision.
865
967
  * @param RequestContext $context Le contexte de la requête.
@@ -891,22 +993,11 @@
891
993
  $context->graphqlOperation = $gqlInfo;
892
994
  }
893
995
  }
894
-
895
-
896
- $this->log('Processing request', ['clientIp' => $context->clientIp, 'path' => $context->path]);
897
-
898
- // Parse GraphQL query if applicable
899
- if ($context->path === '/graphql' && !empty($context->body)) {
900
- $gqlInfo = RequestUtils::parseGraphQLQuery(is_array($context->body) ? $context->body : []);
901
- if ($gqlInfo) {
902
- $context->graphqlOperation = $gqlInfo;
903
- }
904
- }
905
996
 
906
997
  // 1. Vérifier les listes blanches
907
998
  if ($this->checkAllowlists($context)) {
999
+ MetricsManager::incrementCounter('requests_total', ['status' => 'whitelisted']);
908
1000
  return ['action' => 'next', 'score' => 0.0, 'vector' => ['whitelisted' => 100.0]];
909
- MetricsManager::incrementCounter('requests_total', ['status' => 'whitelisted']);
910
1001
  }
911
1002
 
912
1003
  // Initialiser le vecteur de suspicion
@@ -995,6 +1086,28 @@
995
1086
  $problemManager = \Anonympins\Fingerprint\ProblemManager::getInstance($configPath, $store);
996
1087
  // FIX: La solution est directement le $workResult, pas une sous-propriété.
997
1088
  $problemManager->integrateSolution($problemId, $workResult);
1089
+
1090
+ // Si le problème résolu est l'auto-tuning de sécurité et que l'auto-tuning est activé,
1091
+ // on applique directement la meilleure solution calculée au moteur en direct.
1092
+ if ($problemId === 'security_auto_tuning' && ($this->securityConfig['autotuning']['enabled'] ?? false)) {
1093
+ $paretoFront = $workResult['paretoFront'] ?? null;
1094
+ if (is_array($paretoFront) && !empty($paretoFront)) {
1095
+ $bestSolution = $paretoFront[0];
1096
+ $minDistance = sqrt(pow((float)$bestSolution['objectives'][0], 2) + pow((float)$bestSolution['objectives'][1], 2));
1097
+ for ($i = 1; $i < count($paretoFront); $i++) {
1098
+ $distance = sqrt(pow((float)$paretoFront[$i]['objectives'][0], 2) + pow((float)$paretoFront[$i]['objectives'][1], 2));
1099
+ if ($distance < $minDistance) {
1100
+ $minDistance = $distance;
1101
+ $bestSolution = $paretoFront[$i];
1102
+ }
1103
+ }
1104
+ if (isset($bestSolution['solution'])) {
1105
+ $this->updateConfig($bestSolution['solution']);
1106
+ $this->log('Useful Work auto-tuning applied successfully to live config.');
1107
+ }
1108
+ }
1109
+ }
1110
+
998
1111
  $isValid = true;
999
1112
  // FIX: Générer un vrai ticket pour uPoW, comme pour un PoW normal.
1000
1113
  $ticketTtl = $this->securityConfig['ticketMaxAge'] ?? 3600000; // 1 heure par défaut
@@ -1076,8 +1189,8 @@
1076
1189
  return $decision;
1077
1190
  }
1078
1191
 
1079
- $suspicionVector = $this->getSuspicionVector($context, $suspicionVector);
1080
- $finalScore = $this->calculateFinalScore($suspicionVector);
1192
+ $suspicionVector = $context->preCalculatedVector ?? $this->getSuspicionVector($context, $suspicionVector);
1193
+ $finalScore = $context->preCalculatedScore ?? $this->calculateFinalScore($suspicionVector);
1081
1194
  $this->log('Suspicion vector and final score calculated', [
1082
1195
  'finalScore' => round($finalScore, 2),
1083
1196
  'vector' => $suspicionVector
@@ -1234,7 +1347,7 @@
1234
1347
 
1235
1348
  if ($shouldUseUsefulWork) {
1236
1349
  $this->log('Issuing a useful work challenge', ['finalScore' => $finalScore]);
1237
- $defaultPath = dirname(__DIR__, 2) . '/config/problems.config.json';
1350
+ $defaultPath = dirname(__DIR__, 1) . '/config/problems.config.json';
1238
1351
  $configPath = $this->securityConfig['usefulWorkConfigPath'] ?? (file_exists($defaultPath) ? $defaultPath : null);
1239
1352
  $problemManager = ProblemManager::getInstance($configPath, $store);
1240
1353
  $work = $problemManager->dispatchWork($finalScore);
@@ -1317,7 +1430,8 @@
1317
1430
  $memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
1318
1431
 
1319
1432
  $originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
1320
- $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
1433
+ $tlsSessionId = $context->tlsSessionId ?? '';
1434
+ $baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
1321
1435
 
1322
1436
  $challengeContext = [
1323
1437
  'clientSecret' => $clientSecret,
@@ -1437,7 +1551,7 @@
1437
1551
  $signature = '';
1438
1552
  $isAsymmetric = false;
1439
1553
 
1440
- $privateKey = $_ENV['ED25519_PRIVATE_KEY'] ?? getenv('ED25519_PRIVATE_KEY');
1554
+ $privateKey = Env::get('ED25519_PRIVATE_KEY');
1441
1555
  if ($privateKey) {
1442
1556
  try {
1443
1557
  $cleanKey = str_replace('\n', "\n", $privateKey);
@@ -70,18 +70,22 @@ class OptimizationOperators
70
70
  */
71
71
  public static function createFullSecurityConfigEvaluator(array $context): callable
72
72
  {
73
- $trafficData = $context['trafficData'];
73
+ $trafficData = $context['trafficData'] ?? [];
74
+ $currentConfig = $context['currentConfig'] ?? null;
74
75
 
75
- return function (array $config) use ($trafficData): array {
76
- $falsePositives = 0;
77
- $falseNegatives = 0;
78
- $totalHumans = 0;
79
- $totalBots = 0;
76
+ return function (array $config) use ($trafficData, $currentConfig): array {
77
+ $falsePositives = 0.0;
78
+ $falseNegatives = 0.0;
79
+ $totalHumans = 0.0;
80
+ $totalBots = 0.0;
81
+
82
+ $maxHumanScore = 0.0;
83
+ $minBotScore = 100.0;
80
84
 
81
85
  $calculateScore = function (array $log) use ($config): float {
82
86
  $score = 0.0;
83
87
  foreach ($config['weights'] as $key => $weight) {
84
- $score += ($log['vector'][$key] ?? 0) * $weight;
88
+ $score += ($log['vector'][$key] ?? 0.0) * $weight;
85
89
  }
86
90
  return $score;
87
91
  };
@@ -94,30 +98,50 @@ class OptimizationOperators
94
98
  'trap_triggered' => 2.0,
95
99
  ];
96
100
 
101
+ // 1. Évaluation sur les logs de trafic réels
97
102
  foreach ($trafficData as $log) {
98
- $confidence = $confidenceWeights[$log['type']] ?? 1.0;
99
- $isLikelyBot = in_array($log['type'], ['challenge_issued', 'request_blocked', 'trap_triggered']);
100
- $isLikelyHuman = in_array($log['type'], ['request_passed', 'challenge_solved']);
103
+ $weight = (float)($log['weight'] ?? 1.0);
104
+ $confidence = ($confidenceWeights[$log['type'] ?? ''] ?? 1.0) * $weight;
105
+ $isLikelyBot = in_array($log['type'] ?? '', ['challenge_issued', 'request_blocked', 'trap_triggered']);
106
+ $isLikelyHuman = in_array($log['type'] ?? '', ['request_passed', 'challenge_solved']);
107
+
108
+ $score = $calculateScore($log);
101
109
 
102
110
  if ($isLikelyBot) {
103
111
  $totalBots += $confidence;
104
- $score = $calculateScore($log);
112
+ $minBotScore = min($minBotScore, $score);
105
113
  if ($score < $config['thresholds']['low']) {
106
114
  $falseNegatives += $confidence;
107
115
  }
108
116
  } elseif ($isLikelyHuman) {
109
117
  $totalHumans += $confidence;
110
- $score = $calculateScore($log);
118
+ $maxHumanScore = max($maxHumanScore, $score);
111
119
  if ($score >= $config['thresholds']['low']) {
112
120
  $falsePositives += $confidence;
113
121
  }
114
122
  }
115
123
  }
116
124
 
117
- $falsePositiveRate = $totalHumans > 0 ? $falsePositives / $totalHumans : 0;
118
- $falseNegativeRate = $totalBots > 0 ? $falseNegatives / $totalBots : 0;
125
+ $falsePositiveRate = $totalHumans > 0.0 ? $falsePositives / $totalHumans : 0.0;
126
+ $falseNegativeRate = $totalBots > 0.0 ? $falseNegatives / $totalBots : 0.0;
127
+
128
+ // 3. Pénalité de dérive d'échelle (L2 Regularization par rapport au profil d'origine)
129
+ $regularizationPenalty = 0.0;
130
+ if ($currentConfig && isset($currentConfig['weights'])) {
131
+ foreach ($config['weights'] as $key => $val) {
132
+ $originalVal = (float)($currentConfig['weights'][$key] ?? 0.0);
133
+ $regularizationPenalty += pow((float)$val - $originalVal, 2);
134
+ }
135
+ }
136
+
137
+ // 4. Maximisation de la marge
138
+ $marginOverlap = max(0.0, $maxHumanScore - $minBotScore);
139
+ $marginPenalty = $marginOverlap / 100.0;
140
+
141
+ $obj1 = $falsePositiveRate + ($regularizationPenalty * 0.05);
142
+ $obj2 = $falseNegativeRate + $marginPenalty;
119
143
 
120
- return [$falsePositiveRate, $falseNegativeRate];
144
+ return [$obj1, $obj2];
121
145
  };
122
146
  }
123
147
 
@@ -129,6 +153,7 @@ class OptimizationOperators
129
153
  */
130
154
  public static function solveFullSecurityTuning(array $context, array $options = []): array
131
155
  {
156
+ $currentConfig = $context['currentConfig'] ?? null;
132
157
  $fitnessFunction = self::createFullSecurityConfigEvaluator($context);
133
158
 
134
159
  $crossover = function (array $c1, array $c2): array {
@@ -143,31 +168,30 @@ class OptimizationOperators
143
168
  return $child;
144
169
  };
145
170
  $createIndividual = function () use ($currentConfig): array {
146
- if ($currentConfig) {
147
- $ind = [
148
- 'thresholds' => [],
149
- 'weights' => [],
150
- 'patterns' => []
151
- ];
152
- foreach (['thresholds', 'weights', 'patterns'] as $section) {
153
- if (isset($currentConfig[$section]) && is_array($currentConfig[$section])) {
154
- foreach ($currentConfig[$section] as $k => $v) {
155
- if (is_numeric($v) && $k !== 'honeypotScore') {
156
- $randomVariation = 1.0 + (self::secureRandom() - 0.5) * 0.5; // Variation de +/- 25%
157
- $ind[$section][$k] = $v * $randomVariation;
158
- } else {
159
- $ind[$section][$k] = $v;
160
- }
171
+ $config = $currentConfig ?: \Anonympins\Fingerprint\Config\SecurityProfiles::createSecurityProfile('balanced');
172
+ $ind = [
173
+ 'thresholds' => [],
174
+ 'weights' => [],
175
+ 'patterns' => []
176
+ ];
177
+ foreach (['thresholds', 'weights', 'patterns'] as $section) {
178
+ if (isset($config[$section]) && is_array($config[$section])) {
179
+ foreach ($config[$section] as $k => $v) {
180
+ if (is_numeric($v) && $k !== 'honeypotScore') {
181
+ $randomVariation = 1.0 + (self::secureRandom() - 0.5) * 0.5; // Variation de +/- 25%
182
+ $ind[$section][$k] = $v * $randomVariation;
183
+ } else {
184
+ $ind[$section][$k] = $v;
161
185
  }
162
186
  }
163
187
  }
164
- if (isset($ind['thresholds']['low'], $ind['thresholds']['medium'], $ind['thresholds']['high'])) {
165
- $ind['thresholds']['low'] = max(10.0, min(35.0, (float)$ind['thresholds']['low']));
166
- $ind['thresholds']['medium'] = max($ind['thresholds']['low'] + 5.0, min(70.0, (float)$ind['thresholds']['medium']));
167
- $ind['thresholds']['high'] = max($ind['thresholds']['medium'] + 5.0, min(90.0, (float)$ind['thresholds']['high']));
168
- }
169
- return $ind;
170
188
  }
189
+ if (isset($ind['thresholds']['low'], $ind['thresholds']['medium'], $ind['thresholds']['high'])) {
190
+ $ind['thresholds']['low'] = max(10.0, min(35.0, (float)$ind['thresholds']['low']));
191
+ $ind['thresholds']['medium'] = max($ind['thresholds']['low'] + 5.0, min(70.0, (float)$ind['thresholds']['medium']));
192
+ $ind['thresholds']['high'] = max($ind['thresholds']['medium'] + 5.0, min(90.0, (float)$ind['thresholds']['high']));
193
+ }
194
+ return $ind;
171
195
  };
172
196
 
173
197
  $mutate = function (array $c, ?array $currentConfigRef = null) use ($currentConfig): array {
@@ -32,6 +32,13 @@ class RequestContext
32
32
  /** @var ?array<string, mixed> */
33
33
  public ?array $newCookieForResponse = null;
34
34
 
35
+ /** @var ?array<string, mixed> */
36
+ public ?array $resolvedIdentity = null;
37
+
38
+ public ?float $preCalculatedScore = null;
39
+ /** @var ?array<string, float> */
40
+ public ?array $preCalculatedVector = null;
41
+
35
42
  // Propriétés spécifiques qui peuvent être fournies par un proxy inverse
36
43
  public ?string $ja3 = null;
37
44
  public ?string $ja4 = null;
@@ -39,4 +39,10 @@ interface IStore
39
39
  * @return void
40
40
  */
41
41
  public function delete(string $key): void;
42
+
43
+ /**
44
+ * Supprime toutes les clés du stockage.
45
+ * @return void
46
+ */
47
+ public function clear(): void;
42
48
  }
@@ -182,4 +182,16 @@ class MongoDbStore implements IStore
182
182
  $this->lastReconnectAttempt = microtime(true);
183
183
  }
184
184
  }
185
+
186
+ public function clear(): void
187
+ {
188
+ $this->fallbackStore->clear();
189
+ $this->checkConnection();
190
+ if ($this->isDown) return;
191
+ try {
192
+ $this->collection->deleteMany([]);
193
+ } catch (\Throwable $e) {
194
+ $this->isDown = true;
195
+ }
196
+ }
185
197
  }
@@ -131,4 +131,16 @@ class RedisStore implements IStore
131
131
  $this->lastReconnectAttempt = microtime(true);
132
132
  }
133
133
  }
134
+
135
+ public function clear(): void
136
+ {
137
+ $this->fallbackStore->clear();
138
+ $this->checkConnection();
139
+ if ($this->isDown) return;
140
+ try {
141
+ $this->redis->flushdb();
142
+ } catch (\Throwable $e) {
143
+ $this->isDown = true;
144
+ }
145
+ }
134
146
  }
@@ -8,6 +8,7 @@ use Anonympins\Fingerprint\Challenge\ChallengeUtils;
8
8
  use Anonympins\Fingerprint\Store\InMemoryStore;
9
9
  use Anonympins\Fingerprint\Store\StoreManager;
10
10
  use Anonympins\Fingerprint\Utils\BigInt;
11
+ use Anonympins\Fingerprint\Utils\Env;
11
12
  use PHPUnit\Framework\TestCase;
12
13
 
13
14
  class ChallengeUtilsTest extends TestCase
@@ -94,7 +95,7 @@ class ChallengeUtilsTest extends TestCase
94
95
  // Définir une clé secrète pour les tests
95
96
  $store = new InMemoryStore();
96
97
  StoreManager::configureStore($store);
97
- $_ENV['POW_SECRET'] = 'test-secret-key-that-is-long-enough-for-hmac';
98
+ Env::set('POW_SECRET', 'test-secret-key-that-is-long-enough-for-hmac');
98
99
  }
99
100
 
100
101
  public function testZkpProofValidation(): void
@@ -219,8 +220,8 @@ class ChallengeUtilsTest extends TestCase
219
220
  $this->markTestSkipped('Ed25519 signing is not supported due to PHP 8.0.0 openssl_sign() null algorithm bug.');
220
221
  }
221
222
 
222
- $_ENV['ED25519_PRIVATE_KEY'] = $privateKeyPem;
223
- $_ENV['ED25519_PUBLIC_KEY'] = $publicKeyPem;
223
+ Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
224
+ Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
224
225
 
225
226
  $ip = '127.0.0.1';
226
227
  $expiry = (int)floor(microtime(true) * 1000) + 3600000;
@@ -237,7 +238,8 @@ class ChallengeUtilsTest extends TestCase
237
238
  $this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket, 'device-123', 'hash-abc'));
238
239
  $this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $ticket, 'device-123', 'hash-abc'));
239
240
 
240
- unset($_ENV['ED25519_PRIVATE_KEY'], $_ENV['ED25519_PUBLIC_KEY']);
241
+ Env::clear('ED25519_PRIVATE_KEY');
242
+ Env::clear('ED25519_PUBLIC_KEY');
241
243
  }
242
244
 
243
245
  public function testCooperativePoSpaceWorkflow(): void