2025-05-26 17:45:32 +07:00
|
|
|
|
/**
|
|
|
|
|
* Утилита для создания уникального отпечатка устройства (Device Fingerprinting)
|
|
|
|
|
* Используется для блокировки на уровне железа
|
|
|
|
|
*/
|
|
|
|
|
|
2025-05-26 18:00:56 +07:00
|
|
|
|
// Проверка на наличие браузерного окружения
|
|
|
|
|
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
class DeviceFingerprint {
|
|
|
|
|
constructor() {
|
|
|
|
|
this.components = new Map();
|
|
|
|
|
this.fingerprint = null;
|
|
|
|
|
this.initialized = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Инициализация и создание отпечатка устройства
|
|
|
|
|
*/
|
|
|
|
|
async initialize() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
// Если мы не в браузере - возвращаем фиктивный отпечаток
|
|
|
|
|
if (!isBrowser) {
|
|
|
|
|
console.log('[DeviceFingerprint] Не браузерное окружение, отпечаток не создаётся');
|
|
|
|
|
this.fingerprint = 'server-environment-no-fingerprint';
|
|
|
|
|
this.initialized = true;
|
|
|
|
|
return this.fingerprint;
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
console.log('[DeviceFingerprint] Начинаем инициализацию отпечатка устройства...');
|
|
|
|
|
|
|
|
|
|
// Очищаем предыдущие компоненты
|
|
|
|
|
this.components.clear();
|
|
|
|
|
|
|
|
|
|
// Собираем компоненты отпечатка
|
|
|
|
|
await this.collectScreenInfo();
|
|
|
|
|
await this.collectNavigatorInfo();
|
|
|
|
|
await this.collectCanvasFingerprint();
|
|
|
|
|
await this.collectWebGLFingerprint();
|
|
|
|
|
await this.collectAudioFingerprint();
|
|
|
|
|
await this.collectTimezoneInfo();
|
|
|
|
|
await this.collectLanguageInfo();
|
|
|
|
|
await this.collectPlatformInfo();
|
|
|
|
|
await this.collectHardwareInfo();
|
|
|
|
|
await this.collectStorageInfo();
|
|
|
|
|
await this.collectNetworkInfo();
|
|
|
|
|
|
|
|
|
|
// Генерируем финальный отпечаток
|
|
|
|
|
this.fingerprint = await this.generateFingerprint();
|
|
|
|
|
this.initialized = true;
|
|
|
|
|
|
|
|
|
|
console.log('[DeviceFingerprint] Отпечаток устройства создан:', this.fingerprint.substring(0, 16) + '...');
|
|
|
|
|
return this.fingerprint;
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('[DeviceFingerprint] Ошибка при создании отпечатка:', error);
|
|
|
|
|
// Создаем базовый отпечаток даже при ошибках
|
|
|
|
|
this.fingerprint = await this.createFallbackFingerprint();
|
|
|
|
|
this.initialized = true;
|
|
|
|
|
return this.fingerprint;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Информация об экране
|
|
|
|
|
*/
|
|
|
|
|
async collectScreenInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
this.components.set('screen', {
|
|
|
|
|
width: screen.width,
|
|
|
|
|
height: screen.height,
|
|
|
|
|
availWidth: screen.availWidth,
|
|
|
|
|
availHeight: screen.availHeight,
|
|
|
|
|
colorDepth: screen.colorDepth,
|
|
|
|
|
pixelDepth: screen.pixelDepth,
|
|
|
|
|
devicePixelRatio: window.devicePixelRatio || 1
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить информацию об экране:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Информация о навигаторе
|
|
|
|
|
*/
|
|
|
|
|
async collectNavigatorInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
this.components.set('navigator', {
|
|
|
|
|
userAgent: navigator.userAgent,
|
|
|
|
|
language: navigator.language,
|
|
|
|
|
languages: navigator.languages ? Array.from(navigator.languages) : [],
|
|
|
|
|
platform: navigator.platform,
|
|
|
|
|
cookieEnabled: navigator.cookieEnabled,
|
|
|
|
|
doNotTrack: navigator.doNotTrack,
|
|
|
|
|
maxTouchPoints: navigator.maxTouchPoints || 0,
|
|
|
|
|
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
|
|
|
deviceMemory: navigator.deviceMemory || 0
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить информацию навигатора:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Canvas fingerprinting
|
|
|
|
|
*/
|
|
|
|
|
async collectCanvasFingerprint() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
// Рисуем уникальный паттерн
|
|
|
|
|
ctx.textBaseline = 'top';
|
|
|
|
|
ctx.font = '14px Arial';
|
|
|
|
|
ctx.fillStyle = '#f60';
|
|
|
|
|
ctx.fillRect(125, 1, 62, 20);
|
|
|
|
|
ctx.fillStyle = '#069';
|
|
|
|
|
ctx.fillText('Device fingerprint text 🔐', 2, 15);
|
|
|
|
|
ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
|
|
|
|
|
ctx.fillText('Device fingerprint text 🔐', 4, 17);
|
|
|
|
|
|
|
|
|
|
// Добавляем геометрические фигуры
|
|
|
|
|
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();
|
|
|
|
|
|
|
|
|
|
const canvasData = canvas.toDataURL();
|
2025-05-26 18:00:56 +07:00
|
|
|
|
this.components.set('canvas', await this.hashString(canvasData));
|
2025-05-26 17:45:32 +07:00
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WebGL fingerprinting
|
|
|
|
|
*/
|
|
|
|
|
async collectWebGLFingerprint() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
|
|
|
|
|
|
|
|
if (gl) {
|
|
|
|
|
const webglInfo = {
|
|
|
|
|
version: gl.getParameter(gl.VERSION),
|
|
|
|
|
vendor: gl.getParameter(gl.VENDOR),
|
|
|
|
|
renderer: gl.getParameter(gl.RENDERER),
|
|
|
|
|
shadingLanguageVersion: gl.getParameter(gl.SHADING_LANGUAGE_VERSION),
|
|
|
|
|
maxTextureSize: gl.getParameter(gl.MAX_TEXTURE_SIZE),
|
|
|
|
|
maxViewportDims: gl.getParameter(gl.MAX_VIEWPORT_DIMS)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
this.components.set('webgl', webglInfo);
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить WebGL информацию:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Audio fingerprinting
|
|
|
|
|
*/
|
|
|
|
|
async collectAudioFingerprint() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return Promise.resolve();
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
try {
|
|
|
|
|
if (!window.AudioContext && !window.webkitAudioContext) {
|
|
|
|
|
resolve();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
|
|
|
|
const context = new AudioContext();
|
|
|
|
|
|
|
|
|
|
const oscillator = context.createOscillator();
|
|
|
|
|
const analyser = context.createAnalyser();
|
|
|
|
|
const gainNode = context.createGain();
|
|
|
|
|
const scriptProcessor = context.createScriptProcessor(4096, 1, 1);
|
|
|
|
|
|
|
|
|
|
oscillator.type = 'triangle';
|
|
|
|
|
oscillator.frequency.value = 10000;
|
|
|
|
|
|
|
|
|
|
gainNode.gain.value = 0;
|
|
|
|
|
|
|
|
|
|
oscillator.connect(analyser);
|
|
|
|
|
analyser.connect(scriptProcessor);
|
|
|
|
|
scriptProcessor.connect(gainNode);
|
|
|
|
|
gainNode.connect(context.destination);
|
|
|
|
|
|
2025-05-26 18:00:56 +07:00
|
|
|
|
scriptProcessor.onaudioprocess = async (event) => {
|
2025-05-26 17:45:32 +07:00
|
|
|
|
const bins = new Float32Array(analyser.frequencyBinCount);
|
|
|
|
|
analyser.getFloatFrequencyData(bins);
|
|
|
|
|
|
2025-05-26 18:00:56 +07:00
|
|
|
|
const audioHash = await this.hashString(bins.slice(0, 50).join(','));
|
2025-05-26 17:45:32 +07:00
|
|
|
|
this.components.set('audio', audioHash);
|
|
|
|
|
|
|
|
|
|
oscillator.disconnect();
|
|
|
|
|
context.close();
|
|
|
|
|
resolve();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
oscillator.start();
|
|
|
|
|
|
|
|
|
|
// Таймаут на случай проблем
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try {
|
|
|
|
|
oscillator.disconnect();
|
|
|
|
|
context.close();
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
resolve();
|
|
|
|
|
}, 1000);
|
|
|
|
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось создать audio fingerprint:', e);
|
|
|
|
|
resolve();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Информация о временной зоне
|
|
|
|
|
*/
|
|
|
|
|
async collectTimezoneInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
this.components.set('timezone', {
|
|
|
|
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
|
|
|
timezoneOffset: new Date().getTimezoneOffset(),
|
|
|
|
|
locale: Intl.DateTimeFormat().resolvedOptions().locale
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить информацию о временной зоне:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Языковые настройки
|
|
|
|
|
*/
|
|
|
|
|
async collectLanguageInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
this.components.set('language', {
|
|
|
|
|
language: navigator.language,
|
|
|
|
|
languages: navigator.languages ? Array.from(navigator.languages) : []
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить языковую информацию:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Платформа и ОС
|
|
|
|
|
*/
|
|
|
|
|
async collectPlatformInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
this.components.set('platform', {
|
|
|
|
|
platform: navigator.platform,
|
|
|
|
|
oscpu: navigator.oscpu,
|
|
|
|
|
appVersion: navigator.appVersion
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить информацию о платформе:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Аппаратная информация
|
|
|
|
|
*/
|
|
|
|
|
async collectHardwareInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
this.components.set('hardware', {
|
|
|
|
|
hardwareConcurrency: navigator.hardwareConcurrency || 0,
|
|
|
|
|
deviceMemory: navigator.deviceMemory || 0,
|
|
|
|
|
maxTouchPoints: navigator.maxTouchPoints || 0
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить аппаратную информацию:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Информация о хранилище
|
|
|
|
|
*/
|
|
|
|
|
async collectStorageInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
let storageQuota = 0;
|
|
|
|
|
if ('storage' in navigator && 'estimate' in navigator.storage) {
|
|
|
|
|
const estimate = await navigator.storage.estimate();
|
|
|
|
|
storageQuota = estimate.quota || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.components.set('storage', {
|
|
|
|
|
localStorage: !!window.localStorage,
|
|
|
|
|
sessionStorage: !!window.sessionStorage,
|
|
|
|
|
indexedDB: !!window.indexedDB,
|
|
|
|
|
storageQuota
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить информацию о хранилище:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Сетевая информация
|
|
|
|
|
*/
|
|
|
|
|
async collectNetworkInfo() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return;
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
try {
|
|
|
|
|
const networkInfo = {};
|
|
|
|
|
|
|
|
|
|
if ('connection' in navigator) {
|
|
|
|
|
const conn = navigator.connection;
|
|
|
|
|
networkInfo.effectiveType = conn.effectiveType;
|
|
|
|
|
networkInfo.downlink = conn.downlink;
|
|
|
|
|
networkInfo.rtt = conn.rtt;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.components.set('network', networkInfo);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Не удалось получить сетевую информацию:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Генерация финального отпечатка
|
|
|
|
|
*/
|
|
|
|
|
async generateFingerprint() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return 'server-environment-no-fingerprint';
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
const componentsString = JSON.stringify(Array.from(this.components.entries()).sort());
|
|
|
|
|
return await this.hashString(componentsString);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Создание резервного отпечатка при ошибках
|
|
|
|
|
*/
|
|
|
|
|
async createFallbackFingerprint() {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return 'server-environment-no-fingerprint';
|
|
|
|
|
|
2025-05-26 17:45:32 +07:00
|
|
|
|
const fallbackData = {
|
|
|
|
|
userAgent: navigator.userAgent,
|
|
|
|
|
screen: `${screen.width}x${screen.height}`,
|
|
|
|
|
timezone: new Date().getTimezoneOffset(),
|
|
|
|
|
language: navigator.language,
|
|
|
|
|
timestamp: Date.now()
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return await this.hashString(JSON.stringify(fallbackData));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Хеширование строки
|
|
|
|
|
*/
|
|
|
|
|
async hashString(str) {
|
2025-05-26 18:00:56 +07:00
|
|
|
|
if (!isBrowser) return 'server-environment-hash';
|
|
|
|
|
|
|
|
|
|
if (window.crypto && window.crypto.subtle) {
|
|
|
|
|
try {
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
const data = encoder.encode(str);
|
|
|
|
|
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('');
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('[DeviceFingerprint] Ошибка при использовании Crypto API:', e);
|
|
|
|
|
return this.simpleHash(str);
|
|
|
|
|
}
|
2025-05-26 17:45:32 +07:00
|
|
|
|
} else {
|
|
|
|
|
// Fallback для старых браузеров
|
|
|
|
|
return this.simpleHash(str);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Простое хеширование для старых браузеров
|
|
|
|
|
*/
|
|
|
|
|
simpleHash(str) {
|
|
|
|
|
let hash = 0;
|
|
|
|
|
if (str.length === 0) return hash.toString();
|
|
|
|
|
for (let i = 0; i < str.length; i++) {
|
|
|
|
|
const char = str.charCodeAt(i);
|
|
|
|
|
hash = ((hash << 5) - hash) + char;
|
|
|
|
|
hash = hash & hash; // Конвертируем в 32-битное целое
|
|
|
|
|
}
|
|
|
|
|
return Math.abs(hash).toString(16);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Получение текущего отпечатка
|
|
|
|
|
*/
|
|
|
|
|
getFingerprint() {
|
|
|
|
|
return this.fingerprint;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Проверка инициализации
|
|
|
|
|
*/
|
|
|
|
|
isInitialized() {
|
|
|
|
|
return this.initialized;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Получение компонентов отпечатка (для отладки)
|
|
|
|
|
*/
|
|
|
|
|
getComponents() {
|
|
|
|
|
return Object.fromEntries(this.components);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Обновление отпечатка
|
|
|
|
|
*/
|
|
|
|
|
async refresh() {
|
|
|
|
|
return await this.initialize();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Экспортируем класс
|
|
|
|
|
export default DeviceFingerprint;
|