Refactored file encryption/decryption logic for P2P transfers

- Reworked the core logic for encrypting and decrypting files exchanged between users
- Improved key derivation and session handling for file chunks
- Enhanced integrity checks to prevent tampering and replay attacks
- Work in progress: adding hardened encryption schemes and conducting fault-tolerance testing
This commit is contained in:
lockbitchat
2025-08-18 23:56:10 -04:00
parent dadc80a755
commit fe611f7630
2 changed files with 185 additions and 388 deletions

View File

@@ -4053,6 +4053,18 @@ handleSystemMessage(message) {
console.log('✅ Session activation handled successfully'); console.log('✅ Session activation handled successfully');
if (this.fileTransferSystem && this.isConnected()) {
console.log('🔄 Synchronizing file transfer keys after session activation...');
if (typeof this.fileTransferSystem.onSessionUpdate === 'function') {
this.fileTransferSystem.onSessionUpdate({
keyFingerprint: this.keyFingerprint,
sessionSalt: this.sessionSalt,
hasMacKey: !!this.macKey
});
}
}
} catch (error) { } catch (error) {
console.error('❌ Failed to handle session activation:', error); console.error('❌ Failed to handle session activation:', error);
} }

View File

@@ -32,9 +32,8 @@ class EnhancedSecureFileTransfer {
this.transferQueue = []; // Queue for pending transfers this.transferQueue = []; // Queue for pending transfers
this.pendingChunks = new Map(); this.pendingChunks = new Map();
// Session key derivation - КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ // Session key derivation
this.sessionKeys = new Map(); // fileId -> derived session key this.sessionKeys = new Map(); // fileId -> derived session key
this.sharedSecretCache = new Map(); // Кэш для shared secret чтобы sender и receiver использовали одинаковый
// Security // Security
this.processedChunks = new Set(); // Prevent replay attacks this.processedChunks = new Set(); // Prevent replay attacks
@@ -47,138 +46,67 @@ class EnhancedSecureFileTransfer {
} }
// ============================================ // ============================================
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: ДЕТЕРМИНИСТИЧЕСКОЕ СОЗДАНИЕ КЛЮЧЕЙ // SIMPLIFIED KEY DERIVATION - USE SHARED DATA
// ============================================ // ============================================
async createDeterministicSharedSecret(fileId, fileSize, salt = null) { async deriveFileSessionKey(fileId) {
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 { try {
console.log('🔑 Deriving file session key for:', fileId); console.log('🔑 Deriving file session key for:', fileId);
// Получаем детерминистический shared secret // КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Используем keyFingerprint и sessionSalt
const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, providedSalt); // которые уже согласованы между пирами
// Создаем или используем предоставленную соль if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
let salt; throw new Error('WebRTC session data not available');
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( const fileSalt = crypto.getRandomValues(new Uint8Array(32));
'raw',
sharedSecret, // Создаем seed из согласованных данных
{ name: 'PBKDF2' }, const encoder = new TextEncoder();
false, const fingerprintData = encoder.encode(this.webrtcManager.keyFingerprint);
['deriveKey'] const fileIdData = encoder.encode(fileId);
// Объединяем все компоненты для создания уникального seed
const sessionSaltArray = new Uint8Array(this.webrtcManager.sessionSalt);
const combinedSeed = new Uint8Array(
fingerprintData.length +
sessionSaltArray.length +
fileSalt.length +
fileIdData.length
); );
// Создаем файловый ключ сессии с PBKDF2 let offset = 0;
const fileSessionKey = await crypto.subtle.deriveKey( combinedSeed.set(fingerprintData, offset);
{ offset += fingerprintData.length;
name: 'PBKDF2', combinedSeed.set(sessionSaltArray, offset);
salt: salt, offset += sessionSaltArray.length;
iterations: 100000, combinedSeed.set(fileSalt, offset);
hash: 'SHA-384' offset += fileSalt.length;
}, combinedSeed.set(fileIdData, offset);
keyForDerivation,
{ // Хешируем для получения ключевого материала
name: 'AES-GCM', const keyMaterial = await crypto.subtle.digest('SHA-256', combinedSeed);
length: 256
}, // Импортируем как AES ключ напрямую
const fileSessionKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false, false,
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
); );
// Сохраняем ключ сессии // Сохраняем ключ и соль
this.sessionKeys.set(fileId, { this.sessionKeys.set(fileId, {
key: fileSessionKey, key: fileSessionKey,
salt: Array.from(salt), salt: Array.from(fileSalt),
created: Date.now() created: Date.now()
}); });
console.log('✅ File session key derived successfully for:', fileId); console.log('✅ File session key derived successfully for:', fileId);
return { key: fileSessionKey, salt: Array.from(salt) }; return { key: fileSessionKey, salt: Array.from(fileSalt) };
} catch (error) { } catch (error) {
console.error('❌ Failed to derive file session key:', error); console.error('❌ Failed to derive file session key:', error);
@@ -186,45 +114,53 @@ class EnhancedSecureFileTransfer {
} }
} }
// ============================================ async deriveFileSessionKeyFromSalt(fileId, saltArray) {
// ИСПРАВЛЕННЫЙ МЕТОД ДЛЯ ПОЛУЧАТЕЛЯ
// ============================================
async deriveFileSessionKeyFromSalt(fileId, fileSize, saltArray) {
try { try {
console.log('🔑 Deriving session key from salt for receiver:', fileId); console.log('🔑 Deriving session key from salt for receiver:', fileId);
if (!saltArray || !Array.isArray(saltArray)) { // Проверка соли
throw new Error('Invalid salt provided for key derivation'); if (!saltArray || !Array.isArray(saltArray) || saltArray.length !== 32) {
throw new Error(`Invalid salt: ${saltArray?.length || 0} bytes`);
} }
// Получаем тот же детерминистический shared secret что и отправитель if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, saltArray); throw new Error('WebRTC session data not available');
}
const salt = new Uint8Array(saltArray); // Используем тот же процесс что и отправитель
const encoder = new TextEncoder();
const fingerprintData = encoder.encode(this.webrtcManager.keyFingerprint);
const fileIdData = encoder.encode(fileId);
// Импортируем shared secret как PBKDF2 ключ // Используем полученную соль файла
const keyForDerivation = await crypto.subtle.importKey( const fileSalt = new Uint8Array(saltArray);
'raw', const sessionSaltArray = new Uint8Array(this.webrtcManager.sessionSalt);
sharedSecret,
{ name: 'PBKDF2' }, // Объединяем компоненты в том же порядке
false, const combinedSeed = new Uint8Array(
['deriveKey'] fingerprintData.length +
sessionSaltArray.length +
fileSalt.length +
fileIdData.length
); );
// Создаем точно такой же ключ как у отправителя let offset = 0;
const fileSessionKey = await crypto.subtle.deriveKey( combinedSeed.set(fingerprintData, offset);
{ offset += fingerprintData.length;
name: 'PBKDF2', combinedSeed.set(sessionSaltArray, offset);
salt: salt, offset += sessionSaltArray.length;
iterations: 100000, // Те же параметры что у отправителя combinedSeed.set(fileSalt, offset);
hash: 'SHA-384' offset += fileSalt.length;
}, combinedSeed.set(fileIdData, offset);
keyForDerivation,
{ // Хешируем для получения того же ключевого материала
name: 'AES-GCM', const keyMaterial = await crypto.subtle.digest('SHA-256', combinedSeed);
length: 256
}, // Импортируем как AES ключ
const fileSessionKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false, false,
['encrypt', 'decrypt'] ['encrypt', 'decrypt']
); );
@@ -260,7 +196,6 @@ class EnhancedSecureFileTransfer {
webrtcManagerType: this.webrtcManager.constructor?.name, webrtcManagerType: this.webrtcManager.constructor?.name,
hasEncryptionKey: !!this.webrtcManager.encryptionKey, hasEncryptionKey: !!this.webrtcManager.encryptionKey,
hasMacKey: !!this.webrtcManager.macKey, hasMacKey: !!this.webrtcManager.macKey,
hasEcdhKeyPair: !!this.webrtcManager.ecdhKeyPair,
isConnected: this.webrtcManager.isConnected?.(), isConnected: this.webrtcManager.isConnected?.(),
isVerified: this.webrtcManager.isVerified isVerified: this.webrtcManager.isVerified
}); });
@@ -284,8 +219,8 @@ class EnhancedSecureFileTransfer {
// Calculate file hash for integrity verification // Calculate file hash for integrity verification
const fileHash = await this.calculateFileHash(file); const fileHash = await this.calculateFileHash(file);
// Derive session key for this file - ИСПРАВЛЕНИЕ // Derive session key for this file
const keyResult = await this.deriveFileSessionKey(fileId, file.size); const keyResult = await this.deriveFileSessionKey(fileId);
const sessionKey = keyResult.key; const sessionKey = keyResult.key;
const salt = keyResult.salt; const salt = keyResult.salt;
@@ -323,7 +258,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// ИСПРАВЛЕННЫЙ метод отправки метаданных
async sendFileMetadata(transferState) { async sendFileMetadata(transferState) {
try { try {
const metadata = { const metadata = {
@@ -337,7 +271,7 @@ class EnhancedSecureFileTransfer {
chunkSize: this.CHUNK_SIZE, chunkSize: this.CHUNK_SIZE,
salt: transferState.salt, // Отправляем соль получателю salt: transferState.salt, // Отправляем соль получателю
timestamp: Date.now(), timestamp: Date.now(),
version: '1.0' version: '2.0'
}; };
console.log('📁 Sending file metadata for:', transferState.file.name); console.log('📁 Sending file metadata for:', transferState.file.name);
@@ -366,7 +300,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Start chunk transmission
async startChunkTransmission(transferState) { async startChunkTransmission(transferState) {
try { try {
transferState.status = 'transmitting'; transferState.status = 'transmitting';
@@ -426,7 +359,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Read file chunk
async readFileChunk(file, start, end) { async readFileChunk(file, start, end) {
try { try {
const blob = file.slice(start, end); const blob = file.slice(start, end);
@@ -437,7 +369,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Send file chunk
async sendFileChunk(transferState, chunkIndex, chunkData) { async sendFileChunk(transferState, chunkIndex, chunkData) {
try { try {
const sessionKey = transferState.sessionKey; const sessionKey = transferState.sessionKey;
@@ -473,10 +404,9 @@ class EnhancedSecureFileTransfer {
} }
} }
// Send secure message through WebRTC
async sendSecureMessage(message) { async sendSecureMessage(message) {
try { try {
// Send through existing Double Ratchet channel // Send through existing WebRTC channel
const messageString = JSON.stringify(message); const messageString = JSON.stringify(message);
// Use the WebRTC manager's sendMessage method // Use the WebRTC manager's sendMessage method
@@ -491,11 +421,10 @@ class EnhancedSecureFileTransfer {
} }
} }
// Calculate file hash for integrity verification
async calculateFileHash(file) { async calculateFileHash(file) {
try { try {
const arrayBuffer = await file.arrayBuffer(); const arrayBuffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-384', arrayBuffer); const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} catch (error) { } catch (error) {
@@ -509,49 +438,10 @@ class EnhancedSecureFileTransfer {
// ============================================ // ============================================
setupFileMessageHandlers() { setupFileMessageHandlers() {
// Store original message handler // This is now handled by WebRTC manager's processMessage method
const originalHandler = this.webrtcManager.onMessage; // No need to override onMessage here
// 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) { async handleFileTransferStart(metadata) {
try { try {
console.log('📥 Receiving file transfer:', metadata.fileName); console.log('📥 Receiving file transfer:', metadata.fileName);
@@ -567,10 +457,9 @@ class EnhancedSecureFileTransfer {
return; return;
} }
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Используем соль из метаданных // Derive session key from salt
const sessionKey = await this.deriveFileSessionKeyFromSalt( const sessionKey = await this.deriveFileSessionKeyFromSalt(
metadata.fileId, metadata.fileId,
metadata.fileSize,
metadata.salt metadata.salt
); );
@@ -584,6 +473,7 @@ class EnhancedSecureFileTransfer {
totalChunks: metadata.totalChunks, totalChunks: metadata.totalChunks,
chunkSize: metadata.chunkSize || this.CHUNK_SIZE, chunkSize: metadata.chunkSize || this.CHUNK_SIZE,
sessionKey: sessionKey, sessionKey: sessionKey,
salt: metadata.salt,
receivedChunks: new Map(), receivedChunks: new Map(),
receivedCount: 0, receivedCount: 0,
startTime: Date.now(), startTime: Date.now(),
@@ -632,7 +522,6 @@ class EnhancedSecureFileTransfer {
console.error('❌ Failed to handle file transfer start:', error); console.error('❌ Failed to handle file transfer start:', error);
// Send error response // Send error response
try {
const errorResponse = { const errorResponse = {
type: 'file_transfer_response', type: 'file_transfer_response',
fileId: metadata.fileId, fileId: metadata.fileId,
@@ -641,13 +530,9 @@ class EnhancedSecureFileTransfer {
timestamp: Date.now() timestamp: Date.now()
}; };
await this.sendSecureMessage(errorResponse); await this.sendSecureMessage(errorResponse);
} catch (responseError) {
console.error('❌ Failed to send error response:', responseError);
}
} }
} }
// ИСПРАВЛЕННЫЙ Handle incoming file chunk
async handleFileChunk(chunkMessage) { async handleFileChunk(chunkMessage) {
try { try {
let receivingState = this.receivingTransfers.get(chunkMessage.fileId); let receivingState = this.receivingTransfers.get(chunkMessage.fileId);
@@ -678,20 +563,13 @@ class EnhancedSecureFileTransfer {
throw new Error(`Invalid chunk index: ${chunkMessage.chunkIndex}`); throw new Error(`Invalid chunk index: ${chunkMessage.chunkIndex}`);
} }
// ИСПРАВЛЕНИЕ: Улучшенное декодирование чанка // Decrypt chunk
const nonce = new Uint8Array(chunkMessage.nonce); const nonce = new Uint8Array(chunkMessage.nonce);
const encryptedData = new Uint8Array(chunkMessage.encryptedData); const encryptedData = new Uint8Array(chunkMessage.encryptedData);
console.log('🔓 Decrypting chunk:', chunkMessage.chunkIndex, { console.log('🔓 Decrypting chunk:', chunkMessage.chunkIndex);
nonceLength: nonce.length,
encryptedDataLength: encryptedData.length,
expectedSize: chunkMessage.chunkSize
});
// Decrypt chunk with better error handling const decryptedChunk = await crypto.subtle.decrypt(
let decryptedChunk;
try {
decryptedChunk = await crypto.subtle.decrypt(
{ {
name: 'AES-GCM', name: 'AES-GCM',
iv: nonce iv: nonce
@@ -699,28 +577,6 @@ class EnhancedSecureFileTransfer {
receivingState.sessionKey, receivingState.sessionKey,
encryptedData 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 // Verify chunk size
if (decryptedChunk.byteLength !== chunkMessage.chunkSize) { if (decryptedChunk.byteLength !== chunkMessage.chunkSize) {
@@ -766,21 +622,27 @@ class EnhancedSecureFileTransfer {
console.error('❌ Failed to handle file chunk:', error); console.error('❌ Failed to handle file chunk:', error);
// Send error notification // Send error notification
try {
const errorMessage = { const errorMessage = {
type: 'file_transfer_error', type: 'file_transfer_error',
fileId: chunkMessage.fileId, fileId: chunkMessage.fileId,
error: error.message, error: error.message,
chunkIndex: chunkMessage.chunkIndex,
timestamp: Date.now() timestamp: Date.now()
}; };
await this.sendSecureMessage(errorMessage); await this.sendSecureMessage(errorMessage);
} catch (errorSendError) {
console.error('❌ Failed to send chunk error:', errorSendError); // Mark transfer as failed
const receivingState = this.receivingTransfers.get(chunkMessage.fileId);
if (receivingState) {
receivingState.status = 'failed';
}
if (this.onError) {
this.onError(`Chunk processing failed: ${error.message}`);
} }
} }
} }
// Assemble received file
async assembleFile(receivingState) { async assembleFile(receivingState) {
try { try {
console.log('🔄 Assembling file:', receivingState.fileName); console.log('🔄 Assembling file:', receivingState.fileName);
@@ -863,7 +725,6 @@ class EnhancedSecureFileTransfer {
} }
// Send error notification // Send error notification
try {
const errorMessage = { const errorMessage = {
type: 'file_transfer_complete', type: 'file_transfer_complete',
fileId: receivingState.fileId, fileId: receivingState.fileId,
@@ -872,19 +733,15 @@ class EnhancedSecureFileTransfer {
timestamp: Date.now() timestamp: Date.now()
}; };
await this.sendSecureMessage(errorMessage); await this.sendSecureMessage(errorMessage);
} catch (errorSendError) {
console.error('❌ Failed to send assembly error:', errorSendError);
}
// Cleanup failed transfer // Cleanup failed transfer
this.cleanupReceivingTransfer(receivingState.fileId); this.cleanupReceivingTransfer(receivingState.fileId);
} }
} }
// Calculate hash from data
async calculateFileHashFromData(data) { async calculateFileHashFromData(data) {
try { try {
const hashBuffer = await crypto.subtle.digest('SHA-384', data); const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
} catch (error) { } catch (error) {
@@ -893,7 +750,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Handle transfer response
handleTransferResponse(response) { handleTransferResponse(response) {
try { try {
console.log('📨 File transfer response:', response); console.log('📨 File transfer response:', response);
@@ -923,7 +779,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Handle chunk confirmation
handleChunkConfirmation(confirmation) { handleChunkConfirmation(confirmation) {
try { try {
const transferState = this.activeTransfers.get(confirmation.fileId); const transferState = this.activeTransfers.get(confirmation.fileId);
@@ -940,7 +795,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Handle transfer completion
handleTransferComplete(completion) { handleTransferComplete(completion) {
try { try {
console.log('🏁 Transfer completion:', completion); console.log('🏁 Transfer completion:', completion);
@@ -980,7 +834,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Handle transfer error
handleTransferError(errorMessage) { handleTransferError(errorMessage) {
try { try {
console.error('❌ Transfer error received:', errorMessage); console.error('❌ Transfer error received:', errorMessage);
@@ -1010,7 +863,6 @@ class EnhancedSecureFileTransfer {
// UTILITY METHODS // UTILITY METHODS
// ============================================ // ============================================
// Get active transfers
getActiveTransfers() { getActiveTransfers() {
return Array.from(this.activeTransfers.values()).map(transfer => ({ return Array.from(this.activeTransfers.values()).map(transfer => ({
fileId: transfer.fileId, fileId: transfer.fileId,
@@ -1022,7 +874,6 @@ class EnhancedSecureFileTransfer {
})); }));
} }
// Get receiving transfers
getReceivingTransfers() { getReceivingTransfers() {
return Array.from(this.receivingTransfers.values()).map(transfer => ({ return Array.from(this.receivingTransfers.values()).map(transfer => ({
fileId: transfer.fileId, fileId: transfer.fileId,
@@ -1034,7 +885,6 @@ class EnhancedSecureFileTransfer {
})); }));
} }
// Cancel transfer
cancelTransfer(fileId) { cancelTransfer(fileId) {
try { try {
if (this.activeTransfers.has(fileId)) { if (this.activeTransfers.has(fileId)) {
@@ -1052,19 +902,11 @@ class EnhancedSecureFileTransfer {
} }
} }
// Cleanup transfer
cleanupTransfer(fileId) { cleanupTransfer(fileId) {
this.activeTransfers.delete(fileId); this.activeTransfers.delete(fileId);
this.sessionKeys.delete(fileId); this.sessionKeys.delete(fileId);
this.transferNonces.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 // Remove processed chunk IDs for this transfer
for (const chunkId of this.processedChunks) { for (const chunkId of this.processedChunks) {
if (chunkId.startsWith(fileId)) { if (chunkId.startsWith(fileId)) {
@@ -1073,17 +915,12 @@ class EnhancedSecureFileTransfer {
} }
} }
// Cleanup receiving transfer
cleanupReceivingTransfer(fileId) { cleanupReceivingTransfer(fileId) {
this.pendingChunks.delete(fileId); this.pendingChunks.delete(fileId);
const receivingState = this.receivingTransfers.get(fileId); const receivingState = this.receivingTransfers.get(fileId);
if (receivingState) { if (receivingState) {
// Clear chunk data from memory // Clear chunk data from memory
receivingState.receivedChunks.clear(); receivingState.receivedChunks.clear();
// Remove from shared secret cache
const fileIdentifier = `${fileId}-${receivingState.fileSize}`;
this.sharedSecretCache.delete(fileIdentifier);
} }
this.receivingTransfers.delete(fileId); this.receivingTransfers.delete(fileId);
@@ -1097,7 +934,6 @@ class EnhancedSecureFileTransfer {
} }
} }
// Get transfer status
getTransferStatus(fileId) { getTransferStatus(fileId) {
if (this.activeTransfers.has(fileId)) { if (this.activeTransfers.has(fileId)) {
const transfer = this.activeTransfers.get(fileId); const transfer = this.activeTransfers.get(fileId);
@@ -1126,7 +962,6 @@ class EnhancedSecureFileTransfer {
return null; return null;
} }
// Get system status
getSystemStatus() { getSystemStatus() {
return { return {
initialized: true, initialized: true,
@@ -1137,12 +972,10 @@ class EnhancedSecureFileTransfer {
maxFileSize: this.MAX_FILE_SIZE, maxFileSize: this.MAX_FILE_SIZE,
chunkSize: this.CHUNK_SIZE, chunkSize: this.CHUNK_SIZE,
hasWebrtcManager: !!this.webrtcManager, hasWebrtcManager: !!this.webrtcManager,
isConnected: this.webrtcManager?.isConnected?.() || false, isConnected: this.webrtcManager?.isConnected?.() || false
sharedSecretCacheSize: this.sharedSecretCache.size
}; };
} }
// Cleanup all transfers (called on disconnect)
cleanup() { cleanup() {
console.log('🧹 Cleaning up file transfer system'); console.log('🧹 Cleaning up file transfer system');
@@ -1163,123 +996,75 @@ class EnhancedSecureFileTransfer {
this.sessionKeys.clear(); this.sessionKeys.clear();
this.transferNonces.clear(); this.transferNonces.clear();
this.processedChunks.clear(); this.processedChunks.clear();
this.sharedSecretCache.clear(); // Очищаем кэш shared secret }
// ============================================
// SESSION UPDATE HANDLER - FIXED
// ============================================
onSessionUpdate(sessionData) {
console.log('🔄 File transfer system: session updated', sessionData);
// Clear session keys cache for resync
this.sessionKeys.clear();
console.log('✅ File transfer keys cache cleared for resync');
// If there are active transfers, log warning
if (this.activeTransfers.size > 0 || this.receivingTransfers.size > 0) {
console.warn('⚠️ Session updated during active file transfers - may cause issues');
}
} }
// ============================================ // ============================================
// DEBUGGING AND DIAGNOSTICS // DEBUGGING AND DIAGNOSTICS
// ============================================ // ============================================
// Debug method to check key derivation async debugKeyDerivation(fileId) {
async debugKeyDerivation(fileId, fileSize, salt = null) {
try { try {
console.log('🔍 Debug: Testing key derivation for:', fileId); console.log('🔍 Debug: Testing key derivation for:', fileId);
const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, salt); if (!this.webrtcManager.macKey) {
console.log('🔍 Shared secret created, length:', sharedSecret.byteLength); throw new Error('MAC key not available');
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 const salt = crypto.getRandomValues(new Uint8Array(32));
async debugEncryptionDecryption(fileId, fileSize, testData = 'test data') {
try {
console.log('🔍 Debug: Testing encryption/decryption for:', fileId);
const keyResult = await this.deriveFileSessionKey(fileId, fileSize); // Test sender derivation
const sessionKey = keyResult.key; const senderResult = await this.deriveFileSessionKey(fileId);
const salt = keyResult.salt; console.log('✅ Sender key derived successfully');
// Test encryption // Test receiver derivation with same salt
const receiverKey = await this.deriveFileSessionKeyFromSalt(fileId, senderResult.salt);
console.log('✅ Receiver key derived successfully');
// Test encryption/decryption
const testData = new TextEncoder().encode('test data');
const nonce = crypto.getRandomValues(new Uint8Array(12)); const nonce = crypto.getRandomValues(new Uint8Array(12));
const testDataBuffer = new TextEncoder().encode(testData);
const encrypted = await crypto.subtle.encrypt( const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce }, { name: 'AES-GCM', iv: nonce },
sessionKey, senderResult.key,
testDataBuffer testData
); );
console.log('✅ Encryption test successful');
// Test decryption with same key
const decrypted = await crypto.subtle.decrypt( 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 }, { name: 'AES-GCM', iv: nonce },
receiverKey, receiverKey,
encrypted encrypted
); );
const receiverDecryptedText = new TextDecoder().decode(decryptedByReceiver); const decryptedText = new TextDecoder().decode(decrypted);
if (receiverDecryptedText === testData) { if (decryptedText === 'test data') {
console.log('✅ Receiver key derivation test successful'); console.log('✅ Cross-key encryption/decryption test successful');
return { success: true, message: 'All tests passed' }; return { success: true, message: 'All tests passed' };
} else {
throw new Error('Receiver decryption failed');
}
} else { } else {
throw new Error('Decryption verification failed'); throw new Error('Decryption verification failed');
} }
} catch (error) { } catch (error) {
console.error('❌ Encryption/decryption test failed:', error); console.error('❌ Key derivation test failed:', error);
return { success: false, error: error.message }; return { success: false, error: error.message };
} }
} }