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
JavaScript
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
};