@anonympins/fingerprint 0.6.6 → 0.7.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 +32 -0
- package/README.md +11 -2
- package/package.json +1 -1
- package/public/fp.js +1 -1
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +1 -1
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.client.js +1463 -1447
- package/src/js/fingerprint.js +386 -94
- package/src/js/library.js +15 -8
- package/src/js/pow.solver.inline.js +56 -10
- package/src/js/pow.solver.js +56 -10
- package/src/js/pow.worker.js +35 -13
- package/src/js/problem-manager.js +10 -3
- package/src/js/tests/fingerprint.engine.test.js +25 -0
- package/src/js/tests/fingerprint.test.js +69 -35
- package/src/php/Challenge/ChallengeUtils.php +149 -23
- package/src/php/Config/SecurityProfiles.php +1 -0
- package/src/php/FingerprintClient.php +148 -148
- package/src/php/FingerprintEngine.php +39 -84
- package/src/php/Ja3AnomalyDetector.php +257 -227
- package/src/php/Optimization/OptimizationOperators.php +2 -2
- package/src/php/ProblemManager.php +83 -0
- package/src/php/Tests/FingerprintEngineTest.php +2 -33
- package/src/php/Utils/RequestUtils.php +114 -11
- package/src/php/Utils/TLSClientHelloParser.php +158 -1
- package/src/js/fingerprint.client.obfuscated.js +0 -1
|
@@ -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
|
-
|
|
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);
|
|
@@ -998,6 +1043,11 @@ class ChallengeUtils
|
|
|
998
1043
|
return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
999
1044
|
}
|
|
1000
1045
|
|
|
1046
|
+
async function sendWebRtcSignal(targetId, type, data) {
|
|
1047
|
+
const sig = await signCoop("webrtc_signal", nodeId, targetId + ":" + type + ":" + data);
|
|
1048
|
+
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);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1001
1051
|
document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
|
|
1002
1052
|
await new Promise(r => setTimeout(r, 10));
|
|
1003
1053
|
|
|
@@ -1009,9 +1059,50 @@ class ChallengeUtils
|
|
|
1009
1059
|
const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
|
|
1010
1060
|
await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
|
|
1011
1061
|
}
|
|
1062
|
+
|
|
1063
|
+
const peerConnections = {};
|
|
1012
1064
|
|
|
1013
|
-
// Écoute des
|
|
1065
|
+
// Écoute des signaux WebRTC et requêtes entrantes
|
|
1014
1066
|
setInterval(async () => {
|
|
1067
|
+
try {
|
|
1068
|
+
const sigWebrtc = await signCoop("poll_signals", nodeId);
|
|
1069
|
+
const resWebrtc = await fetch(window.location.pathname + "?coop_op=poll_signals&node_id=" + nodeId + "&coop_sig=" + sigWebrtc);
|
|
1070
|
+
const dataWebrtc = await resWebrtc.json();
|
|
1071
|
+
if (dataWebrtc.signals && dataWebrtc.signals.length > 0) {
|
|
1072
|
+
for (const sig of dataWebrtc.signals) {
|
|
1073
|
+
const fromId = sig.from_peer_id;
|
|
1074
|
+
if (sig.signal_type === 'offer') {
|
|
1075
|
+
const pc = new RTCPeerConnection({ iceServers: [] });
|
|
1076
|
+
peerConnections[fromId] = pc;
|
|
1077
|
+
pc.onicecandidate = (e) => {
|
|
1078
|
+
if (e.candidate) sendWebRtcSignal(fromId, 'candidate', JSON.stringify(e.candidate));
|
|
1079
|
+
};
|
|
1080
|
+
pc.ondatachannel = (e) => {
|
|
1081
|
+
const dc = e.channel;
|
|
1082
|
+
dc.onmessage = async (evt) => {
|
|
1083
|
+
try {
|
|
1084
|
+
const req = JSON.parse(evt.data);
|
|
1085
|
+
if (req.type === 'get_block') {
|
|
1086
|
+
document.getElementById('loader').innerText = '📤 Transfert direct P2P (WebRTC) du bloc vers le pair...';
|
|
1087
|
+
const blockData = await window.readSpaceBlock(req.block_idx);
|
|
1088
|
+
dc.send(JSON.stringify({ type: 'block_data', block_data: blockData }));
|
|
1089
|
+
}
|
|
1090
|
+
} catch (err) {}
|
|
1091
|
+
};
|
|
1092
|
+
};
|
|
1093
|
+
await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
|
|
1094
|
+
const answer = await pc.createAnswer();
|
|
1095
|
+
await pc.setLocalDescription(answer);
|
|
1096
|
+
await sendWebRtcSignal(fromId, 'answer', JSON.stringify(answer));
|
|
1097
|
+
} else if (sig.signal_type === 'answer' && peerConnections[fromId]) {
|
|
1098
|
+
await peerConnections[fromId].setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
|
|
1099
|
+
} else if (sig.signal_type === 'candidate' && peerConnections[fromId]) {
|
|
1100
|
+
await peerConnections[fromId].addIceCandidate(new RTCIceCandidate(JSON.parse(sig.signal_data)));
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
} catch (e) {}
|
|
1105
|
+
|
|
1015
1106
|
try {
|
|
1016
1107
|
const sig = await signCoop("poll_requests", nodeId);
|
|
1017
1108
|
const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
|
|
@@ -1027,30 +1118,65 @@ class ChallengeUtils
|
|
|
1027
1118
|
} catch (e) {
|
|
1028
1119
|
console.error("Cooperative polling error", e);
|
|
1029
1120
|
}
|
|
1030
|
-
},
|
|
1121
|
+
}, 800);
|
|
1031
1122
|
|
|
1032
|
-
// Téléchargement du bloc du pair si configuré
|
|
1033
1123
|
let peerBlock = "";
|
|
1034
1124
|
if (peerId && peerBlockIdx !== -1) {
|
|
1035
|
-
document.getElementById('loader').innerText = '📥
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1125
|
+
document.getElementById('loader').innerText = '📥 Connexion WebRTC P2P directe au pair (' + peerId + ')...';
|
|
1126
|
+
|
|
1127
|
+
// 1. Échange direct P2P via WebRTC DataChannel (charge serveur = 0)
|
|
1128
|
+
const webrtcTransferPromise = new Promise(async (resolve) => {
|
|
1129
|
+
if (!window.RTCPeerConnection) return resolve(null);
|
|
1130
|
+
try {
|
|
1131
|
+
const pc = new RTCPeerConnection({ iceServers: [] });
|
|
1132
|
+
peerConnections[peerId] = pc;
|
|
1133
|
+
const dc = pc.createDataChannel("pospace-transfer");
|
|
1134
|
+
pc.onicecandidate = (e) => {
|
|
1135
|
+
if (e.candidate) sendWebRtcSignal(peerId, 'candidate', JSON.stringify(e.candidate));
|
|
1136
|
+
};
|
|
1137
|
+
dc.onopen = () => {
|
|
1138
|
+
dc.send(JSON.stringify({ type: 'get_block', block_idx: peerBlockIdx }));
|
|
1139
|
+
};
|
|
1140
|
+
dc.onmessage = (e) => {
|
|
1141
|
+
try {
|
|
1142
|
+
const msg = JSON.parse(e.data);
|
|
1143
|
+
if (msg.type === 'block_data' && msg.block_data) {
|
|
1144
|
+
resolve(msg.block_data);
|
|
1145
|
+
}
|
|
1146
|
+
} catch (err) {}
|
|
1147
|
+
};
|
|
1148
|
+
const offer = await pc.createOffer();
|
|
1149
|
+
await pc.setLocalDescription(offer);
|
|
1150
|
+
await sendWebRtcSignal(peerId, 'offer', JSON.stringify(offer));
|
|
1151
|
+
} catch (err) {
|
|
1152
|
+
resolve(null);
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
const webrtcTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 5000));
|
|
1157
|
+
peerBlock = await Promise.race([webrtcTransferPromise, webrtcTimeoutPromise]);
|
|
1158
|
+
|
|
1159
|
+
if (peerBlock) {
|
|
1160
|
+
document.getElementById('loader').innerText = '⚡ Bloc reçu en direct via WebRTC P2P sans transit serveur !';
|
|
1161
|
+
} else {
|
|
1162
|
+
// 2. Repli vers le relais HTTP si WebRTC échoue
|
|
1163
|
+
document.getElementById('loader').innerText = '⚠️ WebRTC indisponible. Téléchargement via relais HTTP...';
|
|
1164
|
+
const reqId = Math.random().toString(36).substring(2);
|
|
1165
|
+
const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
|
|
1166
|
+
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);
|
|
1167
|
+
|
|
1168
|
+
let attempts = 0;
|
|
1169
|
+
while (attempts < coopTimeout) {
|
|
1170
|
+
const pollSig = await signCoop("poll_response", nodeId, reqId);
|
|
1171
|
+
const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
|
|
1172
|
+
const data = await res.json();
|
|
1173
|
+
if (data.status === 'ready') {
|
|
1174
|
+
peerBlock = data.block_data;
|
|
1175
|
+
break;
|
|
1176
|
+
}
|
|
1177
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
1178
|
+
attempts++;
|
|
1048
1179
|
}
|
|
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
1180
|
}
|
|
1055
1181
|
}
|
|
1056
1182
|
|
|
@@ -1134,7 +1260,7 @@ JS;
|
|
|
1134
1260
|
}
|
|
1135
1261
|
let memSolution = 0;
|
|
1136
1262
|
try {
|
|
1137
|
-
const memSeed = nonce + ":" + clientSecret;
|
|
1263
|
+
const memSeed = ":" + nonce + ":" + clientSecret;
|
|
1138
1264
|
memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
|
|
1139
1265
|
} catch(e) {
|
|
1140
1266
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
@@ -44,6 +44,7 @@ class SecurityProfiles
|
|
|
44
44
|
'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
45
45
|
'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
|
|
46
46
|
'ipReputationScore' => 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
47
|
+
'virtualizationScore' => 0.8,
|
|
47
48
|
],
|
|
48
49
|
'thresholds' => ['low' => 20, 'medium' => 45, 'high' => 75, 'block' => 95],
|
|
49
50
|
'patterns' => [
|
|
@@ -1,149 +1,149 @@
|
|
|
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 polymorphiques pour cacher le champ de manière robuste.
|
|
81
|
-
$styleOptions = [
|
|
82
|
-
'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;',
|
|
83
|
-
'position:fixed; left:-8888px; top:-8888px; width:0; height:0; overflow:hidden; opacity:0; pointer-events:none;',
|
|
84
|
-
'display:none; visibility:hidden; pointer-events:none;'
|
|
85
|
-
];
|
|
86
|
-
$styles = $styleOptions[array_rand($styleOptions)];
|
|
87
|
-
|
|
88
|
-
$containerTags = ['div', 'span', 'p', 'section'];
|
|
89
|
-
$tag = $containerTags[array_rand($containerTags)];
|
|
90
|
-
|
|
91
|
-
$nestingType = rand(0, 1);
|
|
92
|
-
if ($nestingType === 1) {
|
|
93
|
-
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
94
|
-
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
|
|
95
|
-
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
96
|
-
. '</label>'
|
|
97
|
-
. '</' . $tag . '>';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
101
|
-
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName) . '</label>'
|
|
102
|
-
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
103
|
-
. '</' . $tag . '>';
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
108
|
-
*
|
|
109
|
-
* @return string Le code HTML des balises <script>.
|
|
110
|
-
*/
|
|
111
|
-
public function getScriptTag(): string
|
|
112
|
-
{
|
|
113
|
-
$configJson = json_encode($this->clientConfig);
|
|
114
|
-
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
115
|
-
|
|
116
|
-
// Le script d'initialisation qui sera inclus dans la page.
|
|
117
|
-
$initScript = <<<JS
|
|
118
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
119
|
-
const config = {$configJson};
|
|
120
|
-
if (window.ClientLibrary) {
|
|
121
|
-
if (config.wasmPath) {
|
|
122
|
-
const wasmScript = document.createElement('script');
|
|
123
|
-
wasmScript.src = config.wasmPath;
|
|
124
|
-
wasmScript.async = true;
|
|
125
|
-
wasmScript.nonce = '{$this->nonce}';
|
|
126
|
-
document.head.appendChild(wasmScript);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
window.ClientLibrary.initializeClient(config);
|
|
130
|
-
} else {
|
|
131
|
-
console.error('Fingerprint client library not loaded.');
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
JS;
|
|
135
|
-
|
|
136
|
-
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
137
|
-
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
138
|
-
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
143
|
-
* @return string|null
|
|
144
|
-
*/
|
|
145
|
-
public function getNonce(): ?string
|
|
146
|
-
{
|
|
147
|
-
return $this->nonce;
|
|
148
|
-
}
|
|
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 polymorphiques pour cacher le champ de manière robuste.
|
|
81
|
+
$styleOptions = [
|
|
82
|
+
'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;',
|
|
83
|
+
'position:fixed; left:-8888px; top:-8888px; width:0; height:0; overflow:hidden; opacity:0; pointer-events:none;',
|
|
84
|
+
'display:none; visibility:hidden; pointer-events:none;'
|
|
85
|
+
];
|
|
86
|
+
$styles = $styleOptions[array_rand($styleOptions)];
|
|
87
|
+
|
|
88
|
+
$containerTags = ['div', 'span', 'p', 'section'];
|
|
89
|
+
$tag = $containerTags[array_rand($containerTags)];
|
|
90
|
+
|
|
91
|
+
$nestingType = rand(0, 1);
|
|
92
|
+
if ($nestingType === 1) {
|
|
93
|
+
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
94
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
|
|
95
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
96
|
+
. '</label>'
|
|
97
|
+
. '</' . $tag . '>';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
101
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName) . '</label>'
|
|
102
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
103
|
+
. '</' . $tag . '>';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Génère le bloc de script complet à inclure dans une page HTML.
|
|
108
|
+
*
|
|
109
|
+
* @return string Le code HTML des balises <script>.
|
|
110
|
+
*/
|
|
111
|
+
public function getScriptTag(): string
|
|
112
|
+
{
|
|
113
|
+
$configJson = json_encode($this->clientConfig);
|
|
114
|
+
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
115
|
+
|
|
116
|
+
// Le script d'initialisation qui sera inclus dans la page.
|
|
117
|
+
$initScript = <<<JS
|
|
118
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
119
|
+
const config = {$configJson};
|
|
120
|
+
if (window.ClientLibrary) {
|
|
121
|
+
if (config.wasmPath) {
|
|
122
|
+
const wasmScript = document.createElement('script');
|
|
123
|
+
wasmScript.src = config.wasmPath;
|
|
124
|
+
wasmScript.async = true;
|
|
125
|
+
wasmScript.nonce = '{$this->nonce}';
|
|
126
|
+
document.head.appendChild(wasmScript);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
window.ClientLibrary.initializeClient(config);
|
|
130
|
+
} else {
|
|
131
|
+
console.error('Fingerprint client library not loaded.');
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
JS;
|
|
135
|
+
|
|
136
|
+
// On combine le chargement de la bibliothèque et le script d'initialisation.
|
|
137
|
+
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
138
|
+
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Retourne le nonce généré pour pouvoir l'utiliser dans les en-têtes CSP.
|
|
143
|
+
* @return string|null
|
|
144
|
+
*/
|
|
145
|
+
public function getNonce(): ?string
|
|
146
|
+
{
|
|
147
|
+
return $this->nonce;
|
|
148
|
+
}
|
|
149
149
|
}
|