435 lines
14 KiB
JavaScript
435 lines
14 KiB
JavaScript
/**
|
||
* Утилита для создания уникального отпечатка устройства (Device Fingerprinting)
|
||
* Используется для блокировки на уровне железа
|
||
*/
|
||
|
||
// Проверка на наличие браузерного окружения
|
||
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
|
||
|
||
class DeviceFingerprint {
|
||
constructor() {
|
||
this.components = new Map();
|
||
this.fingerprint = null;
|
||
this.initialized = false;
|
||
}
|
||
|
||
/**
|
||
* Инициализация и создание отпечатка устройства
|
||
*/
|
||
async initialize() {
|
||
// Если мы не в браузере - возвращаем фиктивный отпечаток
|
||
if (!isBrowser) {
|
||
console.log('[DeviceFingerprint] Не браузерное окружение, отпечаток не создаётся');
|
||
this.fingerprint = 'server-environment-no-fingerprint';
|
||
this.initialized = true;
|
||
return this.fingerprint;
|
||
}
|
||
|
||
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() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return;
|
||
|
||
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();
|
||
this.components.set('canvas', await this.hashString(canvasData));
|
||
|
||
} catch (e) {
|
||
console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* WebGL fingerprinting
|
||
*/
|
||
async collectWebGLFingerprint() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return Promise.resolve();
|
||
|
||
return new Promise((resolve) => {
|
||
try {
|
||
// Если AudioContext недоступен, просто пропускаем этот шаг
|
||
if (!window.AudioContext && !window.webkitAudioContext) {
|
||
console.warn('[DeviceFingerprint] AudioContext не поддерживается');
|
||
this.components.set('audio', 'not-available');
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
// Создаём аудио хэш на основе доступных properties AudioContext
|
||
// без использования ScriptProcessorNode
|
||
const createAudioFingerprintFallback = () => {
|
||
try {
|
||
const AudioContext = window.AudioContext || window.webkitAudioContext;
|
||
const context = new AudioContext();
|
||
|
||
const audioProps = {
|
||
sampleRate: context.sampleRate,
|
||
state: context.state,
|
||
baseLatency: context.baseLatency || 0,
|
||
outputLatency: context.outputLatency || 0
|
||
};
|
||
|
||
// Преобразуем эти свойства в хэш
|
||
const audioHash = this.simpleHash(JSON.stringify(audioProps));
|
||
this.components.set('audio', audioHash);
|
||
|
||
// Закрываем context
|
||
if (context.state !== 'closed') {
|
||
context.close();
|
||
}
|
||
|
||
resolve();
|
||
} catch (e) {
|
||
console.warn('[DeviceFingerprint] Ошибка в audio fingerprint fallback:', e);
|
||
this.components.set('audio', 'error');
|
||
resolve();
|
||
}
|
||
};
|
||
|
||
// Используем безопасный fallback вместо устаревшего ScriptProcessor
|
||
// и AudioWorklet (который требует отдельного файла и https)
|
||
createAudioFingerprintFallback();
|
||
} catch (e) {
|
||
console.warn('[DeviceFingerprint] Не удалось создать audio fingerprint:', e);
|
||
this.components.set('audio', 'error');
|
||
resolve();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Информация о временной зоне
|
||
*/
|
||
async collectTimezoneInfo() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return;
|
||
|
||
try {
|
||
this.components.set('language', {
|
||
language: navigator.language,
|
||
languages: navigator.languages ? Array.from(navigator.languages) : []
|
||
});
|
||
} catch (e) {
|
||
console.warn('[DeviceFingerprint] Не удалось получить языковую информацию:', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Платформа и ОС
|
||
*/
|
||
async collectPlatformInfo() {
|
||
if (!isBrowser) return;
|
||
|
||
try {
|
||
this.components.set('platform', {
|
||
platform: navigator.platform,
|
||
oscpu: navigator.oscpu,
|
||
appVersion: navigator.appVersion
|
||
});
|
||
} catch (e) {
|
||
console.warn('[DeviceFingerprint] Не удалось получить информацию о платформе:', e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Аппаратная информация
|
||
*/
|
||
async collectHardwareInfo() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return;
|
||
|
||
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() {
|
||
if (!isBrowser) return 'server-environment-no-fingerprint';
|
||
|
||
const componentsString = JSON.stringify(Array.from(this.components.entries()).sort());
|
||
return await this.hashString(componentsString);
|
||
}
|
||
|
||
/**
|
||
* Создание резервного отпечатка при ошибках
|
||
*/
|
||
async createFallbackFingerprint() {
|
||
if (!isBrowser) return 'server-environment-no-fingerprint';
|
||
|
||
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) {
|
||
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);
|
||
}
|
||
} 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; |