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:
@@ -4053,6 +4053,18 @@ handleSystemMessage(message) {
|
||||
|
||||
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) {
|
||||
console.error('❌ Failed to handle session activation:', error);
|
||||
}
|
||||
|
||||
@@ -32,9 +32,8 @@ class EnhancedSecureFileTransfer {
|
||||
this.transferQueue = []; // Queue for pending transfers
|
||||
this.pendingChunks = new Map();
|
||||
|
||||
// Session key derivation - КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ
|
||||
// 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
|
||||
@@ -47,138 +46,67 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: ДЕТЕРМИНИСТИЧЕСКОЕ СОЗДАНИЕ КЛЮЧЕЙ
|
||||
// SIMPLIFIED KEY DERIVATION - USE SHARED DATA
|
||||
// ============================================
|
||||
|
||||
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) {
|
||||
async deriveFileSessionKey(fileId) {
|
||||
try {
|
||||
console.log('🔑 Deriving file session key for:', fileId);
|
||||
|
||||
// Получаем детерминистический shared secret
|
||||
const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, providedSalt);
|
||||
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Используем keyFingerprint и sessionSalt
|
||||
// которые уже согласованы между пирами
|
||||
|
||||
// Создаем или используем предоставленную соль
|
||||
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');
|
||||
if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
|
||||
throw new Error('WebRTC session data not available');
|
||||
}
|
||||
|
||||
// Импортируем shared secret как PBKDF2 ключ
|
||||
const keyForDerivation = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
sharedSecret,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
// Генерируем соль для этого конкретного файла
|
||||
const fileSalt = crypto.getRandomValues(new Uint8Array(32));
|
||||
|
||||
// Создаем seed из согласованных данных
|
||||
const encoder = new TextEncoder();
|
||||
const fingerprintData = encoder.encode(this.webrtcManager.keyFingerprint);
|
||||
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
|
||||
const fileSessionKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-384'
|
||||
},
|
||||
keyForDerivation,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
let offset = 0;
|
||||
combinedSeed.set(fingerprintData, offset);
|
||||
offset += fingerprintData.length;
|
||||
combinedSeed.set(sessionSaltArray, offset);
|
||||
offset += sessionSaltArray.length;
|
||||
combinedSeed.set(fileSalt, offset);
|
||||
offset += fileSalt.length;
|
||||
combinedSeed.set(fileIdData, offset);
|
||||
|
||||
// Хешируем для получения ключевого материала
|
||||
const keyMaterial = await crypto.subtle.digest('SHA-256', combinedSeed);
|
||||
|
||||
// Импортируем как AES ключ напрямую
|
||||
const fileSessionKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
|
||||
// Сохраняем ключ сессии
|
||||
// Сохраняем ключ и соль
|
||||
this.sessionKeys.set(fileId, {
|
||||
key: fileSessionKey,
|
||||
salt: Array.from(salt),
|
||||
salt: Array.from(fileSalt),
|
||||
created: Date.now()
|
||||
});
|
||||
|
||||
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) {
|
||||
console.error('❌ Failed to derive file session key:', error);
|
||||
@@ -186,45 +114,53 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// ИСПРАВЛЕННЫЙ МЕТОД ДЛЯ ПОЛУЧАТЕЛЯ
|
||||
// ============================================
|
||||
|
||||
async deriveFileSessionKeyFromSalt(fileId, fileSize, saltArray) {
|
||||
async deriveFileSessionKeyFromSalt(fileId, 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');
|
||||
// Проверка соли
|
||||
if (!saltArray || !Array.isArray(saltArray) || saltArray.length !== 32) {
|
||||
throw new Error(`Invalid salt: ${saltArray?.length || 0} bytes`);
|
||||
}
|
||||
|
||||
// Получаем тот же детерминистический shared secret что и отправитель
|
||||
const sharedSecret = await this.createDeterministicSharedSecret(fileId, fileSize, saltArray);
|
||||
if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
|
||||
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(
|
||||
'raw',
|
||||
sharedSecret,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
// Используем полученную соль файла
|
||||
const fileSalt = new Uint8Array(saltArray);
|
||||
const sessionSaltArray = new Uint8Array(this.webrtcManager.sessionSalt);
|
||||
|
||||
// Объединяем компоненты в том же порядке
|
||||
const combinedSeed = new Uint8Array(
|
||||
fingerprintData.length +
|
||||
sessionSaltArray.length +
|
||||
fileSalt.length +
|
||||
fileIdData.length
|
||||
);
|
||||
|
||||
// Создаем точно такой же ключ как у отправителя
|
||||
const fileSessionKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: salt,
|
||||
iterations: 100000, // Те же параметры что у отправителя
|
||||
hash: 'SHA-384'
|
||||
},
|
||||
keyForDerivation,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
let offset = 0;
|
||||
combinedSeed.set(fingerprintData, offset);
|
||||
offset += fingerprintData.length;
|
||||
combinedSeed.set(sessionSaltArray, offset);
|
||||
offset += sessionSaltArray.length;
|
||||
combinedSeed.set(fileSalt, offset);
|
||||
offset += fileSalt.length;
|
||||
combinedSeed.set(fileIdData, offset);
|
||||
|
||||
// Хешируем для получения того же ключевого материала
|
||||
const keyMaterial = await crypto.subtle.digest('SHA-256', combinedSeed);
|
||||
|
||||
// Импортируем как AES ключ
|
||||
const fileSessionKey = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
keyMaterial,
|
||||
{ name: 'AES-GCM' },
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
@@ -260,7 +196,6 @@ class EnhancedSecureFileTransfer {
|
||||
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
|
||||
});
|
||||
@@ -284,8 +219,8 @@ class EnhancedSecureFileTransfer {
|
||||
// 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);
|
||||
// Derive session key for this file
|
||||
const keyResult = await this.deriveFileSessionKey(fileId);
|
||||
const sessionKey = keyResult.key;
|
||||
const salt = keyResult.salt;
|
||||
|
||||
@@ -323,7 +258,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕННЫЙ метод отправки метаданных
|
||||
async sendFileMetadata(transferState) {
|
||||
try {
|
||||
const metadata = {
|
||||
@@ -337,7 +271,7 @@ class EnhancedSecureFileTransfer {
|
||||
chunkSize: this.CHUNK_SIZE,
|
||||
salt: transferState.salt, // Отправляем соль получателю
|
||||
timestamp: Date.now(),
|
||||
version: '1.0'
|
||||
version: '2.0'
|
||||
};
|
||||
|
||||
console.log('📁 Sending file metadata for:', transferState.file.name);
|
||||
@@ -366,7 +300,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Start chunk transmission
|
||||
async startChunkTransmission(transferState) {
|
||||
try {
|
||||
transferState.status = 'transmitting';
|
||||
@@ -426,7 +359,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Read file chunk
|
||||
async readFileChunk(file, start, end) {
|
||||
try {
|
||||
const blob = file.slice(start, end);
|
||||
@@ -437,7 +369,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Send file chunk
|
||||
async sendFileChunk(transferState, chunkIndex, chunkData) {
|
||||
try {
|
||||
const sessionKey = transferState.sessionKey;
|
||||
@@ -473,10 +404,9 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Send secure message through WebRTC
|
||||
async sendSecureMessage(message) {
|
||||
try {
|
||||
// Send through existing Double Ratchet channel
|
||||
// Send through existing WebRTC channel
|
||||
const messageString = JSON.stringify(message);
|
||||
|
||||
// Use the WebRTC manager's sendMessage method
|
||||
@@ -491,11 +421,10 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// 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 hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
} catch (error) {
|
||||
@@ -509,49 +438,10 @@ class EnhancedSecureFileTransfer {
|
||||
// ============================================
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
// This is now handled by WebRTC manager's processMessage method
|
||||
// No need to override onMessage here
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕННЫЙ Handle incoming file transfer start
|
||||
async handleFileTransferStart(metadata) {
|
||||
try {
|
||||
console.log('📥 Receiving file transfer:', metadata.fileName);
|
||||
@@ -567,10 +457,9 @@ class EnhancedSecureFileTransfer {
|
||||
return;
|
||||
}
|
||||
|
||||
// КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: Используем соль из метаданных
|
||||
// Derive session key from salt
|
||||
const sessionKey = await this.deriveFileSessionKeyFromSalt(
|
||||
metadata.fileId,
|
||||
metadata.fileSize,
|
||||
metadata.fileId,
|
||||
metadata.salt
|
||||
);
|
||||
|
||||
@@ -584,6 +473,7 @@ class EnhancedSecureFileTransfer {
|
||||
totalChunks: metadata.totalChunks,
|
||||
chunkSize: metadata.chunkSize || this.CHUNK_SIZE,
|
||||
sessionKey: sessionKey,
|
||||
salt: metadata.salt,
|
||||
receivedChunks: new Map(),
|
||||
receivedCount: 0,
|
||||
startTime: Date.now(),
|
||||
@@ -632,22 +522,17 @@ class EnhancedSecureFileTransfer {
|
||||
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);
|
||||
}
|
||||
const errorResponse = {
|
||||
type: 'file_transfer_response',
|
||||
fileId: metadata.fileId,
|
||||
accepted: false,
|
||||
error: error.message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
await this.sendSecureMessage(errorResponse);
|
||||
}
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕННЫЙ Handle incoming file chunk
|
||||
async handleFileChunk(chunkMessage) {
|
||||
try {
|
||||
let receivingState = this.receivingTransfers.get(chunkMessage.fileId);
|
||||
@@ -678,49 +563,20 @@ class EnhancedSecureFileTransfer {
|
||||
throw new Error(`Invalid chunk index: ${chunkMessage.chunkIndex}`);
|
||||
}
|
||||
|
||||
// ИСПРАВЛЕНИЕ: Улучшенное декодирование чанка
|
||||
// Decrypt chunk
|
||||
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
|
||||
});
|
||||
console.log('🔓 Decrypting chunk:', chunkMessage.chunkIndex);
|
||||
|
||||
// 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;
|
||||
}
|
||||
const decryptedChunk = await crypto.subtle.decrypt(
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
iv: nonce
|
||||
},
|
||||
receivingState.sessionKey,
|
||||
encryptedData
|
||||
);
|
||||
|
||||
// Verify chunk size
|
||||
if (decryptedChunk.byteLength !== chunkMessage.chunkSize) {
|
||||
@@ -766,21 +622,27 @@ class EnhancedSecureFileTransfer {
|
||||
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);
|
||||
const errorMessage = {
|
||||
type: 'file_transfer_error',
|
||||
fileId: chunkMessage.fileId,
|
||||
error: error.message,
|
||||
chunkIndex: chunkMessage.chunkIndex,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
await this.sendSecureMessage(errorMessage);
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
console.log('🔄 Assembling file:', receivingState.fileName);
|
||||
@@ -863,28 +725,23 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
const errorMessage = {
|
||||
type: 'file_transfer_complete',
|
||||
fileId: receivingState.fileId,
|
||||
success: false,
|
||||
error: error.message,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
await this.sendSecureMessage(errorMessage);
|
||||
|
||||
// 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 hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
} catch (error) {
|
||||
@@ -893,7 +750,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle transfer response
|
||||
handleTransferResponse(response) {
|
||||
try {
|
||||
console.log('📨 File transfer response:', response);
|
||||
@@ -923,7 +779,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle chunk confirmation
|
||||
handleChunkConfirmation(confirmation) {
|
||||
try {
|
||||
const transferState = this.activeTransfers.get(confirmation.fileId);
|
||||
@@ -940,7 +795,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle transfer completion
|
||||
handleTransferComplete(completion) {
|
||||
try {
|
||||
console.log('🏁 Transfer completion:', completion);
|
||||
@@ -980,7 +834,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle transfer error
|
||||
handleTransferError(errorMessage) {
|
||||
try {
|
||||
console.error('❌ Transfer error received:', errorMessage);
|
||||
@@ -1010,7 +863,6 @@ class EnhancedSecureFileTransfer {
|
||||
// UTILITY METHODS
|
||||
// ============================================
|
||||
|
||||
// Get active transfers
|
||||
getActiveTransfers() {
|
||||
return Array.from(this.activeTransfers.values()).map(transfer => ({
|
||||
fileId: transfer.fileId,
|
||||
@@ -1022,7 +874,6 @@ class EnhancedSecureFileTransfer {
|
||||
}));
|
||||
}
|
||||
|
||||
// Get receiving transfers
|
||||
getReceivingTransfers() {
|
||||
return Array.from(this.receivingTransfers.values()).map(transfer => ({
|
||||
fileId: transfer.fileId,
|
||||
@@ -1034,7 +885,6 @@ class EnhancedSecureFileTransfer {
|
||||
}));
|
||||
}
|
||||
|
||||
// Cancel transfer
|
||||
cancelTransfer(fileId) {
|
||||
try {
|
||||
if (this.activeTransfers.has(fileId)) {
|
||||
@@ -1052,19 +902,11 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
@@ -1073,17 +915,12 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -1097,7 +934,6 @@ class EnhancedSecureFileTransfer {
|
||||
}
|
||||
}
|
||||
|
||||
// Get transfer status
|
||||
getTransferStatus(fileId) {
|
||||
if (this.activeTransfers.has(fileId)) {
|
||||
const transfer = this.activeTransfers.get(fileId);
|
||||
@@ -1126,7 +962,6 @@ class EnhancedSecureFileTransfer {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get system status
|
||||
getSystemStatus() {
|
||||
return {
|
||||
initialized: true,
|
||||
@@ -1137,12 +972,10 @@ class EnhancedSecureFileTransfer {
|
||||
maxFileSize: this.MAX_FILE_SIZE,
|
||||
chunkSize: this.CHUNK_SIZE,
|
||||
hasWebrtcManager: !!this.webrtcManager,
|
||||
isConnected: this.webrtcManager?.isConnected?.() || false,
|
||||
sharedSecretCacheSize: this.sharedSecretCache.size
|
||||
isConnected: this.webrtcManager?.isConnected?.() || false
|
||||
};
|
||||
}
|
||||
|
||||
// Cleanup all transfers (called on disconnect)
|
||||
cleanup() {
|
||||
console.log('🧹 Cleaning up file transfer system');
|
||||
|
||||
@@ -1163,123 +996,75 @@ class EnhancedSecureFileTransfer {
|
||||
this.sessionKeys.clear();
|
||||
this.transferNonces.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
|
||||
// ============================================
|
||||
|
||||
// Debug method to check key derivation
|
||||
async debugKeyDerivation(fileId, fileSize, salt = null) {
|
||||
async debugKeyDerivation(fileId) {
|
||||
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);
|
||||
if (!this.webrtcManager.macKey) {
|
||||
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 salt = crypto.getRandomValues(new Uint8Array(32));
|
||||
|
||||
const keyForDerivation = await crypto.subtle.importKey(
|
||||
'raw',
|
||||
sharedSecret,
|
||||
{ name: 'PBKDF2' },
|
||||
false,
|
||||
['deriveKey']
|
||||
);
|
||||
// Test sender derivation
|
||||
const senderResult = await this.deriveFileSessionKey(fileId);
|
||||
console.log('✅ Sender key derived successfully');
|
||||
|
||||
const derivedKey = await crypto.subtle.deriveKey(
|
||||
{
|
||||
name: 'PBKDF2',
|
||||
salt: testSalt,
|
||||
iterations: 100000,
|
||||
hash: 'SHA-384'
|
||||
},
|
||||
keyForDerivation,
|
||||
{
|
||||
name: 'AES-GCM',
|
||||
length: 256
|
||||
},
|
||||
false,
|
||||
['encrypt', 'decrypt']
|
||||
);
|
||||
// Test receiver derivation with same salt
|
||||
const receiverKey = await this.deriveFileSessionKeyFromSalt(fileId, senderResult.salt);
|
||||
console.log('✅ Receiver key derived successfully');
|
||||
|
||||
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
|
||||
// Test encryption/decryption
|
||||
const testData = new TextEncoder().encode('test data');
|
||||
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
|
||||
senderResult.key,
|
||||
testData
|
||||
);
|
||||
|
||||
console.log('✅ Encryption test successful');
|
||||
|
||||
// Test decryption with same key
|
||||
const decrypted = await crypto.subtle.decrypt(
|
||||
{ name: 'AES-GCM', iv: nonce },
|
||||
sessionKey,
|
||||
receiverKey,
|
||||
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');
|
||||
}
|
||||
if (decryptedText === 'test data') {
|
||||
console.log('✅ Cross-key encryption/decryption test successful');
|
||||
return { success: true, message: 'All tests passed' };
|
||||
} else {
|
||||
throw new Error('Decryption verification failed');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Encryption/decryption test failed:', error);
|
||||
console.error('❌ Key derivation test failed:', error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user