diff --git a/index.html b/index.html
index 16078c2..394fee8 100644
--- a/index.html
+++ b/index.html
@@ -2321,10 +2321,11 @@
keyFingerprint,
isVerified,
chatMessagesRef,
- scrollToBottom
+ scrollToBottom,
+ webrtcManager
}) => {
const [showScrollButton, setShowScrollButton] = React.useState(false);
-
+ const [showFileTransfer, setShowFileTransfer] = React.useState(false);
React.useEffect(() => {
if (chatMessagesRef.current && messages.length > 0) {
const { scrollTop, scrollHeight, clientHeight } = chatMessagesRef.current;
@@ -2472,7 +2473,28 @@
className: 'fas fa-chevron-down'
})
]),
+ React.createElement('div', {
+ key: 'file-section',
+ className: "file-transfer-section max-w-4xl mx-auto p-4 border-t border-gray-500/10"
+ }, [
+ React.createElement('button', {
+ key: 'toggle-files',
+ onClick: () => setShowFileTransfer(!showFileTransfer),
+ className: `flex items-center text-sm text-secondary hover:text-primary transition-colors ${showFileTransfer ? 'mb-4' : ''}`
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: `fas fa-${showFileTransfer ? 'chevron-up' : 'paperclip'} mr-2`
+ }),
+ showFileTransfer ? 'Hide file transfer' : 'Send files'
+ ]),
+ showFileTransfer && React.createElement(FileTransferComponent, {
+ key: 'file-transfer',
+ webrtcManager: webrtcManager,
+ isConnected: isVerified
+ })
+ ]),
// Enhanced Chat Input Area
React.createElement('div', {
key: 'chat-input',
@@ -2646,6 +2668,9 @@
}, [sessionManager]);
const webrtcManagerRef = React.useRef(null);
+ // Expose for modules/UI that run outside this closure (e.g., inline handlers)
+ // Safe because it's a ref object and we maintain it centrally here
+ window.webrtcManagerRef = webrtcManagerRef;
const addMessageWithAutoScroll = (message, type) => {
const newMessage = {
@@ -2880,30 +2905,17 @@
updateSecurityLevel().catch(console.error);
}
} else if (status === 'disconnected') {
- if (sessionManager && sessionManager.hasActiveSession()) {
- sessionManager.resetSession();
- setSessionTimeLeft(0);
- setHasActiveSession(false);
- }
- document.dispatchEvent(new CustomEvent('peer-disconnect'));
-
- // Complete UI reset on disconnect
- setKeyFingerprint('');
- setVerificationCode('');
- setSecurityLevel(null);
+ // При ошибках соединения не сбрасываем сессию полностью
+ // только обновляем статус соединения
+ setConnectionStatus('disconnected');
setIsVerified(false);
setShowVerification(false);
- setConnectionStatus('disconnected');
- setMessages([]);
-
- if (typeof console.clear === 'function') {
- console.clear();
- }
-
- setTimeout(() => {
- setSessionManager(null);
- }, 1000);
+ // Не очищаем консоль и не сбрасываем сообщения
+ // чтобы пользователь мог видеть ошибки
+
+ // Не сбрасываем сессию при ошибках соединения
+ // только при намеренном отключении
} else if (status === 'peer_disconnected') {
if (sessionManager && sessionManager.hasActiveSession()) {
sessionManager.resetSession();
@@ -2922,11 +2934,12 @@
setShowVerification(false);
setConnectionStatus('disconnected');
- setMessages([]);
-
- if (typeof console.clear === 'function') {
- console.clear();
- }
+ // Не очищаем сообщения и консоль при отключении пира
+ // чтобы сохранить историю соединения
+ // setMessages([]);
+ // if (typeof console.clear === 'function') {
+ // console.clear();
+ // }
setSessionManager(null);
}, 2000);
@@ -3057,22 +3070,59 @@
};
document.addEventListener('visibilitychange', handleVisibilityChange);
+
+ // Setup file transfer callbacks
+ if (webrtcManagerRef.current) {
+ webrtcManagerRef.current.setFileTransferCallbacks(
+ // Progress callback
+ (progress) => {
+ console.log('File progress:', progress);
+ },
+
+ // File received callback
+ (fileData) => {
+ // Auto-download received file
+ const url = URL.createObjectURL(fileData.fileBlob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileData.fileName;
+ a.click();
+ URL.revokeObjectURL(url);
+
+ addMessageWithAutoScroll(`📥 Файл загружен: ${fileData.fileName}`, 'system');
+ },
+
+ // Error callback
+ (error) => {
+ // Более мягкая обработка ошибок файлового трансфера - не закрываем сессию
+ console.error('File transfer error:', error);
+
+ if (error.includes('Connection not ready')) {
+ addMessageWithAutoScroll(`⚠️ Ошибка передачи файла: соединение не готово. Попробуйте позже.`, 'system');
+ } else if (error.includes('File too large')) {
+ addMessageWithAutoScroll(`⚠️ Файл слишком большой. Максимальный размер: 100 МБ`, 'system');
+ } else {
+ addMessageWithAutoScroll(`❌ Ошибка передачи файла: ${error}`, 'system');
+ }
+ }
+ );
+ }
+
+ return () => {
+ window.removeEventListener('beforeunload', handleBeforeUnload);
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
- return () => {
- window.removeEventListener('beforeunload', handleBeforeUnload);
- document.removeEventListener('visibilitychange', handleVisibilityChange);
-
- if (tabSwitchTimeout) {
- clearTimeout(tabSwitchTimeout);
- tabSwitchTimeout = null;
- }
-
- if (webrtcManagerRef.current) {
- console.log('🧹 Cleaning up WebRTC Manager...');
- webrtcManagerRef.current.disconnect();
- webrtcManagerRef.current = null;
- }
- };
+ if (tabSwitchTimeout) {
+ clearTimeout(tabSwitchTimeout);
+ tabSwitchTimeout = null;
+ }
+
+ if (webrtcManagerRef.current) {
+ console.log('🧹 Cleaning up WebRTC Manager...');
+ webrtcManagerRef.current.disconnect();
+ webrtcManagerRef.current = null;
+ }
+ };
}, []); // Empty dependency array to run only once
const ensureActiveSessionOrPurchase = async () => {
@@ -3426,9 +3476,11 @@
setOfferPassword('');
setAnswerPassword('');
- if (typeof console.clear === 'function') {
- console.clear();
- }
+ // Не очищаем консоль при очистке данных
+ // чтобы пользователь мог видеть ошибки
+ // if (typeof console.clear === 'function') {
+ // console.clear();
+ // }
// Cleanup pay-per-session state
if (sessionManager) {
@@ -3467,9 +3519,11 @@
setMessages([]);
- if (typeof console.clear === 'function') {
- console.clear();
- }
+ // Не очищаем консоль при отключении
+ // чтобы пользователь мог видеть ошибки
+ // if (typeof console.clear === 'function') {
+ // console.clear();
+ // }
document.dispatchEvent(new CustomEvent('peer-disconnect'));
@@ -3565,7 +3619,8 @@
keyFingerprint: keyFingerprint,
isVerified: isVerified,
chatMessagesRef: chatMessagesRef,
- scrollToBottom: scrollToBottom
+ scrollToBottom: scrollToBottom,
+ webrtcManager: webrtcManagerRef.current
})
: React.createElement(EnhancedConnectionSetup, {
onCreateOffer: handleCreateOffer,
@@ -3629,10 +3684,11 @@
try {
const timestamp = Date.now();
- const [cryptoModule, webrtcModule, paymentModule] = await Promise.all([
+ const [cryptoModule, webrtcModule, paymentModule, fileTransferModule] = await Promise.all([
import(`./src/crypto/EnhancedSecureCryptoUtils.js?v=${timestamp}`),
import(`./src/network/EnhancedSecureWebRTCManager.js?v=${timestamp}`),
- import(`./src/session/PayPerSessionManager.js?v=${timestamp}`)
+ import(`./src/session/PayPerSessionManager.js?v=${timestamp}`),
+ import(`./src/transfer/EnhancedSecureFileTransfer.js?v=${timestamp}`)
]);
const { EnhancedSecureCryptoUtils } = cryptoModule;
@@ -3644,6 +3700,9 @@
const { PayPerSessionManager } = paymentModule;
window.PayPerSessionManager = PayPerSessionManager;
+ const { EnhancedSecureFileTransfer } = fileTransferModule;
+ window.EnhancedSecureFileTransfer = EnhancedSecureFileTransfer;
+
async function loadReactComponent(path, componentName) {
try {
@@ -3669,7 +3728,8 @@
loadReactComponent('./src/components/ui/SessionTypeSelector.jsx', 'SessionTypeSelector'),
loadReactComponent('./src/components/ui/LightningPayment.jsx', 'LightningPayment'),
loadReactComponent('./src/components/ui/PaymentModal.jsx', 'PaymentModal'),
- loadReactComponent('./src/components/ui/DownloadApps.jsx', 'DownloadApps')
+ loadReactComponent('./src/components/ui/DownloadApps.jsx', 'DownloadApps'),
+ loadReactComponent('./src/components/ui/FileTransfer.jsx', 'FileTransferComponent')
]);
if (typeof initializeApp === 'function') {
@@ -3711,6 +3771,18 @@ document.addEventListener('session-activated', (event) => {
if (window.forceUpdateHeader) {
window.forceUpdateHeader(event.detail.timeLeft, event.detail.sessionType);
}
+
+ // Notify WebRTC Manager about session activation
+ if (window.webrtcManager && window.webrtcManager.handleSessionActivation) {
+ console.log('🔐 Notifying WebRTC Manager about session activation');
+ window.webrtcManager.handleSessionActivation({
+ sessionId: event.detail.sessionId,
+ sessionType: event.detail.sessionType,
+ timeLeft: event.detail.timeLeft,
+ isDemo: event.detail.isDemo,
+ sessionManager: window.sessionManager
+ });
+ }
});
if (window.DEBUG_MODE) {
diff --git a/src/components/ui/FileTransfer.jsx b/src/components/ui/FileTransfer.jsx
new file mode 100644
index 0000000..cb3b7e3
--- /dev/null
+++ b/src/components/ui/FileTransfer.jsx
@@ -0,0 +1,310 @@
+// File Transfer Component for Chat Interface
+const FileTransferComponent = ({ webrtcManager, isConnected }) => {
+ const [dragOver, setDragOver] = React.useState(false);
+ const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
+ const fileInputRef = React.useRef(null);
+
+ // Update transfers periodically
+ React.useEffect(() => {
+ if (!isConnected || !webrtcManager) return;
+
+ const updateTransfers = () => {
+ const currentTransfers = webrtcManager.getFileTransfers();
+ setTransfers(currentTransfers);
+ };
+
+ const interval = setInterval(updateTransfers, 500);
+ return () => clearInterval(interval);
+ }, [isConnected, webrtcManager]);
+
+ // Setup file transfer callbacks
+ React.useEffect(() => {
+ if (!webrtcManager) return;
+
+ webrtcManager.setFileTransferCallbacks(
+ // Progress callback
+ (progress) => {
+ const currentTransfers = webrtcManager.getFileTransfers();
+ setTransfers(currentTransfers);
+ },
+
+ // File received callback
+ (fileData) => {
+ // Auto-download received file
+ const url = URL.createObjectURL(fileData.fileBlob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileData.fileName;
+ a.click();
+ URL.revokeObjectURL(url);
+
+ // Update transfer list
+ const currentTransfers = webrtcManager.getFileTransfers();
+ setTransfers(currentTransfers);
+ },
+
+ // Error callback
+ (error) => {
+ console.error('File transfer error:', error);
+ const currentTransfers = webrtcManager.getFileTransfers();
+ setTransfers(currentTransfers);
+ }
+ );
+ }, [webrtcManager]);
+
+ const handleFileSelect = async (files) => {
+ if (!isConnected || !webrtcManager) {
+ alert('Соединение не установлено. Сначала установите соединение.');
+ return;
+ }
+
+ // Дополнительная проверка состояния соединения
+ if (!webrtcManager.isConnected() || !webrtcManager.isVerified) {
+ alert('Соединение не готово для передачи файлов. Дождитесь завершения установки соединения.');
+ return;
+ }
+
+ for (const file of files) {
+ try {
+ await webrtcManager.sendFile(file);
+ } catch (error) {
+ // Более мягкая обработка ошибок - не закрываем сессию
+ console.error(`Failed to send ${file.name}:`, error);
+
+ // Показываем пользователю ошибку, но не закрываем соединение
+ if (error.message.includes('Connection not ready')) {
+ alert(`Файл ${file.name} не может быть отправлен сейчас. Проверьте соединение и попробуйте снова.`);
+ } else if (error.message.includes('File too large')) {
+ alert(`Файл ${file.name} слишком большой. Максимальный размер: 100 MB`);
+ } else if (error.message.includes('Maximum concurrent transfers')) {
+ alert(`Достигнут лимит одновременных передач. Дождитесь завершения текущих передач.`);
+ } else {
+ alert(`Ошибка отправки файла ${file.name}: ${error.message}`);
+ }
+ }
+ }
+ };
+
+ const handleDrop = (e) => {
+ e.preventDefault();
+ setDragOver(false);
+
+ const files = Array.from(e.dataTransfer.files);
+ handleFileSelect(files);
+ };
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ setDragOver(true);
+ };
+
+ const handleDragLeave = (e) => {
+ e.preventDefault();
+ setDragOver(false);
+ };
+
+ const handleFileInputChange = (e) => {
+ const files = Array.from(e.target.files);
+ handleFileSelect(files);
+ e.target.value = ''; // Reset input
+ };
+
+ const formatFileSize = (bytes) => {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
+ };
+
+ if (!isConnected) {
+ return React.createElement('div', {
+ className: "p-4 text-center text-muted"
+ }, 'Передача файлов доступна только при установленном соединении');
+ }
+
+ // Проверяем дополнительное состояние соединения
+ const isConnectionReady = webrtcManager && webrtcManager.isConnected() && webrtcManager.isVerified;
+
+ if (!isConnectionReady) {
+ return React.createElement('div', {
+ className: "p-4 text-center text-yellow-600"
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-exclamation-triangle mr-2'
+ }),
+ 'Соединение устанавливается... Передача файлов будет доступна после завершения установки.'
+ ]);
+ }
+
+ return React.createElement('div', {
+ className: "file-transfer-component"
+ }, [
+ // File Drop Zone
+ React.createElement('div', {
+ key: 'drop-zone',
+ className: `file-drop-zone ${dragOver ? 'drag-over' : ''}`,
+ onDrop: handleDrop,
+ onDragOver: handleDragOver,
+ onDragLeave: handleDragLeave,
+ onClick: () => fileInputRef.current?.click()
+ }, [
+ React.createElement('div', {
+ key: 'drop-content',
+ className: "drop-content"
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-cloud-upload-alt text-2xl mb-2 text-blue-400'
+ }),
+ React.createElement('p', {
+ key: 'text',
+ className: "text-primary font-medium"
+ }, 'Перетащите файлы сюда или нажмите для выбора'),
+ React.createElement('p', {
+ key: 'subtext',
+ className: "text-muted text-sm"
+ }, 'Максимальный размер: 100 МБ на файл')
+ ])
+ ]),
+
+ // Hidden file input
+ React.createElement('input', {
+ key: 'file-input',
+ ref: fileInputRef,
+ type: 'file',
+ multiple: true,
+ className: 'hidden',
+ onChange: handleFileInputChange
+ }),
+
+ // Active Transfers
+ (transfers.sending.length > 0 || transfers.receiving.length > 0) && React.createElement('div', {
+ key: 'transfers',
+ className: "active-transfers mt-4"
+ }, [
+ React.createElement('h4', {
+ key: 'title',
+ className: "text-primary font-medium mb-3 flex items-center"
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-exchange-alt mr-2'
+ }),
+ 'Передача файлов'
+ ]),
+
+ // Sending files
+ ...transfers.sending.map(transfer =>
+ React.createElement('div', {
+ key: `send-${transfer.fileId}`,
+ className: "transfer-item bg-blue-500/10 border border-blue-500/20 rounded-lg p-3 mb-2"
+ }, [
+ React.createElement('div', {
+ key: 'header',
+ className: "flex items-center justify-between mb-2"
+ }, [
+ React.createElement('div', {
+ key: 'info',
+ className: "flex items-center"
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-upload text-blue-400 mr-2'
+ }),
+ React.createElement('span', {
+ key: 'name',
+ className: "text-primary font-medium text-sm"
+ }, transfer.fileName),
+ React.createElement('span', {
+ key: 'size',
+ className: "text-muted text-xs ml-2"
+ }, formatFileSize(transfer.fileSize))
+ ]),
+ React.createElement('button', {
+ key: 'cancel',
+ onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
+ className: "text-red-400 hover:text-red-300 text-xs"
+ }, [
+ React.createElement('i', {
+ className: 'fas fa-times'
+ })
+ ])
+ ]),
+ React.createElement('div', {
+ key: 'progress',
+ className: "progress-bar"
+ }, [
+ React.createElement('div', {
+ key: 'fill',
+ className: "progress-fill bg-blue-400",
+ style: { width: `${transfer.progress}%` }
+ }),
+ React.createElement('span', {
+ key: 'text',
+ className: "progress-text text-xs"
+ }, `${transfer.progress.toFixed(1)}% • ${transfer.status}`)
+ ])
+ ])
+ ),
+
+ // Receiving files
+ ...transfers.receiving.map(transfer =>
+ React.createElement('div', {
+ key: `recv-${transfer.fileId}`,
+ className: "transfer-item bg-green-500/10 border border-green-500/20 rounded-lg p-3 mb-2"
+ }, [
+ React.createElement('div', {
+ key: 'header',
+ className: "flex items-center justify-between mb-2"
+ }, [
+ React.createElement('div', {
+ key: 'info',
+ className: "flex items-center"
+ }, [
+ React.createElement('i', {
+ key: 'icon',
+ className: 'fas fa-download text-green-400 mr-2'
+ }),
+ React.createElement('span', {
+ key: 'name',
+ className: "text-primary font-medium text-sm"
+ }, transfer.fileName),
+ React.createElement('span', {
+ key: 'size',
+ className: "text-muted text-xs ml-2"
+ }, formatFileSize(transfer.fileSize))
+ ]),
+ React.createElement('button', {
+ key: 'cancel',
+ onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId),
+ className: "text-red-400 hover:text-red-300 text-xs"
+ }, [
+ React.createElement('i', {
+ className: 'fas fa-times'
+ })
+ ])
+ ]),
+ React.createElement('div', {
+ key: 'progress',
+ className: "progress-bar"
+ }, [
+ React.createElement('div', {
+ key: 'fill',
+ className: "progress-fill bg-green-400",
+ style: { width: `${transfer.progress}%` }
+ }),
+ React.createElement('span', {
+ key: 'text',
+ className: "progress-text text-xs"
+ }, `${transfer.progress.toFixed(1)}% • ${transfer.status}`)
+ ])
+ ])
+ )
+ ])
+ ]);
+};
+
+// Export
+window.FileTransferComponent = FileTransferComponent;
\ No newline at end of file
diff --git a/src/network/EnhancedSecureWebRTCManager.js b/src/network/EnhancedSecureWebRTCManager.js
index 866135b..3bd4f72 100644
--- a/src/network/EnhancedSecureWebRTCManager.js
+++ b/src/network/EnhancedSecureWebRTCManager.js
@@ -1,3 +1,6 @@
+// Import EnhancedSecureFileTransfer
+import { EnhancedSecureFileTransfer } from '../transfer/EnhancedSecureFileTransfer.js';
+
class EnhancedSecureWebRTCManager {
constructor(onMessage, onStatusChange, onKeyExchange, onVerificationRequired, onAnswerError = null) {
// Check the availability of the global object
@@ -30,6 +33,10 @@ class EnhancedSecureWebRTCManager {
this.messageQueue = [];
this.ecdhKeyPair = null;
this.ecdsaKeyPair = null;
+ if (this.fileTransferSystem) {
+ this.fileTransferSystem.cleanup();
+ this.fileTransferSystem = null;
+ }
this.verificationCode = null;
this.isVerified = false;
this.processedMessageIds = new Set();
@@ -42,6 +49,11 @@ class EnhancedSecureWebRTCManager {
this.rateLimiterId = null;
this.intentionalDisconnect = false;
this.lastCleanupTime = Date.now();
+ // File transfer integration
+ this.fileTransferSystem = null;
+ this.onFileProgress = null;
+ this.onFileReceived = null;
+ this.onFileError = null;
// PFS (Perfect Forward Secrecy) Implementation
this.keyRotationInterval = 300000; // 5 minutes
@@ -79,83 +91,169 @@ class EnhancedSecureWebRTCManager {
// ============================================
// 1. Nested Encryption Layer
- this.nestedEncryptionKey = null;
- this.nestedEncryptionIV = null;
- this.nestedEncryptionCounter = 0;
-
- // 2. Packet Padding
- this.paddingConfig = {
- enabled: true,
- minPadding: 64,
- maxPadding: 512,
- useRandomPadding: true,
- preserveMessageSize: false
- };
-
- // 3. Fake Traffic Generation
- this.fakeTrafficConfig = {
- enabled: !window.DISABLE_FAKE_TRAFFIC,
- minInterval: 15000,
- maxInterval: 30000,
- minSize: 32,
- maxSize: 128,
- patterns: ['heartbeat', 'status', 'sync']
- };
- this.fakeTrafficTimer = null;
- this.lastFakeTraffic = 0;
-
- // 4. Message Chunking
- this.chunkingConfig = {
- enabled: false,
- maxChunkSize: 2048,
- minDelay: 100,
- maxDelay: 500,
- useRandomDelays: true,
- addChunkHeaders: true
- };
- this.chunkQueue = [];
- this.chunkingInProgress = false;
-
- // 5. Decoy Channels
- this.decoyChannels = new Map();
- this.decoyChannelConfig = {
- enabled: !window.DISABLE_DECOY_CHANNELS,
- maxDecoyChannels: 1,
- decoyChannelNames: ['heartbeat'],
- sendDecoyData: true,
- randomDecoyIntervals: true
- };
- this.decoyTimers = new Map();
-
- // 6. Packet Reordering Protection
- this.reorderingConfig = {
- enabled: false,
- maxOutOfOrder: 5,
- reorderTimeout: 3000,
- useSequenceNumbers: true,
- useTimestamps: true
- };
- this.packetBuffer = new Map(); // sequence -> {data, timestamp}
- this.lastProcessedSequence = -1;
-
- // 7. Anti-Fingerprinting
- this.antiFingerprintingConfig = {
- enabled: false,
- randomizeTiming: true,
- randomizeSizes: false,
- addNoise: true,
- maskPatterns: false,
- useRandomHeaders: false
- };
- this.fingerprintMask = this.generateFingerprintMask();
-
- // Initialize rate limiter ID
- this.rateLimiterId = `webrtc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
-
- // Start periodic cleanup
- this.startPeriodicCleanup();
-
- this.initializeEnhancedSecurity();
+ this.nestedEncryptionKey = null;
+ this.nestedEncryptionIV = null;
+ this.nestedEncryptionCounter = 0;
+
+ // 2. Packet Padding
+ this.paddingConfig = {
+ enabled: true,
+ minPadding: 64,
+ maxPadding: 512,
+ useRandomPadding: true,
+ preserveMessageSize: false
+ };
+
+ // 3. Fake Traffic Generation
+ this.fakeTrafficConfig = {
+ enabled: !window.DISABLE_FAKE_TRAFFIC,
+ minInterval: 15000,
+ maxInterval: 30000,
+ minSize: 32,
+ maxSize: 128,
+ patterns: ['heartbeat', 'status', 'sync']
+ };
+ this.fakeTrafficTimer = null;
+ this.lastFakeTraffic = 0;
+
+ // 4. Message Chunking
+ this.chunkingConfig = {
+ enabled: false,
+ maxChunkSize: 2048,
+ minDelay: 100,
+ maxDelay: 500,
+ useRandomDelays: true,
+ addChunkHeaders: true
+ };
+ this.chunkQueue = [];
+ this.chunkingInProgress = false;
+
+ // 5. Decoy Channels
+ this.decoyChannels = new Map();
+ this.decoyChannelConfig = {
+ enabled: !window.DISABLE_DECOY_CHANNELS,
+ maxDecoyChannels: 1,
+ decoyChannelNames: ['heartbeat'],
+ sendDecoyData: true,
+ randomDecoyIntervals: true
+ };
+ this.decoyTimers = new Map();
+
+ // 6. Packet Reordering Protection
+ this.reorderingConfig = {
+ enabled: false,
+ maxOutOfOrder: 5,
+ reorderTimeout: 3000,
+ useSequenceNumbers: true,
+ useTimestamps: true
+ };
+ this.packetBuffer = new Map(); // sequence -> {data, timestamp}
+ this.lastProcessedSequence = -1;
+
+ // 7. Anti-Fingerprinting
+ this.antiFingerprintingConfig = {
+ enabled: false,
+ randomizeTiming: true,
+ randomizeSizes: false,
+ addNoise: true,
+ maskPatterns: false,
+ useRandomHeaders: false
+ };
+ this.fingerprintMask = this.generateFingerprintMask();
+
+ // Initialize rate limiter ID
+ this.rateLimiterId = `webrtc_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+ // Start periodic cleanup
+ this.startPeriodicCleanup();
+
+ this.initializeEnhancedSecurity();
+ }
+ initializeFileTransfer() {
+ try {
+ console.log('🔧 Initializing Enhanced Secure File Transfer system...');
+
+ // ИСПРАВЛЕНИЕ: Более мягкая проверка готовности
+ if (!this.dataChannel || this.dataChannel.readyState !== 'open') {
+ console.warn('⚠️ Data channel not open, deferring file transfer initialization');
+ // Попробуем позже, не бросаем ошибку
+ setTimeout(() => {
+ if (this.dataChannel && this.dataChannel.readyState === 'open') {
+ this.initializeFileTransfer();
+ }
+ }, 1000);
+ return;
+ }
+
+ // ИСПРАВЛЕНИЕ: Очищаем предыдущую систему если есть
+ if (this.fileTransferSystem) {
+ console.log('🧹 Cleaning up existing file transfer system');
+ this.fileTransferSystem.cleanup();
+ this.fileTransferSystem = null;
+ }
+
+ this.fileTransferSystem = new EnhancedSecureFileTransfer(
+ this, // Pass WebRTC manager reference
+
+ // Progress callback
+ (progress) => {
+ if (this.onFileProgress) {
+ this.onFileProgress(progress);
+ }
+
+ const progressMsg = `📁 ${progress.fileName || 'Unknown file'}: ${progress.progress.toFixed(1)}% (${progress.status})`;
+ if (this.onMessage) {
+ this.onMessage(progressMsg, 'system');
+ }
+ },
+
+ // Completion callback
+ (result) => {
+ const completionMsg = `✅ File sent: ${result.fileName} (${(result.transferTime / 1000).toFixed(1)}s)`;
+ if (this.onMessage) {
+ this.onMessage(completionMsg, 'system');
+ }
+ },
+
+ // Error callback
+ (error) => {
+ if (this.onFileError) {
+ this.onFileError(error);
+ }
+
+ if (this.onMessage) {
+ this.onMessage(`❌ File transfer error: ${error}`, 'system');
+ }
+ },
+
+ // File received callback
+ (fileData) => {
+ if (this.onFileReceived) {
+ this.onFileReceived(fileData);
+ }
+
+ const receivedMsg = `📥 File received: ${fileData.fileName} (${(fileData.fileSize / 1024 / 1024).toFixed(2)} MB)`;
+ if (this.onMessage) {
+ this.onMessage(receivedMsg, 'system');
+ }
+ }
+ );
+
+ console.log('✅ Enhanced Secure File Transfer system initialized successfully');
+
+ // КРИТИЧЕСКОЕ ДОБАВЛЕНИЕ: Проверяем что система готова
+ const status = this.fileTransferSystem.getSystemStatus();
+ console.log('🔍 File transfer system status after init:', status);
+
+ } catch (error) {
+ console.error('❌ Failed to initialize file transfer system:', error);
+ this.fileTransferSystem = null;
+
+ // Не выбрасываем ошибку, чтобы не нарушить основное соединение
+ if (this.onMessage) {
+ this.onMessage('⚠️ File transfer system initialization failed. File transfers may not work.', 'system');
+ }
+ }
}
// ============================================
@@ -1638,7 +1736,23 @@ async processOrderedPackets() {
dataSample: typeof data === 'string' ? data.substring(0, 50) : 'not string'
});
- // For regular text messages, send in simple format without encryption
+ // ИСПРАВЛЕНИЕ: Проверяем, не является ли это файловым сообщением
+ if (typeof data === 'string') {
+ try {
+ const parsed = JSON.parse(data);
+
+ // Файловые сообщения отправляем напрямую без дополнительного шифрования
+ if (parsed.type && parsed.type.startsWith('file_')) {
+ console.log('📁 Sending file message directly:', parsed.type);
+ this.dataChannel.send(data);
+ return true;
+ }
+ } catch (jsonError) {
+ // Не JSON - продолжаем обычную обработку
+ }
+ }
+
+ // Для обычных текстовых сообщений используем простой формат
if (typeof data === 'string') {
const message = {
type: 'message',
@@ -1646,13 +1760,11 @@ async processOrderedPackets() {
timestamp: Date.now()
};
- if (window.DEBUG_MODE) {
- console.log('📤 Sending regular message:', message.data.substring(0, 100));
- }
+ console.log('📤 Sending regular message:', message.data.substring(0, 100));
const messageString = JSON.stringify(message);
console.log('📤 ACTUALLY SENDING:', {
- messageString: messageString,
+ messageString: messageString.substring(0, 100),
messageLength: messageString.length,
dataChannelState: this.dataChannel.readyState,
isInitiator: this.isInitiator,
@@ -1664,7 +1776,7 @@ async processOrderedPackets() {
return true;
}
- // For binary data, apply security layers
+ // Для бинарных данных применяем security layers
console.log('🔐 Applying security layers to non-string data');
const securedData = await this.applySecurityLayers(data, false);
this.dataChannel.send(securedData);
@@ -1699,157 +1811,158 @@ async processOrderedPackets() {
}
async processMessage(data) {
- try {
- console.log('📨 Processing message:', {
- dataType: typeof data,
- isArrayBuffer: data instanceof ArrayBuffer,
- dataLength: data?.length || data?.byteLength || 0
- });
-
- // DEBUG: Check if this is a user message at the start
- if (typeof data === 'string') {
- try {
- const parsed = JSON.parse(data);
- if (parsed.type === 'message') {
- console.log('🎯 USER MESSAGE IN PROCESSMESSAGE:', {
- type: parsed.type,
- data: parsed.data,
- timestamp: parsed.timestamp
- });
- }
- } catch (e) {
- // Not JSON
- }
- }
-
- // Check system messages and regular messages directly
- if (typeof data === 'string') {
- try {
- const systemMessage = JSON.parse(data);
-
- if (systemMessage.type === 'fake') {
- console.log(`🎭 Fake message blocked at entry: ${systemMessage.pattern}`);
- return;
- }
-
- if (systemMessage.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'key_rotation_signal', 'key_rotation_ready', 'security_upgrade'].includes(systemMessage.type)) {
- console.log('🔧 Processing system message directly:', systemMessage.type);
- this.handleSystemMessage(systemMessage);
- return;
- }
-
- if (systemMessage.type === 'message') {
- if (window.DEBUG_MODE) {
- console.log('📝 Regular message detected, extracting for display:', systemMessage.data);
+ try {
+ console.log('📨 Processing message:', {
+ dataType: typeof data,
+ isArrayBuffer: data instanceof ArrayBuffer,
+ dataLength: data?.length || data?.byteLength || 0
+ });
+
+ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Ранняя проверка на файловые сообщения
+ if (typeof data === 'string') {
+ try {
+ const parsed = JSON.parse(data);
+
+ // ИСПРАВЛЕНИЕ: Обработка файловых сообщений
+ if (parsed.type && parsed.type.startsWith('file_')) {
+ console.log('📁 File message detected in processMessage:', parsed.type);
+
+ // КРИТИЧЕСКИ ВАЖНО: Передаем напрямую в файловую систему
+ if (this.fileTransferSystem) {
+ console.log('📁 Forwarding file message to file transfer system');
+
+ // Вызываем обработчики файловой системы напрямую
+ switch (parsed.type) {
+ case 'file_transfer_start':
+ await this.fileTransferSystem.handleFileTransferStart(parsed);
+ break;
+ case 'file_chunk':
+ await this.fileTransferSystem.handleFileChunk(parsed);
+ break;
+ case 'file_transfer_response':
+ this.fileTransferSystem.handleTransferResponse(parsed);
+ break;
+ case 'chunk_confirmation':
+ this.fileTransferSystem.handleChunkConfirmation(parsed);
+ break;
+ case 'file_transfer_complete':
+ this.fileTransferSystem.handleTransferComplete(parsed);
+ break;
+ case 'file_transfer_error':
+ this.fileTransferSystem.handleTransferError(parsed);
+ break;
+ default:
+ console.warn('⚠️ Unknown file message type:', parsed.type);
+ }
+ return; // ВАЖНО: Выходим после обработки файлового сообщения
+ } else {
+ console.error('❌ File transfer system not initialized for file message:', parsed.type);
+ return;
+ }
}
- // Call the message handler directly for regular messages
- if (this.onMessage && systemMessage.data) {
- console.log('📤 Calling message handler with regular message:', systemMessage.data.substring(0, 100));
- this.onMessage(systemMessage.data, 'received');
+ // Обработка обычных пользовательских сообщений
+ if (parsed.type === 'message') {
+ console.log('📝 Regular user message detected in processMessage');
+ if (this.onMessage && parsed.data) {
+ this.onMessage(parsed.data, 'received');
+ }
+ return;
+ }
+
+ // Системные сообщения
+ if (parsed.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'security_upgrade'].includes(parsed.type)) {
+ console.log('🔧 System message in processMessage:', parsed.type);
+ this.handleSystemMessage(parsed);
+ return;
+ }
+
+ // Fake messages
+ if (parsed.type === 'fake') {
+ console.log('🎭 Fake message blocked in processMessage:', parsed.pattern);
+ return;
+ }
+
+ } catch (jsonError) {
+ // Не JSON - обрабатываем как текст
+ console.log('📄 Non-JSON string message in processMessage');
+ if (this.onMessage) {
+ this.onMessage(data, 'received');
}
- return; // Don't continue processing
- }
- console.log('📨 Unknown message type, continuing to processing:', systemMessage.type);
-
- } catch (e) {
- console.log('📄 Not JSON, continuing to processing as raw data');
- }
- }
-
- // Validate input data
- if (!data) {
- console.warn('⚠️ Received empty data in processMessage');
- return;
- }
-
- const originalData = await this.removeSecurityLayers(data);
-
- if (originalData === 'FAKE_MESSAGE_FILTERED') {
- console.log('🎭 Fake message successfully filtered, not displaying to user');
- return;
- }
-
- if (!originalData) {
- console.warn('⚠️ No data returned from removeSecurityLayers');
- return;
- }
-
- console.log('🔍 After removeSecurityLayers:', {
- dataType: typeof originalData,
- isString: typeof originalData === 'string',
- isObject: typeof originalData === 'object',
- hasMessage: originalData?.message,
- value: typeof originalData === 'string' ? originalData.substring(0, 100) : 'not string',
- constructor: originalData?.constructor?.name
- });
-
- let messageText;
-
- if (typeof originalData === 'string') {
- try {
- const message = JSON.parse(originalData);
- if (message.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'security_upgrade'].includes(message.type)) {
- this.handleSystemMessage(message);
return;
}
-
- if (message.type === 'fake') {
- console.log(`🎭 Post-decryption fake message blocked: ${message.pattern}`);
- return;
- }
-
- // Handle regular messages with type 'message'
- if (message.type === 'message' && message.data) {
- if (window.DEBUG_MODE) {
- console.log('📝 Regular message detected, extracting data for display');
+ }
+
+ // Если дошли сюда - применяем security layers
+ const originalData = await this.removeSecurityLayers(data);
+
+ if (originalData === 'FAKE_MESSAGE_FILTERED') {
+ console.log('🎭 Fake message successfully filtered in processMessage');
+ return;
+ }
+
+ if (!originalData) {
+ console.warn('⚠️ No data returned from removeSecurityLayers');
+ return;
+ }
+
+ // Обработка результата после removeSecurityLayers
+ let messageText;
+
+ if (typeof originalData === 'string') {
+ try {
+ const message = JSON.parse(originalData);
+ if (message.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'security_upgrade'].includes(message.type)) {
+ this.handleSystemMessage(message);
+ return;
}
- messageText = message.data;
- } else {
- // Not a recognized message type, treat as plain text
+
+ if (message.type === 'fake') {
+ console.log(`🎭 Post-decryption fake message blocked: ${message.pattern}`);
+ return;
+ }
+
+ // Обычные сообщения
+ if (message.type === 'message' && message.data) {
+ messageText = message.data;
+ } else {
+ messageText = originalData;
+ }
+ } catch (e) {
messageText = originalData;
}
- } catch (e) {
- // Not JSON - treat as plain text
- messageText = originalData;
+ } else if (originalData instanceof ArrayBuffer) {
+ messageText = new TextDecoder().decode(originalData);
+ } else if (originalData && typeof originalData === 'object' && originalData.message) {
+ messageText = originalData.message;
+ } else {
+ console.warn('⚠️ Unexpected data type after processing:', typeof originalData);
+ return;
}
- } else if (originalData instanceof ArrayBuffer) {
- messageText = new TextDecoder().decode(originalData);
- } else if (originalData && typeof originalData === 'object' && originalData.message) {
- messageText = originalData.message;
- } else {
- console.warn('⚠️ Unexpected data type after processing:', typeof originalData);
- console.warn('Data content:', originalData);
- return;
- }
- // FINAL CHECK FOR FAKE MESSAGES IN TEXT (only if it's JSON)
- if (messageText && messageText.trim().startsWith('{')) {
- try {
- const finalCheck = JSON.parse(messageText);
- if (finalCheck.type === 'fake') {
- console.log(`🎭 Final fake message check blocked: ${finalCheck.pattern}`);
- return;
+ // Финальная проверка на fake сообщения
+ if (messageText && messageText.trim().startsWith('{')) {
+ try {
+ const finalCheck = JSON.parse(messageText);
+ if (finalCheck.type === 'fake') {
+ console.log(`🎭 Final fake message check blocked: ${finalCheck.pattern}`);
+ return;
+ }
+ } catch (e) {
+ // Не JSON - это нормально для обычных текстовых сообщений
}
- } catch (e) {
- // Not JSON - this is fine for regular text messages
}
- }
- // Call the message handler ONLY for real messages
- if (this.onMessage && messageText) {
- if (window.DEBUG_MODE) {
+ // Отправляем сообщение пользователю
+ if (this.onMessage && messageText) {
console.log('📤 Calling message handler with:', messageText.substring(0, 100));
+ this.onMessage(messageText, 'received');
}
- this.onMessage(messageText, 'received');
- } else {
- console.warn('⚠️ No message handler or empty message text');
- }
- } catch (error) {
- console.error('❌ Failed to process message:', error);
+ } catch (error) {
+ console.error('❌ Failed to process message:', error);
+ }
}
-}
notifySecurityUpdate() {
try {
@@ -2223,12 +2336,24 @@ handleSystemMessage(message) {
} catch (error) {
console.error('❌ Failed to establish enhanced connection:', error);
+ // Не закрываем соединение при ошибках установки
+ // просто логируем ошибку и продолжаем
+ this.onStatusChange('disconnected');
throw error;
}
}
disconnect() {
try {
+ console.log('🔌 Disconnecting WebRTC Manager...');
+
+ // Cleanup file transfer system
+ if (this.fileTransferSystem) {
+ console.log('🧹 Cleaning up file transfer system during disconnect...');
+ this.fileTransferSystem.cleanup();
+ this.fileTransferSystem = null;
+ }
+
// Stop fake traffic generation
this.stopFakeTrafficGeneration();
@@ -2411,18 +2536,23 @@ handleSystemMessage(message) {
this.onStatusChange('disconnected');
setTimeout(() => this.cleanupConnection(), 100);
} else {
- // Unexpected disconnection — attempting to notify partner.
- this.onStatusChange('reconnecting');
- this.handleUnexpectedDisconnect();
+ // Unexpected disconnection — не пытаемся переподключиться автоматически
+ this.onStatusChange('disconnected');
+ // Не вызываем cleanupConnection автоматически
+ // чтобы не закрывать сессию при ошибках соединения
}
} 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);
- }
+ // Не пытаемся переподключиться автоматически
+ // чтобы не закрывать сессию при ошибках соединения
+ this.onStatusChange('disconnected');
+ // if (!this.intentionalDisconnect && this.connectionAttempts < this.maxConnectionAttempts) {
+ // this.connectionAttempts++;
+ // setTimeout(() => this.retryConnection(), 2000);
+ // } else {
+ // this.onStatusChange('disconnected');
+ // // Не вызываем cleanupConnection автоматически для состояния 'failed'
+ // // чтобы не закрывать сессию при ошибках соединения
+ // }
} else {
this.onStatusChange(state);
}
@@ -2470,9 +2600,18 @@ handleSystemMessage(message) {
dataChannelLabel: this.dataChannel.label
});
+ try {
await this.establishConnection();
- if (this.isVerified) {
+ // КРИТИЧЕСКИ ВАЖНО: Инициализируем file transfer сразу
+ this.initializeFileTransfer();
+
+ } catch (error) {
+ console.error('❌ Error in establishConnection:', error);
+ // Продолжаем несмотря на ошибки
+ }
+
+ if (this.isVerified) {
this.onStatusChange('connected');
this.processMessageQueue();
@@ -2489,14 +2628,9 @@ handleSystemMessage(message) {
};
this.dataChannel.onclose = () => {
-
- // Clean up enhanced security features
- this.disconnect();
-
if (!this.intentionalDisconnect) {
- this.onStatusChange('reconnecting');
- this.onMessage('🔄 Enhanced secure connection closed. Attempting recovery...', 'system');
- this.handleUnexpectedDisconnect();
+ this.onStatusChange('disconnected');
+ this.onMessage('🔌 Enhanced secure connection closed. Check connection status.', 'system');
} else {
this.onStatusChange('disconnected');
this.onMessage('🔌 Enhanced secure connection closed', 'system');
@@ -2506,173 +2640,98 @@ handleSystemMessage(message) {
this.isVerified = false;
};
+ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ ОБРАБОТКИ СООБЩЕНИЙ
this.dataChannel.onmessage = async (event) => {
- try {
- console.log('📨 Raw message received:', {
- dataType: typeof event.data,
- dataLength: event.data?.length || 0,
- firstChars: typeof event.data === 'string' ? event.data.substring(0, 100) : 'not string'
- });
-
- // DEBUG: Additional logging for message processing
- console.log('🔍 dataChannel.onmessage DEBUG:', {
- eventDataType: typeof event.data,
- eventDataConstructor: event.data?.constructor?.name,
- isString: typeof event.data === 'string',
- isArrayBuffer: event.data instanceof ArrayBuffer,
- dataSample: typeof event.data === 'string' ? event.data.substring(0, 50) : 'not string'
- });
-
- // DEBUG: Check if this is a user message
- if (typeof event.data === 'string') {
try {
- const parsed = JSON.parse(event.data);
- if (parsed.type === 'message') {
- console.log('🎯 USER MESSAGE DETECTED:', {
- type: parsed.type,
- data: parsed.data,
- timestamp: parsed.timestamp,
- isInitiator: this.isInitiator
- });
- } else {
- console.log('📨 OTHER MESSAGE DETECTED:', {
- type: parsed.type,
- isInitiator: this.isInitiator
- });
- }
- } catch (e) {
- console.log('📨 NON-JSON MESSAGE:', {
- data: event.data.substring(0, 50),
- isInitiator: this.isInitiator
+ console.log('📨 Raw message received:', {
+ dataType: typeof event.data,
+ dataLength: event.data?.length || 0,
+ firstChars: typeof event.data === 'string' ? event.data.substring(0, 100) : 'not string'
});
- }
- }
-
- // ADDITIONAL DEBUG: Log all incoming messages
- console.log('📨 INCOMING MESSAGE DEBUG:', {
- dataType: typeof event.data,
- isString: typeof event.data === 'string',
- isArrayBuffer: event.data instanceof ArrayBuffer,
- dataLength: event.data?.length || event.data?.byteLength || 0,
- dataSample: typeof event.data === 'string' ? event.data.substring(0, 100) : 'not string',
- isInitiator: this.isInitiator,
- isVerified: this.isVerified,
- channelLabel: this.dataChannel?.label || 'unknown',
- channelState: this.dataChannel?.readyState || 'unknown'
- });
-
- // CRITICAL DEBUG: Check if this is a user message that should be displayed
- if (typeof event.data === 'string') {
- try {
- const parsed = JSON.parse(event.data);
- if (parsed.type === 'message') {
- console.log('🎯 CRITICAL: USER MESSAGE RECEIVED FOR DISPLAY:', {
- type: parsed.type,
- data: parsed.data,
- timestamp: parsed.timestamp,
- isInitiator: this.isInitiator,
- channelLabel: this.dataChannel?.label || 'unknown'
- });
- }
- } catch (e) {
- // Not JSON
- }
- }
-
- // Process message with enhanced security layers
- await this.processMessage(event.data);
- } catch (error) {
- console.error('❌ Failed to process enhanced message:', error);
-
- // Fallback to legacy message processing
- 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;
- }
-
- // 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) {
- console.error('❌ Keys not available for message decryption');
- throw new Error(`Cannot decrypt message: keys for version ${keyVersion} not available`);
- }
-
- const decryptedData = await window.EnhancedSecureCryptoUtils.decryptMessage(
- payload.data,
- keys.encryptionKey,
- keys.macKey,
- keys.metadataKey,
- null // Disabling strict sequence number verification
- );
-
- // 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');
-
- console.log('✅ Enhanced message received via fallback');
- return;
- }
-
- // Legacy message support
- if (payload.type === 'message') {
- if (!this.encryptionKey || !this.macKey) {
- throw new Error('Missing keys to decrypt legacy message');
- }
-
- const decryptedData = await window.EnhancedSecureCryptoUtils.decryptMessage(
- payload.data,
- this.encryptionKey,
- this.macKey,
- this.metadataKey
- );
-
- 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');
-
- console.log('✅ Legacy message received via fallback');
- return;
- }
+ // ИСПРАВЛЕНИЕ: Улучшенная проверка на JSON
+ if (typeof event.data === 'string') {
+ let parsed;
+ try {
+ parsed = JSON.parse(event.data);
+ } catch (jsonError) {
+ console.warn('⚠️ Received non-JSON string message:', event.data.substring(0, 50));
+ // Обрабатываем как обычное текстовое сообщение
+ if (this.onMessage) {
+ this.onMessage(event.data, 'received');
+ }
+ return;
+ }
- console.warn('⚠️ Unknown message type:', payload.type);
-
- } catch (error) {
- console.error('❌ Message processing error:', error.message);
- this.onMessage(`❌ Processing error: ${error.message}`, 'system');
- }
+ if (parsed.type && parsed.type.startsWith('file_')) {
+ console.log('📁 FILE MESSAGE DETECTED:', parsed.type);
+ // НЕМЕДЛЕННО передаем в processMessage для обработки
+ await this.processMessage(event.data);
+ return;
+ }
+
+ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Проверяем тип сообщения
+ if (parsed.type === 'message') {
+ console.log('🎯 USER MESSAGE DETECTED:', {
+ type: parsed.type,
+ data: parsed.data?.substring(0, 50) || 'no data',
+ timestamp: parsed.timestamp,
+ isInitiator: this.isInitiator
+ });
+
+ // Обрабатываем пользовательское сообщение напрямую
+ if (this.onMessage && parsed.data) {
+ this.onMessage(parsed.data, 'received');
+ }
+ return;
+ }
+
+
+ // Системные сообщения
+ if (parsed.type && ['heartbeat', 'verification', 'verification_response', 'peer_disconnect', 'security_upgrade'].includes(parsed.type)) {
+ console.log('🔧 SYSTEM MESSAGE DETECTED:', parsed.type);
+ await this.processMessage(event.data);
+ return;
+ }
+ }
+
+ // Обрабатываем все остальные сообщения через общий процессор
+ await this.processMessage(event.data);
+
+ } catch (error) {
+ console.error('❌ Failed to process message in onmessage:', error);
+
+ // ИСПРАВЛЕНИЕ: Fallback обработка
+ try {
+ if (typeof event.data === 'string') {
+ const fallbackParsed = JSON.parse(event.data);
+
+ // Обработка основных типов сообщений как fallback
+ if (fallbackParsed.type === 'message' && fallbackParsed.data) {
+ console.log('🔄 Fallback: Processing user message');
+ if (this.onMessage) {
+ this.onMessage(fallbackParsed.data, 'received');
+ }
+ return;
+ }
+
+ if (fallbackParsed.type === 'heartbeat') {
+ console.log('🔄 Fallback: Processing heartbeat');
+ this.handleHeartbeat();
+ return;
+ }
+ }
+ } catch (fallbackError) {
+ console.error('❌ Fallback message processing also failed:', fallbackError);
+
+ // Последний fallback - обработка как текст если это строка
+ if (typeof event.data === 'string' && this.onMessage) {
+ this.onMessage(`[Received]: ${event.data}`, 'received');
+ }
+ }
+ }
+ };
}
-};
-}
async createSecureOffer() {
try {
// Check rate limiting
@@ -2789,7 +2848,9 @@ handleSystemMessage(message) {
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced secure offer creation failed', {
error: error.message
});
- this.onStatusChange('failed');
+ this.onStatusChange('disconnected');
+ // Не вызываем cleanupConnection для ошибок создания offer
+ // чтобы не закрывать сессию полностью
throw error;
}
}
@@ -3015,7 +3076,9 @@ handleSystemMessage(message) {
window.EnhancedSecureCryptoUtils.secureLog.log('error', 'Enhanced secure answer creation failed', {
error: error.message
});
- this.onStatusChange('failed');
+ this.onStatusChange('disconnected');
+ // Не вызываем cleanupConnection для ошибок создания answer
+ // чтобы не закрывать сессию полностью
throw error;
}
}
@@ -3448,6 +3511,12 @@ handleSystemMessage(message) {
async sendSecureMessage(message) {
if (!this.isConnected() || !this.isVerified) {
+ // Для файловых сообщений не добавляем в очередь, а выбрасываем ошибку
+ if (message && typeof message === 'object' && message.type && message.type.startsWith('file_')) {
+ throw new Error('Connection not ready for file transfer. Please ensure the connection is established and verified.');
+ }
+
+ // Для обычных сообщений добавляем в очередь
this.messageQueue.push(message);
throw new Error('Connection not ready. Message queued for sending.');
}
@@ -3576,6 +3645,9 @@ handleSystemMessage(message) {
}
disconnect() {
+ if (this.fileTransferSystem) {
+ this.fileTransferSystem.cleanup();
+ }
this.intentionalDisconnect = true;
window.EnhancedSecureCryptoUtils.secureLog.log('info', 'Starting intentional disconnect');
@@ -3593,9 +3665,11 @@ handleSystemMessage(message) {
}
}));
- setTimeout(() => {
- this.cleanupConnection();
- }, 500);
+ // Не вызываем cleanupConnection автоматически
+ // чтобы не закрывать сессию при ошибках
+ // setTimeout(() => {
+ // this.cleanupConnection();
+ // }, 500);
}
handleUnexpectedDisconnect() {
@@ -3603,6 +3677,13 @@ handleSystemMessage(message) {
this.isVerified = false;
this.onMessage('🔌 Connection lost. Attempting to reconnect...', 'system');
+ // Cleanup file transfer system on unexpected disconnect
+ if (this.fileTransferSystem) {
+ console.log('🧹 Cleaning up file transfer system on unexpected disconnect...');
+ this.fileTransferSystem.cleanup();
+ this.fileTransferSystem = null;
+ }
+
document.dispatchEvent(new CustomEvent('peer-disconnect', {
detail: {
reason: 'connection_lost',
@@ -3610,11 +3691,13 @@ handleSystemMessage(message) {
}
}));
- setTimeout(() => {
- if (!this.intentionalDisconnect) {
- this.attemptReconnection();
- }
- }, 3000);
+ // Не пытаемся переподключиться автоматически
+ // чтобы не закрывать сессию при ошибках
+ // setTimeout(() => {
+ // if (!this.intentionalDisconnect) {
+ // this.attemptReconnection();
+ // }
+ // }, 3000);
}
sendDisconnectNotification() {
@@ -3652,7 +3735,9 @@ handleSystemMessage(message) {
attemptReconnection() {
this.onMessage('❌ Unable to reconnect. A new connection is required.', 'system');
- this.cleanupConnection();
+ // Не вызываем cleanupConnection автоматически
+ // чтобы не закрывать сессию при ошибках
+ // this.cleanupConnection();
}
handlePeerDisconnectNotification(data) {
@@ -3741,8 +3826,9 @@ handleSystemMessage(message) {
// Clearing message queue
this.messageQueue = [];
- // IMPORTANT: Clearing security logs
- window.EnhancedSecureCryptoUtils.secureLog.clearLogs();
+ // Не очищаем логи безопасности автоматически
+ // чтобы сохранить информацию об ошибках
+ // window.EnhancedSecureCryptoUtils.secureLog.clearLogs();
document.dispatchEvent(new CustomEvent('connection-cleaned', {
detail: {
@@ -3765,6 +3851,283 @@ handleSystemMessage(message) {
window.gc();
}
}
+ // Public method to send files
+ async sendFile(file) {
+ if (!this.isConnected() || !this.isVerified) {
+ throw new Error('Connection not ready for file transfer. Please ensure the connection is established and verified.');
+ }
+
+ if (!this.fileTransferSystem) {
+ console.log('🔄 File transfer system not initialized, attempting to initialize...');
+ this.initializeFileTransfer();
+
+ // Дать время на инициализацию
+ await new Promise(resolve => setTimeout(resolve, 500));
+
+ if (!this.fileTransferSystem) {
+ throw new Error('File transfer system could not be initialized. Please try reconnecting.');
+ }
+ }
+
+ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Проверяем готовность ключей
+ if (!this.encryptionKey || !this.macKey) {
+ throw new Error('Encryption keys not ready. Please wait for connection to be fully established.');
+ }
+
+ // Debug logging for file transfer system
+ console.log('🔍 Debug: File transfer system in sendFile:', {
+ hasFileTransferSystem: !!this.fileTransferSystem,
+ fileTransferSystemType: this.fileTransferSystem.constructor?.name,
+ hasWebrtcManager: !!this.fileTransferSystem.webrtcManager,
+ webrtcManagerType: this.fileTransferSystem.webrtcManager?.constructor?.name
+ });
+
+ try {
+ console.log('🚀 Starting file transfer for:', file.name, `(${(file.size / 1024 / 1024).toFixed(2)} MB)`);
+ const fileId = await this.fileTransferSystem.sendFile(file);
+ console.log('✅ File transfer initiated successfully with ID:', fileId);
+ return fileId;
+ } catch (error) {
+ console.error('❌ File transfer error:', error);
+
+ // Перебрасываем ошибку с более понятным сообщением
+ if (error.message.includes('Connection not ready')) {
+ throw new Error('Connection not ready for file transfer. Check connection status.');
+ } else if (error.message.includes('Encryption keys not initialized')) {
+ throw new Error('Encryption keys not initialized. Try reconnecting.');
+ } else if (error.message.includes('Transfer timeout')) {
+ throw new Error('File transfer timeout. Check connection and try again.');
+ } else {
+ throw error;
+ }
+ }
+ }
+
+ // Get active file transfers
+ getFileTransfers() {
+ if (!this.fileTransferSystem) {
+ return { sending: [], receiving: [] };
+ }
+
+ try {
+ // Проверяем наличие методов в файловой системе
+ let sending = [];
+ let receiving = [];
+
+ if (typeof this.fileTransferSystem.getActiveTransfers === 'function') {
+ sending = this.fileTransferSystem.getActiveTransfers();
+ } else {
+ console.warn('⚠️ getActiveTransfers method not available in file transfer system');
+ }
+
+ if (typeof this.fileTransferSystem.getReceivingTransfers === 'function') {
+ receiving = this.fileTransferSystem.getReceivingTransfers();
+ } else {
+ console.warn('⚠️ getReceivingTransfers method not available in file transfer system');
+ }
+
+ return {
+ sending: sending || [],
+ receiving: receiving || []
+ };
+ } catch (error) {
+ console.error('❌ Error getting file transfers:', error);
+ return { sending: [], receiving: [] };
+ }
+ }
+
+ // Get file transfer system status
+ getFileTransferStatus() {
+ if (!this.fileTransferSystem) {
+ return {
+ initialized: false,
+ status: 'not_initialized',
+ message: 'File transfer system not initialized'
+ };
+ }
+
+ const activeTransfers = this.fileTransferSystem.getActiveTransfers();
+ const receivingTransfers = this.fileTransferSystem.getReceivingTransfers();
+
+ return {
+ initialized: true,
+ status: 'ready',
+ activeTransfers: activeTransfers.length,
+ receivingTransfers: receivingTransfers.length,
+ totalTransfers: activeTransfers.length + receivingTransfers.length
+ };
+ }
+
+ // Cancel file transfer
+ cancelFileTransfer(fileId) {
+ if (!this.fileTransferSystem) return false;
+ return this.fileTransferSystem.cancelTransfer(fileId);
+ }
+
+ // Force cleanup of file transfer system
+ cleanupFileTransferSystem() {
+ if (this.fileTransferSystem) {
+ console.log('🧹 Force cleaning up file transfer system...');
+ this.fileTransferSystem.cleanup();
+ this.fileTransferSystem = null;
+ return true;
+ }
+ return false;
+ }
+
+ // Reinitialize file transfer system
+ reinitializeFileTransfer() {
+ try {
+ console.log('🔄 Reinitializing file transfer system...');
+ if (this.fileTransferSystem) {
+ this.fileTransferSystem.cleanup();
+ }
+ this.initializeFileTransfer();
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to reinitialize file transfer system:', error);
+ return false;
+ }
+ }
+
+ // Set file transfer callbacks
+ setFileTransferCallbacks(onProgress, onReceived, onError) {
+ this.onFileProgress = onProgress;
+ this.onFileReceived = onReceived;
+ this.onFileError = onError;
+
+ console.log('🔧 File transfer callbacks set:', {
+ hasProgress: !!onProgress,
+ hasReceived: !!onReceived,
+ hasError: !!onError
+ });
+
+ // Reinitialize file transfer system if it exists to update callbacks
+ if (this.fileTransferSystem) {
+ console.log('🔄 Reinitializing file transfer system with new callbacks...');
+ this.initializeFileTransfer();
+ }
+ }
+
+ // ============================================
+ // SESSION ACTIVATION HANDLING
+ // ============================================
+
+ async handleSessionActivation(sessionData) {
+ try {
+ console.log('🔐 Handling session activation:', sessionData);
+
+ // Update session state
+ this.currentSession = sessionData;
+ this.sessionManager = sessionData.sessionManager;
+
+ // ИСПРАВЛЕНИЕ: Более мягкие проверки для активации
+ const hasKeys = !!(this.encryptionKey && this.macKey);
+ const hasSession = !!(this.sessionManager && (this.sessionManager.hasActiveSession?.() || sessionData.sessionId));
+
+ console.log('🔍 Session activation status:', {
+ hasKeys: hasKeys,
+ hasSession: hasSession,
+ sessionType: sessionData.sessionType,
+ isDemo: sessionData.isDemo
+ });
+
+ // Force connection status если у нас есть сессия
+ if (hasSession) {
+ console.log('🔓 Session activated - forcing connection status to connected');
+ this.onStatusChange('connected');
+
+ // Устанавливаем isVerified для активных сессий
+ this.isVerified = true;
+ console.log('✅ Session verified - setting isVerified to true');
+ }
+
+ // Инициализируем file transfer систему с задержкой
+ setTimeout(() => {
+ try {
+ this.initializeFileTransfer();
+ } catch (error) {
+ console.warn('⚠️ File transfer initialization failed during session activation:', error.message);
+ }
+ }, 1000);
+
+ console.log('✅ Session activation handled successfully');
+
+ } catch (error) {
+ console.error('❌ Failed to handle session activation:', error);
+ }
+ }
+ // Метод для проверки готовности файловых трансферов
+checkFileTransferReadiness() {
+ const status = {
+ hasFileTransferSystem: !!this.fileTransferSystem,
+ hasDataChannel: !!this.dataChannel,
+ dataChannelState: this.dataChannel?.readyState,
+ isConnected: this.isConnected(),
+ isVerified: this.isVerified,
+ hasEncryptionKey: !!this.encryptionKey,
+ hasMacKey: !!this.macKey,
+ ready: false
+ };
+
+ status.ready = status.hasFileTransferSystem &&
+ status.hasDataChannel &&
+ status.dataChannelState === 'open' &&
+ status.isConnected &&
+ status.isVerified;
+
+ console.log('🔍 File transfer readiness check:', status);
+ return status;
+ }
+
+ // Метод для принудительной переинициализации файловой системы
+ forceReinitializeFileTransfer() {
+ try {
+ console.log('🔄 Force reinitializing file transfer system...');
+
+ if (this.fileTransferSystem) {
+ this.fileTransferSystem.cleanup();
+ this.fileTransferSystem = null;
+ }
+
+ // Небольшая задержка перед переинициализацией
+ setTimeout(() => {
+ this.initializeFileTransfer();
+ }, 500);
+
+ return true;
+ } catch (error) {
+ console.error('❌ Failed to force reinitialize file transfer:', error);
+ return false;
+ }
+ }
+
+ // Метод для получения диагностической информации
+ getFileTransferDiagnostics() {
+ const diagnostics = {
+ timestamp: new Date().toISOString(),
+ webrtcManager: {
+ hasDataChannel: !!this.dataChannel,
+ dataChannelState: this.dataChannel?.readyState,
+ isConnected: this.isConnected(),
+ isVerified: this.isVerified,
+ hasEncryptionKey: !!this.encryptionKey,
+ hasMacKey: !!this.macKey,
+ hasMetadataKey: !!this.metadataKey
+ },
+ fileTransferSystem: null
+ };
+
+ if (this.fileTransferSystem) {
+ try {
+ diagnostics.fileTransferSystem = this.fileTransferSystem.getSystemStatus();
+ } catch (error) {
+ diagnostics.fileTransferSystem = { error: error.message };
+ }
+ }
+
+ return diagnostics;
+ }
}
export { EnhancedSecureWebRTCManager };
\ No newline at end of file
diff --git a/src/styles/components.css b/src/styles/components.css
index 27900d5..e1f027a 100644
--- a/src/styles/components.css
+++ b/src/styles/components.css
@@ -5,6 +5,91 @@
flex-direction: column;
}
+/* ============================================ */
+/* FILE TRANSFER STYLES */
+/* ============================================ */
+
+.file-transfer-component {
+ margin-top: 1rem;
+}
+
+.file-drop-zone {
+ border: 2px dashed #4b5563;
+ border-radius: 12px;
+ padding: 2rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ background: rgba(55, 65, 81, 0.1);
+}
+
+.file-drop-zone:hover {
+ border-color: #3b82f6;
+ background: rgba(59, 130, 246, 0.1);
+}
+
+.file-drop-zone.drag-over {
+ border-color: #10b981;
+ background: rgba(16, 185, 129, 0.1);
+ transform: scale(1.02);
+}
+
+.drop-content {
+ pointer-events: none;
+}
+
+.active-transfers {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.transfer-item {
+ transition: all 0.2s ease;
+}
+
+.transfer-item:hover {
+ transform: translateY(-1px);
+}
+
+.progress-bar {
+ position: relative;
+ height: 6px;
+ background: rgba(75, 85, 99, 0.3);
+ border-radius: 3px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ transition: width 0.3s ease;
+ border-radius: 3px;
+}
+
+.progress-text {
+ position: absolute;
+ top: -20px;
+ right: 0;
+ color: #9ca3af;
+}
+
+.file-transfer-section {
+ border-top: 1px solid rgba(75, 85, 99, 0.1);
+}
+
+@media (max-width: 640px) {
+ .file-drop-zone {
+ padding: 1.5rem;
+ }
+
+ .transfer-item {
+ padding: 0.75rem;
+ }
+
+ .progress-text {
+ font-size: 0.75rem;
+ }
+}
+
.header-minimal {
background: rgb(35 36 35 / 13%);
backdrop-filter: blur(5px);
diff --git a/src/transfer/EnhancedSecureFileTransfer.js b/src/transfer/EnhancedSecureFileTransfer.js
new file mode 100644
index 0000000..ef3336d
--- /dev/null
+++ b/src/transfer/EnhancedSecureFileTransfer.js
@@ -0,0 +1,1288 @@
+class EnhancedSecureFileTransfer {
+ constructor(webrtcManager, onProgress, onComplete, onError, onFileReceived) {
+ this.webrtcManager = webrtcManager;
+ this.onProgress = onProgress;
+ this.onComplete = onComplete;
+ this.onError = onError;
+ this.onFileReceived = onFileReceived;
+
+ // Validate webrtcManager
+ if (!webrtcManager) {
+ throw new Error('webrtcManager is required for EnhancedSecureFileTransfer');
+ }
+
+ console.log('🔍 Debug: webrtcManager in constructor:', {
+ hasWebrtcManager: !!webrtcManager,
+ webrtcManagerType: webrtcManager.constructor?.name,
+ hasEncryptionKey: !!webrtcManager.encryptionKey,
+ hasMacKey: !!webrtcManager.macKey,
+ hasEcdhKeyPair: !!webrtcManager.ecdhKeyPair
+ });
+
+ // Transfer settings
+ this.CHUNK_SIZE = 65536; // 64 KB chunks
+ this.MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB limit
+ this.MAX_CONCURRENT_TRANSFERS = 3;
+ this.CHUNK_TIMEOUT = 30000; // 30 seconds per chunk
+ this.RETRY_ATTEMPTS = 3;
+
+ // Active transfers tracking
+ this.activeTransfers = new Map(); // fileId -> transfer state
+ this.receivingTransfers = new Map(); // fileId -> receiving state
+ this.transferQueue = []; // Queue for pending transfers
+ this.pendingChunks = new Map();
+
+ // Session key derivation - КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ
+ this.sessionKeys = new Map(); // fileId -> derived session key
+ this.sharedSecretCache = new Map(); // Кэш для shared secret чтобы sender и receiver использовали одинаковый
+
+ // Security
+ this.processedChunks = new Set(); // Prevent replay attacks
+ this.transferNonces = new Map(); // fileId -> current nonce counter
+
+ // Initialize message handlers
+ this.setupFileMessageHandlers();
+
+ console.log('🔒 Enhanced Secure File Transfer initialized');
+ }
+
+ // ============================================
+ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: ДЕТЕРМИНИСТИЧЕСКОЕ СОЗДАНИЕ КЛЮЧЕЙ
+ // ============================================
+
+ async createDeterministicSharedSecret(fileId, fileSize, salt = null) {
+ try {
+ console.log('🔑 Creating deterministic shared secret for:', fileId);
+
+ // Создаем уникальную строку-идентификатор для файла
+ const fileIdentifier = `${fileId}-${fileSize}`;
+
+ // Проверяем кэш
+ if (this.sharedSecretCache.has(fileIdentifier)) {
+ console.log('✅ Using cached shared secret for:', fileIdentifier);
+ return this.sharedSecretCache.get(fileIdentifier);
+ }
+
+ const encoder = new TextEncoder();
+ let seedComponents = [];
+
+ // 1. Добавляем fileId и размер файла (одинаково у отправителя и получателя)
+ seedComponents.push(encoder.encode(fileIdentifier));
+
+ // 2. Пытаемся использовать существующие ключи сессии
+ if (this.webrtcManager.encryptionKey) {
+ try {
+ // Создаем детерминистическую производную из существующего ключа шифрования
+ const keyMaterial = encoder.encode(`FileTransfer-Session-${fileIdentifier}`);
+ const derivedKeyMaterial = await crypto.subtle.sign(
+ 'HMAC',
+ this.webrtcManager.macKey, // Используем MAC ключ для HMAC
+ keyMaterial
+ );
+ seedComponents.push(new Uint8Array(derivedKeyMaterial));
+ console.log('✅ Used session MAC key for deterministic seed');
+ } catch (error) {
+ console.warn('⚠️ Could not use MAC key, using alternative approach:', error.message);
+ }
+ }
+
+ // 3. Добавляем соль если есть (от sender к receiver)
+ if (salt && Array.isArray(salt)) {
+ seedComponents.push(new Uint8Array(salt));
+ console.log('✅ Added salt to deterministic seed');
+ }
+
+ // 4. Если нет других источников, используем fingerprint сессии
+ if (this.webrtcManager.keyFingerprint) {
+ seedComponents.push(encoder.encode(this.webrtcManager.keyFingerprint));
+ console.log('✅ Added session fingerprint to seed');
+ }
+
+ // Объединяем все компоненты
+ const totalLength = seedComponents.reduce((sum, comp) => sum + comp.length, 0);
+ const combinedSeed = new Uint8Array(totalLength);
+ let offset = 0;
+
+ for (const component of seedComponents) {
+ combinedSeed.set(component, offset);
+ offset += component.length;
+ }
+
+ // Хешируем для получения консистентной длины
+ const sharedSecret = await crypto.subtle.digest('SHA-384', combinedSeed);
+
+ // Кэшируем результат
+ this.sharedSecretCache.set(fileIdentifier, sharedSecret);
+
+ console.log('🔑 Created deterministic shared secret, length:', sharedSecret.byteLength);
+ return sharedSecret;
+
+ } catch (error) {
+ console.error('❌ Failed to create deterministic shared secret:', error);
+ throw error;
+ }
+ }
+
+ // ============================================
+ // ИСПРАВЛЕННЫЙ МЕТОД СОЗДАНИЯ КЛЮЧА СЕССИИ
+ // ============================================
+
+ async deriveFileSessionKey(fileId, fileSize, providedSalt = null) {
+ try {
+ console.log('🔑 Deriving file session key for:', fileId);
+
+ // Получаем детерминистический shared secret
+ const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, providedSalt);
+
+ // Создаем или используем предоставленную соль
+ let salt;
+ if (providedSalt && Array.isArray(providedSalt)) {
+ salt = new Uint8Array(providedSalt);
+ console.log('🔑 Using provided salt from metadata');
+ } else {
+ salt = crypto.getRandomValues(new Uint8Array(32));
+ console.log('🔑 Generated new salt for file transfer');
+ }
+
+ // Импортируем shared secret как PBKDF2 ключ
+ const keyForDerivation = await crypto.subtle.importKey(
+ 'raw',
+ sharedSecret,
+ { name: 'PBKDF2' },
+ false,
+ ['deriveKey']
+ );
+
+ // Создаем файловый ключ сессии с PBKDF2
+ const fileSessionKey = await crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: salt,
+ iterations: 100000,
+ hash: 'SHA-384'
+ },
+ keyForDerivation,
+ {
+ name: 'AES-GCM',
+ length: 256
+ },
+ false,
+ ['encrypt', 'decrypt']
+ );
+
+ // Сохраняем ключ сессии
+ this.sessionKeys.set(fileId, {
+ key: fileSessionKey,
+ salt: Array.from(salt),
+ created: Date.now()
+ });
+
+ console.log('✅ File session key derived successfully for:', fileId);
+ return { key: fileSessionKey, salt: Array.from(salt) };
+
+ } catch (error) {
+ console.error('❌ Failed to derive file session key:', error);
+ throw error;
+ }
+ }
+
+ // ============================================
+ // ИСПРАВЛЕННЫЙ МЕТОД ДЛЯ ПОЛУЧАТЕЛЯ
+ // ============================================
+
+ async deriveFileSessionKeyFromSalt(fileId, fileSize, saltArray) {
+ try {
+ console.log('🔑 Deriving session key from salt for receiver:', fileId);
+
+ if (!saltArray || !Array.isArray(saltArray)) {
+ throw new Error('Invalid salt provided for key derivation');
+ }
+
+ // Получаем тот же детерминистический shared secret что и отправитель
+ const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, saltArray);
+
+ const salt = new Uint8Array(saltArray);
+
+ // Импортируем shared secret как PBKDF2 ключ
+ const keyForDerivation = await crypto.subtle.importKey(
+ 'raw',
+ sharedSecret,
+ { name: 'PBKDF2' },
+ false,
+ ['deriveKey']
+ );
+
+ // Создаем точно такой же ключ как у отправителя
+ const fileSessionKey = await crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: salt,
+ iterations: 100000, // Те же параметры что у отправителя
+ hash: 'SHA-384'
+ },
+ keyForDerivation,
+ {
+ name: 'AES-GCM',
+ length: 256
+ },
+ false,
+ ['encrypt', 'decrypt']
+ );
+
+ this.sessionKeys.set(fileId, {
+ key: fileSessionKey,
+ salt: saltArray,
+ created: Date.now()
+ });
+
+ console.log('✅ Session key derived successfully for receiver:', fileId);
+ return fileSessionKey;
+
+ } catch (error) {
+ console.error('❌ Failed to derive session key from salt:', error);
+ throw error;
+ }
+ }
+
+ // ============================================
+ // FILE TRANSFER IMPLEMENTATION
+ // ============================================
+
+ async sendFile(file) {
+ try {
+ // Validate webrtcManager
+ if (!this.webrtcManager) {
+ throw new Error('WebRTC Manager not initialized');
+ }
+
+ console.log('🔍 Debug: webrtcManager in sendFile:', {
+ hasWebrtcManager: !!this.webrtcManager,
+ webrtcManagerType: this.webrtcManager.constructor?.name,
+ hasEncryptionKey: !!this.webrtcManager.encryptionKey,
+ hasMacKey: !!this.webrtcManager.macKey,
+ hasEcdhKeyPair: !!this.webrtcManager.ecdhKeyPair,
+ isConnected: this.webrtcManager.isConnected?.(),
+ isVerified: this.webrtcManager.isVerified
+ });
+
+ // Validate file
+ if (!file || !file.size) {
+ throw new Error('Invalid file object');
+ }
+
+ if (file.size > this.MAX_FILE_SIZE) {
+ throw new Error(`File too large. Maximum size: ${this.MAX_FILE_SIZE / 1024 / 1024} MB`);
+ }
+
+ if (this.activeTransfers.size >= this.MAX_CONCURRENT_TRANSFERS) {
+ throw new Error('Maximum concurrent transfers reached');
+ }
+
+ // Generate unique file ID
+ const fileId = `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
+
+ // Calculate file hash for integrity verification
+ const fileHash = await this.calculateFileHash(file);
+
+ // Derive session key for this file - ИСПРАВЛЕНИЕ
+ const keyResult = await this.deriveFileSessionKey(fileId, file.size);
+ const sessionKey = keyResult.key;
+ const salt = keyResult.salt;
+
+ // Create transfer state
+ const transferState = {
+ fileId: fileId,
+ file: file,
+ fileHash: fileHash,
+ sessionKey: sessionKey,
+ salt: salt, // Сохраняем соль для отправки
+ totalChunks: Math.ceil(file.size / this.CHUNK_SIZE),
+ sentChunks: 0,
+ confirmedChunks: 0,
+ startTime: Date.now(),
+ status: 'preparing',
+ retryCount: 0,
+ lastChunkTime: Date.now()
+ };
+
+ this.activeTransfers.set(fileId, transferState);
+ this.transferNonces.set(fileId, 0);
+
+ // Send file metadata first
+ await this.sendFileMetadata(transferState);
+
+ // Start chunk transmission
+ await this.startChunkTransmission(transferState);
+
+ return fileId;
+
+ } catch (error) {
+ console.error('❌ File sending failed:', error);
+ if (this.onError) this.onError(error.message);
+ throw error;
+ }
+ }
+
+ // ИСПРАВЛЕННЫЙ метод отправки метаданных
+ async sendFileMetadata(transferState) {
+ try {
+ const metadata = {
+ type: 'file_transfer_start',
+ fileId: transferState.fileId,
+ fileName: transferState.file.name,
+ fileSize: transferState.file.size,
+ fileType: transferState.file.type || 'application/octet-stream',
+ fileHash: transferState.fileHash,
+ totalChunks: transferState.totalChunks,
+ chunkSize: this.CHUNK_SIZE,
+ salt: transferState.salt, // Отправляем соль получателю
+ timestamp: Date.now(),
+ version: '1.0'
+ };
+
+ console.log('📁 Sending file metadata for:', transferState.file.name);
+
+ // Send metadata through secure channel
+ await this.sendSecureMessage(metadata);
+
+ transferState.status = 'metadata_sent';
+
+ // Notify progress
+ if (this.onProgress) {
+ this.onProgress({
+ fileId: transferState.fileId,
+ fileName: transferState.file.name,
+ progress: 5, // 5% for metadata sent
+ status: 'metadata_sent',
+ totalChunks: transferState.totalChunks,
+ sentChunks: 0
+ });
+ }
+
+ } catch (error) {
+ console.error('❌ Failed to send file metadata:', error);
+ transferState.status = 'failed';
+ throw error;
+ }
+ }
+
+ // Start chunk transmission
+ async startChunkTransmission(transferState) {
+ try {
+ transferState.status = 'transmitting';
+
+ const file = transferState.file;
+ const totalChunks = transferState.totalChunks;
+
+ for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
+ const start = chunkIndex * this.CHUNK_SIZE;
+ const end = Math.min(start + this.CHUNK_SIZE, file.size);
+
+ // Read chunk from file
+ const chunkData = await this.readFileChunk(file, start, end);
+
+ // Send chunk
+ await this.sendFileChunk(transferState, chunkIndex, chunkData);
+
+ // Update progress
+ transferState.sentChunks++;
+ const progress = Math.round((transferState.sentChunks / totalChunks) * 95) + 5; // 5-100%
+
+ if (this.onProgress) {
+ this.onProgress({
+ fileId: transferState.fileId,
+ fileName: transferState.file.name,
+ progress: progress,
+ status: 'transmitting',
+ totalChunks: totalChunks,
+ sentChunks: transferState.sentChunks
+ });
+ }
+
+ // Small delay between chunks to prevent overwhelming
+ if (chunkIndex < totalChunks - 1) {
+ await new Promise(resolve => setTimeout(resolve, 10));
+ }
+ }
+
+ transferState.status = 'waiting_confirmation';
+ console.log('✅ All chunks sent, waiting for completion confirmation');
+
+ // Timeout for completion confirmation
+ setTimeout(() => {
+ if (this.activeTransfers.has(transferState.fileId)) {
+ const state = this.activeTransfers.get(transferState.fileId);
+ if (state.status === 'waiting_confirmation') {
+ console.log('⏰ Transfer completion timeout, cleaning up');
+ this.cleanupTransfer(transferState.fileId);
+ }
+ }
+ }, 30000);
+
+ } catch (error) {
+ console.error('❌ Chunk transmission failed:', error);
+ transferState.status = 'failed';
+ throw error;
+ }
+ }
+
+ // Read file chunk
+ async readFileChunk(file, start, end) {
+ try {
+ const blob = file.slice(start, end);
+ return await blob.arrayBuffer();
+ } catch (error) {
+ console.error('❌ Failed to read file chunk:', error);
+ throw error;
+ }
+ }
+
+ // Send file chunk
+ async sendFileChunk(transferState, chunkIndex, chunkData) {
+ try {
+ const sessionKey = transferState.sessionKey;
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
+
+ // Encrypt chunk data
+ const encryptedChunk = await crypto.subtle.encrypt(
+ {
+ name: 'AES-GCM',
+ iv: nonce
+ },
+ sessionKey,
+ chunkData
+ );
+
+ const chunkMessage = {
+ type: 'file_chunk',
+ fileId: transferState.fileId,
+ chunkIndex: chunkIndex,
+ totalChunks: transferState.totalChunks,
+ nonce: Array.from(nonce),
+ encryptedData: Array.from(new Uint8Array(encryptedChunk)),
+ chunkSize: chunkData.byteLength,
+ timestamp: Date.now()
+ };
+
+ // Send chunk through secure channel
+ await this.sendSecureMessage(chunkMessage);
+
+ } catch (error) {
+ console.error('❌ Failed to send file chunk:', error);
+ throw error;
+ }
+ }
+
+ // Send secure message through WebRTC
+ async sendSecureMessage(message) {
+ try {
+ // Send through existing Double Ratchet channel
+ const messageString = JSON.stringify(message);
+
+ // Use the WebRTC manager's sendMessage method
+ if (this.webrtcManager.sendMessage) {
+ await this.webrtcManager.sendMessage(messageString);
+ } else {
+ throw new Error('WebRTC manager sendMessage method not available');
+ }
+ } catch (error) {
+ console.error('❌ Failed to send secure message:', error);
+ throw error;
+ }
+ }
+
+ // Calculate file hash for integrity verification
+ async calculateFileHash(file) {
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ const hashBuffer = await crypto.subtle.digest('SHA-384', arrayBuffer);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+ } catch (error) {
+ console.error('❌ File hash calculation failed:', error);
+ throw error;
+ }
+ }
+
+ // ============================================
+ // MESSAGE HANDLERS
+ // ============================================
+
+ setupFileMessageHandlers() {
+ // Store original message handler
+ const originalHandler = this.webrtcManager.onMessage;
+
+ // Wrap message handler to intercept file transfer messages
+ this.webrtcManager.onMessage = (message, type) => {
+ try {
+ // Try to parse as JSON for file transfer messages
+ if (typeof message === 'string' && message.startsWith('{')) {
+ const parsed = JSON.parse(message);
+
+ switch (parsed.type) {
+ case 'file_transfer_start':
+ this.handleFileTransferStart(parsed);
+ return;
+ case 'file_chunk':
+ this.handleFileChunk(parsed);
+ return;
+ case 'file_transfer_response':
+ this.handleTransferResponse(parsed);
+ return;
+ case 'chunk_confirmation':
+ this.handleChunkConfirmation(parsed);
+ return;
+ case 'file_transfer_complete':
+ this.handleTransferComplete(parsed);
+ return;
+ case 'file_transfer_error':
+ this.handleTransferError(parsed);
+ return;
+ }
+ }
+ } catch (e) {
+ // Not a file transfer message, continue with normal handling
+ }
+
+ // Pass to original handler for regular messages
+ if (originalHandler) {
+ originalHandler(message, type);
+ }
+ };
+ }
+
+ // ИСПРАВЛЕННЫЙ Handle incoming file transfer start
+ async handleFileTransferStart(metadata) {
+ try {
+ console.log('📥 Receiving file transfer:', metadata.fileName);
+
+ // Validate metadata
+ if (!metadata.fileId || !metadata.fileName || !metadata.fileSize) {
+ throw new Error('Invalid file transfer metadata');
+ }
+
+ // Check if we already have this transfer
+ if (this.receivingTransfers.has(metadata.fileId)) {
+ console.warn('⚠️ File transfer already in progress:', metadata.fileId);
+ return;
+ }
+
+ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Используем соль из метаданных
+ const sessionKey = await this.deriveFileSessionKeyFromSalt(
+ metadata.fileId,
+ metadata.fileSize,
+ metadata.salt
+ );
+
+ // Create receiving transfer state
+ const receivingState = {
+ fileId: metadata.fileId,
+ fileName: metadata.fileName,
+ fileSize: metadata.fileSize,
+ fileType: metadata.fileType || 'application/octet-stream',
+ fileHash: metadata.fileHash,
+ totalChunks: metadata.totalChunks,
+ chunkSize: metadata.chunkSize || this.CHUNK_SIZE,
+ sessionKey: sessionKey,
+ receivedChunks: new Map(),
+ receivedCount: 0,
+ startTime: Date.now(),
+ lastChunkTime: Date.now(),
+ status: 'receiving'
+ };
+
+ this.receivingTransfers.set(metadata.fileId, receivingState);
+
+ // Send acceptance response
+ const response = {
+ type: 'file_transfer_response',
+ fileId: metadata.fileId,
+ accepted: true,
+ timestamp: Date.now()
+ };
+
+ await this.sendSecureMessage(response);
+
+ // Notify progress
+ if (this.onProgress) {
+ this.onProgress({
+ fileId: receivingState.fileId,
+ fileName: receivingState.fileName,
+ progress: 0,
+ status: 'receiving',
+ totalChunks: receivingState.totalChunks,
+ receivedChunks: 0
+ });
+ }
+
+ // Process buffered chunks if any
+ if (this.pendingChunks.has(metadata.fileId)) {
+ console.log('🔄 Processing buffered chunks for:', metadata.fileId);
+ const bufferedChunks = this.pendingChunks.get(metadata.fileId);
+
+ for (const [chunkIndex, chunkMessage] of bufferedChunks.entries()) {
+ console.log('📦 Processing buffered chunk:', chunkIndex);
+ await this.handleFileChunk(chunkMessage);
+ }
+
+ this.pendingChunks.delete(metadata.fileId);
+ }
+
+ } catch (error) {
+ console.error('❌ Failed to handle file transfer start:', error);
+
+ // Send error response
+ try {
+ const errorResponse = {
+ type: 'file_transfer_response',
+ fileId: metadata.fileId,
+ accepted: false,
+ error: error.message,
+ timestamp: Date.now()
+ };
+ await this.sendSecureMessage(errorResponse);
+ } catch (responseError) {
+ console.error('❌ Failed to send error response:', responseError);
+ }
+ }
+ }
+
+ // ИСПРАВЛЕННЫЙ Handle incoming file chunk
+ async handleFileChunk(chunkMessage) {
+ try {
+ let receivingState = this.receivingTransfers.get(chunkMessage.fileId);
+
+ // Buffer early chunks if transfer not yet initialized
+ if (!receivingState) {
+ console.log('📦 Buffering early chunk for:', chunkMessage.fileId, 'chunk:', chunkMessage.chunkIndex);
+
+ if (!this.pendingChunks.has(chunkMessage.fileId)) {
+ this.pendingChunks.set(chunkMessage.fileId, new Map());
+ }
+
+ this.pendingChunks.get(chunkMessage.fileId).set(chunkMessage.chunkIndex, chunkMessage);
+ return;
+ }
+
+ // Update last chunk time
+ receivingState.lastChunkTime = Date.now();
+
+ // Check if chunk already received
+ if (receivingState.receivedChunks.has(chunkMessage.chunkIndex)) {
+ console.log('⚠️ Duplicate chunk received:', chunkMessage.chunkIndex);
+ return;
+ }
+
+ // Validate chunk
+ if (chunkMessage.chunkIndex < 0 || chunkMessage.chunkIndex >= receivingState.totalChunks) {
+ throw new Error(`Invalid chunk index: ${chunkMessage.chunkIndex}`);
+ }
+
+ // ИСПРАВЛЕНИЕ: Улучшенное декодирование чанка
+ const nonce = new Uint8Array(chunkMessage.nonce);
+ const encryptedData = new Uint8Array(chunkMessage.encryptedData);
+
+ console.log('🔓 Decrypting chunk:', chunkMessage.chunkIndex, {
+ nonceLength: nonce.length,
+ encryptedDataLength: encryptedData.length,
+ expectedSize: chunkMessage.chunkSize
+ });
+
+ // Decrypt chunk with better error handling
+ let decryptedChunk;
+ try {
+ decryptedChunk = await crypto.subtle.decrypt(
+ {
+ name: 'AES-GCM',
+ iv: nonce
+ },
+ receivingState.sessionKey,
+ encryptedData
+ );
+ } catch (decryptError) {
+ console.error('❌ Chunk decryption failed:', decryptError);
+ console.error('Decryption details:', {
+ chunkIndex: chunkMessage.chunkIndex,
+ fileId: chunkMessage.fileId,
+ nonceLength: nonce.length,
+ encryptedDataLength: encryptedData.length,
+ sessionKeyType: receivingState.sessionKey?.constructor?.name,
+ sessionKeyAlgorithm: receivingState.sessionKey?.algorithm?.name
+ });
+
+ // Send specific error message
+ const errorMessage = {
+ type: 'file_transfer_error',
+ fileId: chunkMessage.fileId,
+ error: `Chunk ${chunkMessage.chunkIndex} decryption failed: ${decryptError.message}`,
+ chunkIndex: chunkMessage.chunkIndex,
+ timestamp: Date.now()
+ };
+ await this.sendSecureMessage(errorMessage);
+ return;
+ }
+
+ // Verify chunk size
+ if (decryptedChunk.byteLength !== chunkMessage.chunkSize) {
+ throw new Error(`Chunk size mismatch: expected ${chunkMessage.chunkSize}, got ${decryptedChunk.byteLength}`);
+ }
+
+ // Store chunk
+ receivingState.receivedChunks.set(chunkMessage.chunkIndex, decryptedChunk);
+ receivingState.receivedCount++;
+
+ // Update progress
+ const progress = Math.round((receivingState.receivedCount / receivingState.totalChunks) * 100);
+
+ console.log(`📥 Received chunk ${chunkMessage.chunkIndex + 1}/${receivingState.totalChunks} (${progress}%)`);
+
+ // Notify progress
+ if (this.onProgress) {
+ this.onProgress({
+ fileId: receivingState.fileId,
+ fileName: receivingState.fileName,
+ progress: progress,
+ status: 'receiving',
+ totalChunks: receivingState.totalChunks,
+ receivedChunks: receivingState.receivedCount
+ });
+ }
+
+ // Send chunk confirmation
+ const confirmation = {
+ type: 'chunk_confirmation',
+ fileId: chunkMessage.fileId,
+ chunkIndex: chunkMessage.chunkIndex,
+ timestamp: Date.now()
+ };
+ await this.sendSecureMessage(confirmation);
+
+ // Check if all chunks received
+ if (receivingState.receivedCount === receivingState.totalChunks) {
+ await this.assembleFile(receivingState);
+ }
+
+ } catch (error) {
+ console.error('❌ Failed to handle file chunk:', error);
+
+ // Send error notification
+ try {
+ const errorMessage = {
+ type: 'file_transfer_error',
+ fileId: chunkMessage.fileId,
+ error: error.message,
+ timestamp: Date.now()
+ };
+ await this.sendSecureMessage(errorMessage);
+ } catch (errorSendError) {
+ console.error('❌ Failed to send chunk error:', errorSendError);
+ }
+ }
+ }
+
+ // Assemble received file
+ async assembleFile(receivingState) {
+ try {
+ console.log('🔄 Assembling file:', receivingState.fileName);
+
+ receivingState.status = 'assembling';
+
+ // Verify we have all chunks
+ for (let i = 0; i < receivingState.totalChunks; i++) {
+ if (!receivingState.receivedChunks.has(i)) {
+ throw new Error(`Missing chunk ${i}`);
+ }
+ }
+
+ // Combine all chunks in order
+ const chunks = [];
+ for (let i = 0; i < receivingState.totalChunks; i++) {
+ const chunk = receivingState.receivedChunks.get(i);
+ chunks.push(new Uint8Array(chunk));
+ }
+
+ // Calculate total size
+ const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
+
+ // Verify total size matches expected
+ if (totalSize !== receivingState.fileSize) {
+ throw new Error(`File size mismatch: expected ${receivingState.fileSize}, got ${totalSize}`);
+ }
+
+ // Combine into single array
+ const fileData = new Uint8Array(totalSize);
+ let offset = 0;
+ for (const chunk of chunks) {
+ fileData.set(chunk, offset);
+ offset += chunk.length;
+ }
+
+ // Verify file integrity
+ const receivedHash = await this.calculateFileHashFromData(fileData);
+ if (receivedHash !== receivingState.fileHash) {
+ throw new Error('File integrity check failed - hash mismatch');
+ }
+
+ // Create blob and notify
+ const fileBlob = new Blob([fileData], { type: receivingState.fileType });
+
+ receivingState.endTime = Date.now();
+ receivingState.status = 'completed';
+
+ // Notify file received
+ if (this.onFileReceived) {
+ this.onFileReceived({
+ fileId: receivingState.fileId,
+ fileName: receivingState.fileName,
+ fileSize: receivingState.fileSize,
+ fileBlob: fileBlob,
+ transferTime: receivingState.endTime - receivingState.startTime
+ });
+ }
+
+ // Send completion confirmation
+ const completionMessage = {
+ type: 'file_transfer_complete',
+ fileId: receivingState.fileId,
+ success: true,
+ timestamp: Date.now()
+ };
+ await this.sendSecureMessage(completionMessage);
+
+ // Cleanup
+ this.cleanupReceivingTransfer(receivingState.fileId);
+
+ console.log('✅ File assembly completed:', receivingState.fileName);
+
+ } catch (error) {
+ console.error('❌ File assembly failed:', error);
+ receivingState.status = 'failed';
+
+ if (this.onError) {
+ this.onError(`File assembly failed: ${error.message}`);
+ }
+
+ // Send error notification
+ try {
+ const errorMessage = {
+ type: 'file_transfer_complete',
+ fileId: receivingState.fileId,
+ success: false,
+ error: error.message,
+ timestamp: Date.now()
+ };
+ await this.sendSecureMessage(errorMessage);
+ } catch (errorSendError) {
+ console.error('❌ Failed to send assembly error:', errorSendError);
+ }
+
+ // Cleanup failed transfer
+ this.cleanupReceivingTransfer(receivingState.fileId);
+ }
+ }
+
+ // Calculate hash from data
+ async calculateFileHashFromData(data) {
+ try {
+ const hashBuffer = await crypto.subtle.digest('SHA-384', data);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+ } catch (error) {
+ console.error('❌ Hash calculation failed:', error);
+ throw error;
+ }
+ }
+
+ // Handle transfer response
+ handleTransferResponse(response) {
+ try {
+ console.log('📨 File transfer response:', response);
+
+ const transferState = this.activeTransfers.get(response.fileId);
+
+ if (!transferState) {
+ console.warn('⚠️ Received response for unknown transfer:', response.fileId);
+ return;
+ }
+
+ if (response.accepted) {
+ console.log('✅ File transfer accepted by peer');
+ transferState.status = 'accepted';
+ } else {
+ console.log('❌ File transfer rejected by peer:', response.error);
+ transferState.status = 'rejected';
+
+ if (this.onError) {
+ this.onError(`Transfer rejected: ${response.error || 'Unknown reason'}`);
+ }
+
+ this.cleanupTransfer(response.fileId);
+ }
+ } catch (error) {
+ console.error('❌ Failed to handle transfer response:', error);
+ }
+ }
+
+ // Handle chunk confirmation
+ handleChunkConfirmation(confirmation) {
+ try {
+ const transferState = this.activeTransfers.get(confirmation.fileId);
+ if (!transferState) {
+ return;
+ }
+
+ transferState.confirmedChunks++;
+ transferState.lastChunkTime = Date.now();
+
+ console.log(`✅ Chunk ${confirmation.chunkIndex} confirmed for ${confirmation.fileId}`);
+ } catch (error) {
+ console.error('❌ Failed to handle chunk confirmation:', error);
+ }
+ }
+
+ // Handle transfer completion
+ handleTransferComplete(completion) {
+ try {
+ console.log('🏁 Transfer completion:', completion);
+
+ const transferState = this.activeTransfers.get(completion.fileId);
+ if (!transferState) {
+ return;
+ }
+
+ if (completion.success) {
+ console.log('✅ File transfer completed successfully');
+ transferState.status = 'completed';
+ transferState.endTime = Date.now();
+
+ if (this.onComplete) {
+ this.onComplete({
+ fileId: transferState.fileId,
+ fileName: transferState.file.name,
+ fileSize: transferState.file.size,
+ transferTime: transferState.endTime - transferState.startTime,
+ status: 'completed'
+ });
+ }
+ } else {
+ console.log('❌ File transfer failed:', completion.error);
+ transferState.status = 'failed';
+
+ if (this.onError) {
+ this.onError(`Transfer failed: ${completion.error || 'Unknown error'}`);
+ }
+ }
+
+ this.cleanupTransfer(completion.fileId);
+
+ } catch (error) {
+ console.error('❌ Failed to handle transfer completion:', error);
+ }
+ }
+
+ // Handle transfer error
+ handleTransferError(errorMessage) {
+ try {
+ console.error('❌ Transfer error received:', errorMessage);
+
+ const transferState = this.activeTransfers.get(errorMessage.fileId);
+ if (transferState) {
+ transferState.status = 'failed';
+ this.cleanupTransfer(errorMessage.fileId);
+ }
+
+ const receivingState = this.receivingTransfers.get(errorMessage.fileId);
+ if (receivingState) {
+ receivingState.status = 'failed';
+ this.cleanupReceivingTransfer(errorMessage.fileId);
+ }
+
+ if (this.onError) {
+ this.onError(`Transfer error: ${errorMessage.error || 'Unknown error'}`);
+ }
+
+ } catch (error) {
+ console.error('❌ Failed to handle transfer error:', error);
+ }
+ }
+
+ // ============================================
+ // UTILITY METHODS
+ // ============================================
+
+ // Get active transfers
+ getActiveTransfers() {
+ return Array.from(this.activeTransfers.values()).map(transfer => ({
+ fileId: transfer.fileId,
+ fileName: transfer.file?.name || 'Unknown',
+ fileSize: transfer.file?.size || 0,
+ progress: Math.round((transfer.sentChunks / transfer.totalChunks) * 100),
+ status: transfer.status,
+ startTime: transfer.startTime
+ }));
+ }
+
+ // Get receiving transfers
+ getReceivingTransfers() {
+ return Array.from(this.receivingTransfers.values()).map(transfer => ({
+ fileId: transfer.fileId,
+ fileName: transfer.fileName || 'Unknown',
+ fileSize: transfer.fileSize || 0,
+ progress: Math.round((transfer.receivedCount / transfer.totalChunks) * 100),
+ status: transfer.status,
+ startTime: transfer.startTime
+ }));
+ }
+
+ // Cancel transfer
+ cancelTransfer(fileId) {
+ try {
+ if (this.activeTransfers.has(fileId)) {
+ this.cleanupTransfer(fileId);
+ return true;
+ }
+ if (this.receivingTransfers.has(fileId)) {
+ this.cleanupReceivingTransfer(fileId);
+ return true;
+ }
+ return false;
+ } catch (error) {
+ console.error('❌ Failed to cancel transfer:', error);
+ return false;
+ }
+ }
+
+ // Cleanup transfer
+ cleanupTransfer(fileId) {
+ this.activeTransfers.delete(fileId);
+ this.sessionKeys.delete(fileId);
+ this.transferNonces.delete(fileId);
+
+ // Remove from shared secret cache
+ const transfers = this.activeTransfers.get(fileId) || this.receivingTransfers.get(fileId);
+ if (transfers && transfers.file) {
+ const fileIdentifier = `${fileId}-${transfers.file.size}`;
+ this.sharedSecretCache.delete(fileIdentifier);
+ }
+
+ // Remove processed chunk IDs for this transfer
+ for (const chunkId of this.processedChunks) {
+ if (chunkId.startsWith(fileId)) {
+ this.processedChunks.delete(chunkId);
+ }
+ }
+ }
+
+ // Cleanup receiving transfer
+ cleanupReceivingTransfer(fileId) {
+ this.pendingChunks.delete(fileId);
+ const receivingState = this.receivingTransfers.get(fileId);
+ if (receivingState) {
+ // Clear chunk data from memory
+ receivingState.receivedChunks.clear();
+
+ // Remove from shared secret cache
+ const fileIdentifier = `${fileId}-${receivingState.fileSize}`;
+ this.sharedSecretCache.delete(fileIdentifier);
+ }
+
+ this.receivingTransfers.delete(fileId);
+ this.sessionKeys.delete(fileId);
+
+ // Remove processed chunk IDs
+ for (const chunkId of this.processedChunks) {
+ if (chunkId.startsWith(fileId)) {
+ this.processedChunks.delete(chunkId);
+ }
+ }
+ }
+
+ // Get transfer status
+ getTransferStatus(fileId) {
+ if (this.activeTransfers.has(fileId)) {
+ const transfer = this.activeTransfers.get(fileId);
+ return {
+ type: 'sending',
+ fileId: transfer.fileId,
+ fileName: transfer.file.name,
+ progress: Math.round((transfer.sentChunks / transfer.totalChunks) * 100),
+ status: transfer.status,
+ startTime: transfer.startTime
+ };
+ }
+
+ if (this.receivingTransfers.has(fileId)) {
+ const transfer = this.receivingTransfers.get(fileId);
+ return {
+ type: 'receiving',
+ fileId: transfer.fileId,
+ fileName: transfer.fileName,
+ progress: Math.round((transfer.receivedCount / transfer.totalChunks) * 100),
+ status: transfer.status,
+ startTime: transfer.startTime
+ };
+ }
+
+ return null;
+ }
+
+ // Get system status
+ getSystemStatus() {
+ return {
+ initialized: true,
+ activeTransfers: this.activeTransfers.size,
+ receivingTransfers: this.receivingTransfers.size,
+ totalTransfers: this.activeTransfers.size + this.receivingTransfers.size,
+ maxConcurrentTransfers: this.MAX_CONCURRENT_TRANSFERS,
+ maxFileSize: this.MAX_FILE_SIZE,
+ chunkSize: this.CHUNK_SIZE,
+ hasWebrtcManager: !!this.webrtcManager,
+ isConnected: this.webrtcManager?.isConnected?.() || false,
+ sharedSecretCacheSize: this.sharedSecretCache.size
+ };
+ }
+
+ // Cleanup all transfers (called on disconnect)
+ cleanup() {
+ console.log('🧹 Cleaning up file transfer system');
+
+ // Cleanup all active transfers
+ for (const fileId of this.activeTransfers.keys()) {
+ this.cleanupTransfer(fileId);
+ }
+
+ for (const fileId of this.receivingTransfers.keys()) {
+ this.cleanupReceivingTransfer(fileId);
+ }
+
+ // Clear all state
+ this.pendingChunks.clear();
+ this.activeTransfers.clear();
+ this.receivingTransfers.clear();
+ this.transferQueue.length = 0;
+ this.sessionKeys.clear();
+ this.transferNonces.clear();
+ this.processedChunks.clear();
+ this.sharedSecretCache.clear(); // Очищаем кэш shared secret
+ }
+
+ // ============================================
+ // DEBUGGING AND DIAGNOSTICS
+ // ============================================
+
+ // Debug method to check key derivation
+ async debugKeyDerivation(fileId, fileSize, salt = null) {
+ try {
+ console.log('🔍 Debug: Testing key derivation for:', fileId);
+
+ const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, salt);
+ console.log('🔍 Shared secret created, length:', sharedSecret.byteLength);
+
+ const testSalt = salt ? new Uint8Array(salt) : crypto.getRandomValues(new Uint8Array(32));
+ console.log('🔍 Using salt, length:', testSalt.length);
+
+ const keyForDerivation = await crypto.subtle.importKey(
+ 'raw',
+ sharedSecret,
+ { name: 'PBKDF2' },
+ false,
+ ['deriveKey']
+ );
+
+ const derivedKey = await crypto.subtle.deriveKey(
+ {
+ name: 'PBKDF2',
+ salt: testSalt,
+ iterations: 100000,
+ hash: 'SHA-384'
+ },
+ keyForDerivation,
+ {
+ name: 'AES-GCM',
+ length: 256
+ },
+ false,
+ ['encrypt', 'decrypt']
+ );
+
+ console.log('✅ Key derivation test successful');
+ console.log('🔍 Derived key:', derivedKey.algorithm);
+
+ return {
+ success: true,
+ sharedSecretLength: sharedSecret.byteLength,
+ saltLength: testSalt.length,
+ keyAlgorithm: derivedKey.algorithm
+ };
+
+ } catch (error) {
+ console.error('❌ Key derivation test failed:', error);
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ }
+
+ // Debug method to verify encryption/decryption
+ async debugEncryptionDecryption(fileId, fileSize, testData = 'test data') {
+ try {
+ console.log('🔍 Debug: Testing encryption/decryption for:', fileId);
+
+ const keyResult = await this.deriveFileSessionKey(fileId, fileSize);
+ const sessionKey = keyResult.key;
+ const salt = keyResult.salt;
+
+ // Test encryption
+ const nonce = crypto.getRandomValues(new Uint8Array(12));
+ const testDataBuffer = new TextEncoder().encode(testData);
+
+ const encrypted = await crypto.subtle.encrypt(
+ { name: 'AES-GCM', iv: nonce },
+ sessionKey,
+ testDataBuffer
+ );
+
+ console.log('✅ Encryption test successful');
+
+ // Test decryption with same key
+ const decrypted = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: nonce },
+ sessionKey,
+ encrypted
+ );
+
+ const decryptedText = new TextDecoder().decode(decrypted);
+
+ if (decryptedText === testData) {
+ console.log('✅ Decryption test successful');
+
+ // Test with receiver key derivation
+ const receiverKey = await this.deriveFileSessionKeyFromSalt(fileId, fileSize, salt);
+
+ const decryptedByReceiver = await crypto.subtle.decrypt(
+ { name: 'AES-GCM', iv: nonce },
+ receiverKey,
+ encrypted
+ );
+
+ const receiverDecryptedText = new TextDecoder().decode(decryptedByReceiver);
+
+ if (receiverDecryptedText === testData) {
+ console.log('✅ Receiver key derivation test successful');
+ return { success: true, message: 'All tests passed' };
+ } else {
+ throw new Error('Receiver decryption failed');
+ }
+ } else {
+ throw new Error('Decryption verification failed');
+ }
+
+ } catch (error) {
+ console.error('❌ Encryption/decryption test failed:', error);
+ return { success: false, error: error.message };
+ }
+ }
+}
+
+export { EnhancedSecureFileTransfer };
\ No newline at end of file