@cloudglides/veil 0.1.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/.envrc +1 -0
- package/.github/workflows/build-on-tag.yml +80 -0
- package/example/index.html +226 -0
- package/flake.nix +68 -0
- package/package.json +31 -0
- package/scripts/patch-wasm.js +12 -0
- package/src/cpu.ts +67 -0
- package/src/entropy/adblock.ts +16 -0
- package/src/entropy/approximate.ts +8 -0
- package/src/entropy/audio.ts +17 -0
- package/src/entropy/battery.ts +10 -0
- package/src/entropy/browser.ts +7 -0
- package/src/entropy/canvas.ts +17 -0
- package/src/entropy/complexity.ts +13 -0
- package/src/entropy/connection.ts +14 -0
- package/src/entropy/distribution.ts +8 -0
- package/src/entropy/fonts.ts +4 -0
- package/src/entropy/hardware.ts +10 -0
- package/src/entropy/language.ts +14 -0
- package/src/entropy/os.ts +12 -0
- package/src/entropy/osVersion.ts +6 -0
- package/src/entropy/performance.ts +14 -0
- package/src/entropy/permissions.ts +15 -0
- package/src/entropy/plugins.ts +14 -0
- package/src/entropy/preferences.ts +12 -0
- package/src/entropy/probabilistic.ts +20 -0
- package/src/entropy/screen.ts +12 -0
- package/src/entropy/screenInfo.ts +8 -0
- package/src/entropy/spectral.ts +8 -0
- package/src/entropy/statistical.ts +15 -0
- package/src/entropy/storage.ts +22 -0
- package/src/entropy/timezone.ts +10 -0
- package/src/entropy/userAgent.ts +16 -0
- package/src/entropy/webFeatures.ts +21 -0
- package/src/entropy/webgl.ts +11 -0
- package/src/gpu.ts +132 -0
- package/src/index.test.ts +26 -0
- package/src/index.ts +198 -0
- package/src/normalize/index.ts +31 -0
- package/src/probability.ts +11 -0
- package/src/scoring.ts +106 -0
- package/src/seeded-rng.ts +14 -0
- package/src/types/index.ts +11 -0
- package/src/types.ts +47 -0
- package/src/veil_core.d.ts +4 -0
- package/src/wasm-loader.ts +14 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +35 -0
- package/veil-core/Cargo.lock +114 -0
- package/veil-core/Cargo.toml +12 -0
- package/veil-core/src/entropy.rs +132 -0
- package/veil-core/src/lib.rs +90 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function getPermissionsEntropy(): Promise<string> {
|
|
2
|
+
const perms = [];
|
|
3
|
+
const checks = ["geolocation", "notifications", "camera", "microphone"];
|
|
4
|
+
|
|
5
|
+
for (const perm of checks) {
|
|
6
|
+
try {
|
|
7
|
+
const result = await navigator.permissions.query({ name: perm as any });
|
|
8
|
+
perms.push(`${perm}:${result.state}`);
|
|
9
|
+
} catch {
|
|
10
|
+
perms.push(`${perm}:unknown`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return perms.join("|");
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export async function getPluginsEntropy(): Promise<string> {
|
|
2
|
+
const plugins = Array.from(navigator.plugins).map((p) => p.name);
|
|
3
|
+
const pluginStr = plugins.join("|");
|
|
4
|
+
|
|
5
|
+
const pluginCount = plugins.length;
|
|
6
|
+
const pluginEntropy = pluginCount > 0
|
|
7
|
+
? -plugins.reduce((h, p) => {
|
|
8
|
+
const p_freq = 1 / pluginCount;
|
|
9
|
+
return h + p_freq * Math.log2(p_freq);
|
|
10
|
+
}, 0)
|
|
11
|
+
: 0;
|
|
12
|
+
|
|
13
|
+
return `count:${pluginCount}|H(plugins)=${pluginEntropy.toFixed(3)}|plugins:${pluginStr || "none"}`;
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export async function getPreferencesEntropy(): Promise<string> {
|
|
2
|
+
const darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
3
|
+
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
4
|
+
const highContrast = window.matchMedia("(prefers-contrast: more)").matches;
|
|
5
|
+
|
|
6
|
+
const prefCount = [darkMode, reducedMotion, highContrast].filter(Boolean).length;
|
|
7
|
+
const maxCombinations = Math.pow(2, 3);
|
|
8
|
+
const preferenceBits = Math.log2(maxCombinations);
|
|
9
|
+
const actualEntropy = prefCount > 0 ? -((prefCount / 3) * Math.log2(prefCount / 3) + ((3 - prefCount) / 3) * Math.log2((3 - prefCount) / 3)) : 0;
|
|
10
|
+
|
|
11
|
+
return `dark:${darkMode}|motion:${reducedMotion}|contrast:${highContrast}|enabled:${prefCount}|H(pref)=${actualEntropy.toFixed(3)}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export async function getProbabilisticEntropy(): Promise<string> {
|
|
2
|
+
const metrics = [];
|
|
3
|
+
|
|
4
|
+
const screen_values = [screen.width, screen.height, screen.colorDepth];
|
|
5
|
+
const hw_values = [navigator.hardwareConcurrency || 1, (navigator as any).deviceMemory || 4];
|
|
6
|
+
const all_values = [...screen_values, ...hw_values];
|
|
7
|
+
|
|
8
|
+
const mean = all_values.reduce((a, b) => a + b) / all_values.length;
|
|
9
|
+
const sorted = [...all_values].sort((a, b) => a - b);
|
|
10
|
+
const median = sorted[Math.floor(sorted.length / 2)];
|
|
11
|
+
const q1 = sorted[Math.floor(sorted.length / 4)];
|
|
12
|
+
const q3 = sorted[Math.floor(sorted.length * 3 / 4)];
|
|
13
|
+
const iqr = q3 - q1;
|
|
14
|
+
|
|
15
|
+
metrics.push(`mean:${mean.toFixed(2)}`);
|
|
16
|
+
metrics.push(`median:${median.toFixed(2)}`);
|
|
17
|
+
metrics.push(`iqr:${iqr.toFixed(2)}`);
|
|
18
|
+
|
|
19
|
+
return metrics.join("|");
|
|
20
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export async function getScreenEntropy(): Promise<string> {
|
|
2
|
+
const w = screen.width;
|
|
3
|
+
const h = screen.height;
|
|
4
|
+
const d = screen.colorDepth;
|
|
5
|
+
|
|
6
|
+
const pixelCount = w * h;
|
|
7
|
+
const totalColors = Math.pow(2, d);
|
|
8
|
+
const colorSpace = Math.log2(totalColors);
|
|
9
|
+
const screenSurface = Math.log2(pixelCount);
|
|
10
|
+
|
|
11
|
+
return `${w}x${h}|colors:${d}|log₂(A)=${screenSurface.toFixed(2)}|log₂(C)=${colorSpace.toFixed(2)}`;
|
|
12
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function getScreenInfoEntropy(): Promise<string> {
|
|
2
|
+
const width = screen.width;
|
|
3
|
+
const height = screen.height;
|
|
4
|
+
const depth = screen.colorDepth;
|
|
5
|
+
const pixelDepth = screen.pixelDepth;
|
|
6
|
+
const devicePixelRatio = window.devicePixelRatio;
|
|
7
|
+
return `${width}x${height}|depth:${depth}|px:${pixelDepth}|ratio:${devicePixelRatio}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { spectral } from "./veil_core.js";
|
|
2
|
+
import { seededRng } from "../seeded-rng";
|
|
3
|
+
|
|
4
|
+
export async function getSpectralEntropy(seed: string): Promise<string> {
|
|
5
|
+
const samples = seededRng(seed, 128);
|
|
6
|
+
const entropy = spectral(new Float64Array(samples));
|
|
7
|
+
return `spectral:${entropy.toFixed(6)}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export async function getStatisticalEntropy(): Promise<string> {
|
|
2
|
+
const ua = navigator.userAgent;
|
|
3
|
+
const freq: Record<string, number> = {};
|
|
4
|
+
|
|
5
|
+
for (const char of ua) {
|
|
6
|
+
freq[char] = (freq[char] || 0) + 1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const values = Object.values(freq);
|
|
10
|
+
const mean = values.reduce((a, b) => a + b) / values.length;
|
|
11
|
+
const variance = values.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / values.length;
|
|
12
|
+
const stdDev = Math.sqrt(variance);
|
|
13
|
+
|
|
14
|
+
return `mean:${mean.toFixed(4)}|var:${variance.toFixed(4)}|std:${stdDev.toFixed(4)}`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export async function getStorageEntropy(): Promise<string> {
|
|
2
|
+
let ls = false;
|
|
3
|
+
let idb = false;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const test = "__storage_test__";
|
|
7
|
+
window.localStorage.setItem(test, test);
|
|
8
|
+
window.localStorage.removeItem(test);
|
|
9
|
+
ls = true;
|
|
10
|
+
} catch {
|
|
11
|
+
ls = false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const request = window.indexedDB.open("__idb_test__");
|
|
16
|
+
idb = true;
|
|
17
|
+
} catch {
|
|
18
|
+
idb = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return `localStorage:${ls}|indexedDB:${idb}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export async function getTimezoneEntropy(): Promise<string> {
|
|
2
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3
|
+
const offset = new Date().getTimezoneOffset();
|
|
4
|
+
|
|
5
|
+
const tzEntropy = Math.log2(Math.abs(offset) + 1);
|
|
6
|
+
const offsetBits = offset.toString().length * 8;
|
|
7
|
+
const tzUniqueness = Math.log2(24 * 60 / Math.max(Math.abs(offset), 1));
|
|
8
|
+
|
|
9
|
+
return `TZ:${tz}|offset:${offset}|H(TZ)=${tzEntropy.toFixed(3)}|U=${tzUniqueness.toFixed(3)}`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export async function getUserAgentEntropy(): Promise<string> {
|
|
2
|
+
const ua = navigator.userAgent;
|
|
3
|
+
|
|
4
|
+
let entropy = 0;
|
|
5
|
+
const freq: Record<string, number> = {};
|
|
6
|
+
for (const char of ua) {
|
|
7
|
+
freq[char] = (freq[char] || 0) + 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
for (const count of Object.values(freq)) {
|
|
11
|
+
const p = count / ua.length;
|
|
12
|
+
entropy -= p * Math.log2(p);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${ua}|H(UA)=${entropy.toFixed(4)}`;
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export async function getWebFeaturesEntropy(): Promise<string> {
|
|
2
|
+
const features = {
|
|
3
|
+
localStorage: !!window.localStorage,
|
|
4
|
+
sessionStorage: !!window.sessionStorage,
|
|
5
|
+
indexedDB: !!window.indexedDB,
|
|
6
|
+
openDatabase: !!(window as any).openDatabase,
|
|
7
|
+
serviceWorker: !!navigator.serviceWorker,
|
|
8
|
+
webWorker: typeof Worker !== "undefined",
|
|
9
|
+
geolocation: !!navigator.geolocation,
|
|
10
|
+
notifications: !!window.Notification,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const enabled = Object.values(features).filter(Boolean).length;
|
|
14
|
+
const total = Object.keys(features).length;
|
|
15
|
+
const supportRatio = enabled / total;
|
|
16
|
+
const supportEntropy = Math.log2(total) * (1 - Math.abs(supportRatio - 0.5) * 2);
|
|
17
|
+
|
|
18
|
+
return `enabled:${enabled}/${total}|σ=${supportRatio.toFixed(3)}|H(features)=${supportEntropy.toFixed(3)}|${Object.entries(features)
|
|
19
|
+
.map(([k, v]) => `${k}:${v}`)
|
|
20
|
+
.join("|")}`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export async function getWebGLEntropy(): Promise<string> {
|
|
2
|
+
const canvas = document.createElement("canvas");
|
|
3
|
+
const gl = canvas.getContext("webgl") || canvas.getContext("webgl2");
|
|
4
|
+
if (!gl) return "webgl:unavailable";
|
|
5
|
+
|
|
6
|
+
const vendor = gl.getParameter(gl.VENDOR) || "unknown";
|
|
7
|
+
const renderer = gl.getParameter(gl.RENDERER) || "unknown";
|
|
8
|
+
const version = gl.getParameter(gl.VERSION) || "unknown";
|
|
9
|
+
|
|
10
|
+
return `vendor:${vendor}|renderer:${renderer}|version:${version}`;
|
|
11
|
+
}
|
package/src/gpu.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export async function runGPUBenchmark(): Promise<{
|
|
2
|
+
renderTime: number;
|
|
3
|
+
textureOps: number;
|
|
4
|
+
shaderPerformance: number;
|
|
5
|
+
}> {
|
|
6
|
+
const canvas = document.createElement("canvas");
|
|
7
|
+
canvas.width = 512;
|
|
8
|
+
canvas.height = 512;
|
|
9
|
+
|
|
10
|
+
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
|
11
|
+
if (!gl) {
|
|
12
|
+
return { renderTime: 0, textureOps: 0, shaderPerformance: 0 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const start = performance.now();
|
|
16
|
+
|
|
17
|
+
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
|
|
18
|
+
if (!vertexShader) return { renderTime: 0, textureOps: 0, shaderPerformance: 0 };
|
|
19
|
+
|
|
20
|
+
gl.shaderSource(
|
|
21
|
+
vertexShader,
|
|
22
|
+
`
|
|
23
|
+
attribute vec2 position;
|
|
24
|
+
void main() {
|
|
25
|
+
gl_Position = vec4(position, 0.0, 1.0);
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
);
|
|
29
|
+
gl.compileShader(vertexShader);
|
|
30
|
+
|
|
31
|
+
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
|
|
32
|
+
if (!fragmentShader) return { renderTime: 0, textureOps: 0, shaderPerformance: 0 };
|
|
33
|
+
|
|
34
|
+
gl.shaderSource(
|
|
35
|
+
fragmentShader,
|
|
36
|
+
`
|
|
37
|
+
precision highp float;
|
|
38
|
+
uniform sampler2D tex;
|
|
39
|
+
void main() {
|
|
40
|
+
gl_FragColor = texture2D(tex, vec2(0.5, 0.5));
|
|
41
|
+
}
|
|
42
|
+
`,
|
|
43
|
+
);
|
|
44
|
+
gl.compileShader(fragmentShader);
|
|
45
|
+
|
|
46
|
+
const program = gl.createProgram();
|
|
47
|
+
if (!program) return { renderTime: 0, textureOps: 0, shaderPerformance: 0 };
|
|
48
|
+
|
|
49
|
+
gl.attachShader(program, vertexShader);
|
|
50
|
+
gl.attachShader(program, fragmentShader);
|
|
51
|
+
gl.linkProgram(program);
|
|
52
|
+
gl.useProgram(program);
|
|
53
|
+
|
|
54
|
+
const positionBuffer = gl.createBuffer();
|
|
55
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
|
|
56
|
+
gl.bufferData(
|
|
57
|
+
gl.ARRAY_BUFFER,
|
|
58
|
+
new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]),
|
|
59
|
+
gl.STATIC_DRAW,
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
const texture = gl.createTexture();
|
|
63
|
+
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
64
|
+
gl.texImage2D(
|
|
65
|
+
gl.TEXTURE_2D,
|
|
66
|
+
0,
|
|
67
|
+
gl.RGBA,
|
|
68
|
+
256,
|
|
69
|
+
256,
|
|
70
|
+
0,
|
|
71
|
+
gl.RGBA,
|
|
72
|
+
gl.UNSIGNED_BYTE,
|
|
73
|
+
new Uint8Array(256 * 256 * 4).fill(128),
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < 100; i++) {
|
|
77
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
78
|
+
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4));
|
|
82
|
+
|
|
83
|
+
const renderTime = performance.now() - start;
|
|
84
|
+
|
|
85
|
+
const textureOps = 100;
|
|
86
|
+
const shaderPerformance = 512 * 512 * 100 / (renderTime || 1);
|
|
87
|
+
|
|
88
|
+
gl.deleteShader(vertexShader);
|
|
89
|
+
gl.deleteShader(fragmentShader);
|
|
90
|
+
gl.deleteProgram(program);
|
|
91
|
+
gl.deleteBuffer(positionBuffer);
|
|
92
|
+
gl.deleteTexture(texture);
|
|
93
|
+
|
|
94
|
+
canvas.remove();
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
renderTime,
|
|
98
|
+
textureOps,
|
|
99
|
+
shaderPerformance,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getGPUHash(): Promise<string> {
|
|
104
|
+
const benchmark = await runGPUBenchmark();
|
|
105
|
+
const canvas = document.createElement("canvas");
|
|
106
|
+
canvas.width = 256;
|
|
107
|
+
canvas.height = 256;
|
|
108
|
+
|
|
109
|
+
const ctx = canvas.getContext("2d");
|
|
110
|
+
if (!ctx) return "";
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < 256; i++) {
|
|
113
|
+
for (let j = 0; j < 256; j++) {
|
|
114
|
+
const hue = (benchmark.renderTime * (i + j)) % 360;
|
|
115
|
+
const sat = (benchmark.shaderPerformance * i) % 100;
|
|
116
|
+
const lum = (benchmark.textureOps * j) % 100;
|
|
117
|
+
|
|
118
|
+
ctx.fillStyle = `hsl(${hue}, ${sat}%, ${lum}%)`;
|
|
119
|
+
ctx.fillRect(i, j, 1, 1);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const imageData = ctx.getImageData(0, 0, 256, 256);
|
|
124
|
+
const data = imageData.data;
|
|
125
|
+
|
|
126
|
+
let hash = 0;
|
|
127
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
128
|
+
hash ^= ((data[i] + data[i + 1] + data[i + 2]) * (i * 1)) >> 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return hash.toString(16);
|
|
132
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getFingerprint } from "./index";
|
|
3
|
+
|
|
4
|
+
describe("veil fingerprinting", () => {
|
|
5
|
+
it("should generate consistent fingerprint", async () => {
|
|
6
|
+
const fp1 = await getFingerprint();
|
|
7
|
+
const fp2 = await getFingerprint();
|
|
8
|
+
expect(fp1).toBe(fp2);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("should generate different fingerprints with different options", async () => {
|
|
12
|
+
const fp1 = await getFingerprint({ entropy: { canvas: true } });
|
|
13
|
+
const fp2 = await getFingerprint({ entropy: { canvas: false } });
|
|
14
|
+
expect(fp1).not.toBe(fp2);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("should support SHA-512", async () => {
|
|
18
|
+
const fp = await getFingerprint({ hash: "sha512" });
|
|
19
|
+
expect(fp.length).toBeGreaterThan(64);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return hex string", async () => {
|
|
23
|
+
const fp = await getFingerprint();
|
|
24
|
+
expect(/^[a-f0-9]+$/.test(fp)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { FingerprintOptions, FingerprintResponse, SourceMetric } from "./types";
|
|
2
|
+
import { getUserAgentEntropy } from "./entropy/userAgent";
|
|
3
|
+
import { getCanvasEntropy } from "./entropy/canvas";
|
|
4
|
+
import { getWebGLEntropy } from "./entropy/webgl";
|
|
5
|
+
import { getFontsEntropy } from "./entropy/fonts";
|
|
6
|
+
import { getStorageEntropy } from "./entropy/storage";
|
|
7
|
+
import { getScreenEntropy } from "./entropy/screen";
|
|
8
|
+
import { getDistributionEntropy } from "./entropy/distribution";
|
|
9
|
+
import { getComplexityEntropy } from "./entropy/complexity";
|
|
10
|
+
import { getSpectralEntropy } from "./entropy/spectral";
|
|
11
|
+
import { getApproximateEntropy } from "./entropy/approximate";
|
|
12
|
+
import { getOSEntropy } from "./entropy/os";
|
|
13
|
+
import { getLanguageEntropy } from "./entropy/language";
|
|
14
|
+
import { getTimezoneEntropy } from "./entropy/timezone";
|
|
15
|
+
import { getHardwareEntropy } from "./entropy/hardware";
|
|
16
|
+
import { getPluginsEntropy } from "./entropy/plugins";
|
|
17
|
+
import { getBrowserEntropy } from "./entropy/browser";
|
|
18
|
+
import { getOSVersionEntropy } from "./entropy/osVersion";
|
|
19
|
+
import { getScreenInfoEntropy } from "./entropy/screenInfo";
|
|
20
|
+
import { getAdblockEntropy } from "./entropy/adblock";
|
|
21
|
+
import { getWebFeaturesEntropy } from "./entropy/webFeatures";
|
|
22
|
+
import { getPreferencesEntropy } from "./entropy/preferences";
|
|
23
|
+
import { getPermissionsEntropy } from "./entropy/permissions";
|
|
24
|
+
import { getStatisticalEntropy } from "./entropy/statistical";
|
|
25
|
+
import { getProbabilisticEntropy } from "./entropy/probabilistic";
|
|
26
|
+
import {
|
|
27
|
+
murmur_hash,
|
|
28
|
+
fnv_hash,
|
|
29
|
+
shannon_entropy,
|
|
30
|
+
kolmogorov_complexity,
|
|
31
|
+
} from "./veil_core.js";
|
|
32
|
+
import * as normalize from "./normalize";
|
|
33
|
+
import { initializeWasm } from "./wasm-loader";
|
|
34
|
+
import { scoreFingerprint, calculateEntropy, type EntropySource } from "./scoring";
|
|
35
|
+
import { getGPUHash, runGPUBenchmark } from "./gpu";
|
|
36
|
+
import { getCPUHash, runCPUBenchmark } from "./cpu";
|
|
37
|
+
import { seededRng } from "./seeded-rng";
|
|
38
|
+
|
|
39
|
+
export async function getFingerprint(
|
|
40
|
+
options?: FingerprintOptions,
|
|
41
|
+
): Promise<string | FingerprintResponse> {
|
|
42
|
+
await initializeWasm();
|
|
43
|
+
|
|
44
|
+
const opts = {
|
|
45
|
+
entropy: {},
|
|
46
|
+
hash: "sha256",
|
|
47
|
+
...options,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const sources: EntropySource[] = [];
|
|
51
|
+
|
|
52
|
+
if (opts.entropy.userAgent !== false) {
|
|
53
|
+
const value = await getUserAgentEntropy();
|
|
54
|
+
sources.push({ name: "userAgent", value, entropy: calculateEntropy(value) });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (opts.entropy.canvas !== false) {
|
|
58
|
+
const value = await getCanvasEntropy();
|
|
59
|
+
sources.push({ name: "canvas", value, entropy: calculateEntropy(value) });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (opts.entropy.webgl !== false) {
|
|
63
|
+
const value = await getWebGLEntropy();
|
|
64
|
+
sources.push({ name: "webgl", value, entropy: calculateEntropy(value) });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (opts.entropy.fonts !== false) {
|
|
68
|
+
const value = await getFontsEntropy();
|
|
69
|
+
sources.push({ name: "fonts", value, entropy: calculateEntropy(value) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (opts.entropy.storage !== false) {
|
|
73
|
+
const value = await getStorageEntropy();
|
|
74
|
+
sources.push({ name: "storage", value, entropy: calculateEntropy(value) });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (opts.entropy.screen !== false) {
|
|
78
|
+
const value = await getScreenEntropy();
|
|
79
|
+
sources.push({ name: "screen", value, entropy: calculateEntropy(value) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const baseSeed = sources.map(s => s.value).join("|");
|
|
83
|
+
|
|
84
|
+
sources.push({ name: "distribution", value: await getDistributionEntropy(baseSeed), entropy: 0 });
|
|
85
|
+
sources.push({ name: "complexity", value: await getComplexityEntropy(), entropy: 0 });
|
|
86
|
+
sources.push({ name: "spectral", value: await getSpectralEntropy(baseSeed), entropy: 0 });
|
|
87
|
+
sources.push({ name: "approximate", value: await getApproximateEntropy(baseSeed), entropy: 0 });
|
|
88
|
+
sources.push({ name: "os", value: await getOSEntropy(), entropy: 0 });
|
|
89
|
+
sources.push({ name: "language", value: await getLanguageEntropy(), entropy: 0 });
|
|
90
|
+
sources.push({ name: "timezone", value: await getTimezoneEntropy(), entropy: 0 });
|
|
91
|
+
sources.push({ name: "hardware", value: await getHardwareEntropy(), entropy: 0 });
|
|
92
|
+
sources.push({ name: "plugins", value: await getPluginsEntropy(), entropy: 0 });
|
|
93
|
+
sources.push({ name: "browser", value: await getBrowserEntropy(), entropy: 0 });
|
|
94
|
+
sources.push({ name: "osVersion", value: await getOSVersionEntropy(), entropy: 0 });
|
|
95
|
+
sources.push({ name: "screenInfo", value: await getScreenInfoEntropy(), entropy: 0 });
|
|
96
|
+
sources.push({ name: "adblock", value: await getAdblockEntropy(), entropy: 0 });
|
|
97
|
+
sources.push({ name: "webFeatures", value: await getWebFeaturesEntropy(), entropy: 0 });
|
|
98
|
+
sources.push({ name: "preferences", value: await getPreferencesEntropy(), entropy: 0 });
|
|
99
|
+
sources.push({ name: "permissions", value: await getPermissionsEntropy(), entropy: 0 });
|
|
100
|
+
sources.push({ name: "statistical", value: await getStatisticalEntropy(), entropy: 0 });
|
|
101
|
+
sources.push({ name: "probabilistic", value: await getProbabilisticEntropy(), entropy: 0 });
|
|
102
|
+
|
|
103
|
+
for (const source of sources) {
|
|
104
|
+
if (source.entropy === 0) {
|
|
105
|
+
source.entropy = calculateEntropy(source.value);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const score = scoreFingerprint(sources);
|
|
110
|
+
|
|
111
|
+
const data = sources.map(s => s.value);
|
|
112
|
+
|
|
113
|
+
const dataStr = data.join("|");
|
|
114
|
+
const shannon = shannon_entropy(dataStr);
|
|
115
|
+
const kolmogorov = kolmogorov_complexity(dataStr);
|
|
116
|
+
const murmur = murmur_hash(dataStr);
|
|
117
|
+
const fnv = fnv_hash(dataStr);
|
|
118
|
+
|
|
119
|
+
const mathMetrics = `${shannon}|${kolmogorov}|${murmur}|${fnv}`;
|
|
120
|
+
const combined = dataStr + "|" + mathMetrics;
|
|
121
|
+
|
|
122
|
+
const algorithm = opts.hash === "sha512" ? "SHA-512" : "SHA-256";
|
|
123
|
+
const hash = await crypto.subtle.digest(
|
|
124
|
+
algorithm,
|
|
125
|
+
new TextEncoder().encode(combined),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
let fingerprint = Array.from(new Uint8Array(hash))
|
|
129
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
130
|
+
.join("");
|
|
131
|
+
|
|
132
|
+
if (opts.gpuBenchmark) {
|
|
133
|
+
const gpuHash = await getGPUHash();
|
|
134
|
+
fingerprint = Array.from(new Uint8Array(
|
|
135
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(fingerprint + gpuHash))
|
|
136
|
+
))
|
|
137
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
138
|
+
.join("");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (opts.cpuBenchmark) {
|
|
142
|
+
const cpuHash = await getCPUHash();
|
|
143
|
+
fingerprint = Array.from(new Uint8Array(
|
|
144
|
+
await crypto.subtle.digest("SHA-256", new TextEncoder().encode(fingerprint + cpuHash))
|
|
145
|
+
))
|
|
146
|
+
.map((b) => b.toString(16).padStart(2, "0"))
|
|
147
|
+
.join("");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (opts.detailed) {
|
|
151
|
+
const os = await getOSVersionEntropy();
|
|
152
|
+
const lang = await getLanguageEntropy();
|
|
153
|
+
const tz = await getTimezoneEntropy();
|
|
154
|
+
const hw = await getHardwareEntropy();
|
|
155
|
+
const sr = await getScreenInfoEntropy();
|
|
156
|
+
const ua = await getUserAgentEntropy();
|
|
157
|
+
const browser = await getBrowserEntropy();
|
|
158
|
+
|
|
159
|
+
const sourceMetrics: SourceMetric[] = sources.map(s => ({
|
|
160
|
+
source: s.name,
|
|
161
|
+
value: s.value,
|
|
162
|
+
entropy: s.entropy,
|
|
163
|
+
confidence: score.likelihood > 0 ? Math.min(s.entropy / score.likelihood, 1) : 0,
|
|
164
|
+
}));
|
|
165
|
+
|
|
166
|
+
const response: FingerprintResponse = {
|
|
167
|
+
hash: fingerprint,
|
|
168
|
+
uniqueness: score.uniqueness,
|
|
169
|
+
confidence: score.confidence,
|
|
170
|
+
sources: sourceMetrics,
|
|
171
|
+
system: {
|
|
172
|
+
os,
|
|
173
|
+
language: lang.split("|")[0],
|
|
174
|
+
timezone: tz.split("|")[0],
|
|
175
|
+
hardware: {
|
|
176
|
+
cores: navigator.hardwareConcurrency || 0,
|
|
177
|
+
memory: (navigator as any).deviceMemory || 0,
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
display: {
|
|
181
|
+
resolution: `${screen.width}x${screen.height}`,
|
|
182
|
+
colorDepth: screen.colorDepth,
|
|
183
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
184
|
+
},
|
|
185
|
+
browser: {
|
|
186
|
+
userAgent: navigator.userAgent,
|
|
187
|
+
vendor: navigator.vendor,
|
|
188
|
+
cookieEnabled: navigator.cookieEnabled,
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
return response;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return fingerprint;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export type { FingerprintOptions, FingerprintResponse } from "./types";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function normalizeScreen(width: number, height: number): string {
|
|
2
|
+
const bucketSize = 100;
|
|
3
|
+
const bw = Math.floor(width / bucketSize) * bucketSize;
|
|
4
|
+
const bh = Math.floor(height / bucketSize) * bucketSize;
|
|
5
|
+
return `${bw}x${bh}`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function normalizeTimezone(tz: string): string {
|
|
9
|
+
return tz.split("/")[0];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function normalizeUserAgent(ua: string): string {
|
|
13
|
+
const parts = ua.split(" ");
|
|
14
|
+
return parts.slice(0, 3).join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function normalizeFloat(num: number, decimals: number = 10): string {
|
|
18
|
+
return num.toFixed(decimals);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function normalizeCanvas(dataUrl: string): string {
|
|
22
|
+
return dataUrl.substring(0, 100);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function normalizeStorage(storageStr: string): string {
|
|
26
|
+
const [localStorage, indexedDB] = storageStr.split("|");
|
|
27
|
+
const ls = localStorage.includes("true") ? "1" : "0";
|
|
28
|
+
const idb = indexedDB.includes("true") ? "1" : "0";
|
|
29
|
+
|
|
30
|
+
return `${ls}${idb}`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function calculateMean(values: number[]): number {
|
|
2
|
+
if (values.length === 0) return 0;
|
|
3
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function hellingerDistance(p: number[], q: number[]): number {
|
|
7
|
+
if (p.length !== q.length) return 0;
|
|
8
|
+
|
|
9
|
+
const sum = p.reduce((s, pi, i) => s + Math.pow(Math.sqrt(pi) - Math.sqrt(q[i]), 2), 0);
|
|
10
|
+
return Math.sqrt(sum / 2);
|
|
11
|
+
}
|