Reflex/src/utils/deviceFingerprint.js

435 lines
14 KiB
JavaScript
Raw Normal View History

/**
* Утилита для создания уникального отпечатка устройства (Device Fingerprinting)
* Используется для блокировки на уровне железа
*/
2025-05-26 18:00:56 +07:00
// Проверка на наличие браузерного окружения
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';
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;
}
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;
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;
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;
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));
} catch (e) {
console.warn('[DeviceFingerprint] Не удалось создать canvas fingerprint:', e);
}
}
/**
* WebGL fingerprinting
*/
async collectWebGLFingerprint() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
if (!isBrowser) return Promise.resolve();
return new Promise((resolve) => {
try {
2025-05-26 18:05:58 +07:00
// Если AudioContext недоступен, просто пропускаем этот шаг
if (!window.AudioContext && !window.webkitAudioContext) {
2025-05-26 18:05:58 +07:00
console.warn('[DeviceFingerprint] AudioContext не поддерживается');
this.components.set('audio', 'not-available');
resolve();
return;
}
2025-05-26 18:05:58 +07:00
// Создаём аудио хэш на основе доступных properties AudioContext
// без использования ScriptProcessorNode
const createAudioFingerprintFallback = () => {
try {
2025-05-26 18:05:58 +07:00
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();
}
};
2025-05-26 18:05:58 +07:00
// Используем безопасный fallback вместо устаревшего ScriptProcessor
// и AudioWorklet (который требует отдельного файла и https)
createAudioFingerprintFallback();
} catch (e) {
console.warn('[DeviceFingerprint] Не удалось создать audio fingerprint:', e);
2025-05-26 18:05:58 +07:00
this.components.set('audio', 'error');
resolve();
}
});
}
/**
* Информация о временной зоне
*/
async collectTimezoneInfo() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
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() {
2025-05-26 18:00:56 +07:00
if (!isBrowser) return 'server-environment-no-fingerprint';
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';
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);
}
} 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;