@anonympins/fingerprint 0.7.0 → 0.7.2

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 (66) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +1 -1
  3. package/composer.json +8 -1
  4. package/package.json +1 -1
  5. package/public/fingerprint-wordpress.zip +0 -0
  6. package/src/js/build-client.js +8 -10
  7. package/src/js/fingerprint.builder.js +37 -38
  8. package/src/js/fingerprint.client.js +1531 -1463
  9. package/src/js/fingerprint.client.obfuscated.js +1 -0
  10. package/src/js/fingerprint.js +190 -16
  11. package/src/js/fingerprint.utils.js +213 -213
  12. package/src/js/library.js +340 -376
  13. package/src/js/mongodb-store.js +1 -1
  14. package/src/js/optimization.worker.js +27 -27
  15. package/src/js/pow.solver.inline.js +154 -56
  16. package/src/js/pow.solver.js +180 -92
  17. package/src/js/pow.worker.js +48 -48
  18. package/src/js/redis-store.js +1 -1
  19. package/src/js/tests/fingerprint.client.init.test.js +146 -143
  20. package/src/js/tests/fingerprint.test.js +124 -10
  21. package/src/js/tests/ip-reputation.test.js +1 -0
  22. package/src/js/upow-model-task.js +14 -15
  23. package/src/php/AutoTuner.php +34 -34
  24. package/src/php/Challenge/ChallengeUtils.php +57 -11
  25. package/src/php/Config/SecurityProfiles.php +20 -0
  26. package/src/php/DirectFingerprint.php +190 -145
  27. package/src/php/FingerprintBuilder.php +29 -30
  28. package/src/php/FingerprintClient.php +147 -148
  29. package/src/php/FingerprintEngine.php +44 -17
  30. package/src/php/Ja3AnomalyDetector.php +33 -32
  31. package/src/php/Optimization/FunctionRegistry.php +7 -7
  32. package/src/php/Optimization/Optimization.php +24 -24
  33. package/src/php/Optimization/OptimizationOperators.php +26 -25
  34. package/src/php/Optimization/ProblemInitializers.php +52 -52
  35. package/src/php/ProblemManager.php +31 -34
  36. package/src/php/RequestContext.php +8 -1
  37. package/src/php/Store/IStore.php +7 -8
  38. package/src/php/Store/InMemoryStore.php +66 -66
  39. package/src/php/Store/MongoDbStore.php +5 -5
  40. package/src/php/Store/RedisStore.php +5 -5
  41. package/src/php/Store/StoreManager.php +35 -35
  42. package/src/php/Tests/ChallengeUtilsTest.php +453 -453
  43. package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
  44. package/src/php/Tests/FingerprintEngineTest.php +38 -0
  45. package/src/php/Tests/IpReputationTest.php +10 -0
  46. package/src/php/Tests/MaliciousPatternsTest.php +103 -103
  47. package/src/php/Tests/MetricsTest.php +97 -97
  48. package/src/php/Tests/ProblemManagerTest.php +376 -376
  49. package/src/php/Tests/QuicFingerprintTest.php +53 -53
  50. package/src/php/Tests/RequestUtilsTest.php +471 -471
  51. package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
  52. package/src/php/Tests/config/ed25519_key.json +3 -3
  53. package/src/php/Utils/BlockList.php +99 -99
  54. package/src/php/Utils/Env.php +49 -49
  55. package/src/php/Utils/MaliciousPatterns.php +74 -74
  56. package/src/php/Utils/MetricsManager.php +209 -209
  57. package/src/php/Utils/RequestUtils.php +129 -7
  58. package/src/php/Utils/TLSClientHelloParser.php +369 -369
  59. package/src/php/WordPress/WpDbStore.php +155 -0
  60. package/src/php/WordPress/fingerprint-wordpress.php +554 -0
  61. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  62. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
  63. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  64. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
  65. package/src/php/WordPress/package.php +218 -0
  66. package/src/php/bin/auto-tune.php +118 -118
