@anonympins/fingerprint 0.3.2 → 0.3.3
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 +172 -0
- package/README.md +276 -34
- package/composer.json +38 -0
- package/index.js +5 -0
- package/package.json +23 -18
- package/phpunit.xml +20 -0
- package/public/fp.js +2 -0
- package/src/js/build-client.js +69 -0
- package/{fingerprint.builder.js → src/js/fingerprint.builder.js} +168 -175
- package/{fingerprint.client.js → src/js/fingerprint.client.js} +634 -538
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/{fingerprint.js → src/js/fingerprint.js} +255 -101
- package/{library.js → src/js/library.js} +1729 -1729
- package/{problem-manager.js → src/js/problem-manager.js} +539 -522
- package/src/php/AutoTuner.php +155 -0
- package/src/php/Challenge/ChallengeUtils.php +306 -0
- package/src/php/Config/SecurityProfiles.php +257 -0
- package/src/php/DirectFingerprint.php +81 -0
- package/src/php/FingerprintBuilder.php +185 -0
- package/src/php/FingerprintClient.php +118 -0
- package/src/php/FingerprintEngine.php +850 -0
- package/src/php/Optimization/FunctionRegistry.php +63 -0
- package/src/php/Optimization/Optimization.php +256 -0
- package/src/php/Optimization/OptimizationOperators.php +305 -0
- package/src/php/Optimization/ProblemInitializers.php +53 -0
- package/src/php/ProblemManager.php +255 -0
- package/src/php/RequestContext.php +87 -0
- package/src/php/Store/IStore.php +42 -0
- package/src/php/Store/InMemoryStore.php +67 -0
- package/src/php/Store/StoreManager.php +26 -0
- package/src/php/Tests/ChallengeUtilsTest.php +82 -0
- package/src/php/Tests/FingerprintBuilderTest.php +58 -0
- package/src/php/Tests/FingerprintEngineTest.php +219 -0
- package/src/php/Tests/PowTest.php +40 -0
- package/src/php/Tests/ProblemManagerTest.php +295 -0
- package/src/php/Tests/RequestUtilsTest.php +81 -0
- package/src/php/Tests/problems.config.json +9 -0
- package/src/php/Utils/BigInt.php +102 -0
- package/src/php/Utils/BlockList.php +100 -0
- package/src/php/Utils/Logger.php +30 -0
- package/src/php/Utils/MaliciousPatterns.php +59 -0
- package/src/php/Utils/RequestUtils.php +673 -0
- package/fingerprint.client.obfuscated.js +0 -1
- /package/{mongodb-store.js → src/js/mongodb-store.js} +0 -0
- /package/{optimization.worker.js → src/js/optimization.worker.js} +0 -0
- /package/{pow.solver.inline.js → src/js/pow.solver.inline.js} +0 -0
- /package/{pow.solver.js → src/js/pow.solver.js} +0 -0
- /package/{pow.worker.js → src/js/pow.worker.js} +0 -0
- /package/{redis-store.js → src/js/redis-store.js} +0 -0
- /package/{sql-store.js → src/js/sql-store.js} +0 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Optimization\OptimizationOperators;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Gère le processus d'auto-ajustement en arrière-plan pour les seuils et poids de sécurité.
|
|
11
|
+
* Conçu pour être exécuté périodiquement (par exemple, via une tâche cron).
|
|
12
|
+
*/
|
|
13
|
+
class AutoTuner
|
|
14
|
+
{
|
|
15
|
+
/**
|
|
16
|
+
* @var array<string, mixed> La configuration de sécurité en direct qui sera mutée.
|
|
17
|
+
*/
|
|
18
|
+
private array $securityConfig;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @var array<int, array<string, mixed>> Les données de trafic collectées.
|
|
22
|
+
*/
|
|
23
|
+
private array $trafficData;
|
|
24
|
+
|
|
25
|
+
private int $minDataPoints;
|
|
26
|
+
private int $maxDataPoints;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @var ?array<string, mixed> La dernière meilleure solution trouvée par l'optimiseur.
|
|
30
|
+
*/
|
|
31
|
+
private static ?array $lastBestSolution = null;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param array<string, mixed> &$securityConfig La configuration de sécurité (passée par référence).
|
|
35
|
+
* @param array<int, array<string, mixed>> &$trafficData Les données de trafic (passées par référence).
|
|
36
|
+
* @param array<string, int> $options Options pour l'auto-ajustement.
|
|
37
|
+
*/
|
|
38
|
+
public function __construct(array &$securityConfig, array &$trafficData, array $options = [])
|
|
39
|
+
{
|
|
40
|
+
$this->securityConfig = &$securityConfig;
|
|
41
|
+
$this->trafficData = &$trafficData;
|
|
42
|
+
$this->minDataPoints = $options['minDataPoints'] ?? 200;
|
|
43
|
+
$this->maxDataPoints = $options['maxDataPoints'] ?? 10000;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Exécute un cycle d'optimisation des seuils.
|
|
48
|
+
*/
|
|
49
|
+
public function runOptimizationCycle(): void
|
|
50
|
+
{
|
|
51
|
+
$highConfidenceLogs = count(array_filter(
|
|
52
|
+
$this->trafficData,
|
|
53
|
+
fn ($log) => in_array($log['type'], ['challenge_solved', 'trap_triggered'])
|
|
54
|
+
));
|
|
55
|
+
$highConfidenceRatio = count($this->trafficData) > 0 ? $highConfidenceLogs / count($this->trafficData) : 0;
|
|
56
|
+
$minConfidenceRatio = 0.05; // Exiger au moins 5% de signaux forts.
|
|
57
|
+
|
|
58
|
+
if (count($this->trafficData) < $this->minDataPoints || $highConfidenceRatio < $minConfidenceRatio) {
|
|
59
|
+
if (count($this->trafficData) < $this->minDataPoints) {
|
|
60
|
+
echo sprintf("[AutoTuning] Reporté : %d/%d points de données.\n", count($this->trafficData), $this->minDataPoints);
|
|
61
|
+
} else {
|
|
62
|
+
echo sprintf("[AutoTuning] Reporté : Ratio de confiance insuffisant (%.2f%% < %.2f%%).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (count($this->trafficData) > $this->maxDataPoints) {
|
|
68
|
+
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);
|
|
69
|
+
$this->trafficData = array_slice($this->trafficData, count($this->trafficData) - $this->maxDataPoints);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
echo sprintf("[AutoTuning] Démarrage du cycle d'optimisation avec %d points de données.\n", count($this->trafficData));
|
|
73
|
+
|
|
74
|
+
$paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $this->trafficData]);
|
|
75
|
+
|
|
76
|
+
if (empty($paretoFront)) {
|
|
77
|
+
echo "[AutoTuning] L'optimisation n'a retourné aucune solution.\n";
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Stratégie de sélection : choisir la solution la plus équilibrée (la plus proche de l'origine).
|
|
82
|
+
$bestSolution = $paretoFront[0];
|
|
83
|
+
$minDistance = sqrt(pow($bestSolution['objectives'][0], 2) + pow($bestSolution['objectives'][1], 2));
|
|
84
|
+
|
|
85
|
+
for ($i = 1; $i < count($paretoFront); $i++) {
|
|
86
|
+
$distance = sqrt(pow($paretoFront[$i]['objectives'][0], 2) + pow($paretoFront[$i]['objectives'][1], 2));
|
|
87
|
+
if ($distance < $minDistance) {
|
|
88
|
+
$minDistance = $distance;
|
|
89
|
+
$bestSolution = $paretoFront[$i];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Logique d'inertie pour l'application de la configuration.
|
|
94
|
+
$newConfig = $bestSolution['solution'];
|
|
95
|
+
$maxChangeVelocity = 0.15; // 15% de changement maximum par cycle.
|
|
96
|
+
|
|
97
|
+
$applyInertialUpdate = function (&$currentConfig, $targetConfig) use ($maxChangeVelocity) {
|
|
98
|
+
if (empty($currentConfig) || empty($targetConfig)) return;
|
|
99
|
+
|
|
100
|
+
$totalCurrentWeight = 0;
|
|
101
|
+
$totalTargetWeight = 0;
|
|
102
|
+
|
|
103
|
+
foreach ($currentConfig as $key => $value) {
|
|
104
|
+
if (isset($targetConfig[$key])) {
|
|
105
|
+
$totalCurrentWeight += $value;
|
|
106
|
+
$totalTargetWeight += $targetConfig[$key];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if ($totalCurrentWeight === 0) return;
|
|
111
|
+
|
|
112
|
+
$globalChangeRatio = ($totalTargetWeight - $totalCurrentWeight) / $totalCurrentWeight;
|
|
113
|
+
$adjustmentFactor = max(-$maxChangeVelocity, min($maxChangeVelocity, $globalChangeRatio));
|
|
114
|
+
|
|
115
|
+
foreach ($currentConfig as $key => &$value) {
|
|
116
|
+
if (isset($targetConfig[$key])) {
|
|
117
|
+
$value *= (1 + $adjustmentFactor);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
$applyInertialUpdate($this->securityConfig['thresholds'], $newConfig['thresholds']);
|
|
123
|
+
$applyInertialUpdate($this->securityConfig['weights'], $newConfig['weights']);
|
|
124
|
+
$applyInertialUpdate($this->securityConfig['patterns'], $newConfig['patterns']);
|
|
125
|
+
|
|
126
|
+
self::$lastBestSolution = $bestSolution;
|
|
127
|
+
|
|
128
|
+
echo "[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.\n";
|
|
129
|
+
echo "[AutoTuning] Objectifs atteints : " . json_encode([
|
|
130
|
+
'falsePositiveRate' => round($bestSolution['objectives'][0], 4),
|
|
131
|
+
'falseNegativeRate' => round($bestSolution['objectives'][1], 4)
|
|
132
|
+
]) . "\n";
|
|
133
|
+
echo "[AutoTuning] Nouveaux seuils : " . json_encode($this->securityConfig['thresholds']) . "\n";
|
|
134
|
+
echo "[AutoTuning] Nouveaux poids : " . json_encode($this->securityConfig['weights']) . "\n";
|
|
135
|
+
echo "[AutoTuning] Nouveaux patterns : " . json_encode($this->securityConfig['patterns']) . "\n";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Retourne la dernière meilleure solution trouvée par l'auto-tuner.
|
|
140
|
+
* @return array<string, mixed>|null
|
|
141
|
+
*/
|
|
142
|
+
public static function getBestTuningSolution(): ?array
|
|
143
|
+
{
|
|
144
|
+
return self::$lastBestSolution;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Réinitialise la meilleure solution statique. Utile pour les tests.
|
|
149
|
+
* @internal
|
|
150
|
+
*/
|
|
151
|
+
public static function resetBestTuningSolution(): void
|
|
152
|
+
{
|
|
153
|
+
self::$lastBestSolution = null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Challenge;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Utils\BigInt;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Classe utilitaire pour la génération et la vérification des challenges Proof-of-Work.
|
|
11
|
+
*/
|
|
12
|
+
class ChallengeUtils
|
|
13
|
+
{
|
|
14
|
+
private const TRAP_URL_TEMPLATES = [
|
|
15
|
+
'/includes/config-{RANDOM}.php',
|
|
16
|
+
'/.env.{RANDOM}',
|
|
17
|
+
'/backups/db_backup_{RANDOM}.sql.gz',
|
|
18
|
+
'/api/v1/internal/status?trace={RANDOM}',
|
|
19
|
+
'/_private/deploy_key_{RANDOM}.pem',
|
|
20
|
+
'/logs/app_error_{RANDOM}.log',
|
|
21
|
+
'/.git/config_{RANDOM}'
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Récupère la clé secrète pour les PoW depuis les variables d'environnement.
|
|
26
|
+
*/
|
|
27
|
+
private static function getPowSecret(): string
|
|
28
|
+
{
|
|
29
|
+
$secret = $_ENV['POW_SECRET'] ?? getenv('POW_SECRET');
|
|
30
|
+
if (!$secret && ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production') {
|
|
31
|
+
throw new \RuntimeException('POW_SECRET environment variable is not set. This is required for production.');
|
|
32
|
+
}
|
|
33
|
+
return $secret ?: "fallback-dev-secret-32-chars-minimum";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Vérifie si un ticket de passage est valide.
|
|
38
|
+
*/
|
|
39
|
+
public static function isTicketValid(?string $ip, ?string $ticket): bool
|
|
40
|
+
{
|
|
41
|
+
if (empty($ip) || empty($ticket) || !str_contains($ticket, ':')) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[$expiry, $sig] = explode(':', $ticket, 2);
|
|
46
|
+
if (empty($expiry) || empty($sig) || (int)floor((float)$expiry) < (int)floor(microtime(true) * 1000)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
$expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", self::getPowSecret());
|
|
51
|
+
|
|
52
|
+
return hash_equals($expectedSig, $sig);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Calcule la cible de difficulté pour un challenge CPU en fonction du facteur de suspicion.
|
|
57
|
+
*/
|
|
58
|
+
public static function calculateCpuTarget(float $suspicionFactor, array $securityConfig): string
|
|
59
|
+
{
|
|
60
|
+
$cpuConfig = $securityConfig['cpu'] ?? [];
|
|
61
|
+
$minDifficultyBits = $cpuConfig['minDifficultyBits'] ?? 8;
|
|
62
|
+
$maxDifficultyBits = $cpuConfig['maxDifficultyBits'] ?? 24;
|
|
63
|
+
|
|
64
|
+
$totalDifficultyBits = $minDifficultyBits + $suspicionFactor * ($maxDifficultyBits - $minDifficultyBits);
|
|
65
|
+
|
|
66
|
+
if ($totalDifficultyBits <= 0) {
|
|
67
|
+
// Cible maximale (challenge trivial)
|
|
68
|
+
return (BigInt::pow(2, 256)->sub(new BigInt(1)))->toHex();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
$shift = 256 - (int)floor($totalDifficultyBits);
|
|
72
|
+
return (new BigInt(1))->shiftLeft($shift)->toHex();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Crée le bloc de données de base pour le challenge CPU.
|
|
77
|
+
*/
|
|
78
|
+
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint): string
|
|
79
|
+
{
|
|
80
|
+
$parts = explode('|', $fingerprint);
|
|
81
|
+
$filteredParts = array_filter($parts);
|
|
82
|
+
sort($filteredParts);
|
|
83
|
+
$sortedFingerprint = implode('|', $filteredParts);
|
|
84
|
+
|
|
85
|
+
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Vérifie une solution de PoW CPU et génère un ticket si elle est valide.
|
|
90
|
+
* @return string|null Le ticket en cas de succès, sinon null.
|
|
91
|
+
*/
|
|
92
|
+
public static function verifyCpuTargetPoWAndGenerateTicket(
|
|
93
|
+
string $clientIp,
|
|
94
|
+
int $ticketTtl,
|
|
95
|
+
string $nonce,
|
|
96
|
+
string $solution,
|
|
97
|
+
array $challengeContext
|
|
98
|
+
): ?string {
|
|
99
|
+
$cpuTargetHex = $challengeContext['cpuTarget'] ?? null;
|
|
100
|
+
$baseBlock = $challengeContext['baseBlock'] ?? null;
|
|
101
|
+
|
|
102
|
+
if ($cpuTargetHex === null || $baseBlock === null) {
|
|
103
|
+
error_log('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
$finalBlock = $baseBlock . $solution;
|
|
108
|
+
$hash = hash('sha256', $finalBlock);
|
|
109
|
+
|
|
110
|
+
$hashAsInt = BigInt::fromHex($hash);
|
|
111
|
+
$targetAsInt = BigInt::fromHex($cpuTargetHex);
|
|
112
|
+
|
|
113
|
+
$isValid = $hashAsInt->compareTo($targetAsInt) < 0;
|
|
114
|
+
|
|
115
|
+
if ($isValid) {
|
|
116
|
+
error_log('[FP Server Verify] CPU PoW verification PASSED.');
|
|
117
|
+
$expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
|
|
118
|
+
$signature = hash_hmac('sha256', "{$clientIp}:{$expiry}", self::getPowSecret());
|
|
119
|
+
return "{$expiry}:{$signature}";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Log details on failure
|
|
123
|
+
error_log(sprintf(
|
|
124
|
+
'[FP Server Verify] CPU PoW verification FAILED. Details: hashCalculated=0x%s, target=0x%s',
|
|
125
|
+
$hash,
|
|
126
|
+
$cpuTargetHex
|
|
127
|
+
));
|
|
128
|
+
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Vérifie une solution de PoW mémoire.
|
|
134
|
+
*/
|
|
135
|
+
public static function verifyMemoryPoW(
|
|
136
|
+
string $nonce,
|
|
137
|
+
string $solution,
|
|
138
|
+
int $difficulty,
|
|
139
|
+
string $clientSecret
|
|
140
|
+
): bool {
|
|
141
|
+
$maxAllowedMemDifficulty = 128; // 128MB
|
|
142
|
+
if ($difficulty > $maxAllowedMemDifficulty) {
|
|
143
|
+
error_log("[Security] Memory PoW verification attempt with excessive difficulty: {$difficulty}MB. Denied.");
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$size = $difficulty * 1024 * 1024;
|
|
148
|
+
if ($size <= 0) {
|
|
149
|
+
return true; // Pas de challenge mémoire si la difficulté est nulle ou négative.
|
|
150
|
+
}
|
|
151
|
+
$iterations = (int)floor($size / 16);
|
|
152
|
+
$buffer = new \SplFixedArray((int)floor($size / 4));
|
|
153
|
+
|
|
154
|
+
$seed = ":{$nonce}:{$clientSecret}";
|
|
155
|
+
$h = 0;
|
|
156
|
+
foreach (unpack('C*', $seed) as $byte) {
|
|
157
|
+
$h += $byte;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for ($i = 0; $i < count($buffer); $i++) {
|
|
161
|
+
$buffer[$i] = $h = self::gmp_imul($h ^ $i, 1597334677);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
$finalHash = 0;
|
|
165
|
+
$addr = count($buffer) > 0 ? $buffer[0] % count($buffer) : 0;
|
|
166
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
167
|
+
$addr = $buffer[$addr] % count($buffer);
|
|
168
|
+
$finalHash ^= $addr;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return $finalHash === (int)$solution;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Émule la multiplication 32-bit `Math.imul` de JavaScript.
|
|
176
|
+
*/
|
|
177
|
+
private static function gmp_imul(int $a, int $b): int
|
|
178
|
+
{
|
|
179
|
+
$a_lo = $a & 0xffff;
|
|
180
|
+
$a_hi = $a >> 16;
|
|
181
|
+
$b_lo = $b & 0xffff;
|
|
182
|
+
$b_hi = $b >> 16;
|
|
183
|
+
return (($a_lo * $b_lo) + ((($a_hi * $b_lo + $a_lo * $b_hi) << 16) & 0xffffffff)) | 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Génère une URL piège signée.
|
|
188
|
+
* @param string $nonce Le nonce pour signer l'URL.
|
|
189
|
+
* @return string L'URL piège.
|
|
190
|
+
*/
|
|
191
|
+
public static function generateTrapUrl(string $nonce): string
|
|
192
|
+
{
|
|
193
|
+
$template = self::TRAP_URL_TEMPLATES[array_rand(self::TRAP_URL_TEMPLATES)];
|
|
194
|
+
$randomPart = bin2hex(random_bytes(8));
|
|
195
|
+
$path = str_replace('{RANDOM}', $randomPart, $template);
|
|
196
|
+
|
|
197
|
+
$signature = substr(hash_hmac('sha256', $nonce . $path, self::getPowSecret()), 0, 16);
|
|
198
|
+
return "{$path}?sig={$signature}";
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Vérifie si une URL donnée est une URL piège valide pour un nonce donné.
|
|
203
|
+
* @param string $path Le chemin de la requête.
|
|
204
|
+
* @param string $signature La signature provenant de la query string.
|
|
205
|
+
* @param string $nonce Le nonce à vérifier.
|
|
206
|
+
* @return bool
|
|
207
|
+
*/
|
|
208
|
+
public static function verifyTrapUrl(string $path, string $signature, string $nonce): bool
|
|
209
|
+
{
|
|
210
|
+
if (empty($signature)) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
$expectedSignature = substr(hash_hmac('sha256', $nonce . $path, self::getPowSecret()), 0, 16);
|
|
214
|
+
// Utilise hash_equals pour une comparaison sécurisée contre les attaques temporelles.
|
|
215
|
+
return hash_equals($expectedSignature, $signature);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Charge le contenu du solveur JS pour l'injection inline.
|
|
220
|
+
* @return string Le code JavaScript du solveur.
|
|
221
|
+
*/
|
|
222
|
+
private static function getPowSolverCode(): string
|
|
223
|
+
{
|
|
224
|
+
// Le chemin doit être relatif à ce fichier ou absolu.
|
|
225
|
+
$solverPath = __DIR__ . '/../../js/pow.solver.inline.js';
|
|
226
|
+
if (!file_exists($solverPath)) {
|
|
227
|
+
error_log("[ChallengeUtils] Erreur: Le fichier pow.solver.inline.js n'a pas été trouvé à l'emplacement attendu.");
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
return file_get_contents($solverPath) ?: '';
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Génère le contenu HTML pour un challenge combiné CPU + Mémoire.
|
|
235
|
+
* @param array $cpuChallengeDetails
|
|
236
|
+
* @param int $memoryDifficulty
|
|
237
|
+
* @param string $clientSecret
|
|
238
|
+
* @param array $securityConfig
|
|
239
|
+
* @param array $trapUrls
|
|
240
|
+
* @param string $originalFingerprint
|
|
241
|
+
* @return string
|
|
242
|
+
*/
|
|
243
|
+
public static function generateCombinedPoWChallengePage(
|
|
244
|
+
array $cpuChallengeDetails,
|
|
245
|
+
int $memoryDifficulty,
|
|
246
|
+
string $clientSecret,
|
|
247
|
+
array $securityConfig,
|
|
248
|
+
array $trapUrls,
|
|
249
|
+
string $originalFingerprint
|
|
250
|
+
): string {
|
|
251
|
+
$nonce = $cpuChallengeDetails['nonce'];
|
|
252
|
+
$target = $cpuChallengeDetails['target']; // @phpstan-ignore-line
|
|
253
|
+
$path = $cpuChallengeDetails['path'];
|
|
254
|
+
|
|
255
|
+
$solverCode = self::getPowSolverCode();
|
|
256
|
+
$baseBlock = self::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint);
|
|
257
|
+
$baseBlockBytes = '[' . implode(',', array_values(unpack('C*', $baseBlock))) . ']';
|
|
258
|
+
|
|
259
|
+
$trapLinksHtml = implode(' ', array_map(fn($url) => "<a href=\"{$url}\" tabindex=\"-1\">config</a>", $trapUrls));
|
|
260
|
+
$trapContainerHtml = "<div style=\"position:absolute;left:-9999px;top:-9999px;\" aria-hidden=\"true\">{$trapLinksHtml}</div>";
|
|
261
|
+
|
|
262
|
+
$challengeScript = <<<JS
|
|
263
|
+
async function solve() {
|
|
264
|
+
const nonce = "{$nonce}";
|
|
265
|
+
const path = "{$path}";
|
|
266
|
+
const clientSecret = "{$clientSecret}";
|
|
267
|
+
const cpuTarget = BigInt("0x" + "{$target}");
|
|
268
|
+
const memDifficulty = {$memoryDifficulty};
|
|
269
|
+
const baseBlock = new Uint8Array({$baseBlockBytes});
|
|
270
|
+
|
|
271
|
+
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
272
|
+
const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
|
|
273
|
+
|
|
274
|
+
if (memDifficulty > 0) {
|
|
275
|
+
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
|
|
276
|
+
await new Promise(r => setTimeout(r, 10));
|
|
277
|
+
}
|
|
278
|
+
let memSolution = 0;
|
|
279
|
+
try {
|
|
280
|
+
const memSeed = nonce + ":" + clientSecret;
|
|
281
|
+
memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
|
|
282
|
+
} catch(e) {
|
|
283
|
+
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
288
|
+
window.location.href = finalUrl;
|
|
289
|
+
}
|
|
290
|
+
solve();
|
|
291
|
+
JS;
|
|
292
|
+
|
|
293
|
+
$htmlTemplate = '<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>';
|
|
294
|
+
$customTemplatePath = $securityConfig['challengePagePath'] ?? null;
|
|
295
|
+
|
|
296
|
+
if ($customTemplatePath && file_exists($customTemplatePath)) {
|
|
297
|
+
$htmlTemplate = file_get_contents($customTemplatePath) ?: $htmlTemplate;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return str_replace(
|
|
301
|
+
['<!-- FINGERPRINT_SOLVER_SCRIPT -->', '<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', '<!-- FINGERPRINT_TRAPS -->'],
|
|
302
|
+
[$solverCode, $challengeScript, $trapContainerHtml],
|
|
303
|
+
$htmlTemplate
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
}
|