@scrubbe-auth/device-fingerprint
Version:
Advanced device fingerprinting for unique user identification
1,080 lines (1,064 loc) • 39.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
class CryptoUtils {
static async sha256(text) {
if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) {
const encoder = new TextEncoder();
const data = encoder.encode(text);
const hashBuffer = await window.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// Fallback for Node.js or older browsers
return this.simpleHash(text);
}
static sha1(text) {
// Simple SHA-1 implementation for fallback
return this.simpleHash(text, 'sha1');
}
static md5(text) {
// Simple MD5 implementation for fallback
return this.simpleHash(text, 'md5');
}
static murmur3(text, seed = 0) {
let h1 = seed;
for (let i = 0; i < text.length; i++) {
let k1 = text.charCodeAt(i);
k1 = Math.imul(k1, 0xcc9e2d51);
k1 = (k1 << 15) | (k1 >>> 17);
k1 = Math.imul(k1, 0x1b873593);
h1 ^= k1;
h1 = (h1 << 13) | (h1 >>> 19);
h1 = Math.imul(h1, 5) + 0xe6546b64;
}
h1 ^= text.length;
h1 ^= h1 >>> 16;
h1 = Math.imul(h1, 0x85ebca6b);
h1 ^= h1 >>> 13;
h1 = Math.imul(h1, 0xc2b2ae35);
h1 ^= h1 >>> 16;
return (h1 >>> 0).toString(16);
}
static simpleHash(text, algorithm = 'sha256') {
let hash = 0;
if (text.length === 0)
return hash.toString();
for (let i = 0; i < text.length; i++) {
const char = text.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32bit integer
}
const abs = Math.abs(hash);
return abs.toString(16).padStart(8, '0');
}
}
class CanvasCollector {
async collect(config = {}) {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return {
hash: 'unavailable',
supported: false,
error: 'Canvas API not available'
};
}
this.canvas = document.createElement('canvas');
this.canvas.width = 200;
this.canvas.height = 50;
this.ctx = this.canvas.getContext('2d');
if (!this.ctx) {
return {
hash: 'unsupported',
supported: false,
error: 'Canvas 2D context not supported'
};
}
// Draw complex pattern for more unique fingerprint
this.drawFingerprint(config.text || 'Scrubbe Analytics 2025! 🚀');
const dataURL = this.canvas.toDataURL();
const hash = await CryptoUtils.sha256(dataURL);
return {
hash,
dataURL: dataURL.substring(0, 100) + '...', // Truncate for privacy
supported: true
};
}
catch (error) {
return {
hash: 'error',
supported: false,
error: error instanceof Error ? error.message : 'Unknown canvas error'
};
}
finally {
this.cleanup();
}
}
drawFingerprint(text) {
if (!this.ctx || !this.canvas)
return;
// Set complex styling
this.ctx.textBaseline = 'top';
this.ctx.font = '14px "Arial, sans-serif"';
this.ctx.textBaseline = 'alphabetic';
this.ctx.fillStyle = '#f60';
this.ctx.fillRect(125, 1, 62, 20);
// Add gradient
const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, 0);
gradient.addColorStop(0, '#00ff00');
gradient.addColorStop(0.5, '#ff0000');
gradient.addColorStop(1, '#0000ff');
this.ctx.fillStyle = gradient;
// Draw text with shadow
this.ctx.shadowColor = '#333';
this.ctx.shadowOffsetX = 2;
this.ctx.shadowOffsetY = 2;
this.ctx.shadowBlur = 2;
this.ctx.fillText(text, 2, 15);
// Add geometric shapes
this.ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
this.ctx.fillText(text, 4, 17);
// Draw circle
this.ctx.beginPath();
this.ctx.arc(50, 25, 20, 0, Math.PI * 2);
this.ctx.closePath();
this.ctx.fill();
// Add more complexity
this.ctx.globalCompositeOperation = 'multiply';
this.ctx.fillStyle = 'rgb(255,0,255)';
this.ctx.beginPath();
this.ctx.arc(75, 25, 20, 0, Math.PI * 2);
this.ctx.closePath();
this.ctx.fill();
}
cleanup() {
if (this.canvas) {
this.canvas.remove();
this.canvas = undefined;
this.ctx = undefined;
}
}
}
class WebGLCollector {
async collect() {
try {
if (typeof window === 'undefined' || typeof document === 'undefined') {
return {
renderer: 'unavailable',
vendor: 'unavailable',
version: 'unavailable',
shadingLanguageVersion: 'unavailable',
extensions: [],
parameters: {},
hash: 'unavailable',
supported: false,
error: 'WebGL not available'
};
}
const canvas = document.createElement('canvas');
// Try to get WebGL context with proper type casting
const gl = canvas.getContext('webgl') ||
canvas.getContext('experimental-webgl');
if (!gl) {
return {
renderer: 'unsupported',
vendor: 'unsupported',
version: 'unsupported',
shadingLanguageVersion: 'unsupported',
extensions: [],
parameters: {},
hash: 'unsupported',
supported: false,
error: 'WebGL context creation failed'
};
}
// Get debug info extension with proper type checking
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
// Get renderer and vendor info
const renderer = debugInfo ?
gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) :
gl.getParameter(gl.RENDERER);
const vendor = debugInfo ?
gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) :
gl.getParameter(gl.VENDOR);
const fingerprint = {
renderer: this.safeStringify(renderer) || 'unknown',
vendor: this.safeStringify(vendor) || 'unknown',
version: this.safeStringify(gl.getParameter(gl.VERSION)) || 'unknown',
shadingLanguageVersion: this.safeStringify(gl.getParameter(gl.SHADING_LANGUAGE_VERSION)) || 'unknown',
extensions: this.getExtensions(gl),
parameters: this.getParameters(gl),
hash: '',
supported: true
};
// Create hash from all collected data
const hashInput = JSON.stringify({
renderer: fingerprint.renderer,
vendor: fingerprint.vendor,
version: fingerprint.version,
shadingLanguageVersion: fingerprint.shadingLanguageVersion,
extensions: fingerprint.extensions,
parameters: fingerprint.parameters
});
fingerprint.hash = await CryptoUtils.sha256(hashInput);
// Clean up
canvas.remove();
return fingerprint;
}
catch (error) {
return {
renderer: 'error',
vendor: 'error',
version: 'error',
shadingLanguageVersion: 'error',
extensions: [],
parameters: {},
hash: 'error',
supported: false,
error: error instanceof Error ? error.message : 'Unknown WebGL error'
};
}
}
getExtensions(gl) {
const extensions = [];
try {
const supportedExtensions = gl.getSupportedExtensions();
if (supportedExtensions) {
extensions.push(...supportedExtensions);
}
}
catch (error) {
// Ignore extension enumeration errors
}
return extensions.sort();
}
getParameters(gl) {
const parameters = {};
// Define parameter constants with their values
const parameterMap = {
'MAX_VERTEX_ATTRIBS': gl.MAX_VERTEX_ATTRIBS,
'MAX_VERTEX_UNIFORM_VECTORS': gl.MAX_VERTEX_UNIFORM_VECTORS,
'MAX_VERTEX_TEXTURE_IMAGE_UNITS': gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS,
'MAX_VARYING_VECTORS': gl.MAX_VARYING_VECTORS,
'ALIASED_LINE_WIDTH_RANGE': gl.ALIASED_LINE_WIDTH_RANGE,
'ALIASED_POINT_SIZE_RANGE': gl.ALIASED_POINT_SIZE_RANGE,
'MAX_FRAGMENT_UNIFORM_VECTORS': gl.MAX_FRAGMENT_UNIFORM_VECTORS,
'MAX_TEXTURE_IMAGE_UNITS': gl.MAX_TEXTURE_IMAGE_UNITS,
'MAX_TEXTURE_SIZE': gl.MAX_TEXTURE_SIZE,
'MAX_CUBE_MAP_TEXTURE_SIZE': gl.MAX_CUBE_MAP_TEXTURE_SIZE,
'MAX_VIEWPORT_DIMS': gl.MAX_VIEWPORT_DIMS,
'RED_BITS': gl.RED_BITS,
'GREEN_BITS': gl.GREEN_BITS,
'BLUE_BITS': gl.BLUE_BITS,
'ALPHA_BITS': gl.ALPHA_BITS,
'DEPTH_BITS': gl.DEPTH_BITS,
'STENCIL_BITS': gl.STENCIL_BITS
};
Object.entries(parameterMap).forEach(([name, constant]) => {
try {
const param = gl.getParameter(constant);
if (param !== null) {
parameters[name] = Array.isArray(param) ? Array.from(param) : param;
}
}
catch (error) {
// Ignore individual parameter errors
}
});
return parameters;
}
safeStringify(value) {
if (value === null || value === undefined) {
return 'null';
}
if (typeof value === 'string') {
return value;
}
try {
return String(value);
}
catch {
return 'unknown';
}
}
}
class AudioCollector {
async collect(config = {}) {
const timeout = config.timeout || 1000;
return new Promise((resolve) => {
const timer = setTimeout(() => {
resolve({
hash: 'timeout',
oscillatorHash: 'timeout',
dynamicsHash: 'timeout',
supported: false,
error: 'Audio fingerprinting timeout'
});
}, timeout);
try {
if (typeof window === 'undefined' || !window.AudioContext && !window.webkitAudioContext) {
clearTimeout(timer);
resolve({
hash: 'unavailable',
oscillatorHash: 'unavailable',
dynamicsHash: 'unavailable',
supported: false,
error: 'AudioContext not available'
});
return;
}
const AudioContext = window.AudioContext || window.webkitAudioContext;
const context = new AudioContext();
// Create oscillator fingerprint
const oscillator = context.createOscillator();
const analyser = context.createAnalyser();
const gain = context.createGain();
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
gain.gain.value = 0; // Mute output
oscillator.type = 'triangle';
oscillator.frequency.value = 10000;
oscillator.connect(analyser);
analyser.connect(scriptProcessor);
scriptProcessor.connect(gain);
gain.connect(context.destination);
oscillator.start(0);
const oscillatorData = [];
scriptProcessor.onaudioprocess = (event) => {
const buffer = event.inputBuffer.getChannelData(0);
for (let i = 0; i < buffer.length; i++) {
oscillatorData.push(buffer[i]);
}
if (oscillatorData.length >= 4096) {
oscillator.stop();
oscillator.disconnect();
scriptProcessor.disconnect();
context.close();
clearTimeout(timer);
this.processAudioData(oscillatorData).then(result => {
resolve({
hash: result.combined,
oscillatorHash: result.oscillator,
dynamicsHash: result.dynamics,
supported: true
});
});
}
};
}
catch (error) {
clearTimeout(timer);
resolve({
hash: 'error',
oscillatorHash: 'error',
dynamicsHash: 'error',
supported: false,
error: error instanceof Error ? error.message : 'Unknown audio error'
});
}
});
}
async processAudioData(data) {
// Calculate various audio characteristics
const sum = data.reduce((a, b) => a + b, 0);
const avg = sum / data.length;
const variance = data.reduce((acc, val) => acc + Math.pow(val - avg, 2), 0) / data.length;
const stdDev = Math.sqrt(variance);
// Create frequency analysis
const frequencies = this.getFrequencySignature(data);
const oscillatorHash = await CryptoUtils.sha256(JSON.stringify({
sum: sum.toFixed(10),
avg: avg.toFixed(10),
length: data.length
}));
const dynamicsHash = await CryptoUtils.sha256(JSON.stringify({
variance: variance.toFixed(10),
stdDev: stdDev.toFixed(10),
frequencies
}));
const combined = await CryptoUtils.sha256(oscillatorHash + dynamicsHash);
return { combined, oscillator: oscillatorHash, dynamics: dynamicsHash };
}
getFrequencySignature(data) {
// Simple frequency analysis - get peaks at different intervals
const signature = [];
const bucketSize = Math.floor(data.length / 32);
for (let i = 0; i < 32; i++) {
const start = i * bucketSize;
const end = Math.min(start + bucketSize, data.length);
const bucket = data.slice(start, end);
const max = Math.max(...bucket);
signature.push(parseFloat(max.toFixed(6)));
}
return signature;
}
}
class ScreenCollector {
collect() {
if (typeof window === 'undefined' || typeof screen === 'undefined') {
return {
width: 0,
height: 0,
availWidth: 0,
availHeight: 0,
colorDepth: 0,
pixelDepth: 0,
pixelRatio: 1,
touch: false
};
}
return {
width: screen.width,
height: screen.height,
availWidth: screen.availWidth,
availHeight: screen.availHeight,
colorDepth: screen.colorDepth,
pixelDepth: screen.pixelDepth || screen.colorDepth,
pixelRatio: window.devicePixelRatio || 1,
orientation: screen.orientation?.type,
touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0
};
}
}
class BrowserCollector {
collect() {
if (typeof navigator === 'undefined') {
return {
userAgent: 'unavailable',
name: 'unknown',
version: 'unknown',
engine: 'unknown',
engineVersion: 'unknown',
os: 'unknown',
osVersion: 'unknown',
mobile: false,
cookieEnabled: false,
doNotTrack: null,
onLine: false
};
}
const userAgent = navigator.userAgent;
const parsed = this.parseUserAgent(userAgent);
return {
userAgent,
name: parsed.browser,
version: parsed.browserVersion,
engine: parsed.engine,
engineVersion: parsed.engineVersion,
os: parsed.os,
osVersion: parsed.osVersion,
mobile: parsed.mobile,
cookieEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack || null,
buildID: navigator.buildID,
productSub: navigator.productSub,
onLine: navigator.onLine
};
}
parseUserAgent(ua) {
// Simplified user agent parsing
const result = {
browser: 'Unknown',
browserVersion: '0',
engine: 'Unknown',
engineVersion: '0',
os: 'Unknown',
osVersion: '0',
mobile: false
};
// Detect browser
if (ua.includes('Chrome/')) {
result.browser = 'Chrome';
result.browserVersion = this.extractVersion(ua, 'Chrome/');
result.engine = 'Blink';
}
else if (ua.includes('Firefox/')) {
result.browser = 'Firefox';
result.browserVersion = this.extractVersion(ua, 'Firefox/');
result.engine = 'Gecko';
}
else if (ua.includes('Safari/') && !ua.includes('Chrome/')) {
result.browser = 'Safari';
result.browserVersion = this.extractVersion(ua, 'Version/');
result.engine = 'WebKit';
}
else if (ua.includes('Edge/')) {
result.browser = 'Edge';
result.browserVersion = this.extractVersion(ua, 'Edge/');
result.engine = 'EdgeHTML';
}
// Detect OS
if (ua.includes('Windows NT')) {
result.os = 'Windows';
result.osVersion = this.extractVersion(ua, 'Windows NT ');
}
else if (ua.includes('Mac OS X')) {
result.os = 'macOS';
result.osVersion = this.extractVersion(ua, 'Mac OS X ').replace(/_/g, '.');
}
else if (ua.includes('Linux')) {
result.os = 'Linux';
}
else if (ua.includes('Android')) {
result.os = 'Android';
result.osVersion = this.extractVersion(ua, 'Android ');
result.mobile = true;
}
else if (ua.includes('iPhone') || ua.includes('iPad')) {
result.os = 'iOS';
result.osVersion = this.extractVersion(ua, 'OS ').replace(/_/g, '.');
result.mobile = ua.includes('iPhone');
}
return result;
}
extractVersion(ua, marker) {
const index = ua.indexOf(marker);
if (index === -1)
return '0';
const start = index + marker.length;
const version = ua.substring(start).split(/[^\d.]/)[0];
return version || '0';
}
}
class HardwareCollector {
collect() {
if (typeof navigator === 'undefined') {
return {
cpuCores: 0,
platform: 'unknown',
maxTouchPoints: 0,
hardwareConcurrency: 0
};
}
return {
cpuCores: navigator.hardwareConcurrency || 0,
memory: navigator.deviceMemory,
platform: navigator.platform,
architecture: navigator.userAgentData?.platform,
maxTouchPoints: navigator.maxTouchPoints || 0,
hardwareConcurrency: navigator.hardwareConcurrency || 0,
deviceMemory: navigator.deviceMemory
};
}
}
class FontCollector {
async collect(customFonts = []) {
const fontsToTest = [...FontCollector.DEFAULT_FONTS, ...customFonts];
const availableFonts = [];
if (typeof document === 'undefined') {
return availableFonts;
}
const testContainer = this.createTestContainer();
try {
for (const font of fontsToTest) {
if (await this.isFontAvailable(font, testContainer)) {
availableFonts.push(font);
}
}
}
finally {
this.cleanupTestContainer(testContainer);
}
return availableFonts.sort();
}
createTestContainer() {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.left = '-9999px';
container.style.top = '-9999px';
container.style.visibility = 'hidden';
document.body.appendChild(container);
return container;
}
async isFontAvailable(fontName, container) {
const testText = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const fontSize = '72px';
const baseFonts = ['monospace', 'sans-serif', 'serif'];
const measurements = [];
// Measure with base fonts
for (const baseFont of baseFonts) {
const testElement = document.createElement('span');
testElement.style.fontSize = fontSize;
testElement.style.fontFamily = baseFont;
testElement.textContent = testText;
container.appendChild(testElement);
measurements.push(testElement.offsetWidth);
container.removeChild(testElement);
}
// Test with target font
for (let i = 0; i < baseFonts.length; i++) {
const testElement = document.createElement('span');
testElement.style.fontSize = fontSize;
testElement.style.fontFamily = `"${fontName}", ${baseFonts[i]}`;
testElement.textContent = testText;
container.appendChild(testElement);
const width = testElement.offsetWidth;
container.removeChild(testElement);
if (width !== measurements[i]) {
return true;
}
}
return false;
}
cleanupTestContainer(container) {
if (container.parentNode) {
container.parentNode.removeChild(container);
}
}
}
FontCollector.DEFAULT_FONTS = [
// Windows fonts
'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS', 'Consolas',
'Courier New', 'Georgia', 'Impact', 'Lucida Console', 'Lucida Sans Unicode',
'Microsoft Sans Serif', 'Palatino Linotype', 'Segoe UI', 'Tahoma', 'Times New Roman',
'Trebuchet MS', 'Verdana',
// macOS fonts
'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji',
'Arial Unicode MS', 'Avenir', 'Avenir Next', 'Big Caslon', 'Brush Script MT',
'Cochin', 'Copperplate', 'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica',
'Helvetica Neue', 'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Menlo',
'Monaco', 'Optima', 'Palatino', 'Papyrus', 'Phosphate', 'Rockwell', 'Savoye LET',
'SignPainter', 'Skia', 'Snell Roundhand', 'System Font', 'Times',
// Linux fonts
'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif', 'Droid Sans', 'Droid Serif',
'FreeMono', 'FreeSans', 'FreeSerif', 'Liberation Mono', 'Liberation Sans',
'Liberation Serif', 'Linux Libertine', 'Noto Sans', 'Open Sans', 'Source Code Pro',
'Ubuntu', 'Ubuntu Mono'
];
class PluginCollector {
collect() {
const plugins = [];
if (typeof navigator === 'undefined' || !navigator.plugins) {
return plugins;
}
try {
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,
version: plugin.version || 'unknown'
});
}
}
catch (error) {
// Some browsers might restrict plugin enumeration
}
return plugins.sort((a, b) => a.name.localeCompare(b.name));
}
}
class TimezoneCollector {
collect() {
try {
return {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
offset: new Date().getTimezoneOffset(),
dst: this.isDST(),
locale: Intl.DateTimeFormat().resolvedOptions().locale
};
}
catch (error) {
return {
timezone: 'UTC',
offset: 0,
dst: false,
locale: 'en-US'
};
}
}
isDST() {
const now = new Date();
const january = new Date(now.getFullYear(), 0, 1);
const july = new Date(now.getFullYear(), 6, 1);
return Math.max(january.getTimezoneOffset(), july.getTimezoneOffset()) !== now.getTimezoneOffset();
}
}
class LanguageCollector {
collect() {
try {
if (typeof navigator === 'undefined') {
return {
language: 'en-US',
languages: ['en-US'],
acceptLanguage: undefined
};
}
return {
language: navigator.language,
languages: Array.from(navigator.languages || []),
acceptLanguage: navigator.userLanguage
};
}
catch (error) {
return {
language: 'en-US',
languages: ['en-US'],
acceptLanguage: undefined
};
}
}
}
class StorageCollector {
collect() {
try {
return {
localStorage: this.testStorage('localStorage'),
sessionStorage: this.testStorage('sessionStorage'),
indexedDB: 'indexedDB' in window,
webSQL: 'openDatabase' in window,
quota: this.getQuotaInfo()
};
}
catch (error) {
return {
localStorage: false,
sessionStorage: false,
indexedDB: false,
webSQL: false,
quota: undefined
};
}
}
testStorage(type) {
try {
if (typeof window === 'undefined')
return false;
const storage = window[type];
const test = '__storage_test__';
storage.setItem(test, test);
storage.removeItem(test);
return true;
}
catch {
return false;
}
}
getQuotaInfo() {
try {
if (typeof navigator !== 'undefined' && navigator.webkitTemporaryStorage) {
return navigator.webkitTemporaryStorage.quotaUsage;
}
return undefined;
}
catch {
return undefined;
}
}
}
const VERSION = '1.0.0';
class DeviceFingerprints {
constructor(config = {}) {
this.errors = [];
this.config = {
// Core methods
enableCanvas: config.enableCanvas ?? true,
enableWebGL: config.enableWebGL ?? true,
enableAudio: config.enableAudio ?? false,
enableScreen: config.enableScreen ?? true,
enableBrowser: config.enableBrowser ?? true,
enableFonts: config.enableFonts ?? false,
enablePlugins: config.enablePlugins ?? false,
enableTimezone: config.enableTimezone ?? true,
enableLanguage: config.enableLanguage ?? true,
enableHardware: config.enableHardware ?? true,
enableStorage: config.enableStorage ?? true,
// Advanced options
canvasText: config.canvasText || 'Scrubbe Analytics 2025! 🚀',
audioTimeout: config.audioTimeout || 1000,
fontList: config.fontList || [],
excludeUserAgent: config.excludeUserAgent ?? false,
excludeLanguage: config.excludeLanguage ?? false,
excludeColorDepth: config.excludeColorDepth ?? false,
excludePixelRatio: config.excludePixelRatio ?? false,
excludeTimezone: config.excludeTimezone ?? false,
// Privacy & Security
respectDNT: config.respectDNT ?? true,
anonymize: config.anonymize ?? false,
hashAlgorithm: config.hashAlgorithm || 'sha256',
// Performance
timeout: config.timeout || 5000,
debug: config.debug ?? false,
cache: config.cache ?? true,
cacheTimeout: config.cacheTimeout || 300000
};
if (this.config.debug) {
console.log('DeviceFingerprint initialized with config:', this.config);
}
}
async generate() {
// Check DNT
if (this.config.respectDNT && this.isDNTEnabled()) {
return this.createMinimalFingerprint('dnt-enabled');
}
// Check cache
if (this.config.cache && this.isCacheValid()) {
if (this.config.debug) {
console.log('Returning cached fingerprint');
}
return this.cache;
}
try {
const startTime = Date.now();
const fingerprint = await this.collectFingerprint();
const endTime = Date.now();
if (this.config.debug) {
console.log(`Fingerprint generated in ${endTime - startTime}ms`);
console.log('Errors encountered:', this.errors);
}
// Cache result
if (this.config.cache) {
this.cache = fingerprint;
this.cacheTimestamp = Date.now();
}
return fingerprint;
}
catch (error) {
this.addError('generation', error instanceof Error ? error.message : 'Unknown error');
return this.createMinimalFingerprint('generation-error');
}
}
async collectFingerprint() {
const promises = [];
// Collect asynchronous fingerprint components
if (this.config.enableCanvas) {
promises.push(this.collectCanvas());
}
if (this.config.enableWebGL) {
promises.push(this.collectWebGL());
}
if (this.config.enableAudio) {
promises.push(this.collectAudio());
}
if (this.config.enableFonts) {
promises.push(this.collectFonts());
}
const [canvas, webgl, audio, fonts] = await Promise.allSettled(promises);
// Collect synchronous data
const screen = this.config.enableScreen ? new ScreenCollector().collect() : undefined;
const browser = this.config.enableBrowser ? new BrowserCollector().collect() : undefined;
const hardware = this.config.enableHardware ? new HardwareCollector().collect() : undefined;
const plugins = this.config.enablePlugins ? new PluginCollector().collect() : undefined;
const timezone = this.config.enableTimezone ? new TimezoneCollector().collect() : undefined;
const language = this.config.enableLanguage ? new LanguageCollector().collect() : undefined;
const storage = this.config.enableStorage ? new StorageCollector().collect() : undefined;
// Extract settled results
const canvasResult = canvas.status === 'fulfilled' ? canvas.value : undefined;
const webglResult = webgl.status === 'fulfilled' ? webgl.value : undefined;
const audioResult = audio.status === 'fulfilled' ? audio.value : undefined;
const fontsResult = fonts.status === 'fulfilled' ? fonts.value : undefined;
// Generate unique device ID
const components = this.getHashComponents({
canvas: canvasResult,
webgl: webglResult,
audio: audioResult,
screen,
browser,
hardware,
plugins,
timezone,
language,
storage,
fonts: fontsResult
});
const deviceId = await this.generateDeviceId(components);
const confidence = this.calculateConfidence({
canvas: canvasResult,
webgl: webglResult,
audio: audioResult,
screen,
browser,
fonts: fontsResult
});
const fingerprint = {
deviceId,
confidence,
timestamp: Date.now(),
canvas: canvasResult,
webgl: webglResult,
audio: audioResult,
screen,
browser,
hardware,
plugins,
timezone,
language,
storage,
fonts: fontsResult,
version: VERSION,
generatedAt: Date.now()
};
if (!this.config.anonymize && browser) {
fingerprint.userAgent = browser.userAgent;
}
return fingerprint;
}
async collectCanvas() {
try {
return await new CanvasCollector().collect({
text: this.config.canvasText,
timeout: this.config.timeout
});
}
catch (error) {
this.addError('canvas', error instanceof Error ? error.message : 'Canvas collection failed');
return undefined;
}
}
async collectWebGL() {
try {
return await new WebGLCollector().collect();
}
catch (error) {
this.addError('webgl', error instanceof Error ? error.message : 'WebGL collection failed');
return undefined;
}
}
async collectAudio() {
try {
return await new AudioCollector().collect({
timeout: this.config.audioTimeout
});
}
catch (error) {
this.addError('audio', error instanceof Error ? error.message : 'Audio collection failed');
return undefined;
}
}
async collectFonts() {
try {
return await new FontCollector().collect(this.config.fontList);
}
catch (error) {
this.addError('fonts', error instanceof Error ? error.message : 'Font collection failed');
return undefined;
}
}
getHashComponents(data) {
const components = [];
if (data.canvas?.hash)
components.push(data.canvas.hash);
if (data.webgl?.hash)
components.push(data.webgl.hash);
if (data.audio?.hash)
components.push(data.audio.hash);
if (data.screen && !this.config.excludeColorDepth && !this.config.excludePixelRatio) {
components.push(JSON.stringify(data.screen));
}
if (data.browser && !this.config.excludeUserAgent) {
components.push(data.browser.userAgent);
}
if (data.hardware)
components.push(JSON.stringify(data.hardware));
if (data.timezone && !this.config.excludeTimezone) {
components.push(JSON.stringify(data.timezone));
}
if (data.language && !this.config.excludeLanguage) {
components.push(JSON.stringify(data.language));
}
if (data.storage)
components.push(JSON.stringify(data.storage));
if (data.fonts)
components.push(JSON.stringify(data.fonts));
if (data.plugins)
components.push(JSON.stringify(data.plugins));
return components;
}
async generateDeviceId(components) {
const combined = components.join('|');
switch (this.config.hashAlgorithm) {
case 'sha256':
return await CryptoUtils.sha256(combined);
case 'sha1':
return await CryptoUtils.sha1(combined);
case 'md5':
return CryptoUtils.md5(combined);
case 'murmur3':
return CryptoUtils.murmur3(combined);
default:
return await CryptoUtils.sha256(combined);
}
}
calculateConfidence(data) {
let score = 0;
let maxScore = 0;
// Canvas fingerprint (high uniqueness)
maxScore += 30;
if (data.canvas?.supported && data.canvas.hash !== 'error')
score += 30;
// WebGL fingerprint (high uniqueness)
maxScore += 25;
if (data.webgl?.supported && data.webgl.hash !== 'error')
score += 25;
// Audio fingerprint (very high uniqueness)
maxScore += 20;
if (data.audio?.supported && data.audio.hash !== 'error')
score += 20;
// Screen info (medium uniqueness)
maxScore += 10;
if (data.screen)
score += 10;
// Browser info (low uniqueness but important)
maxScore += 10;
if (data.browser)
score += 10;
// Fonts (medium uniqueness)
maxScore += 5;
if (data.fonts && data.fonts.length > 0)
score += 5;
return Math.round((score / maxScore) * 100);
}
isDNTEnabled() {
if (typeof navigator === 'undefined')
return false;
return navigator.doNotTrack === '1' ||
navigator.doNotTrack === 'yes' ||
navigator.msDoNotTrack === '1';
}
isCacheValid() {
if (!this.cache || !this.cacheTimestamp)
return false;
return Date.now() - this.cacheTimestamp < this.config.cacheTimeout;
}
createMinimalFingerprint(reason) {
return {
deviceId: reason,
confidence: 0,
timestamp: Date.now(),
version: VERSION,
generatedAt: Date.now()
};
}
addError(component, error) {
this.errors.push({
component,
error,
timestamp: Date.now()
});
}
// Public utility methods
clearCache() {
this.cache = undefined;
this.cacheTimestamp = undefined;
}
getErrors() {
return [...this.errors];
}
getConfig() {
return { ...this.config };
}
}
exports.AudioCollector = AudioCollector;
exports.BrowserCollector = BrowserCollector;
exports.CanvasCollector = CanvasCollector;
exports.CryptoUtils = CryptoUtils;
exports.DeviceFingerprint = DeviceFingerprints;
exports.FontCollector = FontCollector;
exports.HardwareCollector = HardwareCollector;
exports.LanguageCollector = LanguageCollector;
exports.PluginCollector = PluginCollector;
exports.ScreenCollector = ScreenCollector;
exports.StorageCollector = StorageCollector;
exports.TimezoneCollector = TimezoneCollector;
exports.WebGLCollector = WebGLCollector;
exports.default = DeviceFingerprints;
//# sourceMappingURL=index.js.map