get-browser-fingerprint 3.2.3 → 4.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 CHANGED
@@ -1,31 +1,27 @@
1
1
  # get-browser-fingerprint
2
2
 
3
- Zero dependencies package exporting a single, fast (<15ms) and synchronous function which computes a browser fingerprint, without requiring any permission to the user.
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 `false`): leverage only hardware info about device
16
- - `enableWebgl` (default `false`): enable webgl renderer, ~4x times slower but adds another deadly powerful hardware detection layer on top of canvas
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
- nvm install
27
- yarn install
28
- yarn test
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.2.3",
3
+ "version": "4.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",
@@ -18,9 +18,9 @@
18
18
  "test": "vitest run"
19
19
  },
20
20
  "devDependencies": {
21
- "@biomejs/biome": "^1.8.3",
22
- "@playwright/test": "^1.47.0",
23
- "serve": "^14.2.3",
24
- "vitest": "^2.0.5"
21
+ "@biomejs/biome": "^1.9.4",
22
+ "@playwright/test": "^1.48.1",
23
+ "serve": "^14.2.4",
24
+ "vitest": "^2.1.3"
25
25
  }
26
26
  }
package/src/index.d.ts CHANGED
@@ -1,11 +1,9 @@
1
1
  export interface FingerprintOptions {
2
2
  hardwareOnly?: boolean;
3
- enableWebgl?: boolean;
4
- enableScreen?: boolean;
5
3
  debug?: boolean;
6
4
  }
7
5
 
8
- export default function getBrowserFingerprint(options?: FingerprintOptions): number;
6
+ export default function getBrowserFingerprint(options?: FingerprintOptions): Promise<number>;
9
7
 
10
8
  declare global {
11
9
  interface Window {
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
- const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enableScreen = true, debug = false } = {}) => {
1
+ /** @type {import('./index.d.ts').getBrowserFingerprint} */
2
+ const getBrowserFingerprint = async ({ hardwareOnly = true, debug = false } = {}) => {
2
3
  const { cookieEnabled, deviceMemory, doNotTrack, hardwareConcurrency, language, languages, maxTouchPoints, platform, userAgent, vendor } = window.navigator;
3
4
 
5
+ // we use screen info only on mobile, because on desktop the user may use multiple monitors
6
+ const enableScreen = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
7
+
4
8
  const { width, height, colorDepth, pixelDepth } = enableScreen ? window.screen : {}; // undefined will remove this from the stringify down here
5
9
  const timezoneOffset = new Date().getTimezoneOffset();
6
10
  const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
@@ -8,11 +12,15 @@ const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enab
8
12
  const devicePixelRatio = window.devicePixelRatio;
9
13
 
10
14
  const canvas = getCanvasID(debug);
11
- const webgl = enableWebgl ? getWebglID(debug) : undefined; // undefined will remove this from the stringify down here
12
- const webglInfo = enableWebgl ? getWebglInfo(debug) : undefined; // undefined will remove this from the stringify down here
15
+ const audio = await getAudioID(debug);
16
+ const audioInfo = getAudioInfo();
17
+ const webgl = getWebglID(debug);
18
+ const webglInfo = getWebglInfo();
13
19
 
14
20
  const data = hardwareOnly
15
- ? JSON.stringify({
21
+ ? {
22
+ audioInfo,
23
+ audio,
16
24
  canvas,
17
25
  colorDepth,
18
26
  deviceMemory,
@@ -26,8 +34,10 @@ const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enab
26
34
  webgl,
27
35
  webglInfo,
28
36
  width,
29
- })
30
- : JSON.stringify({
37
+ }
38
+ : {
39
+ audioInfo,
40
+ audio,
31
41
  canvas,
32
42
  colorDepth,
33
43
  cookieEnabled,
@@ -49,13 +59,12 @@ const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enab
49
59
  webgl,
50
60
  webglInfo,
51
61
  width,
52
- });
53
-
54
- const datastring = JSON.stringify(data, null, 4);
62
+ };
55
63
 
56
- if (debug) console.log("fingerprint data", datastring);
64
+ if (debug) console.log("Fingerprint data:", JSON.stringify(data, null, 2));
57
65
 
58
- const result = murmurhash3_32_gc(datastring);
66
+ const payload = JSON.stringify(data, null, 2);
67
+ const result = murmurhash3_32_gc(payload);
59
68
  return result;
60
69
  };
61
70
 
@@ -156,7 +165,7 @@ const getWebglInfo = () => {
156
165
  VERSION: String(ctx.getParameter(ctx.VERSION)),
157
166
  SHADING_LANGUAGE_VERSION: String(ctx.getParameter(ctx.SHADING_LANGUAGE_VERSION)),
158
167
  VENDOR: String(ctx.getParameter(ctx.VENDOR)),
159
- SUPORTED_EXTENSIONS: String(ctx.getSupportedExtensions()),
168
+ SUPPORTED_EXTENSIONS: String(ctx.getSupportedExtensions()),
160
169
  };
161
170
 
162
171
  return result;
@@ -165,6 +174,106 @@ const getWebglInfo = () => {
165
174
  }
166
175
  };
167
176
 
177
+ const getAudioInfo = () => {
178
+ try {
179
+ const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
180
+ const length = 44100;
181
+ const sampleRate = 44100;
182
+ const context = new OfflineAudioContext(1, length, sampleRate);
183
+
184
+ const result = {
185
+ sampleRate: context.sampleRate,
186
+ channelCount: context.destination.maxChannelCount,
187
+ outputLatency: context.outputLatency,
188
+ state: context.state,
189
+ baseLatency: context.baseLatency,
190
+ };
191
+
192
+ return result;
193
+ } catch {
194
+ return null;
195
+ }
196
+ };
197
+
198
+ const getAudioID = async (debug) => {
199
+ try {
200
+ const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
201
+ const sampleRate = 44100;
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
+ }
229
+
230
+ return murmurhash3_32_gc(result);
231
+ } catch {
232
+ return null;
233
+ }
234
+ };
235
+
236
+ const bufferToWav = (buffer) => {
237
+ const numOfChannels = buffer.numberOfChannels;
238
+ const length = buffer.length * numOfChannels * 2 + 44; // Buffer size in bytes
239
+ const wavBuffer = new ArrayBuffer(length);
240
+ const view = new DataView(wavBuffer);
241
+
242
+ // Write WAV file header
243
+ writeString(view, 0, "RIFF");
244
+ view.setUint32(4, length - 8, true);
245
+ writeString(view, 8, "WAVE");
246
+ writeString(view, 12, "fmt ");
247
+ view.setUint32(16, 16, true);
248
+ view.setUint16(20, 1, true);
249
+ view.setUint16(22, numOfChannels, true);
250
+ view.setUint32(24, buffer.sampleRate, true);
251
+ view.setUint32(28, buffer.sampleRate * numOfChannels * 2, true);
252
+ view.setUint16(32, numOfChannels * 2, true);
253
+ view.setUint16(34, 16, true);
254
+ writeString(view, 36, "data");
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
+ }
266
+ }
267
+
268
+ return new Blob([view], { type: "audio/wav" });
269
+ };
270
+
271
+ const writeString = (view, offset, string) => {
272
+ for (let i = 0; i < string.length; i++) {
273
+ view.setUint8(offset + i, string.charCodeAt(i));
274
+ }
275
+ };
276
+
168
277
  const murmurhash3_32_gc = (key) => {
169
278
  const remainder = key.length & 3; // key.length % 4
170
279
  const bytes = key.length - remainder;