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 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.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.8.3",
22
- "@playwright/test": "^1.47.0",
21
+ "@biomejs/biome": "^1.9.2",
22
+ "@playwright/test": "^1.47.2",
23
23
  "serve": "^14.2.3",
24
- "vitest": "^2.0.5"
24
+ "vitest": "^2.1.1"
25
25
  }
26
26
  }
package/src/index.d.ts CHANGED
@@ -1,7 +1,5 @@
1
1
  export interface FingerprintOptions {
2
2
  hardwareOnly?: boolean;
3
- enableWebgl?: boolean;
4
- enableScreen?: boolean;
5
3
  debug?: boolean;
6
4
  }
7
5
 
package/src/index.js CHANGED
@@ -1,6 +1,9 @@
1
- const getBrowserFingerprint = ({ hardwareOnly = false, enableWebgl = false, enableScreen = true, debug = false } = {}) => {
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 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
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
- ? JSON.stringify({
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
- : JSON.stringify({
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("fingerprint data", datastring);
63
+ if (debug) console.log("Fingerprint data:", JSON.stringify(data, null, 2));
57
64
 
58
- const result = murmurhash3_32_gc(datastring);
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
- SUPORTED_EXTENSIONS: String(ctx.getSupportedExtensions()),
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;