@cloudglides/veil 1.0.0 → 1.0.1

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 (58) hide show
  1. package/README.md +190 -0
  2. package/dist/index.d.ts +103 -0
  3. package/dist/index.js +1000 -0
  4. package/dist/veil_core.js +370 -0
  5. package/dist/veil_core_bg.wasm +0 -0
  6. package/package.json +24 -10
  7. package/.envrc +0 -1
  8. package/.github/workflows/build-on-tag.yml +0 -75
  9. package/example/index.html +0 -226
  10. package/flake.nix +0 -68
  11. package/scripts/patch-wasm.js +0 -12
  12. package/src/cpu.ts +0 -67
  13. package/src/entropy/adblock.ts +0 -16
  14. package/src/entropy/approximate.ts +0 -8
  15. package/src/entropy/audio.ts +0 -17
  16. package/src/entropy/battery.ts +0 -10
  17. package/src/entropy/browser.ts +0 -7
  18. package/src/entropy/canvas.ts +0 -17
  19. package/src/entropy/complexity.ts +0 -13
  20. package/src/entropy/connection.ts +0 -14
  21. package/src/entropy/distribution.ts +0 -8
  22. package/src/entropy/fonts.ts +0 -4
  23. package/src/entropy/hardware.ts +0 -10
  24. package/src/entropy/language.ts +0 -14
  25. package/src/entropy/os.ts +0 -12
  26. package/src/entropy/osVersion.ts +0 -6
  27. package/src/entropy/performance.ts +0 -14
  28. package/src/entropy/permissions.ts +0 -15
  29. package/src/entropy/plugins.ts +0 -14
  30. package/src/entropy/preferences.ts +0 -12
  31. package/src/entropy/probabilistic.ts +0 -20
  32. package/src/entropy/screen.ts +0 -12
  33. package/src/entropy/screenInfo.ts +0 -8
  34. package/src/entropy/spectral.ts +0 -8
  35. package/src/entropy/statistical.ts +0 -15
  36. package/src/entropy/storage.ts +0 -22
  37. package/src/entropy/timezone.ts +0 -10
  38. package/src/entropy/userAgent.ts +0 -16
  39. package/src/entropy/webFeatures.ts +0 -21
  40. package/src/entropy/webgl.ts +0 -11
  41. package/src/gpu.ts +0 -132
  42. package/src/index.test.ts +0 -26
  43. package/src/index.ts +0 -198
  44. package/src/normalize/index.ts +0 -31
  45. package/src/probability.ts +0 -11
  46. package/src/scoring.ts +0 -106
  47. package/src/seeded-rng.ts +0 -14
  48. package/src/types/index.ts +0 -11
  49. package/src/types.ts +0 -47
  50. package/src/veil_core.d.ts +0 -4
  51. package/src/wasm-loader.ts +0 -14
  52. package/tsconfig.json +0 -12
  53. package/tsup.config.ts +0 -35
  54. package/veil-core/Cargo.lock +0 -114
  55. package/veil-core/Cargo.toml +0 -12
  56. package/veil-core/src/entropy.rs +0 -132
  57. package/veil-core/src/lib.rs +0 -90
  58. package/vitest.config.ts +0 -15
