@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.
@@ -15,9 +15,13 @@ use Anonympins\Fingerprint\Utils\RequestUtils;
15
15
  */
16
16
  class ChallengeUtils
17
17
  {
18
+ /** @var array<string, float> Cache local des floats convertis pour éviter les appels système pack/unpack */
19
+ private static array $froundCache = [];
20
+
18
21
  private static function fround(float $value): float
19
22
  {
20
- return unpack('f', pack('f', $value))[1];
23
+ $key = (string)$value;
24
+ return self::$froundCache[$key] ?? (self::$froundCache[$key] = unpack('f', pack('f', $value))[1]);
21
25
  }
22
26
 
23
27
  public static function hashSeedToFloat(string $seed): float
@@ -261,6 +265,15 @@ class ChallengeUtils
261
265
  case 'register':
262
266
  $expectedMsg = "{$clientSecret}:register:{$nodeId}:" . ($params['seed'] ?? '');
263
267
  break;
268
+ case 'find_peer':
269
+ $expectedMsg = "{$clientSecret}:find_peer:{$nodeId}";
270
+ break;
271
+ case 'webrtc_signal':
272
+ $expectedMsg = "{$clientSecret}:webrtc_signal:{$nodeId}:" . ($params['target_peer_id'] ?? '') . ":" . ($params['signal_type'] ?? '') . ":" . ($params['signal_data'] ?? '');
273
+ break;
274
+ case 'poll_signals':
275
+ $expectedMsg = "{$clientSecret}:poll_signals:{$nodeId}";
276
+ break;
264
277
  case 'request_peer_block':
265
278
  $expectedMsg = "{$clientSecret}:request_peer_block:{$nodeId}:" . ($params['peer_id'] ?? '') . ":" . ($params['block_idx'] ?? '0') . ":" . ($params['req_id'] ?? '');
266
279
  break;
@@ -290,6 +303,38 @@ class ChallengeUtils
290
303
  self::registerCooperativeNode($clientIp, $nodeId, $seed);
291
304
  return ['status' => 'registered'];
292
305
 
306
+ case 'find_peer':
307
+ $peer = self::findPeerInSubnet($clientIp, $nodeId);
308
+ if ($peer !== null) {
309
+ return ['status' => 'peer_found', 'peer_id' => $peer['nodeId'], 'seed' => $peer['seed']];
310
+ }
311
+ return ['status' => 'no_peers'];
312
+
313
+ case 'webrtc_signal':
314
+ $targetPeerId = $params['target_peer_id'] ?? '';
315
+ $signalType = $params['signal_type'] ?? '';
316
+ $signalData = $params['signal_data'] ?? '';
317
+ if (empty($targetPeerId) || empty($signalType) || empty($signalData)) {
318
+ return ['error' => 'Invalid parameters'];
319
+ }
320
+ $signalQueueKey = "coop-webrtc:signals:{$targetPeerId}";
321
+ $signals = $store->get($signalQueueKey) ?? [];
322
+ $signals[] = [
323
+ 'from_peer_id' => $nodeId,
324
+ 'signal_type' => $signalType,
325
+ 'signal_data' => $signalData
326
+ ];
327
+ $store->set($signalQueueKey, $signals, 30);
328
+ return ['status' => 'signal_queued'];
329
+
330
+ case 'poll_signals':
331
+ $pollSignalKey = "coop-webrtc:signals:{$nodeId}";
332
+ $signals = $store->get($pollSignalKey) ?? [];
333
+ if (!empty($signals)) {
334
+ $store->delete($pollSignalKey);
335
+ }
336
+ return ['status' => 'ok', 'signals' => $signals];
337
+
293
338
  case 'request_peer_block':
294
339
  $peerId = $params['peer_id'] ?? '';
295
340
  $blockIdx = (int)($params['block_idx'] ?? 0);
@@ -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 requêtes entrantes de nos pairs suspects
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
- }, 1000);
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 = '📥 Téléchargement du bloc de validation du pair (' + peerId + ')...';
1036
- const reqId = Math.random().toString(36).substring(2);
1037
- const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1038
- await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
1039
-
1040
- let attempts = 0;
1041
- while (attempts < coopTimeout) {
1042
- const pollSig = await signCoop("poll_response", nodeId, reqId);
1043
- const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1044
- const data = await res.json();
1045
- if (data.status === 'ready') {
1046
- peerBlock = data.block_data;
1047
- break;
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
  }