@@ -1,370 +1,370 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint\Utils;
6
-
7
- /**
8
- * Décode nativement un paquet TLS Client Hello binaire pour calculer l'empreinte JA3/JA4.
9
- */
10
- class TLSClientHelloParser
11
- {
12
- private const GREASE_VALUES = [
13
- 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
14
- 35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
- ];
16
-
17
- /**
18
- * Lit un entier encodé en longueur variable QUIC (RFC 9000 section 16).
19
- */
20
- public static function readVarInt(string $data, int &$offset): ?int
21
- {
22
- $len = strlen($data);
23
- if ($offset >= $len) {
24
- return null;
25
- }
26
-
27
- $first = ord($data[$offset]);
28
- $prefix = $first >> 6;
29
- $firstVal = $first & 0x3f;
30
-
31
- if ($prefix === 0) {
32
- $offset += 1;
33
- return $firstVal;
34
- } elseif ($prefix === 1) {
35
- if ($offset + 2 > $len) return null;
36
- $val = ($firstVal << 8) | ord($data[$offset + 1]);
37
- $offset += 2;
38
- return $val;
39
- } elseif ($prefix === 2) {
40
- if ($offset + 4 > $len) return null;
41
- $val = ($firstVal << 24) | (ord($data[$offset + 1]) << 16) | (ord($data[$offset + 2]) << 8) | ord($data[$offset + 3]);
42
- $offset += 4;
43
- return $val;
44
- } else {
45
- if ($offset + 8 > $len) return null;
46
- $val = $firstVal;
47
- for ($i = 1; $i < 8; $i++) {
48
- $val = ($val << 8) | ord($data[$offset + $i]);
49
- }
50
- $offset += 8;
51
- return $val;
52
- }
53
- }
54
-
55
- /**
56
- * Parse les paramètres de transport QUIC (RFC 9000 section 18.2).
57
- */
58
- public static function parseQuicTransportParameters(string $data): array
59
- {
60
- $params = [];
61
- $offset = 0;
62
- $len = strlen($data);
63
-
64
- while ($offset < $len) {
65
- $paramId = self::readVarInt($data, $offset);
66
- if ($paramId === null) break;
67
- $paramLen = self::readVarInt($data, $offset);
68
- if ($paramLen === null || $offset + $paramLen > $len) break;
69
-
70
- $paramValBytes = substr($data, $offset, $paramLen);
71
- $offset += $paramLen;
72
-
73
- if ($paramLen > 0 && in_array($paramId, [1, 3, 4, 5, 6, 7, 8, 9, 11, 14], true)) {
74
- $valOffset = 0;
75
- $numVal = self::readVarInt($paramValBytes, $valOffset);
76
- $params[$paramId] = $numVal ?? $paramValBytes;
77
- } else {
78
- $params[$paramId] = bin2hex($paramValBytes);
79
- }
80
- }
81
-
82
- return $params;
83
- }
84
-
85
- /**
86
- * Analyse l'ordre des trames de contrôle QUIC / HTTP/3 (SETTINGS, MAX_STREAMS, PRIORITY).
87
- */
88
- public static function parseQuicControlFrames(string $streamData): array
89
- {
90
- $frames = [];
91
- $frameOrder = [];
92
- $settings = [];
93
- $offset = 0;
94
- $len = strlen($streamData);
95
-
96
- if ($len === 0) {
97
- return ['frames' => [], 'frame_order' => '', 'settings' => []];
98
- }
99
-
100
- if (ord($streamData[0]) === 0x00) {
101
- $offset = 1; // Stream type Control Stream
102
- }
103
-
104
- while ($offset < $len) {
105
- $frameType = self::readVarInt($streamData, $offset);
106
- if ($frameType === null) break;
107
- $frameLen = self::readVarInt($streamData, $offset);
108
- if ($frameLen === null || $offset + $frameLen > $len) break;
109
-
110
- $payload = substr($streamData, $offset, $frameLen);
111
- $offset += $frameLen;
112
-
113
- $abbr = match ($frameType) {
114
- 0x04 => 's', // SETTINGS
115
- 0x12, 0x02 => 'm', // MAX_STREAMS
116
- 0x0f, 0xaf, 0xf0700 => 'p', // PRIORITY_UPDATE
117
- 0x10, 0x0d => 'd', // MAX_DATA
118
- 0x07 => 'g', // GOAWAY
119
- default => 'u'
120
- };
121
-
122
- $frames[] = ['type' => $frameType, 'length' => $frameLen];
123
- $frameOrder[] = $abbr;
124
-
125
- if ($frameType === 0x04) {
126
- $sOffset = 0;
127
- $sLen = strlen($payload);
128
- while ($sOffset < $sLen) {
129
- $sId = self::readVarInt($payload, $sOffset);
130
- if ($sId === null) break;
131
- $sVal = self::readVarInt($payload, $sOffset);
132
- if ($sVal === null) break;
133
- $settings[$sId] = $sVal;
134
- }
135
- }
136
- }
137
-
138
- return [
139
- 'frames' => $frames,
140
- 'frame_order' => implode(',', $frameOrder),
141
- 'settings' => $settings
142
- ];
143
- }
144
-
145
- public static function formatQuicFingerprint(array $params, string $priority = '', string $frameOrder = ''): string
146
- {
147
- $paramParts = [];
148
- foreach ($params as $k => $v) {
149
- $paramParts[] = "{$k}={$v}";
150
- }
151
- $fp = "1;" . implode(',', $paramParts);
152
- if ($priority !== '' || $frameOrder !== '') {
153
- $fp .= ";{$priority}";
154
- }
155
- if ($frameOrder !== '') {
156
- $fp .= ";{$frameOrder}";
157
- }
158
- return $fp;
159
- }
160
-
161
- /**
162
- * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
163
- *
164
- * @param string $binary Le premier paquet TCP reçu sur la socket.
165
- * @return array{ja3_string: string, ja3_hash: string, ja4_raw: string}|null
166
- */
167
- public static function parse(string $binary): ?array
168
- {
169
- $len = strlen($binary);
170
- if ($len < 43) {
171
- return null; // Paquet trop court
172
- }
173
-
174
- // 1. Vérification du Record Layer Type (0x16 = Handshake)
175
- if (ord($binary[0]) !== 0x16) {
176
- return null;
177
- }
178
-
179
- // 2. Vérification du Handshake Type (0x01 = Client Hello)
180
- if (ord($binary[5]) !== 0x01) {
181
- return null;
182
- }
183
-
184
- $offset = 43; // Sauter l'en-tête, la version et le Random Client (32 octets)
185
- if ($len < $offset + 1) return null;
186
-
187
- // 3. Lecture du Session ID
188
- $sessionLen = ord($binary[$offset]);
189
- $offset += 1 + $sessionLen;
190
- if ($len < $offset + 2) return null;
191
-
192
- // 4. Lecture des Cipher Suites
193
- $ciphersLen = unpack('n', substr($binary, $offset, 2))[1];
194
- $offset += 2;
195
- if ($len < $offset + $ciphersLen + 1) return null;
196
-
197
- $ciphers = [];
198
- for ($i = 0; $i < $ciphersLen; $i += 2) {
199
- $ciphers[] = unpack('n', substr($binary, $offset + $i, 2))[1];
200
- }
201
- $offset += $ciphersLen;
202
-
203
- // 5. Lecture des Compression Methods
204
- $compressionLen = ord($binary[$offset]);
205
- $offset += 1 + $compressionLen;
206
- if ($len < $offset + 2) return null;
207
-
208
- // 6. Lecture des Extensions
209
- $extensionsLen = unpack('n', substr($binary, $offset, 2))[1];
210
- $offset += 2;
211
-
212
- $extensions = [];
213
- $curves = [];
214
- $points = [];
215
- $sigAlgs = [];
216
- $supportedVersions = [];
217
- $hasSni = false;
218
- $alpnProtocol = '';
219
- $quicParams = null;
220
-
221
- $extLimit = $offset + $extensionsLen;
222
- while ($offset < $extLimit && $offset + 4 <= $len) {
223
- $extType = unpack('n', substr($binary, $offset, 2))[1];
224
- $extLen = unpack('n', substr($binary, $offset + 2, 2))[1];
225
- $offset += 4;
226
-
227
- if ($offset + $extLen > $len) break;
228
-
229
- $extensions[] = $extType;
230
-
231
- if ($extType === 0) {
232
- $hasSni = true;
233
- } elseif ($extType === 10) { // Extension Supported Groups (Elliptic Curves)
234
- if ($extLen >= 2) {
235
- $curvesLen = unpack('n', substr($binary, $offset, 2))[1];
236
- for ($j = 2; $j < $curvesLen + 2; $j += 2) {
237
- $curves[] = unpack('n', substr($binary, $offset + $j, 2))[1];
238
- }
239
- }
240
- } elseif ($extType === 11) { // Extension EC Point Formats
241
- if ($extLen >= 1) {
242
- $pointsLen = ord($binary[$offset]);
243
- for ($j = 1; $j < $pointsLen + 1; $j++) {
244
- $points[] = ord($binary[$offset + $j]);
245
- }
246
- }
247
- } elseif ($extType === 13) { // Signature Algorithms
248
- if ($extLen >= 2 && $offset + 2 <= $len) {
249
- $sigAlgsLen = unpack('n', substr($binary, $offset, 2))[1];
250
- for ($j = 2; $j < $sigAlgsLen + 2; $j += 2) {
251
- if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
252
- $sigAlgs[] = unpack('n', substr($binary, $offset + $j, 2))[1];
253
- }
254
- }
255
- }
256
- } elseif ($extType === 16) { // ALPN
257
- if ($extLen >= 3 && $offset + 2 <= $len) {
258
- $alpnListLen = unpack('n', substr($binary, $offset, 2))[1];
259
- if ($extLen >= 2 + $alpnListLen && $offset + 2 + $alpnListLen <= $len) {
260
- $alpnStrLen = ord($binary[$offset + 2]);
261
- if ($alpnListLen >= 1 + $alpnStrLen) {
262
- $alpnProtocol = substr($binary, $offset + 3, $alpnStrLen);
263
- }
264
- }
265
- }
266
- } elseif ($extType === 43) { // Supported Versions
267
- if ($extLen >= 1 && $offset + 1 <= $len) {
268
- $versionsLen = ord($binary[$offset]);
269
- for ($j = 1; $j < $versionsLen + 1; $j += 2) {
270
- if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
271
- $supportedVersions[] = unpack('n', substr($binary, $offset + $j, 2))[1];
272
- }
273
- }
274
- }
275
- } elseif ($extType === 57 || $extType === 0xffa5) { // quic_transport_parameters (RFC 9001 / draft)
276
- if ($extLen > 0 && $offset + $extLen <= $len) {
277
- $quicData = substr($binary, $offset, $extLen);
278
- $quicParams = self::parseQuicTransportParameters($quicData);
279
- }
280
- }
281
- $offset += $extLen;
282
- }
283
-
284
- // Nettoyage des valeurs GREASE (RFC 8701) pour la conformité JA3
285
- $filterGrease = fn(array $arr) => array_values(array_filter($arr, fn($v) => !in_array($v, self::GREASE_VALUES, true)));
286
-
287
- $cleanCiphers = $filterGrease($ciphers);
288
- $cleanExtensions = $filterGrease($extensions);
289
- $cleanCurves = $filterGrease($curves);
290
- $cleanPoints = $filterGrease($points);
291
- $cleanSigAlgs = $filterGrease($sigAlgs);
292
- $cleanSupportedVersions = $filterGrease($supportedVersions);
293
-
294
- $sslVersion = unpack('n', substr($binary, 9, 2))[1];
295
- $ja3String = implode(',', [
296
- $sslVersion,
297
- implode('-', $cleanCiphers),
298
- implode('-', $cleanExtensions),
299
- implode('-', $cleanCurves),
300
- implode('-', $cleanPoints)
301
- ]);
302
-
303
- // Calcul natif de JA4
304
- $highestVersion = $sslVersion;
305
- if (!empty($cleanSupportedVersions)) {
306
- $highestVersion = max($cleanSupportedVersions);
307
- }
308
-
309
- $ja4Version = "12";
310
- if ($highestVersion === 0x0304) {
311
- $ja4Version = "13";
312
- } elseif ($highestVersion === 0x0303) {
313
- $ja4Version = "12";
314
- } elseif ($highestVersion === 0x0302) {
315
- $ja4Version = "11";
316
- } elseif ($highestVersion === 0x0301) {
317
- $ja4Version = "10";
318
- }
319
-
320
- $sniStatus = $hasSni ? "d" : "i";
321
- $numCiphers = min(99, count($cleanCiphers));
322
- $numExtensions = min(99, count($cleanExtensions));
323
-
324
- $ja4Alpn = "00";
325
- if ($alpnProtocol !== '') {
326
- $lenAlpn = strlen($alpnProtocol);
327
- if ($lenAlpn === 1) {
328
- $ja4Alpn = $alpnProtocol . $alpnProtocol;
329
- } else {
330
- $ja4Alpn = $alpnProtocol[0] . $alpnProtocol[$lenAlpn - 1];
331
- }
332
- }
333
-
334
- $ja4_a = "t" . $ja4Version . $sniStatus . sprintf("%02d", $numCiphers) . sprintf("%02d", $numExtensions) . $ja4Alpn;
335
-
336
- $sortedCiphers = $cleanCiphers;
337
- sort($sortedCiphers);
338
- $ciphersHex = array_map(fn($c) => sprintf('%04x', $c), $sortedCiphers);
339
- $ciphersString = implode(',', $ciphersHex);
340
- $ja4_b = substr(hash('sha256', $ciphersString), 0, 12);
341
-
342
- $sortedExtensions = $cleanExtensions;
343
- sort($sortedExtensions);
344
- $extensionsHex = array_map(fn($e) => sprintf('%04x', $e), $sortedExtensions);
345
- $extensionsString = implode(',', $extensionsHex);
346
-
347
- $sortedSigAlgs = $cleanSigAlgs;
348
- sort($sortedSigAlgs);
349
- $sigAlgsHex = array_map(fn($s) => sprintf('%04x', $s), $sortedSigAlgs);
350
- $sigAlgsString = implode(',', $sigAlgsHex);
351
-
352
- $ja4_c_input = $extensionsString . '_' . $sigAlgsString;
353
- $ja4_c = substr(hash('sha256', $ja4_c_input), 0, 12);
354
-
355
- $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
356
-
357
- $result = [
358
- 'ja3_string' => $ja3String,
359
- 'ja3_hash' => md5($ja3String),
360
- 'ja4_raw' => $ja4_hash
361
- ];
362
-
363
- if ($quicParams !== null) {
364
- $result['quic_params'] = $quicParams;
365
- $result['quic_fp'] = self::formatQuicFingerprint($quicParams);
366
- }
367
-
368
- return $result;
369
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint\Utils;
6
+
7
+ /**
8
+ * Décode nativement un paquet TLS Client Hello binaire pour calculer l'empreinte JA3/JA4.
9
+ */
10
+ class TLSClientHelloParser
11
+ {
12
+ private const GREASE_VALUES = [
13
+ 2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
14
+ 35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
15
+ ];
16
+
17
+ /**
18
+ * Lit un entier encodé en longueur variable QUIC (RFC 9000 section 16).
19
+ */
20
+ public static function readVarInt(string $data, int &$offset): ?int
21
+ {
22
+ $len = strlen($data);
23
+ if ($offset >= $len) {
24
+ return null;
25
+ }
26
+
27
+ $first = ord($data[$offset]);
28
+ $prefix = $first >> 6;
29
+ $firstVal = $first & 0x3f;
30
+
31
+ if ($prefix === 0) {
32
+ $offset += 1;
33
+ return $firstVal;
34
+ } elseif ($prefix === 1) {
35
+ if ($offset + 2 > $len) return null;
36
+ $val = ($firstVal << 8) | ord($data[$offset + 1]);
37
+ $offset += 2;
38
+ return $val;
39
+ } elseif ($prefix === 2) {
40
+ if ($offset + 4 > $len) return null;
41
+ $val = ($firstVal << 24) | (ord($data[$offset + 1]) << 16) | (ord($data[$offset + 2]) << 8) | ord($data[$offset + 3]);
42
+ $offset += 4;
43
+ return $val;
44
+ } else {
45
+ if ($offset + 8 > $len) return null;
46
+ $val = $firstVal;
47
+ for ($i = 1; $i < 8; $i++) {
48
+ $val = ($val << 8) | ord($data[$offset + $i]);
49
+ }
50
+ $offset += 8;
51
+ return $val;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Parse les paramètres de transport QUIC (RFC 9000 section 18.2).
57
+ */
58
+ public static function parseQuicTransportParameters(string $data): array
59
+ {
60
+ $params = [];
61
+ $offset = 0;
62
+ $len = strlen($data);
63
+
64
+ while ($offset < $len) {
65
+ $paramId = self::readVarInt($data, $offset);
66
+ if ($paramId === null) break;
67
+ $paramLen = self::readVarInt($data, $offset);
68
+ if ($paramLen === null || $offset + $paramLen > $len) break;
69
+
70
+ $paramValBytes = substr($data, $offset, $paramLen);
71
+ $offset += $paramLen;
72
+
73
+ if ($paramLen > 0 && in_array($paramId, [1, 3, 4, 5, 6, 7, 8, 9, 11, 14], true)) {
74
+ $valOffset = 0;
75
+ $numVal = self::readVarInt($paramValBytes, $valOffset);
76
+ $params[$paramId] = $numVal ?? $paramValBytes;
77
+ } else {
78
+ $params[$paramId] = bin2hex($paramValBytes);
79
+ }
80
+ }
81
+
82
+ return $params;
83
+ }
84
+
85
+ /**
86
+ * Analyse l'ordre des trames de contrôle QUIC / HTTP/3 (SETTINGS, MAX_STREAMS, PRIORITY).
87
+ */
88
+ public static function parseQuicControlFrames(string $streamData): array
89
+ {
90
+ $frames = [];
91
+ $frameOrder = [];
92
+ $settings = [];
93
+ $offset = 0;
94
+ $len = strlen($streamData);
95
+
96
+ if ($len === 0) {
97
+ return ['frames' => [], 'frame_order' => '', 'settings' => []];
98
+ }
99
+
100
+ if (ord($streamData[0]) === 0x00) {
101
+ $offset = 1; // Stream type Control Stream
102
+ }
103
+
104
+ while ($offset < $len) {
105
+ $frameType = self::readVarInt($streamData, $offset);
106
+ if ($frameType === null) break;
107
+ $frameLen = self::readVarInt($streamData, $offset);
108
+ if ($frameLen === null || $offset + $frameLen > $len) break;
109
+
110
+ $payload = substr($streamData, $offset, $frameLen);
111
+ $offset += $frameLen;
112
+
113
+ $abbr = match ($frameType) {
114
+ 0x04 => 's', // SETTINGS
115
+ 0x12, 0x02 => 'm', // MAX_STREAMS
116
+ 0x0f, 0xaf, 0xf0700 => 'p', // PRIORITY_UPDATE
117
+ 0x10, 0x0d => 'd', // MAX_DATA
118
+ 0x07 => 'g', // GOAWAY
119
+ default => 'u'
120
+ };
121
+
122
+ $frames[] = ['type' => $frameType, 'length' => $frameLen];
123
+ $frameOrder[] = $abbr;
124
+
125
+ if ($frameType === 0x04) {
126
+ $sOffset = 0;
127
+ $sLen = strlen($payload);
128
+ while ($sOffset < $sLen) {
129
+ $sId = self::readVarInt($payload, $sOffset);
130
+ if ($sId === null) break;
131
+ $sVal = self::readVarInt($payload, $sOffset);
132
+ if ($sVal === null) break;
133
+ $settings[$sId] = $sVal;
134
+ }
135
+ }
136
+ }
137
+
138
+ return [
139
+ 'frames' => $frames,
140
+ 'frame_order' => implode(',', $frameOrder),
141
+ 'settings' => $settings
142
+ ];
143
+ }
144
+
145
+ public static function formatQuicFingerprint(array $params, string $priority = '', string $frameOrder = ''): string
146
+ {
147
+ $paramParts = [];
148
+ foreach ($params as $k => $v) {
149
+ $paramParts[] = "{$k}={$v}";
150
+ }
151
+ $fp = "1;" . implode(',', $paramParts);
152
+ if ($priority !== '' || $frameOrder !== '') {
153
+ $fp .= ";{$priority}";
154
+ }
155
+ if ($frameOrder !== '') {
156
+ $fp .= ";{$frameOrder}";
157
+ }
158
+ return $fp;
159
+ }
160
+
161
+ /**
162
+ * Parse le Client Hello brut et retourne l'empreinte JA3 et une approximation JA4.
163
+ *
164
+ * @param string $binary Le premier paquet TCP reçu sur la socket.
165
+ * @return array{ja3_string: string, ja3_hash: string, ja4_raw: string}|null
166
+ */
167
+ public static function parse(string $binary): ?array
168
+ {
169
+ $len = strlen($binary);
170
+ if ($len < 43) {
171
+ return null; // Paquet trop court
172
+ }
173
+
174
+ // 1. Vérification du Record Layer Type (0x16 = Handshake)
175
+ if (ord($binary[0]) !== 0x16) {
176
+ return null;
177
+ }
178
+
179
+ // 2. Vérification du Handshake Type (0x01 = Client Hello)
180
+ if (ord($binary[5]) !== 0x01) {
181
+ return null;
182
+ }
183
+
184
+ $offset = 43; // Sauter l'en-tête, la version et le Random Client (32 octets)
185
+ if ($len < $offset + 1) return null;
186
+
187
+ // 3. Lecture du Session ID
188
+ $sessionLen = ord($binary[$offset]);
189
+ $offset += 1 + $sessionLen;
190
+ if ($len < $offset + 2) return null;
191
+
192
+ // 4. Lecture des Cipher Suites
193
+ $ciphersLen = unpack('n', substr($binary, $offset, 2))[1];
194
+ $offset += 2;
195
+ if ($len < $offset + $ciphersLen + 1) return null;
196
+
197
+ $ciphers = [];
198
+ for ($i = 0; $i < $ciphersLen; $i += 2) {
199
+ $ciphers[] = unpack('n', substr($binary, $offset + $i, 2))[1];
200
+ }
201
+ $offset += $ciphersLen;
202
+
203
+ // 5. Lecture des Compression Methods
204
+ $compressionLen = ord($binary[$offset]);
205
+ $offset += 1 + $compressionLen;
206
+ if ($len < $offset + 2) return null;
207
+
208
+ // 6. Lecture des Extensions
209
+ $extensionsLen = unpack('n', substr($binary, $offset, 2))[1];
210
+ $offset += 2;
211
+
212
+ $extensions = [];
213
+ $curves = [];
214
+ $points = [];
215
+ $sigAlgs = [];
216
+ $supportedVersions = [];
217
+ $hasSni = false;
218
+ $alpnProtocol = '';
219
+ $quicParams = null;
220
+
221
+ $extLimit = $offset + $extensionsLen;
222
+ while ($offset < $extLimit && $offset + 4 <= $len) {
223
+ $extType = unpack('n', substr($binary, $offset, 2))[1];
224
+ $extLen = unpack('n', substr($binary, $offset + 2, 2))[1];
225
+ $offset += 4;
226
+
227
+ if ($offset + $extLen > $len) break;
228
+
229
+ $extensions[] = $extType;
230
+
231
+ if ($extType === 0) {
232
+ $hasSni = true;
233
+ } elseif ($extType === 10) { // Extension Supported Groups (Elliptic Curves)
234
+ if ($extLen >= 2) {
235
+ $curvesLen = unpack('n', substr($binary, $offset, 2))[1];
236
+ for ($j = 2; $j < $curvesLen + 2; $j += 2) {
237
+ $curves[] = unpack('n', substr($binary, $offset + $j, 2))[1];
238
+ }
239
+ }
240
+ } elseif ($extType === 11) { // Extension EC Point Formats
241
+ if ($extLen >= 1) {
242
+ $pointsLen = ord($binary[$offset]);
243
+ for ($j = 1; $j < $pointsLen + 1; $j++) {
244
+ $points[] = ord($binary[$offset + $j]);
245
+ }
246
+ }
247
+ } elseif ($extType === 13) { // Signature Algorithms
248
+ if ($extLen >= 2 && $offset + 2 <= $len) {
249
+ $sigAlgsLen = unpack('n', substr($binary, $offset, 2))[1];
250
+ for ($j = 2; $j < $sigAlgsLen + 2; $j += 2) {
251
+ if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
252
+ $sigAlgs[] = unpack('n', substr($binary, $offset + $j, 2))[1];
253
+ }
254
+ }
255
+ }
256
+ } elseif ($extType === 16) { // ALPN
257
+ if ($extLen >= 3 && $offset + 2 <= $len) {
258
+ $alpnListLen = unpack('n', substr($binary, $offset, 2))[1];
259
+ if ($extLen >= 2 + $alpnListLen && $offset + 2 + $alpnListLen <= $len) {
260
+ $alpnStrLen = ord($binary[$offset + 2]);
261
+ if ($alpnListLen >= 1 + $alpnStrLen) {
262
+ $alpnProtocol = substr($binary, $offset + 3, $alpnStrLen);
263
+ }
264
+ }
265
+ }
266
+ } elseif ($extType === 43) { // Supported Versions
267
+ if ($extLen >= 1 && $offset + 1 <= $len) {
268
+ $versionsLen = ord($binary[$offset]);
269
+ for ($j = 1; $j < $versionsLen + 1; $j += 2) {
270
+ if ($offset + $j + 2 <= $len && $j + 2 <= $extLen) {
271
+ $supportedVersions[] = unpack('n', substr($binary, $offset + $j, 2))[1];
272
+ }
273
+ }
274
+ }
275
+ } elseif ($extType === 57 || $extType === 0xffa5) { // quic_transport_parameters (RFC 9001 / draft)
276
+ if ($extLen > 0 && $offset + $extLen <= $len) {
277
+ $quicData = substr($binary, $offset, $extLen);
278
+ $quicParams = self::parseQuicTransportParameters($quicData);
279
+ }
280
+ }
281
+ $offset += $extLen;
282
+ }
283
+
284
+ // Nettoyage des valeurs GREASE (RFC 8701) pour la conformité JA3
285
+ $filterGrease = fn(array $arr) => array_values(array_filter($arr, fn($v) => !in_array($v, self::GREASE_VALUES, true)));
286
+
287
+ $cleanCiphers = $filterGrease($ciphers);
288
+ $cleanExtensions = $filterGrease($extensions);
289
+ $cleanCurves = $filterGrease($curves);
290
+ $cleanPoints = $filterGrease($points);
291
+ $cleanSigAlgs = $filterGrease($sigAlgs);
292
+ $cleanSupportedVersions = $filterGrease($supportedVersions);
293
+
294
+ $sslVersion = unpack('n', substr($binary, 9, 2))[1];
295
+ $ja3String = implode(',', [
296
+ $sslVersion,
297
+ implode('-', $cleanCiphers),
298
+ implode('-', $cleanExtensions),
299
+ implode('-', $cleanCurves),
300
+ implode('-', $cleanPoints)
301
+ ]);
302
+
303
+ // Calcul natif de JA4
304
+ $highestVersion = $sslVersion;
305
+ if (!empty($cleanSupportedVersions)) {
306
+ $highestVersion = max($cleanSupportedVersions);
307
+ }
308
+
309
+ $ja4Version = "12";
310
+ if ($highestVersion === 0x0304) {
311
+ $ja4Version = "13";
312
+ } elseif ($highestVersion === 0x0303) {
313
+ $ja4Version = "12";
314
+ } elseif ($highestVersion === 0x0302) {
315
+ $ja4Version = "11";
316
+ } elseif ($highestVersion === 0x0301) {
317
+ $ja4Version = "10";
318
+ }
319
+
320
+ $sniStatus = $hasSni ? "d" : "i";
321
+ $numCiphers = min(99, count($cleanCiphers));
322
+ $numExtensions = min(99, count($cleanExtensions));
323
+
324
+ $ja4Alpn = "00";
325
+ if ($alpnProtocol !== '') {
326
+ $lenAlpn = strlen($alpnProtocol);
327
+ if ($lenAlpn === 1) {
328
+ $ja4Alpn = $alpnProtocol . $alpnProtocol;
329
+ } else {
330
+ $ja4Alpn = $alpnProtocol[0] . $alpnProtocol[$lenAlpn - 1];
331
+ }
332
+ }
333
+
334
+ $ja4_a = "t" . $ja4Version . $sniStatus . sprintf("%02d", $numCiphers) . sprintf("%02d", $numExtensions) . $ja4Alpn;
335
+
336
+ $sortedCiphers = $cleanCiphers;
337
+ sort($sortedCiphers);
338
+ $ciphersHex = array_map(fn($c) => sprintf('%04x', $c), $sortedCiphers);
339
+ $ciphersString = implode(',', $ciphersHex);
340
+ $ja4_b = substr(hash('sha256', $ciphersString), 0, 12);
341
+
342
+ $sortedExtensions = $cleanExtensions;
343
+ sort($sortedExtensions);
344
+ $extensionsHex = array_map(fn($e) => sprintf('%04x', $e), $sortedExtensions);
345
+ $extensionsString = implode(',', $extensionsHex);
346
+
347
+ $sortedSigAlgs = $cleanSigAlgs;
348
+ sort($sortedSigAlgs);
349
+ $sigAlgsHex = array_map(fn($s) => sprintf('%04x', $s), $sortedSigAlgs);
350
+ $sigAlgsString = implode(',', $sigAlgsHex);
351
+
352
+ $ja4_c_input = $extensionsString . '_' . $sigAlgsString;
353
+ $ja4_c = substr(hash('sha256', $ja4_c_input), 0, 12);
354
+
355
+ $ja4_hash = $ja4_a . "_" . $ja4_b . "_" . $ja4_c;
356
+
357
+ $result = [
358
+ 'ja3_string' => $ja3String,
359
+ 'ja3_hash' => md5($ja3String),
360
+ 'ja4_raw' => $ja4_hash
361
+ ];
362
+
363
+ if ($quicParams !== null) {
364
+ $result['quic_params'] = $quicParams;
365
+ $result['quic_fp'] = self::formatQuicFingerprint($quicParams);
366
+ }
367
+
368
+ return $result;
369
+ }
370
370
  }