From f07e8400cf71f6da77659d0dbb23764ea70820e8 Mon Sep 17 00:00:00 2001 From: aegisinvestment Date: Mon, 11 Aug 2025 20:52:14 -0400 Subject: [PATCH] First commit - all files added --- README-LNbits-Integration.md | 159 + ico.svg | 1 + index.html | 3622 ++++++++++++++++++++ logo/alby.svg | 14 + logo/atomic.svg | 4 + logo/blink.svg | 10 + logo/breez.svg | 10 + logo/impervious.svg | 13 + logo/lightning-labs.svg | 37 + logo/lnbits.svg | 13 + logo/muun.svg | 7 + logo/strike.svg | 20 + logo/wos.svg | 1 + logo/zeus.svg | 14 + src.zip | Bin 0 -> 49203 bytes src/components/ui/Header.jsx | 248 ++ src/components/ui/LightningPayment.jsx | 392 +++ src/components/ui/PasswordModal.jsx | 91 + src/components/ui/PaymentModal.jsx | 574 ++++ src/components/ui/SessionTimer.jsx | 45 + src/components/ui/SessionTypeSelector.jsx | 110 + src/crypto/EnhancedSecureCryptoUtils.js | 1883 ++++++++++ src/main.js | 0 src/network/EnhancedSecureWebRTCManager.js | 1448 ++++++++ src/session/PayPerSessionManager.js | 588 ++++ src/styles/animations.css | 29 + src/styles/components.css | 366 ++ src/styles/main.css | 96 + test-lnbits-integration.html | 360 ++ test.js | 0 30 files changed, 10155 insertions(+) create mode 100644 README-LNbits-Integration.md create mode 100644 ico.svg create mode 100644 index.html create mode 100644 logo/alby.svg create mode 100644 logo/atomic.svg create mode 100644 logo/blink.svg create mode 100644 logo/breez.svg create mode 100644 logo/impervious.svg create mode 100644 logo/lightning-labs.svg create mode 100644 logo/lnbits.svg create mode 100644 logo/muun.svg create mode 100644 logo/strike.svg create mode 100644 logo/wos.svg create mode 100644 logo/zeus.svg create mode 100644 src.zip create mode 100644 src/components/ui/Header.jsx create mode 100644 src/components/ui/LightningPayment.jsx create mode 100644 src/components/ui/PasswordModal.jsx create mode 100644 src/components/ui/PaymentModal.jsx create mode 100644 src/components/ui/SessionTimer.jsx create mode 100644 src/components/ui/SessionTypeSelector.jsx create mode 100644 src/crypto/EnhancedSecureCryptoUtils.js create mode 100644 src/main.js create mode 100644 src/network/EnhancedSecureWebRTCManager.js create mode 100644 src/session/PayPerSessionManager.js create mode 100644 src/styles/animations.css create mode 100644 src/styles/components.css create mode 100644 src/styles/main.css create mode 100644 test-lnbits-integration.html create mode 100644 test.js 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 0000000000000000000000000000000000000000..b93ddd9eddc6151fe7fec41006ec4f4e46e3b94c GIT binary patch literal 49203 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 Zm6Zg2pR54@P~JaFApihu-#q{T{2wSr$)*4R literal 0 HcmV?d00001 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