Reflex/src/utils/deviceFingerprint.js
Professional 489f718fec фикс
2025-05-26 18:05:58 +07:00

435 lines
14 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.

/**
* Утилита для создания уникального отпечатка устройства (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;