get-browser-fingerprint 4.1.1 → 5.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.
- package/README.md +33 -4
- package/package.json +12 -7
- package/src/index.d.ts +5 -2
- package/src/index.js +317 -298
package/README.md
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
# get-browser-fingerprint
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A single and fast (<50ms) asynchronous function returning a browser fingerprint, without requiring any permission to the user.
|
|
4
|
+
|
|
5
|
+
## Assumptions
|
|
6
|
+
|
|
7
|
+
The function is targeting stock installations of the following browsers (no extensions or add-ons installed):
|
|
8
|
+
- Google Chrome (desktop and Android)
|
|
9
|
+
- Mozilla Firefox (desktop, default tracking protection, no custom privacy flags enabled)
|
|
10
|
+
- Microsoft Edge (desktop and Android)
|
|
11
|
+
- Apple Safari (macOS)
|
|
12
|
+
- Safari on iOS / iPadOS
|
|
13
|
+
|
|
14
|
+
Explicit assumptions:
|
|
15
|
+
- No privacy-focused extensions or add-ons installed.
|
|
16
|
+
- Browser privacy settings left at factory defaults (no manual changes to fingerprint resistance features).
|
|
17
|
+
- No hardened, anti-detect or heavily modified browser variants.
|
|
18
|
+
|
|
19
|
+
Under these conditions the collected signals retain meaningful entropy in February 2026:
|
|
20
|
+
- Canvas rendering (stable on Chrome, Edge, Firefox default; noisy or low-entropy on iOS Safari and partially on macOS Safari)
|
|
21
|
+
- WebGL unmasked vendor and renderer (strong on desktop, more uniform on mobile)
|
|
22
|
+
- Offline audio context (useful on Chrome, Edge, Firefox; heavily restricted on Safari)
|
|
23
|
+
- WebGPU properties (if available; low but increasing entropy)
|
|
24
|
+
- Locally installed fonts (valuable mainly on desktop Windows and macOS)
|
|
25
|
+
- Passive signals (hardwareConcurrency, deviceMemory, screen, timezone, languages, etc.)
|
|
26
|
+
|
|
27
|
+
On browsers with strong default fingerprint resistance (Safari iOS, certain Firefox modes) entropy and stability decrease significantly.
|
|
28
|
+
The script does not attempt to bypass intentional randomization or hardening.
|
|
4
29
|
|
|
5
30
|
## Usage
|
|
6
31
|
|
|
7
|
-
Get browser fingerprint:
|
|
32
|
+
Get browser fingerprint:
|
|
8
33
|
```js
|
|
9
34
|
import getBrowserFingerprint from 'get-browser-fingerprint';
|
|
10
35
|
const fingerprint = await getBrowserFingerprint();
|
|
@@ -19,9 +44,13 @@ Options available:
|
|
|
19
44
|
|
|
20
45
|
To test locally:
|
|
21
46
|
```sh
|
|
22
|
-
fnm install
|
|
23
|
-
|
|
47
|
+
fnm install # nodejs from .nvmrc
|
|
48
|
+
npm install -g corepack
|
|
49
|
+
corepack enable # package manager from package.json
|
|
50
|
+
corepack install # package manager from package.json
|
|
51
|
+
pnpm install # install deps
|
|
24
52
|
pnpm exec playwright install chromium
|
|
53
|
+
pnpm exec playwright install firefox
|
|
25
54
|
pnpm test
|
|
26
55
|
```
|
|
27
56
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get-browser-fingerprint",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"author": "Damiano Barbati <damiano.barbati@gmail.com> (https://github.com/damianobarbati)",
|
|
5
5
|
"repository": "https://github.com/damianobarbati/get-browser-fingerprint",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"
|
|
7
|
+
"packageManager": "pnpm@10.28.2",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=25.6",
|
|
10
|
+
"pnpm": ">=10.28"
|
|
11
|
+
},
|
|
8
12
|
"type": "module",
|
|
13
|
+
"main": "src/index.js",
|
|
9
14
|
"files": [
|
|
10
15
|
"README.md",
|
|
11
16
|
"package.json",
|
|
@@ -14,13 +19,13 @@
|
|
|
14
19
|
],
|
|
15
20
|
"types": "./src/index.d.ts",
|
|
16
21
|
"scripts": {
|
|
22
|
+
"postinstall": "pnpm audit --audit-level critical",
|
|
17
23
|
"lint": "biome check --write",
|
|
18
|
-
"test": "
|
|
24
|
+
"test": "node --test --test-reporter=spec --test-reporter-destination=stdout"
|
|
19
25
|
},
|
|
20
26
|
"devDependencies": {
|
|
21
|
-
"@biomejs/biome": "^2.
|
|
22
|
-
"@playwright/test": "^1.
|
|
23
|
-
"serve": "^14.2.
|
|
24
|
-
"vitest": "^3.2.4"
|
|
27
|
+
"@biomejs/biome": "^2.4.0",
|
|
28
|
+
"@playwright/test": "^1.58.2",
|
|
29
|
+
"serve": "^14.2.5"
|
|
25
30
|
}
|
|
26
31
|
}
|
package/src/index.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export interface FingerprintOptions {
|
|
2
|
-
hardwareOnly?: boolean;
|
|
3
2
|
debug?: boolean;
|
|
4
3
|
}
|
|
5
4
|
|
|
6
|
-
export
|
|
5
|
+
export type FingerprintResult = {
|
|
6
|
+
fingerprint: string;
|
|
7
|
+
} & Record<string, string>;
|
|
8
|
+
|
|
9
|
+
export default function getBrowserFingerprint(options?: FingerprintOptions): Promise<FingerprintResult>;
|
|
7
10
|
|
|
8
11
|
declare global {
|
|
9
12
|
interface Window {
|
package/src/index.js
CHANGED
|
@@ -1,339 +1,358 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
const murmurHash3 = (key, seed = 0) => {
|
|
2
|
+
let h = seed ^ key.length;
|
|
3
|
+
|
|
4
|
+
let i = 0;
|
|
5
|
+
const len = key.length;
|
|
6
|
+
|
|
7
|
+
while (i + 3 < len) {
|
|
8
|
+
let k = (key.charCodeAt(i) & 0xff) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24);
|
|
9
|
+
k = Math.imul(k, 0xcc9e2d51);
|
|
10
|
+
k = (k << 15) | (k >>> 17); // ROTL32(k, 15)
|
|
11
|
+
k = Math.imul(k, 0x1b873593);
|
|
12
|
+
h ^= k;
|
|
13
|
+
h = (h << 13) | (h >>> 19); // ROTL32(h, 13)
|
|
14
|
+
h = Math.imul(h, 5) + 0xe6546b64;
|
|
15
|
+
i += 4;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// tail
|
|
19
|
+
let k1 = 0;
|
|
20
|
+
switch (len - i) {
|
|
21
|
+
// biome-ignore lint/suspicious/noFallthroughSwitchClause: ignore
|
|
22
|
+
case 3:
|
|
23
|
+
k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
|
|
24
|
+
// biome-ignore lint/suspicious/noFallthroughSwitchClause: ignore
|
|
25
|
+
case 2:
|
|
26
|
+
k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
|
|
27
|
+
case 1:
|
|
28
|
+
k1 ^= key.charCodeAt(i) & 0xff;
|
|
29
|
+
k1 = Math.imul(k1, 0xcc9e2d51);
|
|
30
|
+
k1 = (k1 << 15) | (k1 >>> 17);
|
|
31
|
+
k1 = Math.imul(k1, 0x1b873593);
|
|
32
|
+
h ^= k1;
|
|
33
|
+
}
|
|
4
34
|
|
|
5
|
-
|
|
6
|
-
|
|
35
|
+
h ^= h >>> 16;
|
|
36
|
+
h = Math.imul(h, 0x85ebca6b);
|
|
37
|
+
h ^= h >>> 13;
|
|
38
|
+
h = Math.imul(h, 0xc2b2ae35);
|
|
39
|
+
h ^= h >>> 16;
|
|
40
|
+
h = h >>> 0; // force unsigned 32 bit
|
|
7
41
|
|
|
8
|
-
const
|
|
9
|
-
const timezoneOffset = new Date().getTimezoneOffset();
|
|
10
|
-
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
11
|
-
const touchSupport = "ontouchstart" in window;
|
|
12
|
-
const devicePixelRatio = window.devicePixelRatio;
|
|
13
|
-
|
|
14
|
-
const canvas = getCanvasID(debug);
|
|
15
|
-
const audio = await getAudioID(debug);
|
|
16
|
-
const audioInfo = getAudioInfo();
|
|
17
|
-
const webgl = getWebglID(debug);
|
|
18
|
-
const webglInfo = getWebglInfo();
|
|
19
|
-
|
|
20
|
-
const data = hardwareOnly
|
|
21
|
-
? {
|
|
22
|
-
audioInfo,
|
|
23
|
-
audio,
|
|
24
|
-
canvas,
|
|
25
|
-
colorDepth,
|
|
26
|
-
deviceMemory,
|
|
27
|
-
devicePixelRatio,
|
|
28
|
-
hardwareConcurrency,
|
|
29
|
-
height,
|
|
30
|
-
maxTouchPoints,
|
|
31
|
-
pixelDepth,
|
|
32
|
-
platform,
|
|
33
|
-
touchSupport,
|
|
34
|
-
webgl,
|
|
35
|
-
webglInfo,
|
|
36
|
-
width,
|
|
37
|
-
}
|
|
38
|
-
: {
|
|
39
|
-
audioInfo,
|
|
40
|
-
audio,
|
|
41
|
-
canvas,
|
|
42
|
-
colorDepth,
|
|
43
|
-
cookieEnabled,
|
|
44
|
-
deviceMemory,
|
|
45
|
-
devicePixelRatio,
|
|
46
|
-
doNotTrack,
|
|
47
|
-
hardwareConcurrency,
|
|
48
|
-
height,
|
|
49
|
-
language,
|
|
50
|
-
languages,
|
|
51
|
-
maxTouchPoints,
|
|
52
|
-
pixelDepth,
|
|
53
|
-
platform,
|
|
54
|
-
timezone,
|
|
55
|
-
timezoneOffset,
|
|
56
|
-
touchSupport,
|
|
57
|
-
userAgent,
|
|
58
|
-
vendor,
|
|
59
|
-
webgl,
|
|
60
|
-
webglInfo,
|
|
61
|
-
width,
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
if (debug) console.log("Fingerprint data:", JSON.stringify(data, null, 2));
|
|
65
|
-
|
|
66
|
-
const payload = JSON.stringify(data, null, 2);
|
|
67
|
-
const result = murmurhash3_32_gc(payload);
|
|
42
|
+
const result = h.toString(16).padStart(8, '0');
|
|
68
43
|
return result;
|
|
69
44
|
};
|
|
70
45
|
|
|
71
|
-
const
|
|
46
|
+
const safe = async (fn) => {
|
|
72
47
|
try {
|
|
73
|
-
|
|
74
|
-
const ctx = canvas.getContext("2d");
|
|
75
|
-
const text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?";
|
|
76
|
-
ctx.textBaseline = "top";
|
|
77
|
-
ctx.font = "14px 'Arial'";
|
|
78
|
-
ctx.textBaseline = "alphabetic";
|
|
79
|
-
ctx.fillStyle = "#f60";
|
|
80
|
-
ctx.fillRect(125, 1, 62, 20);
|
|
81
|
-
ctx.fillStyle = "#069";
|
|
82
|
-
ctx.fillText(text, 2, 15);
|
|
83
|
-
ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
|
|
84
|
-
ctx.fillText(text, 4, 17);
|
|
85
|
-
|
|
86
|
-
const result = canvas.toDataURL();
|
|
87
|
-
|
|
88
|
-
if (debug) {
|
|
89
|
-
document.body.appendChild(canvas);
|
|
90
|
-
} else {
|
|
91
|
-
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return murmurhash3_32_gc(result);
|
|
48
|
+
return await fn();
|
|
95
49
|
} catch {
|
|
96
50
|
return null;
|
|
97
51
|
}
|
|
98
52
|
};
|
|
99
53
|
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
ctx.bindBuffer(ctx.ARRAY_BUFFER, h);
|
|
113
|
-
|
|
114
|
-
const i = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]);
|
|
115
|
-
|
|
116
|
-
ctx.bufferData(ctx.ARRAY_BUFFER, i, ctx.STATIC_DRAW);
|
|
117
|
-
h.itemSize = 3;
|
|
118
|
-
h.numItems = 3;
|
|
119
|
-
|
|
120
|
-
const j = ctx.createProgram();
|
|
121
|
-
const k = ctx.createShader(ctx.VERTEX_SHADER);
|
|
122
|
-
|
|
123
|
-
ctx.shaderSource(k, f);
|
|
124
|
-
ctx.compileShader(k);
|
|
125
|
-
|
|
126
|
-
const l = ctx.createShader(ctx.FRAGMENT_SHADER);
|
|
127
|
-
|
|
128
|
-
ctx.shaderSource(l, g);
|
|
129
|
-
ctx.compileShader(l);
|
|
130
|
-
ctx.attachShader(j, k);
|
|
131
|
-
ctx.attachShader(j, l);
|
|
132
|
-
ctx.linkProgram(j);
|
|
133
|
-
ctx.useProgram(j);
|
|
134
|
-
|
|
135
|
-
j.vertexPosAttrib = ctx.getAttribLocation(j, "attrVertex");
|
|
136
|
-
j.offsetUniform = ctx.getUniformLocation(j, "uniformOffset");
|
|
137
|
-
|
|
138
|
-
ctx.enableVertexAttribArray(j.vertexPosArray);
|
|
139
|
-
ctx.vertexAttribPointer(j.vertexPosAttrib, h.itemSize, ctx.FLOAT, !1, 0, 0);
|
|
140
|
-
ctx.uniform2f(j.offsetUniform, 1, 1);
|
|
141
|
-
ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, h.numItems);
|
|
142
|
-
|
|
143
|
-
const n = new Uint8Array(canvas.width * canvas.height * 4);
|
|
144
|
-
ctx.readPixels(0, 0, canvas.width, canvas.height, ctx.RGBA, ctx.UNSIGNED_BYTE, n);
|
|
145
|
-
|
|
146
|
-
const result = JSON.stringify(n).replace(/,?"[0-9]+":/g, "");
|
|
147
|
-
|
|
148
|
-
if (debug) {
|
|
149
|
-
document.body.appendChild(canvas);
|
|
150
|
-
} else {
|
|
151
|
-
ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
return murmurhash3_32_gc(result);
|
|
155
|
-
} catch {
|
|
156
|
-
return null;
|
|
54
|
+
const stableStringify = (obj) => {
|
|
55
|
+
// handle null, string, number, boolean
|
|
56
|
+
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
|
|
57
|
+
// handle arrays recursively
|
|
58
|
+
if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`;
|
|
59
|
+
|
|
60
|
+
const keys = Object.keys(obj).sort();
|
|
61
|
+
const parts = [];
|
|
62
|
+
for (const key of keys) {
|
|
63
|
+
const value = obj[key];
|
|
64
|
+
if (value !== undefined) parts.push(`${stableStringify(key)}:${stableStringify(value)}`);
|
|
157
65
|
}
|
|
158
|
-
};
|
|
159
66
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const ctx = document.createElement("canvas").getContext("webgl");
|
|
67
|
+
return `{${parts.join(',')}}`;
|
|
68
|
+
};
|
|
163
69
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
SUPPORTED_EXTENSIONS: String(ctx.getSupportedExtensions()),
|
|
169
|
-
};
|
|
70
|
+
const isMobile = () =>
|
|
71
|
+
navigator.userAgentData?.mobile === true ||
|
|
72
|
+
/Mobi|Android|iPhone|iPad|iPod|webOS|BlackBerry|Windows Phone/i.test(navigator.userAgent) ||
|
|
73
|
+
(navigator.maxTouchPoints > 0 && matchMedia('(pointer:coarse)').matches && innerWidth <= 1024);
|
|
170
74
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
75
|
+
/** @type {import('./index.d.ts').getBrowserFingerprint} */
|
|
76
|
+
const getBrowserFingerprint = async ({ debug = false } = {}) => {
|
|
77
|
+
// software
|
|
78
|
+
const fonts = await safe(() => getFonts());
|
|
79
|
+
const numberingSystem = await safe(() => new Intl.NumberFormat(window.navigator.languages?.[0] || 'en').resolvedOptions().numberingSystem);
|
|
80
|
+
const languages = window.navigator.languages ? Array.from(window.navigator.languages).join(',') : window.navigator.language || null;
|
|
81
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
82
|
+
const timezoneOffset = new Date().getTimezoneOffset();
|
|
83
|
+
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : 'no-preference';
|
|
84
|
+
const colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
85
|
+
|
|
86
|
+
// hardware
|
|
87
|
+
const forcedColors = window.matchMedia('(forced-colors: active)').matches ? 'active' : null;
|
|
88
|
+
const { pixelDepth, colorDepth } = window.screen;
|
|
89
|
+
const touchSupport = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0;
|
|
90
|
+
const canvasID = await safe(() => getCanvasID(debug));
|
|
91
|
+
const webglID = await safe(() => getWebglID());
|
|
92
|
+
const webgpuID = await safe(() => getWebgpuID(debug));
|
|
93
|
+
const audioID = await safe(() => getAudioID(debug));
|
|
94
|
+
const aspectRatio = window.screen.width && window.screen.height ? (window.screen.width / window.screen.height).toFixed(4) : null;
|
|
95
|
+
|
|
96
|
+
const data = {
|
|
97
|
+
// SOFTWARE
|
|
98
|
+
vendor: window.navigator.vendor || null, // vendor
|
|
99
|
+
platform: window.navigator.platform || null, // os
|
|
100
|
+
fonts, // stable default os setting
|
|
101
|
+
numberingSystem, // stable default os setting
|
|
102
|
+
languages, // stable user locale/setting
|
|
103
|
+
timezone, // stable user locale/setting
|
|
104
|
+
timezoneOffset, // stable user locale/setting
|
|
105
|
+
reducedMotion, // stable user locale/setting
|
|
106
|
+
colorScheme, // stable user locale/setting
|
|
107
|
+
// HARDWARE
|
|
108
|
+
hardwareConcurrency: window.navigator.hardwareConcurrency || null, // cpu
|
|
109
|
+
deviceMemory: window.navigator.deviceMemory || null, // ram
|
|
110
|
+
forcedColors, // display
|
|
111
|
+
pixelDepth, // display
|
|
112
|
+
colorDepth, // display
|
|
113
|
+
touchSupport, // display
|
|
114
|
+
canvasID, // gpu
|
|
115
|
+
webglID, // gpu
|
|
116
|
+
webgpuID, // gpu
|
|
117
|
+
audioID, // audio
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const mobileData = {
|
|
121
|
+
aspectRatio,
|
|
122
|
+
width: window.screen.width,
|
|
123
|
+
height: window.screen.height,
|
|
124
|
+
maxTouchPoints: window.navigator.maxTouchPoints,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (isMobile()) Object.assign(data, mobileData);
|
|
128
|
+
|
|
129
|
+
const payload = stableStringify(data);
|
|
130
|
+
const fingerprint = murmurHash3(payload);
|
|
131
|
+
return { fingerprint, ...data };
|
|
175
132
|
};
|
|
176
133
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
134
|
+
const getCanvasID = async (debug) => {
|
|
135
|
+
const canvas = document.createElement('canvas');
|
|
136
|
+
canvas.width = 280;
|
|
137
|
+
canvas.height = 100;
|
|
138
|
+
const ctx = canvas.getContext('2d');
|
|
139
|
+
|
|
140
|
+
// background with gradient
|
|
141
|
+
const grad = ctx.createLinearGradient(0, 0, 280, 100);
|
|
142
|
+
grad.addColorStop(0, '#f60');
|
|
143
|
+
grad.addColorStop(0.4, '#09f');
|
|
144
|
+
grad.addColorStop(0.7, '#f09');
|
|
145
|
+
grad.addColorStop(1, '#0f9');
|
|
146
|
+
ctx.fillStyle = grad;
|
|
147
|
+
ctx.fillRect(0, 0, 280, 100);
|
|
148
|
+
|
|
149
|
+
// text with multiple fonts and emoji
|
|
150
|
+
ctx.font = '18px "Arial","Helvetica","DejaVu Sans",sans-serif';
|
|
151
|
+
ctx.fillStyle = '#000';
|
|
152
|
+
ctx.textBaseline = 'alphabetic';
|
|
153
|
+
ctx.fillText('Fingerprint ¼½¾™©®∆π∑€¶§', 12, 45);
|
|
154
|
+
|
|
155
|
+
ctx.font = 'bold 22px "Georgia","Times New Roman",serif';
|
|
156
|
+
ctx.fillStyle = '#222';
|
|
157
|
+
ctx.fillText('🦊🐱🚀 2026', 12, 78);
|
|
158
|
+
|
|
159
|
+
// content with antialiasing and compositing
|
|
160
|
+
ctx.globalAlpha = 0.7;
|
|
161
|
+
ctx.fillStyle = 'rgba(255, 80, 0, 0.35)';
|
|
162
|
+
ctx.fillRect(180, 20, 80, 60);
|
|
163
|
+
|
|
164
|
+
ctx.beginPath();
|
|
165
|
+
ctx.arc(220, 50, 32, 0, Math.PI * 2);
|
|
166
|
+
ctx.strokeStyle = '#fff';
|
|
167
|
+
ctx.lineWidth = 4;
|
|
168
|
+
ctx.stroke();
|
|
169
|
+
|
|
170
|
+
ctx.beginPath();
|
|
171
|
+
ctx.moveTo(30, 85);
|
|
172
|
+
ctx.quadraticCurveTo(140, 20, 240, 85);
|
|
173
|
+
ctx.strokeStyle = '#00f';
|
|
174
|
+
ctx.lineWidth = 3;
|
|
175
|
+
ctx.stroke();
|
|
176
|
+
|
|
177
|
+
if (debug) {
|
|
178
|
+
canvas.style.border = '1px solid #444';
|
|
179
|
+
document.body.appendChild(canvas);
|
|
195
180
|
}
|
|
196
|
-
};
|
|
197
181
|
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
const length = 44100; // Number of samples (1 second of audio)
|
|
203
|
-
const context = new OfflineAudioContext(1, length, sampleRate);
|
|
204
|
-
|
|
205
|
-
// Create an oscillator to generate sound
|
|
206
|
-
const oscillator = context.createOscillator();
|
|
207
|
-
oscillator.type = "sine";
|
|
208
|
-
oscillator.frequency.value = 440;
|
|
209
|
-
|
|
210
|
-
oscillator.connect(context.destination);
|
|
211
|
-
oscillator.start();
|
|
212
|
-
|
|
213
|
-
// Render the audio into a buffer
|
|
214
|
-
const renderedBuffer = await context.startRendering();
|
|
215
|
-
const channelData = renderedBuffer.getChannelData(0);
|
|
216
|
-
|
|
217
|
-
// Generate fingerprint by summing the absolute values of the audio data
|
|
218
|
-
const result = channelData.reduce((acc, val) => acc + Math.abs(val), 0).toString();
|
|
219
|
-
|
|
220
|
-
if (debug) {
|
|
221
|
-
const wavBlob = bufferToWav(renderedBuffer);
|
|
222
|
-
const audioURL = URL.createObjectURL(wavBlob);
|
|
223
|
-
|
|
224
|
-
const audioElement = document.createElement("audio");
|
|
225
|
-
audioElement.controls = true;
|
|
226
|
-
audioElement.src = audioURL;
|
|
227
|
-
document.body.appendChild(audioElement);
|
|
228
|
-
}
|
|
182
|
+
const dataUrl = canvas.toDataURL('image/png');
|
|
183
|
+
const result = murmurHash3(dataUrl);
|
|
184
|
+
return result;
|
|
185
|
+
};
|
|
229
186
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
187
|
+
const getWebglID = () => {
|
|
188
|
+
const canvas = document.createElement('canvas');
|
|
189
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
190
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
191
|
+
const vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
|
|
192
|
+
const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
|
|
193
|
+
const info = { vendor, renderer };
|
|
194
|
+
const result = murmurHash3(stableStringify(info));
|
|
195
|
+
return result;
|
|
234
196
|
};
|
|
235
197
|
|
|
236
|
-
const
|
|
237
|
-
const
|
|
238
|
-
const
|
|
239
|
-
const
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
view.setUint32(40, length - 44, true);
|
|
256
|
-
|
|
257
|
-
// Write interleaved audio data
|
|
258
|
-
let offset = 44;
|
|
259
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
260
|
-
for (let channel = 0; channel < numOfChannels; channel++) {
|
|
261
|
-
const sample = buffer.getChannelData(channel)[i];
|
|
262
|
-
const intSample = Math.max(-1, Math.min(1, sample)) * 32767;
|
|
263
|
-
view.setInt16(offset, intSample, true);
|
|
264
|
-
offset += 2;
|
|
265
|
-
}
|
|
198
|
+
const getWebgpuID = async (debug) => {
|
|
199
|
+
const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
|
|
200
|
+
const vendor = adapter.info?.vendor ?? '';
|
|
201
|
+
const architecture = adapter.info?.architecture ?? '';
|
|
202
|
+
const description = adapter.info?.description ?? '';
|
|
203
|
+
const features = Array.from(adapter.features).sort();
|
|
204
|
+
const limits = Array.from(adapter.limits).sort();
|
|
205
|
+
const info = { vendor, architecture, description, features, limits };
|
|
206
|
+
|
|
207
|
+
if (debug) {
|
|
208
|
+
const debugDiv = document.createElement('pre');
|
|
209
|
+
debugDiv.style.margin = '10px 0';
|
|
210
|
+
debugDiv.style.padding = '10px';
|
|
211
|
+
debugDiv.style.border = '1px solid #444';
|
|
212
|
+
debugDiv.style.background = '#111';
|
|
213
|
+
debugDiv.style.color = '#0f8';
|
|
214
|
+
debugDiv.style.fontFamily = 'monospace';
|
|
215
|
+
debugDiv.textContent = `WebGPU:\n${JSON.stringify(info, null, 2)}`;
|
|
216
|
+
document.body.appendChild(debugDiv);
|
|
266
217
|
}
|
|
267
218
|
|
|
268
|
-
|
|
219
|
+
const result = murmurHash3(stableStringify(info));
|
|
220
|
+
return result;
|
|
269
221
|
};
|
|
270
222
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
223
|
+
const getAudioID = async (debug) => {
|
|
224
|
+
const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
225
|
+
|
|
226
|
+
const context = new OfflineCtx(1, 6000, 44100);
|
|
227
|
+
const osc = context.createOscillator();
|
|
228
|
+
osc.type = 'sawtooth';
|
|
229
|
+
osc.frequency.value = 8800;
|
|
230
|
+
|
|
231
|
+
const compressor = context.createDynamicsCompressor();
|
|
232
|
+
compressor.threshold.value = -48;
|
|
233
|
+
compressor.knee.value = 30;
|
|
234
|
+
compressor.ratio.value = 14;
|
|
235
|
+
compressor.attack.value = 0;
|
|
236
|
+
compressor.release.value = 0.3;
|
|
237
|
+
|
|
238
|
+
const gain = context.createGain();
|
|
239
|
+
gain.gain.value = 0.4;
|
|
240
|
+
|
|
241
|
+
osc.connect(compressor);
|
|
242
|
+
compressor.connect(gain);
|
|
243
|
+
gain.connect(context.destination);
|
|
244
|
+
|
|
245
|
+
osc.start(0);
|
|
246
|
+
const buffer = await context.startRendering();
|
|
247
|
+
const channel = buffer.getChannelData(0);
|
|
248
|
+
|
|
249
|
+
let hash = 0;
|
|
250
|
+
let hash2 = 0;
|
|
251
|
+
for (let i = 3000; i < 5800; i += 2) {
|
|
252
|
+
const v = channel[i];
|
|
253
|
+
hash = (hash * 31 + Math.floor((v + 1) * 100000)) | 0;
|
|
254
|
+
hash2 = Math.imul(hash2 ^ Math.floor(v * 98765), 0x85ebca77);
|
|
274
255
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
256
|
+
const final = (hash >>> 0) ^ (hash2 >>> 0);
|
|
257
|
+
|
|
258
|
+
if (debug) {
|
|
259
|
+
const wc = document.createElement('canvas');
|
|
260
|
+
wc.width = 500;
|
|
261
|
+
wc.height = 120;
|
|
262
|
+
wc.style.border = '1px solid #444';
|
|
263
|
+
wc.style.margin = '10px 0';
|
|
264
|
+
wc.title = 'Audio buffer snippet (4500–5000 samples)';
|
|
265
|
+
|
|
266
|
+
const ctx = wc.getContext('2d');
|
|
267
|
+
ctx.fillStyle = '#111';
|
|
268
|
+
ctx.fillRect(0, 0, wc.width, wc.height);
|
|
269
|
+
|
|
270
|
+
ctx.font = '14px monospace';
|
|
271
|
+
ctx.fillStyle = '#0f8';
|
|
272
|
+
ctx.textAlign = 'left';
|
|
273
|
+
ctx.fillText('AudioID', 10, 20);
|
|
274
|
+
|
|
275
|
+
ctx.strokeStyle = '#0f8';
|
|
276
|
+
ctx.lineWidth = 1.5;
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
|
|
279
|
+
const waveform = Array.from(channel.slice(4500, 5000));
|
|
280
|
+
const step = wc.width / waveform.length;
|
|
281
|
+
|
|
282
|
+
for (let i = 0; i < waveform.length; i++) {
|
|
283
|
+
const x = i * step;
|
|
284
|
+
const y = ((waveform[i] + 1) / 2) * wc.height;
|
|
285
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
286
|
+
else ctx.lineTo(x, y);
|
|
287
|
+
}
|
|
288
|
+
ctx.stroke();
|
|
294
289
|
|
|
295
|
-
|
|
296
|
-
h1 = (h1 << 13) | (h1 >>> 19);
|
|
297
|
-
h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
|
|
298
|
-
h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
|
|
290
|
+
document.body.appendChild(wc);
|
|
299
291
|
}
|
|
300
292
|
|
|
301
|
-
const
|
|
293
|
+
const result = murmurHash3(final.toString(16));
|
|
294
|
+
return result;
|
|
295
|
+
};
|
|
302
296
|
|
|
303
|
-
|
|
297
|
+
const getFonts = () => {
|
|
298
|
+
const baseFonts = ['monospace', 'serif', 'sans-serif'];
|
|
299
|
+
const testFonts = [
|
|
300
|
+
'Arial',
|
|
301
|
+
'Helvetica',
|
|
302
|
+
'Verdana',
|
|
303
|
+
'Trebuchet MS',
|
|
304
|
+
'Comic Sans MS',
|
|
305
|
+
'Georgia',
|
|
306
|
+
'Times New Roman',
|
|
307
|
+
'Courier New',
|
|
308
|
+
'Segoe UI',
|
|
309
|
+
'Roboto',
|
|
310
|
+
'Open Sans',
|
|
311
|
+
'Noto Sans',
|
|
312
|
+
'system-ui',
|
|
313
|
+
'-apple-system',
|
|
314
|
+
'BlinkMacSystemFont',
|
|
315
|
+
];
|
|
316
|
+
|
|
317
|
+
const span = document.createElement('span');
|
|
318
|
+
span.style.fontSize = '72px';
|
|
319
|
+
span.style.position = 'absolute';
|
|
320
|
+
span.style.visibility = 'hidden';
|
|
321
|
+
span.style.whiteSpace = 'nowrap';
|
|
322
|
+
span.textContent = 'mmmmmmmmmwwwwwww';
|
|
323
|
+
document.body.appendChild(span);
|
|
324
|
+
|
|
325
|
+
const defaults = {};
|
|
326
|
+
for (const base of baseFonts) {
|
|
327
|
+
span.style.fontFamily = base;
|
|
328
|
+
defaults[base] = span.offsetWidth;
|
|
329
|
+
}
|
|
304
330
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
331
|
+
const detected = [];
|
|
332
|
+
for (const font of testFonts) {
|
|
333
|
+
let found = false;
|
|
334
|
+
for (const base of baseFonts) {
|
|
335
|
+
span.style.fontFamily = `"${font}", ${base}`;
|
|
336
|
+
if (span.offsetWidth !== defaults[base]) {
|
|
337
|
+
detected.push(font);
|
|
338
|
+
found = true;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
313
341
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
342
|
+
if (!found) {
|
|
343
|
+
span.style.fontFamily = `"${font}"`;
|
|
344
|
+
if (span.offsetWidth !== defaults.monospace && span.offsetWidth !== defaults.serif) {
|
|
345
|
+
detected.push(font);
|
|
346
|
+
}
|
|
317
347
|
}
|
|
318
348
|
}
|
|
319
349
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
h1 ^= k1;
|
|
324
|
-
|
|
325
|
-
h1 ^= key.length;
|
|
326
|
-
|
|
327
|
-
h1 ^= h1 >>> 16;
|
|
328
|
-
h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
|
|
329
|
-
h1 ^= h1 >>> 13;
|
|
330
|
-
h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff;
|
|
331
|
-
h1 ^= h1 >>> 16;
|
|
332
|
-
|
|
333
|
-
return h1 >>> 0;
|
|
350
|
+
document.body.removeChild(span);
|
|
351
|
+
const info = detected.length ? detected.sort().join(',') : null;
|
|
352
|
+
return info;
|
|
334
353
|
};
|
|
335
354
|
|
|
336
|
-
if (typeof window !==
|
|
355
|
+
if (typeof window !== 'undefined') {
|
|
337
356
|
window.getBrowserFingerprint = getBrowserFingerprint;
|
|
338
357
|
}
|
|
339
358
|
|