feat(security): comprehensive connection security overhaul with mutex framework

Implemented robust security framework with custom withMutex system:

**Race condition protection:**
- Custom _withMutex('connectionOperation') implementation with 15s timeout
- Atomic key generation through _generateEncryptionKeys()
- Serialized connection operations to prevent conflicts

**Multi-stage validation pipeline:**
- Step-by-step validation (keys, fingerprints, SDP)
- Automatic rollback via _cleanupFailedOfferCreation() on failures
- Error phase detection for precise diagnostics

**Enhanced MITM protection:**
- Unique encryption key fingerprints
- Session ID anti-hijacking protection
- Mutual authentication challenge system
- Package integrity validation

**Advanced logging & monitoring:**
- Secure logging without sensitive data leaks
- Operation tracking via unique operationId
- Comprehensive error diagnostics and phase tracking
- Deadlock detection with emergency recovery

Breaking changes: Connection establishment now requires mutex coordination
This commit is contained in:
lockbitchat
2025-08-21 04:07:16 -04:00
parent 9b2884a3af
commit 31485989f7
4 changed files with 2796 additions and 916 deletions

View File

@@ -3191,15 +3191,32 @@
// File received callback // File received callback
(fileData) => { (fileData) => {
// Auto-download received file const sizeMb = Math.max(1, Math.round((fileData.fileSize || 0) / (1024 * 1024)));
const url = URL.createObjectURL(fileData.fileBlob); const downloadMessage = React.createElement('div', {
className: 'flex items-center space-x-2'
}, [
React.createElement('span', { key: 'label' }, `📥 Файл получен: ${fileData.fileName} (${sizeMb} MB)`),
React.createElement('button', {
key: 'btn',
className: 'px-3 py-1 rounded bg-blue-600 hover:bg-blue-700 text-white text-xs',
onClick: async () => {
try {
const url = await fileData.getObjectURL();
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = fileData.fileName; a.download = fileData.fileName;
a.click(); a.click();
URL.revokeObjectURL(url); // Даем браузеру время начать загрузку, затем освобождаем URL
setTimeout(() => fileData.revokeObjectURL(url), 15000);
} catch (e) {
console.error('Download failed:', e);
addMessageWithAutoScroll(`❌ Ошибка загрузки файла: ${String(e?.message || e)}`, 'system');
}
}
}, 'Скачать')
]);
addMessageWithAutoScroll(`📥 Файл загружен: ${fileData.fileName}`, 'system'); addMessageWithAutoScroll(downloadMessage, 'system');
}, },
// Error callback // Error callback
@@ -3563,7 +3580,10 @@
await webrtcManagerRef.current.sendMessage(messageInput); await webrtcManagerRef.current.sendMessage(messageInput);
setMessageInput(''); setMessageInput('');
} catch (error) { } catch (error) {
addMessageWithAutoScroll(`❌ Sending error: ${error.message}`, 'system'); const msg = String(error?.message || error);
if (!/queued for sending|Data channel not ready/i.test(msg)) {
addMessageWithAutoScroll(`❌ Sending error: ${msg}`,'system');
}
} }
}; };

View File

