/* eslint-disable unicorn/no-null */ const sha256 = async (str) => { const buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)); return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join(''); }; const getCanvasFingerprint = async () => { try { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); if (!ctx) return null; canvas.width = 220; canvas.height = 30; ctx.textBaseline = 'top'; ctx.font = '14px Arial'; ctx.fillStyle = '#f60'; ctx.fillRect(0, 0, 50, 30); ctx.fillStyle = '#069'; ctx.fillText('fingerprint123', 2, 2); return await sha256(canvas.toDataURL()); } catch { return null; } }; const getWebGLFingerprint = async () => { try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl'); if (!gl) return null; const dbg = gl.getExtension('WEBGL_debug_renderer_info'); const props = { vendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR), renderer: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER), version: gl.getParameter(gl.VERSION), shadingLanguage: gl.getParameter(gl.SHADING_LANGUAGE_VERSION), maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE), aliasedLineWidthRange: gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE), aliasedPointSizeRange: gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE), redBits: gl.getParameter(gl.RED_BITS), greenBits: gl.getParameter(gl.GREEN_BITS), blueBits: gl.getParameter(gl.BLUE_BITS), alphaBits: gl.getParameter(gl.ALPHA_BITS), depthBits: gl.getParameter(gl.DEPTH_BITS), stencilBits: gl.getParameter(gl.STENCIL_BITS), extensions: gl.getSupportedExtensions(), }; return { hash: await sha256(JSON.stringify(props)), props }; } catch { return null; } }; const getAudioFingerprint = async () => { try { const Offline = window.OfflineAudioContext || window.webkitOfflineAudioContext; if (!Offline) return null; const ctx = new Offline(1, 44_100, 44_100); const osc = ctx.createOscillator(); const comp = ctx.createDynamicsCompressor(); osc.type = 'triangle'; osc.frequency.value = 10_000; osc.connect(comp); comp.connect(ctx.destination); osc.start(0); const buf = await ctx.startRendering(); let sum = 0; const data = buf.getChannelData(0); for (let i = 0; i < data.length; i += 100) sum += Math.abs(data[i]); return await sha256(String(sum)); } catch { return null; } }; const getMathFingerprint = async () => { try { const values = { acos: Math.acos(0.123_456_789), asin: Math.asin(0.123_456_789), atan: Math.atan(0.123_456_789), sin: Math.sin(1e37), cos: Math.cos(1e-37), tan: Math.tan(10), exp: Math.exp(1), log: Math.log(10), sqrt: Math.sqrt(2), pow: Math.pow(123_456.789, -0.123), round: (0.1 + 0.2).toFixed(20), toExp: (123_456_789).toExponential(20), oneDivNegZero: 1 / -0, }; return await sha256(JSON.stringify(values)); } catch { return null; } }; const getBattery = async () => { try { if (!navigator.getBattery) return null; const b = await navigator.getBattery(); return { charging: b.charging, level: b.level, chargingTime: b.chargingTime, dischargingTime: b.dischargingTime, }; } catch { return null; } }; const getWebGPU = async () => { try { if (!navigator.gpu?.requestAdapter) return null; const adapter = await navigator.gpu.requestAdapter(); if (!adapter) return null; return { name: adapter.name, features: [...(adapter.features?.values?.() ?? [])], limits: adapter.limits ? Object.fromEntries(Object.entries(adapter.limits)) : {}, }; } catch { return null; } }; const getStorage = async () => { try { const est = await navigator.storage.estimate(); return { quota: est.quota, usage: est.usage }; } catch { return null; } }; const getVoices = () => { try { if (!window.speechSynthesis) return null; return window.speechSynthesis.getVoices().map((v) => ({ name: v.name, lang: v.lang, local: v.localService, default: v.default, })); } catch { return null; } }; const getNetwork = () => { try { const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!c) return null; return { downlink: c.downlink, effectiveType: c.effectiveType, rtt: c.rtt, saveData: c.saveData, }; } catch { return null; } }; const getIntlInfo = () => { try { const locales = navigator.languages ?? [navigator.language]; const dateSample = new Date(2020, 11, 31, 19, 5, 7); const numSample = 1_234_567.89; return { locales, date: new Intl.DateTimeFormat(locales).format(dateSample), time: new Intl.DateTimeFormat(locales, { hour: 'numeric', minute: 'numeric' }).format(dateSample), num: new Intl.NumberFormat(locales).format(numSample), currency: new Intl.NumberFormat(locales, { style: 'currency', currency: 'USD' }).format(numSample), resolved: { date: new Intl.DateTimeFormat(locales).resolvedOptions(), num: new Intl.NumberFormat(locales).resolvedOptions(), }, }; } catch { return null; } }; const getFeatures = () => ({ webgl: !!window.WebGLRenderingContext, workers: !!window.Worker, wasm: typeof WebAssembly === 'object', hid: !!navigator.hid, bluetooth: !!navigator.bluetooth, gpu: !!navigator.gpu, share: !!navigator.share, clipboard: !!navigator.clipboard, idle: !!navigator.requestIdleCallback, }); const mq = (q) => window.matchMedia(q).matches; const getCSSFeatures = () => ({ prefersColorSchemeDark: mq('(prefers-color-scheme: dark)'), prefersColorSchemeLight: mq('(prefers-color-scheme: light)'), prefersReducedMotion: mq('(prefers-reduced-motion: reduce)'), prefersReducedTransparency: mq('(prefers-reduced-transparency: reduce)'), prefersReducedData: mq('(prefers-reduced-data: reduce)'), prefersContrastMore: mq('(prefers-contrast: more)'), prefersContrastLess: mq('(prefers-contrast: less)'), forcedColorsActive: mq('(forced-colors: active)'), invertedColorsInverted: mq('(inverted-colors: inverted)'), hoverHover: mq('(hover: hover)'), hoverNone: mq('(hover: none)'), anyHoverHover: mq('(any-hover: hover)'), anyHoverNone: mq('(any-hover: none)'), pointerFine: mq('(pointer: fine)'), pointerCoarse: mq('(pointer: coarse)'), pointerNone: mq('(pointer: none)'), anyPointerFine: mq('(any-pointer: fine)'), anyPointerCoarse: mq('(any-pointer: coarse)'), anyPointerNone: mq('(any-pointer: none)'), lightLevelDim: mq('(light-level: dim)'), lightLevelNormal: mq('(light-level: normal)'), lightLevelWashed: mq('(light-level: washed)'), }); export const get = async () => { return { date: Date.now(), // Базовые данные ua: navigator.userAgent, platform: navigator.platform ?? null, lang: navigator.language, languages: navigator.languages, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, dnt: navigator.doNotTrack === '1', // Hardware width: window.innerWidth, height: window.innerHeight, devicePixelRatio: window.devicePixelRatio, hardwareConcurrency: navigator.hardwareConcurrency, deviceMemory: navigator.deviceMemory ?? null, maxTouchPoints: navigator.maxTouchPoints ?? 0, // Storage / Cookies cookies: navigator.cookieEnabled, localStorage: !!window.localStorage, sessionStorage: !!window.sessionStorage, storage: await getStorage(), // Fingerprints canvas: await getCanvasFingerprint(), webgl: await getWebGLFingerprint(), audio: await getAudioFingerprint(), math: await getMathFingerprint(), // Power battery: await getBattery(), // Graphics webgpu: await getWebGPU(), // Lists plugins: [...(navigator.plugins ?? [])].map(({ name, filename, description }) => ({ name, filename, description, })), mimes: [...(navigator.mimeTypes ?? [])].map(({ type, suffixes }) => ({ type, suffixes })), // Voices voices: getVoices(), // Network network: getNetwork(), // Intl intl: getIntlInfo(), // Возможности features: getFeatures(), css: getCSSFeatures(), }; };