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

@@ -2,6 +2,7 @@
const FileTransferComponent = ({ webrtcManager, isConnected }) => {
const [dragOver, setDragOver] = React.useState(false);
const [transfers, setTransfers] = React.useState({ sending: [], receiving: [] });
const [readyFiles, setReadyFiles] = React.useState([]); // файлы, готовые к скачиванию
const fileInputRef = React.useRef(null);
// Update transfers periodically
@@ -33,24 +34,27 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
// НЕ отправляем сообщения в чат!
},
// File received callback - показываем только финальное уведомление
// File received callback - добавляем кнопку скачивания в UI
(fileData) => {
console.log(`📥 File received in UI: ${fileData.fileName}`);
// 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
// Добавляем в список готовых к скачиванию
setReadyFiles(prev => {
// избегаем дублей по fileId
if (prev.some(f => f.fileId === fileData.fileId)) return prev;
return [...prev, {
fileId: fileData.fileId,
fileName: fileData.fileName,
fileSize: fileData.fileSize,
mimeType: fileData.mimeType,
getBlob: fileData.getBlob,
getObjectURL: fileData.getObjectURL,
revokeObjectURL: fileData.revokeObjectURL
}];
});
// Обновляем список активных передач
const currentTransfers = webrtcManager.getFileTransfers();
setTransfers(currentTransfers);
// ИСПРАВЛЕНИЕ: НЕ дублируем системные сообщения
// Финальное уведомление уже отправляется в WebRTC менеджере
},
// Error callback
@@ -342,14 +346,40 @@ const FileTransferComponent = ({ webrtcManager, isConnected }) => {
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: 'actions', className: 'flex items-center space-x-2' }, [
// Кнопка скачать, если файл уже готов (есть в readyFiles)
(() => {
const rf = readyFiles.find(f => f.fileId === transfer.fileId);
if (!rf || transfer.status !== 'completed') return null;
return React.createElement('button', {
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', {

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,8 @@ class EnhancedSecureFileTransfer {
});
// 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_CONCURRENT_TRANSFERS = 3;
this.CHUNK_TIMEOUT = 30000; // 30 seconds per chunk
@@ -42,6 +43,7 @@ class EnhancedSecureFileTransfer {
// Security
this.processedChunks = new Set(); // Prevent replay attacks
this.transferNonces = new Map(); // fileId -> current nonce counter
this.receivedFileBuffers = new Map(); // fileId -> { buffer:ArrayBuffer, type:string, name:string, size:number }
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Регистрируем обработчик сообщений
this.setupFileMessageHandlers();
@@ -49,6 +51,54 @@ class EnhancedSecureFileTransfer {
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
const chunkData = await this.readFileChunk(file, start, end);
// Send chunk
// Send chunk (с учётом backpressure)
await this.sendFileChunk(transferState, chunkIndex, chunkData);
// Update progress
@@ -485,10 +535,8 @@ class EnhancedSecureFileTransfer {
// Только логируем
console.log(`📤 Chunk sent ${transferState.sentChunks}/${totalChunks} (${progress}%)`);
// Small delay between chunks to prevent overwhelming
if (chunkIndex < totalChunks - 1) {
await new Promise(resolve => setTimeout(resolve, 10));
}
// Backpressure: ждём разгрузки очереди перед следующим чанком
await this.waitForBackpressure();
}
transferState.status = 'waiting_confirmation';
@@ -537,17 +585,21 @@ class EnhancedSecureFileTransfer {
chunkData
);
// Use Base64 to drastically reduce JSON overhead
const encryptedB64 = this.arrayBufferToBase64(new Uint8Array(encryptedChunk));
const chunkMessage = {
type: 'file_chunk',
fileId: transferState.fileId,
chunkIndex: chunkIndex,
totalChunks: transferState.totalChunks,
nonce: Array.from(nonce),
encryptedData: Array.from(new Uint8Array(encryptedChunk)),
encryptedDataB64: encryptedB64,
chunkSize: chunkData.byteLength,
timestamp: Date.now()
};
// Перед отправкой проверяем backpressure (доп. защита)
await this.waitForBackpressure();
// Send chunk through secure channel
await this.sendSecureMessage(chunkMessage);
@@ -558,19 +610,64 @@ class EnhancedSecureFileTransfer {
}
async sendSecureMessage(message) {
try {
// Send through existing WebRTC 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');
// ВАЖНО: отправляем напрямую в DataChannel, чтобы file_* и chunk_confirmation
// приходили верхнего уровня и перехватывались файловой системой, без обёртки type: 'message'
const messageString = JSON.stringify(message);
const dc = this.webrtcManager?.dataChannel;
const maxRetries = 10;
let attempt = 0;
const wait = (ms) => new Promise(r => setTimeout(r, ms));
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
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);
@@ -816,20 +921,43 @@ class EnhancedSecureFileTransfer {
throw new Error('File integrity check failed - hash mismatch');
}
// Create blob and notify
const fileBlob = new Blob([fileData], { type: receivingState.fileType });
// Lazy: храним буфер, но для совместимости формируем Blob для onFileReceived
const fileBuffer = fileData.buffer;
const fileBlob = new Blob([fileBuffer], { type: receivingState.fileType });
receivingState.endTime = Date.now();
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) {
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({
fileId: receivingState.fileId,
fileName: receivingState.fileName,
fileSize: receivingState.fileSize,
fileBlob: fileBlob,
transferTime: receivingState.endTime - receivingState.startTime
mimeType: receivingState.fileType,
transferTime: receivingState.endTime - receivingState.startTime,
// backward-compatibility for existing UIs
fileBlob,
getBlob,
getObjectURL,
revokeObjectURL
});
}
@@ -843,7 +971,13 @@ class EnhancedSecureFileTransfer {
await this.sendSecureMessage(completionMessage);
// 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);
@@ -1058,6 +1192,8 @@ class EnhancedSecureFileTransfer {
this.receivingTransfers.delete(fileId);
this.sessionKeys.delete(fileId);
// Также очищаем финальный буфер, если он ещё хранится
this.receivedFileBuffers.delete(fileId);
// Remove processed chunk IDs
for (const chunkId of this.processedChunks) {