@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.
- package/CHANGELOG.md +25 -0
- package/README.md +128 -119
- package/composer.json +2 -1
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +452 -109
- package/src/js/library.js +131 -54
- package/src/js/mongodb-store.js +13 -0
- package/src/js/problem-manager.js +6 -2
- package/src/js/redis-store.js +13 -0
- package/src/js/sql-store.js +80 -77
- package/src/js/tests/fingerprint.engine.test.js +2 -3
- package/src/js/tests/fingerprint.test.js +49 -12
- package/src/js/tests/metrics.test.js +157 -103
- package/src/php/Challenge/ChallengeUtils.php +8 -6
- package/src/php/Config/SecurityProfiles.php +8 -2
- package/src/php/FingerprintEngine.php +155 -41
- package/src/php/Optimization/OptimizationOperators.php +60 -36
- package/src/php/RequestContext.php +7 -0
- package/src/php/Store/IStore.php +6 -0
- package/src/php/Store/MongoDbStore.php +12 -0
- package/src/php/Store/RedisStore.php +12 -0
- package/src/php/Tests/ChallengeUtilsTest.php +6 -4
- package/src/php/Tests/FingerprintEngineTest.php +232 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Tests/MetricsTest.php +97 -45
- package/src/php/Tests/RequestUtilsTest.php +14 -14
- package/src/php/Tests/config/ed25519_key.json +4 -0
- package/src/php/Utils/Env.php +50 -0
- package/src/php/Utils/MetricsManager.php +209 -166
- package/src/php/Utils/RequestUtils.php +105 -22
|
@@ -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 = (
|
|
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
|
-
|
|
172
|
+
Env::set('ED25519_PRIVATE_KEY', $securityConfig['ed25519_private_key']);
|
|
172
173
|
}
|
|
173
174
|
if (isset($securityConfig['ed25519_public_key'])) {
|
|
174
|
-
|
|
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
|
-
$
|
|
458
|
-
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
$
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
$
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if ($
|
|
474
|
-
$this->
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
$
|
|
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 =
|
|
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
|
-
$
|
|
99
|
-
$
|
|
100
|
-
$
|
|
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
|
-
$
|
|
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
|
-
$
|
|
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 [$
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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;
|
package/src/php/Store/IStore.php
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
223
|
-
|
|
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
|
-
|
|
241
|
+
Env::clear('ED25519_PRIVATE_KEY');
|
|
242
|
+
Env::clear('ED25519_PUBLIC_KEY');
|
|
241
243
|
}
|
|
242
244
|
|
|
243
245
|
public function testCooperativePoSpaceWorkflow(): void
|