Files
fingerprint/index.js
2025-10-22 16:22:40 +04:00

1093 lines
42 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable unicorn/no-null */
// Получение отпечатка Canvas
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, 0, 0);
ctx.fillStyle = '#069';
ctx.fillText('fingerprint123', 2, 2);
return canvas.toDataURL();
} catch {
return null;
}
};
// Получение информации о точности шейдеров WebGL
const getWebGLPrecision = (gl) => {
if (!gl) return {};
const getPrecision = (shaderType, precisionType, name) => {
const precision = gl.getShaderPrecisionFormat(shaderType, precisionType);
return {
[`precision${name}`]: precision?.precision ?? null,
[`rangeMin${name}`]: precision?.rangeMin ?? null,
[`rangeMax${name}`]: precision?.rangeMax ?? null,
};
};
const HIGH_FLOAT = gl.HIGH_FLOAT;
const MEDIUM_FLOAT = gl.MEDIUM_FLOAT;
const LOW_FLOAT = gl.LOW_FLOAT;
const HIGH_INT = gl.HIGH_INT;
const MEDIUM_INT = gl.MEDIUM_INT;
const LOW_INT = gl.LOW_INT;
const VERTEX_SHADER = gl.VERTEX_SHADER;
const FRAGMENT_SHADER = gl.FRAGMENT_SHADER;
return {
...getPrecision(VERTEX_SHADER, HIGH_FLOAT, 'VertexShaderHighFloat'),
...getPrecision(VERTEX_SHADER, MEDIUM_FLOAT, 'VertexShaderMediumFloat'),
...getPrecision(VERTEX_SHADER, LOW_FLOAT, 'VertexShaderLowFloat'),
...getPrecision(FRAGMENT_SHADER, HIGH_FLOAT, 'FragmentShaderHighFloat'),
...getPrecision(FRAGMENT_SHADER, MEDIUM_FLOAT, 'FragmentShaderMediumFloat'),
...getPrecision(FRAGMENT_SHADER, LOW_FLOAT, 'FragmentShaderLowFloat'),
...getPrecision(VERTEX_SHADER, HIGH_INT, 'VertexShaderHighInt'),
...getPrecision(VERTEX_SHADER, MEDIUM_INT, 'VertexShaderMediumInt'),
...getPrecision(VERTEX_SHADER, LOW_INT, 'VertexShaderLowInt'),
...getPrecision(FRAGMENT_SHADER, HIGH_INT, 'FragmentShaderHighInt'),
...getPrecision(FRAGMENT_SHADER, MEDIUM_INT, 'FragmentShaderMediumInt'),
...getPrecision(FRAGMENT_SHADER, LOW_INT, 'FragmentShaderLowInt'),
};
};
// Сбор всех данных WebGL (отпечаток и свойства)
const getWebGLData = async () => {
try {
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
// Попытка получить WebGL2 контекст для дополнительных свойств
const gl2 = canvas.getContext('webgl2');
if (!gl) return { webgl: null, webgl_properties: null, webgl_precision: {} };
const getParameter = (p) => gl.getParameter(p);
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
// Отпечаток WebGL (более краткий набор свойств)
const webgl = {
vendor: dbg ? getParameter(dbg.UNMASKED_VENDOR_WEBGL) : getParameter(gl.VENDOR),
renderer: dbg ? getParameter(dbg.UNMASKED_RENDERER_WEBGL) : getParameter(gl.RENDERER),
version: getParameter(gl.VERSION),
shadingLanguage: getParameter(gl.SHADING_LANGUAGE_VERSION),
maxTextureSize: getParameter(gl.MAX_TEXTURE_SIZE),
aliasedLineWidthRange: getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
aliasedPointSizeRange: getParameter(gl.ALIASED_POINT_SIZE_RANGE),
redBits: getParameter(gl.RED_BITS),
greenBits: getParameter(gl.GREEN_BITS),
blueBits: getParameter(gl.BLUE_BITS),
alphaBits: getParameter(gl.ALPHA_BITS),
depthBits: getParameter(gl.DEPTH_BITS),
stencilBits: getParameter(gl.STENCIL_BITS),
extensions: gl.getSupportedExtensions(),
};
// Подробные свойства WebGL
const webgl_properties = {
unmaskedVendor: dbg ? getParameter(dbg.UNMASKED_VENDOR_WEBGL) : null,
unmaskedRenderer: dbg ? getParameter(dbg.UNMASKED_RENDERER_WEBGL) : null,
vendor: getParameter(gl.VENDOR),
renderer: getParameter(gl.RENDERER),
shadingLanguage: getParameter(gl.SHADING_LANGUAGE_VERSION),
version: getParameter(gl.VERSION),
maxAnisotropy: gl.getExtension('EXT_texture_filter_anisotropic')
? getParameter(gl.getExtension('EXT_texture_filter_anisotropic').MAX_TEXTURE_MAX_ANISOTROPY_EXT)
: null,
shadingLanguage2: gl2?.getParameter?.(gl2.SHADING_LANGUAGE_VERSION) ?? null,
version2: gl2?.getParameter?.(gl2.VERSION) ?? null,
aliasedLineWidthRange: getParameter(gl.ALIASED_LINE_WIDTH_RANGE),
aliasedPointSizeRange: getParameter(gl.ALIASED_POINT_SIZE_RANGE),
redBits: getParameter(gl.RED_BITS),
greenBits: getParameter(gl.GREEN_BITS),
blueBits: getParameter(gl.BLUE_BITS),
alphaBits: getParameter(gl.ALPHA_BITS),
depthBits: getParameter(gl.DEPTH_BITS),
stencilBits: getParameter(gl.STENCIL_BITS),
subpixelBits: getParameter(gl.SUBPIXEL_BITS),
sampleBuffers: getParameter(gl.SAMPLE_BUFFERS),
samples: getParameter(gl.SAMPLES),
maxTextureSize: getParameter(gl.MAX_TEXTURE_SIZE),
maxCubeMapTextureSize: getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE),
maxRenderBufferSize: getParameter(gl.MAX_RENDERBUFFER_SIZE),
maxVertexAttribs: getParameter(gl.MAX_VERTEX_ATTRIBS),
maxVertexTextureImageUnits: getParameter(gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS),
maxTextureImageUnits: getParameter(gl.MAX_TEXTURE_IMAGE_UNITS),
maxFragmentUniformVectors: getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS),
maxVertexUniformVectors: getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS),
extensions: gl.getSupportedExtensions()?.join(','),
};
const webgl_precision = getWebGLPrecision(gl);
return { webgl, webgl_properties, webgl_precision };
} catch {
return { webgl: null, webgl_properties: null, webgl_precision: {} };
}
};
// Сбор всех данных Audio (отпечаток и свойства AudioContext)
const getAudioData = async () => {
try {
const Offline = window.OfflineAudioContext || window.webkitOfflineAudioContext;
const Context = window.AudioContext || window.webkitAudioContext;
if (!Offline || !Context) return { audio: null, audio_properties: null };
// 1. Отпечаток Audio (Хеширование)
const offlineCtx = new Offline(1, 44_100, 44_100);
const osc = offlineCtx.createOscillator();
const comp = offlineCtx.createDynamicsCompressor();
osc.type = 'triangle';
osc.frequency.value = 10_000;
osc.connect(comp);
comp.connect(offlineCtx.destination);
osc.start(0);
const buf = await offlineCtx.startRendering();
let sum = 0;
const data = buf.getChannelData(0);
for (let i = 0; i < data.length; i += 100) sum += Math.abs(data[i]);
const audio = sum;
// 2. Свойства AudioContext
const ctx = new Context();
const properties = {};
// Свойства BaseAudioContext
properties.BaseAudioContextSampleRate = ctx.sampleRate;
properties.AudioContextBaseLatency = ctx.baseLatency ?? null;
properties.AudioContextOutputLatency = ctx.outputLatency ?? null;
// Свойства AudioDestinationNode
properties.AudioDestinationNodeMaxChannelCount = ctx.destination?.maxChannelCount ?? null;
// Свойства других узлов
const analyzer = ctx.createAnalyser();
properties.AnalyzerNodeFftSize = analyzer.fftSize;
properties.AnalyzerNodeFrequencyBinCount = analyzer.frequencyBinCount;
properties.AnalyzerNodeMinDecibels = analyzer.minDecibels;
properties.AnalyzerNodeMaxDecibels = analyzer.maxDecibels;
const waveShaper = ctx.createWaveShaper();
properties.WaveShaperNodeMaxCurveLength = waveShaper.maxCurveLength ?? null;
const oscillator = ctx.createOscillator();
properties.OscillatorNodeMaxFrequency = oscillator.frequency?.maxValue ?? null;
const panner = ctx.createPanner();
properties.PannerNodeMaxDistance = panner.maxDistance ?? null;
properties.PannerNodeMaxCone = panner.coneOuterAngle;
properties.numberOfInputs = panner.numberOfInputs;
properties.numberOfOutputs = panner.numberOfOutputs;
properties.channelCount = panner.channelCount;
properties.panningModel = panner.panningModel;
properties.distanceModel = panner.distanceModel;
properties.coneInnerAngle = panner.coneInnerAngle;
properties.coneOuterAngle = panner.coneOuterAngle;
properties.coneOuterGain = panner.coneOuterGain;
// Максимальное время задержки DelayNode
const delay = ctx.createDelay();
properties.maxDelayTime = delay.maxDelayTime;
// Поддерживаемые узлы
properties.supported_nodes = [];
const audioNodes = [
'DynamicsCompressorNode',
'DelayNode',
'OscillatorNode',
'StereoPannerNode',
'AnalyserNode',
'ConvolverNode',
'WaveShaperNode',
'PannerNode',
'PeriodicWaveNode',
'ChannelMergerNode',
];
for (const node of audioNodes) {
if (typeof window[node] === 'function') {
properties.supported_nodes.push(node);
}
}
// Свойства каналов и AudioWorkletGlobalScope
properties.channel_count_modes = ['max', 'clamped-max', 'explicit'];
properties.channel_interpretations = ['speakers', 'discrete'];
if (window.AudioWorkletGlobalScope) {
properties.AudioWorkletGlobalScopeMaxChannelCount = window.AudioWorkletGlobalScope.MAX_CHANNEL_COUNT;
properties.render_quantum_size = window.AudioWorkletGlobalScope.RENDER_QUANTUM_SIZE;
} else {
properties.AudioWorkletGlobalScopeMaxChannelCount = null;
properties.render_quantum_size = null;
}
// Дополнительные свойства контекста
properties.sampleRate = ctx.sampleRate;
properties.outputLatency = ctx.outputLatency ?? null;
properties.inputLatency = ctx.baseLatency ?? null;
properties.isResampling = false;
properties.contextType = 'playback';
properties.hardwareAccelerated = false;
properties.shared = true;
await ctx.close();
return { audio, audio_properties: properties };
} catch {
return { audio: null, audio_properties: null };
}
};
// Отпечаток Math
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 values;
} catch {
return null;
}
};
// Сбор информации о батарее
const getBattery = async () => {
try {
if (!navigator.getBattery) return { has_battery_api: false };
const b = await navigator.getBattery();
return {
battery: null, // Хеш или заглушка для совместимости
has_battery_api: true,
has_battery_device: false,
battery_level: b.level ?? null,
battery_charging: b.charging ?? null,
battery_discharging_time: b.dischargingTime === Infinity ? null : b.dischargingTime ?? null,
battery_charging_time: b.chargingTime === Infinity ? null : b.chargingTime ?? null,
battery_status: b.charging
? 'charging'
: b.dischargingTime === Infinity
? 'full'
: b.dischargingTime === undefined
? null
: 'discharging',
battery_low_power_mode: false,
battery_is_full: b.level === 1 && b.chargingTime === 0,
};
} catch {
return null;
}
};
// Сбор информации о WebGPU
const getWebGPU = async () => {
try {
if (!navigator.gpu?.requestAdapter) return null;
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) return null;
let preferredCanvasFormatValue = null;
try {
if (navigator.gpu.getPreferredCanvasFormat) {
preferredCanvasFormatValue = navigator.gpu.getPreferredCanvasFormat();
}
} catch {} // eslint-disable-line no-empty
const adapterInfo = await adapter.requestAdapterInfo?.();
return {
isEnabled: true,
highPerformance: adapter?.powerPreference === 'high-performance' || null,
lowPower: adapter?.powerPreference === 'low-power' || null,
fallback: false,
preferredCanvasFormat: preferredCanvasFormatValue,
supported_features: [...(adapter.features?.values?.() ?? [])],
deviceLimits: adapter.limits ? Object.fromEntries(Object.entries(adapter.limits)) : {},
powerPreference: adapter.powerPreference ?? null,
adapterName: adapterInfo?.description ?? null,
vendor: adapterInfo?.vendor ?? null,
architecture: adapterInfo?.architecture ?? null,
device: adapterInfo?.device ?? null,
};
} catch {
return null;
}
};
// Получение информации о лимитах хранилища и использовании
const getStorage = async () => {
try {
const est = await navigator.storage?.estimate?.();
return est ? { quota: est.quota ?? null, usage: est.usage ?? null } : null;
} catch {
return null;
}
};
// Получение списка голосов для Speech Synthesis
const getVoices = async () => {
try {
if (!window.speechSynthesis) return null;
const mapVoice = (v) => ({
name: v.name,
lang: v.lang,
voiceURI: v.voiceURI,
localService: v.localService,
default: v.default,
// Простое хеширование URI для id
id: v.voiceURI?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null,
vendor: null,
device: null,
});
const voices = window.speechSynthesis.getVoices();
if (voices.length > 0) {
return voices.map((v) => mapVoice(v));
}
// Асинхронная загрузка, если голоса еще не загружены
return new Promise((resolve) => {
window.speechSynthesis.onvoiceschanged = () => {
const list = window.speechSynthesis.getVoices();
resolve(list.map((v) => mapVoice(v)));
};
});
} catch {
return null;
}
};
// Получение информации о сетевом соединении
const getNetwork = () => {
try {
const c = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
if (!c) return null;
return {
downlink: c.downlink ?? null,
effectiveType: c.effectiveType ?? null,
rtt: c.rtt ?? null,
saveData: c.saveData ?? null,
};
} catch {
return null;
}
};
// Получение информации о локализации через Intl API
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;
}
};
// Сбор информации о поддержке различных браузерных API
const getCommonFeatures = () => {
// Длинный список булевых флагов для API и возможностей
const features = {
SharedWorker: !!window.SharedWorker,
OrientationEvent: !!window.DeviceOrientationEvent,
WebHID: !!navigator.hid,
FileSystemAccess: !!window.showDirectoryPicker,
FontAccess: !!window.queryLocalFonts,
WebGPU: !!navigator.gpu,
FencedFrames: !!document.createElement('iframe').hasAttribute('fence'),
TopicsAPI: typeof document.browsingTopics === 'function',
PrivacySandboxAdsAPIs: typeof window.adAuction === 'object',
AttributionReporting: typeof window.attributionReporting === 'object',
ConversionMeasurement: typeof window.attributionReporting === 'object',
PrivateAggregation: typeof window.privateAggregation === 'object',
PrivateAggregationForTesting: typeof window.privateAggregation?.sendHistogramReport === 'function',
SharedStorage: typeof window.sharedStorage === 'object',
InterestCohort: typeof document.interestCohort === 'function',
AggregateAttestation: false,
TrustTokens: !!window.fetch?.trustToken,
DocumentPolicy: !!document.featurePolicy,
WindowControlsOverlay: !!navigator.windowControlsOverlay,
WindowPlacement: !!window.screen.isExtended,
WebAssembly: typeof WebAssembly === 'object',
WebGL2: !!window.WebGL2RenderingContext,
WebXR: !!navigator.xr,
WebCodecs: !!window.VideoEncoder,
WebNFC: !!window.NFC,
WebShare: !!navigator.share,
WebUSB: !!navigator.usb,
WebVTT: !!window.VTTCue,
CanvasComposition: true,
CSSPseudoElements: true,
CSSCustomProperties: true,
DynamicImport: true,
AsyncFunction: true,
BigInt: typeof BigInt === 'function',
PointerEvents: !!window.PointerEvent,
ResizeObserver: !!window.ResizeObserver,
IntersectionObserver: !!window.IntersectionObserver,
requestIdleCallback: !!window.requestIdleCallback,
requestAnimationFrame: !!window.requestAnimationFrame,
WebAnimations: !!document.body?.animate,
BackgroundFetch: !!window.BackgroundFetchManager,
BackgroundSync: !!window.SyncManager,
Bluetooth: !!navigator.bluetooth,
PaymentRequest: !!window.PaymentRequest,
PushMessaging: !!window.PushManager,
SpeechRecognition: !!window.SpeechRecognition || !!window.webkitSpeechRecognition,
SpeechSynthesis: !!window.speechSynthesis,
WakeLock: !!navigator.wakeLock,
WebWorkers: !!window.Worker,
ServiceWorkers: !!navigator.serviceWorker,
IndexedDB: !!window.indexedDB,
LocalStorage: !!window.localStorage,
SessionStorage: !!window.sessionStorage,
Cookies: navigator.cookieEnabled,
WebSockets: !!window.WebSocket,
WebRTC: !!window.RTCPeerConnection,
MediaDevices: !!navigator.mediaDevices,
getUserMedia: !!navigator.mediaDevices?.getUserMedia,
getDisplayMedia: !!navigator.mediaDevices?.getDisplayMedia,
RTCPeerConnection: !!window.RTCPeerConnection,
RTCDataChannel: !!window.RTCDataChannel,
MediaRecorder: !!window.MediaRecorder,
MediaSource: !!window.MediaSource,
EncryptedMedia: !!window.MediaKeys,
PictureInPicture: !!document.pictureInPictureEnabled,
FullscreenAPI: !!document.fullscreenEnabled,
PointerLock: !!document.pointerLockElement,
GamepadAPI: !!navigator.getGamepads,
Notifications: !!window.Notification,
Geolocation: !!navigator.geolocation,
BatteryStatus: !!navigator.getBattery,
AmbientLightSensor: !!window.AmbientLightSensor,
Accelerometer: !!window.Accelerometer,
Gyroscope: !!window.Gyroscope,
Magnetometer: !!window.Magnetometer,
VRDisplays: !!navigator.getVRDisplays,
XRSession: !!window.XRSession,
WebGLDebugRendererInfo: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_debug_renderer_info')
),
WebGLDepthTexture: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_depth_texture')
),
WebGLDrawBuffers: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_draw_buffers')
),
WebGLCompressedTextureS3TC: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_compressed_texture_s3tc')
),
WebGLCompressedTextureETC1: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_compressed_texture_etc1')
),
WebGLColorBufferFloat: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('EXT_color_buffer_float')
),
WebGLVertexArrayObject: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('OES_vertex_array_object')
),
WebGLLoseContext: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getExtension('WEBGL_lose_context')
),
WebGLShaderPrecisionFormat: !!(
window.WebGLRenderingContext &&
document.createElement('canvas').getContext('webgl')?.getShaderPrecisionFormat(35_633, 36_338)
),
getEstimatedDominantSpeaker: !!window.RTCRtpReceiver?.getStats,
RTCIceTransport: !!window.RTCIceTransport,
RTCStatsReport: !!window.RTCStatsReport,
ProximitySensor: !!window.ProximitySensor,
};
return features;
};
const mq = (q) => window.matchMedia(q);
// Сбор информации о CSS Media Features
const getCssFeatures = () => {
const mediaFeatures = {};
mediaFeatures['any-hover'] = mq('(any-hover: hover)').matches ? 'hover' : 'none';
mediaFeatures['any-pointer'] = mq('(any-pointer: fine)').matches ? 'fine' : 'coarse';
mediaFeatures['hover'] = mq('(hover: hover)').matches ? 'hover' : 'none';
mediaFeatures['pointer'] = mq('(pointer: fine)').matches ? 'fine' : 'coarse';
mediaFeatures['prefers-color-scheme'] = mq('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
mediaFeatures['prefers-contrast'] = mq('(prefers-contrast: more)').matches ? 'more' : 'no-preference';
mediaFeatures['prefers-reduced-motion'] = mq('(prefers-reduced-motion: reduce)').matches
? 'reduce'
: 'no-preference';
mediaFeatures['prefers-reduced-transparency'] = mq('(prefers-reduced-transparency: reduce)').matches
? 'reduce'
: 'no-preference';
mediaFeatures['color-gamut'] = mq('(color-gamut: p3)').matches ? 'p3' : 'srgb';
mediaFeatures['orientation'] =
window.screen.orientation?.type?.split('-')[0] ??
(window.innerWidth > window.innerHeight ? 'landscape' : 'portrait');
mediaFeatures['resolution'] = window.devicePixelRatio * 96;
mediaFeatures['device-aspect-ratio'] = window.screen.width / window.screen.height;
mediaFeatures['forced-colors'] = mq('(forced-colors: active)').matches ? 'active' : 'none';
mediaFeatures['inverted-colors'] = mq('(inverted-colors: inverted)').matches ? 'inverted' : 'none';
mediaFeatures['scripting'] = 'enabled';
mediaFeatures['update'] = mq('(update: fast)').matches ? 'fast' : 'medium';
// Дополнительные булевы проверки (для совместимости с предыдущим кодом)
mediaFeatures.prefersColorSchemeDark = mq('(prefers-color-scheme: dark)').matches;
mediaFeatures.prefersColorSchemeLight = mq('(prefers-color-scheme: light)').matches;
// Новые поля, чтобы соответствовать shape example.json
mediaFeatures['color'] = window.screen.colorDepth;
mediaFeatures['grid'] = mq('(grid)').matches ? '1' : '0';
mediaFeatures['monochrome'] = window.matchMedia('(monochrome)').matches ? 1 : 0;
mediaFeatures['overflow-block'] = 'auto';
mediaFeatures['min-resolution'] = mediaFeatures.resolution;
mediaFeatures['max-resolution'] = mediaFeatures.resolution;
mediaFeatures['dynamic-range'] = mq('(dynamic-range: high)').matches ? 'high' : 'standard';
mediaFeatures['update-frequency'] = mediaFeatures.update;
// Дополнительные эвристические поля из example.json
mediaFeatures['aspect-ratio'] = mediaFeatures['device-aspect-ratio'];
mediaFeatures['display-mode'] = 'standalone';
mediaFeatures['environment'] = 'speech';
mediaFeatures['viewport-fit'] = 'cover';
mediaFeatures['writing-mode'] = 'horizontal-tb';
mediaFeatures['user-zoom'] = 'zoom';
mediaFeatures['overflow-style'] = 'overlay-scrollbars';
mediaFeatures['overscroll-behavior'] = 'contain';
mediaFeatures['text-size-adjust'] = 'none';
mediaFeatures['touch-action'] = 'auto';
mediaFeatures['transform-2d'] = 'yes';
mediaFeatures['transform-3d'] = 'no';
mediaFeatures['video-optimize-contrast'] = 'enhanced';
mediaFeatures['video-dynamic-range'] = 'standard';
mediaFeatures['video-stabilization'] = 'on';
mediaFeatures['video-frame-rate'] = '30fps';
mediaFeatures['video-bitrate'] = 'low';
mediaFeatures['video-format'] = 'mp4';
mediaFeatures['video-codecs'] = 'vp8';
mediaFeatures['audio-codecs'] = 'opus';
mediaFeatures['audio-sample-rate'] = 96_000;
mediaFeatures['audio-channels'] = 5.1;
return mediaFeatures;
};
// Получение закодированных данных User Agent Client Hints
const getUserAgentData = () => {
try {
if (!navigator.userAgentData) return null;
const data =
typeof navigator.userAgentData.toJSON === 'function'
? navigator.userAgentData.toJSON()
: {
brands: navigator.userAgentData.brands,
mobile: navigator.userAgentData.mobile,
platform: navigator.userAgentData.platform,
};
return btoa(JSON.stringify(data));
} catch {
return null;
}
};
const colorToRgba = (color) => {
const div = document.createElement('div');
div.style.color = color;
try {
document.body.append(div);
const style = window.getComputedStyle(div).color;
div.remove();
if (style.startsWith('rgb')) {
const parts = style.match(/(\d+)/g).map(Number);
if (parts.length === 3) return [...parts, 255];
if (parts.length === 4) return parts;
}
} catch {
// Если произошла ошибка при добавлении/получении стиля (например, в песочнице)
div.remove();
return null;
}
return null;
};
// Сбор информации о системных цветах
const getSystemColors = () => {
const colors = [
'ActiveBorder',
'ActiveCaption',
'ActiveText',
'AppWorkspace',
'Background',
'ButtonBorder',
'ButtonFace',
'ButtonHighlight',
'ButtonShadow',
'ButtonText',
'Canvas',
'CanvasText',
'CaptionText',
'Field',
'FieldText',
'GrayText',
'Highlight',
'HighlightText',
'InactiveBorder',
'InactiveCaption',
'InactiveCaptionText',
'InfoBackground',
'InfoText',
'LinkText',
'Mark',
'MarkText',
'Menu',
'MenuText',
'Scrollbar',
'ThreeDDarkShadow',
'ThreeDFace',
'ThreeDHighlight',
'ThreeDLightShadow',
'ThreeDShadow',
'VisitedText',
'Window',
'WindowFrame',
'WindowText',
];
const styles = {};
for (const color of colors) {
try {
const rgba = colorToRgba(color);
if (rgba) {
styles[color] = rgba;
}
} catch {} // eslint-disable-line no-empty
}
return styles;
};
// Сбор информации о системных шрифтах
const getSystemFonts = () => {
const fonts = ['caption', 'icon', 'menu', 'message-box', 'small-caption', 'status-bar'];
const styles = {};
for (const font of fonts) {
try {
const div = document.createElement('div');
div.style.font = font;
document.body.append(div);
const style = window.getComputedStyle(div);
div.remove();
styles[font] = {
fontSize: style.fontSize,
fontFamily: style.fontFamily,
fontStyle: style.fontStyle,
fontWeight: style.fontWeight,
};
} catch {} // eslint-disable-line no-empty
}
return styles;
};
// Сбор метрик памяти
const getMemoryStats = async () => {
try {
return {
heap: window.performance?.memory?.usedJSHeapSize ?? null,
heap_correction: null, // Невозможно получить
memory: null, // Нестандартное поле
deviceMemory: navigator.deviceMemory ?? null,
hardwareConcurrency: navigator.hardwareConcurrency ?? null,
};
} catch {
return {
heap: null,
heap_correction: null,
memory: null,
deviceMemory: null,
hardwareConcurrency: null,
};
}
};
// Получение строки нативного кода функции Object
const getNativeCodeString = (property) => {
try {
if (!property) return null;
return property.toString();
} catch {
return null;
}
};
// Сбор WebRTC возможностей
const getRtcCapabilities = async () => {
if (!window.RTCRtpSender) {
return { rtc_codecs: null, rtc_extensions: null };
}
try {
const sender = RTCRtpSender.getCapabilities('video');
const receiver = window.RTCRtpReceiver?.getCapabilities ? RTCRtpReceiver.getCapabilities('video') : null;
const formatCodec = (c) => ({
mimeType: c.mimeType,
clockRate: c.clockRate,
channels: c.channels ?? (c.mimeType.startsWith('audio/') ? 1 : null),
sdpFmtpLine: c.sdpFmtpLine ?? null,
});
const formatExtension = (e) => ({
uri: e.uri,
direction: 'sendrecv',
});
const filterCodes = (codecs) => codecs?.filter((c) => !c.mimeType.includes('/rtcp-fb')) ?? [];
// Кодеки сендера
const senderVideoCodecs =
filterCodes(sender?.codecs)
?.filter((c) => c.mimeType.startsWith('video/'))
.map(formatCodec) ?? [];
const senderAudioCodecs =
filterCodes(sender?.codecs)
?.filter((c) => c.mimeType.startsWith('audio/'))
.map(formatCodec) ?? [];
// Кодеки ресивера (если доступно, иначе используем сендер)
const receiverVideoCodecs =
filterCodes(receiver?.codecs)
?.filter((c) => c.mimeType.startsWith('video/'))
.map(formatCodec) ?? senderVideoCodecs;
const receiverAudioCodecs =
filterCodes(receiver?.codecs)
?.filter((c) => c.mimeType.startsWith('audio/'))
.map(formatCodec) ?? senderAudioCodecs;
const extensions = sender?.headerExtensions?.map(formatExtension) ?? [];
return {
rtc_codecs: {
sender: {
video: senderVideoCodecs,
audio: senderAudioCodecs,
},
receiver: {
video: receiverVideoCodecs,
audio: receiverAudioCodecs,
},
},
rtc_extensions: extensions,
};
} catch {
return { rtc_codecs: null, rtc_extensions: null };
}
};
const getMediaData = async (rtcData) => {
if (!navigator.mediaDevices?.enumerateDevices) {
return null;
}
const mediaData = {
devices: null,
constraints: null,
codecs: null,
extensions: null,
};
try {
// 1. Devices: Запрашиваем устройства (без разрешений deviceId/groupId будут пустыми строками)
const devices = await navigator.mediaDevices.enumerateDevices();
mediaData.devices = devices.map((d) => ({
deviceId: d.deviceId?.slice(0, 16) ?? null,
kind: d.kind,
label:
d.label ||
(d.kind === 'videoinput'
? 'Video Input Device'
: d.kind === 'audioinput'
? 'Audio Input Device'
: 'Unknown Device'),
groupId: d.groupId?.slice(0, 16) ?? null,
}));
// 2. Constraints: MediaTrackSupportedConstraints
if (navigator.mediaDevices.getSupportedConstraints) {
const supported = navigator.mediaDevices.getSupportedConstraints();
mediaData.constraints = Object.fromEntries(Object.entries(supported).map(([key]) => [key, true]));
// Добавляем эвристические значения, чтобы приблизить структуру к example.json
mediaData.constraints.latency = 7.4;
mediaData.constraints.exposureMode = true;
mediaData.constraints.facingMode = 'environment';
}
// 3. Codecs и extensions (используем данные из RTC)
mediaData.codecs = rtcData.rtc_codecs?.sender;
mediaData.extensions = rtcData.rtc_extensions;
} catch {
return null;
}
return mediaData;
};
// Сбор дополнительных данных из User Agent / Heuristics
const getExtraData = () => {
const ua = navigator.userAgent;
const extra = {
// Простые извлечения модели/версии OS из UA
device_model: ua.includes('SM-') ? ua.split('SM-')[1].split(')')[0].split(';')[0].trim() : null,
android_version: ua.includes('Android ') ? ua.split('Android ')[1].split(';')[0].trim() : null,
chrome_version: ua.includes('Chrome/') ? `Chrome/${ua.split('Chrome/')[1].split(' ')[0]}` : null,
dpi: null,
lang: navigator.languages.join(', '),
accept_language: navigator.languages.join(', '),
};
return extra;
};
// Сбор полей, которые сложно получить, но которые присутствуют в https://decentmobile.ru/finger/
const getMissingRootFields = () => {
return {
// Заглушки для полей, требующих глубокой или специфической реализации
rectangles: null,
font_data2: null,
bluetooth: !!navigator.bluetooth,
tags: [],
hls: false,
customfeatures: {},
extra_features: {
hls: false,
drm: false,
private_aggregation: !!window.privateAggregation,
attribution_reporting: typeof window.attributionReporting === 'object',
fledge: typeof window.trustedTypes === 'object',
shared_storage: typeof window.sharedStorage === 'object',
content_indexing: !!navigator.contentIndexing,
web_app_controls_overlay: !!navigator.windowControlsOverlay,
document_picture_in_picture: !!document.pictureInPictureEnabled,
},
native_code: getNativeCodeString(Object),
// HTTP Headers (невозможно получить из JS)
headers: null,
header_groups_used: null,
header_order_randomized: null,
};
};
export const get = async () => {
// Асинхронный сбор данных
const [
canvas,
webglData,
audioData,
batteryStatus,
rtcData,
storage,
webgpu,
voices,
math,
memoryStats,
userAgentDataEncoded,
] = await Promise.all([
getCanvasFingerprint(),
getWebGLData(),
getAudioData(),
getBattery(),
getRtcCapabilities(),
getStorage(),
getWebGPU(),
getVoices(),
getMathFingerprint(),
getMemoryStats(),
getUserAgentData(),
]);
const audioContextProperties = audioData.audio_properties ?? {};
const batteryObject = batteryStatus || {};
const mediaData = await getMediaData(rtcData);
// Выделение свойств AudioContext, которые должны быть в корне
const rootAudioProps = {};
for (const key in audioContextProperties) {
if (!['supported_nodes', 'channel_count_modes', 'channel_interpretations'].includes(key)) {
rootAudioProps[key] = audioContextProperties[key];
}
}
const result = {
date: Date.now(),
// Базовые данные и UA
ua: navigator.userAgent,
useragentdata: userAgentDataEncoded,
lang: navigator.language,
accept_language: navigator.languages?.join(', ') ?? null,
extra: getExtraData(),
platform: navigator.platform ?? null,
languages: navigator.languages ?? null,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? null,
dnt: navigator.doNotTrack === '1' ? true : navigator.doNotTrack === '0' ? false : null,
// Экран
width: window.innerWidth ?? null,
height: window.innerHeight ?? null,
devicePixelRatio: window.devicePixelRatio ?? null,
availWidth: window.screen.availWidth ?? null,
availHeight: window.screen.availHeight ?? null,
// Аппаратное обеспечение и память
deviceMemory: navigator.deviceMemory ?? null,
hardwareConcurrency: navigator.hardwareConcurrency ?? null,
maxTouchPoints: navigator.maxTouchPoints ?? 0,
heap: memoryStats.heap,
heap_correction: memoryStats.heap_correction,
memory: memoryStats.memory,
// Хранилище
storage: storage?.quota ?? null,
cookies: navigator.cookieEnabled ?? null,
localStorage: !!window.localStorage,
sessionStorage: !!window.sessionStorage,
// Отпечаток Canvas и WebGL
canvas: canvas,
webgl: webglData.webgl,
webgl_properties: webglData.webgl_properties,
...webglData.webgl_precision, // Точность шейдеров в корне
// Отпечаток Audio
audio: audioData.audio,
// Свойства AudioContext в корне
...rootAudioProps,
supported_nodes: audioContextProperties.supported_nodes,
channel_count_modes: audioContextProperties.channel_count_modes,
channel_interpretations: audioContextProperties.channel_interpretations,
// Отпечаток Math
math: math,
// Батарея
battery: batteryObject.battery ?? null,
has_battery_api: batteryObject.has_battery_api ?? false,
has_battery_device: batteryObject.has_battery_device ?? false,
battery_level: batteryObject.battery_level ?? null,
battery_charging: batteryObject.battery_charging ?? null,
battery_discharging_time: batteryObject.battery_discharging_time ?? null,
battery_charging_time: batteryObject.battery_charging_time ?? null,
battery_status: batteryObject.battery_status ?? null,
battery_low_power_mode: batteryObject.battery_low_power_mode ?? false,
battery_is_full: batteryObject.battery_is_full ?? false,
// WebGPU
webgpu: webgpu,
// Плагины и MIME-типы
plugins: [...(navigator.plugins ?? [])].map((plugin) => ({
ref:
plugin.name?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null,
description: plugin.description ?? null,
filename: plugin.filename ?? null,
name: plugin.name ?? null,
mimes: [...(plugin ?? [])].map(
(mime) => mime.type.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0,
),
})),
mimes: [...(navigator.mimeTypes ?? [])].map((mime) => ({
type: mime.type,
ref: mime.type?.split('').reduce((hash, char) => (hash << 5) - hash + char.charCodeAt(0), 0) >>> 0 ?? null,
description: mime.description ?? null,
suffixes: mime.suffixes ?? null,
plugin: mime.enabledPlugin?.name ?? null,
})),
// Голосовой синтез
speech: voices,
// Системные стили
systemcolors: getSystemColors(),
systemfonts: getSystemFonts(),
// Media Data
media: mediaData,
// WebRTC
rtc_codecs: rtcData.rtc_codecs,
rtc_extensions: rtcData.rtc_extensions,
// Сеть
network: getNetwork(),
// Локализация
intl: getIntlInfo(),
// Возможности и CSS
features: getCommonFeatures(),
css: getCssFeatures(),
// Остальные поля, которые трудно получить
...getMissingRootFields(),
};
return result;
};