package/dist/index.js ADDED
@@ -0,0 +1,1000 @@
1
+ // src/entropy/userAgent.ts
2
+ async function getUserAgentEntropy() {
3
+ const ua = navigator.userAgent;
4
+ let entropy = 0;
5
+ const freq = {};
6
+ for (const char of ua) {
7
+ freq[char] = (freq[char] || 0) + 1;
8
+ }
9
+ for (const count of Object.values(freq)) {
10
+ const p = count / ua.length;
11
+ entropy -= p * Math.log2(p);
12
+ }
13
+ return `${ua}|H(UA)=${entropy.toFixed(4)}`;
14
+ }
15
+
16
+ // src/entropy/canvas.ts
17
+ async function getCanvasEntropy() {
18
+ try {
19
+ const canvas = document.createElement("canvas");
20
+ if (canvas.getContext === void 0) return "canvas:unavailable";
21
+ const ctx = canvas.getContext("2d");
22
+ if (!ctx) return "canvas:unavailable";
23
+ const support = ctx.fillText ? "fillText" : "none";
24
+ const textMetrics = ctx.measureText ? "measureText" : "none";
25
+ const imageDataSupport = ctx.getImageData ? "getImageData" : "none";
26
+ return `support:${support}|metrics:${textMetrics}|imageData:${imageDataSupport}`;
27
+ } catch {
28
+ return "canvas:unavailable";
29
+ }
30
+ }
31
+
32
+ // src/entropy/webgl.ts
33
+ async function getWebGLEntropy() {
34
+ const canvas = document.createElement("canvas");
35
+ const gl = canvas.getContext("webgl") || canvas.getContext("webgl2");
36
+ if (!gl) return "webgl:unavailable";
37
+ const vendor = gl.getParameter(gl.VENDOR) || "unknown";
38
+ const renderer = gl.getParameter(gl.RENDERER) || "unknown";
39
+ const version = gl.getParameter(gl.VERSION) || "unknown";
40
+ return `vendor:${vendor}|renderer:${renderer}|version:${version}`;
41
+ }
42
+
43
+ // src/entropy/fonts.ts
44
+ async function getFontsEntropy() {
45
+ const fonts = ["Arial", "Courier", "Georgia", "Verdana"];
46
+ return fonts.join(",");
47
+ }
48
+
49
+ // src/entropy/storage.ts
50
+ async function getStorageEntropy() {
51
+ let ls = false;
52
+ let idb = false;
53
+ try {
54
+ const test = "__storage_test__";
55
+ window.localStorage.setItem(test, test);
56
+ window.localStorage.removeItem(test);
57
+ ls = true;
58
+ } catch {
59
+ ls = false;
60
+ }
61
+ try {
62
+ const request = window.indexedDB.open("__idb_test__");
63
+ idb = true;
64
+ } catch {
65
+ idb = false;
66
+ }
67
+ return `localStorage:${ls}|indexedDB:${idb}`;
68
+ }
69
+
70
+ // src/entropy/screen.ts
71
+ async function getScreenEntropy() {
72
+ const w = screen.width;
73
+ const h = screen.height;
74
+ const d = screen.colorDepth;
75
+ const pixelCount = w * h;
76
+ const totalColors = Math.pow(2, d);
77
+ const colorSpace = Math.log2(totalColors);
78
+ const screenSurface = Math.log2(pixelCount);
79
+ return `${w}x${h}|colors:${d}|log\u2082(A)=${screenSurface.toFixed(2)}|log\u2082(C)=${colorSpace.toFixed(2)}`;
80
+ }
81
+
82
+ // src/wasm-loader.ts
83
+ var wasmInitialized = false;
84
+ var wasmModule = null;
85
+ async function loadWasmModule() {
86
+ if (wasmModule) return wasmModule;
87
+ const paths = [
88
+ () => import("./veil_core.js"),
89
+ () => import("../veil-core/pkg/veil_core.js")
90
+ ];
91
+ for (const pathFn of paths) {
92
+ try {
93
+ wasmModule = await pathFn();
94
+ return wasmModule;
95
+ } catch {
96
+ continue;
97
+ }
98
+ }
99
+ throw new Error("Failed to load WASM module from any location");
100
+ }
101
+ async function initializeWasm() {
102
+ if (wasmInitialized) return;
103
+ try {
104
+ const wasm = await loadWasmModule();
105
+ if (typeof wasm.default === "function") {
106
+ await wasm.default();
107
+ }
108
+ wasmInitialized = true;
109
+ } catch (error) {
110
+ console.error("Failed to initialize WASM module:", error);
111
+ throw error;
112
+ }
113
+ }
114
+ async function getWasmModule() {
115
+ if (!wasmInitialized) {
116
+ await initializeWasm();
117
+ }
118
+ return wasmModule;
119
+ }
120
+
121
+ // src/seeded-rng.ts
122
+ function seededRng(seed, count) {
123
+ let hash = 5381;
124
+ for (let i = 0; i < seed.length; i++) {
125
+ hash = (hash << 5) + hash ^ seed.charCodeAt(i);
126
+ }
127
+ const samples = [];
128
+ for (let i = 0; i < count; i++) {
129
+ hash = hash * 1103515245 + 12345 & 2147483647;
130
+ samples.push(hash / 2147483647);
131
+ }
132
+ return samples;
133
+ }
134
+
135
+ // src/entropy/distribution.ts
136
+ async function getDistributionEntropy(seed) {
137
+ const samples = seededRng(seed, 1e3);
138
+ const wasm = await getWasmModule();
139
+ const ksStatistic = wasm.ks_test(new Float64Array(samples));
140
+ return `ks:${ksStatistic.toFixed(6)}`;
141
+ }
142
+
143
+ // src/entropy/complexity.ts
144
+ async function getComplexityEntropy() {
145
+ const ua = navigator.userAgent;
146
+ const bytes = new Uint8Array(ua.length);
147
+ for (let i = 0; i < ua.length; i++) {
148
+ bytes[i] = ua.charCodeAt(i) % 256;
149
+ }
150
+ const wasm = await getWasmModule();
151
+ const complexity = wasm.lz_complexity(bytes);
152
+ return `lz:${complexity}`;
153
+ }
154
+
155
+ // src/entropy/spectral.ts
156
+ async function getSpectralEntropy(seed) {
157
+ const samples = seededRng(seed, 128);
158
+ const wasm = await getWasmModule();
159
+ const entropy = wasm.spectral(new Float64Array(samples));
160
+ return `spectral:${entropy.toFixed(6)}`;
161
+ }
162
+
163
+ // src/entropy/approximate.ts
164
+ async function getApproximateEntropy(seed) {
165
+ const samples = seededRng(seed, 256);
166
+ const wasm = await getWasmModule();
167
+ const apen = wasm.approx_entropy(new Float64Array(samples), 2);
168
+ return `apen:${apen.toFixed(6)}`;
169
+ }
170
+
171
+ // src/entropy/os.ts
172
+ async function getOSEntropy() {
173
+ const platform = navigator.platform || "unknown";
174
+ const oscpu = navigator.oscpu || "unknown";
175
+ const combined = platform + oscpu;
176
+ const uniqueChars = new Set(combined).size;
177
+ const totalChars = combined.length;
178
+ const charDiversity = uniqueChars / totalChars;
179
+ const surprisal = -Math.log2(1 / uniqueChars);
180
+ return `OS:${platform}|CPU:${oscpu}|\u03B4=${charDiversity.toFixed(4)}|I=${surprisal.toFixed(2)}`;
181
+ }
182
+
183
+ // src/entropy/language.ts
184
+ async function getLanguageEntropy() {
185
+ const language = navigator.language;
186
+ const languages = navigator.languages;
187
+ const langCount = languages.length;
188
+ const langDiversity = Math.log2(langCount + 1);
189
+ const primaryEntropy = -Array.from(language).reduce((h, c) => {
190
+ const p = 1 / language.length;
191
+ return h + p * Math.log2(p);
192
+ }, 0);
193
+ const combined = languages.join("|");
194
+ return `primary:${language}|langs:${langCount}|H(L)=${primaryEntropy.toFixed(3)}|D=${langDiversity.toFixed(3)}`;
195
+ }
196
+
197
+ // src/entropy/timezone.ts
198
+ async function getTimezoneEntropy() {
199
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
200
+ const offset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
201
+ const tzEntropy = Math.log2(Math.abs(offset) + 1);
202
+ const offsetBits = offset.toString().length * 8;
203
+ const tzUniqueness = Math.log2(24 * 60 / Math.max(Math.abs(offset), 1));
204
+ return `TZ:${tz}|offset:${offset}|H(TZ)=${tzEntropy.toFixed(3)}|U=${tzUniqueness.toFixed(3)}`;
205
+ }
206
+
207
+ // src/entropy/hardware.ts
208
+ async function getHardwareEntropy() {
209
+ const cores = navigator.hardwareConcurrency || 1;
210
+ const memory = navigator.deviceMemory || 4;
211
+ const computePower = Math.log2(cores) * Math.log2(memory);
212
+ const memoryBits = Math.ceil(Math.log2(memory * 1024));
213
+ const coreEntropy = cores > 0 ? Math.log2(cores) : 0;
214
+ return `cores:${cores}|mem:${memory}GB|P(compute)=${computePower.toFixed(3)}|H(cores)=${coreEntropy.toFixed(3)}`;
215
+ }
216
+
217
+ // src/entropy/plugins.ts
218
+ async function getPluginsEntropy() {
219
+ const plugins = Array.from(navigator.plugins).map((p) => p.name);
220
+ const pluginStr = plugins.join("|");
221
+ const pluginCount = plugins.length;
222
+ const pluginEntropy = pluginCount > 0 ? -plugins.reduce((h, p) => {
223
+ const p_freq = 1 / pluginCount;
224
+ return h + p_freq * Math.log2(p_freq);
225
+ }, 0) : 0;
226
+ return `count:${pluginCount}|H(plugins)=${pluginEntropy.toFixed(3)}|plugins:${pluginStr || "none"}`;
227
+ }
228
+
229
+ // src/entropy/browser.ts
230
+ async function getBrowserEntropy() {
231
+ const ua = navigator.userAgent;
232
+ const vendor = navigator.vendor;
233
+ const cookieEnabled = navigator.cookieEnabled;
234
+ const doNotTrack = navigator.doNotTrack;
235
+ return `ua:${ua}|vendor:${vendor}|cookies:${cookieEnabled}|dnt:${doNotTrack}`;
236
+ }
237
+
238
+ // src/entropy/osVersion.ts
239
+ async function getOSVersionEntropy() {
240
+ const ua = navigator.userAgent;
241
+ const match = ua.match(/Windows NT|Mac OS|Linux|Android|iOS/i);
242
+ const os = match ? match[0] : "unknown";
243
+ return os;
244
+ }
245
+
246
+ // src/entropy/screenInfo.ts
247
+ async function getScreenInfoEntropy() {
248
+ const width = screen.width;
249
+ const height = screen.height;
250
+ const depth = screen.colorDepth;
251
+ const pixelDepth = screen.pixelDepth;
252
+ const devicePixelRatio = window.devicePixelRatio;
253
+ return `${width}x${height}|depth:${depth}|px:${pixelDepth}|ratio:${devicePixelRatio}`;
254
+ }
255
+
256
+ // src/entropy/adblock.ts
257
+ async function detectAdblock() {
258
+ try {
259
+ const response = await fetch("https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js", {
260
+ method: "HEAD",
261
+ mode: "no-cors"
262
+ });
263
+ return false;
264
+ } catch {
265
+ return true;
266
+ }
267
+ }
268
+ async function getAdblockEntropy() {
269
+ const adblockPresent = await detectAdblock();
270
+ return `adblock:${adblockPresent}`;
271
+ }
272
+
273
+ // src/entropy/webFeatures.ts
274
+ async function getWebFeaturesEntropy() {
275
+ const features = {
276
+ localStorage: !!window.localStorage,
277
+ sessionStorage: !!window.sessionStorage,
278
+ indexedDB: !!window.indexedDB,
279
+ openDatabase: !!window.openDatabase,
280
+ serviceWorker: !!navigator.serviceWorker,
281
+ webWorker: typeof Worker !== "undefined",
282
+ geolocation: !!navigator.geolocation,
283
+ notifications: !!window.Notification
284
+ };
285
+ const enabled = Object.values(features).filter(Boolean).length;
286
+ const total = Object.keys(features).length;
287
+ const supportRatio = enabled / total;
288
+ const supportEntropy = Math.log2(total) * (1 - Math.abs(supportRatio - 0.5) * 2);
289
+ return `enabled:${enabled}/${total}|\u03C3=${supportRatio.toFixed(3)}|H(features)=${supportEntropy.toFixed(3)}|${Object.entries(features).map(([k, v]) => `${k}:${v}`).join("|")}`;
290
+ }
291
+
292
+ // src/entropy/preferences.ts
293
+ async function getPreferencesEntropy() {
294
+ const darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
295
+ const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
296
+ const highContrast = window.matchMedia("(prefers-contrast: more)").matches;
297
+ const prefCount = [darkMode, reducedMotion, highContrast].filter(Boolean).length;
298
+ const maxCombinations = Math.pow(2, 3);
299
+ const preferenceBits = Math.log2(maxCombinations);
300
+ const actualEntropy = prefCount > 0 ? -(prefCount / 3 * Math.log2(prefCount / 3) + (3 - prefCount) / 3 * Math.log2((3 - prefCount) / 3)) : 0;
301
+ return `dark:${darkMode}|motion:${reducedMotion}|contrast:${highContrast}|enabled:${prefCount}|H(pref)=${actualEntropy.toFixed(3)}`;
302
+ }
303
+
304
+ // src/entropy/permissions.ts
305
+ async function getPermissionsEntropy() {
306
+ const perms = [];
307
+ const checks = ["geolocation", "notifications", "camera", "microphone"];
308
+ for (const perm of checks) {
309
+ try {
310
+ const result = await navigator.permissions.query({ name: perm });
311
+ perms.push(`${perm}:${result.state}`);
312
+ } catch {
313
+ perms.push(`${perm}:unknown`);
314
+ }
315
+ }
316
+ return perms.join("|");
317
+ }
318
+
319
+ // src/entropy/statistical.ts
320
+ async function getStatisticalEntropy() {
321
+ const ua = navigator.userAgent;
322
+ const freq = {};
323
+ for (const char of ua) {
324
+ freq[char] = (freq[char] || 0) + 1;
325
+ }
326
+ const values = Object.values(freq);
327
+ const mean = values.reduce((a, b) => a + b) / values.length;
328
+ const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length;
329
+ const stdDev = Math.sqrt(variance);
330
+ return `mean:${mean.toFixed(4)}|var:${variance.toFixed(4)}|std:${stdDev.toFixed(4)}`;
331
+ }
332
+
333
+ // src/entropy/probabilistic.ts
334
+ async function getProbabilisticEntropy() {
335
+ const metrics = [];
336
+ const screen_values = [screen.width, screen.height, screen.colorDepth];
337
+ const hw_values = [navigator.hardwareConcurrency || 1, navigator.deviceMemory || 4];
338
+ const all_values = [...screen_values, ...hw_values];
339
+ const mean = all_values.reduce((a, b) => a + b) / all_values.length;
340
+ const sorted = [...all_values].sort((a, b) => a - b);
341
+ const median = sorted[Math.floor(sorted.length / 2)];
342
+ const q1 = sorted[Math.floor(sorted.length / 4)];
343
+ const q3 = sorted[Math.floor(sorted.length * 3 / 4)];
344
+ const iqr = q3 - q1;
345
+ metrics.push(`mean:${mean.toFixed(2)}`);
346
+ metrics.push(`median:${median.toFixed(2)}`);
347
+ metrics.push(`iqr:${iqr.toFixed(2)}`);
348
+ return metrics.join("|");
349
+ }
350
+
351
+ // src/probability.ts
352
+ function calculateMean(values) {
353
+ if (values.length === 0) return 0;
354
+ return values.reduce((a, b) => a + b, 0) / values.length;
355
+ }
356
+ function hellingerDistance(p, q) {
357
+ if (p.length !== q.length) return 0;
358
+ const sum = p.reduce((s, pi, i) => s + Math.pow(Math.sqrt(pi) - Math.sqrt(q[i]), 2), 0);
359
+ return Math.sqrt(sum / 2);
360
+ }
361
+
362
+ // src/scoring.ts
363
+ function calculateEntropy(data) {
364
+ const freq = {};
365
+ for (const char of data) {
366
+ freq[char] = (freq[char] || 0) + 1;
367
+ }
368
+ let entropy = 0;
369
+ for (const count of Object.values(freq)) {
370
+ const p = count / data.length;
371
+ if (p > 0) {
372
+ entropy -= p * Math.log2(p);
373
+ }
374
+ }
375
+ return entropy;
376
+ }
377
+ function calculateLikelihood(sources) {
378
+ const entropies = sources.map((s) => s.entropy).filter((e) => !isNaN(e) && e > 0);
379
+ if (entropies.length === 0) return 0;
380
+ const mean = entropies.reduce((a, b) => a + b) / entropies.length;
381
+ const variance = entropies.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / entropies.length;
382
+ const stdDev = Math.sqrt(variance);
383
+ return mean * (1 + stdDev / mean);
384
+ }
385
+ function calculateConfidence(sources, likelihood) {
386
+ const maxPossibleEntropy = Math.log2(256);
387
+ const sourceCount = sources.length;
388
+ const normalizedLikelihood = Math.min(likelihood / maxPossibleEntropy, 1);
389
+ const sourceWeighting = Math.log2(sourceCount + 1) / Math.log2(32);
390
+ return normalizedLikelihood * sourceWeighting;
391
+ }
392
+ function calculateUniqueness(sources, likelihood, confidence) {
393
+ const distinctSources = new Set(sources.map((s) => s.value)).size;
394
+ const totalSources = sources.length;
395
+ const diversity = distinctSources / totalSources;
396
+ const informationContent = likelihood * Math.log2(totalSources);
397
+ const uniquenessScore = informationContent * confidence * diversity;
398
+ return Math.min(uniquenessScore / 100, 0.999);
399
+ }
400
+ function scoreFingerprint(sources) {
401
+ const likelihood = calculateLikelihood(sources);
402
+ const confidence = calculateConfidence(sources, likelihood);
403
+ const uniqueness = calculateUniqueness(sources, likelihood, confidence);
404
+ const entropies = sources.map((s) => s.entropy).filter((e) => !isNaN(e) && e > 0);
405
+ const uniformDist = new Array(entropies.length).fill(1 / entropies.length);
406
+ const normalizedEntropies = entropies.length > 0 ? entropies.map((e) => e / calculateMean(entropies)) : [];
407
+ const divergence = normalizedEntropies.length > 0 ? hellingerDistance(normalizedEntropies, uniformDist.slice(0, normalizedEntropies.length)) : 0;
408
+ return {
409
+ likelihood,
410
+ confidence,
411
+ uniqueness,
412
+ divergence,
413
+ sources
414
+ };
415
+ }
416
+
417
+ // src/tamper.ts
418
+ function analyzeTamper(sources) {
419
+ const anomalies = [];
420
+ let riskScore = 0;
421
+ if (sources.length === 0) {
422
+ anomalies.push({
423
+ id: "no_sources",
424
+ category: "missing_sources",
425
+ severity: "critical",
426
+ message: "No entropy sources collected",
427
+ explanation: "The fingerprinting process failed to collect any entropy data. This may indicate a sandbox environment or blocked APIs.",
428
+ riskContribution: 0.9
429
+ });
430
+ return { tampering_risk: 0.9, anomalies };
431
+ }
432
+ const entropies = sources.map((s) => s.entropy);
433
+ const values = sources.map((s) => s.value);
434
+ const mean = entropies.reduce((a, b) => a + b, 0) / entropies.length;
435
+ const variance = entropies.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / entropies.length;
436
+ const stdDev = Math.sqrt(variance);
437
+ if (stdDev < 0.05) {
438
+ const contribution = 0.25;
439
+ anomalies.push({
440
+ id: "uniform_entropy",
441
+ category: "entropy_distribution",
442
+ severity: "high",
443
+ message: "All entropy sources have suspiciously similar values",
444
+ explanation: `Standard deviation of entropy across sources is ${stdDev.toFixed(4)}, indicating very low variance. Real fingerprints should show diverse entropy patterns.`,
445
+ riskContribution: contribution
446
+ });
447
+ riskScore += contribution;
448
+ }
449
+ const uniqueValues = new Set(values);
450
+ if (uniqueValues.size < sources.length * 0.5) {
451
+ const contribution = 0.2;
452
+ const duplicateCount = sources.length - uniqueValues.size;
453
+ anomalies.push({
454
+ id: "duplicate_values",
455
+ category: "data_quality",
456
+ severity: "medium",
457
+ message: `${duplicateCount} duplicate values detected across entropy sources`,
458
+ explanation: "High duplication suggests cached or placeholder values. Expected diversity is not present.",
459
+ riskContribution: contribution
460
+ });
461
+ riskScore += contribution;
462
+ }
463
+ const emptyOrShort = values.filter((v) => !v || v.length < 2).length;
464
+ if (emptyOrShort > sources.length * 0.3) {
465
+ const contribution = 0.15;
466
+ anomalies.push({
467
+ id: "empty_sources",
468
+ category: "data_quality",
469
+ severity: "high",
470
+ message: `${emptyOrShort} sources are empty or too short`,
471
+ explanation: "Several entropy sources produced minimal data. This may indicate blocked APIs or restricted environment.",
472
+ riskContribution: contribution
473
+ });
474
+ riskScore += contribution;
475
+ }
476
+ const sourceNames = sources.map((s) => s.name);
477
+ const criticalSources = ["userAgent", "canvas", "webgl", "fonts"];
478
+ const missingCritical = criticalSources.filter(
479
+ (c) => !sourceNames.includes(c)
480
+ );
481
+ if (missingCritical.length > 0) {
482
+ const contribution = 0.2 * (missingCritical.length / criticalSources.length);
483
+ anomalies.push({
484
+ id: "missing_critical",
485
+ category: "missing_sources",
486
+ severity: missingCritical.length === criticalSources.length ? "critical" : "high",
487
+ message: `Missing critical sources: ${missingCritical.join(", ")}`,
488
+ explanation: "These sources are fundamental to browser fingerprinting. Their absence reduces fingerprint reliability and suggests API restrictions or tampering.",
489
+ riskContribution: contribution
490
+ });
491
+ riskScore += contribution;
492
+ }
493
+ const canvasSource = sources.find((s) => s.name === "canvas");
494
+ const webglSource = sources.find((s) => s.name === "webgl");
495
+ if (canvasSource && webglSource && canvasSource.value === webglSource.value && canvasSource.entropy < 0.5) {
496
+ const contribution = 0.15;
497
+ anomalies.push({
498
+ id: "canvas_webgl_match",
499
+ category: "cross_source_inconsistency",
500
+ severity: "high",
501
+ message: "Canvas and WebGL fingerprints are identical",
502
+ explanation: "Canvas and WebGL rendering engines should produce different fingerprints. Identical values indicate either spoofing or a very unusual environment.",
503
+ riskContribution: contribution
504
+ });
505
+ riskScore += contribution;
506
+ }
507
+ const screenSource = sources.find((s) => s.name === "screen");
508
+ if (screenSource && screenSource.value.length < 3) {
509
+ const contribution = 0.1;
510
+ anomalies.push({
511
+ id: "screen_entropy_low",
512
+ category: "data_quality",
513
+ severity: "low",
514
+ message: "Screen entropy data is suspiciously minimal",
515
+ explanation: "Screen information should provide resolution and color depth data. Very short values suggest incomplete data collection.",
516
+ riskContribution: contribution
517
+ });
518
+ riskScore += contribution;
519
+ }
520
+ const maxEntropy = Math.max(...entropies);
521
+ const minEntropy = Math.min(...entropies);
522
+ if (maxEntropy > 0 && minEntropy / maxEntropy < 0.1) {
523
+ const contribution = 0.15;
524
+ anomalies.push({
525
+ id: "entropy_variance",
526
+ category: "entropy_distribution",
527
+ severity: "medium",
528
+ message: `Extreme variance in entropy: min/max ratio is ${(minEntropy / maxEntropy).toFixed(3)}`,
529
+ explanation: "Some sources have drastically different entropy levels, suggesting uneven data collection or selective API blocking.",
530
+ riskContribution: contribution
531
+ });
532
+ riskScore += contribution;
533
+ }
534
+ const zeroEntropyCount = entropies.filter((e) => e === 0).length;
535
+ if (zeroEntropyCount > sources.length * 0.4) {
536
+ const contribution = 0.1;
537
+ anomalies.push({
538
+ id: "zero_entropy",
539
+ category: "data_quality",
540
+ severity: "medium",
541
+ message: `${zeroEntropyCount} sources have zero entropy`,
542
+ explanation: "Zero entropy typically means identical or highly repetitive data. Many sources with zero entropy suggests a restricted or heavily spoofed environment.",
543
+ riskContribution: contribution
544
+ });
545
+ riskScore += contribution;
546
+ }
547
+ const suspiciousPatterns = sources.filter(
548
+ (s) => /^(null|undefined|test|fake|mock)$/i.test(s.value)
549
+ );
550
+ if (suspiciousPatterns.length > 0) {
551
+ const contribution = 0.2;
552
+ anomalies.push({
553
+ id: "placeholder_values",
554
+ category: "value_spoofing",
555
+ severity: "critical",
556
+ message: `Placeholder values detected: ${suspiciousPatterns.map((s) => s.name).join(", ")}`,
557
+ explanation: "Sources contain obvious placeholder/test values (null, undefined, fake, mock). This is a strong indicator of intentional spoofing.",
558
+ riskContribution: contribution
559
+ });
560
+ riskScore += contribution;
561
+ }
562
+ const finalRisk = Math.min(riskScore, 1);
563
+ return {
564
+ tampering_risk: finalRisk,
565
+ anomalies
566
+ };
567
+ }
568
+
569
+ // src/stability.ts
570
+ var STABILITY_THRESHOLDS = {
571
+ stable: 0.85,
572
+ semi_volatile: 0.5
573
+ };
574
+ function getSourceStability(sources) {
575
+ const stabilityMap = {
576
+ userAgent: 0.95,
577
+ os: 0.95,
578
+ timezone: 0.98,
579
+ hardware: 0.9,
580
+ language: 0.92,
581
+ screen: 0.88,
582
+ osVersion: 0.85,
583
+ browser: 0.93,
584
+ canvas: 0.82,
585
+ webgl: 0.8,
586
+ fonts: 0.7,
587
+ plugins: 0.75,
588
+ storage: 0.5,
589
+ adblock: 0.65,
590
+ screenInfo: 0.85,
591
+ webFeatures: 0.8,
592
+ permissions: 0.6,
593
+ preferences: 0.55,
594
+ statistical: 0.7,
595
+ probabilistic: 0.65,
596
+ distribution: 0.75,
597
+ complexity: 0.8,
598
+ spectral: 0.75,
599
+ approximate: 0.75,
600
+ connection: 0.4,
601
+ battery: 0.3,
602
+ audio: 0.85,
603
+ performance: 0.2
604
+ };
605
+ return sources.map((source) => {
606
+ const stability = stabilityMap[source.name] ?? 0.5;
607
+ const volatility = 1 - stability;
608
+ let category;
609
+ if (stability >= STABILITY_THRESHOLDS.stable) {
610
+ category = "stable";
611
+ } else if (stability >= STABILITY_THRESHOLDS.semi_volatile) {
612
+ category = "semi_volatile";
613
+ } else {
614
+ category = "volatile";
615
+ }
616
+ return {
617
+ source: source.name,
618
+ stability,
619
+ volatility,
620
+ category
621
+ };
622
+ });
623
+ }
624
+ function hashString(str) {
625
+ let hash = 0;
626
+ for (let i = 0; i < str.length; i++) {
627
+ const char = str.charCodeAt(i);
628
+ hash = (hash << 5) - hash + char;
629
+ hash = hash & hash;
630
+ }
631
+ return Math.abs(hash).toString(16);
632
+ }
633
+ function detectEntropyAnomalies(sources) {
634
+ const warnings = [];
635
+ const sourceMap = new Map(sources.map((s) => [s.name, s]));
636
+ for (const source of sources) {
637
+ const value = source.value.toLowerCase();
638
+ if (!value || value.length === 0) {
639
+ warnings.push({
640
+ source: source.name,
641
+ severity: "error",
642
+ message: "Empty entropy value",
643
+ reason: "Source returned no data, likely blocked or unavailable API"
644
+ });
645
+ }
646
+ if (/^(null|undefined|nan|n\/a|-1|0x0|unknown|not available|blocked)$/i.test(value)) {
647
+ warnings.push({
648
+ source: source.name,
649
+ severity: "error",
650
+ message: "Suspicious placeholder value detected",
651
+ reason: "Value appears to be a default/placeholder, not real entropy"
652
+ });
653
+ }
654
+ if (source.entropy === 0 && value.length > 2) {
655
+ warnings.push({
656
+ source: source.name,
657
+ severity: "warning",
658
+ message: "Zero entropy despite non-empty value",
659
+ reason: "Value is highly repetitive or contains minimal information"
660
+ });
661
+ }
662
+ if (source.entropy > 8 && value.length < 5) {
663
+ warnings.push({
664
+ source: source.name,
665
+ severity: "warning",
666
+ message: "Suspiciously high entropy in very short value",
667
+ reason: "Short strings cannot contain this much entropy mathematically"
668
+ });
669
+ }
670
+ if (source.name === "canvas" || source.name === "webgl") {
671
+ if (value.length < 10) {
672
+ warnings.push({
673
+ source: source.name,
674
+ severity: "warning",
675
+ message: "Graphics fingerprint is unusually short",
676
+ reason: "Canvas/WebGL fingerprints should contain detailed rendering info"
677
+ });
678
+ }
679
+ }
680
+ if (source.name === "fonts") {
681
+ if (value === "0" || value === "[]" || value.split(",").length < 3) {
682
+ warnings.push({
683
+ source: source.name,
684
+ severity: "warning",
685
+ message: "Suspiciously few fonts detected",
686
+ reason: "System should report 50+ fonts; low count suggests API blocking"
687
+ });
688
+ }
689
+ }
690
+ if (source.name === "screen") {
691
+ const match = value.match(/(\d+)x(\d+)/);
692
+ if (match) {
693
+ const [w, h] = [parseInt(match[1]), parseInt(match[2])];
694
+ if (w < 320 || h < 240 || w > 7680 || h > 4320) {
695
+ warnings.push({
696
+ source: source.name,
697
+ severity: "warning",
698
+ message: "Unusual screen resolution detected",
699
+ reason: `Resolution ${w}x${h} is outside normal range`
700
+ });
701
+ }
702
+ }
703
+ }
704
+ if (source.name === "userAgent") {
705
+ const isCommonUA = /chrome|firefox|safari|edge|mozilla/i.test(value);
706
+ if (!isCommonUA && value.length > 10) {
707
+ warnings.push({
708
+ source: source.name,
709
+ severity: "info",
710
+ message: "Unusual user agent detected",
711
+ reason: "User agent does not match known browsers"
712
+ });
713
+ }
714
+ }
715
+ }
716
+ if (sources.length > 0) {
717
+ const avgEntropy = sources.reduce((a, b) => a + b.entropy, 0) / sources.length;
718
+ if (avgEntropy < 0.3) {
719
+ warnings.push({
720
+ source: "_system",
721
+ severity: "warning",
722
+ message: "Overall entropy is very low",
723
+ reason: "Average entropy across sources is below 0.3, fingerprint may be unreliable"
724
+ });
725
+ }
726
+ }
727
+ return warnings;
728
+ }
729
+ function extractDeviceSignature(sources) {
730
+ const osSource = sources.find((s) => s.name === "os");
731
+ const hardwareSource = sources.find((s) => s.name === "hardware");
732
+ const screenSource = sources.find((s) => s.name === "screen");
733
+ const uaSource = sources.find((s) => s.name === "userAgent");
734
+ const os = osSource?.value.split("|")[0] ?? "unknown";
735
+ const hardwareStr = hardwareSource?.value ?? "0";
736
+ const cores = parseInt(hardwareStr.match(/\d+/)?.[0] ?? "0");
737
+ const resolution = screenSource?.value ?? "0x0";
738
+ const uaHash = uaSource ? hashString(uaSource.value) : "0";
739
+ return {
740
+ os,
741
+ hardware_cores: cores,
742
+ screen_resolution: resolution,
743
+ user_agent_hash: uaHash
744
+ };
745
+ }
746
+ function extractPreferencesOffset(sources) {
747
+ const langSource = sources.find((s) => s.name === "language");
748
+ const tzSource = sources.find((s) => s.name === "timezone");
749
+ const prefSource = sources.find((s) => s.name === "preferences");
750
+ let theme = null;
751
+ let dark_mode = null;
752
+ let accessibility = null;
753
+ if (prefSource && prefSource.value) {
754
+ const parts = prefSource.value.split("|");
755
+ if (parts.length > 0) dark_mode = parts[0] === "true";
756
+ if (parts.length > 1) theme = parts[1];
757
+ if (parts.length > 2) accessibility = parts[2];
758
+ }
759
+ return {
760
+ language: langSource ? langSource.value.split("|")[0] : null,
761
+ timezone: tzSource ? tzSource.value.split("|")[0] : null,
762
+ theme,
763
+ dark_mode,
764
+ font_size: null,
765
+ accessibility
766
+ };
767
+ }
768
+ function generateDeviceIdentity(sources) {
769
+ const signature = extractDeviceSignature(sources);
770
+ const signatureStr = `${signature.os}|${signature.hardware_cores}|${signature.screen_resolution}|${signature.user_agent_hash}`;
771
+ const coreHash = hashString(signatureStr);
772
+ const preferences = extractPreferencesOffset(sources);
773
+ const prefsStr = `${preferences.language}|${preferences.timezone}|${preferences.theme}|${preferences.dark_mode}|${preferences.accessibility}`;
774
+ const prefsHash = hashString(prefsStr);
775
+ const qualityScore = sources.filter((s) => s.entropy > 0.5).length / Math.max(sources.length, 1);
776
+ return {
777
+ core_hash: coreHash,
778
+ device_signature: signature,
779
+ entropy_quality: qualityScore,
780
+ preferences_offset: preferences,
781
+ preferences_hash: prefsHash
782
+ };
783
+ }
784
+ function assessFingerprint(sources) {
785
+ const stability = getSourceStability(sources);
786
+ const entropyWarnings = detectEntropyAnomalies(sources);
787
+ const warnings = [];
788
+ let usable = true;
789
+ let confidence = 1;
790
+ const stableCount = stability.filter((s) => s.category === "stable").length;
791
+ const volatileCount = stability.filter((s) => s.category === "volatile").length;
792
+ if (stableCount < 5) {
793
+ warnings.push(
794
+ `Only ${stableCount} stable sources available, fingerprint may be unreliable`
795
+ );
796
+ confidence -= 0.2;
797
+ }
798
+ if (volatileCount > sources.length * 0.5) {
799
+ warnings.push("Fingerprint heavily dependent on volatile sources");
800
+ confidence -= 0.15;
801
+ }
802
+ const zeroEntropyCount = sources.filter((s) => s.entropy === 0).length;
803
+ if (zeroEntropyCount > sources.length * 0.3) {
804
+ warnings.push(`${zeroEntropyCount} sources have zero entropy`);
805
+ confidence -= 0.25;
806
+ if (zeroEntropyCount > sources.length * 0.6) {
807
+ usable = false;
808
+ }
809
+ }
810
+ const errorWarnings = entropyWarnings.filter((w) => w.severity === "error").length;
811
+ if (errorWarnings > sources.length * 0.2) {
812
+ confidence -= 0.2;
813
+ if (errorWarnings > sources.length * 0.4) {
814
+ usable = false;
815
+ }
816
+ }
817
+ return {
818
+ usable,
819
+ confidence: Math.max(0, confidence),
820
+ warnings,
821
+ entropy_warnings: entropyWarnings
822
+ };
823
+ }
824
+
825
+ // src/index.ts
826
+ function levenshteinDistance(s1, s2) {
827
+ const len1 = s1.length;
828
+ const len2 = s2.length;
829
+ const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
830
+ for (let i = 0; i <= len1; i++) matrix[i][0] = i;
831
+ for (let j = 0; j <= len2; j++) matrix[0][j] = j;
832
+ for (let i = 1; i <= len1; i++) {
833
+ for (let j = 1; j <= len2; j++) {
834
+ const cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
835
+ matrix[i][j] = Math.min(
836
+ matrix[i - 1][j] + 1,
837
+ matrix[i][j - 1] + 1,
838
+ matrix[i - 1][j - 1] + cost
839
+ );
840
+ }
841
+ }
842
+ return matrix[len1][len2];
843
+ }
844
+ function similarityScore(s1, s2) {
845
+ const distance = levenshteinDistance(s1, s2);
846
+ const maxLen = Math.max(s1.length, s2.length);
847
+ return maxLen === 0 ? 1 : 1 - distance / maxLen;
848
+ }
849
+ async function getFingerprint(options) {
850
+ await initializeWasm();
851
+ const opts = {
852
+ entropy: {},
853
+ hash: "sha256",
854
+ ...options
855
+ };
856
+ const sources = [];
857
+ if (opts.entropy.userAgent !== false) {
858
+ const value = await getUserAgentEntropy();
859
+ sources.push({ name: "userAgent", value, entropy: calculateEntropy(value) });
860
+ }
861
+ if (opts.entropy.canvas !== false) {
862
+ const value = await getCanvasEntropy();
863
+ sources.push({ name: "canvas", value, entropy: calculateEntropy(value) });
864
+ }
865
+ if (opts.entropy.webgl !== false) {
866
+ const value = await getWebGLEntropy();
867
+ sources.push({ name: "webgl", value, entropy: calculateEntropy(value) });
868
+ }
869
+ if (opts.entropy.fonts !== false) {
870
+ const value = await getFontsEntropy();
871
+ sources.push({ name: "fonts", value, entropy: calculateEntropy(value) });
872
+ }
873
+ if (opts.entropy.storage !== false) {
874
+ const value = await getStorageEntropy();
875
+ sources.push({ name: "storage", value, entropy: calculateEntropy(value) });
876
+ }
877
+ if (opts.entropy.screen !== false) {
878
+ const value = await getScreenEntropy();
879
+ sources.push({ name: "screen", value, entropy: calculateEntropy(value) });
880
+ }
881
+ const baseSeed = sources.map((s) => s.value).join("|");
882
+ sources.push({ name: "distribution", value: await getDistributionEntropy(baseSeed), entropy: 0 });
883
+ sources.push({ name: "complexity", value: await getComplexityEntropy(), entropy: 0 });
884
+ sources.push({ name: "spectral", value: await getSpectralEntropy(baseSeed), entropy: 0 });
885
+ sources.push({ name: "approximate", value: await getApproximateEntropy(baseSeed), entropy: 0 });
886
+ sources.push({ name: "os", value: await getOSEntropy(), entropy: 0 });
887
+ sources.push({ name: "language", value: await getLanguageEntropy(), entropy: 0 });
888
+ sources.push({ name: "timezone", value: await getTimezoneEntropy(), entropy: 0 });
889
+ sources.push({ name: "hardware", value: await getHardwareEntropy(), entropy: 0 });
890
+ sources.push({ name: "plugins", value: await getPluginsEntropy(), entropy: 0 });
891
+ sources.push({ name: "browser", value: await getBrowserEntropy(), entropy: 0 });
892
+ sources.push({ name: "osVersion", value: await getOSVersionEntropy(), entropy: 0 });
893
+ sources.push({ name: "screenInfo", value: await getScreenInfoEntropy(), entropy: 0 });
894
+ sources.push({ name: "adblock", value: await getAdblockEntropy(), entropy: 0 });
895
+ sources.push({ name: "webFeatures", value: await getWebFeaturesEntropy(), entropy: 0 });
896
+ sources.push({ name: "preferences", value: await getPreferencesEntropy(), entropy: 0 });
897
+ sources.push({ name: "permissions", value: await getPermissionsEntropy(), entropy: 0 });
898
+ sources.push({ name: "statistical", value: await getStatisticalEntropy(), entropy: 0 });
899
+ sources.push({ name: "probabilistic", value: await getProbabilisticEntropy(), entropy: 0 });
900
+ for (const source of sources) {
901
+ if (source.entropy === 0) {
902
+ source.entropy = calculateEntropy(source.value);
903
+ }
904
+ }
905
+ const score = scoreFingerprint(sources);
906
+ const tamper = analyzeTamper(sources);
907
+ const stability = getSourceStability(sources);
908
+ const assessment = assessFingerprint(sources);
909
+ const deviceIdentity = generateDeviceIdentity(sources);
910
+ const data = sources.map((s) => s.value);
911
+ const combined = data.join("|");
912
+ const coreWeight = "core:" + deviceIdentity.core_hash;
913
+ const prefsWeight = "prefs:" + deviceIdentity.preferences_hash;
914
+ const weightedInput = `${coreWeight}|${prefsWeight}|${combined}`;
915
+ const algorithm = opts.hash === "sha512" ? "SHA-512" : "SHA-256";
916
+ const hash = await crypto.subtle.digest(
917
+ algorithm,
918
+ new TextEncoder().encode(weightedInput)
919
+ );
920
+ let fingerprint = Array.from(new Uint8Array(hash)).map((b) => b.toString(16).padStart(2, "0")).join("");
921
+ if (opts.detailed) {
922
+ const os = await getOSVersionEntropy();
923
+ const lang = await getLanguageEntropy();
924
+ const tz = await getTimezoneEntropy();
925
+ const hw = await getHardwareEntropy();
926
+ const sr = await getScreenInfoEntropy();
927
+ const ua = await getUserAgentEntropy();
928
+ const browser = await getBrowserEntropy();
929
+ const sourceMetrics = sources.map((s) => {
930
+ const stab = stability.find((st) => st.source === s.name);
931
+ return {
932
+ source: s.name,
933
+ value: s.value,
934
+ entropy: s.entropy,
935
+ confidence: score.likelihood > 0 ? Math.min(s.entropy / score.likelihood, 1) : 0,
936
+ stability: stab?.stability
937
+ };
938
+ });
939
+ const avgStability = stability.length > 0 ? stability.reduce((a, b) => a + b.stability, 0) / stability.length : 0;
940
+ const response = {
941
+ hash: fingerprint,
942
+ uniqueness: score.uniqueness,
943
+ confidence: score.confidence,
944
+ stability_score: avgStability,
945
+ usable: assessment.usable,
946
+ warnings: assessment.warnings.length > 0 ? assessment.warnings : void 0,
947
+ entropy_warnings: assessment.entropy_warnings.length > 0 ? assessment.entropy_warnings : void 0,
948
+ tampering_risk: tamper.tampering_risk,
949
+ anomalies: tamper.anomalies,
950
+ device_identity: deviceIdentity,
951
+ sources: sourceMetrics,
952
+ system: {
953
+ os,
954
+ language: lang.split("|")[0],
955
+ timezone: tz.split("|")[0],
956
+ hardware: {
957
+ cores: navigator.hardwareConcurrency || 0,
958
+ memory: navigator.deviceMemory || 0
959
+ }
960
+ },
961
+ display: {
962
+ resolution: `${screen.width}x${screen.height}`,
963
+ colorDepth: screen.colorDepth,
964
+ devicePixelRatio: window.devicePixelRatio
965
+ },
966
+ browser: {
967
+ userAgent: navigator.userAgent,
968
+ vendor: navigator.vendor,
969
+ cookieEnabled: navigator.cookieEnabled
970
+ }
971
+ };
972
+ return response;
973
+ }
974
+ return fingerprint;
975
+ }
976
+ async function compareFingerpints(fp1, fp2) {
977
+ const hash1 = typeof fp1 === "string" ? fp1 : fp1.hash;
978
+ const hash2 = typeof fp2 === "string" ? fp2 : fp2.hash;
979
+ const sim = similarityScore(hash1, hash2);
980
+ const match = sim > 0.95;
981
+ return { similarity: sim, match };
982
+ }
983
+ async function matchProbability(storedFingerprints, currentFingerprint) {
984
+ const currentHash = typeof currentFingerprint === "string" ? currentFingerprint : currentFingerprint.hash;
985
+ let bestMatch = 0;
986
+ for (const stored of storedFingerprints) {
987
+ const storedHash = typeof stored === "string" ? stored : stored.hash;
988
+ const sim = similarityScore(currentHash, storedHash);
989
+ if (sim > bestMatch) {
990
+ bestMatch = sim;
991
+ }
992
+ }
993
+ const confidence = Math.min(bestMatch * 1.2, 1);
994
+ return { bestMatch, confidence };
995
+ }
996
+ export {
997
+ compareFingerpints,
998
+ getFingerprint,
999
+ matchProbability
1000
+ };