@anonympins/fingerprint 0.6.7 → 0.7.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +3 -7
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/public/fp.wasm +0 -0
  7. package/src/js/build-client.js +9 -11
  8. package/src/js/dynamic-wasm.js +7 -7
  9. package/src/js/fingerprint.builder.js +37 -38
  10. package/src/js/fingerprint.client.js +236 -152
  11. package/src/js/fingerprint.client.obfuscated.js +1 -1
  12. package/src/js/fingerprint.js +517 -101
  13. package/src/js/fingerprint.utils.js +213 -213
  14. package/src/js/library.js +355 -384
  15. package/src/js/mongodb-store.js +1 -1
  16. package/src/js/optimization.worker.js +27 -27
  17. package/src/js/pow.solver.inline.js +206 -62
  18. package/src/js/pow.solver.js +235 -101
  19. package/src/js/pow.worker.js +48 -26
  20. package/src/js/redis-store.js +1 -1
  21. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  22. package/src/js/tests/fingerprint.test.js +101 -40
  23. package/src/js/tests/ip-reputation.test.js +1 -0
  24. package/src/js/upow-model-task.js +14 -15
  25. package/src/php/AutoTuner.php +34 -34
  26. package/src/php/Challenge/ChallengeUtils.php +204 -32
  27. package/src/php/Config/SecurityProfiles.php +21 -0
  28. package/src/php/DirectFingerprint.php +190 -145
  29. package/src/php/FingerprintBuilder.php +29 -30
  30. package/src/php/FingerprintClient.php +24 -25
  31. package/src/php/FingerprintEngine.php +1836 -1880
  32. package/src/php/Ja3AnomalyDetector.php +258 -227
  33. package/src/php/Optimization/FunctionRegistry.php +7 -7
  34. package/src/php/Optimization/Optimization.php +24 -24
  35. package/src/php/Optimization/OptimizationOperators.php +28 -27
  36. package/src/php/Optimization/ProblemInitializers.php +52 -52
  37. package/src/php/ProblemManager.php +31 -34
  38. package/src/php/RequestContext.php +8 -1
  39. package/src/php/Store/IStore.php +7 -8
  40. package/src/php/Store/InMemoryStore.php +66 -66
  41. package/src/php/Store/MongoDbStore.php +5 -5
  42. package/src/php/Store/RedisStore.php +5 -5
  43. package/src/php/Store/StoreManager.php +35 -35
  44. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  45. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  46. package/src/php/Tests/FingerprintEngineTest.php +662 -693
  47. package/src/php/Tests/IpReputationTest.php +10 -0
  48. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  49. package/src/php/Tests/MetricsTest.php +97 -97
  50. package/src/php/Tests/ProblemManagerTest.php +376 -376
  51. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  52. package/src/php/Tests/RequestUtilsTest.php +471 -471
  53. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  54. package/src/php/Tests/config/ed25519_key.json +3 -3
  55. package/src/php/Utils/Env.php +49 -49
  56. package/src/php/Utils/MaliciousPatterns.php +74 -74
  57. package/src/php/Utils/MetricsManager.php +209 -209
  58. package/src/php/Utils/RequestUtils.php +236 -19
  59. package/src/php/Utils/TLSClientHelloParser.php +369 -212
  60. package/src/php/WordPress/WpDbStore.php +155 -0
  61. package/src/php/WordPress/fingerprint-wordpress.php +217 -0
  62. package/src/php/WordPress/package.php +123 -0
  63. package/src/php/bin/auto-tune.php +118 -118
@@ -1,8 +1,8 @@
1
1
  import crypto from "node:crypto";
2
2
 
3
3
  /**
4
- * Classe de base abstraite pour définir des tâches uPoW basées sur l'entraînement
5
- * ou l'évaluation de modèles IA (PyTorch exportés au format ONNX).
4
+ * Abstract base class defining useful Proof-of-Work (uPoW) tasks based on ML training
5
+ * or inference validation (e.g. PyTorch models exported to ONNX format).
6
6
  */
