@anonympins/fingerprint 0.3.7 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +331 -244
- package/README.md +63 -1201
- package/composer.json +38 -38
- package/index.js +4 -4
- package/package.json +103 -103
- package/phpunit.xml +20 -20
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/fingerprint.client.js +2 -0
- package/src/js/fingerprint.js +4429 -4220
- package/src/js/mongodb-store.js +79 -79
- package/src/js/pow.solver.inline.js +31 -0
- package/src/js/pow.solver.js +31 -0
- package/src/js/tests/fingerprint.builder.test.js +79 -0
- package/src/js/tests/fingerprint.client.init.test.js +120 -0
- package/src/js/tests/fingerprint.client.test.js +105 -0
- package/src/js/tests/fingerprint.engine.test.js +371 -0
- package/src/js/tests/fingerprint.isMalicious.test.js +117 -0
- package/src/js/tests/fingerprint.test.js +2319 -0
- package/src/js/tests/ip-reputation.test.js +132 -0
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -0
- package/src/js/tests/library.test.js +96 -0
- package/src/js/tests/metrics.test.js +104 -0
- package/src/js/tests/pow.solver.test.js +198 -0
- package/src/js/tests/problem-manager.test.js +323 -0
- package/src/js/tests/stores.test.js +118 -0
- package/src/php/Challenge/ChallengeUtils.php +361 -305
- package/src/php/Config/SecurityProfiles.php +271 -266
- package/src/php/FingerprintBuilder.php +185 -185
- package/src/php/FingerprintClient.php +131 -131
- package/src/php/FingerprintEngine.php +1006 -1006
- package/src/php/Ja3AnomalyDetector.php +227 -227
- package/src/php/Optimization/FunctionRegistry.php +62 -62
- package/src/php/Optimization/Optimization.php +255 -255
- package/src/php/Optimization/OptimizationOperators.php +304 -304
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +104 -104
- package/src/php/Store/RedisStore.php +53 -53
- package/src/php/Tests/ChallengeUtilsTest.php +81 -81
- package/src/php/Tests/FingerprintBuilderTest.php +57 -57
- package/src/php/Tests/FingerprintClientTest.php +71 -0
- package/src/php/Tests/FingerprintEngineTest.php +299 -299
- package/src/php/Tests/IpReputationTest.php +156 -156
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +179 -179
- package/src/php/Tests/MetricsTest.php +45 -45
- package/src/php/Tests/PowTest.php +39 -39
- package/src/php/Tests/ProblemManagerTest.php +296 -296
- package/src/php/Tests/RequestUtilsTest.php +253 -145
- package/src/php/Tests/TLSClientHelloParserTest.php +118 -0
- package/src/php/Tests/problems.config.json +8 -8
- package/src/php/Utils/BigInt.php +144 -144
- package/src/php/Utils/Logger.php +29 -29
- package/src/php/Utils/MaliciousPatterns.php +58 -58
- package/src/php/Utils/MetricsManager.php +166 -166
- package/src/php/Utils/RequestUtils.php +31 -4
- package/src/php/Utils/TLSClientHelloParser.php +117 -0
- package/src/php/bin/auto-tune.php +117 -117
|
@@ -1,186 +1,186 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
9
|
-
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
10
|
-
*/
|
|
11
|
-
class FingerprintBuilder
|
|
12
|
-
{
|
|
13
|
-
/**
|
|
14
|
-
* @var array<string, string|int>
|
|
15
|
-
*/
|
|
16
|
-
private array $components = [];
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Ajoute un composant à l'empreinte.
|
|
20
|
-
* La valeur est hachée pour l'anonymiser et réduire sa taille.
|
|
21
|
-
*
|
|
22
|
-
* @param string $group Le nom du groupe (ex: 'hw', 'screen', 'geo').
|
|
23
|
-
* @param string|int|bool|null $value La valeur brute à hacher.
|
|
24
|
-
* @return self
|
|
25
|
-
*/
|
|
26
|
-
public function add(string $group, $value): self
|
|
27
|
-
{
|
|
28
|
-
if ($value === null || $value === '') {
|
|
29
|
-
return $this;
|
|
30
|
-
}
|
|
31
|
-
// On hache la valeur individuellement.
|
|
32
|
-
$this->components[$group] = self::cyrb53((string)$value);
|
|
33
|
-
return $this;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Ajoute un composant brut sans le hacher.
|
|
38
|
-
* Utile pour les métriques qui doivent être lues telles quelles par le serveur.
|
|
39
|
-
*
|
|
40
|
-
* @param string $group Le nom du groupe.
|
|
41
|
-
* @param string|int|null $value La valeur brute.
|
|
42
|
-
* @return self
|
|
43
|
-
*/
|
|
44
|
-
public function addRaw(string $group, $value): self
|
|
45
|
-
{
|
|
46
|
-
if ($value === null) {
|
|
47
|
-
return $this;
|
|
48
|
-
}
|
|
49
|
-
$this->components[$group] = $value;
|
|
50
|
-
return $this;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Génère la chaîne de l'empreinte finale.
|
|
55
|
-
* Les composants sont triés par clé pour garantir un ordre déterministe.
|
|
56
|
-
*
|
|
57
|
-
* @return string
|
|
58
|
-
*/
|
|
59
|
-
public function __toString(): string
|
|
60
|
-
{
|
|
61
|
-
// ksort trie le tableau par clé.
|
|
62
|
-
ksort($this->components);
|
|
63
|
-
|
|
64
|
-
$parts = [];
|
|
65
|
-
foreach ($this->components as $key => $hash) {
|
|
66
|
-
$parts[] = "{$key}:{$hash}";
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return implode('|', $parts);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Compare deux empreintes et retourne un score de similarité (de 0 à 1).
|
|
74
|
-
* Utilise une pondération pour donner plus d'importance aux invariants forts (Canvas, GPU, JA3).
|
|
75
|
-
*
|
|
76
|
-
* @param string|null $fpString1 Empreinte A.
|
|
77
|
-
* @param string|null $fpString2 Empreinte B.
|
|
78
|
-
* @return float
|
|
79
|
-
*/
|
|
80
|
-
public static function compare(?string $fpString1, ?string $fpString2): float
|
|
81
|
-
{
|
|
82
|
-
if (empty($fpString1) || empty($fpString2)) {
|
|
83
|
-
return 0.0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
$parse = function (string $str): array {
|
|
87
|
-
$map = [];
|
|
88
|
-
foreach (explode('|', $str) as $part) {
|
|
89
|
-
$pair = explode(':', $part, 2);
|
|
90
|
-
if (count($pair) === 2 && !empty($pair[0]) && !empty($pair[1])) {
|
|
91
|
-
$map[$pair[0]] = $pair[1];
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return $map;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
$map1 = $parse($fpString1);
|
|
98
|
-
$map2 = $parse($fpString2);
|
|
99
|
-
|
|
100
|
-
$volatileKeys = [
|
|
101
|
-
'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
|
|
102
|
-
'cookie_keys', 'upgrade', 'network', 'http_ver',
|
|
103
|
-
'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
|
|
104
|
-
];
|
|
105
|
-
|
|
106
|
-
$weights = [
|
|
107
|
-
'cvs' => 5.0, 'gpu' => 4.0, 'ja3' => 3.5, 'ja4' => 4.0, 'ja4s' => 4.0, 'ja4h' => 3.8,
|
|
108
|
-
'h2_settings' => 3.0, 'tcp_fp' => 2.5, 'ua' => 2.0,
|
|
109
|
-
'client_fp_hash' => 3.0, 'browser' => 1.5, 'os_version' => 1.5,
|
|
110
|
-
'device_type' => 1.0, 'hw' => 1.5, 'scr' => 1.0, 'os' => 0.8, 'geo' => 0.5,
|
|
111
|
-
];
|
|
112
|
-
|
|
113
|
-
$weightedMatches = 0.0;
|
|
114
|
-
$totalWeight = 0.0;
|
|
115
|
-
|
|
116
|
-
$allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
|
|
117
|
-
|
|
118
|
-
foreach ($allKeys as $key) {
|
|
119
|
-
// On ignore les clés volatiles pour cette comparaison spécifique.
|
|
120
|
-
if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
|
|
121
|
-
continue;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// On ne compare que les clés qui ont un poids défini.
|
|
125
|
-
$weight = $weights[$key] ?? null;
|
|
126
|
-
if ($weight === null) continue;
|
|
127
|
-
|
|
128
|
-
$totalWeight += $weight;
|
|
129
|
-
if (isset($map1[$key]) && isset($map2[$key])) {
|
|
130
|
-
if ($map1[$key] === $map2[$key]) {
|
|
131
|
-
$weightedMatches += $weight;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return $totalWeight === 0.0 ? 0.0 : $weightedMatches / $totalWeight;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
141
|
-
* Porté depuis la version JavaScript.
|
|
142
|
-
*
|
|
143
|
-
* @param string $str La chaîne à hacher.
|
|
144
|
-
* @param int $seed Une graine optionnelle.
|
|
145
|
-
* @return string Le hash sous forme de chaîne de caractères.
|
|
146
|
-
*/
|
|
147
|
-
public static function cyrb53(string $str, int $seed = 0): string
|
|
148
|
-
{
|
|
149
|
-
$h1 = 0xdeadbeef ^ $seed;
|
|
150
|
-
$h2 = 0x41c6ce57 ^ $seed;
|
|
151
|
-
|
|
152
|
-
for ($i = 0, $l = strlen($str); $i < $l; $i++) {
|
|
153
|
-
$ch = ord($str[$i]);
|
|
154
|
-
$h1 = self::imul($h1 ^ $ch, 2654435761);
|
|
155
|
-
$h2 = self::imul($h2 ^ $ch, 1597334677);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
$h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
|
|
159
|
-
$h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
|
|
160
|
-
|
|
161
|
-
// En PHP, les opérations sur les grands nombres peuvent être délicates.
|
|
162
|
-
// On utilise bcmath pour une arithmétique de précision arbitraire, garantissant le même résultat que JS.
|
|
163
|
-
$val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
|
|
164
|
-
return $val_h2;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Émule la multiplication 32-bit `Math.imul` de JavaScript.
|
|
169
|
-
*
|
|
170
|
-
* @param int $a
|
|
171
|
-
* @param int $b
|
|
172
|
-
* @return int Un entier signé 32-bit.
|
|
173
|
-
*/
|
|
174
|
-
private static function imul(int $a, int $b): int
|
|
175
|
-
{
|
|
176
|
-
// Emulation of JavaScript's Math.imul for signed 32-bit integer multiplication.
|
|
177
|
-
// This version correctly handles overflows on 64-bit systems.
|
|
178
|
-
$ah = ($a >> 16) & 0xffff;
|
|
179
|
-
$al = $a & 0xffff;
|
|
180
|
-
$bh = ($b >> 16) & 0xffff;
|
|
181
|
-
$bl = $b & 0xffff;
|
|
182
|
-
$lo = $al * $bl;
|
|
183
|
-
$hi = (($lo >> 16) + ($al * $bh) + ($ah * $bl)) & 0xffff;
|
|
184
|
-
return (($hi << 16) | ($lo & 0xffff)) | 0;
|
|
185
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Classe pour construire une empreinte composite (Multi-Hash).
|
|
9
|
+
* Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
|
|
10
|
+
*/
|
|
11
|
+
class FingerprintBuilder
|
|
12
|
+
{
|
|
13
|
+
/**
|
|
14
|
+
* @var array<string, string|int>
|
|
15
|
+
*/
|
|
16
|
+
private array $components = [];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ajoute un composant à l'empreinte.
|
|
20
|
+
* La valeur est hachée pour l'anonymiser et réduire sa taille.
|
|
21
|
+
*
|
|
22
|
+
* @param string $group Le nom du groupe (ex: 'hw', 'screen', 'geo').
|
|
23
|
+
* @param string|int|bool|null $value La valeur brute à hacher.
|
|
24
|
+
* @return self
|
|
25
|
+
*/
|
|
26
|
+
public function add(string $group, $value): self
|
|
27
|
+
{
|
|
28
|
+
if ($value === null || $value === '') {
|
|
29
|
+
return $this;
|
|
30
|
+
}
|
|
31
|
+
// On hache la valeur individuellement.
|
|
32
|
+
$this->components[$group] = self::cyrb53((string)$value);
|
|
33
|
+
return $this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ajoute un composant brut sans le hacher.
|
|
38
|
+
* Utile pour les métriques qui doivent être lues telles quelles par le serveur.
|
|
39
|
+
*
|
|
40
|
+
* @param string $group Le nom du groupe.
|
|
41
|
+
* @param string|int|null $value La valeur brute.
|
|
42
|
+
* @return self
|
|
43
|
+
*/
|
|
44
|
+
public function addRaw(string $group, $value): self
|
|
45
|
+
{
|
|
46
|
+
if ($value === null) {
|
|
47
|
+
return $this;
|
|
48
|
+
}
|
|
49
|
+
$this->components[$group] = $value;
|
|
50
|
+
return $this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Génère la chaîne de l'empreinte finale.
|
|
55
|
+
* Les composants sont triés par clé pour garantir un ordre déterministe.
|
|
56
|
+
*
|
|
57
|
+
* @return string
|
|
58
|
+
*/
|
|
59
|
+
public function __toString(): string
|
|
60
|
+
{
|
|
61
|
+
// ksort trie le tableau par clé.
|
|
62
|
+
ksort($this->components);
|
|
63
|
+
|
|
64
|
+
$parts = [];
|
|
65
|
+
foreach ($this->components as $key => $hash) {
|
|
66
|
+
$parts[] = "{$key}:{$hash}";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return implode('|', $parts);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compare deux empreintes et retourne un score de similarité (de 0 à 1).
|
|
74
|
+
* Utilise une pondération pour donner plus d'importance aux invariants forts (Canvas, GPU, JA3).
|
|
75
|
+
*
|
|
76
|
+
* @param string|null $fpString1 Empreinte A.
|
|
77
|
+
* @param string|null $fpString2 Empreinte B.
|
|
78
|
+
* @return float
|
|
79
|
+
*/
|
|
80
|
+
public static function compare(?string $fpString1, ?string $fpString2): float
|
|
81
|
+
{
|
|
82
|
+
if (empty($fpString1) || empty($fpString2)) {
|
|
83
|
+
return 0.0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$parse = function (string $str): array {
|
|
87
|
+
$map = [];
|
|
88
|
+
foreach (explode('|', $str) as $part) {
|
|
89
|
+
$pair = explode(':', $part, 2);
|
|
90
|
+
if (count($pair) === 2 && !empty($pair[0]) && !empty($pair[1])) {
|
|
91
|
+
$map[$pair[0]] = $pair[1];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return $map;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
$map1 = $parse($fpString1);
|
|
98
|
+
$map2 = $parse($fpString2);
|
|
99
|
+
|
|
100
|
+
$volatileKeys = [
|
|
101
|
+
'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
|
|
102
|
+
'cookie_keys', 'upgrade', 'network', 'http_ver',
|
|
103
|
+
'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
$weights = [
|
|
107
|
+
'cvs' => 5.0, 'gpu' => 4.0, 'ja3' => 3.5, 'ja4' => 4.0, 'ja4s' => 4.0, 'ja4h' => 3.8,
|
|
108
|
+
'h2_settings' => 3.0, 'tcp_fp' => 2.5, 'ua' => 2.0,
|
|
109
|
+
'client_fp_hash' => 3.0, 'browser' => 1.5, 'os_version' => 1.5,
|
|
110
|
+
'device_type' => 1.0, 'hw' => 1.5, 'scr' => 1.0, 'os' => 0.8, 'geo' => 0.5,
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
$weightedMatches = 0.0;
|
|
114
|
+
$totalWeight = 0.0;
|
|
115
|
+
|
|
116
|
+
$allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
|
|
117
|
+
|
|
118
|
+
foreach ($allKeys as $key) {
|
|
119
|
+
// On ignore les clés volatiles pour cette comparaison spécifique.
|
|
120
|
+
if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// On ne compare que les clés qui ont un poids défini.
|
|
125
|
+
$weight = $weights[$key] ?? null;
|
|
126
|
+
if ($weight === null) continue;
|
|
127
|
+
|
|
128
|
+
$totalWeight += $weight;
|
|
129
|
+
if (isset($map1[$key]) && isset($map2[$key])) {
|
|
130
|
+
if ($map1[$key] === $map2[$key]) {
|
|
131
|
+
$weightedMatches += $weight;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return $totalWeight === 0.0 ? 0.0 : $weightedMatches / $totalWeight;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Algorithme de hachage cyrb53 (rapide et faible taux de collision).
|
|
141
|
+
* Porté depuis la version JavaScript.
|
|
142
|
+
*
|
|
143
|
+
* @param string $str La chaîne à hacher.
|
|
144
|
+
* @param int $seed Une graine optionnelle.
|
|
145
|
+
* @return string Le hash sous forme de chaîne de caractères.
|
|
146
|
+
*/
|
|
147
|
+
public static function cyrb53(string $str, int $seed = 0): string
|
|
148
|
+
{
|
|
149
|
+
$h1 = 0xdeadbeef ^ $seed;
|
|
150
|
+
$h2 = 0x41c6ce57 ^ $seed;
|
|
151
|
+
|
|
152
|
+
for ($i = 0, $l = strlen($str); $i < $l; $i++) {
|
|
153
|
+
$ch = ord($str[$i]);
|
|
154
|
+
$h1 = self::imul($h1 ^ $ch, 2654435761);
|
|
155
|
+
$h2 = self::imul($h2 ^ $ch, 1597334677);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
$h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
|
|
159
|
+
$h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
|
|
160
|
+
|
|
161
|
+
// En PHP, les opérations sur les grands nombres peuvent être délicates.
|
|
162
|
+
// On utilise bcmath pour une arithmétique de précision arbitraire, garantissant le même résultat que JS.
|
|
163
|
+
$val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
|
|
164
|
+
return $val_h2;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Émule la multiplication 32-bit `Math.imul` de JavaScript.
|
|
169
|
+
*
|
|
170
|
+
* @param int $a
|
|
171
|
+
* @param int $b
|
|
172
|
+
* @return int Un entier signé 32-bit.
|
|
173
|
+
*/
|
|
174
|
+
private static function imul(int $a, int $b): int
|
|
175
|
+
{
|
|
176
|
+
// Emulation of JavaScript's Math.imul for signed 32-bit integer multiplication.
|
|
177
|
+
// This version correctly handles overflows on 64-bit systems.
|
|
178
|
+
$ah = ($a >> 16) & 0xffff;
|
|
179
|
+
$al = $a & 0xffff;
|
|
180
|
+
$bh = ($b >> 16) & 0xffff;
|
|
181
|
+
$bl = $b & 0xffff;
|
|
182
|
+
$lo = $al * $bl;
|
|
183
|
+
$hi = (($lo >> 16) + ($al * $bh) + ($ah * $bl)) & 0xffff;
|
|
184
|
+
return (($hi << 16) | ($lo & 0xffff)) | 0;
|
|
185
|
+
}
|
|
186
186
|
}
|
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
|
|
10
|
-
*
|
|
11
|
-
* Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
|
|
12
|
-
* dans une application PHP. Elle gère l'injection sécurisée du script et la création
|
|
13
|
-
* de "honeypots" (pièges à bots) dans les formulaires.
|
|
14
|
-
*/
|
|
15
|
-
class FingerprintClient
|
|
16
|
-
{
|
|
17
|
-
/**
|
|
18
|
-
* @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
|
|
19
|
-
*/
|
|
20
|
-
private string $clientScriptPath;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
|
|
24
|
-
*/
|
|
25
|
-
private array $clientConfig;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
|
|
29
|
-
*/
|
|
30
|
-
private ?string $nonce;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Constructeur de la classe.
|
|
34
|
-
*
|
|
35
|
-
* @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
|
|
36
|
-
* @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
|
|
37
|
-
*/
|
|
38
|
-
public function __construct(string $clientScriptPath, array $clientConfig = [])
|
|
39
|
-
{
|
|
40
|
-
$this->clientScriptPath = $clientScriptPath;
|
|
41
|
-
|
|
42
|
-
$defaultConfig = [
|
|
43
|
-
'mouse' => true,
|
|
44
|
-
'keystrokes' => true,
|
|
45
|
-
'clicks' => true,
|
|
46
|
-
'honeypots' => [],
|
|
47
|
-
'fetch' => [
|
|
48
|
-
'handleChallenges' => true,
|
|
49
|
-
'probationaryTtl' => 30000, // 30 seconds
|
|
50
|
-
],
|
|
51
|
-
'wasm' => true, // Activer la tentative de chargement du module WASM
|
|
52
|
-
'wasmPath' => '/fp.js' // Chemin vers le script de chargement WASM
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
// Utiliser une fusion profonde pour permettre de surcharger des sous-clés
|
|
56
|
-
$this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
|
|
57
|
-
|
|
58
|
-
try {
|
|
59
|
-
// Génère un nonce pour CSP si possible, pour une sécurité renforcée.
|
|
60
|
-
$this->nonce = bin2hex(random_bytes(16));
|
|
61
|
-
} catch (\Exception $e) {
|
|
62
|
-
$this->nonce = null;
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Génère un champ de formulaire "honeypot" caché.
|
|
68
|
-
* Les bots le rempliront, mais il sera invisible pour les humains.
|
|
69
|
-
*
|
|
70
|
-
* @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
|
|
71
|
-
* @return string Le code HTML du champ honeypot.
|
|
72
|
-
*/
|
|
73
|
-
public function generateHoneypotField(string $fieldName): string
|
|
74
|
-
{
|
|
75
|
-
// Ajoute le champ à la configuration pour que le script client le surveille.
|
|
76
|
-
if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
|
|
77
|
-
$this->clientConfig['honeypots'][] = $fieldName;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Styles CSS pour cacher le champ de manière robuste.
|
|
81
|
-
$styles = 'position:absolute; left:-9999px; top:-9999px; opacity:0;';
|
|
82
|
-
|
|
83
|
-
return '<div style="' . $styles . '" aria-hidden="true">'
|
|
84
|
-
. '<label for="' . htmlspecialchars($fieldName) . '">Ne pas remplir ce champ</label>'
|
|
85
|
-
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
86
|
-
. '</div>';
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
91
|
-
*
|
|
92
|
-
* @return string Le code HTML des balises <script>.
|
|
93
|
-
*/
|
|
94
|
-
public function getScriptTag(): string
|
|
95
|
-
{
|
|
96
|
-
$configJson = json_encode($this->clientConfig);
|
|
97
|
-
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
98
|
-
|
|
99
|
-
// Le script d'initialisation qui sera inclus dans la page.
|
|
100
|
-
$initScript = <<<JS
|
|
101
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
102
|
-
const config = {$configJson};
|
|
103
|
-
if (window.ClientLibrary) {
|
|
104
|
-
if (config.wasmPath) {
|
|
105
|
-
const wasmScript = document.createElement('script');
|
|
106
|
-
wasmScript.src = config.wasmPath;
|
|
107
|
-
wasmScript.async = true;
|
|
108
|
-
wasmScript.nonce = '{$this->nonce}';
|
|
109
|
-
document.head.appendChild(wasmScript);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
window.ClientLibrary.initializeClient(config);
|
|
113
|
-
} else {
|
|
114
|
-
console.error('Fingerprint client library not loaded.');
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
JS;
|
|
118
|
-
|
|
119
|
-
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
120
|
-
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
121
|
-
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
126
|
-
* @return string|null
|
|
127
|
-
*/
|
|
128
|
-
public function getNonce(): ?string
|
|
129
|
-
{
|
|
130
|
-
return $this->nonce;
|
|
131
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FingerprintClient - Wrapper PHP pour la bibliothèque de fingerprinting côté client.
|
|
10
|
+
*
|
|
11
|
+
* Cette classe facilite l'intégration de la bibliothèque JavaScript `fingerprint.client.js`
|
|
12
|
+
* dans une application PHP. Elle gère l'injection sécurisée du script et la création
|
|
13
|
+
* de "honeypots" (pièges à bots) dans les formulaires.
|
|
14
|
+
*/
|
|
15
|
+
class FingerprintClient
|
|
16
|
+
{
|
|
17
|
+
/**
|
|
18
|
+
* @var string Le chemin vers le fichier de la bibliothèque client JavaScript.
|
|
19
|
+
*/
|
|
20
|
+
private string $clientScriptPath;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @var array La configuration à passer à la fonction `initializeClient` de la bibliothèque JS.
|
|
24
|
+
*/
|
|
25
|
+
private array $clientConfig;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @var string|null Un nonce cryptographique pour la Content Security Policy (CSP).
|
|
29
|
+
*/
|
|
30
|
+
private ?string $nonce;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Constructeur de la classe.
|
|
34
|
+
*
|
|
35
|
+
* @param string $clientScriptPath Le chemin d'accès web au fichier `fingerprint.client.js`.
|
|
36
|
+
* @param array $clientConfig La configuration pour la bibliothèque client (souris, frappes, honeypots, etc.).
|
|
37
|
+
*/
|
|
38
|
+
public function __construct(string $clientScriptPath, array $clientConfig = [])
|
|
39
|
+
{
|
|
40
|
+
$this->clientScriptPath = $clientScriptPath;
|
|
41
|
+
|
|
42
|
+
$defaultConfig = [
|
|
43
|
+
'mouse' => true,
|
|
44
|
+
'keystrokes' => true,
|
|
45
|
+
'clicks' => true,
|
|
46
|
+
'honeypots' => [],
|
|
47
|
+
'fetch' => [
|
|
48
|
+
'handleChallenges' => true,
|
|
49
|
+
'probationaryTtl' => 30000, // 30 seconds
|
|
50
|
+
],
|
|
51
|
+
'wasm' => true, // Activer la tentative de chargement du module WASM
|
|
52
|
+
'wasmPath' => '/fp.js' // Chemin vers le script de chargement WASM
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Utiliser une fusion profonde pour permettre de surcharger des sous-clés
|
|
56
|
+
$this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
// Génère un nonce pour CSP si possible, pour une sécurité renforcée.
|
|
60
|
+
$this->nonce = bin2hex(random_bytes(16));
|
|
61
|
+
} catch (\Exception $e) {
|
|
62
|
+
$this->nonce = null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Génère un champ de formulaire "honeypot" caché.
|
|
68
|
+
* Les bots le rempliront, mais il sera invisible pour les humains.
|
|
69
|
+
*
|
|
70
|
+
* @param string $fieldName Le nom du champ (doit correspondre à la configuration client).
|
|
71
|
+
* @return string Le code HTML du champ honeypot.
|
|
72
|
+
*/
|
|
73
|
+
public function generateHoneypotField(string $fieldName): string
|
|
74
|
+
{
|
|
75
|
+
// Ajoute le champ à la configuration pour que le script client le surveille.
|
|
76
|
+
if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
|
|
77
|
+
$this->clientConfig['honeypots'][] = $fieldName;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Styles CSS pour cacher le champ de manière robuste.
|
|
81
|
+
$styles = 'position:absolute; left:-9999px; top:-9999px; opacity:0;';
|
|
82
|
+
|
|
83
|
+
return '<div style="' . $styles . '" aria-hidden="true">'
|
|
84
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">Ne pas remplir ce champ</label>'
|
|
85
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
86
|
+
. '</div>';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
91
|
+
*
|
|
92
|
+
* @return string Le code HTML des balises <script>.
|
|
93
|
+
*/
|
|
94
|
+
public function getScriptTag(): string
|
|
95
|
+
{
|
|
96
|
+
$configJson = json_encode($this->clientConfig);
|
|
97
|
+
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
98
|
+
|
|
99
|
+
// Le script d'initialisation qui sera inclus dans la page.
|
|
100
|
+
$initScript = <<<JS
|
|
101
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
102
|
+
const config = {$configJson};
|
|
103
|
+
if (window.ClientLibrary) {
|
|
104
|
+
if (config.wasmPath) {
|
|
105
|
+
const wasmScript = document.createElement('script');
|
|
106
|
+
wasmScript.src = config.wasmPath;
|
|
107
|
+
wasmScript.async = true;
|
|
108
|
+
wasmScript.nonce = '{$this->nonce}';
|
|
109
|
+
document.head.appendChild(wasmScript);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
window.ClientLibrary.initializeClient(config);
|
|
113
|
+
} else {
|
|
114
|
+
console.error('Fingerprint client library not loaded.');
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
JS;
|
|
118
|
+
|
|
119
|
+
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
120
|
+
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
121
|
+
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
126
|
+
* @return string|null
|
|
127
|
+
*/
|
|
128
|
+
public function getNonce(): ?string
|
|
129
|
+
{
|
|
130
|
+
return $this->nonce;
|
|
131
|
+
}
|
|
132
132
|
}
|