commit f07e8400cf71f6da77659d0dbb23764ea70820e8 Author: aegisinvestment Date: Mon Aug 11 20:52:14 2025 -0400 First commit - all files added diff --git a/README-LNbits-Integration.md b/README-LNbits-Integration.md new file mode 100644 index 0000000..d03fcd9 --- /dev/null +++ b/README-LNbits-Integration.md @@ -0,0 +1,159 @@ +# 🔧 Интеграция с LNbits - Руководство по тестированию + +## 📋 Обзор + +Интеграция с [LNbits](https://demo.lnbits.com) позволяет создавать Lightning Network инвойсы и верифицировать платежи в реальном времени. + +## 🚀 Быстрый старт + +### 1. Запуск тестов +```bash +# Откройте в браузере +test-lnbits-integration.html +``` + +### 2. Автоматическое тестирование +Нажмите кнопку **"🚀 Запустить все тесты"** для полной проверки интеграции. + +## 🧪 Доступные тесты + +### ✅ 1. Проверка API +- Тестирует доступность LNbits API +- Проверяет статус сервера +- Валидирует API ключ + +### ✅ 2. Создание инвойса +- Создает Lightning инвойс на 500 sats +- Проверяет корректность ответа +- Валидирует структуру данных + +### ✅ 3. Проверка статуса +- Проверяет статус созданного инвойса +- Отображает детали платежа +- Показывает время создания + +### ✅ 4. Верификация платежа +- Тестирует криптографические функции +- Проверяет SHA-256 хеширование +- Валидирует preimage/hash пары + +### ✅ 5. Тест реального платежа +- Проверяет готовность к реальным платежам +- Показывает инструкции по оплате +- Демонстрирует полный цикл + +## 💡 Как протестировать реальный платеж + +### Шаг 1: Создайте инвойс +1. Нажмите **"2. Создание инвойса"** +2. Скопируйте Payment Request из логов +3. Или отсканируйте QR код (если доступен) + +### Шаг 2: Оплатите инвойс +Используйте любой Lightning кошелек: +- **Alby** (браузерное расширение) +- **Zeus** (мобильный кошелек) +- **Phoenix** (мобильный кошелек) +- **Wallet of Satoshi** (мобильный кошелек) + +### Шаг 3: Проверьте статус +1. Нажмите **"3. Проверка статуса"** +2. Убедитесь, что `paid: true` +3. Скопируйте preimage из кошелька + +### Шаг 4: Верифицируйте платеж +1. Нажмите **"5. Тест реального платежа"** +2. Введите preimage в поле +3. Проверьте результат верификации + +## 🔧 Конфигурация + +### API настройки +```javascript +{ + apiUrl: 'https://demo.lnbits.com', + apiKey: '623515641d2e4ebcb1d5992d6d78419c', + walletId: 'bcd00f561c7b46b4a7b118f069e68997', + isDemo: true, + demoTimeout: 30000 +} +``` + +### Типы сессий +```javascript +{ + free: { sats: 0, hours: 1/60, usd: 0.00 }, + basic: { sats: 500, hours: 1, usd: 0.20 }, + premium: { sats: 1000, hours: 4, usd: 0.40 }, + extended: { sats: 2000, hours: 24, usd: 0.80 } +} +``` + +## 📊 Ожидаемые результаты + +### Успешный тест +``` +✅ API доступен +✅ Инвойс создан успешно +✅ Статус получен +✅ Криптографическая верификация работает +✅ Платеж готов к тестированию +``` + +### Возможные ошибки +- **API недоступен**: Проверьте интернет соединение +- **Ошибка создания инвойса**: Проверьте API ключ +- **Ошибка верификации**: Проверьте preimage формат + +## 🔍 Отладка + +### Логи в консоли +Откройте Developer Tools (F12) для детальных логов: +```javascript +console.log('🔍 Тестирование доступности API...'); +console.log('✅ API доступен'); +console.log('📊 Статус:', data); +``` + +### Проверка сети +В Network tab проверьте: +- Статус HTTP запросов +- Заголовки авторизации +- Тело ответов + +## 🚨 Известные проблемы + +### 1. CORS ошибки +**Проблема**: Браузер блокирует запросы к LNbits +**Решение**: Используйте локальный сервер или прокси + +### 2. API лимиты +**Проблема**: Слишком много запросов +**Решение**: Добавьте задержки между тестами + +### 3. Неверный preimage +**Проблема**: Ошибка верификации +**Решение**: Убедитесь, что preimage 64 символа hex + +## 📞 Поддержка + +### Полезные ссылки +- [LNbits Documentation](https://docs.lnbits.com/) +- [Lightning Network](https://lightning.network/) +- [BOLT11 Specification](https://github.com/lightning/bolts/blob/master/11-payment-encoding.md) + +### Контакты +- **GitHub**: [LockBit.chat](https://github.com/lockbitchat/lockbit-chat) +- **Документация**: [README.md](../README.md) + +## 🎯 Следующие шаги + +1. **Протестируйте все функции** +2. **Настройте продакшн API ключи** +3. **Интегрируйте в основное приложение** +4. **Добавьте мониторинг платежей** +5. **Настройте уведомления** + +--- + +**🎉 Интеграция готова к использованию!** diff --git a/ico.svg b/ico.svg new file mode 100644 index 0000000..18e4619 --- /dev/null +++ b/ico.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..2ed0831 --- /dev/null +++ b/index.html @@ -0,0 +1,3622 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LockBit.chat - Enhanced Security Edition + + + + + + + + + + + + + + +
+ + + + + + \ No newline at end of file diff --git a/logo/alby.svg b/logo/alby.svg new file mode 100644 index 0000000..dd74ef7 --- /dev/null +++ b/logo/alby.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo/atomic.svg b/logo/atomic.svg new file mode 100644 index 0000000..5646de8 --- /dev/null +++ b/logo/atomic.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/logo/blink.svg b/logo/blink.svg new file mode 100644 index 0000000..18b87fb --- /dev/null +++ b/logo/blink.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/logo/breez.svg b/logo/breez.svg new file mode 100644 index 0000000..32fdfdc --- /dev/null +++ b/logo/breez.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/logo/impervious.svg b/logo/impervious.svg new file mode 100644 index 0000000..0eaf5cf --- /dev/null +++ b/logo/impervious.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo/lightning-labs.svg b/logo/lightning-labs.svg new file mode 100644 index 0000000..ad01091 --- /dev/null +++ b/logo/lightning-labs.svg @@ -0,0 +1,37 @@ + + + + logo-invert + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo/lnbits.svg b/logo/lnbits.svg new file mode 100644 index 0000000..5312079 --- /dev/null +++ b/logo/lnbits.svg @@ -0,0 +1,13 @@ + + Group 6 + + + + + + + + + + + \ No newline at end of file diff --git a/logo/muun.svg b/logo/muun.svg new file mode 100644 index 0000000..367b1de --- /dev/null +++ b/logo/muun.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/logo/strike.svg b/logo/strike.svg new file mode 100644 index 0000000..04b74c8 --- /dev/null +++ b/logo/strike.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/logo/wos.svg b/logo/wos.svg new file mode 100644 index 0000000..a27b291 --- /dev/null +++ b/logo/wos.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/logo/zeus.svg b/logo/zeus.svg new file mode 100644 index 0000000..5953e32 --- /dev/null +++ b/logo/zeus.svg @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/src.zip b/src.zip new file mode 100644 index 0000000..b93ddd9 Binary files /dev/null and b/src.zip differ diff --git a/src/components/ui/Header.jsx b/src/components/ui/Header.jsx new file mode 100644 index 0000000..756af87 --- /dev/null +++ b/src/components/ui/Header.jsx @@ -0,0 +1,248 @@ +const React = window.React; + +const EnhancedMinimalHeader = ({ + status, + fingerprint, + verificationCode, + onDisconnect, + isConnected, + securityLevel, + sessionManager, + sessionTimeLeft +}) => { + const getStatusConfig = () => { + switch (status) { + case 'connected': + return { + text: 'Подключено', + className: 'status-connected', + badgeClass: 'bg-green-500/10 text-green-400 border-green-500/20' + }; + case 'verifying': + return { + text: 'Верификация...', + className: 'status-verifying', + badgeClass: 'bg-purple-500/10 text-purple-400 border-purple-500/20' + }; + case 'connecting': + return { + text: 'Подключение...', + className: 'status-connecting', + badgeClass: 'bg-blue-500/10 text-blue-400 border-blue-500/20' + }; + case 'retrying': + return { + text: 'Переподключение...', + className: 'status-connecting', + badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20' + }; + case 'failed': + return { + text: 'Ошибка', + className: 'status-failed', + badgeClass: 'bg-red-500/10 text-red-400 border-red-500/20' + }; + case 'reconnecting': + return { + text: 'Переподключение...', + className: 'status-connecting', + badgeClass: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20' + }; + case 'peer_disconnected': + return { + text: 'Собеседник отключился', + className: 'status-failed', + badgeClass: 'bg-orange-500/10 text-orange-400 border-orange-500/20' + }; + default: + return { + text: 'Не подключен', + className: 'status-disconnected', + badgeClass: 'bg-gray-500/10 text-gray-400 border-gray-500/20' + }; + } + }; + + const config = getStatusConfig(); + + const handleSecurityClick = () => { + if (securityLevel?.verificationResults) { + console.log('Security verification results:', securityLevel.verificationResults); + alert('Детали проверки безопасности:\n\n' + + Object.entries(securityLevel.verificationResults) + .map(([key, result]) => `${key}: ${result.passed ? '✅' : '❌'} ${result.details}`) + .join('\n') + ); + } + }; + + return React.createElement('header', { + className: 'header-minimal sticky top-0 z-50' + }, [ + React.createElement('div', { + key: 'container', + className: 'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8' + }, [ + React.createElement('div', { + key: 'content', + className: 'flex items-center justify-between h-16' + }, [ + // Logo and Title - Mobile Responsive + React.createElement('div', { + key: 'logo-section', + className: 'flex items-center space-x-2 sm:space-x-3' + }, [ + React.createElement('div', { + key: 'logo', + className: 'icon-container w-8 h-8 sm:w-10 sm:h-10' + }, [ + React.createElement('i', { + className: 'fas fa-shield-halved accent-orange text-sm sm:text-base' + }) + ]), + React.createElement('div', { + key: 'title-section' + }, [ + React.createElement('h1', { + key: 'title', + className: 'text-lg sm:text-xl font-semibold text-primary' + }, 'LockBit.chat'), + React.createElement('p', { + key: 'subtitle', + className: 'text-xs sm:text-sm text-muted hidden sm:block' + }, 'End-to-end freedom') + ]) + ]), + + // Status and Controls - Mobile Responsive + React.createElement('div', { + key: 'status-section', + className: 'flex items-center space-x-2 sm:space-x-3' + }, [ + // Session Timer - показывать если есть активная сессия + (() => { + const hasActive = sessionManager?.hasActiveSession(); + const hasTimer = !!window.SessionTimer; + console.log('Header SessionTimer check:', { + hasActive, + hasTimer, + sessionTimeLeft, + sessionType: sessionManager?.currentSession?.type + }); + + return hasActive && hasTimer && React.createElement(window.SessionTimer, { + key: 'session-timer', + timeLeft: sessionTimeLeft, + sessionType: sessionManager.currentSession?.type || 'unknown' + }); + })(), + + // Security Level Indicator - Hidden on mobile, shown on tablet+ (Clickable) + securityLevel && React.createElement('div', { + key: 'security-level', + className: 'hidden md:flex items-center space-x-2 cursor-pointer hover:opacity-80 transition-opacity duration-200', + onClick: handleSecurityClick, + title: 'Нажмите для просмотра деталей безопасности' + }, [ + React.createElement('div', { + key: 'security-icon', + className: `w-6 h-6 rounded-full flex items-center justify-center ${ + securityLevel.color === 'green' ? 'bg-green-500/20' : + securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20' + }` + }, [ + React.createElement('i', { + className: `fas fa-shield-alt text-xs ${ + securityLevel.color === 'green' ? 'text-green-400' : + securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400' + }` + }) + ]), + React.createElement('div', { + key: 'security-info', + className: 'flex flex-col' + }, [ + React.createElement('div', { + key: 'security-level-text', + className: 'text-xs font-medium text-primary' + }, `${securityLevel.level} (${securityLevel.score}%)`), + securityLevel.details && React.createElement('div', { + key: 'security-details', + className: 'text-xs text-muted mt-1 hidden lg:block' + }, securityLevel.details), + React.createElement('div', { + key: 'security-progress', + className: 'w-16 h-1 bg-gray-600 rounded-full overflow-hidden' + }, [ + React.createElement('div', { + key: 'progress-bar', + className: `h-full transition-all duration-500 ${ + securityLevel.color === 'green' ? 'bg-green-400' : + securityLevel.color === 'yellow' ? 'bg-yellow-400' : 'bg-red-400' + }`, + style: { width: `${securityLevel.score}%` } + }) + ]) + ]) + ]), + + // Mobile Security Indicator - Only icon on mobile (Clickable) + securityLevel && React.createElement('div', { + key: 'mobile-security', + className: 'md:hidden flex items-center' + }, [ + React.createElement('div', { + key: 'mobile-security-icon', + className: `w-8 h-8 rounded-full flex items-center justify-center cursor-pointer hover:opacity-80 transition-opacity duration-200 ${ + securityLevel.color === 'green' ? 'bg-green-500/20' : + securityLevel.color === 'yellow' ? 'bg-yellow-500/20' : 'bg-red-500/20' + }`, + title: `${securityLevel.level} (${securityLevel.score}%) - Нажмите для деталей`, + onClick: handleSecurityClick + }, [ + React.createElement('i', { + className: `fas fa-shield-alt text-sm ${ + securityLevel.color === 'green' ? 'text-green-400' : + securityLevel.color === 'yellow' ? 'text-yellow-400' : 'text-red-400' + }` + }) + ]) + ]), + + // Status Badge - Compact on mobile + React.createElement('div', { + key: 'status-badge', + className: `px-2 sm:px-3 py-1.5 rounded-lg border ${config.badgeClass} flex items-center space-x-1 sm:space-x-2` + }, [ + React.createElement('span', { + key: 'status-dot', + className: `status-dot ${config.className}` + }), + React.createElement('span', { + key: 'status-text', + className: 'text-xs sm:text-sm font-medium' + }, config.text) + ]), + + // Disconnect Button - Icon only on mobile + isConnected && React.createElement('button', { + key: 'disconnect-btn', + onClick: onDisconnect, + className: 'p-1.5 sm:px-3 sm:py-1.5 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/20 rounded-lg transition-all duration-200 text-sm' + }, [ + React.createElement('i', { + key: 'disconnect-icon', + className: 'fas fa-power-off sm:mr-2' + }), + React.createElement('span', { + key: 'disconnect-text', + className: 'hidden sm:inline' + }, 'Отключить') + ]) + ]) + ]) + ]) + ]); +}; + +window.EnhancedMinimalHeader = EnhancedMinimalHeader; \ No newline at end of file diff --git a/src/components/ui/LightningPayment.jsx b/src/components/ui/LightningPayment.jsx new file mode 100644 index 0000000..4aee246 --- /dev/null +++ b/src/components/ui/LightningPayment.jsx @@ -0,0 +1,392 @@ +const React = window.React; +const { useState, useEffect } = React; + +const IntegratedLightningPayment = ({ sessionType, onSuccess, onCancel, paymentManager }) => { + const [paymentMethod, setPaymentMethod] = useState('webln'); + const [preimage, setPreimage] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [error, setError] = useState(''); + const [invoice, setInvoice] = useState(null); + const [paymentStatus, setPaymentStatus] = useState('pending'); // pending, created, paid, expired + const [qrCodeUrl, setQrCodeUrl] = useState(''); + + // Создаем инвойс при загрузке компонента + useEffect(() => { + createInvoice(); + }, [sessionType]); + + const createInvoice = async () => { + if (sessionType === 'free') { + // Для бесплатной сессии не нужен инвойс + setPaymentStatus('free'); + return; + } + + setIsProcessing(true); + setError(''); + + try { + console.log('Creating Lightning invoice for', sessionType); + console.log('Payment manager available:', !!paymentManager); + + if (!paymentManager) { + throw new Error('Payment manager not available. Please check sessionManager initialization.'); + } + + // Создаем инвойс через paymentManager + const createdInvoice = await paymentManager.createLightningInvoice(sessionType); + + if (!createdInvoice) { + throw new Error('Failed to create invoice'); + } + + setInvoice(createdInvoice); + setPaymentStatus('created'); + + // Создаем QR код + if (createdInvoice.paymentRequest) { + const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(createdInvoice.paymentRequest)}`; + setQrCodeUrl(qrUrl); + } + + console.log('Invoice created successfully:', createdInvoice); + + } catch (err) { + console.error('Invoice creation failed:', err); + setError(`Ошибка создания инвойса: ${err.message}`); + } finally { + setIsProcessing(false); + } + }; + + const handleWebLNPayment = async () => { + if (!window.webln) { + setError('WebLN не поддерживается. Используйте кошелек Alby или Zeus'); + return; + } + + if (!invoice || !invoice.paymentRequest) { + setError('Инвойс не готов для оплаты'); + return; + } + + setIsProcessing(true); + setError(''); + + try { + console.log('Enabling WebLN...'); + await window.webln.enable(); + + console.log('Sending WebLN payment...'); + const result = await window.webln.sendPayment(invoice.paymentRequest); + + if (result.preimage) { + console.log('WebLN payment successful, preimage:', result.preimage); + setPaymentStatus('paid'); + + // Активируем сессию + await activateSession(result.preimage); + } else { + setError('Платеж не содержит preimage'); + } + } catch (err) { + console.error('WebLN payment failed:', err); + setError(`Ошибка WebLN: ${err.message}`); + } finally { + setIsProcessing(false); + } + }; + + const handleManualVerification = async () => { + const trimmedPreimage = preimage.trim(); + + if (!trimmedPreimage) { + setError('Введите preimage платежа'); + return; + } + + if (trimmedPreimage.length !== 64) { + setError('Preimage должен содержать ровно 64 символа'); + return; + } + + if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) { + setError('Preimage должен содержать только шестнадцатеричные символы (0-9, a-f, A-F)'); + return; + } + + if (trimmedPreimage === '1'.repeat(64) || + trimmedPreimage === 'a'.repeat(64) || + trimmedPreimage === 'f'.repeat(64)) { + setError('Введенный preimage слишком простой. Проверьте правильность ключа.'); + return; + } + + setError(''); + setIsProcessing(true); + + try { + await activateSession(trimmedPreimage); + } catch (err) { + setError(`Ошибка активации: ${err.message}`); + } finally { + setIsProcessing(false); + } + }; + + const activateSession = async (preimageValue) => { + try { + console.log('🚀 Activating session with preimage:', preimageValue); + console.log('Payment manager available:', !!paymentManager); + console.log('Invoice available:', !!invoice); + + let result; + if (paymentManager) { + const paymentHash = invoice?.paymentHash || 'dummy_hash'; + console.log('Using payment hash:', paymentHash); + result = await paymentManager.safeActivateSession(sessionType, preimageValue, paymentHash); + } else { + console.warn('Payment manager not available, using fallback'); + // Fallback если paymentManager недоступен + result = { success: true, method: 'fallback' }; + } + + if (result.success) { + console.log('✅ Session activated successfully:', result); + setPaymentStatus('paid'); + onSuccess(preimageValue, invoice); + } else { + console.error('❌ Session activation failed:', result); + throw new Error(`Session activation failed: ${result.reason}`); + } + + } catch (err) { + console.error('❌ Session activation failed:', err); + throw err; + } + }; + + const handleFreeSession = async () => { + setIsProcessing(true); + try { + await activateSession('0'.repeat(64)); + } catch (err) { + setError(`Ошибка активации бесплатной сессии: ${err.message}`); + } finally { + setIsProcessing(false); + } + }; + + const copyToClipboard = (text) => { + navigator.clipboard.writeText(text).then(() => { + // Можно добавить уведомление о копировании + }); + }; + + const pricing = { + free: { sats: 1, hours: 1/60 }, + basic: { sats: 500, hours: 1 }, + premium: { sats: 1000, hours: 4 }, + extended: { sats: 2000, hours: 24 } + }[sessionType]; + + return React.createElement('div', { className: 'space-y-4 max-w-md mx-auto' }, [ + // Header + React.createElement('div', { key: 'header', className: 'text-center' }, [ + React.createElement('h3', { + key: 'title', + className: 'text-xl font-semibold text-white mb-2' + }, sessionType === 'free' ? 'Бесплатная сессия' : 'Оплата Lightning'), + React.createElement('div', { + key: 'amount', + className: 'text-2xl font-bold text-orange-400' + }, sessionType === 'free' + ? '1 сат за 1 минуту' + : `${pricing.sats} сат за ${pricing.hours}ч` + ), + sessionType !== 'free' && React.createElement('div', { + key: 'usd', + className: 'text-sm text-gray-400 mt-1' + }, `≈ $${(pricing.sats * 0.0004).toFixed(2)} USD`) + ]), + + // Loading State + isProcessing && paymentStatus === 'pending' && React.createElement('div', { + key: 'loading', + className: 'text-center' + }, [ + React.createElement('div', { + key: 'spinner', + className: 'text-orange-400' + }, [ + React.createElement('i', { className: 'fas fa-spinner fa-spin mr-2' }), + 'Создание инвойса...' + ]) + ]), + + // Free Session + sessionType === 'free' && React.createElement('div', { + key: 'free-session', + className: 'space-y-3' + }, [ + React.createElement('div', { + key: 'info', + className: 'p-3 bg-blue-500/10 border border-blue-500/20 rounded text-blue-300 text-sm' + }, 'Будет активирована бесплатная сессия на 1 минуту.'), + React.createElement('button', { + key: 'start-btn', + onClick: handleFreeSession, + disabled: isProcessing, + className: 'w-full bg-blue-600 hover:bg-blue-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50' + }, [ + React.createElement('i', { + key: 'icon', + className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2` + }), + isProcessing ? 'Активация...' : 'Начать бесплатную сессию' + ]) + ]), + + // Paid Sessions + sessionType !== 'free' && paymentStatus === 'created' && invoice && React.createElement('div', { + key: 'paid-session', + className: 'space-y-4' + }, [ + // QR Code + qrCodeUrl && React.createElement('div', { + key: 'qr-section', + className: 'text-center' + }, [ + React.createElement('div', { + key: 'qr-container', + className: 'bg-white p-4 rounded-lg inline-block' + }, [ + React.createElement('img', { + key: 'qr-img', + src: qrCodeUrl, + alt: 'Payment QR Code', + className: 'w-48 h-48' + }) + ]), + React.createElement('div', { + key: 'qr-hint', + className: 'text-xs text-gray-400 mt-2' + }, 'Сканируйте QR код любым Lightning кошельком') + ]), + + // Payment Request + invoice.paymentRequest && React.createElement('div', { + key: 'payment-request', + className: 'space-y-2' + }, [ + React.createElement('div', { + key: 'label', + className: 'text-sm font-medium text-white' + }, 'Payment Request:'), + React.createElement('div', { + key: 'request', + className: 'p-3 bg-gray-800 rounded border text-xs font-mono text-gray-300 cursor-pointer hover:bg-gray-700', + onClick: () => copyToClipboard(invoice.paymentRequest) + }, [ + invoice.paymentRequest.substring(0, 50) + '...', + React.createElement('i', { key: 'copy-icon', className: 'fas fa-copy ml-2 text-orange-400' }) + ]) + ]), + + // WebLN Payment + React.createElement('div', { + key: 'webln-section', + className: 'space-y-3' + }, [ + React.createElement('h4', { + key: 'webln-title', + className: 'text-white font-medium flex items-center' + }, [ + React.createElement('i', { key: 'bolt-icon', className: 'fas fa-bolt text-orange-400 mr-2' }), + 'WebLN кошелек (Alby, Zeus)' + ]), + React.createElement('button', { + key: 'webln-btn', + onClick: handleWebLNPayment, + disabled: isProcessing, + className: 'w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50' + }, [ + React.createElement('i', { + key: 'webln-icon', + className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-bolt'} mr-2` + }), + isProcessing ? 'Обработка...' : 'Оплатить через WebLN' + ]) + ]), + + // Manual Payment + React.createElement('div', { + key: 'divider', + className: 'text-center text-gray-400' + }, 'или'), + + React.createElement('div', { + key: 'manual-section', + className: 'space-y-3' + }, [ + React.createElement('h4', { + key: 'manual-title', + className: 'text-white font-medium' + }, 'Ручная проверка платежа'), + React.createElement('input', { + key: 'preimage-input', + type: 'text', + value: preimage, + onChange: (e) => setPreimage(e.target.value), + placeholder: 'Введите preimage после оплаты...', + className: 'w-full p-3 bg-gray-800 border border-gray-600 rounded text-white placeholder-gray-400 text-sm' + }), + React.createElement('button', { + key: 'verify-btn', + onClick: handleManualVerification, + disabled: isProcessing, + className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50' + }, [ + React.createElement('i', { + key: 'verify-icon', + className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-check'} mr-2` + }), + isProcessing ? 'Проверка...' : 'Подтвердить платеж' + ]) + ]) + ]), + + // Success State + paymentStatus === 'paid' && React.createElement('div', { + key: 'success', + className: 'text-center p-4 bg-green-500/10 border border-green-500/20 rounded' + }, [ + React.createElement('i', { key: 'success-icon', className: 'fas fa-check-circle text-green-400 text-2xl mb-2' }), + React.createElement('div', { key: 'success-text', className: 'text-green-300 font-medium' }, 'Платеж подтвержден!'), + React.createElement('div', { key: 'success-subtext', className: 'text-green-400 text-sm' }, 'Сессия активирована') + ]), + + // Error State + error && React.createElement('div', { + key: 'error', + className: 'p-3 bg-red-500/10 border border-red-500/20 rounded text-red-400 text-sm' + }, [ + React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle mr-2' }), + error, + error.includes('инвойса') && React.createElement('button', { + key: 'retry-btn', + onClick: createInvoice, + className: 'ml-2 text-orange-400 hover:text-orange-300 underline' + }, 'Попробовать снова') + ]), + + // Cancel Button + React.createElement('button', { + key: 'cancel-btn', + onClick: onCancel, + className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded' + }, 'Отмена') + ]); +}; + +window.LightningPayment = IntegratedLightningPayment; \ No newline at end of file diff --git a/src/components/ui/PasswordModal.jsx b/src/components/ui/PasswordModal.jsx new file mode 100644 index 0000000..1ebdbd3 --- /dev/null +++ b/src/components/ui/PasswordModal.jsx @@ -0,0 +1,91 @@ +const React = window.React; + +const PasswordModal = ({ isOpen, onClose, onSubmit, action, password, setPassword }) => { + if (!isOpen) return null; + + const handleSubmit = (e) => { + e.preventDefault(); + if (password.trim()) { + onSubmit(password.trim()); + setPassword(''); + } + }; + + const getActionText = () => { + return action === 'offer' ? 'приглашения' : 'ответа'; + }; + + return React.createElement('div', { + className: 'fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4' + }, [ + React.createElement('div', { + key: 'modal', + className: 'card-minimal rounded-xl p-6 max-w-md w-full border-purple-500/20' + }, [ + React.createElement('div', { + key: 'header', + className: 'flex items-center mb-4' + }, [ + React.createElement('div', { + key: 'icon', + className: 'w-10 h-10 bg-purple-500/10 border border-purple-500/20 rounded-lg flex items-center justify-center mr-3' + }, [ + React.createElement('i', { + className: 'fas fa-key accent-purple' + }) + ]), + React.createElement('h3', { + key: 'title', + className: 'text-lg font-medium text-primary' + }, 'Ввод пароля') + ]), + React.createElement('form', { + key: 'form', + onSubmit: handleSubmit, + className: 'space-y-4' + }, [ + React.createElement('p', { + key: 'description', + className: 'text-secondary text-sm' + }, `Введите пароль для расшифровки ${getActionText()}:`), + React.createElement('input', { + key: 'password-input', + type: 'password', + value: password, + onChange: (e) => setPassword(e.target.value), + placeholder: 'Введите пароль...', + className: 'w-full p-3 bg-gray-900/30 border border-gray-500/20 rounded-lg text-primary placeholder-gray-500 focus:border-purple-500/40 focus:outline-none transition-all', + autoFocus: true + }), + React.createElement('div', { + key: 'buttons', + className: 'flex space-x-3' + }, [ + React.createElement('button', { + key: 'submit', + type: 'submit', + className: 'flex-1 btn-primary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200' + }, [ + React.createElement('i', { + className: 'fas fa-unlock-alt mr-2' + }), + 'Расшифровать' + ]), + React.createElement('button', { + key: 'cancel', + type: 'button', + onClick: onClose, + className: 'flex-1 btn-secondary text-white py-3 px-4 rounded-lg font-medium transition-all duration-200' + }, [ + React.createElement('i', { + className: 'fas fa-times mr-2' + }), + 'Отмена' + ]) + ]) + ]) + ]) + ]); +}; + +window.PasswordModal = PasswordModal; \ No newline at end of file diff --git a/src/components/ui/PaymentModal.jsx b/src/components/ui/PaymentModal.jsx new file mode 100644 index 0000000..aad5353 --- /dev/null +++ b/src/components/ui/PaymentModal.jsx @@ -0,0 +1,574 @@ +const React = window.React; +const { useState, useEffect, useRef } = React; + +const PaymentModal = ({ isOpen, onClose, sessionManager, onSessionPurchased }) => { + const [step, setStep] = useState('select'); + const [selectedType, setSelectedType] = useState(null); + const [invoice, setInvoice] = useState(null); + const [paymentStatus, setPaymentStatus] = useState('pending'); // pending, creating, created, paying, paid, failed, expired + const [error, setError] = useState(''); + const [paymentMethod, setPaymentMethod] = useState('webln'); // webln, manual, qr + const [preimageInput, setPreimageInput] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + const [qrCodeUrl, setQrCodeUrl] = useState(''); + const [paymentTimer, setPaymentTimer] = useState(null); + const [timeLeft, setTimeLeft] = useState(0); + const pollInterval = useRef(null); + + // Cleanup на закрытие + useEffect(() => { + if (!isOpen) { + resetModal(); + if (pollInterval.current) { + clearInterval(pollInterval.current); + } + if (paymentTimer) { + clearInterval(paymentTimer); + } + } + }, [isOpen]); + + const resetModal = () => { + setStep('select'); + setSelectedType(null); + setInvoice(null); + setPaymentStatus('pending'); + setError(''); + setPaymentMethod('webln'); + setPreimageInput(''); + setIsProcessing(false); + setQrCodeUrl(''); + setTimeLeft(0); + }; + + const handleSelectType = async (type) => { + setSelectedType(type); + setError(''); + + if (type === 'free') { + // Для бесплатной сессии создаем фиктивный инвойс + setInvoice({ + sessionType: 'free', + amount: 1, + paymentHash: '0'.repeat(64), + memo: 'Free session (1 minute)', + createdAt: Date.now() + }); + setPaymentStatus('free'); + } else { + await createRealInvoice(type); + } + setStep('payment'); + }; + + const createRealInvoice = async (type) => { + setPaymentStatus('creating'); + setIsProcessing(true); + setError(''); + + try { + console.log(`Creating real Lightning invoice for ${type} session...`); + + if (!sessionManager) { + throw new Error('Session manager не инициализирован'); + } + + // Создаем реальный Lightning инвойс через LNbits + const createdInvoice = await sessionManager.createLightningInvoice(type); + + if (!createdInvoice || !createdInvoice.paymentRequest) { + throw new Error('Не удалось создать Lightning инвойс'); + } + + setInvoice(createdInvoice); + setPaymentStatus('created'); + + // Создаем QR код для инвойса + const qrUrl = `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(createdInvoice.paymentRequest)}`; + setQrCodeUrl(qrUrl); + + // Запускаем таймер на 15 минут + const expirationTime = 15 * 60 * 1000; // 15 минут + setTimeLeft(expirationTime); + + const timer = setInterval(() => { + setTimeLeft(prev => { + const newTime = prev - 1000; + if (newTime <= 0) { + clearInterval(timer); + setPaymentStatus('expired'); + setError('Время для оплаты истекло. Создайте новый инвойс.'); + return 0; + } + return newTime; + }); + }, 1000); + setPaymentTimer(timer); + + // Запускаем автопроверку статуса платежа + startPaymentPolling(createdInvoice.checkingId); + + console.log('✅ Lightning invoice created successfully:', createdInvoice); + + } catch (err) { + console.error('❌ Invoice creation failed:', err); + setError(`Ошибка создания инвойса: ${err.message}`); + setPaymentStatus('failed'); + } finally { + setIsProcessing(false); + } + }; + + // Автопроверка статуса платежа каждые 3 секунды + const startPaymentPolling = (checkingId) => { + if (pollInterval.current) { + clearInterval(pollInterval.current); + } + + pollInterval.current = setInterval(async () => { + try { + const status = await sessionManager.checkPaymentStatus(checkingId); + + if (status.paid && status.preimage) { + console.log('✅ Payment confirmed automatically!', status); + clearInterval(pollInterval.current); + setPaymentStatus('paid'); + await handlePaymentSuccess(status.preimage); + } + } catch (error) { + console.warn('Payment status check failed:', error); + // Продолжаем проверять, не останавливаем polling из-за одной ошибки + } + }, 3000); // Проверяем каждые 3 секунды + }; + + const handleWebLNPayment = async () => { + if (!window.webln) { + setError('WebLN не поддерживается. Установите кошелек Alby или Zeus'); + return; + } + + if (!invoice || !invoice.paymentRequest) { + setError('Инвойс не готов для оплаты'); + return; + } + + setIsProcessing(true); + setError(''); + setPaymentStatus('paying'); + + try { + console.log('🔌 Enabling WebLN...'); + await window.webln.enable(); + + console.log('💰 Sending WebLN payment...'); + const result = await window.webln.sendPayment(invoice.paymentRequest); + + if (result.preimage) { + console.log('✅ WebLN payment successful!', result); + setPaymentStatus('paid'); + await handlePaymentSuccess(result.preimage); + } else { + throw new Error('Платеж не содержит preimage'); + } + } catch (err) { + console.error('❌ WebLN payment failed:', err); + setError(`Ошибка WebLN платежа: ${err.message}`); + setPaymentStatus('created'); // Возвращаем к состоянию "создан" + } finally { + setIsProcessing(false); + } + }; + + const handleManualVerification = async () => { + const trimmedPreimage = preimageInput.trim(); + + if (!trimmedPreimage) { + setError('Введите preimage платежа'); + return; + } + + if (trimmedPreimage.length !== 64) { + setError('Preimage должен содержать ровно 64 символа'); + return; + } + + if (!/^[0-9a-fA-F]{64}$/.test(trimmedPreimage)) { + setError('Preimage должен содержать только шестнадцатеричные символы (0-9, a-f, A-F)'); + return; + } + + // Проверяем на простые/тестовые preimage + const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64), '0'.repeat(64)]; + if (dummyPreimages.includes(trimmedPreimage) && selectedType !== 'free') { + setError('Введенный preimage недействителен. Используйте настоящий preimage от платежа.'); + return; + } + + setIsProcessing(true); + setError(''); + setPaymentStatus('paying'); + + try { + await handlePaymentSuccess(trimmedPreimage); + } catch (err) { + setError(err.message); + setPaymentStatus('created'); + } finally { + setIsProcessing(false); + } + }; + + const handleFreeSession = async () => { + setIsProcessing(true); + setError(''); + + try { + await handlePaymentSuccess('0'.repeat(64)); + } catch (err) { + setError(`Ошибка активации бесплатной сессии: ${err.message}`); + } finally { + setIsProcessing(false); + } + }; + + const handlePaymentSuccess = async (preimage) => { + try { + console.log('🔍 Verifying payment...', { selectedType, preimage }); + + let isValid; + if (selectedType === 'free') { + isValid = true; + } else { + // Верифицируем реальный платеж + isValid = await sessionManager.verifyPayment(preimage, invoice.paymentHash); + } + + if (isValid) { + console.log('✅ Payment verified successfully!'); + + // Останавливаем polling и таймеры + if (pollInterval.current) { + clearInterval(pollInterval.current); + } + if (paymentTimer) { + clearInterval(paymentTimer); + } + + // Передаем данные о покупке + onSessionPurchased({ + type: selectedType, + preimage, + paymentHash: invoice.paymentHash, + amount: invoice.amount + }); + + // Закрываем модалку с задержкой для показа успеха + setTimeout(() => { + onClose(); + }, 1500); + + } else { + throw new Error('Платеж не прошел верификацию. Проверьте правильность preimage или попробуйте снова.'); + } + } catch (error) { + console.error('❌ Payment verification failed:', error); + throw error; + } + }; + + const copyToClipboard = async (text) => { + try { + await navigator.clipboard.writeText(text); + // Можно добавить visual feedback + } catch (err) { + console.error('Failed to copy:', err); + } + }; + + const formatTime = (ms) => { + const minutes = Math.floor(ms / 60000); + const seconds = Math.floor((ms % 60000) / 1000); + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + }; + + const pricing = sessionManager?.sessionPrices || { + free: { sats: 1, hours: 1/60, usd: 0.00 }, + basic: { sats: 500, hours: 1, usd: 0.20 }, + premium: { sats: 1000, hours: 4, usd: 0.40 }, + extended: { sats: 2000, hours: 24, usd: 0.80 } + }; + + if (!isOpen) return null; + + return React.createElement('div', { + className: 'fixed inset-0 bg-black/80 backdrop-blur-sm z-50 flex items-center justify-center p-4' + }, [ + React.createElement('div', { + key: 'modal', + className: 'card-minimal rounded-xl p-6 max-w-lg w-full max-h-[90vh] overflow-y-auto custom-scrollbar' + }, [ + // Header с кнопкой закрытия + React.createElement('div', { + key: 'header', + className: 'flex items-center justify-between mb-6' + }, [ + React.createElement('h2', { + key: 'title', + className: 'text-xl font-semibold text-primary' + }, step === 'select' ? 'Выберите тип сессии' : 'Оплата сессии'), + React.createElement('button', { + key: 'close', + onClick: onClose, + className: 'text-gray-400 hover:text-white transition-colors' + }, React.createElement('i', { className: 'fas fa-times' })) + ]), + + // Step 1: Session Type Selection + step === 'select' && window.SessionTypeSelector && React.createElement(window.SessionTypeSelector, { + key: 'selector', + onSelectType: handleSelectType, + onCancel: onClose + }), + + // Step 2: Payment Processing + step === 'payment' && React.createElement('div', { + key: 'payment-step', + className: 'space-y-6' + }, [ + // Session Info + React.createElement('div', { + key: 'session-info', + className: 'text-center p-4 bg-orange-500/10 border border-orange-500/20 rounded-lg' + }, [ + React.createElement('h3', { + key: 'session-title', + className: 'text-lg font-semibold text-orange-400 mb-2' + }, `${selectedType.charAt(0).toUpperCase() + selectedType.slice(1)} сессия`), + React.createElement('div', { + key: 'session-details', + className: 'text-sm text-secondary' + }, [ + React.createElement('div', { key: 'amount' }, `${pricing[selectedType].sats} сат за ${pricing[selectedType].hours}ч`), + pricing[selectedType].usd > 0 && React.createElement('div', { + key: 'usd', + className: 'text-gray-400' + }, `≈ $${pricing[selectedType].usd} USD`) + ]) + ]), + + // Timer для платных сессий + timeLeft > 0 && paymentStatus === 'created' && React.createElement('div', { + key: 'timer', + className: 'text-center p-3 bg-yellow-500/10 border border-yellow-500/20 rounded' + }, [ + React.createElement('div', { + key: 'timer-text', + className: 'text-yellow-400 font-medium' + }, `⏱️ Время на оплату: ${formatTime(timeLeft)}`) + ]), + + // Бесплатная сессия + paymentStatus === 'free' && React.createElement('div', { + key: 'free-payment', + className: 'space-y-4' + }, [ + React.createElement('div', { + key: 'free-info', + className: 'p-4 bg-blue-500/10 border border-blue-500/20 rounded text-blue-300 text-sm text-center' + }, '🎉 Бесплатная сессия на 1 минуту'), + React.createElement('button', { + key: 'free-btn', + onClick: handleFreeSession, + disabled: isProcessing, + className: 'w-full bg-blue-600 hover:bg-blue-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed' + }, [ + React.createElement('i', { + key: 'free-icon', + className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-play'} mr-2` + }), + isProcessing ? 'Активация...' : 'Активировать бесплатную сессию' + ]) + ]), + + // Создание инвойса + paymentStatus === 'creating' && React.createElement('div', { + key: 'creating', + className: 'text-center p-4' + }, [ + React.createElement('i', { className: 'fas fa-spinner fa-spin text-orange-400 text-2xl mb-2' }), + React.createElement('div', { className: 'text-primary' }, 'Создание Lightning инвойса...'), + React.createElement('div', { className: 'text-secondary text-sm mt-1' }, 'Подключение к Lightning Network...') + ]), + + // Платная сессия с инвойсом + (paymentStatus === 'created' || paymentStatus === 'paying') && invoice && React.createElement('div', { + key: 'payment-methods', + className: 'space-y-6' + }, [ + // QR Code + qrCodeUrl && React.createElement('div', { + key: 'qr-section', + className: 'text-center' + }, [ + React.createElement('div', { + key: 'qr-container', + className: 'bg-white p-4 rounded-lg inline-block' + }, [ + React.createElement('img', { + key: 'qr-img', + src: qrCodeUrl, + alt: 'Lightning Payment QR Code', + className: 'w-48 h-48' + }) + ]), + React.createElement('div', { + key: 'qr-hint', + className: 'text-xs text-gray-400 mt-2' + }, 'Сканируйте любым Lightning кошельком') + ]), + + // Payment Request для копирования + invoice.paymentRequest && React.createElement('div', { + key: 'payment-request', + className: 'space-y-2' + }, [ + React.createElement('div', { + key: 'pr-label', + className: 'text-sm font-medium text-primary' + }, 'Lightning Payment Request:'), + React.createElement('div', { + key: 'pr-container', + className: 'p-3 bg-gray-800/50 rounded border border-gray-600 text-xs font-mono text-gray-300 cursor-pointer hover:bg-gray-700/50 transition-colors', + onClick: () => copyToClipboard(invoice.paymentRequest), + title: 'Нажмите для копирования' + }, [ + invoice.paymentRequest.substring(0, 60) + '...', + React.createElement('i', { key: 'copy-icon', className: 'fas fa-copy ml-2 text-orange-400' }) + ]) + ]), + + // WebLN Payment + React.createElement('div', { + key: 'webln-section', + className: 'space-y-3' + }, [ + React.createElement('h4', { + key: 'webln-title', + className: 'text-primary font-medium flex items-center' + }, [ + React.createElement('i', { key: 'bolt-icon', className: 'fas fa-bolt text-orange-400 mr-2' }), + 'WebLN кошелек (рекомендуется)' + ]), + React.createElement('div', { + key: 'webln-info', + className: 'text-xs text-gray-400 mb-2' + }, 'Alby, Zeus, или другие WebLN совместимые кошельки'), + React.createElement('button', { + key: 'webln-btn', + onClick: handleWebLNPayment, + disabled: isProcessing || paymentStatus === 'paying', + className: 'w-full bg-orange-600 hover:bg-orange-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors' + }, [ + React.createElement('i', { + key: 'webln-icon', + className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-bolt'} mr-2` + }), + paymentStatus === 'paying' ? 'Обработка платежа...' : 'Оплатить через WebLN' + ]) + ]), + + // Divider + React.createElement('div', { + key: 'divider', + className: 'text-center text-gray-400 text-sm' + }, '— или —'), + + // Manual Verification + React.createElement('div', { + key: 'manual-section', + className: 'space-y-3' + }, [ + React.createElement('h4', { + key: 'manual-title', + className: 'text-primary font-medium' + }, 'Ручное подтверждение платежа'), + React.createElement('div', { + key: 'manual-info', + className: 'text-xs text-gray-400' + }, 'Оплатите инвойс в любом кошельке и введите preimage:'), + React.createElement('input', { + key: 'preimage-input', + type: 'text', + value: preimageInput, + onChange: (e) => setPreimageInput(e.target.value), + placeholder: 'Введите preimage (64 hex символа)...', + className: 'w-full p-3 bg-gray-800 border border-gray-600 rounded text-white placeholder-gray-400 text-sm font-mono', + maxLength: 64 + }), + React.createElement('button', { + key: 'verify-btn', + onClick: handleManualVerification, + disabled: isProcessing || !preimageInput.trim(), + className: 'w-full bg-green-600 hover:bg-green-500 text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors' + }, [ + React.createElement('i', { + key: 'verify-icon', + className: `fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-check'} mr-2` + }), + isProcessing ? 'Проверка платежа...' : 'Подтвердить платеж' + ]) + ]) + ]), + + // Success State + paymentStatus === 'paid' && React.createElement('div', { + key: 'success', + className: 'text-center p-6 bg-green-500/10 border border-green-500/20 rounded-lg' + }, [ + React.createElement('i', { key: 'success-icon', className: 'fas fa-check-circle text-green-400 text-3xl mb-3' }), + React.createElement('div', { key: 'success-title', className: 'text-green-300 font-semibold text-lg mb-1' }, '✅ Платеж подтвержден!'), + React.createElement('div', { key: 'success-text', className: 'text-green-400 text-sm' }, 'Сессия будет активирована при подключении к чату') + ]), + + // Error State + error && React.createElement('div', { + key: 'error', + className: 'p-4 bg-red-500/10 border border-red-500/20 rounded-lg' + }, [ + React.createElement('div', { + key: 'error-content', + className: 'flex items-start space-x-3' + }, [ + React.createElement('i', { key: 'error-icon', className: 'fas fa-exclamation-triangle text-red-400 mt-0.5' }), + React.createElement('div', { key: 'error-text', className: 'flex-1' }, [ + React.createElement('div', { key: 'error-message', className: 'text-red-400 text-sm' }, error), + (error.includes('инвойса') || paymentStatus === 'failed') && React.createElement('button', { + key: 'retry-btn', + onClick: () => createRealInvoice(selectedType), + className: 'mt-2 text-orange-400 hover:text-orange-300 underline text-sm' + }, 'Создать новый инвойс') + ]) + ]) + ]), + + // Back button (кроме случая успешной оплаты) + paymentStatus !== 'paid' && React.createElement('div', { + key: 'back-section', + className: 'pt-4 border-t border-gray-600' + }, [ + React.createElement('button', { + key: 'back-btn', + onClick: () => setStep('select'), + className: 'w-full bg-gray-600 hover:bg-gray-500 text-white py-2 px-4 rounded transition-colors' + }, [ + React.createElement('i', { key: 'back-icon', className: 'fas fa-arrow-left mr-2' }), + 'Выбрать другую сессию' + ]) + ]) + ]) + ]) + ]); +}; + +window.PaymentModal = PaymentModal; \ No newline at end of file diff --git a/src/components/ui/SessionTimer.jsx b/src/components/ui/SessionTimer.jsx new file mode 100644 index 0000000..3954de1 --- /dev/null +++ b/src/components/ui/SessionTimer.jsx @@ -0,0 +1,45 @@ +const React = window.React; + +const SessionTimer = ({ timeLeft, sessionType }) => { + // Отладочная информация + console.log('SessionTimer render:', { timeLeft, sessionType }); + + if (!timeLeft || timeLeft <= 0) { + console.log('SessionTimer: no time left, not rendering'); + return null; + } + + const totalMinutes = Math.floor(timeLeft / (60 * 1000)); + const isWarning = totalMinutes <= 10; + const isCritical = totalMinutes <= 5; + + const formatTime = (ms) => { + const hours = Math.floor(ms / (60 * 60 * 1000)); + const minutes = Math.floor((ms % (60 * 60 * 1000)) / (60 * 1000)); + const seconds = Math.floor((ms % (60 * 1000)) / 1000); + + if (hours > 0) { + return `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; + } else { + return `${minutes}:${seconds.toString().padStart(2, '0')}`; + } + }; + + return React.createElement('div', { + className: `session-timer ${isCritical ? 'critical' : isWarning ? 'warning' : ''}` + }, [ + React.createElement('i', { + key: 'icon', + className: 'fas fa-clock' + }), + React.createElement('span', { + key: 'time' + }, formatTime(timeLeft)), + React.createElement('span', { + key: 'type', + className: 'text-xs opacity-80' + }, sessionType?.toUpperCase() || '') + ]); +}; + +window.SessionTimer = SessionTimer; \ No newline at end of file diff --git a/src/components/ui/SessionTypeSelector.jsx b/src/components/ui/SessionTypeSelector.jsx new file mode 100644 index 0000000..9e1245b --- /dev/null +++ b/src/components/ui/SessionTypeSelector.jsx @@ -0,0 +1,110 @@ +const React = window.React; + +const SessionTypeSelector = ({ onSelectType, onCancel }) => { + const [selectedType, setSelectedType] = React.useState(null); + + const sessionTypes = [ + { + id: 'free', + name: 'Бесплатная', + duration: '1 минута', + price: '0 сат', + usd: '$0.00', + popular: true + }, + { + id: 'basic', + name: 'Базовая', + duration: '1 час', + price: '500 сат', + usd: '$0.20' + }, + { + id: 'premium', + name: 'Премиум', + duration: '4 часа', + price: '1000 сат', + usd: '$0.40', + popular: true + }, + { + id: 'extended', + name: 'Расширенная', + duration: '24 часа', + price: '2000 сат', + usd: '$0.80' + } + ]; + + return React.createElement('div', { className: 'space-y-6' }, [ + React.createElement('div', { key: 'header', className: 'text-center' }, [ + React.createElement('h3', { + key: 'title', + className: 'text-xl font-semibold text-white mb-2' + }, 'Выберите тариф'), + React.createElement('p', { + key: 'subtitle', + className: 'text-gray-300 text-sm' + }, 'Оплатите через Lightning Network для доступа к чату') + ]), + + React.createElement('div', { key: 'types', className: 'space-y-3' }, + sessionTypes.map(type => + React.createElement('div', { + key: type.id, + onClick: () => setSelectedType(type.id), + className: `card-minimal rounded-lg p-4 cursor-pointer border-2 transition-all ${ + selectedType === type.id ? 'border-orange-500 bg-orange-500/10' : 'border-gray-600 hover:border-orange-400' + } ${type.popular ? 'relative' : ''}` + }, [ + type.popular && React.createElement('div', { + key: 'badge', + className: 'absolute -top-2 right-3 bg-orange-500 text-white text-xs px-2 py-1 rounded-full' + }, 'Популярный'), + + React.createElement('div', { key: 'content', className: 'flex items-center justify-between' }, [ + React.createElement('div', { key: 'info' }, [ + React.createElement('h4', { + key: 'name', + className: 'text-lg font-semibold text-white' + }, type.name), + React.createElement('p', { + key: 'duration', + className: 'text-gray-300 text-sm' + }, type.duration) + ]), + React.createElement('div', { key: 'pricing', className: 'text-right' }, [ + React.createElement('div', { + key: 'sats', + className: 'text-lg font-bold text-orange-400' + }, type.price), + React.createElement('div', { + key: 'usd', + className: 'text-xs text-gray-400' + }, type.usd) + ]) + ]) + ]) + ) + ), + + React.createElement('div', { key: 'buttons', className: 'flex space-x-3' }, [ + React.createElement('button', { + key: 'continue', + onClick: () => selectedType && onSelectType(selectedType), + disabled: !selectedType, + className: 'flex-1 lightning-button text-white py-3 px-4 rounded-lg font-medium disabled:opacity-50' + }, [ + React.createElement('i', { className: 'fas fa-bolt mr-2' }), + 'Продолжить к оплате' + ]), + React.createElement('button', { + key: 'cancel', + onClick: onCancel, + className: 'px-6 py-3 bg-gray-600 hover:bg-gray-500 text-white rounded-lg' + }, 'Отмена') + ]) + ]); +}; + +window.SessionTypeSelector = SessionTypeSelector; \ No newline at end of file diff --git a/src/crypto/EnhancedSecureCryptoUtils.js b/src/crypto/EnhancedSecureCryptoUtils.js new file mode 100644 index 0000000..ab1ac88 --- /dev/null +++ b/src/crypto/EnhancedSecureCryptoUtils.js @@ -0,0 +1,1883 @@ +class EnhancedSecureCryptoUtils { + + static _keyMetadata = new WeakMap(); + + // Utility to sort object keys for deterministic serialization + static sortObjectKeys(obj) { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + + if (Array.isArray(obj)) { + return obj.map(EnhancedSecureCryptoUtils.sortObjectKeys); + } + + const sortedObj = {}; + Object.keys(obj).sort().forEach(key => { + sortedObj[key] = EnhancedSecureCryptoUtils.sortObjectKeys(obj[key]); + }); + return sortedObj; + } + + // Utility to assert CryptoKey type and properties + static assertCryptoKey(key, expectedName = null, expectedUsages = []) { + if (!(key instanceof CryptoKey)) throw new Error('Expected CryptoKey'); + if (expectedName && key.algorithm?.name !== expectedName) { + throw new Error(`Expected algorithm ${expectedName}, got ${key.algorithm?.name}`); + } + for (const u of expectedUsages) { + if (!key.usages || !key.usages.includes(u)) { + throw new Error(`Missing required key usage: ${u}`); + } + } + } + // Helper function to convert ArrayBuffer to Base64 + static arrayBufferToBase64(buffer) { + let binary = ''; + const bytes = new Uint8Array(buffer); + const len = bytes.byteLength; + for (let i = 0; i < len; i++) { + binary += String.fromCharCode(bytes[i]); + } + return btoa(binary); + } + + // Helper function to convert Base64 to ArrayBuffer + static base64ToArrayBuffer(base64) { + const binaryString = atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + } + + static async encryptData(data, password) { + try { + const dataString = typeof data === 'string' ? data : JSON.stringify(data); + const salt = crypto.getRandomValues(new Uint8Array(16)); + const encoder = new TextEncoder(); + const passwordBuffer = encoder.encode(password); + + const keyMaterial = await crypto.subtle.importKey( + 'raw', + passwordBuffer, + { name: 'PBKDF2' }, + false, + ['deriveKey'] + ); + + const key = await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt, + iterations: 100000, + hash: 'SHA-256', + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['encrypt'] + ); + + const iv = crypto.getRandomValues(new Uint8Array(12)); + const dataBuffer = encoder.encode(dataString); + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: iv }, + key, + dataBuffer + ); + + const encryptedPackage = { + version: '1.0', + salt: Array.from(salt), + iv: Array.from(iv), + data: Array.from(new Uint8Array(encrypted)), + timestamp: Date.now(), + }; + + const packageString = JSON.stringify(encryptedPackage); + return EnhancedSecureCryptoUtils.arrayBufferToBase64(new TextEncoder().encode(packageString).buffer); + + } catch (error) { + console.error('Encryption failed:', error); + throw new Error(`Encryption error: ${error.message}`); + } + } + + static async decryptData(encryptedData, password) { + try { + const packageBuffer = EnhancedSecureCryptoUtils.base64ToArrayBuffer(encryptedData); + const packageString = new TextDecoder().decode(packageBuffer); + const encryptedPackage = JSON.parse(packageString); + + if (!encryptedPackage.version || !encryptedPackage.salt || !encryptedPackage.iv || !encryptedPackage.data) { + throw new Error('Invalid encrypted data format'); + } + + const salt = new Uint8Array(encryptedPackage.salt); + const iv = new Uint8Array(encryptedPackage.iv); + const encrypted = new Uint8Array(encryptedPackage.data); + + const encoder = new TextEncoder(); + const passwordBuffer = encoder.encode(password); + + const keyMaterial = await crypto.subtle.importKey( + 'raw', + passwordBuffer, + { name: 'PBKDF2' }, + false, + ['deriveKey'] + ); + + const key = await crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt: salt, + iterations: 100000, + hash: 'SHA-256' + }, + keyMaterial, + { name: 'AES-GCM', length: 256 }, + false, + ['decrypt'] + ); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + key, + encrypted + ); + + const decryptedString = new TextDecoder().decode(decrypted); + + try { + return JSON.parse(decryptedString); + } catch { + return decryptedString; + } + + } catch (error) { + console.error('Decryption failed:', error); + throw new Error(`Decryption error: ${error.message}`); + } + } + + + // Generate secure password for data exchange + static generateSecurePassword() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + const randomValues = new Uint32Array(16); + crypto.getRandomValues(randomValues); + + let password = ''; + for (let i = 0; i < 16; i++) { + password += chars[randomValues[i] % chars.length]; + } + return password; + } + + // Real security level calculation with actual verification + static async calculateSecurityLevel(securityManager) { + let score = 0; + const maxScore = 110; // Increased for PFS + const verificationResults = {}; + + try { + // Fallback to basic calculation if securityManager is not fully initialized + if (!securityManager || !securityManager.securityFeatures) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'Security manager not fully initialized, using fallback calculation'); + return { + level: 'INITIALIZING', + score: 35, + color: 'yellow', + verificationResults: {}, + timestamp: Date.now(), + details: 'Security system initializing...' + }; + } + // 1. Base encryption verification (20 points) + try { + if (await EnhancedSecureCryptoUtils.verifyEncryption(securityManager)) { + score += 20; + verificationResults.encryption = { passed: true, details: 'AES-GCM encryption verified' }; + } else { + verificationResults.encryption = { passed: false, details: 'Encryption not working' }; + } + } catch (error) { + verificationResults.encryption = { passed: false, details: `Encryption check failed: ${error.message}` }; + } + + // 2. ECDH key exchange verification (15 points) + try { + if (await EnhancedSecureCryptoUtils.verifyECDHKeyExchange(securityManager)) { + score += 15; + verificationResults.ecdh = { passed: true, details: 'ECDH key exchange verified' }; + } else { + verificationResults.ecdh = { passed: false, details: 'ECDH key exchange failed' }; + } + } catch (error) { + verificationResults.ecdh = { passed: false, details: `ECDH check failed: ${error.message}` }; + } + + // 3. ECDSA signatures verification (15 points) + if (await EnhancedSecureCryptoUtils.verifyECDSASignatures(securityManager)) { + score += 15; + verificationResults.ecdsa = { passed: true, details: 'ECDSA signatures verified' }; + } else { + verificationResults.ecdsa = { passed: false, details: 'ECDSA signatures failed' }; + } + + // 4. Mutual authentication verification (10 points) + if (await EnhancedSecureCryptoUtils.verifyMutualAuth(securityManager)) { + score += 10; + verificationResults.mutualAuth = { passed: true, details: 'Mutual authentication verified' }; + } else { + verificationResults.mutualAuth = { passed: false, details: 'Mutual authentication failed' }; + } + + // 5. Metadata protection verification (10 points) + if (await EnhancedSecureCryptoUtils.verifyMetadataProtection(securityManager)) { + score += 10; + verificationResults.metadataProtection = { passed: true, details: 'Metadata protection verified' }; + } else { + verificationResults.metadataProtection = { passed: false, details: 'Metadata protection failed' }; + } + + // 6. Enhanced replay protection verification (10 points) + if (await EnhancedSecureCryptoUtils.verifyReplayProtection(securityManager)) { + score += 10; + verificationResults.replayProtection = { passed: true, details: 'Replay protection verified' }; + } else { + verificationResults.replayProtection = { passed: false, details: 'Replay protection failed' }; + } + + // 7. Non-extractable keys verification (10 points) + if (await EnhancedSecureCryptoUtils.verifyNonExtractableKeys(securityManager)) { + score += 10; + verificationResults.nonExtractableKeys = { passed: true, details: 'Non-extractable keys verified' }; + } else { + verificationResults.nonExtractableKeys = { passed: false, details: 'Keys are extractable' }; + } + + // 8. Rate limiting verification (5 points) + if (await EnhancedSecureCryptoUtils.verifyRateLimiting(securityManager)) { + score += 5; + verificationResults.rateLimiting = { passed: true, details: 'Rate limiting active' }; + } else { + verificationResults.rateLimiting = { passed: false, details: 'Rate limiting not working' }; + } + + // 9. Enhanced validation verification (5 points) + if (await EnhancedSecureCryptoUtils.verifyEnhancedValidation(securityManager)) { + score += 5; + verificationResults.enhancedValidation = { passed: true, details: 'Enhanced validation active' }; + } else { + verificationResults.enhancedValidation = { passed: false, details: 'Enhanced validation failed' }; + } + + // 10. Perfect Forward Secrecy verification (10 points) + if (await EnhancedSecureCryptoUtils.verifyPFS(securityManager)) { + score += 10; + verificationResults.pfs = { passed: true, details: 'Perfect Forward Secrecy active' }; + } else { + verificationResults.pfs = { passed: false, details: 'PFS not active' }; + } + + const percentage = Math.round((score / maxScore) * 100); + + const result = { + level: percentage >= 80 ? 'HIGH' : percentage >= 50 ? 'MEDIUM' : 'LOW', + score: percentage, + color: percentage >= 80 ? 'green' : percentage >= 50 ? 'yellow' : 'red', + verificationResults, + timestamp: Date.now(), + details: `Real verification: ${score}/${maxScore} security checks passed` + }; + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Real security level calculated', { + score: percentage, + level: result.level, + passedChecks: Object.values(verificationResults).filter(r => r.passed).length, + totalChecks: Object.keys(verificationResults).length + }); + + return result; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Security level calculation failed', { error: error.message }); + return { + level: 'UNKNOWN', + score: 0, + color: 'red', + verificationResults: {}, + timestamp: Date.now(), + details: `Verification failed: ${error.message}` + }; + } + } + + // Real verification functions + static async verifyEncryption(securityManager) { + try { + if (!securityManager.encryptionKey) return false; + + // Test actual encryption/decryption + const testData = 'Test encryption verification'; + const encoder = new TextEncoder(); + const testBuffer = encoder.encode(testData); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + securityManager.encryptionKey, + testBuffer + ); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + securityManager.encryptionKey, + encrypted + ); + + const decryptedText = new TextDecoder().decode(decrypted); + return decryptedText === testData; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Encryption verification failed', { error: error.message }); + return false; + } + } + + static async verifyECDHKeyExchange(securityManager) { + try { + if (!securityManager.ecdhKeyPair || !securityManager.ecdhKeyPair.privateKey || !securityManager.ecdhKeyPair.publicKey) { + return false; + } + + // Test that keys are actually ECDH keys + const keyType = securityManager.ecdhKeyPair.privateKey.algorithm.name; + const curve = securityManager.ecdhKeyPair.privateKey.algorithm.namedCurve; + + return keyType === 'ECDH' && (curve === 'P-384' || curve === 'P-256'); + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH verification failed', { error: error.message }); + return false; + } + } + + static async verifyECDSASignatures(securityManager) { + try { + if (!securityManager.ecdsaKeyPair || !securityManager.ecdsaKeyPair.privateKey || !securityManager.ecdsaKeyPair.publicKey) { + return false; + } + + // Test actual signing and verification + const testData = 'Test ECDSA signature verification'; + const encoder = new TextEncoder(); + const testBuffer = encoder.encode(testData); + + const signature = await crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-384' }, + securityManager.ecdsaKeyPair.privateKey, + testBuffer + ); + + const isValid = await crypto.subtle.verify( + { name: 'ECDSA', hash: 'SHA-384' }, + securityManager.ecdsaKeyPair.publicKey, + signature, + testBuffer + ); + + return isValid; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDSA verification failed', { error: error.message }); + return false; + } + } + + static async verifyMutualAuth(securityManager) { + try { + // Check if mutual authentication challenge was created and processed + return securityManager.isVerified === true; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Mutual auth verification failed', { error: error.message }); + return false; + } + } + + static async verifyMetadataProtection(securityManager) { + try { + if (!securityManager.metadataKey) return false; + + // Test metadata encryption/decryption + const testMetadata = { test: 'metadata', timestamp: Date.now() }; + const encoder = new TextEncoder(); + const testBuffer = encoder.encode(JSON.stringify(testMetadata)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encrypted = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv }, + securityManager.metadataKey, + testBuffer + ); + + const decrypted = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv }, + securityManager.metadataKey, + encrypted + ); + + const decryptedMetadata = JSON.parse(new TextDecoder().decode(decrypted)); + return decryptedMetadata.test === testMetadata.test; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Metadata protection verification failed', { error: error.message }); + return false; + } + } + + static async verifyReplayProtection(securityManager) { + try { + // Check if replay protection mechanisms are in place + return securityManager.processedMessageIds && + typeof securityManager.processedMessageIds.has === 'function' && + securityManager.sequenceNumber !== undefined; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Replay protection verification failed', { error: error.message }); + return false; + } + } + + static async verifyNonExtractableKeys(securityManager) { + try { + // Check that keys are non-extractable + if (securityManager.ecdhKeyPair && securityManager.ecdhKeyPair.privateKey) { + return securityManager.ecdhKeyPair.privateKey.extractable === false; + } + return false; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Non-extractable keys verification failed', { error: error.message }); + return false; + } + } + + static async verifyRateLimiting(securityManager) { + try { + // Check if rate limiting is active + return securityManager.rateLimiterId && + EnhancedSecureCryptoUtils.rateLimiter && + typeof EnhancedSecureCryptoUtils.rateLimiter.checkMessageRate === 'function'; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Rate limiting verification failed', { error: error.message }); + return false; + } + } + + static async verifyEnhancedValidation(securityManager) { + try { + // Check if enhanced validation is active + return securityManager.sessionSalt && + securityManager.sessionSalt.length === 64 && + securityManager.keyFingerprint; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced validation verification failed', { error: error.message }); + return false; + } + } + + static async verifyPFS(securityManager) { + try { + // Check if PFS is active + return securityManager.securityFeatures && + securityManager.securityFeatures.hasPFS === true && + securityManager.keyRotationInterval && + securityManager.currentKeyVersion !== undefined && + securityManager.keyVersions && + securityManager.keyVersions instanceof Map; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'PFS verification failed', { error: error.message }); + return false; + } + } + + // Rate limiting implementation + static rateLimiter = { + messages: new Map(), + connections: new Map(), + + checkMessageRate(identifier, limit = 60, windowMs = 60000) { + const now = Date.now(); + const key = `msg_${identifier}`; + + if (!this.messages.has(key)) { + this.messages.set(key, []); + } + + const timestamps = this.messages.get(key); + + // Remove old timestamps + const validTimestamps = timestamps.filter(ts => now - ts < windowMs); + this.messages.set(key, validTimestamps); + + if (validTimestamps.length >= limit) { + return false; // Rate limit exceeded + } + + validTimestamps.push(now); + return true; + }, + + checkConnectionRate(identifier, limit = 5, windowMs = 300000) { + const now = Date.now(); + const key = `conn_${identifier}`; + + if (!this.connections.has(key)) { + this.connections.set(key, []); + } + + const timestamps = this.connections.get(key); + const validTimestamps = timestamps.filter(ts => now - ts < windowMs); + this.connections.set(key, validTimestamps); + + if (validTimestamps.length >= limit) { + return false; + } + + validTimestamps.push(now); + return true; + }, + + cleanup() { + const now = Date.now(); + const maxAge = 3600000; // 1 hour + + for (const [key, timestamps] of this.messages.entries()) { + const valid = timestamps.filter(ts => now - ts < maxAge); + if (valid.length === 0) { + this.messages.delete(key); + } else { + this.messages.set(key, valid); + } + } + + for (const [key, timestamps] of this.connections.entries()) { + const valid = timestamps.filter(ts => now - ts < maxAge); + if (valid.length === 0) { + this.connections.delete(key); + } else { + this.connections.set(key, valid); + } + } + } + }; + + // Secure logging without data leaks + static secureLog = { + logs: [], + maxLogs: 100, + + log(level, message, context = {}) { + const sanitizedContext = this.sanitizeContext(context); + const logEntry = { + timestamp: Date.now(), + level, + message, + context: sanitizedContext, + id: crypto.getRandomValues(new Uint32Array(1))[0] + }; + + this.logs.push(logEntry); + + // Keep only recent logs + if (this.logs.length > this.maxLogs) { + this.logs = this.logs.slice(-this.maxLogs); + } + + // Console output for development + if (level === 'error') { + console.error(`[SecureChat] ${message}`, sanitizedContext); + } else if (level === 'warn') { + console.warn(`[SecureChat] ${message}`, sanitizedContext); + } else { + console.log(`[SecureChat] ${message}`, sanitizedContext); + } + }, + + sanitizeContext(context) { + const sanitized = {}; + for (const [key, value] of Object.entries(context)) { + if (key.toLowerCase().includes('key') || + key.toLowerCase().includes('secret') || + key.toLowerCase().includes('password') || + key.toLowerCase().includes('token')) { + sanitized[key] = '[REDACTED]'; + } else if (typeof value === 'string' && value.length > 100) { + sanitized[key] = value.substring(0, 100) + '...[TRUNCATED]'; + } else { + sanitized[key] = value; + } + } + return sanitized; + }, + + getLogs(level = null) { + if (level) { + return this.logs.filter(log => log.level === level); + } + return [...this.logs]; + }, + + clearLogs() { + this.logs = []; + } + }; + + // Generate ECDH key pair for secure key exchange (non-extractable) with fallback + static async generateECDHKeyPair() { + try { + // Try P-384 first + try { + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-384' + }, + false, // Non-extractable for enhanced security + ['deriveKey'] + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDH key pair generated successfully (P-384)', { + curve: 'P-384', + extractable: false + }); + + return keyPair; + } catch (p384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 generation failed, trying P-256', { error: p384Error.message }); + + // Fallback to P-256 + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: 'P-256' + }, + false, // Non-extractable for enhanced security + ['deriveKey'] + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDH key pair generated successfully (P-256 fallback)', { + curve: 'P-256', + extractable: false + }); + + return keyPair; + } + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDH key generation failed', { error: error.message }); + throw new Error('Не удалось создать ключи для безопасного обмена'); + } + } + + // Generate ECDSA key pair for digital signatures with fallback + static async generateECDSAKeyPair() { + try { + // Try P-384 first + try { + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-384' + }, + false, // Non-extractable for enhanced security + ['sign', 'verify'] + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA key pair generated successfully (P-384)', { + curve: 'P-384', + extractable: false + }); + + return keyPair; + } catch (p384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 generation failed, trying P-256', { error: p384Error.message }); + + // Fallback to P-256 + const keyPair = await crypto.subtle.generateKey( + { + name: 'ECDSA', + namedCurve: 'P-256' + }, + false, // Non-extractable for enhanced security + ['sign', 'verify'] + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA key pair generated successfully (P-256 fallback)', { + curve: 'P-256', + extractable: false + }); + + return keyPair; + } + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'ECDSA key generation failed', { error: error.message }); + throw new Error('Не удалось создать ключи для цифровых подписей'); + } + } + + // Sign data with ECDSA (P-384 or P-256) + static async signData(privateKey, data) { + try { + const encoder = new TextEncoder(); + const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data; + + // Try SHA-384 first, fallback to SHA-256 + try { + const signature = await crypto.subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-384' + }, + privateKey, + dataBuffer + ); + + return Array.from(new Uint8Array(signature)); + } catch (sha384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 signing failed, trying SHA-256', { error: sha384Error.message }); + + const signature = await crypto.subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-256' + }, + privateKey, + dataBuffer + ); + + return Array.from(new Uint8Array(signature)); + } + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Data signing failed', { error: error.message }); + throw new Error('Не удалось подписать данные'); + } + } + + // Verify ECDSA signature (P-384 or P-256) + static async verifySignature(publicKey, signature, data) { + try { + const encoder = new TextEncoder(); + const dataBuffer = typeof data === 'string' ? encoder.encode(data) : data; + const signatureBuffer = new Uint8Array(signature); + + // Try SHA-384 first, fallback to SHA-256 + try { + const isValid = await crypto.subtle.verify( + { + name: 'ECDSA', + hash: 'SHA-384' + }, + publicKey, + signatureBuffer, + dataBuffer + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Signature verification completed (SHA-384)', { + isValid, + dataSize: dataBuffer.length + }); + + return isValid; + } catch (sha384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 verification failed, trying SHA-256', { error: sha384Error.message }); + + const isValid = await crypto.subtle.verify( + { + name: 'ECDSA', + hash: 'SHA-256' + }, + publicKey, + signatureBuffer, + dataBuffer + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Signature verification completed (SHA-256 fallback)', { + isValid, + dataSize: dataBuffer.length + }); + + return isValid; + } + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Signature verification failed', { error: error.message }); + throw new Error('Не удалось проверить цифровую подпись'); + } + } + + // Enhanced DER/SPKI validation with improved error handling + static async validateKeyStructure(keyData, expectedAlgorithm = 'ECDH') { + try { + if (!Array.isArray(keyData) || keyData.length === 0) { + throw new Error('Invalid key data format'); + } + + const keyBytes = new Uint8Array(keyData); + + // Basic DER check + if (keyBytes[0] !== 0x30) { + throw new Error('Invalid DER structure - missing SEQUENCE tag'); + } + + if (keyBytes.length > 2000) { // более жёсткая граница + throw new Error('Key data too long - possible attack'); + } + + // Try to import; await the promise + const alg = (expectedAlgorithm === 'ECDSA' || expectedAlgorithm === 'ECDH') + ? { name: expectedAlgorithm, namedCurve: 'P-384' } + : { name: expectedAlgorithm }; + + await crypto.subtle.importKey('spki', keyBytes.buffer, alg, false, expectedAlgorithm === 'ECDSA' ? ['verify'] : []); + EnhancedSecureCryptoUtils.secureLog.log('info', 'Key structure validation passed', { keyLen: keyBytes.length }); + return true; + } catch (err) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Key structure validation failed', { short: err.message }); + throw new Error('Invalid key structure'); + } + } + + // Export public key for transmission with signature + static async exportPublicKeyWithSignature(publicKey, signingKey, keyType = 'ECDH') { + try { + // Validate key type + if (!['ECDH', 'ECDSA'].includes(keyType)) { + throw new Error('Invalid key type'); + } + + const exported = await crypto.subtle.exportKey('spki', publicKey); + const keyData = Array.from(new Uint8Array(exported)); + + // Validate exported key structure + await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType); + + // Create signed key package + const keyPackage = { + keyType, + keyData, + timestamp: Date.now(), + version: '4.0' + }; + + // Sign the key package + const packageString = JSON.stringify(keyPackage); + const signature = await EnhancedSecureCryptoUtils.signData(signingKey, packageString); + + const signedPackage = { + ...keyPackage, + signature + }; + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Public key exported with signature', { + keyType, + keySize: keyData.length, + signed: true + }); + + return signedPackage; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key export failed', { + error: error.message, + keyType + }); + throw new Error(`Не удалось экспортировать ${keyType} ключ: ${error.message}`); + } + } + + // Import and verify signed public key + static async importSignedPublicKey(signedPackage, verifyingKey, expectedKeyType = 'ECDH') { + try { + // Validate package structure + if (!signedPackage || typeof signedPackage !== 'object') { + throw new Error('Invalid signed package format'); + } + + const { keyType, keyData, timestamp, version, signature } = signedPackage; + + if (!keyType || !keyData || !timestamp || !signature) { + throw new Error('Missing required fields in signed package'); + } + + if (keyType !== expectedKeyType) { + throw new Error(`Key type mismatch: expected ${expectedKeyType}, got ${keyType}`); + } + + // Check timestamp (reject keys older than 1 hour) + const keyAge = Date.now() - timestamp; + if (keyAge > 3600000) { + throw new Error('Signed key package is too old'); + } + + // Validate key structure + await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, keyType); + + // Verify signature + const packageCopy = { keyType, keyData, timestamp, version }; + const packageString = JSON.stringify(packageCopy); + const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signature, packageString); + + if (!isValidSignature) { + throw new Error('Invalid signature on key package - possible MITM attack'); + } + + // Import the key + const keyBytes = new Uint8Array(keyData); + const algorithm = keyType === 'ECDH' ? + { name: 'ECDH', namedCurve: 'P-384' } : + { name: 'ECDSA', namedCurve: 'P-384' }; + + const keyUsages = keyType === 'ECDH' ? [] : ['verify']; + + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + algorithm, + false, // Non-extractable + keyUsages + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Signed public key imported successfully', { + keyType, + signatureValid: true, + keyAge: Math.round(keyAge / 1000) + 's' + }); + + return publicKey; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed public key import failed', { + error: error.message, + expectedKeyType + }); + throw new Error(`Не удалось импортировать подписанный ключ: ${error.message}`); + } + } + + // Legacy export for backward compatibility + static async exportPublicKey(publicKey) { + try { + const exported = await crypto.subtle.exportKey('spki', publicKey); + const keyData = Array.from(new Uint8Array(exported)); + + // Validate exported key + await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH'); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key exported', { keySize: keyData.length }); + return keyData; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key export failed', { error: error.message }); + throw new Error('Не удалось экспортировать публичный ключ'); + } + } + + // Legacy import for backward compatibility with fallback + static async importPublicKey(keyData) { + try { + await EnhancedSecureCryptoUtils.validateKeyStructure(keyData, 'ECDH'); + + const keyBytes = new Uint8Array(keyData); + + // Try P-384 first + try { + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + { + name: 'ECDH', + namedCurve: 'P-384' + }, + false, // Non-extractable + [] + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key imported (P-384)', { keySize: keyData.length }); + return publicKey; + } catch (p384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message }); + + // Fallback to P-256 + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + { + name: 'ECDH', + namedCurve: 'P-256' + }, + false, // Non-extractable + [] + ); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy public key imported (P-256 fallback)', { keySize: keyData.length }); + return publicKey; + } + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Legacy public key import failed', { error: error.message }); + throw new Error('Не удалось импортировать публичный ключ'); + } + } + + // Helper method for unsafe import (should only be used in testing/debugging) + static async _importKeyUnsafe(signedPackage) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'UNSAFE KEY IMPORT - This should never happen in production', { + keyType: signedPackage.keyType, + keySize: signedPackage.keyData.length, + securityRisk: 'CRITICAL' + }); + + const keyBytes = new Uint8Array(signedPackage.keyData); + const keyType = signedPackage.keyType || 'ECDH'; + + // Try P-384 first + try { + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + { + name: keyType, + namedCurve: 'P-384' + }, + false, + [] + ); + + return publicKey; + } catch (p384Error) { + // Fallback to P-256 + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + { + name: keyType, + namedCurve: 'P-256' + }, + false, + [] + ); + + return publicKey; + } + } + + // Method to check if a key is trusted + static isKeyTrusted(keyOrFingerprint) { + if (keyOrFingerprint instanceof CryptoKey) { + const meta = EnhancedSecureCryptoUtils._keyMetadata.get(keyOrFingerprint); + return meta ? meta.trusted === true : false; + } else if (keyOrFingerprint && keyOrFingerprint._securityMetadata) { + // Check by key metadata + return keyOrFingerprint._securityMetadata.trusted === true; + } + + // If we can't determine trust status, assume untrusted for safety + return false; + } + + + // Import public key from signed package with MANDATORY verification + static async importPublicKeyFromSignedPackage(signedPackage, verifyingKey = null, options = {}) { + try { + if (!signedPackage || !signedPackage.keyData || !signedPackage.signature) { + throw new Error('Неверный формат подписанного пакета ключа'); + } + + // Validate all required fields are present + const requiredFields = ['keyData', 'signature', 'keyType', 'timestamp', 'version']; + const missingFields = requiredFields.filter(field => !signedPackage[field]); + + if (missingFields.length > 0) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Missing required fields in signed package', { + missingFields: missingFields, + availableFields: Object.keys(signedPackage) + }); + throw new Error(`Отсутствуют обязательные поля в подписанном пакете: ${missingFields.join(', ')}`); + } + + // SECURITY ENHANCEMENT: MANDATORY signature verification for signed packages + if (!verifyingKey) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY VIOLATION: Signed package received without verifying key', { + keyType: signedPackage.keyType, + keySize: signedPackage.keyData.length, + timestamp: signedPackage.timestamp, + version: signedPackage.version, + securityRisk: 'HIGH - Potential MITM attack vector' + }); + + // Check if insecure mode is explicitly allowed (for debugging/testing only) + if (options.allowInsecureImport === true && options.explicitWarningAcknowledged === true) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'INSECURE MODE: Importing signed package without verification (DANGEROUS)', { + keyType: signedPackage.keyType, + securityLevel: 'COMPROMISED', + recommendation: 'This mode should NEVER be used in production' + }); + + // Continue with insecure import but mark the key as untrusted + const key = await EnhancedSecureCryptoUtils._importKeyUnsafe(signedPackage); + + // Use WeakMap to store metadata + EnhancedSecureCryptoUtils._keyMetadata.set(key, { + trusted: false, + verificationStatus: 'UNVERIFIED_DANGEROUS', + verificationTimestamp: Date.now() + }); + + return key; + } + + // REJECT the signed package if no verifying key provided + throw new Error('КРИТИЧЕСКАЯ ОШИБКА БЕЗОПАСНОСТИ: Подписанный пакет ключа получен без ключа проверки. ' + + 'Это может указывать на попытку MITM-атаки. Импорт отклонен для обеспечения безопасности.'); + } + + // Validate key structure + await EnhancedSecureCryptoUtils.validateKeyStructure(signedPackage.keyData, signedPackage.keyType || 'ECDH'); + + // MANDATORY signature verification when verifyingKey is provided + const packageCopy = { ...signedPackage }; + delete packageCopy.signature; + const packageString = JSON.stringify(packageCopy); + const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(verifyingKey, signedPackage.signature, packageString); + + if (!isValidSignature) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'SECURITY BREACH: Invalid signature detected - MITM attack prevented', { + keyType: signedPackage.keyType, + keySize: signedPackage.keyData.length, + timestamp: signedPackage.timestamp, + version: signedPackage.version, + attackPrevented: true + }); + throw new Error('КРИТИЧЕСКАЯ ОШИБКА БЕЗОПАСНОСТИ: Недействительная подпись ключа обнаружена. ' + + 'Это указывает на попытку MITM-атаки. Импорт ключа отклонен.'); + } + + // Additional MITM protection: Check for key reuse and suspicious patterns + const keyFingerprint = await EnhancedSecureCryptoUtils.calculateKeyFingerprint(signedPackage.keyData); + + // Log successful verification with security details + EnhancedSecureCryptoUtils.secureLog.log('info', 'SECURE: Signature verification passed for signed package', { + keyType: signedPackage.keyType, + keySize: signedPackage.keyData.length, + timestamp: signedPackage.timestamp, + version: signedPackage.version, + signatureVerified: true, + securityLevel: 'HIGH', + keyFingerprint: keyFingerprint.substring(0, 8) // Only log first 8 chars for security + }); + + // Import the public key with fallback + const keyBytes = new Uint8Array(signedPackage.keyData); + const keyType = signedPackage.keyType || 'ECDH'; + + // Try P-384 first + try { + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + { + name: keyType, + namedCurve: 'P-384' + }, + false, // Non-extractable + [] + ); + + // Use WeakMap to store metadata + EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, { + trusted: true, + verificationStatus: 'VERIFIED_SECURE', + verificationTimestamp: Date.now() + }); + + return publicKey; + } catch (p384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'P-384 import failed, trying P-256', { error: p384Error.message }); + + // Fallback to P-256 + const publicKey = await crypto.subtle.importKey( + 'spki', + keyBytes, + { + name: keyType, + namedCurve: 'P-256' + }, + false, // Non-extractable + [] + ); + + // Use WeakMap to store metadata + EnhancedSecureCryptoUtils._keyMetadata.set(publicKey, { + trusted: true, + verificationStatus: 'VERIFIED_SECURE', + verificationTimestamp: Date.now() + }); + + return publicKey; + } + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Signed package key import failed', { + error: error.message, + securityImplications: 'Potential security breach prevented' + }); + throw new Error(`Не удалось импортировать публичный ключ из подписанного пакета: ${error.message}`); + } + } + + // Enhanced key derivation with metadata protection and 64-byte salt + static async deriveSharedKeys(privateKey, publicKey, salt) { + try { + // Validate input parameters are CryptoKey instances + if (!(privateKey instanceof CryptoKey)) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Private key is not a CryptoKey', { + privateKeyType: typeof privateKey, + privateKeyAlgorithm: privateKey?.algorithm?.name + }); + throw new Error('Приватный ключ не является CryptoKey'); + } + + if (!(publicKey instanceof CryptoKey)) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key is not a CryptoKey', { + publicKeyType: typeof publicKey, + publicKeyAlgorithm: publicKey?.algorithm?.name + }); + throw new Error('Публичный ключ не является CryptoKey'); + } + + // Validate salt size (should be 64 bytes for enhanced security) + if (!salt || salt.length !== 64) { + throw new Error('Salt must be exactly 64 bytes for enhanced security'); + } + + const saltBytes = new Uint8Array(salt); + const encoder = new TextEncoder(); + + // Enhanced context info with version and additional entropy + const contextInfo = encoder.encode('LockBit.chat v4.0 Enhanced Security Edition'); + + // Derive master shared secret with enhanced parameters + // Try SHA-384 first, fallback to SHA-256 + let sharedSecret; + try { + sharedSecret = await crypto.subtle.deriveKey( + { + name: 'ECDH', + public: publicKey + }, + privateKey, + { + name: 'HKDF', + hash: 'SHA-384', + salt: saltBytes, + info: contextInfo + }, + false, // Non-extractable + ['deriveKey'] + ); + } catch (sha384Error) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'SHA-384 key derivation failed, trying SHA-256', { + error: sha384Error.message, + privateKeyType: typeof privateKey, + publicKeyType: typeof publicKey, + privateKeyAlgorithm: privateKey?.algorithm?.name, + publicKeyAlgorithm: publicKey?.algorithm?.name + }); + + sharedSecret = await crypto.subtle.deriveKey( + { + name: 'ECDH', + public: publicKey + }, + privateKey, + { + name: 'HKDF', + hash: 'SHA-256', + salt: saltBytes, + info: contextInfo + }, + false, // Non-extractable + ['deriveKey'] + ); + } + + // Derive message encryption key with fallback + let encryptionKey; + try { + encryptionKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-384', + salt: saltBytes, + info: encoder.encode('message-encryption-v4') + }, + sharedSecret, + { + name: 'AES-GCM', + length: 256 + }, + false, // Non-extractable for enhanced security + ['encrypt', 'decrypt'] + ); + } catch (sha384Error) { + encryptionKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: saltBytes, + info: encoder.encode('message-encryption-v4') + }, + sharedSecret, + { + name: 'AES-GCM', + length: 256 + }, + false, // Non-extractable for enhanced security + ['encrypt', 'decrypt'] + ); + } + + // Derive MAC key for message authentication with fallback + let macKey; + try { + macKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-384', + salt: saltBytes, + info: encoder.encode('message-authentication-v4') + }, + sharedSecret, + { + name: 'HMAC', + hash: 'SHA-384' + }, + false, // Non-extractable + ['sign', 'verify'] + ); + } catch (sha384Error) { + macKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: saltBytes, + info: encoder.encode('message-authentication-v4') + }, + sharedSecret, + { + name: 'HMAC', + hash: 'SHA-256' + }, + false, // Non-extractable + ['sign', 'verify'] + ); + } + + // Derive separate metadata encryption key with fallback + let metadataKey; + try { + metadataKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-384', + salt: saltBytes, + info: encoder.encode('metadata-protection-v4') + }, + sharedSecret, + { + name: 'AES-GCM', + length: 256 + }, + false, // Non-extractable + ['encrypt', 'decrypt'] + ); + } catch (sha384Error) { + metadataKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: saltBytes, + info: encoder.encode('metadata-protection-v4') + }, + sharedSecret, + { + name: 'AES-GCM', + length: 256 + }, + false, // Non-extractable + ['encrypt', 'decrypt'] + ); + } + + // Generate temporary extractable key for fingerprint calculation with fallback + let fingerprintKey; + try { + fingerprintKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-384', + salt: saltBytes, + info: encoder.encode('fingerprint-generation-v4') + }, + sharedSecret, + { + name: 'AES-GCM', + length: 256 + }, + true, // Extractable only for fingerprint + ['encrypt', 'decrypt'] + ); + } catch (sha384Error) { + fingerprintKey = await crypto.subtle.deriveKey( + { + name: 'HKDF', + hash: 'SHA-256', + salt: saltBytes, + info: encoder.encode('fingerprint-generation-v4') + }, + sharedSecret, + { + name: 'AES-GCM', + length: 256 + }, + true, // Extractable only for fingerprint + ['encrypt', 'decrypt'] + ); + } + + // Generate key fingerprint for verification + const fingerprintKeyData = await crypto.subtle.exportKey('raw', fingerprintKey); + const fingerprint = await EnhancedSecureCryptoUtils.generateKeyFingerprint(Array.from(new Uint8Array(fingerprintKeyData))); + + // Validate that all derived keys are CryptoKey instances + if (!(encryptionKey instanceof CryptoKey)) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived encryption key is not a CryptoKey', { + encryptionKeyType: typeof encryptionKey, + encryptionKeyAlgorithm: encryptionKey?.algorithm?.name + }); + throw new Error('Производный ключ шифрования не является CryptoKey'); + } + + if (!(macKey instanceof CryptoKey)) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived MAC key is not a CryptoKey', { + macKeyType: typeof macKey, + macKeyAlgorithm: macKey?.algorithm?.name + }); + throw new Error('Производный MAC ключ не является CryptoKey'); + } + + if (!(metadataKey instanceof CryptoKey)) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Derived metadata key is not a CryptoKey', { + metadataKeyType: typeof metadataKey, + metadataKeyAlgorithm: metadataKey?.algorithm?.name + }); + throw new Error('Производный ключ метаданных не является CryptoKey'); + } + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced shared keys derived successfully', { + saltSize: salt.length, + hasMetadataKey: true, + nonExtractable: true, + version: '4.0', + allKeysValid: true + }); + + return { + encryptionKey, + macKey, + metadataKey, + fingerprint, + timestamp: Date.now(), + version: '4.0' + }; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced key derivation failed', { error: error.message }); + throw new Error(`Не удалось создать общие ключи шифрования: ${error.message}`); + } + } + + static async generateKeyFingerprint(keyData) { + const keyBuffer = new Uint8Array(keyData); + const hashBuffer = await crypto.subtle.digest('SHA-384', keyBuffer); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + return hashArray.slice(0, 12).map(b => b.toString(16).padStart(2, '0')).join(':'); + } + + // Generate mutual authentication challenge + static generateMutualAuthChallenge() { + const challenge = crypto.getRandomValues(new Uint8Array(48)); // Increased to 48 bytes + const timestamp = Date.now(); + const nonce = crypto.getRandomValues(new Uint8Array(16)); + + return { + challenge: Array.from(challenge), + timestamp, + nonce: Array.from(nonce), + version: '4.0' + }; + } + + // Create cryptographic proof for mutual authentication + static async createAuthProof(challenge, privateKey, publicKey) { + try { + if (!challenge || !challenge.challenge || !challenge.timestamp || !challenge.nonce) { + throw new Error('Invalid challenge structure'); + } + + // Check challenge age (max 2 minutes) + const challengeAge = Date.now() - challenge.timestamp; + if (challengeAge > 120000) { + throw new Error('Challenge expired'); + } + + // Create proof data + const proofData = { + challenge: challenge.challenge, + timestamp: challenge.timestamp, + nonce: challenge.nonce, + responseTimestamp: Date.now(), + publicKeyHash: await EnhancedSecureCryptoUtils.hashPublicKey(publicKey) + }; + + // Sign the proof + const proofString = JSON.stringify(proofData); + const signature = await EnhancedSecureCryptoUtils.signData(privateKey, proofString); + + const proof = { + ...proofData, + signature, + version: '4.0' + }; + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof created', { + challengeAge: Math.round(challengeAge / 1000) + 's' + }); + + return proof; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof creation failed', { error: error.message }); + throw new Error(`Не удалось создать криптографическое доказательство: ${error.message}`); + } + } + + // Verify mutual authentication proof + static async verifyAuthProof(proof, challenge, publicKey) { + try { + // Assert the public key is valid and has the correct usage + EnhancedSecureCryptoUtils.assertCryptoKey(publicKey, 'ECDSA', ['verify']); + + if (!proof || !challenge || !publicKey) { + throw new Error('Missing required parameters for proof verification'); + } + + // Validate proof structure + const requiredFields = ['challenge', 'timestamp', 'nonce', 'responseTimestamp', 'publicKeyHash', 'signature']; + for (const field of requiredFields) { + if (!proof[field]) { + throw new Error(`Missing required field: ${field}`); + } + } + + // Verify challenge matches + if (JSON.stringify(proof.challenge) !== JSON.stringify(challenge.challenge) || + proof.timestamp !== challenge.timestamp || + JSON.stringify(proof.nonce) !== JSON.stringify(challenge.nonce)) { + throw new Error('Challenge mismatch - possible replay attack'); + } + + // Check response time (max 5 minutes) + const responseAge = Date.now() - proof.responseTimestamp; + if (responseAge > 300000) { + throw new Error('Proof response expired'); + } + + // Verify public key hash + const expectedHash = await EnhancedSecureCryptoUtils.hashPublicKey(publicKey); + if (proof.publicKeyHash !== expectedHash) { + throw new Error('Public key hash mismatch'); + } + + // Verify signature + const proofCopy = { ...proof }; + delete proofCopy.signature; + const proofString = JSON.stringify(proofCopy); + const isValidSignature = await EnhancedSecureCryptoUtils.verifySignature(publicKey, proof.signature, proofString); + + if (!isValidSignature) { + throw new Error('Invalid proof signature'); + } + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Authentication proof verified successfully', { + responseAge: Math.round(responseAge / 1000) + 's' + }); + + return true; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Authentication proof verification failed', { error: error.message }); + throw new Error(`Не удалось проверить криптографическое доказательство: ${error.message}`); + } + } + + // Hash public key for verification + static async hashPublicKey(publicKey) { + try { + const exported = await crypto.subtle.exportKey('spki', publicKey); + const hash = await crypto.subtle.digest('SHA-384', exported); + const hashArray = Array.from(new Uint8Array(hash)); + return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Public key hashing failed', { error: error.message }); + throw new Error('Не удалось создать хеш публичного ключа'); + } + } + + // Legacy authentication challenge for backward compatibility + static generateAuthChallenge() { + const challenge = crypto.getRandomValues(new Uint8Array(32)); + return Array.from(challenge); + } + + // Generate verification code for out-of-band authentication + static generateVerificationCode() { + const chars = '0123456789ABCDEF'; + let result = ''; + const values = crypto.getRandomValues(new Uint8Array(6)); + for (let i = 0; i < 6; i++) { + result += chars[values[i] % chars.length]; + } + return result.match(/.{1,2}/g).join('-'); + } + + // Enhanced message encryption with metadata protection and sequence numbers + static async encryptMessage(message, encryptionKey, macKey, metadataKey, messageId, sequenceNumber = 0) { + try { + if (!message || typeof message !== 'string') { + throw new Error('Invalid message format'); + } + + EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['encrypt']); + EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['sign']); + EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['encrypt']); + + const encoder = new TextEncoder(); + const messageData = encoder.encode(message); + const messageIv = crypto.getRandomValues(new Uint8Array(12)); + const metadataIv = crypto.getRandomValues(new Uint8Array(12)); + const timestamp = Date.now(); + + const paddingSize = 16 - (messageData.length % 16); + const paddedMessage = new Uint8Array(messageData.length + paddingSize); + paddedMessage.set(messageData); + const padding = crypto.getRandomValues(new Uint8Array(paddingSize)); + paddedMessage.set(padding, messageData.length); + + const encryptedMessage = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: messageIv }, + encryptionKey, + paddedMessage + ); + + const metadata = { + id: messageId, + timestamp: timestamp, + sequenceNumber: sequenceNumber, + originalLength: messageData.length, + version: '4.0' + }; + + const metadataStr = JSON.stringify(EnhancedSecureCryptoUtils.sortObjectKeys(metadata)); + const encryptedMetadata = await crypto.subtle.encrypt( + { name: 'AES-GCM', iv: metadataIv }, + metadataKey, + encoder.encode(metadataStr) + ); + + const payload = { + messageIv: Array.from(messageIv), + messageData: Array.from(new Uint8Array(encryptedMessage)), + metadataIv: Array.from(metadataIv), + metadataData: Array.from(new Uint8Array(encryptedMetadata)), + version: '4.0' + }; + + const sortedPayload = EnhancedSecureCryptoUtils.sortObjectKeys(payload); + const payloadStr = JSON.stringify(sortedPayload); + + const mac = await crypto.subtle.sign( + 'HMAC', + macKey, + encoder.encode(payloadStr) + ); + + payload.mac = Array.from(new Uint8Array(mac)); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Message encrypted with metadata protection', { + messageId, + sequenceNumber, + hasMetadataProtection: true, + hasPadding: true + }); + + return payload; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Message encryption failed', { + error: error.message, + messageId + }); + throw new Error(`Не удалось зашифровать сообщение: ${error.message}`); + } + } + + // Enhanced message decryption with metadata protection and sequence validation + static async decryptMessage(encryptedPayload, encryptionKey, macKey, metadataKey, expectedSequenceNumber = null) { + try { + EnhancedSecureCryptoUtils.assertCryptoKey(encryptionKey, 'AES-GCM', ['decrypt']); + EnhancedSecureCryptoUtils.assertCryptoKey(macKey, 'HMAC', ['verify']); + EnhancedSecureCryptoUtils.assertCryptoKey(metadataKey, 'AES-GCM', ['decrypt']); + + const requiredFields = ['messageIv', 'messageData', 'metadataIv', 'metadataData', 'mac', 'version']; + for (const field of requiredFields) { + if (!encryptedPayload[field]) { + throw new Error(`Missing required field: ${field}`); + } + } + + const payloadCopy = { ...encryptedPayload }; + delete payloadCopy.mac; + const sortedPayloadCopy = EnhancedSecureCryptoUtils.sortObjectKeys(payloadCopy); + const payloadStr = JSON.stringify(sortedPayloadCopy); + + const macValid = await crypto.subtle.verify( + 'HMAC', + macKey, + new Uint8Array(encryptedPayload.mac), + new TextEncoder().encode(payloadStr) + ); + + if (!macValid) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'MAC verification failed', { + payloadFields: Object.keys(encryptedPayload), + macLength: encryptedPayload.mac?.length + }); + throw new Error('Message authentication failed - possible tampering'); + } + + const metadataIv = new Uint8Array(encryptedPayload.metadataIv); + const metadataData = new Uint8Array(encryptedPayload.metadataData); + + const decryptedMetadataBuffer = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: metadataIv }, + metadataKey, + metadataData + ); + + const metadataStr = new TextDecoder().decode(decryptedMetadataBuffer); + const metadata = JSON.parse(metadataStr); + + if (!metadata.id || !metadata.timestamp || metadata.sequenceNumber === undefined || !metadata.originalLength) { + throw new Error('Invalid metadata structure'); + } + + const messageAge = Date.now() - metadata.timestamp; + if (messageAge > 300000) { + throw new Error('Message expired (older than 5 minutes)'); + } + + if (expectedSequenceNumber !== null) { + if (metadata.sequenceNumber < expectedSequenceNumber) { + EnhancedSecureCryptoUtils.secureLog.log('warn', 'Received message with lower sequence number, possible queued message', { + expected: expectedSequenceNumber, + received: metadata.sequenceNumber, + messageId: metadata.id + }); + } else if (metadata.sequenceNumber > expectedSequenceNumber + 10) { + throw new Error(`Sequence number gap too large: expected around ${expectedSequenceNumber}, got ${metadata.sequenceNumber}`); + } + } + + const messageIv = new Uint8Array(encryptedPayload.messageIv); + const messageData = new Uint8Array(encryptedPayload.messageData); + + const decryptedMessageBuffer = await crypto.subtle.decrypt( + { name: 'AES-GCM', iv: messageIv }, + encryptionKey, + messageData + ); + + const paddedMessage = new Uint8Array(decryptedMessageBuffer); + const originalMessage = paddedMessage.slice(0, metadata.originalLength); + + const decoder = new TextDecoder(); + const message = decoder.decode(originalMessage); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Message decrypted successfully', { + messageId: metadata.id, + sequenceNumber: metadata.sequenceNumber, + messageAge: Math.round(messageAge / 1000) + 's' + }); + + return { + message: message, + messageId: metadata.id, + timestamp: metadata.timestamp, + sequenceNumber: metadata.sequenceNumber + }; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Message decryption failed', { error: error.message }); + throw new Error(`Не удалось расшифровать сообщение: ${error.message}`); + } + } + + // Enhanced input sanitization + static sanitizeMessage(message) { + if (typeof message !== 'string') { + throw new Error('Message must be a string'); + } + + return message + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/javascript:/gi, '') + .replace(/data:/gi, '') + .replace(/vbscript:/gi, '') + .replace(/onload\s*=/gi, '') + .replace(/onerror\s*=/gi, '') + .replace(/onclick\s*=/gi, '') + .trim() + .substring(0, 2000); // Increased limit + } + + // Generate cryptographically secure salt (64 bytes for enhanced security) + static generateSalt() { + return Array.from(crypto.getRandomValues(new Uint8Array(64))); + } + + // Calculate key fingerprint for MITM protection + static async calculateKeyFingerprint(keyData) { + try { + const encoder = new TextEncoder(); + const keyBytes = new Uint8Array(keyData); + + // Create a hash of the key data for fingerprinting + const hashBuffer = await crypto.subtle.digest('SHA-256', keyBytes); + const hashArray = Array.from(new Uint8Array(hashBuffer)); + + // Convert to hexadecimal string + const fingerprint = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); + + EnhancedSecureCryptoUtils.secureLog.log('info', 'Key fingerprint calculated', { + keySize: keyData.length, + fingerprintLength: fingerprint.length + }); + + return fingerprint; + } catch (error) { + EnhancedSecureCryptoUtils.secureLog.log('error', 'Key fingerprint calculation failed', { error: error.message }); + throw new Error('Не удалось вычислить отпечаток ключа'); + } + } +} + +export { EnhancedSecureCryptoUtils }; \ No newline at end of file diff --git a/src/main.js b/src/main.js new file mode 100644 index 0000000..e69de29 diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js new file mode 100644 index 0000000..4c7fca0 --- /dev/null +++ b/src/network/EnhancedSecureWebRTCManager.js @@ -0,0 +1,1448 @@ +class EnhancedSecureWebRTCManager { + constructor(onMessage, onStatusChange, onKeyExchange, onVerificationRequired, onAnswerError = null) { + // Проверяем доступность глобального объекта + if (!window.EnhancedSecureCryptoUtils) { + throw new Error('EnhancedSecureCryptoUtils не загружен. Убедитесь, что модуль загружен первым.'); + } + + this.peerConnection = null; + this.dataChannel = null; + this.encryptionKey = null; + this.macKey = null; + this.metadataKey = null; + this.keyFingerprint = null; + this.onMessage = onMessage; + this.onStatusChange = onStatusChange; + this.onKeyExchange = onKeyExchange; + this.onVerificationRequired = onVerificationRequired; + this.onAnswerError = onAnswerError; // Callback для ошибок обработки ответа + this.isInitiator = false; + this.connectionAttempts = 0; + this.maxConnectionAttempts = 3; + this.heartbeatInterval = null; + this.messageQueue = []; + this.ecdhKeyPair = null; + this.ecdsaKeyPair = null; + this.verificationCode = null; + this.isVerified = false; + this.processedMessageIds = new Set(); + this.messageCounter = 0; + this.sequenceNumber = 0; + this.expectedSequenceNumber = 0; + this.sessionSalt = null; + this.sessionId = null; // MITM protection: Session identifier + this.peerPublicKey = null; // Store peer's public key for PFS + this.rateLimiterId = null; + this.intentionalDisconnect = false; + this.lastCleanupTime = Date.now(); + + // PFS (Perfect Forward Secrecy) Implementation + this.keyRotationInterval = 300000; // 5 minutes + this.lastKeyRotation = Date.now(); + this.currentKeyVersion = 0; + this.keyVersions = new Map(); // Store key versions for PFS + this.oldKeys = new Map(); // Store old keys temporarily for decryption + this.maxOldKeys = 3; // Keep last 3 key versions for decryption + + this.securityFeatures = { + hasEncryption: true, + hasECDH: true, + hasECDSA: false, + hasMutualAuth: false, + hasMetadataProtection: false, + hasEnhancedReplayProtection: false, + hasNonExtractableKeys: false, + hasRateLimiting: false, + hasEnhancedValidation: false, + hasPFS: true // New PFS feature flag + }; + + // Initialize rate limiter ID + this.rateLimiterId = `webrtc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // Start periodic cleanup + this.startPeriodicCleanup(); + } + + // Start periodic cleanup for rate limiting and security + startPeriodicCleanup() { + setInterval(() => { + const now = Date.now(); + if (now - this.lastCleanupTime > 300000) { // Every 5 minutes + window.EnhancedSecureCryptoUtils.rateLimiter.cleanup(); + this.lastCleanupTime = now; + + // Clean old processed message IDs (keep only last hour) + if (this.processedMessageIds.size > 1000) { + this.processedMessageIds.clear(); + } + + // PFS: Clean old keys that are no longer needed + this.cleanupOldKeys(); + } + }, 60000); // Check every minute + } + + // Calculate current security level with real verification + async calculateSecurityLevel() { + return await window.EnhancedSecureCryptoUtils.calculateSecurityLevel(this); + } + + // PFS: Check if key rotation is needed + shouldRotateKeys() { + if (!this.isConnected() || !this.isVerified) { + return false; + } + + const now = Date.now(); + const timeSinceLastRotation = now - this.lastKeyRotation; + + // Rotate keys every 5 minutes or after 100 messages + return timeSinceLastRotation > this.keyRotationInterval || + this.messageCounter % 100 === 0; + } + + // PFS: Rotate encryption keys for Perfect Forward Secrecy + async rotateKeys() { + try { + if (!this.isConnected() || !this.isVerified) { + return false; + } + + // Отправляем сигнал о ротации ключей партнеру + const rotationSignal = { + type: 'key_rotation_signal', + newVersion: this.currentKeyVersion + 1, + timestamp: Date.now() + }; + + this.dataChannel.send(JSON.stringify(rotationSignal)); + + // Ждем подтверждения от партнера перед ротацией + return new Promise((resolve) => { + this.pendingRotation = { + newVersion: this.currentKeyVersion + 1, + resolve: resolve + }; + + // Таймаут на случай если партнер не ответит + setTimeout(() => { + if (this.pendingRotation) { + this.pendingRotation.resolve(false); + this.pendingRotation = null; + } + }, 5000); + }); + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Key rotation failed', { + error: error.message + }); + return false; + } + } + + // PFS: Clean up old keys that are no longer needed + cleanupOldKeys() { + const now = Date.now(); + const maxKeyAge = 900000; // 15 minutes - keys older than this are deleted + + for (const [version, keySet] of this.oldKeys.entries()) { + if (now - keySet.timestamp > maxKeyAge) { + this.oldKeys.delete(version); + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Old PFS keys cleaned up', { + version: version, + age: Math.round((now - keySet.timestamp) / 1000) + 's' + }); + } + } + } + + // PFS: Get keys for specific version (for decryption) + getKeysForVersion(version) { + // Сначала проверяем старые ключи (включая версию 0) + const oldKeySet = this.oldKeys.get(version); + if (oldKeySet && oldKeySet.encryptionKey && oldKeySet.macKey && oldKeySet.metadataKey) { + return { + encryptionKey: oldKeySet.encryptionKey, + macKey: oldKeySet.macKey, + metadataKey: oldKeySet.metadataKey + }; + } + + // Если это текущая версия, возвращаем текущие ключи + if (version === this.currentKeyVersion) { + if (this.encryptionKey && this.macKey && this.metadataKey) { + return { + encryptionKey: this.encryptionKey, + macKey: this.macKey, + metadataKey: this.metadataKey + }; + } + } + + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'No valid keys found for version', { + requestedVersion: version, + currentVersion: this.currentKeyVersion, + availableVersions: Array.from(this.oldKeys.keys()) + }); + + return null; + } + + createPeerConnection() { + const config = { + iceServers: [ + { urls: 'stun:stun.l.google.com:19302' }, + { urls: 'stun:stun1.l.google.com:19302' }, + { urls: 'stun:stun2.l.google.com:19302' }, + { urls: 'stun:stun3.l.google.com:19302' }, + { urls: 'stun:stun4.l.google.com:19302' } + ], + iceCandidatePoolSize: 10, + bundlePolicy: 'balanced' + }; + + this.peerConnection = new RTCPeerConnection(config); + + this.peerConnection.onconnectionstatechange = () => { + const state = this.peerConnection.connectionState; + console.log('Connection state:', state); + + if (state === 'connected' && !this.isVerified) { + this.onStatusChange('verifying'); + } else if (state === 'connected' && this.isVerified) { + this.onStatusChange('connected'); + } else if (state === 'disconnected' || state === 'closed') { + // Если это намеренное отключение, сразу очищаем + if (this.intentionalDisconnect) { + this.onStatusChange('disconnected'); + setTimeout(() => this.cleanupConnection(), 100); + } else { + // Неожиданное отключение - пытаемся уведомить партнера + this.onStatusChange('reconnecting'); + this.handleUnexpectedDisconnect(); + } + } else if (state === 'failed') { + if (!this.intentionalDisconnect && this.connectionAttempts < this.maxConnectionAttempts) { + this.connectionAttempts++; + setTimeout(() => this.retryConnection(), 2000); + } else { + this.onStatusChange('failed'); + setTimeout(() => this.cleanupConnection(), 1000); + } + } else { + this.onStatusChange(state); + } + }; + + this.peerConnection.ondatachannel = (event) => { + console.log('Data channel received'); + this.setupDataChannel(event.channel); + }; + } + + setupDataChannel(channel) { + this.dataChannel = channel; + + this.dataChannel.onopen = () => { + console.log('Secure data channel opened'); + if (this.isVerified) { + this.onStatusChange('connected'); + this.processMessageQueue(); + } else { + this.onStatusChange('verifying'); + this.initiateVerification(); + } + this.startHeartbeat(); + }; + + this.dataChannel.onclose = () => { + console.log('Data channel closed'); + + if (!this.intentionalDisconnect) { + this.onStatusChange('reconnecting'); + this.onMessage('🔄 Канал данных закрыт. Попытка восстановления...', 'system'); + this.handleUnexpectedDisconnect(); + } else { + this.onStatusChange('disconnected'); + this.onMessage('🔌 Соединение закрыто', 'system'); + } + + this.stopHeartbeat(); + this.isVerified = false; + }; + + this.dataChannel.onmessage = async (event) => { + try { + const payload = JSON.parse(event.data); + + if (payload.type === 'heartbeat') { + this.handleHeartbeat(); + return; + } + + if (payload.type === 'verification') { + this.handleVerificationRequest(payload.data); + return; + } + + if (payload.type === 'verification_response') { + this.handleVerificationResponse(payload.data); + return; + } + + if (payload.type === 'peer_disconnect') { + this.handlePeerDisconnectNotification(payload); + return; + } + + if (payload.type === 'key_rotation_signal') { + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Key rotation signal received but ignored for stability', { + newVersion: payload.newVersion + }); + return; + } + + if (payload.type === 'key_rotation_ready') { + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Key rotation ready signal received but ignored for stability'); + return; + } + // Handle enhanced messages with metadata protection and PFS + if (payload.type === 'enhanced_message') { + const keyVersion = payload.keyVersion || 0; + const keys = this.getKeysForVersion(keyVersion); + + if (!keys) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Keys not available for message decryption', { + keyVersion: keyVersion, + currentKeyVersion: this.currentKeyVersion, + hasCurrentKeys: !!(this.encryptionKey && this.macKey && this.metadataKey), + availableOldVersions: Array.from(this.oldKeys.keys()) + }); + throw new Error(`Cannot decrypt message: keys for version ${keyVersion} not available`); + } + + if (!(keys.encryptionKey instanceof CryptoKey) || + !(keys.macKey instanceof CryptoKey) || + !(keys.metadataKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid key types for message decryption', { + keyVersion: keyVersion, + encryptionKeyType: typeof keys.encryptionKey, + macKeyType: typeof keys.macKey, + metadataKeyType: typeof keys.metadataKey + }); + throw new Error(`Invalid key types for version ${keyVersion}`); + } + + // Используем более гибкую проверку sequence number + const decryptedData = await window.EnhancedSecureCryptoUtils.decryptMessage( + payload.data, + keys.encryptionKey, + keys.macKey, + keys.metadataKey, + null // Отключаем строгую проверку sequence number + ); + + // Проверяем replay attack по messageId + if (this.processedMessageIds.has(decryptedData.messageId)) { + throw new Error('Duplicate message detected - possible replay attack'); + } + this.processedMessageIds.add(decryptedData.messageId); + + // Обновляем ожидаемый sequence number более гибко + if (decryptedData.sequenceNumber >= this.expectedSequenceNumber) { + this.expectedSequenceNumber = decryptedData.sequenceNumber + 1; + } + + const sanitizedMessage = window.EnhancedSecureCryptoUtils.sanitizeMessage(decryptedData.message); + this.onMessage(sanitizedMessage, 'received'); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced message received with PFS', { + messageId: decryptedData.messageId, + sequenceNumber: decryptedData.sequenceNumber, + keyVersion: keyVersion, + hasMetadataProtection: true, + hasPFS: true + }); + return; + } + + // Legacy message support for backward compatibility + if (payload.type === 'message') { + // Additional validation for legacy messages + if (!this.encryptionKey || !this.macKey) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Missing keys for legacy message decryption', { + hasEncryptionKey: !!this.encryptionKey, + hasMacKey: !!this.macKey, + hasMetadataKey: !!this.metadataKey + }); + throw new Error('Отсутствуют ключи для расшифровки legacy сообщения'); + } + + const decryptedData = await window.EnhancedSecureCryptoUtils.decryptMessage( + payload.data, + this.encryptionKey, + this.macKey, + this.metadataKey // Add metadataKey for consistency + ); + + // Check for replay attacks + if (this.processedMessageIds.has(decryptedData.messageId)) { + throw new Error('Duplicate message detected - possible replay attack'); + } + this.processedMessageIds.add(decryptedData.messageId); + + const sanitizedMessage = window.EnhancedSecureCryptoUtils.sanitizeMessage(decryptedData.message); + this.onMessage(sanitizedMessage, 'received'); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Legacy message received', { + messageId: decryptedData.messageId, + legacy: true + }); + return; + } + + // Unknown message type + window.EnhancedSecureCryptoUtils.secureLog.log('warn', 'Unknown message type received', { + type: payload.type + }); + + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Message processing error', { + error: error.message + }); + this.onMessage(`❌ Ошибка обработки: ${error.message}`, 'system'); + } + }; + + this.dataChannel.onerror = (error) => { + console.error('Data channel error:', error); + this.onMessage('❌ Ошибка канала данных', 'system'); + }; + } + + async createSecureOffer() { + try { + // Check rate limiting + if (!window.EnhancedSecureCryptoUtils.rateLimiter.checkConnectionRate(this.rateLimiterId)) { + throw new Error('Connection rate limit exceeded. Please wait before trying again.'); + } + + this.connectionAttempts = 0; + this.sessionSalt = window.EnhancedSecureCryptoUtils.generateSalt(); // Now 64 bytes + + // Generate ECDH key pair (non-extractable) + this.ecdhKeyPair = await window.EnhancedSecureCryptoUtils.generateECDHKeyPair(); + + // Generate ECDSA key pair for digital signatures + this.ecdsaKeyPair = await window.EnhancedSecureCryptoUtils.generateECDSAKeyPair(); + + // MITM Protection: Verify key uniqueness and prevent key reuse attacks + const ecdhFingerprint = await window.EnhancedSecureCryptoUtils.calculateKeyFingerprint( + await crypto.subtle.exportKey('spki', this.ecdhKeyPair.publicKey) + ); + const ecdsaFingerprint = await window.EnhancedSecureCryptoUtils.calculateKeyFingerprint( + await crypto.subtle.exportKey('spki', this.ecdsaKeyPair.publicKey) + ); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Generated unique key pairs for MITM protection', { + ecdhFingerprint: ecdhFingerprint.substring(0, 8), + ecdsaFingerprint: ecdsaFingerprint.substring(0, 8), + timestamp: Date.now() + }); + + // Export keys with signatures + const ecdhPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature( + this.ecdhKeyPair.publicKey, + this.ecdsaKeyPair.privateKey, + 'ECDH' + ); + + const ecdsaPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature( + this.ecdsaKeyPair.publicKey, + this.ecdsaKeyPair.privateKey, + 'ECDSA' + ); + + // Update security features + this.securityFeatures.hasECDSA = true; + this.securityFeatures.hasMutualAuth = true; + this.securityFeatures.hasMetadataProtection = true; + this.securityFeatures.hasEnhancedReplayProtection = true; + this.securityFeatures.hasNonExtractableKeys = true; + this.securityFeatures.hasRateLimiting = true; + this.securityFeatures.hasEnhancedValidation = true; + this.securityFeatures.hasPFS = true; + + this.isInitiator = true; + this.onStatusChange('connecting'); + + this.createPeerConnection(); + + this.dataChannel = this.peerConnection.createDataChannel('securechat', { + ordered: true, + maxRetransmits: 3 + }); + this.setupDataChannel(this.dataChannel); + + const offer = await this.peerConnection.createOffer({ + offerToReceiveAudio: false, + offerToReceiveVideo: false + }); + + await this.peerConnection.setLocalDescription(offer); + await this.waitForIceGathering(); + + // Generate verification code for out-of-band authentication + this.verificationCode = window.EnhancedSecureCryptoUtils.generateVerificationCode(); + this.onVerificationRequired(this.verificationCode); + + // Generate mutual authentication challenge + const authChallenge = window.EnhancedSecureCryptoUtils.generateMutualAuthChallenge(); + + // MITM Protection: Add session-specific data to prevent session hijacking + this.sessionId = Array.from(crypto.getRandomValues(new Uint8Array(16))) + .map(b => b.toString(16).padStart(2, '0')).join(''); + + const offerPackage = { + type: 'enhanced_secure_offer', + sdp: this.peerConnection.localDescription.sdp, + ecdhPublicKey: ecdhPublicKeyData, + ecdsaPublicKey: ecdsaPublicKeyData, + salt: this.sessionSalt, + verificationCode: this.verificationCode, + authChallenge: authChallenge, + sessionId: this.sessionId, // Additional MITM protection + timestamp: Date.now(), + version: '4.0', + securityLevel: await this.calculateSecurityLevel() + }; + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced secure offer created', { + version: '4.0', + hasECDSA: true, + saltSize: this.sessionSalt.length, + securityLevel: offerPackage.securityLevel.level + }); + + return offerPackage; + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced secure offer creation failed', { + error: error.message + }); + this.onStatusChange('failed'); + throw error; + } + } + + async createSecureAnswer(offerData) { + try { + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting createSecureAnswer', { + hasOfferData: !!offerData, + offerType: offerData?.type, + hasECDHKey: !!offerData?.ecdhPublicKey, + hasECDSAKey: !!offerData?.ecdsaPublicKey, + hasSalt: !!offerData?.salt + }); + + if (!this.validateEnhancedOfferData(offerData)) { + throw new Error('Неверный формат данных подключения'); + } + + // Check rate limiting + if (!window.EnhancedSecureCryptoUtils.rateLimiter.checkConnectionRate(this.rateLimiterId)) { + throw new Error('Connection rate limit exceeded. Please wait before trying again.'); + } + + this.sessionSalt = offerData.salt; + + // Generate our ECDH key pair (non-extractable) + this.ecdhKeyPair = await window.EnhancedSecureCryptoUtils.generateECDHKeyPair(); + + // Generate our ECDSA key pair for digital signatures + this.ecdsaKeyPair = await window.EnhancedSecureCryptoUtils.generateECDSAKeyPair(); + + // First, import the ECDSA public key without signature verification (for self-signed keys) + const peerECDSAPublicKey = await crypto.subtle.importKey( + 'spki', + new Uint8Array(offerData.ecdsaPublicKey.keyData), + { + name: 'ECDSA', + namedCurve: 'P-384' + }, + false, + ['verify'] + ); + + // Now verify the ECDSA key's self-signature + const ecdsaPackageCopy = { ...offerData.ecdsaPublicKey }; + delete ecdsaPackageCopy.signature; + const ecdsaPackageString = JSON.stringify(ecdsaPackageCopy); + const ecdsaSignatureValid = await window.EnhancedSecureCryptoUtils.verifySignature( + peerECDSAPublicKey, + offerData.ecdsaPublicKey.signature, + ecdsaPackageString + ); + + if (!ecdsaSignatureValid) { + throw new Error('Invalid ECDSA key self-signature'); + } + + // Now import and verify the ECDH public key using the verified ECDSA key + const peerECDHPublicKey = await window.EnhancedSecureCryptoUtils.importSignedPublicKey( + offerData.ecdhPublicKey, + peerECDSAPublicKey, + 'ECDH' + ); + + // Additional validation: Ensure all keys are CryptoKey instances before derivation + if (!(this.ecdhKeyPair?.privateKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Local ECDH private key is not a CryptoKey in createEnhancedSecureAnswer', { + hasKeyPair: !!this.ecdhKeyPair, + privateKeyType: typeof this.ecdhKeyPair?.privateKey, + privateKeyAlgorithm: this.ecdhKeyPair?.privateKey?.algorithm?.name + }); + throw new Error('Локальный ECDH приватный ключ не является CryptoKey'); + } + + if (!(peerECDHPublicKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Peer ECDH public key is not a CryptoKey in createEnhancedSecureAnswer', { + publicKeyType: typeof peerECDHPublicKey, + publicKeyAlgorithm: peerECDHPublicKey?.algorithm?.name + }); + throw new Error('ECDH публичный ключ собеседника не является CryptoKey'); + } + + // Store peer's public key for PFS key rotation + this.peerPublicKey = peerECDHPublicKey; + + // Derive shared keys with metadata protection + const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys( + this.ecdhKeyPair.privateKey, + peerECDHPublicKey, + this.sessionSalt + ); + + this.encryptionKey = derivedKeys.encryptionKey; + this.macKey = derivedKeys.macKey; + this.metadataKey = derivedKeys.metadataKey; + this.keyFingerprint = derivedKeys.fingerprint; + this.sequenceNumber = 0; + this.expectedSequenceNumber = 0; + this.messageCounter = 0; + this.processedMessageIds.clear(); + this.verificationCode = offerData.verificationCode; + + // Validate that all keys are properly set + if (!(this.encryptionKey instanceof CryptoKey) || + !(this.macKey instanceof CryptoKey) || + !(this.metadataKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid key types after derivation in createEnhancedSecureAnswer', { + encryptionKeyType: typeof this.encryptionKey, + macKeyType: typeof this.macKey, + metadataKeyType: typeof this.metadataKey, + encryptionKeyAlgorithm: this.encryptionKey?.algorithm?.name, + macKeyAlgorithm: this.macKey?.algorithm?.name, + metadataKeyAlgorithm: this.metadataKey?.algorithm?.name + }); + throw new Error('Недействительные типы ключей после вывода'); + } + + // PFS: Initialize key version tracking + this.currentKeyVersion = 0; + this.lastKeyRotation = Date.now(); + this.keyVersions.set(0, { + salt: this.sessionSalt, + timestamp: this.lastKeyRotation, + messageCount: 0 + }); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Encryption keys set in createEnhancedSecureAnswer', { + hasEncryptionKey: !!this.encryptionKey, + hasMacKey: !!this.macKey, + hasMetadataKey: !!this.metadataKey, + keyFingerprint: this.keyFingerprint + }); + + // Update security features + this.securityFeatures.hasECDSA = true; + this.securityFeatures.hasMutualAuth = true; + this.securityFeatures.hasMetadataProtection = true; + this.securityFeatures.hasEnhancedReplayProtection = true; + this.securityFeatures.hasNonExtractableKeys = true; + this.securityFeatures.hasRateLimiting = true; + this.securityFeatures.hasEnhancedValidation = true; + this.securityFeatures.hasPFS = true; + + // Create authentication proof for mutual authentication + const authProof = await window.EnhancedSecureCryptoUtils.createAuthProof( + offerData.authChallenge, + this.ecdsaKeyPair.privateKey, + this.ecdsaKeyPair.publicKey + ); + + this.isInitiator = false; + this.onStatusChange('connecting'); + this.onKeyExchange(this.keyFingerprint); + this.onVerificationRequired(this.verificationCode); + + this.createPeerConnection(); + + await this.peerConnection.setRemoteDescription(new RTCSessionDescription({ + type: 'offer', + sdp: offerData.sdp + })); + + const answer = await this.peerConnection.createAnswer({ + offerToReceiveAudio: false, + offerToReceiveVideo: false + }); + + await this.peerConnection.setLocalDescription(answer); + await this.waitForIceGathering(); + + // Export our keys with signatures + const ecdhPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature( + this.ecdhKeyPair.publicKey, + this.ecdsaKeyPair.privateKey, + 'ECDH' + ); + + const ecdsaPublicKeyData = await window.EnhancedSecureCryptoUtils.exportPublicKeyWithSignature( + this.ecdsaKeyPair.publicKey, + this.ecdsaKeyPair.privateKey, + 'ECDSA' + ); + + const answerPackage = { + type: 'enhanced_secure_answer', + sdp: this.peerConnection.localDescription.sdp, + ecdhPublicKey: ecdhPublicKeyData, + ecdsaPublicKey: ecdsaPublicKeyData, + authProof: authProof, + timestamp: Date.now(), + version: '4.0', + securityLevel: await this.calculateSecurityLevel() + }; + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced secure answer created', { + version: '4.0', + hasECDSA: true, + hasMutualAuth: true, + securityLevel: answerPackage.securityLevel.level + }); + + return answerPackage; + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced secure answer creation failed', { + error: error.message + }); + this.onStatusChange('failed'); + throw error; + } + } + + async handleSecureAnswer(answerData) { + try { + if (!answerData || answerData.type !== 'enhanced_secure_answer' || !answerData.sdp) { + throw new Error('Неверный формат ответа'); + } + + // Import peer's ECDH public key from the signed package + if (!answerData.ecdhPublicKey || !answerData.ecdhPublicKey.keyData) { + throw new Error('Отсутствуют данные ECDH публичного ключа'); + } + + // First, import and verify the ECDSA public key for signature verification + if (!answerData.ecdsaPublicKey || !answerData.ecdsaPublicKey.keyData) { + throw new Error('Отсутствуют данные ECDSA публичного ключа для верификации подписи'); + } + + // Additional MITM protection: Validate answer data structure + if (!answerData.timestamp || !answerData.version) { + throw new Error('Отсутствуют обязательные поля в данных ответа - возможная MITM атака'); + } + + // MITM Protection: Verify session ID if present (for enhanced security) + if (answerData.sessionId && this.sessionId && answerData.sessionId !== this.sessionId) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Session ID mismatch detected - possible MITM attack', { + expectedSessionId: this.sessionId, + receivedSessionId: answerData.sessionId + }); + throw new Error('Несоответствие идентификатора сессии - возможная MITM атака'); + } + + // Check for replay attacks (reject answers older than 1 hour) + const answerAge = Date.now() - answerData.timestamp; + if (answerAge > 3600000) { // 1 hour in milliseconds + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Answer data is too old - possible replay attack', { + answerAge: answerAge, + timestamp: answerData.timestamp + }); + + // Уведомляем основной код о ошибке replay attack + if (this.onAnswerError) { + this.onAnswerError('replay_attack', 'Данные ответа слишком старые - возможная атака повтора'); + } + + throw new Error('Данные ответа слишком старые - возможная атака повтора'); + } + + // Check protocol version compatibility + if (answerData.version !== '4.0') { + window.EnhancedSecureCryptoUtils.secureLog.log('warn', 'Incompatible protocol version in answer', { + expectedVersion: '4.0', + receivedVersion: answerData.version + }); + } + + // Import ECDSA public key for verification (self-signed) + const peerECDSAPublicKey = await crypto.subtle.importKey( + 'spki', + new Uint8Array(answerData.ecdsaPublicKey.keyData), + { + name: 'ECDSA', + namedCurve: 'P-384' + }, + false, + ['verify'] + ); + + // Verify ECDSA key's self-signature + const ecdsaPackageCopy = { ...answerData.ecdsaPublicKey }; + delete ecdsaPackageCopy.signature; + const ecdsaPackageString = JSON.stringify(ecdsaPackageCopy); + const ecdsaSignatureValid = await window.EnhancedSecureCryptoUtils.verifySignature( + peerECDSAPublicKey, + answerData.ecdsaPublicKey.signature, + ecdsaPackageString + ); + + if (!ecdsaSignatureValid) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid ECDSA signature detected - possible MITM attack', { + timestamp: answerData.timestamp, + version: answerData.version + }); + throw new Error('Недействительная подпись ECDSA ключа - возможная MITM атака'); + } + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'ECDSA signature verification passed', { + timestamp: answerData.timestamp, + version: answerData.version + }); + + // Now import and verify the ECDH public key using the verified ECDSA key + const peerPublicKey = await window.EnhancedSecureCryptoUtils.importPublicKeyFromSignedPackage( + answerData.ecdhPublicKey, + peerECDSAPublicKey + ); + + // Additional MITM protection: Verify session salt integrity + if (!this.sessionSalt || this.sessionSalt.length !== 64) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid session salt detected - possible session hijacking', { + saltLength: this.sessionSalt ? this.sessionSalt.length : 0 + }); + throw new Error('Недействительная сессионная соль - возможная атака перехвата сессии'); + } + + // Verify that the session salt hasn't been tampered with + const expectedSaltHash = await window.EnhancedSecureCryptoUtils.calculateKeyFingerprint(this.sessionSalt); + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Session salt integrity verified', { + saltFingerprint: expectedSaltHash.substring(0, 8) + }); + + // Additional validation: Ensure all keys are CryptoKey instances before derivation + if (!(this.ecdhKeyPair?.privateKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Local ECDH private key is not a CryptoKey in handleSecureAnswer', { + hasKeyPair: !!this.ecdhKeyPair, + privateKeyType: typeof this.ecdhKeyPair?.privateKey, + privateKeyAlgorithm: this.ecdhKeyPair?.privateKey?.algorithm?.name + }); + throw new Error('Локальный ECDH приватный ключ не является CryptoKey'); + } + + if (!(peerPublicKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Peer ECDH public key is not a CryptoKey in handleSecureAnswer', { + publicKeyType: typeof peerPublicKey, + publicKeyAlgorithm: peerPublicKey?.algorithm?.name + }); + throw new Error('ECDH публичный ключ собеседника не является CryptoKey'); + } + + // Store peer's public key for PFS key rotation + this.peerPublicKey = peerPublicKey; + + const derivedKeys = await window.EnhancedSecureCryptoUtils.deriveSharedKeys( + this.ecdhKeyPair.privateKey, + peerPublicKey, + this.sessionSalt + ); + + this.encryptionKey = derivedKeys.encryptionKey; + this.macKey = derivedKeys.macKey; + this.metadataKey = derivedKeys.metadataKey; + this.keyFingerprint = derivedKeys.fingerprint; + this.sequenceNumber = 0; + this.expectedSequenceNumber = 0; + this.messageCounter = 0; + this.processedMessageIds.clear(); + // Validate that all keys are properly set + if (!(this.encryptionKey instanceof CryptoKey) || + !(this.macKey instanceof CryptoKey) || + !(this.metadataKey instanceof CryptoKey)) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Invalid key types after derivation in handleSecureAnswer', { + encryptionKeyType: typeof this.encryptionKey, + macKeyType: typeof this.macKey, + metadataKeyType: typeof this.metadataKey, + encryptionKeyAlgorithm: this.encryptionKey?.algorithm?.name, + macKeyAlgorithm: this.macKey?.algorithm?.name, + metadataKeyAlgorithm: this.metadataKey?.algorithm?.name + }); + throw new Error('Недействительные типы ключей после вывода'); + } + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Encryption keys set in handleSecureAnswer', { + hasEncryptionKey: !!this.encryptionKey, + hasMacKey: !!this.macKey, + hasMetadataKey: !!this.metadataKey, + keyFingerprint: this.keyFingerprint, + mitmProtection: 'enabled', + signatureVerified: true, + timestamp: answerData.timestamp, + version: answerData.version + }); + + // Update security features for initiator after successful key exchange + this.securityFeatures.hasMutualAuth = true; + this.securityFeatures.hasMetadataProtection = true; + this.securityFeatures.hasEnhancedReplayProtection = true; + this.securityFeatures.hasPFS = true; + + // PFS: Initialize key version tracking + this.currentKeyVersion = 0; + this.lastKeyRotation = Date.now(); + this.keyVersions.set(0, { + salt: this.sessionSalt, + timestamp: this.lastKeyRotation, + messageCount: 0 + }); + + this.onKeyExchange(this.keyFingerprint); + + await this.peerConnection.setRemoteDescription({ + type: 'answer', + sdp: answerData.sdp + }); + + console.log('Enhanced secure connection established'); + } catch (error) { + console.error('Enhanced secure answer handling failed:', error); + this.onStatusChange('failed'); + + // Уведомляем основной код о критических ошибках + if (this.onAnswerError) { + if (error.message.includes('слишком старые') || error.message.includes('too old')) { + this.onAnswerError('replay_attack', error.message); + } else if (error.message.includes('MITM') || error.message.includes('подпись')) { + this.onAnswerError('security_violation', error.message); + } else { + this.onAnswerError('general_error', error.message); + } + } + + throw error; + } + } + + initiateVerification() { + if (this.isInitiator) { + // Initiator waits for verification confirmation + this.onMessage('🔐 Подтвердите код безопасности с собеседником для завершения подключения', 'system'); + } else { + // Responder confirms verification automatically if codes match + this.confirmVerification(); + } + } + + confirmVerification() { + try { + const verificationPayload = { + type: 'verification', + data: { + code: this.verificationCode, + timestamp: Date.now() + } + }; + + this.dataChannel.send(JSON.stringify(verificationPayload)); + this.isVerified = true; + this.onStatusChange('connected'); + this.onMessage('✅ Верификация прошла успешно. Канал защищен!', 'system'); + this.processMessageQueue(); + } catch (error) { + console.error('Verification failed:', error); + this.onMessage('❌ Ошибка верификации', 'system'); + } + } + + handleVerificationRequest(data) { + if (data.code === this.verificationCode) { + const responsePayload = { + type: 'verification_response', + data: { + verified: true, + timestamp: Date.now() + } + }; + this.dataChannel.send(JSON.stringify(responsePayload)); + this.isVerified = true; + this.onStatusChange('connected'); + this.onMessage('✅ Верификация прошла успешно. Канал защищен!', 'system'); + this.processMessageQueue(); + } else { + this.onMessage('❌ Код верификации не совпадает! Возможна атака!', 'system'); + this.disconnect(); + } + } + + handleVerificationResponse(data) { + if (data.verified) { + this.isVerified = true; + this.onStatusChange('connected'); + this.onMessage('✅ Верификация прошла успешно. Канал защищен!', 'system'); + this.processMessageQueue(); + } else { + this.onMessage('❌ Верификация не прошла!', 'system'); + this.disconnect(); + } + } + + validateOfferData(offerData) { + return offerData && + offerData.type === 'enhanced_secure_offer' && + offerData.sdp && + offerData.publicKey && + offerData.salt && + offerData.verificationCode && + Array.isArray(offerData.publicKey) && + Array.isArray(offerData.salt) && + offerData.salt.length === 32; + } + + // Enhanced validation with backward compatibility + validateEnhancedOfferData(offerData) { + try { + if (!offerData || typeof offerData !== 'object') { + throw new Error('Offer data must be an object'); + } + + // Basic required fields for all versions + const basicFields = ['type', 'sdp']; + for (const field of basicFields) { + if (!offerData[field]) { + throw new Error(`Missing required field: ${field}`); + } + } + + // Validate offer type (support both v3.0 and v4.0 formats) + if (!['enhanced_secure_offer', 'secure_offer'].includes(offerData.type)) { + throw new Error('Invalid offer type'); + } + + // Check if this is v4.0 format with enhanced features + const isV4Format = offerData.version === '4.0' && offerData.ecdhPublicKey && offerData.ecdsaPublicKey; + + if (isV4Format) { + // v4.0 enhanced validation + const v4RequiredFields = [ + 'ecdhPublicKey', 'ecdsaPublicKey', 'salt', 'verificationCode', + 'authChallenge', 'timestamp', 'version', 'securityLevel' + ]; + + for (const field of v4RequiredFields) { + if (!offerData[field]) { + throw new Error(`Missing v4.0 field: ${field}`); + } + } + + // Validate salt (must be 64 bytes for v4.0) + if (!Array.isArray(offerData.salt) || offerData.salt.length !== 64) { + throw new Error('Salt must be exactly 64 bytes for v4.0'); + } + + // Validate timestamp (not older than 1 hour) + const offerAge = Date.now() - offerData.timestamp; + if (offerAge > 3600000) { + throw new Error('Offer is too old (older than 1 hour)'); + } + + // Validate key structures (more lenient) + if (!offerData.ecdhPublicKey || typeof offerData.ecdhPublicKey !== 'object') { + throw new Error('Invalid ECDH public key structure'); + } + + if (!offerData.ecdsaPublicKey || typeof offerData.ecdsaPublicKey !== 'object') { + throw new Error('Invalid ECDSA public key structure'); + } + + // Validate verification code format (more flexible) + if (typeof offerData.verificationCode !== 'string' || offerData.verificationCode.length < 6) { + throw new Error('Invalid verification code format'); + } + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'v4.0 offer validation passed', { + version: offerData.version, + securityLevel: offerData.securityLevel?.level || 'unknown', + offerAge: Math.round(offerAge / 1000) + 's' + }); + } else { + // v3.0 backward compatibility validation + const v3RequiredFields = ['publicKey', 'salt', 'verificationCode']; + for (const field of v3RequiredFields) { + if (!offerData[field]) { + throw new Error(`Missing v3.0 field: ${field}`); + } + } + + // Validate salt (32 bytes for v3.0) + if (!Array.isArray(offerData.salt) || offerData.salt.length !== 32) { + throw new Error('Salt must be exactly 32 bytes for v3.0'); + } + + // Validate public key + if (!Array.isArray(offerData.publicKey)) { + throw new Error('Invalid public key format for v3.0'); + } + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'v3.0 offer validation passed (backward compatibility)', { + version: 'v3.0', + legacy: true + }); + } + + // Validate SDP structure (basic check for all versions) + if (typeof offerData.sdp !== 'string' || !offerData.sdp.includes('v=0')) { + throw new Error('Invalid SDP structure'); + } + + return true; + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Offer validation failed', { + error: error.message + }); + return false; // Return false instead of throwing to allow graceful handling + } + } + + async sendSecureMessage(message) { + if (!this.isConnected() || !this.isVerified) { + this.messageQueue.push(message); + throw new Error('Соединение не готово. Сообщение добавлено в очередь.'); + } + + // Validate encryption keys + if (!this.encryptionKey || !this.macKey || !this.metadataKey) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Encryption keys not initialized', { + hasEncryptionKey: !!this.encryptionKey, + hasMacKey: !!this.macKey, + hasMetadataKey: !!this.metadataKey, + isConnected: this.isConnected(), + isVerified: this.isVerified + }); + throw new Error('Ключи шифрования не инициализированы. Проверьте соединение.'); + } + + try { + // Check rate limiting + if (!window.EnhancedSecureCryptoUtils.rateLimiter.checkMessageRate(this.rateLimiterId)) { + throw new Error('Message rate limit exceeded (60 messages per minute)'); + } + + const sanitizedMessage = window.EnhancedSecureCryptoUtils.sanitizeMessage(message); + const messageId = `msg_${Date.now()}_${this.messageCounter++}`; + + // Use enhanced encryption with metadata protection, sequence numbers, and PFS key version + const encryptedData = await window.EnhancedSecureCryptoUtils.encryptMessage( + sanitizedMessage, + this.encryptionKey, + this.macKey, + this.metadataKey, + messageId, + this.sequenceNumber++ + ); + + const payload = { + type: 'enhanced_message', + data: encryptedData, + keyVersion: this.currentKeyVersion, // PFS: Include key version + version: '4.0' + }; + + this.dataChannel.send(JSON.stringify(payload)); + this.onMessage(sanitizedMessage, 'sent'); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Enhanced message sent with PFS', { + messageId, + sequenceNumber: this.sequenceNumber - 1, + keyVersion: this.currentKeyVersion, + hasMetadataProtection: true, + hasPFS: true + }); + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced message sending failed', { + error: error.message + }); + throw error; + } + } + + processMessageQueue() { + while (this.messageQueue.length > 0 && this.isConnected() && this.isVerified) { + const message = this.messageQueue.shift(); + this.sendSecureMessage(message).catch(console.error); + } + } + + startHeartbeat() { + this.heartbeatInterval = setInterval(() => { + if (this.isConnected()) { + try { + this.dataChannel.send(JSON.stringify({ + type: 'heartbeat', + timestamp: Date.now() + })); + } catch (error) { + console.error('Heartbeat failed:', error); + } + } + }, 30000); + } + + stopHeartbeat() { + if (this.heartbeatInterval) { + clearInterval(this.heartbeatInterval); + this.heartbeatInterval = null; + } + } + + handleHeartbeat() { + console.log('Heartbeat received - connection alive'); + } + + waitForIceGathering() { + return new Promise((resolve) => { + if (this.peerConnection.iceGatheringState === 'complete') { + resolve(); + return; + } + + const checkState = () => { + if (this.peerConnection.iceGatheringState === 'complete') { + this.peerConnection.removeEventListener('icegatheringstatechange', checkState); + resolve(); + } + }; + + this.peerConnection.addEventListener('icegatheringstatechange', checkState); + + setTimeout(() => { + this.peerConnection.removeEventListener('icegatheringstatechange', checkState); + resolve(); + }, 10000); + }); + } + + retryConnection() { + console.log(`Retrying connection (attempt ${this.connectionAttempts}/${this.maxConnectionAttempts})`); + this.onStatusChange('retrying'); + } + + isConnected() { + return this.dataChannel && this.dataChannel.readyState === 'open'; + } + + getConnectionInfo() { + return { + fingerprint: this.keyFingerprint, + isConnected: this.isConnected(), + isVerified: this.isVerified, + connectionState: this.peerConnection?.connectionState, + iceConnectionState: this.peerConnection?.iceConnectionState, + verificationCode: this.verificationCode + }; + } + + disconnect() { + // Устанавливаем флаг намеренного отключения + this.intentionalDisconnect = true; + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting intentional disconnect'); + + // Отправляем уведомление несколько раз для надежности + this.sendDisconnectNotification(); + + // Ждем немного для доставки уведомления, затем очищаем + setTimeout(() => { + this.sendDisconnectNotification(); // Еще одна попытка + }, 100); + + setTimeout(() => { + this.cleanupConnection(); + }, 500); + } + + handleUnexpectedDisconnect() { + this.sendDisconnectNotification(); + this.isVerified = false; + this.onMessage('🔌 Соединение потеряно. Попытка переподключения...', 'system'); + + setTimeout(() => { + if (!this.intentionalDisconnect) { + this.attemptReconnection(); + } + }, 3000); + } + + sendDisconnectNotification() { + try { + if (this.dataChannel && this.dataChannel.readyState === 'open') { + const notification = { + type: 'peer_disconnect', + timestamp: Date.now(), + reason: this.intentionalDisconnect ? 'user_disconnect' : 'connection_lost' + }; + + // Пытаемся отправить уведомление несколько раз + for (let i = 0; i < 3; i++) { + try { + this.dataChannel.send(JSON.stringify(notification)); + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Disconnect notification sent', { + reason: notification.reason, + attempt: i + 1 + }); + break; + } catch (sendError) { + if (i === 2) { // Последняя попытка + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Failed to send disconnect notification', { + error: sendError.message + }); + } + } + } + } + } catch (error) { + window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Could not send disconnect notification', { + error: error.message + }); + } + } + + attemptReconnection() { + this.onMessage('❌ Не удается переподключиться. Требуется новое соединение.', 'system'); + this.cleanupConnection(); + } + + handlePeerDisconnectNotification(data) { + const reason = data.reason || 'unknown'; + const reasonText = reason === 'user_disconnect' ? 'намеренно отключился' : 'потерял соединение'; + + this.onMessage(`👋 Собеседник ${reasonText}`, 'system'); + this.onStatusChange('peer_disconnected'); + + // Устанавливаем флаг что это не наше намеренное отключение + this.intentionalDisconnect = false; + this.isVerified = false; + this.stopHeartbeat(); + + // Очищаем UI данные + this.onKeyExchange(''); // Очищаем отпечаток + this.onVerificationRequired(''); // Очищаем код верификации + + // Очищаем соединение через небольшую задержку + setTimeout(() => { + this.cleanupConnection(); + }, 2000); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Peer disconnect notification processed', { + reason: reason + }); + } + + cleanupConnection() { + this.stopHeartbeat(); + this.isVerified = false; + this.processedMessageIds.clear(); + this.messageCounter = 0; + + // Полная очистка всех криптографических данных + this.encryptionKey = null; + this.macKey = null; + this.metadataKey = null; + this.keyFingerprint = null; + this.sessionSalt = null; + this.sessionId = null; + this.peerPublicKey = null; + this.verificationCode = null; + + // PFS: Очистка всех версий ключей + this.keyVersions.clear(); + this.oldKeys.clear(); + this.currentKeyVersion = 0; + this.lastKeyRotation = Date.now(); + + // Очистка пар ключей + this.ecdhKeyPair = null; + this.ecdsaKeyPair = null; + + // Сброс счетчиков сообщений + this.sequenceNumber = 0; + this.expectedSequenceNumber = 0; + + // Сброс флагов безопасности + this.securityFeatures = { + hasEncryption: false, + hasECDH: false, + hasECDSA: false, + hasMutualAuth: false, + hasMetadataProtection: false, + hasEnhancedReplayProtection: false, + hasNonExtractableKeys: false, + hasRateLimiting: false, + hasEnhancedValidation: false, + hasPFS: false + }; + + // Закрытие соединений + if (this.dataChannel) { + this.dataChannel.close(); + this.dataChannel = null; + } + if (this.peerConnection) { + this.peerConnection.close(); + this.peerConnection = null; + } + + // Очистка очереди сообщений + this.messageQueue = []; + + // ВАЖНО: Очистка логов безопасности + window.EnhancedSecureCryptoUtils.secureLog.clearLogs(); + + // Уведомляем UI о полной очистке + this.onStatusChange('disconnected'); + this.onKeyExchange(''); + this.onVerificationRequired(''); + + window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Connection cleaned up completely'); + + // Сброс флага намеренного отключения + this.intentionalDisconnect = false; + + // Принудительная сборка мусора + if (window.gc) { + window.gc(); + } + } +} + +export { EnhancedSecureWebRTCManager }; \ No newline at end of file diff --git a/src/session/PayPerSessionManager.js b/src/session/PayPerSessionManager.js new file mode 100644 index 0000000..dfc5ab7 --- /dev/null +++ b/src/session/PayPerSessionManager.js @@ -0,0 +1,588 @@ +class PayPerSessionManager { + constructor(config = {}) { + this.sessionPrices = { + free: { sats: 0, hours: 1/60, usd: 0.00 }, + basic: { sats: 500, hours: 1, usd: 0.20 }, + premium: { sats: 1000, hours: 4, usd: 0.40 }, + extended: { sats: 2000, hours: 24, usd: 0.80 } + }; + this.currentSession = null; + this.sessionTimer = null; + this.onSessionExpired = null; + this.staticLightningAddress = "dullpastry62@walletofsatoshi.com"; + + // Конфигурация для LNbits (ваши реальные данные) + this.verificationConfig = { + method: config.method || 'lnbits', + apiUrl: config.apiUrl || 'https://demo.lnbits.com', + apiKey: config.apiKey || '623515641d2e4ebcb1d5992d6d78419c', // Ваш Invoice/read ключ + walletId: config.walletId || 'bcd00f561c7b46b4a7b118f069e68997', + // Дополнительные настройки для демо + isDemo: true, + demoTimeout: 30000, // 30 секунд для демо + retryAttempts: 3 + }; + } + + hasActiveSession() { + if (!this.currentSession) return false; + return Date.now() < this.currentSession.expiresAt; + } + + createInvoice(sessionType) { + const pricing = this.sessionPrices[sessionType]; + if (!pricing) throw new Error('Invalid session type'); + + return { + amount: pricing.sats, + memo: `LockBit.chat ${sessionType} session (${pricing.hours}h)`, + sessionType: sessionType, + timestamp: Date.now(), + paymentHash: Array.from(crypto.getRandomValues(new Uint8Array(32))) + .map(b => b.toString(16).padStart(2, '0')).join(''), + lightningAddress: this.staticLightningAddress + }; + } + + // Создание реального Lightning инвойса через LNbits + async createLightningInvoice(sessionType) { + const pricing = this.sessionPrices[sessionType]; + if (!pricing) throw new Error('Invalid session type'); + + try { + console.log(`Creating ${sessionType} invoice for ${pricing.sats} sats...`); + + // Проверяем доступность API + const healthCheck = await fetch(`${this.verificationConfig.apiUrl}/api/v1/health`, { + method: 'GET', + headers: { + 'X-Api-Key': this.verificationConfig.apiKey + } + }); + + if (!healthCheck.ok) { + throw new Error(`LNbits API недоступен: ${healthCheck.status}`); + } + + const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments`, { + method: 'POST', + headers: { + 'X-Api-Key': this.verificationConfig.apiKey, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + out: false, // incoming payment + amount: pricing.sats, + memo: `LockBit.chat ${sessionType} session (${pricing.hours}h)`, + unit: 'sat', + expiry: this.verificationConfig.isDemo ? 300 : 900 // 5 минут для демо, 15 для продакшена + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('LNbits API response:', errorText); + throw new Error(`LNbits API error ${response.status}: ${errorText}`); + } + + const data = await response.json(); + + console.log('✅ Lightning invoice created successfully!', data); + + return { + paymentRequest: data.bolt11 || data.payment_request, // BOLT11 invoice для QR кода + paymentHash: data.payment_hash, + checkingId: data.checking_id || data.payment_hash, // Для проверки статуса + amount: data.amount || pricing.sats, + sessionType: sessionType, + createdAt: Date.now(), + expiresAt: Date.now() + (this.verificationConfig.isDemo ? 5 * 60 * 1000 : 15 * 60 * 1000), // 5 минут для демо + description: data.description || data.memo || `LockBit.chat ${sessionType} session`, + lnurl: data.lnurl || null, + memo: data.memo || `LockBit.chat ${sessionType} session`, + bolt11: data.bolt11 || data.payment_request, + // Дополнительные поля для совместимости + payment_request: data.bolt11 || data.payment_request, + checking_id: data.checking_id || data.payment_hash + }; + + } catch (error) { + console.error('❌ Error creating Lightning invoice:', error); + + // Для демо режима создаем фиктивный инвойс + if (this.verificationConfig.isDemo && error.message.includes('API')) { + console.log('🔄 Creating demo invoice for testing...'); + return this.createDemoInvoice(sessionType); + } + + throw error; + } + } + + // Создание демо инвойса для тестирования + createDemoInvoice(sessionType) { + const pricing = this.sessionPrices[sessionType]; + const demoHash = Array.from(crypto.getRandomValues(new Uint8Array(32))) + .map(b => b.toString(16).padStart(2, '0')).join(''); + + return { + paymentRequest: `lntb${pricing.sats}1p${demoHash}...`, // Фиктивный BOLT11 + paymentHash: demoHash, + checkingId: demoHash, + amount: pricing.sats, + sessionType: sessionType, + createdAt: Date.now(), + expiresAt: Date.now() + (5 * 60 * 1000), // 5 минут + description: `LockBit.chat ${sessionType} session (DEMO)`, + isDemo: true + }; + } + + // Проверка статуса платежа через LNbits + async checkPaymentStatus(checkingId) { + try { + console.log(`🔍 Checking payment status for: ${checkingId}`); + + const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments/${checkingId}`, { + method: 'GET', + headers: { + 'X-Api-Key': this.verificationConfig.apiKey, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Payment status check failed:', errorText); + throw new Error(`Payment check failed: ${response.status} - ${errorText}`); + } + + const data = await response.json(); + console.log('📊 Payment status response:', data); + + return { + paid: data.paid || false, + preimage: data.preimage || null, + details: data.details || {}, + amount: data.amount || 0, + fee: data.fee || 0, + timestamp: data.timestamp || Date.now(), + bolt11: data.bolt11 || null + }; + + } catch (error) { + console.error('❌ Error checking payment status:', error); + + // Для демо режима возвращаем фиктивный статус + if (this.verificationConfig.isDemo && error.message.includes('API')) { + console.log('🔄 Returning demo payment status...'); + return { + paid: false, + preimage: null, + details: { demo: true }, + amount: 0, + fee: 0, + timestamp: Date.now() + }; + } + + throw error; + } + } + + // Метод 1: Верификация через LNbits API + async verifyPaymentLNbits(preimage, paymentHash) { + try { + console.log(`🔐 Verifying payment via LNbits: ${paymentHash}`); + + if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) { + throw new Error('LNbits API configuration missing'); + } + + const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/payments/${paymentHash}`, { + method: 'GET', + headers: { + 'X-Api-Key': this.verificationConfig.apiKey, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('LNbits verification failed:', errorText); + throw new Error(`API request failed: ${response.status} - ${errorText}`); + } + + const paymentData = await response.json(); + console.log('📋 Payment verification data:', paymentData); + + // Проверяем статус платежа + if (paymentData.paid && paymentData.preimage === preimage) { + console.log('✅ Payment verified successfully via LNbits'); + return { + verified: true, + amount: paymentData.amount, + fee: paymentData.fee || 0, + timestamp: paymentData.timestamp || Date.now(), + method: 'lnbits' + }; + } + + console.log('❌ Payment verification failed: paid=', paymentData.paid, 'preimage match=', paymentData.preimage === preimage); + return { + verified: false, + reason: 'Payment not paid or preimage mismatch', + method: 'lnbits' + }; + + } catch (error) { + console.error('❌ LNbits payment verification failed:', error); + + // Для демо режима возвращаем успешную верификацию + if (this.verificationConfig.isDemo && error.message.includes('API')) { + console.log('🔄 Demo payment verification successful'); + return { + verified: true, + amount: 0, + fee: 0, + timestamp: Date.now(), + method: 'demo' + }; + } + + return { + verified: false, + reason: error.message, + method: 'lnbits' + }; + } + } + + // Метод 2: Верификация через LND REST API + async verifyPaymentLND(preimage, paymentHash) { + try { + if (!this.verificationConfig.nodeUrl || !this.verificationConfig.macaroon) { + throw new Error('LND configuration missing'); + } + + const response = await fetch(`${this.verificationConfig.nodeUrl}/v1/invoice/${paymentHash}`, { + method: 'GET', + headers: { + 'Grpc-Metadata-macaroon': this.verificationConfig.macaroon, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`LND API request failed: ${response.status}`); + } + + const invoiceData = await response.json(); + + // Проверяем, что инвойс оплачен и preimage совпадает + if (invoiceData.settled && invoiceData.r_preimage === preimage) { + return true; + } + + return false; + } catch (error) { + console.error('LND payment verification failed:', error); + return false; + } + } + + // Метод 3: Верификация через Core Lightning (CLN) + async verifyPaymentCLN(preimage, paymentHash) { + try { + if (!this.verificationConfig.nodeUrl) { + throw new Error('CLN configuration missing'); + } + + const response = await fetch(`${this.verificationConfig.nodeUrl}/v1/listinvoices`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + payment_hash: paymentHash + }) + }); + + if (!response.ok) { + throw new Error(`CLN API request failed: ${response.status}`); + } + + const data = await response.json(); + + if (data.invoices && data.invoices.length > 0) { + const invoice = data.invoices[0]; + if (invoice.status === 'paid' && invoice.payment_preimage === preimage) { + return true; + } + } + + return false; + } catch (error) { + console.error('CLN payment verification failed:', error); + return false; + } + } + + // Метод 4: Верификация через Wallet of Satoshi API (если доступен) + async verifyPaymentWOS(preimage, paymentHash) { + try { + // Wallet of Satoshi обычно не предоставляет публичного API + // Этот метод для примера структуры + console.warn('Wallet of Satoshi API verification not implemented'); + return false; + } catch (error) { + console.error('WOS payment verification failed:', error); + return false; + } + } + + // Метод 5: Верификация через BTCPay Server + async verifyPaymentBTCPay(preimage, paymentHash) { + try { + if (!this.verificationConfig.apiUrl || !this.verificationConfig.apiKey) { + throw new Error('BTCPay Server configuration missing'); + } + + const response = await fetch(`${this.verificationConfig.apiUrl}/api/v1/invoices/${paymentHash}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.verificationConfig.apiKey}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + throw new Error(`BTCPay API request failed: ${response.status}`); + } + + const invoiceData = await response.json(); + + if (invoiceData.status === 'Settled' && invoiceData.payment && invoiceData.payment.preimage === preimage) { + return true; + } + + return false; + } catch (error) { + console.error('BTCPay payment verification failed:', error); + return false; + } + } + + // Криптографическая верификация preimage + async verifyCryptographically(preimage, paymentHash) { + try { + // Преобразуем preimage в байты + const preimageBytes = new Uint8Array(preimage.match(/.{2}/g).map(byte => parseInt(byte, 16))); + + // Вычисляем SHA256 от preimage + const hashBuffer = await crypto.subtle.digest('SHA-256', preimageBytes); + const computedHash = Array.from(new Uint8Array(hashBuffer)) + .map(b => b.toString(16).padStart(2, '0')).join(''); + + // Сравниваем с payment_hash + return computedHash === paymentHash; + } catch (error) { + console.error('Cryptographic verification failed:', error); + return false; + } + } + + // Основной метод верификации платежа + async verifyPayment(preimage, paymentHash) { + console.log(`🔐 Verifying payment: preimage=${preimage}, hash=${paymentHash}`); + + // Базовые проверки формата + if (!preimage || preimage.length !== 64) { + console.log('❌ Invalid preimage length'); + return { verified: false, reason: 'Invalid preimage length' }; + } + + if (!/^[0-9a-fA-F]{64}$/.test(preimage)) { + console.log('❌ Invalid preimage format'); + return { verified: false, reason: 'Invalid preimage format' }; + } + + // Для бесплатных сессий + if (preimage === '0'.repeat(64)) { + console.log('✅ Free session preimage accepted'); + return { verified: true, method: 'free' }; + } + + // Проверяем, что preimage не является заглушкой + const dummyPreimages = ['1'.repeat(64), 'a'.repeat(64), 'f'.repeat(64)]; + if (dummyPreimages.includes(preimage)) { + console.log('❌ Dummy preimage detected'); + return { verified: false, reason: 'Dummy preimage detected' }; + } + + try { + // Сначала проверяем криптографически + const cryptoValid = await this.verifyCryptographically(preimage, paymentHash); + if (!cryptoValid) { + console.log('❌ Cryptographic verification failed'); + return { verified: false, reason: 'Cryptographic verification failed' }; + } + + console.log('✅ Cryptographic verification passed'); + + // Затем проверяем через выбранный метод + switch (this.verificationConfig.method) { + case 'lnbits': + const lnbitsResult = await this.verifyPaymentLNbits(preimage, paymentHash); + return lnbitsResult.verified ? lnbitsResult : { verified: false, reason: 'LNbits verification failed' }; + + case 'lnd': + const lndResult = await this.verifyPaymentLND(preimage, paymentHash); + return lndResult ? { verified: true, method: 'lnd' } : { verified: false, reason: 'LND verification failed' }; + + case 'cln': + const clnResult = await this.verifyPaymentCLN(preimage, paymentHash); + return clnResult ? { verified: true, method: 'cln' } : { verified: false, reason: 'CLN verification failed' }; + + case 'btcpay': + const btcpayResult = await this.verifyPaymentBTCPay(preimage, paymentHash); + return btcpayResult ? { verified: true, method: 'btcpay' } : { verified: false, reason: 'BTCPay verification failed' }; + + case 'walletofsatoshi': + const wosResult = await this.verifyPaymentWOS(preimage, paymentHash); + return wosResult ? { verified: true, method: 'wos' } : { verified: false, reason: 'WOS verification failed' }; + + default: + console.warn('Unknown verification method, using crypto-only verification'); + return { verified: cryptoValid, method: 'crypto-only' }; + } + } catch (error) { + console.error('❌ Payment verification failed:', error); + return { verified: false, reason: error.message }; + } + } + + // Остальные методы остаются без изменений... + activateSession(sessionType, preimage) { + // Очистка предыдущей сессии + this.cleanup(); + + const pricing = this.sessionPrices[sessionType]; + const now = Date.now(); + const expiresAt = now + (pricing.hours * 60 * 60 * 1000); + + this.currentSession = { + type: sessionType, + startTime: now, + expiresAt: expiresAt, + preimage: preimage + }; + + this.startSessionTimer(); + return this.currentSession; + } + + startSessionTimer() { + if (this.sessionTimer) { + clearInterval(this.sessionTimer); + } + + this.sessionTimer = setInterval(() => { + if (!this.hasActiveSession()) { + this.expireSession(); + } + }, 60000); + } + + expireSession() { + if (this.sessionTimer) { + clearInterval(this.sessionTimer); + } + + this.currentSession = null; + + if (this.onSessionExpired) { + this.onSessionExpired(); + } + } + + getTimeLeft() { + if (!this.currentSession) return 0; + return Math.max(0, this.currentSession.expiresAt - Date.now()); + } + + forceUpdateTimer() { + if (this.currentSession) { + const timeLeft = this.getTimeLeft(); + console.log('Timer updated:', timeLeft, 'ms left'); + return timeLeft; + } + return 0; + } + + cleanup() { + if (this.sessionTimer) { + clearInterval(this.sessionTimer); + } + this.currentSession = null; + } + + resetSession() { + if (this.sessionTimer) { + clearInterval(this.sessionTimer); + } + this.currentSession = null; + console.log('Session reset due to failed verification'); + } + + canActivateSession() { + return !this.hasActiveSession() && !this.currentSession; + } + + async safeActivateSession(sessionType, preimage, paymentHash) { + try { + console.log(`🚀 Activating session: ${sessionType} with preimage: ${preimage}`); + + if (!sessionType || !preimage) { + console.warn('❌ Session activation failed: missing sessionType or preimage'); + return { success: false, reason: 'Missing sessionType or preimage' }; + } + + if (!this.sessionPrices[sessionType]) { + console.warn('❌ Session activation failed: invalid session type'); + return { success: false, reason: 'Invalid session type' }; + } + + // Верифицируем платеж + const verificationResult = await this.verifyPayment(preimage, paymentHash); + + if (verificationResult.verified) { + this.activateSession(sessionType, preimage); + console.log(`✅ Session activated successfully: ${sessionType} via ${verificationResult.method}`); + return { + success: true, + sessionType: sessionType, + method: verificationResult.method, + details: verificationResult, + timeLeft: this.getTimeLeft() + }; + } else { + console.log('❌ Payment verification failed:', verificationResult.reason); + return { + success: false, + reason: verificationResult.reason, + method: verificationResult.method + }; + } + } catch (error) { + console.error('❌ Session activation failed:', error); + return { + success: false, + reason: error.message, + method: 'error' + }; + } + } +} + +export { PayPerSessionManager }; \ No newline at end of file diff --git a/src/styles/animations.css b/src/styles/animations.css new file mode 100644 index 0000000..b08f3dc --- /dev/null +++ b/src/styles/animations.css @@ -0,0 +1,29 @@ +/* Плавная прокрутка сообщений / появление */ +@keyframes messageSlideIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Пульсация иконок */ +@keyframes iconPulse { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } +} + +/* Пульс для таймера */ +@keyframes pulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.05); } +} + +/* Скролл логотипов */ +@keyframes walletLogosScroll { + 0% { transform: translateX(0); } + 100% { transform: translateX(-50%); } +} diff --git a/src/styles/components.css b/src/styles/components.css new file mode 100644 index 0000000..bbdf42e --- /dev/null +++ b/src/styles/components.css @@ -0,0 +1,366 @@ +/* Хедер и карточки */ +.header-minimal { + background: rgb(35 36 35 / 13%); + backdrop-filter: blur(5px); + -webkit-backdrop-filter: blur(5px); +} + +.header-minimal .cursor-pointer:hover { + transform: scale(1.05); + transition: transform 0.2s ease; +} +.header-minimal .cursor-pointer:active { + transform: scale(0.95); +} + +.card-minimal { + background: rgba(42, 43, 42, 0.8); + backdrop-filter: blur(16px); + border: 1px solid rgba(75, 85, 99, 0.2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} +.card-minimal:hover { + border-color: rgba(249, 115, 22, 0.3); + transform: translateY(-1px); + transition: all 0.2s ease; +} + +/* Статусы */ +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + display: inline-block; + margin-right: 8px; +} +.status-connected { background: #10b981; } +.status-connecting { background: #6b7280; } +.status-failed { background: #ef4444; } +.status-disconnected { background: #6b7280; } +.status-verifying { background: #9ca3af; } + +/* Security / verification */ +.security-shield { + background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%); + border: 1px solid rgba(75, 85, 99, 0.3); +} + +.verification-code { + background: rgba(42, 43, 42, 0.8); + border: 1px solid rgba(75, 85, 99, 0.3); + color: #f1f5f9; + font-family: 'Monaco', 'Menlo', monospace; + letter-spacing: 0.1em; + font-size: 1.2em; + padding: 8px 12px; + border-radius: 8px; + text-align: center; +} + +/* Иконки и размеры */ +.icon-container { + width: 40px; + height: 40px; + background: rgba(42, 43, 42, 0.8); + border: 1px solid rgba(75, 85, 99, 0.3); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} +.icon-container i { + font-size: 1.25rem; + line-height: 1; +} + +.icon-sm { font-size: 0.875rem; } +.icon-md { font-size: 1rem; } +.icon-lg { font-size: 1.125rem; } +.icon-xl { font-size: 1.25rem; } +.icon-2xl { font-size: 1.5rem; } + +/* Step number */ +.step-number { + width: 32px; + height: 32px; + background: linear-gradient(135deg, #f97316, #ea580c); + color: white; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 14px; +} + +/* Fallback styles for icons (ВОТ ОН — блок, который я пропустил) */ +.icon-fallback { + display: inline-block; + width: 1em; + height: 1em; + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} + +/* Ensure icons are visible */ +.fas, .far, .fab { + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; + vertical-align: middle; +} + +/* Improve icon rendering */ +.fas::before, .far::before, .fab::before { + display: inline-block; + font-style: normal; + font-variant: normal; + text-rendering: auto; + line-height: 1; +} + +/* Icon loading fallback (первый вариант) */ +.icon-loading { + opacity: 0.7; + animation: iconPulse 1.5s ease-in-out infinite; +} + +/* Fallback icons content */ +.fa-fallback .fas.fa-shield-halved::before { content: "🛡️"; } +.fa-fallback .fas.fa-shield-alt::before { content: "🛡️"; } +.fa-fallback .fas.fa-lock::before { content: "🔒"; } +.fa-fallback .fas.fa-unlock-alt::before { content: "🔓"; } +.fa-fallback .fas.fa-key::before { content: "🔑"; } +.fa-fallback .fas.fa-fingerprint::before { content: "👆"; } +.fa-fallback .fas.fa-exchange-alt::before { content: "🔄"; } +.fa-fallback .fas.fa-plus::before { content: "➕"; } +.fa-fallback .fas.fa-link::before { content: "🔗"; } +.fa-fallback .fas.fa-paste::before { content: "📋"; } +.fa-fallback .fas.fa-check-circle::before { content: "✅"; } +.fa-fallback .fas.fa-cogs::before { content: "⚙️"; } +.fa-fallback .fas.fa-rocket::before { content: "🚀"; } +.fa-fallback .fas.fa-copy::before { content: "📄"; } +.fa-fallback .fas.fa-check::before { content: "✓"; } +.fa-fallback .fas.fa-times::before { content: "✗"; } +.fa-fallback .fas.fa-exclamation-triangle::before { content: "⚠️"; } +.fa-fallback .fas.fa-info-circle::before { content: "ℹ️"; } +.fa-fallback .fas.fa-circle::before { content: "●"; } +.fa-fallback .fas.fa-paper-plane::before { content: "📤"; } +.fa-fallback .fas.fa-comments::before { content: "💬"; } +.fa-fallback .fas.fa-signature::before { content: "✍️"; } +.fa-fallback .fas.fa-power-off::before { content: "⏻"; } +.fa-fallback .fas.fa-arrow-left::before { content: "←"; } + +/* Ensure fallback icons are properly sized & use emoji font */ +.fa-fallback .fas::before { + font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: inherit; +} + +/* Icon alignment in buttons */ +button i { + vertical-align: middle; + margin-right: 0.5rem; +} + +/* Повторное определение не удалял — второй вариант icon-loading переопределит первый (как в оригинале) */ +.icon-loading { + opacity: 0.6; + animation: iconPulse 2s ease-in-out infinite; +} + +/* Чат */ +.chat-container { + display: flex; + flex-direction: column; + height: calc(100vh - 128px); + min-height: 0; +} +.chat-messages-area { + flex: 1; + min-height: 0; + overflow: hidden; +} +.chat-input-area { + flex-shrink: 0; + position: sticky; + bottom: 0; + background: rgba(42, 43, 42, 0.95); + backdrop-filter: blur(10px); +} + +/* Интерактивные индикаторы безопасности */ +.header-minimal .cursor-pointer:hover { + transform: scale(1.05); + transition: transform 0.2s ease; +} +.header-minimal .cursor-pointer:active { + transform: scale(0.95); +} + +/* Pay-per-session UI */ +.session-timer { + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + border: 1px solid rgba(249, 115, 22, 0.3); + color: white; + padding: 8px 16px; + border-radius: 8px; + font-weight: 600; + font-size: 14px; + display: flex; + align-items: center; + gap: 8px; +} + +.session-timer.warning { + background: linear-gradient(135deg, #eab308 0%, #ca8a04 100%); + animation: pulse 2s ease-in-out infinite; +} + +.session-timer.critical { + background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%); + animation: pulse 1s ease-in-out infinite; +} + +/* Lightning button */ +.lightning-button { + background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%); + border: 1px solid rgba(245, 158, 11, 0.3); + transition: all 0.3s ease; +} +.lightning-button:hover { + background: linear-gradient(135deg, #d97706 0%, #b45309 100%); + transform: translateY(-2px); +} + +/* Кнопки */ +.btn-primary { + background: linear-gradient(135deg, #f97316 0%, #ea580c 100%); + border: 1px solid rgba(249, 115, 22, 0.3); +} +.btn-primary:hover { + background: linear-gradient(135deg, #ea580c 0%, #dc2626 100%); + transform: translateY(-1px); +} + +.btn-secondary { + background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%); + border: 1px solid rgba(75, 85, 99, 0.3); +} +.btn-secondary:hover { + background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%); + transform: translateY(-1px); +} + +.btn-verify { + background: linear-gradient(135deg, #2A2B2A 0%, #3a3b3a 100%); + border: 1px solid rgba(75, 85, 99, 0.3); +} +.btn-verify:hover { + background: linear-gradient(135deg, #3a3b3a 0%, #4a4b4a 100%); + transform: translateY(-1px); +} + +/* Wallet logos container & per-wallet filters */ +.wallet-logos-container { + display: flex; + align-items: center; + overflow: hidden; + position: relative; + height: 64px; + margin: 20px 0; + width: 100%; +} + +.wallet-logos-track { + display: flex; + align-items: center; + gap: 20px; + animation: walletLogosScroll 30s linear infinite; + width: max-content; +} + +.wallet-logo { + display: flex; + align-items: center; + justify-content: center; + width: 100px; + height: 48px; + background: rgba(42, 43, 42, 0.8); + border: 1px solid rgba(75, 85, 99, 0.3); + border-radius: 8px; + font-size: 14px; + font-weight: 600; + color: #f1f5f9; + flex-shrink: 0; + transition: all 0.3s ease; +} +.wallet-logo:hover { + border-color: rgba(249, 115, 22, 0.3); + background: rgba(249, 115, 22, 0.1); + transform: translateY(-2px) scale(1.05); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); +} + +/* Примеры per-wallet классов (как в оригинале) */ +.wallet-logo.bitcoin-lightning { background: transparent; padding: 4px; } +.wallet-logo.bitcoin-lightning img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.impervious { background: transparent; padding: 4px; } +.wallet-logo.impervious img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.strike { background: transparent; padding: 4px; } +.wallet-logo.strike img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.lnbits { background: transparent; padding: 4px; } +.wallet-logo.lnbits img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.lightning-labs { background: transparent; padding: 4px; } +.wallet-logo.lightning-labs img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.atomic { background: transparent; padding: 4px; } +.wallet-logo.atomic img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.breez { background: transparent; padding: 4px; } +.wallet-logo.breez img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.alby { background: transparent; padding: 4px; } +.wallet-logo.alby img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.phoenix { background: transparent; } +.wallet-logo.blixt { background: transparent; } +.wallet-logo.zeus { background: transparent; padding: 4px; } +.wallet-logo.zeus img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.wos { background: transparent; padding: 4px; } +.wallet-logo.wos img { width: 80px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +.wallet-logo.muun { background: transparent; padding: 4px; } +.wallet-logo.muun img { width: 48px; height: 48px; filter: brightness(0) saturate(100%) invert(43%) sepia(8%) saturate(670%) hue-rotate(202deg) brightness(97%) contrast(86%); } + +/* Pause animation on hover for logos */ +.wallet-logos-container:hover .wallet-logos-track { + animation-play-state: paused; +} + +/* Анимация появления сообщений использует keyframes из animations.css */ +.message-slide { + animation: messageSlideIn 0.3s ease-out; +} + +/* Icon color improvements (повтор в оригинале — сохранил) */ +.accent-orange { color: #fb923c !important; } +.accent-green { color: #34d399 !important; } +.accent-red { color: #f87171 !important; } +.accent-yellow { color: #fbbf24 !important; } +.accent-purple { color: #a78bfa !important; } +.accent-gray { color: #9ca3af !important; } +.accent-cyan { color: #22d3ee !important; } + +/* Ensure icons visible in dark backgrounds */ +.text-secondary i { + opacity: 0.8; +} diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100644 index 0000000..656b3d8 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,96 @@ +/* Основные шрифты и цвета */ +* { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; +} + +body { + background: #2A2B2A; + overflow-x: hidden; +} + +/* Базовые фоны */ +.bg-custom-bg { + background-color: rgb(37 38 37) !important; +} + +.bg-header { + background-color: rgb(35 35 35) !important; +} + +.minimal-bg { + background: linear-gradient(135deg, #1a1a1a 0%, #2A2B2A 100%); + position: relative; + min-height: 100vh; +} + +/* Текстовые стили */ +.text-primary { color: #f1f5f9; } +.text-secondary { color: #9ca3af; } +.text-muted { color: #6b7280; } + +/* Акцентные цвета */ +.accent-orange { color: #fb923c; } +.accent-green { color: #34d399; } +.accent-red { color: #f87171; } +.accent-yellow { color: #fbbf24; } +.accent-purple { color: #a78bfa; } +.accent-blue { color: #60a5fa; } +.accent-gray { color: #9ca3af; } +.accent-cyan { color: #22d3ee; } + +/* Кастомный скролл */ +.custom-scrollbar::-webkit-scrollbar { width: 6px; } +.custom-scrollbar::-webkit-scrollbar-track { + background: rgba(42, 43, 42, 0.3); + border-radius: 3px; +} +.custom-scrollbar::-webkit-scrollbar-thumb { + background: rgba(75, 85, 99, 0.5); + border-radius: 3px; +} +.custom-scrollbar::-webkit-scrollbar-thumb:hover { + background: rgba(75, 85, 99, 0.7); +} + +/* Плавная прокрутка */ +.scroll-smooth { + scroll-behavior: smooth; +} + +/* Улучшенная прокрутка для сообщений */ +.messages-container { + scroll-behavior: smooth; + scroll-padding-bottom: 20px; +} + +/* Медиа-запросы (мобильные/планшет) */ +@media (max-width: 640px) { + .header-minimal { padding: 0 8px; } + + .icon-container { + min-width: 32px; + min-height: 32px; + } + + .verification-code { + font-size: 0.875rem; + padding: 6px 8px; + } + + .header-minimal .max-w-7xl { + padding-left: 8px; + padding-right: 8px; + } + + .header-minimal button { + min-width: 32px; + min-height: 32px; + } +} + +@media (min-width: 641px) and (max-width: 1024px) { + .header-minimal .max-w-7xl { + padding-left: 16px; + padding-right: 16px; + } +} diff --git a/test-lnbits-integration.html b/test-lnbits-integration.html new file mode 100644 index 0000000..b203272 --- /dev/null +++ b/test-lnbits-integration.html @@ -0,0 +1,360 @@ + + + + + + LNbits Integration Test + + + +
+

🔧 Тест интеграции LNbits

+ +
+

📋 Конфигурация

+

API URL: https://demo.lnbits.com

+

API Key: 623515641d2e4ebcb1d5992d6d78419c

+

Wallet ID: bcd00f561c7b46b4a7b118f069e68997

+
+ +
+

🧪 Тесты

+ + + + + + + +
+ +
+

📊 Результаты

+
+
+ +
+

📝 Логи

+
+
+
+ + + + + + + + + + + diff --git a/test.js b/test.js new file mode 100644 index 0000000..e69de29