7
7
  export class UpowModelTask {
8
8
  constructor(problemId, modelPath, config = {}) {
@@ -20,7 +20,7 @@ export class UpowModelTask {
20
20
  }
21
21
 
22
22
  /**
23
- * Récupère l'identifiant du problème.
23
+ * Returns the unique problem identifier.
24
24
  * @returns {string}
25
25
  */
26
26
  getProblemId() {
@@ -28,7 +28,7 @@ export class UpowModelTask {
28
28
  }
29
29
 
30
30
  /**
31
- * Récupère le chemin réseau du modèle ONNX.
31
+ * Returns the network asset path of the model.
32
32
  * @returns {string}
33
33
  */
34
34
  getModelAssetPath() {
@@ -36,18 +36,18 @@ export class UpowModelTask {
36
36
  }
37
37
 
38
38
  /**
39
- * Génère la charge utile de travail pour le client.
40
- * Envoie l'état actuel des poids du modèle et un lot de données (mini-batch) à traiter.
39
+ * Dispatches task workload to the client.
40
+ * Transmits current model weights and mini-batch sample data.
41
41
  * @param {number} suspicionFactor
42
- * @returns {object} Données à envoyer au client
42
+ * @returns {object} Task payload dispatched to client
43
43
  */
44
44
  dispatchTask(suspicionFactor) {
45
45
  throw new Error("Method 'dispatchTask(suspicionFactor)' must be implemented.");
46
46
  }
47
47
 
48
48
  /**
49
- * Vérifie la validité de la solution retournée par le client.
50
- * Implémente des gardes-fous contre l'empoisonnement (ex: vérification des gradients).
49
+ * Validates solution returned by the client.
50
+ * Implements anti-poisoning guards and gradient bounds checks.
51
51
  * @param {object} taskContext
52
52
  * @param {object} solution
53
53
  * @returns {boolean}
@@ -57,8 +57,7 @@ export class UpowModelTask {
57
57
  }
58
58
 
59
59
  /**
60
- * Intègre les gradients ou résultats validés dans le modèle global.
61
- * Par exemple : algorithme FedAvg (Federated Averaging).
60
+ * Integrates validated gradients into global model weights (e.g. FedAvg).
62
61
  * @param {object} solution
63
62
  */
64
63
  integrateSolution(solution) {
@@ -67,17 +66,17 @@ export class UpowModelTask {
67
66
  }
68
67
 
69
68
  /**
70
- * Exemple d'implémentation d'une tâche d'entraînement (Classifieur de requêtes suspectes)
69
+ * Example training task implementation (suspicious request classifier).
71
70
  */
72
71
  export class RequestClassifierTask extends UpowModelTask {
73
72
  constructor(modelPath, config) {
74
73
  super("request_classifier_nn", modelPath, config);
75
- // Poids initiaux du modèle (Réseau de neurones linéaire simple)
74
+ // Initial model weights (linear neural network)
76
75
  this.weights = [0.1, -0.2, 0.8, 0.5];
77
76
  }
78
77
 
79
78
  dispatchTask(suspicionFactor) {
80
- // Génération de données d'entraînement anonymisées pour le client
79
+ // Anonymized sample training batch dispatched to client
81
80
  const inputs = [
82
81
  [1.0, 0.5, 0.0, 1.2],
83
82
  [0.0, 1.0, -0.5, 0.8]
@@ -95,7 +94,7 @@ export class RequestClassifierTask extends UpowModelTask {
95
94
  if (!Array.isArray(gradients) || gradients.length !== this.weights.length) {
96
95
  return false;
97
96
  }
98
- // Anti-poisoning : s'assurer que les gradients ne contiennent pas de valeurs aberrantes (clipping)
97
+ // Anti-poisoning guard: ensure gradients do not exceed threshold norms (gradient clipping)
99
98
  const maxGradientNorm = 10.0;
100
99
  for (const grad of gradients) {
101
100
  if (typeof grad !== 'number' || isNaN(grad) || Math.abs(grad) > maxGradientNorm) {
@@ -8,18 +8,18 @@ use Anonympins\Fingerprint\Optimization\OptimizationOperators;
8
8
  use Anonympins\Fingerprint\Utils\RequestUtils;
9
9
 
10
10
  /**
11
- * Gère le processus d'auto-ajustement en arrière-plan pour les seuils et poids de sécurité.
12
- * Conçu pour être exécuté périodiquement (par exemple, via une tâche cron).
11
+ * Manages the background auto-tuning process for security thresholds and weights.
12
+ * Designed to be executed periodically (e.g. via a cron job).
13
13
  */
14
14
  class AutoTuner
15
15
  {
16
16
  /**
17
- * @var array<string, mixed> La configuration de sécurité en direct qui sera mutée.
17
+ * @var array<string, mixed> Live security configuration that will be mutated.
18
18
  */
19
19
  private array $securityConfig;
20
20
 
21
21
  /**
22
- * @var array<int, array<string, mixed>> Les données de trafic collectées.
22
+ * @var array<int, array<string, mixed>> Collected traffic data log.
23
23
  */
24
24
  private array $trafficData;
25
25
 
@@ -33,14 +33,14 @@ class AutoTuner
33
33
  private float $validationTolerance;
34
34
 
35
35
  /**
36
- * @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
36
+ * @var ?array<string, mixed> Last best solution found by the optimizer.
37
37
  */
38
38
  private static ?array $lastBestSolution = null;
39
39
 
40
40
  /**
41
- * @param array<string, mixed> &$securityConfig La configuration de sécurité (passée par référence).
42
- * @param array<int, array<string, mixed>> &$trafficData Les données de trafic (passées par référence).
43
- * @param array<string, int> $options Options pour l'auto-ajustement.
41
+ * @param array<string, mixed> &$securityConfig Security configuration passed by reference.
42
+ * @param array<int, array<string, mixed>> &$trafficData Traffic logs passed by reference.
43
+ * @param array<string, int> $options Auto-tuning options.
44
44
  */
45
45
  public function __construct(array &$securityConfig, array &$trafficData, array $options = [])
46
46
  {
@@ -104,7 +104,7 @@ class AutoTuner
104
104
  $now = (int)(microtime(true) * 1000);
105
105
  $removed = [];
106
106
 
107
- // 1. Expire par temps (maxAgeMs)
107
+ // 1. Time-based expiration (maxAgeMs)
108
108
  if ($this->maxAgeMs !== null && $this->maxAgeMs > 0) {
109
109
  $threshold = $now - $this->maxAgeMs;
110
110
  foreach ($this->trafficData as $key => $log) {
@@ -117,14 +117,14 @@ class AutoTuner
117
117
  $this->trafficData = array_values($this->trafficData);
118
118
  }
119
119
 
120
- // 2. Politique de taille maximale (maxDataPoints)
120
+ // 2. Maximum dataset size enforcement (maxDataPoints)
121
121
  if ($this->maxDataPoints > 0 && count($this->trafficData) > $this->maxDataPoints) {
122
122
  $overflowCount = count($this->trafficData) - $this->maxDataPoints;
123
123
  $spliced = array_splice($this->trafficData, 0, $overflowCount);
124
124
  $removed = array_merge($removed, $spliced);
125
125
  }
126
126
 
127
- // 3. Invocation du callback onCleanup
127
+ // 3. Execute cleanup callback
128
128
  if ($this->onCleanup !== null && is_callable($this->onCleanup) && !empty($removed)) {
129
129
  try {
130
130
  call_user_func($this->onCleanup, $removed);
@@ -135,7 +135,7 @@ class AutoTuner
135
135
  }
136
136
 
137
137
  /**
138
- * Exécute un cycle d'optimisation des seuils.
138
+ * Executes a threshold optimization cycle.
139
139
  */
140
140
  public function runOptimizationCycle(): void
141
141
  {
@@ -148,35 +148,35 @@ class AutoTuner
148
148
  fn ($log) => in_array($log['type'] ?? '', ['challenge_solved', 'trap_triggered'])
149
149
  ));
150
150
  $highConfidenceRatio = count($sanitizedData) > 0 ? $highConfidenceLogs / count($sanitizedData) : 0;
151
- $minConfidenceRatio = 0.05; // Exiger au moins 5% de signaux forts.
152
- $minHighConfidenceCount = 10; // Absolu de secours pour éviter le gel lors de floods
151
+ $minConfidenceRatio = 0.05; // Require at least 5% high-confidence signals
152
+ $minHighConfidenceCount = 10; // Fallback absolute count to avoid starvation during floods
153
153
 
154
154
  $hasEnoughSignal = $highConfidenceRatio >= $minConfidenceRatio || $highConfidenceLogs >= $minHighConfidenceCount;
155
155
 
156
156
  if (count($sanitizedData) < $this->minDataPoints || !$hasEnoughSignal) {
157
157
  if (count($sanitizedData) < $this->minDataPoints) {
158
- echo sprintf("[AutoTuning] Reporté : %d/%d points de données.\n", count($sanitizedData), $this->minDataPoints);
158
+ echo sprintf("[AutoTuning] Postponed: %d/%d data points collected.\n", count($sanitizedData), $this->minDataPoints);
159
159
  } else {
160
- echo sprintf("[AutoTuning] Reporté : Signaux de confiance insuffisants (Ratio: %.2f%% < %.2f%% et absolu: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount);
160
+ echo sprintf("[AutoTuning] Postponed: Insufficient confidence signals (Ratio: %.2f%% < %.2f%% and count: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount);
161
161
  }
162
162
  return;
163
163
  }
164
164
 
165
165
  if (count($this->trafficData) > $this->maxDataPoints) {
166
- echo sprintf("[AutoTuning] Le journal de trafic a atteint %d entrées (max: %d). Troncation des données les plus anciennes.\n", count($this->trafficData), $this->maxDataPoints);
166
+ echo sprintf("[AutoTuning] Traffic log reached %d entries (max: %d). Truncating oldest logs.\n", count($this->trafficData), $this->maxDataPoints);
167
167
  $this->trafficData = array_slice($this->trafficData, count($this->trafficData) - $this->maxDataPoints);
168
168
  }
169
169
 
170
- echo sprintf("[AutoTuning] Démarrage du cycle d'optimisation complet avec %d points de données assainis.\n", count($sanitizedData));
170
+ echo sprintf("[AutoTuning] Starting complete optimization cycle with %d sanitized data points.\n", count($sanitizedData));
171
171
 
172
172
  $paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
173
173
 
174
174
  if (empty($paretoFront)) {
175
- echo "[AutoTuning] L'optimisation n'a retourné aucune solution.\n";
175
+ echo "[AutoTuning] Optimization returned no solutions.\n";
176
176
  return;
177
177
  }
178
178
 
179
- // Règles de gardiennage (Sanity Guardrails) pour filtrer le front de Pareto
179
+ // Sanity guardrails to filter candidate Pareto solutions
180
180
  $isValidSecurityConfig = function (array $config): bool {
181
181
  if (!isset($config['weights']) || !isset($config['thresholds'])) return false;
182
182
  $w = $config['weights'];
@@ -192,13 +192,13 @@ class AutoTuner
192
192
 
193
193
  $filteredFront = array_filter($paretoFront, fn ($p) => $isValidSecurityConfig($p['solution']));
194
194
  if (empty($filteredFront)) {
195
- echo "[AutoTuning] Attention : Toutes les solutions ont été rejetées par les règles de gardiennage. Rétablissement du front brut.\n";
195
+ echo "[AutoTuning] Warning: All Pareto solutions violated sanity guardrails. Restoring raw front.\n";
196
196
  $filteredFront = $paretoFront;
197
197
  } else {
198
198
  $filteredFront = array_values($filteredFront);
199
199
  }
200
200
 
201
- // Stratégie de sélection : choisir la solution la plus équilibrée (la plus proche de l'origine).
201
+ // Selection strategy: pick the most balanced solution (closest to origin in objective space)
202
202
  $bestSolution = $filteredFront[0];
203
203
  $minDistance = sqrt(pow($bestSolution['objectives'][0], 2) + pow($bestSolution['objectives'][1], 2));
204
204
 
@@ -210,7 +210,7 @@ class AutoTuner
210
210
  }
211
211
  }
212
212
 
213
- // Logique d'inertie pour l'application de la configuration.
213
+ // Inertial update logic to prevent drastic parameter jumps
214
214
  $newConfig = $bestSolution['solution'];
215
215
  $trafficConfidence = min(1.5, max(0.3, $highConfidenceRatio * 4));
216
216
 
@@ -254,7 +254,7 @@ class AutoTuner
254
254
  }
255
255
  };
256
256
 
257
- // --- VALIDATION POST-CALCUL (PHP Rollback & Seuil de Tolérance) ---
257
+ // --- POST-COMPUTATION VALIDATION (Rollback guard & tolerance threshold) ---
258
258
  $tempConfig = [
259
259
  'thresholds' => $this->securityConfig['thresholds'],
260
260
  'weights' => $this->securityConfig['weights'],
@@ -300,22 +300,22 @@ class AutoTuner
300
300
 
301
301
  self::$lastBestSolution = $bestSolution;
302
302
 
303
- echo "[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.\n";
304
- echo "[AutoTuning] Objectifs atteints : " . json_encode([
303
+ echo "[AutoTuning] New optimized security configuration applied.\n";
304
+ echo "[AutoTuning] Objectives achieved: " . json_encode([
305
305
  'falsePositiveRate' => round($bestSolution['objectives'][0], 4),
306
306
  'falseNegativeRate' => round($bestSolution['objectives'][1], 4)
307
307
  ]) . "\n";
308
- echo "[AutoTuning] Nouveaux seuils : " . json_encode($this->securityConfig['thresholds']) . "\n";
309
- echo "[AutoTuning] Nouveaux poids : " . json_encode($this->securityConfig['weights']) . "\n";
310
- echo "[AutoTuning] Nouveaux patterns : " . json_encode($this->securityConfig['patterns']) . "\n";
308
+ echo "[AutoTuning] New thresholds: " . json_encode($this->securityConfig['thresholds']) . "\n";
309
+ echo "[AutoTuning] New weights: " . json_encode($this->securityConfig['weights']) . "\n";
310
+ echo "[AutoTuning] New patterns: " . json_encode($this->securityConfig['patterns']) . "\n";
311
311
 
312
- // NOUVEAU: Sauvegarder la meilleure configuration si un chemin est fourni.
312
+ // Persist best configuration if savePath is configured
313
313
  if ($this->savePath !== null) {
314
314
  try {
315
315
  file_put_contents($this->savePath, json_encode($bestSolution['solution'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
316
- echo "[AutoTuning] Meilleure configuration sauvegardée dans : {$this->savePath}\n";
316
+ echo "[AutoTuning] Best configuration saved to: {$this->savePath}\n";
317
317
  } catch (\Throwable $e) {
318
- error_log("[AutoTuning] Erreur lors de la sauvegarde de la configuration optimisée : " . $e->getMessage());
318
+ error_log("[AutoTuning] Error saving optimized configuration: " . $e->getMessage());
319
319
  }
320
320
  }
321
321
 
@@ -333,7 +333,7 @@ class AutoTuner
333
333
  }
334
334
 
335
335
  /**
336
- * Retourne la dernière meilleure solution trouvée par l'auto-tuner.
336
+ * Returns the latest best solution found by the auto-tuner.
337
337
  * @return array<string, mixed>|null
338
338
  */
339
339
  public static function getBestTuningSolution(): ?array
@@ -342,7 +342,7 @@ class AutoTuner
342
342
  }
343
343
 
344
344
  /**
345
- * Réinitialise la meilleure solution statique. Utile pour les tests.
345
+ * Resets the static best solution. Intended for testing.
346
346
  * @internal
347
347
  */
348
348
  public static function resetBestTuningSolution(): void
@@ -15,9 +15,13 @@ use Anonympins\Fingerprint\Utils\RequestUtils;
15
15
  */
16
16
  class ChallengeUtils
17
17
  {
18
+ /** @var array<string, float> Cache local des floats convertis pour éviter les appels système pack/unpack */
19
+ private static array $froundCache = [];
20
+
18
21
  private static function fround(float $value): float
19
22
  {
20
- return unpack('f', pack('f', $value))[1];
23
+ $key = (string)$value;
24
+ return self::$froundCache[$key] ?? (self::$froundCache[$key] = unpack('f', pack('f', $value))[1]);
21
25
  }
22
26
 
23
27
  public static function hashSeedToFloat(string $seed): float
@@ -261,6 +265,15 @@ class ChallengeUtils
261
265
  case 'register':
262
266
  $expectedMsg = "{$clientSecret}:register:{$nodeId}:" . ($params['seed'] ?? '');
263
267
  break;
268
+ case 'find_peer':
269
+ $expectedMsg = "{$clientSecret}:find_peer:{$nodeId}";
270
+ break;
271
+ case 'webrtc_signal':
272
+ $expectedMsg = "{$clientSecret}:webrtc_signal:{$nodeId}:" . ($params['target_peer_id'] ?? '') . ":" . ($params['signal_type'] ?? '') . ":" . ($params['signal_data'] ?? '');
273
+ break;
274
+ case 'poll_signals':
275
+ $expectedMsg = "{$clientSecret}:poll_signals:{$nodeId}";
276
+ break;
264
277
  case 'request_peer_block':
265
278
  $expectedMsg = "{$clientSecret}:request_peer_block:{$nodeId}:" . ($params['peer_id'] ?? '') . ":" . ($params['block_idx'] ?? '0') . ":" . ($params['req_id'] ?? '');
266
279
  break;
@@ -290,6 +303,38 @@ class ChallengeUtils
290
303
  self::registerCooperativeNode($clientIp, $nodeId, $seed);
291
304
  return ['status' => 'registered'];
292
305
 
306
+ case 'find_peer':
307
+ $peer = self::findPeerInSubnet($clientIp, $nodeId);
308
+ if ($peer !== null) {
309
+ return ['status' => 'peer_found', 'peer_id' => $peer['nodeId'], 'seed' => $peer['seed']];
310
+ }
311
+ return ['status' => 'no_peers'];
312
+
313
+ case 'webrtc_signal':
314
+ $targetPeerId = $params['target_peer_id'] ?? '';
315
+ $signalType = $params['signal_type'] ?? '';
316
+ $signalData = $params['signal_data'] ?? '';
317
+ if (empty($targetPeerId) || empty($signalType) || empty($signalData)) {
318
+ return ['error' => 'Invalid parameters'];
319
+ }
320
+ $signalQueueKey = "coop-webrtc:signals:{$targetPeerId}";
321
+ $signals = $store->get($signalQueueKey) ?? [];
322
+ $signals[] = [
323
+ 'from_peer_id' => $nodeId,
324
+ 'signal_type' => $signalType,
325
+ 'signal_data' => $signalData
326
+ ];
327
+ $store->set($signalQueueKey, $signals, 30);
328
+ return ['status' => 'signal_queued'];
329
+
330
+ case 'poll_signals':
331
+ $pollSignalKey = "coop-webrtc:signals:{$nodeId}";
332
+ $signals = $store->get($pollSignalKey) ?? [];
333
+ if (!empty($signals)) {
334
+ $store->delete($pollSignalKey);
335
+ }
336
+ return ['status' => 'ok', 'signals' => $signals];
337
+
293
338
  case 'request_peer_block':
294
339
  $peerId = $params['peer_id'] ?? '';
295
340
  $blockIdx = (int)($params['block_idx'] ?? 0);
@@ -650,8 +695,8 @@ class ChallengeUtils
650
695
  public static function calculateCpuTarget(float $suspicionFactor, array $securityConfig): string
651
696
  {
652
697
  $cpuConfig = $securityConfig['cpu'] ?? [];
653
- $minDifficultyBits = $cpuConfig['minDifficultyBits'] ?? 8;
654
- $maxDifficultyBits = $cpuConfig['maxDifficultyBits'] ?? 24;
698
+ $minDifficultyBits = (float)($cpuConfig['minDifficultyBits'] ?? 4);
699
+ $maxDifficultyBits = (float)($cpuConfig['maxDifficultyBits'] ?? 22);
655
700
 
656
701
  $totalDifficultyBits = $minDifficultyBits + $suspicionFactor * ($maxDifficultyBits - $minDifficultyBits);
657
702
 
@@ -690,6 +735,18 @@ class ChallengeUtils
690
735
  string $deviceId = '',
691
736
  string $deviceHash = ''
692
737
  ): ?string {
738
+ // En mode HTTP (non sécurisé), aucun calcul SHA-256 n'est exigé
739
+ if (!empty($challengeContext['isHttp'])) {
740
+ error_log('[FP Server Verify] Mode HTTP détecté (insecure policy) : validation sans SHA-256 acceptée.');
741
+ $expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
742
+ return self::generateStatelessTicket([
743
+ 'expiry' => $expiry,
744
+ 'originalIp' => $clientIp,
745
+ 'deviceId' => $deviceId,
746
+ 'deviceHash' => $deviceHash
747
+ ]);
748
+ }
749
+
693
750
  $cpuTargetHex = $challengeContext['cpuTarget'] ?? null;
694
751
  $baseBlock = $challengeContext['baseBlock'] ?? null;
695
752
 
@@ -826,6 +883,9 @@ class ChallengeUtils
826
883
  int $difficulty,
827
884
  string $clientSecret
828
885
  ): bool {
886
+ if ($difficulty === 0) {
887
+ return true;
888
+ }
829
889
  $maxAllowedMemDifficulty = 128; // 128MB
830
890
  if ($difficulty > $maxAllowedMemDifficulty) {
831
891
  error_log("[Security] Memory PoW verification attempt with excessive difficulty: {$difficulty}MB. Denied.");
@@ -951,13 +1011,27 @@ class ChallengeUtils
951
1011
  */
952
1012
  private static function getPowSolverCode(): string
953
1013
  {
954
- // Le chemin doit être relatif à ce fichier ou absolu.
955
- $solverPath = __DIR__ . '/../../js/pow.solver.inline.js';
956
- if (!file_exists($solverPath)) {
957
- error_log("[ChallengeUtils] Erreur: Le fichier pow.solver.inline.js n'a pas été trouvé à l'emplacement attendu.");
958
- return '';
1014
+ $candidates = [
1015
+ __DIR__ . '/../../js/pow.solver.inline.js',
1016
+ __DIR__ . '/../assets/pow.solver.inline.js',
1017
+ __DIR__ . '/../js/pow.solver.inline.js',
1018
+ dirname(__DIR__, 2) . '/assets/pow.solver.inline.js',
1019
+ dirname(__DIR__, 2) . '/js/pow.solver.inline.js',
1020
+ dirname(__DIR__, 1) . '/assets/pow.solver.inline.js',
1021
+ ];
1022
+ if (defined('ABSPATH')) {
1023
+ $wpPluginSolver = ABSPATH . 'wp-content/plugins/fingerprint-wordpress/assets/pow.solver.inline.js';
1024
+ if (file_exists($wpPluginSolver)) {
1025
+ return file_get_contents($wpPluginSolver) ?: '';
1026
+ }
959
1027
  }
960
- return file_get_contents($solverPath) ?: '';
1028
+ foreach ($candidates as $solverPath) {
1029
+ if (file_exists($solverPath)) {
1030
+ return file_get_contents($solverPath) ?: '';
1031
+ }
1032
+ }
1033
+ error_log("[ChallengeUtils] Erreur: Le fichier pow.solver.inline.js n'a pas été trouvé à l'emplacement attendu.");
1034
+ return '';
961
1035
  }
962
1036
 
963
1037
  public static function generateSpaceChallengePage(array $challengeDetails, string $clientSecret, array $securityConfig): string
@@ -998,6 +1072,11 @@ class ChallengeUtils
998
1072
  return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
999
1073
  }
1000
1074
 
1075
+ async function sendWebRtcSignal(targetId, type, data) {
1076
+ const sig = await signCoop("webrtc_signal", nodeId, targetId + ":" + type + ":" + data);
1077
+ await fetch(window.location.pathname + "?coop_op=webrtc_signal&node_id=" + nodeId + "&target_peer_id=" + targetId + "&signal_type=" + type + "&signal_data=" + encodeURIComponent(data) + "&coop_sig=" + sig);
1078
+ }
1079
+
1001
1080
  document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1002
1081
  await new Promise(r => setTimeout(r, 10));
1003
1082
 
@@ -1009,9 +1088,50 @@ class ChallengeUtils
1009
1088
  const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
1010
1089
  await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
1011
1090
  }
1091
+
1092
+ const peerConnections = {};
1012
1093
 
1013
- // Écoute des requêtes entrantes de nos pairs suspects
1094
+ // Écoute des signaux WebRTC et requêtes entrantes
1014
1095
  setInterval(async () => {
1096
+ try {
1097
+ const sigWebrtc = await signCoop("poll_signals", nodeId);
1098
+ const resWebrtc = await fetch(window.location.pathname + "?coop_op=poll_signals&node_id=" + nodeId + "&coop_sig=" + sigWebrtc);
1099
+ const dataWebrtc = await resWebrtc.json();
1100
+ if (dataWebrtc.signals && dataWebrtc.signals.length > 0) {
1101
+ for (const sig of dataWebrtc.signals) {
1102
+ const fromId = sig.from_peer_id;
1103
+ if (sig.signal_type === 'offer') {
1104
+ const pc = new RTCPeerConnection({ iceServers: [] });
1105
+ peerConnections[fromId] = pc;
1106
+ pc.onicecandidate = (e) => {
1107
+ if (e.candidate) sendWebRtcSignal(fromId, 'candidate', JSON.stringify(e.candidate));
1108
+ };
1109
+ pc.ondatachannel = (e) => {
1110
+ const dc = e.channel;
1111
+ dc.onmessage = async (evt) => {
1112
+ try {
1113
+ const req = JSON.parse(evt.data);
1114
+ if (req.type === 'get_block') {
1115
+ document.getElementById('loader').innerText = '📤 Transfert direct P2P (WebRTC) du bloc vers le pair...';
1116
+ const blockData = await window.readSpaceBlock(req.block_idx);
1117
+ dc.send(JSON.stringify({ type: 'block_data', block_data: blockData }));
1118
+ }
1119
+ } catch (err) {}
1120
+ };
1121
+ };
1122
+ await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
1123
+ const answer = await pc.createAnswer();
1124
+ await pc.setLocalDescription(answer);
1125
+ await sendWebRtcSignal(fromId, 'answer', JSON.stringify(answer));
1126
+ } else if (sig.signal_type === 'answer' && peerConnections[fromId]) {
1127
+ await peerConnections[fromId].setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
1128
+ } else if (sig.signal_type === 'candidate' && peerConnections[fromId]) {
1129
+ await peerConnections[fromId].addIceCandidate(new RTCIceCandidate(JSON.parse(sig.signal_data)));
1130
+ }
1131
+ }
1132
+ }
1133
+ } catch (e) {}
1134
+
1015
1135
  try {
1016
1136
  const sig = await signCoop("poll_requests", nodeId);
1017
1137
  const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
@@ -1027,30 +1147,65 @@ class ChallengeUtils
1027
1147
  } catch (e) {
1028
1148
  console.error("Cooperative polling error", e);
1029
1149
  }
1030
- }, 1000);
1150
+ }, 800);
1031
1151
 
1032
- // Téléchargement du bloc du pair si configuré
1033
1152
  let peerBlock = "";
1034
1153
  if (peerId && peerBlockIdx !== -1) {
1035
- document.getElementById('loader').innerText = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1036
- const reqId = Math.random().toString(36).substring(2);
1037
- const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1038
- await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
1039
-
1040
- let attempts = 0;
1041
- while (attempts < coopTimeout) {
1042
- const pollSig = await signCoop("poll_response", nodeId, reqId);
1043
- const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1044
- const data = await res.json();
1045
- if (data.status === 'ready') {
1046
- peerBlock = data.block_data;
1047
- break;
1154
+ document.getElementById('loader').innerText = '📥 Connexion WebRTC P2P directe au pair (' + peerId + ')...';
1155
+
1156
+ // 1. Échange direct P2P via WebRTC DataChannel (charge serveur = 0)
1157
+ const webrtcTransferPromise = new Promise(async (resolve) => {
1158
+ if (!window.RTCPeerConnection) return resolve(null);
1159
+ try {
1160
+ const pc = new RTCPeerConnection({ iceServers: [] });
1161
+ peerConnections[peerId] = pc;
1162
+ const dc = pc.createDataChannel("pospace-transfer");
1163
+ pc.onicecandidate = (e) => {
1164
+ if (e.candidate) sendWebRtcSignal(peerId, 'candidate', JSON.stringify(e.candidate));
1165
+ };
1166
+ dc.onopen = () => {
1167
+ dc.send(JSON.stringify({ type: 'get_block', block_idx: peerBlockIdx }));
1168
+ };
1169
+ dc.onmessage = (e) => {
1170
+ try {
1171
+ const msg = JSON.parse(e.data);
1172
+ if (msg.type === 'block_data' && msg.block_data) {
1173
+ resolve(msg.block_data);
1174
+ }
1175
+ } catch (err) {}
1176
+ };
1177
+ const offer = await pc.createOffer();
1178
+ await pc.setLocalDescription(offer);
1179
+ await sendWebRtcSignal(peerId, 'offer', JSON.stringify(offer));
1180
+ } catch (err) {
1181
+ resolve(null);
1182
+ }
1183
+ });
1184
+
1185
+ const webrtcTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 5000));
1186
+ peerBlock = await Promise.race([webrtcTransferPromise, webrtcTimeoutPromise]);
1187
+
1188
+ if (peerBlock) {
1189
+ document.getElementById('loader').innerText = '⚡ Bloc reçu en direct via WebRTC P2P sans transit serveur !';
1190
+ } else {
1191
+ // 2. Repli vers le relais HTTP si WebRTC échoue
1192
+ document.getElementById('loader').innerText = '⚠️ WebRTC indisponible. Téléchargement via relais HTTP...';
1193
+ const reqId = Math.random().toString(36).substring(2);
1194
+ const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1195
+ await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
1196
+
1197
+ let attempts = 0;
1198
+ while (attempts < coopTimeout) {
1199
+ const pollSig = await signCoop("poll_response", nodeId, reqId);
1200
+ const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1201
+ const data = await res.json();
1202
+ if (data.status === 'ready') {
1203
+ peerBlock = data.block_data;
1204
+ break;
1205
+ }
1206
+ await new Promise(r => setTimeout(r, 1000));
1207
+ attempts++;
1048
1208
  }
1049
- await new Promise(r => setTimeout(r, 1000));
1050
- attempts++;
1051
- }
1052
- if (!peerBlock) {
1053
- document.getElementById('loader').innerText = '⚠️ Peer de sous-réseau injoignable. Validation solo...';
1054
1209
  }
1055
1210
  }
1056
1211
 
@@ -1084,14 +1239,21 @@ JS;
1084
1239
  string $clientSecret,
1085
1240
  array $securityConfig,
1086
1241
  array $trapUrls,
1087
- string $originalFingerprint
1242
+ string $originalFingerprint,
1243
+ string $clientIp = '',
1244
+ string $tlsSessionId = '',
1245
+ ?string $baseBlock = null,
1246
+ bool $isHttps = true
1088
1247
  ): string {
1089
1248
  $nonce = $cpuChallengeDetails['nonce'];
1090
1249
  $target = $cpuChallengeDetails['target']; // @phpstan-ignore-line
1091
1250
  $path = $cpuChallengeDetails['path'];
1251
+ $isHttp = !$isHttps;
1092
1252
 
1093
1253
  $solverCode = self::getPowSolverCode();
1094
- $baseBlock = self::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
1254
+ if ($baseBlock === null || $baseBlock === '') {
1255
+ $baseBlock = self::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $clientIp, $tlsSessionId);
1256
+ }
1095
1257
  $baseBlockBytes = '[' . implode(',', array_values(unpack('C*', $baseBlock))) . ']';
1096
1258
 
1097
1259
  $trapTags = ['div', 'span', 'p', 'section'];
@@ -1115,16 +1277,26 @@ JS;
1115
1277
  $safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
1116
1278
  $safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
1117
1279
  $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
1280
+ $isHttpJs = $isHttp ? 'true' : 'false';
1118
1281
 
1119
1282
  $challengeScript = <<<JS
1120
1283
  async function solve() {
1121
1284
  const nonce = {$safeNonce};
1122
1285
  const path = {$safePath};
1123
1286
  const clientSecret = {$safeClientSecret};
1287
+ const isHttp = {$isHttpJs};
1124
1288
  const cpuTarget = BigInt("0x" + "{$target}");
1125
1289
  const memDifficulty = {$memoryDifficulty};
1126
1290
  const baseBlock = new Uint8Array({$baseBlockBytes});
1127
1291
 
1292
+ if (isHttp) {
1293
+ document.getElementById('loader').innerText = '⚠️ Connexion HTTP non sécurisée : validation simplifiée en cours...';
1294
+ await new Promise(r => setTimeout(r, 150));
1295
+ const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=http_simulacre_ack&pow_solution_mem=0";
1296
+ window.location.href = finalUrl;
1297
+ return;
1298
+ }
1299
+
1128
1300
  document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1129
1301
  const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
1130
1302