UNPKG

fingerprint-web

Version:

A blazing-fast, dependency-free browser fingerprinting library to uniquely identify devices using entropy data. Designed for high performance, privacy-respecting analytics, bot detection, and session tracking in modern web apps.

318 lines (317 loc) 12 kB
var m = Object.defineProperty; var g = Object.getOwnPropertySymbols; var d = Object.prototype.hasOwnProperty, f = Object.prototype.propertyIsEnumerable; var p = (c, e, t) => e in c ? m(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t, h = (c, e) => { for (var t in e || (e = {})) d.call(e, t) && p(c, t, e[t]); if (g) for (var t of g(e)) f.call(e, t) && p(c, t, e[t]); return c; }; var l = (c, e, t) => new Promise((n, a) => { var r = (i) => { try { o(t.next(i)); } catch (u) { a(u); } }, s = (i) => { try { o(t.throw(i)); } catch (u) { a(u); } }, o = (i) => i.done ? n(i.value) : Promise.resolve(i.value).then(r, s); o((t = t.apply(c, e)).next()); }); class b { constructor(e = {}) { this.data = [], this.options = h({ enableFonts: !0, // Include font detection excludeVolatile: !0, // Exclude highly volatile components hashAlgorithm: "SHA-256", // Hash algorithm to use (SHA-256, SHA-1, SHA-384, SHA-512) hashLength: 0, // Output hash length (0 = full length, positive number = truncate to length) components: { // Enable/disable specific components userAgent: !0, language: !0, screen: !0, timezone: !0, touchPoints: !0, hardware: !0, doNotTrack: !0, canvas: !0, webGL: !0, colorGamut: !0, plugins: !0, fonts: !0, audio: !1 // Disabled by default as it's more volatile }, weights: { // Weights to apply to different components (higher = more influence) userAgent: 1, language: 1, screen: 1, timezone: 1, touchPoints: 1, hardware: 1, doNotTrack: 1, canvas: 1, webGL: 1, colorGamut: 1, plugins: 1, fonts: 1, audio: 1 }, customComponents: [], // Array of custom component functions to include separator: "||" }, e); } /** * Generates the unique fingerprint by collecting system/browser data * and applying the chosen hash algorithm. * @param {Object} overrideOptions - Optional runtime options to override constructor options * @returns {Promise<string>} The generated fingerprint hash */ get() { return l(this, arguments, function* (e = {}) { const t = h(h({}, this.options), e); yield this.collect(t); const n = this.data.join(t.separator), a = yield this.hash(n, t.hashAlgorithm); return t.hashLength > 0 && a.length > t.hashLength ? a.substring(0, t.hashLength) : a; }); } /** * Returns the raw fingerprint data before hashing, for advanced customization. * @returns {Promise<Array>} Array of collected data points */ getRawData() { return l(this, null, function* () { return yield this.collect(), [...this.data]; }); } /** * Collects various stable browser/device characteristics to generate a fingerprint. * Focuses on characteristics that don't change between sessions. * @param {Object} options - Configuration options */ collect() { return l(this, arguments, function* (e = this.options) { const t = e.components; e.weights; const n = []; if (t.userAgent && n.push(navigator.userAgent), t.language && n.push(navigator.language), t.screen && n.push(screen.width + "x" + screen.height + "x" + screen.colorDepth), t.timezone && n.push(Intl.DateTimeFormat().resolvedOptions().timeZone), t.touchPoints && n.push(navigator.maxTouchPoints), t.hardware && (n.push(navigator.hardwareConcurrency || "unknown"), n.push(navigator.deviceMemory || "unknown")), t.doNotTrack && n.push(navigator.doNotTrack), t.canvas && n.push(this.getCanvas()), t.webGL && n.push(this.getWebGL()), t.colorGamut && n.push(this.getColorGamut()), t.plugins && n.push(this.getPlugins()), t.fonts && e.enableFonts && n.push(yield this.getAvailableFonts()), !e.excludeVolatile && t.audio && n.push(yield this.getAudio()), e.customComponents && e.customComponents.length > 0) for (const a of e.customComponents) try { const r = yield a.call(this); r != null && n.push(r); } catch (r) { console.error("Error in custom component:", r); } this.data = n.filter((a) => a != null); }); } /** * Generates a fingerprint based on the canvas rendering result. * This is unique to the specific device's GPU but consistent. * @returns {string} A hash of the canvas content (not the full base64 which can vary) */ getCanvas() { try { const e = document.createElement("canvas"); e.width = 250, e.height = 60; const t = e.getContext("2d"); t.textBaseline = "alphabetic", t.fillStyle = "#f60", t.fillRect(125, 1, 62, 20), t.fillStyle = "#069", t.font = "15px Arial", t.fillText("Consistent-Fingerprint", 2, 15), t.fillStyle = "rgba(102, 204, 0, 0.7)", t.font = "16px Georgia", t.fillText("FingerprintWeb", 4, 45), t.strokeStyle = "#FF0000", t.beginPath(), t.arc(50, 30, 15, 0, Math.PI * 2, !0), t.closePath(), t.stroke(); const n = t.getImageData(0, 0, e.width, e.height).data, a = []; for (let r = 0; r < n.length; r += 4e3) r < n.length && a.push(n[r]); return a.join(","); } catch (e) { return "canvas_unsupported"; } } /** * Generates an audio fingerprint by rendering a fixed audio context and measuring the output. * Takes specific samples from the result for consistency. * @returns {Promise<string>} A deterministic audio fingerprint value */ getAudio() { return l(this, null, function* () { try { const e = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 44100, 44100), t = e.createOscillator(); t.type = "sine", t.frequency.setValueAtTime(1e4, e.currentTime); const n = e.createGain(); return n.gain.setValueAtTime(0.5, e.currentTime), t.connect(n), n.connect(e.destination), t.start(0), e.startRendering(), new Promise((a) => { e.oncomplete = (r) => { const s = r.renderedBuffer.getChannelData(0), o = [], i = [0, 4410, 8820, 13230, 17640, 22050, 26460, 30870, 35280, 39690]; for (const u of i) u < s.length && o.push(s[u].toFixed(6)); a(o.join(",")); }; }); } catch (e) { return "audio_unsupported"; } }); } /** * Collects WebGL information, specifically the vendor and renderer information, * to uniquely identify the GPU. * @returns {string} WebGL renderer information */ getWebGL() { try { const e = document.createElement("canvas"), t = e.getContext("webgl") || e.getContext("experimental-webgl"); if (!t) return "webgl_unsupported"; let n, a; try { const s = t.getExtension("WEBGL_debug_renderer_info"); s && (n = t.getParameter(s.UNMASKED_VENDOR_WEBGL), a = t.getParameter(s.UNMASKED_RENDERER_WEBGL)); } catch (s) { } n = n || t.getParameter(t.VENDOR), a = a || t.getParameter(t.RENDERER); const r = []; return r.push(`max_texture_size:${t.getParameter(t.MAX_TEXTURE_SIZE)}`), r.push(`max_viewport_dims:${t.getParameter(t.MAX_VIEWPORT_DIMS)}`), r.push(`aliased_line_width_range:${t.getParameter(t.ALIASED_LINE_WIDTH_RANGE)}`), `${n}~${a}~${r.join(",")}`; } catch (e) { return "webgl_unsupported"; } } /** * Retrieves the list of installed browser plugins in a normalized format. * @returns {string} A normalized list of plugin names */ getPlugins() { try { if (!navigator.plugins || navigator.plugins.length === 0) return "no_plugins"; const e = []; for (let t = 0; t < navigator.plugins.length; t++) { const n = navigator.plugins[t]; if (n && n.name) { let a = n.name.replace(/\s+/g, " ").trim(); e.push(a); } } return e.sort().join(","); } catch (e) { return "plugins_unsupported"; } } /** * Determines the color gamut of the device (P3 or sRGB). * @returns {string} The color gamut of the device */ getColorGamut() { try { return window.matchMedia("(color-gamut: rec2020)").matches ? "rec2020" : window.matchMedia("(color-gamut: p3)").matches ? "p3" : window.matchMedia("(color-gamut: srgb)").matches ? "srgb" : "unknown"; } catch (e) { return "gamut_unsupported"; } } /** * Tests for availability of standard fonts to add entropy. * This is more reliable than checking battery or network which change frequently. * @returns {Promise<string>} A comma-separated list of available fonts */ getAvailableFonts() { return l(this, null, function* () { if (!this.options.enableFonts) return ""; const e = ["monospace", "sans-serif", "serif"], t = [ "Arial", "Courier New", "Georgia", "Times New Roman", "Trebuchet MS", "Verdana", "Tahoma", "Helvetica" ], n = "mmMMMwWWiii"; try { const a = document.createElement("div"); a.style.cssText = "position: absolute; left: -9999px; visibility: hidden;", document.body.appendChild(a); const r = {}, s = []; for (const o of e) a.style.fontFamily = o, a.innerHTML = n, r[o] = a.clientWidth; for (const o of t) { let i = !1; for (const u of e) if (a.style.fontFamily = `${o}, ${u}`, a.innerHTML = n, a.clientWidth !== r[u]) { i = !0; break; } i && s.push(o); } return document.body.removeChild(a), s.sort().join(","); } catch (a) { return "font_detection_unsupported"; } }); } /** * Generates a hash of the provided string using the specified algorithm. * @param {string} str - The string to hash * @param {string} algorithm - The hash algorithm to use (default: SHA-256) * @returns {Promise<string>} The hash of the string */ hash(e, t = "SHA-256") { return l(this, null, function* () { const a = ["SHA-1", "SHA-256", "SHA-384", "SHA-512"].includes(t) ? t : "SHA-256", r = new TextEncoder().encode(e); try { const s = yield crypto.subtle.digest(a, r); return Array.from(new Uint8Array(s)).map((o) => o.toString(16).padStart(2, "0")).join(""); } catch (s) { console.warn(`Hash algorithm ${t} failed, falling back to SHA-256`); const o = yield crypto.subtle.digest("SHA-256", r); return Array.from(new Uint8Array(o)).map((i) => i.toString(16).padStart(2, "0")).join(""); } }); } /** * Converts the fingerprint hash to various output formats. * @param {string} hash - The fingerprint hash to convert * @param {string} format - The output format (hex, base64, int, binary) * @returns {string|number} The formatted hash */ formatHash(e, t = "hex") { switch (t.toLowerCase()) { case "base64": const n = e.match(/.{2}/g).map((a) => parseInt(a, 16)); return btoa(String.fromCharCode.apply(null, n)); case "int": return parseInt(e.slice(0, 13), 16); case "binary": return e.split("").map((a) => parseInt(a, 16).toString(2).padStart(4, "0")).join(""); case "hex": default: return e; } } /** * Creates a new component function that can be added to customComponents. * @param {Function} fn - Function that returns a fingerprint component value * @returns {Function} Properly formatted component function */ static createComponent(e) { return function() { return l(this, null, function* () { try { return yield e.call(this); } catch (t) { return console.error("Error in custom component:", t), null; } }); }; } } export { b as default };