UNPKG

fingerprinter-js

Version:

Enterprise-grade browser fingerprinting with 19 collectors and advanced bot detection

1,456 lines (1,441 loc) 72.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.FingerprintJS = {})); })(this, (function (exports) { 'use strict'; /** * Suspect Score Analyzer v2.0 * Advanced bot, fraud, and automation detection */ class SuspectAnalyzer { /** * Analyzes a fingerprint to detect suspicious signals */ static analyze(components) { const signals = []; // 1. Automation detection signals.push(...this.checkAutomation(components)); // 2. Technical inconsistencies signals.push(...this.checkConsistency(components)); // 3. Suspicious environment signals.push(...this.checkEnvironment(components)); // 4. Bot patterns signals.push(...this.checkBotPatterns(components)); // 5. Privacy tools detection signals.push(...this.checkPrivacyTools(components)); // Calculate total score const detectedSignals = signals.filter((s) => s.detected); let totalScore = detectedSignals.reduce((sum, signal) => sum + signal.severity * 10, 0); totalScore = Math.min(100, totalScore); const riskLevel = this.calculateRiskLevel(totalScore); return { score: totalScore, signals: detectedSignals, riskLevel, details: { totalSignalsDetected: detectedSignals.length, highSeveritySignals: detectedSignals.filter((s) => s.severity >= 8) .length, automationDetected: signals .filter((s) => s.category === "automation") .some((s) => s.detected), inconsistenciesFound: signals .filter((s) => s.category === "inconsistency") .some((s) => s.detected), privacyToolsDetected: signals .filter((s) => s.category === "privacy") .some((s) => s.detected), }, }; } /** * Detects automation signals */ static checkAutomation(components) { const signals = []; // WebDriver detection signals.push({ type: "webdriver", severity: 9, description: "WebDriver automation detected", detected: this.hasWebDriver(), category: "automation", }); // Headless browser signals.push({ type: "headless", severity: 8, description: "Headless browser detected", detected: this.isHeadless(components), category: "automation", }); // PhantomJS signatures signals.push({ type: "phantom", severity: 7, description: "PhantomJS signatures detected", detected: this.hasPhantomSignatures(components), category: "automation", }); // Selenium signatures signals.push({ type: "selenium", severity: 8, description: "Selenium signatures detected", detected: this.hasSeleniumSignatures(), category: "automation", }); // Puppeteer detection signals.push({ type: "puppeteer", severity: 9, description: "Puppeteer automation detected", detected: this.hasPuppeteerSignatures(), category: "automation", }); // Playwright detection signals.push({ type: "playwright", severity: 9, description: "Playwright automation detected", detected: this.hasPlaywrightSignatures(), category: "automation", }); // Chrome DevTools Protocol signals.push({ type: "cdp", severity: 7, description: "Chrome DevTools Protocol artifacts detected", detected: this.hasCDPArtifacts(), category: "automation", }); return signals; } /** * Checks data consistency */ static checkConsistency(components) { const signals = []; // Timezone vs Language inconsistency signals.push({ type: "timezone_language", severity: 5, description: "Timezone/language inconsistency", detected: this.hasTimezoneLanguageInconsistency(components), category: "inconsistency", }); // Screen resolution inconsistency signals.push({ type: "screen_consistency", severity: 6, description: "Screen resolution inconsistency", detected: this.hasScreenInconsistency(components), category: "inconsistency", }); // Canvas fingerprint too generic signals.push({ type: "generic_canvas", severity: 4, description: "Canvas fingerprint too generic", detected: this.hasGenericCanvas(components), category: "inconsistency", }); // User agent vs platform mismatch signals.push({ type: "ua_platform_mismatch", severity: 6, description: "User-Agent doesn't match platform", detected: this.hasUAPlatformMismatch(components), category: "inconsistency", }); // Hardware inconsistency (0 cores or 0 memory) signals.push({ type: "hardware_inconsistency", severity: 5, description: "Suspicious hardware values", detected: this.hasHardwareInconsistency(components), category: "inconsistency", }); return signals; } /** * Checks execution environment */ static checkEnvironment(components) { const signals = []; // Missing expected APIs signals.push({ type: "missing_apis", severity: 6, description: "Missing browser APIs", detected: this.hasMissingAPIs(components), category: "environment", }); // Too many errors in collection signals.push({ type: "collection_errors", severity: 5, description: "Too many collection errors", detected: this.hasTooManyErrors(components), category: "environment", }); // Suspicious user agent signals.push({ type: "suspicious_ua", severity: 7, description: "Suspicious user agent", detected: this.hasSuspiciousUserAgent(components), category: "environment", }); // Zero touch points on mobile UA signals.push({ type: "touch_ua_mismatch", severity: 5, description: "Mobile UA but no touch support", detected: this.hasTouchUAMismatch(components), category: "environment", }); return signals; } /** * Detects bot patterns */ static checkBotPatterns(components) { const signals = []; // Perfect fingerprint (too stable) signals.push({ type: "too_perfect", severity: 3, description: "Fingerprint too perfect/stable", detected: this.isTooStable(components), category: "bot", }); // Known bot signatures signals.push({ type: "bot_signature", severity: 9, description: "Known bot signature detected", detected: this.hasKnownBotSignature(components), category: "bot", }); // Unusual WebGL vendor signals.push({ type: "webgl_vendor", severity: 4, description: "Unusual WebGL vendor", detected: this.hasUnusualWebGLVendor(components), category: "bot", }); return signals; } /** * Detects privacy tools and fingerprint spoofing */ static checkPrivacyTools(components) { const signals = []; // Canvas noise detection (anti-fingerprint extensions) signals.push({ type: "canvas_noise", severity: 5, description: "Canvas fingerprint noise detected", detected: this.hasCanvasNoise(components), category: "privacy", }); // Property tampering detection signals.push({ type: "property_tampering", severity: 6, description: "Browser property tampering detected", detected: this.hasPropertyTampering(), category: "privacy", }); // Audio context tampering signals.push({ type: "audio_tampering", severity: 5, description: "Audio context tampering detected", detected: this.hasAudioTampering(components), category: "privacy", }); return signals; } // ================== Detection Methods ================== static hasWebDriver() { if (typeof window === "undefined") return false; return (navigator.webdriver === true || !!window.callPhantom || !!window._phantom); } static isHeadless(components) { const ua = components.userAgent || ""; if (ua.includes("HeadlessChrome") || ua.includes("PhantomJS") || ua.includes("Headless")) { return true; } if (typeof window !== "undefined") { // Check for zero dimensions (headless indicators) if (window.outerHeight === 0 || window.outerWidth === 0) return true; // Check for missing chrome object in Chrome if (ua.includes("Chrome") && !window.chrome) return true; } return false; } static hasPhantomSignatures(components) { if (typeof window === "undefined") return false; const ua = components.userAgent || ""; return (ua.includes("PhantomJS") || !!window._phantom || !!window.callPhantom || !!window.__phantomas); } static hasSeleniumSignatures() { if (typeof window === "undefined") return false; return !!(window.selenium || window.webdriver || window.document.__selenium_unwrapped || window.document.__webdriver_evaluate || window.document.__driver_evaluate || window.document.__webdriver_script_function || window.document.__webdriver_script_func || window.document.__webdriver_script_fn || window.document.__fxdriver_evaluate || window.document.__driver_unwrapped || window.document.__webdriver_unwrapped || window.document.$cdc_asdjflasutopfhvcZLmcfl_ || window.document.$chrome_asyncScriptInfo); } static hasPuppeteerSignatures() { if (typeof window === "undefined") return false; return !!(window.__puppeteer_evaluation_script__ || navigator.webdriver || window.puppeteerBinding); } static hasPlaywrightSignatures() { if (typeof window === "undefined") return false; return !!(window.__playwright || window.__pw_manual || window._playwright); } static hasCDPArtifacts() { if (typeof window === "undefined") return false; return !!(window.__cdp__ || window.cdc_adoQpoasnfa76pfcZLmcfl_Array || window.cdc_adoQpoasnfa76pfcZLmcfl_Promise || window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol); } static hasTimezoneLanguageInconsistency(components) { const timezone = components.timezone; const language = components.language; if (!timezone || !language) return false; // Suspicious combinations const suspiciousCombinations = [ { tz: "America/New_York", lang: "zh-CN" }, { tz: "Europe/Paris", lang: "ja-JP" }, { tz: "Asia/Tokyo", lang: "es-ES" }, { tz: "Europe/London", lang: "zh-CN" }, { tz: "America/Los_Angeles", lang: "ru-RU" }, ]; return suspiciousCombinations.some((combo) => timezone.includes(combo.tz) && Array.isArray(language) && language.some((l) => l.includes(combo.lang))); } static hasScreenInconsistency(components) { const screen = components.screen; if (!screen) return false; // Very common resolutions used by bots const suspiciousResolutions = ["800x600", "1024x768"]; const resolution = `${screen.width}x${screen.height}`; return (suspiciousResolutions.includes(resolution) && screen.colorDepth === 24); } static hasGenericCanvas(components) { const canvas = components.canvas; if (!canvas || typeof canvas !== "string") return false; return canvas.length < 100 || canvas === "no-canvas"; } static hasUAPlatformMismatch(components) { const ua = components.userAgent || ""; const hardware = components.hardware; if (!hardware) return false; const platform = hardware.platform || ""; // Windows UA but Mac/Linux platform if (ua.includes("Windows") && !platform.toLowerCase().includes("win")) { return true; } // Mac UA but Windows/Linux platform if (ua.includes("Macintosh") && !platform.toLowerCase().includes("mac")) { return true; } return false; } static hasHardwareInconsistency(components) { const hardware = components.hardware; if (!hardware) return false; const cores = hardware.hardwareConcurrency; const memory = hardware.deviceMemory; // 0 cores is impossible if (cores === 0) return true; // More than 128 cores is suspicious if (cores > 128) return true; // Less than 0.25GB is suspicious for desktop if (memory !== null && memory < 0.25) return true; return false; } static hasMissingAPIs(components) { const expectedAPIs = ["userAgent", "language", "screen"]; const missingAPIs = expectedAPIs.filter((api) => { const value = components[api]; return !value || (typeof value === "object" && value.error); }); return missingAPIs.length > 1; } static hasTooManyErrors(components) { const errorCount = Object.values(components).filter((value) => value && typeof value === "object" && value.error).length; return errorCount > 5; } static hasSuspiciousUserAgent(components) { const ua = components.userAgent || ""; const suspiciousPatterns = [ "HeadlessChrome", "PhantomJS", "bot", "crawler", "spider", "scraper", "python-requests", "curl", "wget", "node-fetch", "axios", ]; return suspiciousPatterns.some((pattern) => ua.toLowerCase().includes(pattern.toLowerCase())); } static hasTouchUAMismatch(components) { const ua = components.userAgent || ""; const touch = components.touch; if (!touch) return false; const maxTouchPoints = touch.maxTouchPoints; const isMobileUA = ua.includes("Mobile") || ua.includes("Android") || ua.includes("iPhone"); // Mobile UA but no touch support return isMobileUA && maxTouchPoints === 0; } static isTooStable(components) { const perfectComponents = Object.values(components).filter((value) => value && !(typeof value === "object" && value.error) && value !== "unknown").length; // If every single component is perfect with no errors, it's suspicious return perfectComponents === Object.keys(components).length; } static hasKnownBotSignature(components) { const ua = components.userAgent || ""; const botSignatures = [ "Googlebot", "Bingbot", "Slurp", "DuckDuckBot", "Baiduspider", "YandexBot", "facebookexternalhit", "Twitterbot", "LinkedInBot", "WhatsApp", "python-requests", "curl/", "wget", "Scrapy", "HttpClient", ]; return botSignatures.some((signature) => ua.includes(signature)); } static hasUnusualWebGLVendor(components) { const webgl = components.webgl; if (!webgl || webgl.error) return false; const vendor = webgl.vendor || ""; const renderer = webgl.renderer || ""; // SwiftShader is commonly used by headless browsers if (renderer.includes("SwiftShader") || renderer.includes("llvmpipe") || renderer.includes("VMware")) { return true; } // Brian Paul Mesa is software rendering if (vendor.includes("Brian Paul") || renderer.includes("Mesa")) { return true; } return false; } static hasCanvasNoise(components) { // This would require comparing multiple canvas renders // For now, check if canvas is suspiciously different from expected const canvas = components.canvas; if (!canvas || typeof canvas !== "string") return false; // Very short canvas data might indicate blocking if (canvas.length < 50) return true; return false; } static hasPropertyTampering() { if (typeof window === "undefined") return false; try { // Check if navigator.webdriver is modified const descriptor = Object.getOwnPropertyDescriptor(Navigator.prototype, "webdriver"); if (descriptor && descriptor.get && descriptor.get.toString) { const getterStr = descriptor.get.toString(); if (getterStr.includes("proxy") || getterStr.includes("return false") || getterStr.includes("undefined")) { return true; } } } catch (_a) { // If it throws, might be tampered return true; } return false; } static hasAudioTampering(components) { const audio = components.audio; if (!audio || audio.error) return false; // Check for impossible values const sampleRate = audio.sampleRate; if (sampleRate === 0 || sampleRate > 192000) return true; // Standard sample rates const standardRates = [8000, 11025, 16000, 22050, 44100, 48000, 96000]; if (!standardRates.includes(sampleRate)) return true; return false; } static calculateRiskLevel(score) { if (score < 30) return "LOW"; if (score < 70) return "MEDIUM"; return "HIGH"; } } /** * FingerprinterJS v2.0 Types * Enterprise-grade type definitions */ /** * Default options */ const DEFAULT_OPTIONS = { timeout: 5000, parallel: true, allowUnstableData: false, }; // ============================================================ // Error Types // ============================================================ class FingerprintError extends Error { constructor(message, code, collector) { super(message); this.code = code; this.collector = collector; this.name = "FingerprintError"; } } // ============================================================ // Library Version // ============================================================ const VERSION = "2.0.0"; /** * Utility functions for fingerprinting */ /** * Simple hash function to convert string to number */ function simpleHash(str) { let hash = 0; if (str.length === 0) return hash; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash); } /** * Generate SHA-256 hash (using Web Crypto API if available) */ async function sha256(message) { if (typeof crypto !== "undefined" && crypto.subtle) { const msgBuffer = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); return hashHex; } // Fallback to simple hash if crypto API is not available return simpleHash(message).toString(16); } /** * Check if running in browser environment */ function isBrowser() { return typeof window !== "undefined" && typeof document !== "undefined"; } /** * Safe JSON stringify that handles circular references */ function safeStringify(obj) { const seen = new WeakSet(); return JSON.stringify(obj, (_key, val) => { if (val != null && typeof val === "object") { if (seen.has(val)) { return "[Circular]"; } seen.add(val); } return val; }); } /** * Get a value safely from an object with error handling */ function safeGet(fn, defaultValue) { try { return fn(); } catch (_a) { return defaultValue; } } /** * Base Collector Class * Abstract base class for all fingerprint collectors */ /** * Abstract base class for collectors */ class BaseCollector { /** * Check if collector is supported * Override in subclasses for specific checks */ isSupported() { return isBrowser(); } /** * Safe collection with error handling */ safeCollect(fn, defaultValue) { return safeGet(fn, defaultValue); } /** * Create metadata helper */ static createMetadata(name, options = {}) { var _a, _b, _c, _d; return { name, weight: (_a = options.weight) !== null && _a !== void 0 ? _a : 5, entropy: (_b = options.entropy) !== null && _b !== void 0 ? _b : 2, stable: (_c = options.stable) !== null && _c !== void 0 ? _c : true, category: (_d = options.category) !== null && _d !== void 0 ? _d : "browser", }; } } /** * Async base collector with timeout support */ class AsyncBaseCollector extends BaseCollector { constructor() { super(...arguments); this.timeout = 5000; } /** * Set collector timeout */ setTimeout(ms) { this.timeout = ms; return this; } /** * Wrap async operation with timeout */ async withTimeout(promise, fallback) { return Promise.race([ promise, new Promise((resolve) => setTimeout(() => resolve(fallback), this.timeout)), ]); } } /** * Basic Collectors * Simple browser property collectors */ /** * User Agent Collector */ class UserAgentCollector extends BaseCollector { constructor() { super(...arguments); this.name = "userAgent"; this.metadata = BaseCollector.createMetadata("userAgent", { weight: 8, entropy: 10, stable: true, category: "browser", }); } collect() { return this.safeCollect(() => navigator.userAgent, "unknown"); } } /** * Language Collector */ class LanguageCollector extends BaseCollector { constructor() { super(...arguments); this.name = "language"; this.metadata = BaseCollector.createMetadata("language", { weight: 6, entropy: 5, stable: true, category: "browser", }); } collect() { return this.safeCollect(() => { const languages = []; if (navigator.language) { languages.push(navigator.language); } if (navigator.languages) { languages.push(...navigator.languages); } return [...new Set(languages)]; }, ["unknown"]); } } /** * Timezone Collector */ class TimezoneCollector extends BaseCollector { constructor() { super(...arguments); this.name = "timezone"; this.metadata = BaseCollector.createMetadata("timezone", { weight: 7, entropy: 6, stable: true, category: "browser", }); } collect() { return this.safeCollect(() => { return Intl.DateTimeFormat().resolvedOptions().timeZone; }, "unknown"); } } /** * Screen Collector */ class ScreenCollector extends BaseCollector { constructor() { super(...arguments); this.name = "screen"; this.metadata = BaseCollector.createMetadata("screen", { weight: 7, entropy: 8, stable: true, category: "hardware", }); } collect() { return this.safeCollect(() => ({ width: screen.width, height: screen.height, availWidth: screen.availWidth, availHeight: screen.availHeight, colorDepth: screen.colorDepth, pixelDepth: screen.pixelDepth, devicePixelRatio: window.devicePixelRatio || 1, }), { width: 0, height: 0, availWidth: 0, availHeight: 0, colorDepth: 0, pixelDepth: 0, devicePixelRatio: 1, }); } } /** * Plugins Collector */ class PluginsCollector extends BaseCollector { constructor() { super(...arguments); this.name = "plugins"; this.metadata = BaseCollector.createMetadata("plugins", { weight: 5, entropy: 6, stable: true, category: "browser", }); } collect() { return this.safeCollect(() => { const plugins = []; for (let i = 0; i < navigator.plugins.length; i++) { const plugin = navigator.plugins[i]; plugins.push({ name: plugin.name, description: plugin.description, filename: plugin.filename, }); } return plugins; }, []); } } /** * Canvas and WebGL Collectors * Graphics-based fingerprinting */ /** * Canvas Collector * Creates a unique canvas fingerprint based on rendering differences */ class CanvasCollector extends BaseCollector { constructor() { super(...arguments); this.name = "canvas"; this.metadata = BaseCollector.createMetadata("canvas", { weight: 9, entropy: 12, stable: true, category: "graphics", }); } isSupported() { return (super.isSupported() && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined"); } collect() { return this.safeCollect(() => { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (!ctx) return "no-canvas-context"; // Set canvas size canvas.width = 200; canvas.height = 50; // Draw text with various styles ctx.textBaseline = "top"; ctx.font = "14px Arial"; ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = "#069"; ctx.fillText("Canvas fingerprint 🎨", 2, 15); ctx.fillStyle = "rgba(102, 204, 0, 0.7)"; ctx.fillText("Canvas fingerprint 🎨", 4, 17); // Draw some shapes ctx.globalCompositeOperation = "multiply"; ctx.fillStyle = "rgb(255,0,255)"; ctx.beginPath(); ctx.arc(50, 50, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.fillStyle = "rgb(0,255,255)"; ctx.beginPath(); ctx.arc(100, 50, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); ctx.fillStyle = "rgb(255,255,0)"; ctx.beginPath(); ctx.arc(75, 100, 50, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); return canvas.toDataURL(); }, "no-canvas"); } } /** * WebGL Collector * Collects WebGL rendering context information */ class WebGLCollector extends BaseCollector { constructor() { super(...arguments); this.name = "webgl"; this.metadata = BaseCollector.createMetadata("webgl", { weight: 9, entropy: 15, stable: true, category: "graphics", }); } isSupported() { return (super.isSupported() && typeof document !== "undefined" && typeof HTMLCanvasElement !== "undefined"); } collect() { return this.safeCollect(() => { const canvas = document.createElement("canvas"); const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl"); if (!gl) return { error: "no-webgl-context" }; const result = {}; // Get basic WebGL info result.vendor = gl.getParameter(gl.VENDOR); result.renderer = gl.getParameter(gl.RENDERER); result.version = gl.getParameter(gl.VERSION); result.shadingLanguageVersion = gl.getParameter(gl.SHADING_LANGUAGE_VERSION); // Get supported extensions const extensions = gl.getSupportedExtensions(); result.extensions = extensions ? extensions.sort() : []; // Get additional parameters result.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); result.maxViewportDims = Array.from(gl.getParameter(gl.MAX_VIEWPORT_DIMS)); result.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS); result.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS); result.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS); result.maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS); // Get unmasked info if available const debugInfo = gl.getExtension("WEBGL_debug_renderer_info"); if (debugInfo) { result.unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL); result.unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL); } return result; }, { error: "webgl-not-available" }); } } /** * Advanced Collectors * Audio and Fonts fingerprinting */ /** * Audio Collector * Creates a fingerprint based on audio processing */ class AudioCollector extends AsyncBaseCollector { constructor() { super(...arguments); this.name = "audio"; this.metadata = BaseCollector.createMetadata("audio", { weight: 8, entropy: 10, stable: false, // Audio fingerprint can vary on first run category: "audio", }); } isSupported() { return (super.isSupported() && (typeof AudioContext !== "undefined" || typeof window.webkitAudioContext !== "undefined")); } async collect() { if (!this.isSupported()) { return { error: "audio-not-available" }; } return this.withTimeout(this.collectAudio(), { error: "audio-timeout", }); } async collectAudio() { const AudioContextClass = window.AudioContext || window.webkitAudioContext; if (!AudioContextClass) { return { error: "no-audio-context" }; } const audioContext = new AudioContextClass(); const result = {}; try { result.sampleRate = audioContext.sampleRate; result.state = audioContext.state; result.maxChannelCount = audioContext.destination.maxChannelCount; result.channelCount = audioContext.destination.channelCount; result.channelCountMode = audioContext.destination.channelCountMode; result.channelInterpretation = audioContext.destination.channelInterpretation; // Create audio fingerprint using oscillator const oscillator = audioContext.createOscillator(); const analyser = audioContext.createAnalyser(); const gainNode = audioContext.createGain(); const scriptProcessor = audioContext.createScriptProcessor(4096, 1, 1); gainNode.gain.setValueAtTime(0, audioContext.currentTime); oscillator.type = "triangle"; oscillator.frequency.setValueAtTime(10000, audioContext.currentTime); oscillator.connect(analyser); analyser.connect(scriptProcessor); scriptProcessor.connect(gainNode); gainNode.connect(audioContext.destination); let audioFingerprint = ""; scriptProcessor.onaudioprocess = () => { const freqData = new Float32Array(analyser.frequencyBinCount); analyser.getFloatFrequencyData(freqData); let sum = 0; for (let i = 0; i < freqData.length; i++) { sum += Math.abs(freqData[i]); } audioFingerprint = sum.toString(); }; oscillator.start(audioContext.currentTime); oscillator.stop(audioContext.currentTime + 0.1); await new Promise((resolve) => setTimeout(resolve, 100)); result.audioFingerprint = audioFingerprint; await audioContext.close(); return result; } catch (error) { try { await audioContext.close(); } catch (_a) { } return { error: "audio-processing-failed", }; } } } /** * Fonts Collector * Detects available fonts on the system */ class FontsCollector extends BaseCollector { constructor() { super(...arguments); this.name = "fonts"; this.metadata = BaseCollector.createMetadata("fonts", { weight: 8, entropy: 12, stable: true, category: "browser", }); this.testFonts = [ "Arial", "Arial Black", "Arial Narrow", "Arial Rounded MT Bold", "Bookman Old Style", "Bradley Hand ITC", "Century", "Century Gothic", "Comic Sans MS", "Courier", "Courier New", "Georgia", "Gentium", "Helvetica", "Helvetica Neue", "Impact", "King", "Lucida Console", "Lalit", "Modena", "Monotype Corsiva", "Papyrus", "Tahoma", "TeX", "Times", "Times New Roman", "Trebuchet MS", "Verdana", "Verona", // Additional common fonts "Monaco", "Consolas", "Lucida Sans Unicode", "Palatino Linotype", "Segoe UI", "Candara", "Cambria", "Garamond", "Perpetua", "Rockwell", "Franklin Gothic Medium", ]; } isSupported() { return super.isSupported() && typeof document !== "undefined"; } collect() { if (!this.isSupported()) { return []; } return this.safeCollect(() => { const availableFonts = []; const testString = "mmmmmmmmmmlli"; const testSize = "72px"; const body = document.getElementsByTagName("body")[0]; if (!body) return []; // Create test span element const span = document.createElement("span"); span.style.fontSize = testSize; span.style.position = "absolute"; span.style.left = "-9999px"; span.style.visibility = "hidden"; span.innerHTML = testString; body.appendChild(span); // Get default width and height span.style.fontFamily = "monospace"; const defaultWidth = span.offsetWidth; const defaultHeight = span.offsetHeight; // Test each font for (const font of this.testFonts) { span.style.fontFamily = `'${font}', monospace`; if (span.offsetWidth !== defaultWidth || span.offsetHeight !== defaultHeight) { availableFonts.push(font); } } body.removeChild(span); return availableFonts; }, []); } } /** * Battery Collector * Collects battery status information (where available) */ /** * Battery Collector * Note: Battery API is deprecated but still works in some browsers */ class BatteryCollector extends AsyncBaseCollector { constructor() { super(...arguments); this.name = "battery"; this.metadata = BaseCollector.createMetadata("battery", { weight: 3, entropy: 3, stable: false, // Battery stats change category: "hardware", }); } isSupported() { return (super.isSupported() && "getBattery" in navigator); } async collect() { if (!this.isSupported()) { return { supported: false }; } return this.withTimeout(this.collectBattery(), { supported: false }); } async collectBattery() { try { const battery = await navigator.getBattery(); return { supported: true, charging: battery.charging, level: battery.level, chargingTime: battery.chargingTime, dischargingTime: battery.dischargingTime, }; } catch (_a) { return { supported: false }; } } } /** * Client Hints Collector * Collects modern User-Agent Client Hints data */ /** * Client Hints Collector * Modern alternative to User-Agent parsing */ class ClientHintsCollector extends AsyncBaseCollector { constructor() { super(...arguments); this.name = "clientHints"; this.metadata = BaseCollector.createMetadata("clientHints", { weight: 7, entropy: 10, stable: true, category: "browser", }); } isSupported() { var _a; return (super.isSupported() && "userAgentData" in navigator && typeof ((_a = navigator.userAgentData) === null || _a === void 0 ? void 0 : _a.getHighEntropyValues) === "function"); } async collect() { if (!this.isSupported()) { return { brands: [], mobile: false, platform: "unknown", }; } return this.withTimeout(this.collectClientHints(), { brands: [], mobile: false, platform: "unknown", }); } async collectClientHints() { try { const uaData = navigator.userAgentData; const result = { brands: uaData.brands || [], mobile: uaData.mobile || false, platform: uaData.platform || "unknown", }; // Try to get high entropy values try { const highEntropy = await uaData.getHighEntropyValues([ "architecture", "bitness", "model", "platformVersion", "fullVersionList", ]); result.architecture = highEntropy.architecture; result.bitness = highEntropy.bitness; result.model = highEntropy.model; result.platformVersion = highEntropy.platformVersion; result.fullVersionList = highEntropy.fullVersionList; } catch (_a) { // High entropy values may be blocked } return result; } catch (error) { return { brands: [], mobile: false, platform: "unknown", }; } } } /** * Connection Collector * Collects network connection information */ /** * Connection Collector * Uses Network Information API */ class ConnectionCollector extends BaseCollector { constructor() { super(...arguments); this.name = "connection"; this.metadata = BaseCollector.createMetadata("connection", { weight: 4, entropy: 4, stable: false, // Connection can change category: "network", }); } isSupported() { return (super.isSupported() && "connection" in navigator); } collect() { if (!this.isSupported()) { return { supported: false }; } return this.safeCollect(() => { const connection = navigator.connection; return { supported: true, effectiveType: connection.effectiveType, downlink: connection.downlink, rtt: connection.rtt, saveData: connection.saveData, type: connection.type, }; }, { supported: false }); } } /** * Hardware Collector * Collects hardware-related information */ /** * Hardware Collector * Collects CPU, memory, and device information */ class HardwareCollector extends BaseCollector { constructor() { super(...arguments); this.name = "hardware"; this.metadata = BaseCollector.createMetadata("hardware", { weight: 8, entropy: 8, stable: true, category: "hardware", }); } collect() { return this.safeCollect(() => { const nav = navigator; return { hardwareConcurrency: nav.hardwareConcurrency || 0, deviceMemory: nav.deviceMemory || null, platform: nav.platform || "unknown", maxTouchPoints: nav.maxTouchPoints || 0, oscpu: nav.oscpu, // Firefox only }; }, { hardwareConcurrency: 0, deviceMemory: null, platform: "unknown", maxTouchPoints: 0, }); } } /** * Math Collector * Creates fingerprint based on math precision differences */ /** * Math Collector * Different browsers/CPUs can produce slightly different math results */ class MathCollector extends BaseCollector { constructor() { super(...arguments); this.name = "math"; this.metadata = BaseCollector.createMetadata("math", { weight: 6, entropy: 6, stable: true, category: "browser", }); } collect() { return this.safeCollect(() => ({ tan: Math.tan(-1e300).toString(), sin: Math.sin(-1e300).toString(), atan2: Math.atan2(0.5, 0.5).toString(), log: Math.log(1000).toString(), pow: Math.pow(Math.PI, -100).toString(), sqrt: Math.sqrt(2).toString(), }), { tan: "", sin: "", atan2: "", log: "", pow: "", sqrt: "", }); } } /** * Media Devices Collector * Enumerates available media input/output devices */ /** * Media Devices Collector * Counts cameras, microphones, and speakers */ class MediaDevicesCollector extends AsyncBaseCollector { constructor() { super(...arguments); this.name = "mediaDevices"; this.metadata = BaseCollector.createMetadata("mediaDevices", { weight: 5, entropy: 5, stable: true, category: "hardware", }); } isSupported() { return (super.isSupported() && "mediaDevices" in navigator && typeof navigator.mediaDevices.enumerateDevices === "function"); } async collect() { if (!this.isSupported()) { return { audioInputs: 0, audioOutputs: 0, videoInputs: 0, hasMediaDevices: false, }; } return this.withTimeout(this.collectDevices(), { audioInputs: 0, audioOutputs: 0, videoInputs: 0, hasMediaDevices: true, }); } async collectDevices() { try { const devices = await navigator.mediaDevices.enumerateDevices(); return { audioInputs: devices.filter((d) => d.kind === "audioinput").length, audioOutputs: devices.filter((d) => d.kind === "audiooutput").length, videoInputs: devices.filter((d) => d.kind === "videoinput").length, hasMediaDevices: true, }; } catch (_a) { return { audioInputs: 0, audioOutputs: 0, videoInputs: 0, hasMediaDevices: true, }; } } } /** * Permissions Collector * Checks permission states for various APIs */ /** * Permissions Collector * Uses Permissions API to check permission states */ class PermissionsCollector extends AsyncBaseCollector { constructor() { super(...arguments); this.name = "permissions"; this.metadata = BaseCollector.createMetadata("permissions", { weight: 4, entropy: 5, stable: false, // Permissions can change category: "permissions", }); } isSupported() { return (super.isSupported() && "permissions" in navigator && typeof navigator.permissions.query === "function"); }