@@ -2,6 +2,7 @@
const FileTransferComponent = ({ webrtcManager, isConnected }) => { const FileTransferComponent = ({ webrtcManager, isConnected }) => {
const [dragOver, setDragOver] = React.useState(false); const [dragOver, setDragOver] = React.useState(false);
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] }); const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию
const fileInputRef = React.useRef(null); const fileInputRef = React.useRef(null);
// Update transfers periodically // Update transfers periodically
@@ -33,24 +34,27 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
// НЕ отправляем сообщения в чат! // НЕ отправляем сообщения в чат!
}, },
// File received callback - показываем только финальное уведомление // File received callback - добавляем кнопку скачивания в UI
(fileData) => { (fileData) => {
console.log(`📥 File received in UI: ${fileData.fileName}`); console.log(`📥 File received in UI: ${fileData.fileName}`);
// Добавляем в список готовых к скачиванию
// Auto-download received file setReadyFiles(prev => {
const url = URL.createObjectURL(fileData.fileBlob); // избегаем дублей по fileId
const a = document.createElement('a'); if (prev.some(f => f.fileId === fileData.fileId)) return prev;
a.href = url; return [...prev, {
a.download = fileData.fileName; fileId: fileData.fileId,
a.click(); fileName: fileData.fileName,
URL.revokeObjectURL(url); fileSize: fileData.fileSize,
mimeType: fileData.mimeType,
// Update transfer list getBlob: fileData.getBlob,
getObjectURL: fileData.getObjectURL,
revokeObjectURL: fileData.revokeObjectURL
}];
});
// Обновляем список активных передач
const currentTransfers = webrtcManager.getFileTransfers(); const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers); setTransfers(currentTransfers);
// ИСПРАВЛЕНИЕ: НЕ дублируем системные сообщения
// Финальное уведомление уже отправляется в WebRTC менеджере
}, },
// Error callback // Error callback
@@ -342,14 +346,40 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
className: "text-muted text-xs ml-2" className: "text-muted text-xs ml-2"
}, formatFileSize(transfer.fileSize)) }, formatFileSize(transfer.fileSize))
]), ]),
React.createElement('button', { React.createElement('div', { key: 'actions', className: 'flex items-center space-x-2' }, [
key: 'cancel', // Кнопка скачать, если файл уже готов (есть в readyFiles)
onClick: () => webrtcManager.cancelFileTransfer(transfer.fileId), (() => {
className: "text-red-400 hover:text-red-300 text-xs" const rf = readyFiles.find(f => f.fileId === transfer.fileId);
}, [ if (!rf || transfer.status !== 'completed') return null;
React.createElement('i', { return React.createElement('button', {
className: 'fas fa-times' key: 'download',
}) className: 'text-green-400 hover:text-green-300 text-xs flex items-center',
onClick: async () => {
try {
const url = await rf.getObjectURL();
const a = document.createElement('a');
a.href = url;
a.download = rf.fileName || 'file';
a.click();
rf.revokeObjectURL(url);
} catch (e) {
alert('Не удалось начать скачивание: ' + e.message);
}
}
}, [
React.createElement('i', { key: 'i', className: 'fas fa-download mr-1' }),
'Скачать'
]);
})(),
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', { React.createElement('div', {

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,8 @@ class EnhancedSecureFileTransfer {
}); });
// Transfer settings // Transfer settings
this.CHUNK_SIZE = 65536; // 64 KB chunks // Размер чанка по умолчанию (баланс нагрузки и стабильности очереди)
this.CHUNK_SIZE = 64 * 1024; // 64 KB
this.MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB limit this.MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MB limit
this.MAX_CONCURRENT_TRANSFERS = 3; this.MAX_CONCURRENT_TRANSFERS = 3;
this.CHUNK_TIMEOUT = 30000; // 30 seconds per chunk this.CHUNK_TIMEOUT = 30000; // 30 seconds per chunk
@@ -42,6 +43,7 @@ class EnhancedSecureFileTransfer {
// Security // Security
this.processedChunks = new Set(); // Prevent replay attacks this.processedChunks = new Set(); // Prevent replay attacks
this.transferNonces = new Map(); // fileId -> current nonce counter this.transferNonces = new Map(); // fileId -> current nonce counter
this.receivedFileBuffers = new Map(); // fileId -> { buffer:ArrayBuffer, type:string, name:string, size:number }
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Регистрируем обработчик сообщений // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Регистрируем обработчик сообщений
this.setupFileMessageHandlers(); this.setupFileMessageHandlers();
@@ -49,6 +51,54 @@ class EnhancedSecureFileTransfer {
console.log('🔒 Enhanced Secure File Transfer initialized'); console.log('🔒 Enhanced Secure File Transfer initialized');
} }
// ============================================
// ENCODING HELPERS (Base64 for efficient transport)
// ============================================
arrayBufferToBase64(buffer) {
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
let binary = '';
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
base64ToUint8Array(base64) {
const binaryString = atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
}
// ============================================
// PUBLIC ACCESSORS FOR RECEIVED FILES
// ============================================
getReceivedFileMeta(fileId) {
const entry = this.receivedFileBuffers.get(fileId);
if (!entry) return null;
return { fileId, fileName: entry.name, fileSize: entry.size, mimeType: entry.type };
}
async getBlob(fileId) {
const entry = this.receivedFileBuffers.get(fileId);
if (!entry) return null;
return new Blob([entry.buffer], { type: entry.type });
}
async getObjectURL(fileId) {
const blob = await this.getBlob(fileId);
if (!blob) return null;
return URL.createObjectURL(blob);
}
revokeObjectURL(url) {
try { URL.revokeObjectURL(url); } catch (_) {}
}
// ============================================ // ============================================
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ - ОБРАБОТКА СООБЩЕНИЙ // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ - ОБРАБОТКА СООБЩЕНИЙ
// ============================================ // ============================================
@@ -474,7 +524,7 @@ class EnhancedSecureFileTransfer {
// Read chunk from file // Read chunk from file
const chunkData = await this.readFileChunk(file, start, end); const chunkData = await this.readFileChunk(file, start, end);
// Send chunk // Send chunk (с учётом backpressure)
await this.sendFileChunk(transferState, chunkIndex, chunkData); await this.sendFileChunk(transferState, chunkIndex, chunkData);
// Update progress // Update progress
@@ -485,10 +535,8 @@ class EnhancedSecureFileTransfer {
// Только логируем // Только логируем
console.log(`📤 Chunk sent ${transferState.sentChunks}/${totalChunks} (${progress}%)`); console.log(`📤 Chunk sent ${transferState.sentChunks}/${totalChunks} (${progress}%)`);
// Small delay between chunks to prevent overwhelming // Backpressure: ждём разгрузки очереди перед следующим чанком
if (chunkIndex < totalChunks - 1) { await this.waitForBackpressure();
await new Promise(resolve => setTimeout(resolve, 10));
}
} }
transferState.status = 'waiting_confirmation'; transferState.status = 'waiting_confirmation';
@@ -537,17 +585,21 @@ class EnhancedSecureFileTransfer {
chunkData chunkData
); );
// Use Base64 to drastically reduce JSON overhead
const encryptedB64 = this.arrayBufferToBase64(new Uint8Array(encryptedChunk));
const chunkMessage = { const chunkMessage = {
type: 'file_chunk', type: 'file_chunk',
fileId: transferState.fileId, fileId: transferState.fileId,
chunkIndex: chunkIndex, chunkIndex: chunkIndex,
totalChunks: transferState.totalChunks, totalChunks: transferState.totalChunks,
nonce: Array.from(nonce), nonce: Array.from(nonce),
encryptedData: Array.from(new Uint8Array(encryptedChunk)), encryptedDataB64: encryptedB64,
chunkSize: chunkData.byteLength, chunkSize: chunkData.byteLength,
timestamp: Date.now() timestamp: Date.now()
}; };
// Перед отправкой проверяем backpressure (доп. защита)
await this.waitForBackpressure();
// Send chunk through secure channel // Send chunk through secure channel
await this.sendSecureMessage(chunkMessage); await this.sendSecureMessage(chunkMessage);
@@ -558,19 +610,64 @@ class EnhancedSecureFileTransfer {
} }
async sendSecureMessage(message) { async sendSecureMessage(message) {
try { // ВАЖНО: отправляем напрямую в DataChannel, чтобы file_* и chunk_confirmation
// Send through existing WebRTC channel // приходили верхнего уровня и перехватывались файловой системой, без обёртки type: 'message'
const messageString = JSON.stringify(message); const messageString = JSON.stringify(message);
const dc = this.webrtcManager?.dataChannel;
// Use the WebRTC manager's sendMessage method const maxRetries = 10;
if (this.webrtcManager.sendMessage) { let attempt = 0;
await this.webrtcManager.sendMessage(messageString); const wait = (ms) => new Promise(r => setTimeout(r, ms));
} else {
throw new Error('WebRTC manager sendMessage method not available'); while (true) {
try {
if (!dc || dc.readyState !== 'open') {
throw new Error('Data channel not ready');
}
await this.waitForBackpressure();
dc.send(messageString);
return; // success
} catch (error) {
const msg = String(error?.message || '');
const queueFull = msg.includes('send queue is full') || msg.includes('bufferedAmount');
const opErr = error?.name === 'OperationError';
if ((queueFull || opErr) && attempt < maxRetries) {
attempt++;
await this.waitForBackpressure();
await wait(Math.min(50 * attempt, 500));
continue;
}
console.error('❌ Failed to send secure message:', error);
throw error;
} }
} catch (error) { }
console.error('❌ Failed to send secure message:', error); }
throw error;
async waitForBackpressure() {
try {
const dc = this.webrtcManager?.dataChannel;
if (!dc) return;
if (typeof dc.bufferedAmountLowThreshold === 'number') {
// Если буфер превышает порог — ждём события снижения
if (dc.bufferedAmount > dc.bufferedAmountLowThreshold) {
await new Promise(resolve => {
const handler = () => {
dc.removeEventListener('bufferedamountlow', handler);
resolve();
};
dc.addEventListener('bufferedamountlow', handler, { once: true });
});
}
return;
}
// Фолбэк: опрашиваем bufferedAmount и ждём пока не упадёт ниже 4MB
const softLimit = 4 * 1024 * 1024;
while (dc.bufferedAmount > softLimit) {
await new Promise(r => setTimeout(r, 20));
}
} catch (_) {
// ignore
} }
} }
@@ -705,7 +802,15 @@ class EnhancedSecureFileTransfer {
// Decrypt chunk // Decrypt chunk
const nonce = new Uint8Array(chunkMessage.nonce); const nonce = new Uint8Array(chunkMessage.nonce);
const encryptedData = new Uint8Array(chunkMessage.encryptedData); // Backward compatible: prefer Base64, fallback to numeric array
let encryptedData;
if (chunkMessage.encryptedDataB64) {
encryptedData = this.base64ToUint8Array(chunkMessage.encryptedDataB64);
} else if (chunkMessage.encryptedData) {
encryptedData = new Uint8Array(chunkMessage.encryptedData);
} else {
throw new Error('Missing encrypted data');
}
console.log('🔓 Decrypting chunk:', chunkMessage.chunkIndex); console.log('🔓 Decrypting chunk:', chunkMessage.chunkIndex);
@@ -816,20 +921,43 @@ class EnhancedSecureFileTransfer {
throw new Error('File integrity check failed - hash mismatch'); throw new Error('File integrity check failed - hash mismatch');
} }
// Create blob and notify // Lazy: храним буфер, но для совместимости формируем Blob для onFileReceived
const fileBlob = new Blob([fileData], { type: receivingState.fileType }); const fileBuffer = fileData.buffer;
const fileBlob = new Blob([fileBuffer], { type: receivingState.fileType });
receivingState.endTime = Date.now(); receivingState.endTime = Date.now();
receivingState.status = 'completed'; receivingState.status = 'completed';
// Notify file received // Сохраняем в кэше до запроса скачивания
this.receivedFileBuffers.set(receivingState.fileId, {
buffer: fileBuffer,
type: receivingState.fileType,
name: receivingState.fileName,
size: receivingState.fileSize
});
// Сообщаем UI о готовности файла и даём ленивые методы получения
if (this.onFileReceived) { if (this.onFileReceived) {
const getBlob = async () => new Blob([this.receivedFileBuffers.get(receivingState.fileId).buffer], { type: receivingState.fileType });
const getObjectURL = async () => {
const blob = await getBlob();
return URL.createObjectURL(blob);
};
const revokeObjectURL = (url) => {
try { URL.revokeObjectURL(url); } catch (_) {}
};
this.onFileReceived({ this.onFileReceived({
fileId: receivingState.fileId, fileId: receivingState.fileId,
fileName: receivingState.fileName, fileName: receivingState.fileName,
fileSize: receivingState.fileSize, fileSize: receivingState.fileSize,
fileBlob: fileBlob, mimeType: receivingState.fileType,
transferTime: receivingState.endTime - receivingState.startTime transferTime: receivingState.endTime - receivingState.startTime,
// backward-compatibility for existing UIs
fileBlob,
getBlob,
getObjectURL,
revokeObjectURL
}); });
} }
@@ -843,7 +971,13 @@ class EnhancedSecureFileTransfer {
await this.sendSecureMessage(completionMessage); await this.sendSecureMessage(completionMessage);
// Cleanup // Cleanup
this.cleanupReceivingTransfer(receivingState.fileId); // Не удаляем буфер сразу, оставляем до загрузки пользователем
// Очистим метаданные чанков, оставив итоговый буфер
if (this.receivingTransfers.has(receivingState.fileId)) {
const rs = this.receivingTransfers.get(receivingState.fileId);
if (rs && rs.receivedChunks) rs.receivedChunks.clear();
}
this.receivingTransfers.delete(receivingState.fileId);
console.log('✅ File assembly completed:', receivingState.fileName); console.log('✅ File assembly completed:', receivingState.fileName);
@@ -1058,6 +1192,8 @@ class EnhancedSecureFileTransfer {
this.receivingTransfers.delete(fileId); this.receivingTransfers.delete(fileId);
this.sessionKeys.delete(fileId); this.sessionKeys.delete(fileId);
// Также очищаем финальный буфер, если он ещё хранится
this.receivedFileBuffers.delete(fileId);
// Remove processed chunk IDs // Remove processed chunk IDs
for (const chunkId of this.processedChunks) { for (const chunkId of this.processedChunks) {