get-browser-fingerprint 3.2.3 → 4.0.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/README.md +7 -11
- package/package.json +4 -4
- package/src/index.d.ts +0 -2
- package/src/index.js +120 -12
package/README.md
CHANGED
|
@@ -1,31 +1,27 @@
|
|
|
1
1
|
# get-browser-fingerprint
|
|
2
2
|
|
|
3
|
-
Zero dependencies package exporting a single
|
|
3
|
+
Zero dependencies package exporting a single and fast (<50ms) asynchronous function returning a browser fingerprint, without requiring any permission to the user.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
Get browser fingerprint:
|
|
8
8
|
```js
|
|
9
9
|
import getBrowserFingerprint from 'get-browser-fingerprint';
|
|
10
|
-
const fingerprint = getBrowserFingerprint();
|
|
10
|
+
const fingerprint = await getBrowserFingerprint();
|
|
11
11
|
console.log(fingerprint);
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Options available:
|
|
15
|
-
- `hardwareOnly` (default `
|
|
16
|
-
- `
|
|
17
|
-
- `enableScreen` (default `true`): enable screen resolution detection, disable it if your userbase may use multiple screens
|
|
18
|
-
- `debug`: log data used to generate fingerprint to console and add canvas/webgl canvas to body to see rendered image (default `false`)
|
|
19
|
-
|
|
20
|
-
⚠️ Be careful: the strongest discriminating factor is canvas token which can't be computed on old devices (eg: iPhone 6), deal accordingly ⚠️
|
|
15
|
+
- `hardwareOnly` (default `true`): use only hardware info about device.
|
|
16
|
+
- `debug` (default `false`): log data used to generate fingerprint to console and add canvas/webgl/audio elements to body.
|
|
21
17
|
|
|
22
18
|
## Development
|
|
23
19
|
|
|
24
20
|
To test locally:
|
|
25
21
|
```sh
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
fnm install
|
|
23
|
+
pnpm install
|
|
24
|
+
pnpm test
|
|
29
25
|
```
|
|
30
26
|
|
|
31
27
|
To run example locally:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get-browser-fingerprint",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
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",
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
"test": "vitest run"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
|
-
"@biomejs/biome": "^1.
|
|
22
|
-
"@playwright/test": "^1.47.
|
|
21
|
+
"@biomejs/biome": "^1.9.2",
|
|
22
|
+
"@playwright/test": "^1.47.2",
|
|
23
23
|
"serve": "^14.2.3",
|
|
24
|
-
"vitest": "^2.
|
|
24
|
+
"vitest": "^2.1.1"
|
|
25
25
|
}
|
|
26
26
|
}
|
package/src/index.d.ts
CHANGED
package/src/index.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const getBrowserFingerprint = ({ hardwareOnly =
|
|
1
|
+
const getBrowserFingerprint = async ({ hardwareOnly = true, debug = false } = {}) => {
|
|
2
2
|
const { cookieEnabled, deviceMemory, doNotTrack, hardwareConcurrency, language, languages, maxTouchPoints, platform, userAgent, vendor } = window.navigator;
|
|
3
3
|
|
|
4
|
+
// we use screen info only on mobile, because on desktop the user may use multiple monitors
|
|
5
|
+
const enableScreen = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
|
6
|
+
|
|
4
7
|
const { width, height, colorDepth, pixelDepth } = enableScreen ? window.screen : {}; // undefined will remove this from the stringify down here
|
|
5
8
|
const timezoneOffset = new Date().getTimezoneOffset();
|
|
6
9
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
@@ -8,11 +11,15 @@ const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enab
|
|
|
8
11
|
const devicePixelRatio = window.devicePixelRatio;
|
|
9
12
|
|
|
10
13
|
const canvas = getCanvasID(debug);
|
|
11
|
-
const
|
|
12
|
-
const
|
|
14
|
+
const audio = await getAudioID(debug);
|
|
15
|
+
const audioInfo = getAudioInfo();
|
|
16
|
+
const webgl = getWebglID(debug);
|
|
17
|
+
const webglInfo = getWebglInfo();
|
|
13
18
|
|
|
14
19
|
const data = hardwareOnly
|
|
15
|
-
?
|
|
20
|
+
? {
|
|
21
|
+
audioInfo,
|
|
22
|
+
audio,
|
|
16
23
|
canvas,
|
|
17
24
|
colorDepth,
|
|
18
25
|
deviceMemory,
|
|
@@ -26,8 +33,10 @@ const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enab
|
|
|
26
33
|
webgl,
|
|
27
34
|
webglInfo,
|
|
28
35
|
width,
|
|
29
|
-
}
|
|
30
|
-
:
|
|
36
|
+
}
|
|
37
|
+
: {
|
|
38
|
+
audioInfo,
|
|
39
|
+
audio,
|
|
31
40
|
canvas,
|
|
32
41
|
colorDepth,
|
|
33
42
|
cookieEnabled,
|
|
@@ -49,13 +58,12 @@ const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enab
|
|
|
49
58
|
webgl,
|
|
50
59
|
webglInfo,
|
|
51
60
|
width,
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const datastring = JSON.stringify(data, null, 4);
|
|
61
|
+
};
|
|
55
62
|
|
|
56
|
-
if (debug) console.log("
|
|
63
|
+
if (debug) console.log("Fingerprint data:", JSON.stringify(data, null, 2));
|
|
57
64
|
|
|
58
|
-
const
|
|
65
|
+
const payload = JSON.stringify(data, null, 2);
|
|
66
|
+
const result = murmurhash3_32_gc(payload);
|
|
59
67
|
return result;
|
|
60
68
|
};
|
|
61
69
|
|
|
@@ -156,7 +164,7 @@ const getWebglInfo = () => {
|
|
|
156
164
|
VERSION: String(ctx.getParameter(ctx.VERSION)),
|
|
157
165
|
SHADING_LANGUAGE_VERSION: String(ctx.getParameter(ctx.SHADING_LANGUAGE_VERSION)),
|
|
158
166
|
VENDOR: String(ctx.getParameter(ctx.VENDOR)),
|
|
159
|
-
|
|
167
|
+
SUPPORTED_EXTENSIONS: String(ctx.getSupportedExtensions()),
|
|
160
168
|
};
|
|
161
169
|
|
|
162
170
|
return result;
|
|
@@ -165,6 +173,106 @@ const getWebglInfo = () => {
|
|
|
165
173
|
}
|
|
166
174
|
};
|
|
167
175
|
|
|
176
|
+
const getAudioInfo = () => {
|
|
177
|
+
try {
|
|
178
|
+
const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
179
|
+
const length = 44100;
|
|
180
|
+
const sampleRate = 44100;
|
|
181
|
+
const context = new OfflineAudioContext(1, length, sampleRate);
|
|
182
|
+
|
|
183
|
+
const result = {
|
|
184
|
+
sampleRate: context.sampleRate,
|
|
185
|
+
channelCount: context.destination.maxChannelCount,
|
|
186
|
+
outputLatency: context.outputLatency,
|
|
187
|
+
state: context.state,
|
|
188
|
+
baseLatency: context.baseLatency,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
} catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const getAudioID = async (debug) => {
|
|
198
|
+
try {
|
|
199
|
+
const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
|
200
|
+
const sampleRate = 44100;
|
|
201
|
+
const length = 44100; // Number of samples (1 second of audio)
|
|
202
|
+
const context = new OfflineAudioContext(1, length, sampleRate);
|
|
203
|
+
|
|
204
|
+
// Create an oscillator to generate sound
|
|
205
|
+
const oscillator = context.createOscillator();
|
|
206
|
+
oscillator.type = "sine";
|
|
207
|
+
oscillator.frequency.value = 440;
|
|
208
|
+
|
|
209
|
+
oscillator.connect(context.destination);
|
|
210
|
+
oscillator.start();
|
|
211
|
+
|
|
212
|
+
// Render the audio into a buffer
|
|
213
|
+
const renderedBuffer = await context.startRendering();
|
|
214
|
+
const channelData = renderedBuffer.getChannelData(0);
|
|
215
|
+
|
|
216
|
+
// Generate fingerprint by summing the absolute values of the audio data
|
|
217
|
+
const result = channelData.reduce((acc, val) => acc + Math.abs(val), 0).toString();
|
|
218
|
+
|
|
219
|
+
if (debug) {
|
|
220
|
+
const wavBlob = bufferToWav(renderedBuffer);
|
|
221
|
+
const audioURL = URL.createObjectURL(wavBlob);
|
|
222
|
+
|
|
223
|
+
const audioElement = document.createElement("audio");
|
|
224
|
+
audioElement.controls = true;
|
|
225
|
+
audioElement.src = audioURL;
|
|
226
|
+
document.body.appendChild(audioElement);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return murmurhash3_32_gc(result);
|
|
230
|
+
} catch {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const bufferToWav = (buffer) => {
|
|
236
|
+
const numOfChannels = buffer.numberOfChannels;
|
|
237
|
+
const length = buffer.length * numOfChannels * 2 + 44; // Buffer size in bytes
|
|
238
|
+
const wavBuffer = new ArrayBuffer(length);
|
|
239
|
+
const view = new DataView(wavBuffer);
|
|
240
|
+
|
|
241
|
+
// Write WAV file header
|
|
242
|
+
writeString(view, 0, "RIFF");
|
|
243
|
+
view.setUint32(4, length - 8, true);
|
|
244
|
+
writeString(view, 8, "WAVE");
|
|
245
|
+
writeString(view, 12, "fmt ");
|
|
246
|
+
view.setUint32(16, 16, true);
|
|
247
|
+
view.setUint16(20, 1, true);
|
|
248
|
+
view.setUint16(22, numOfChannels, true);
|
|
249
|
+
view.setUint32(24, buffer.sampleRate, true);
|
|
250
|
+
view.setUint32(28, buffer.sampleRate * numOfChannels * 2, true);
|
|
251
|
+
view.setUint16(32, numOfChannels * 2, true);
|
|
252
|
+
view.setUint16(34, 16, true);
|
|
253
|
+
writeString(view, 36, "data");
|
|
254
|
+
view.setUint32(40, length - 44, true);
|
|
255
|
+
|
|
256
|
+
// Write interleaved audio data
|
|
257
|
+
let offset = 44;
|
|
258
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
259
|
+
for (let channel = 0; channel < numOfChannels; channel++) {
|
|
260
|
+
const sample = buffer.getChannelData(channel)[i];
|
|
261
|
+
const intSample = Math.max(-1, Math.min(1, sample)) * 32767;
|
|
262
|
+
view.setInt16(offset, intSample, true);
|
|
263
|
+
offset += 2;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return new Blob([view], { type: "audio/wav" });
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const writeString = (view, offset, string) => {
|
|
271
|
+
for (let i = 0; i < string.length; i++) {
|
|
272
|
+
view.setUint8(offset + i, string.charCodeAt(i));
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
|
|
168
276
|
const murmurhash3_32_gc = (key) => {
|
|
169
277
|
const remainder = key.length & 3; // key.length % 4
|
|
170
278
|
const bytes = key.length - remainder;
|