Files
securebit-chat/src/transfer/EnhancedSecureFileTransfer.js
T
2026-05-17 23:05:43 -04:00

2176 lines
81 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================
// SECURE FILE TRANSFER CONTEXT
// ============================================
class SecureFileTransferContext {
static #instance = null;
static #contextKey = Symbol('SecureFileTransferContext');
static getInstance() {
if (!this.#instance) {
this.#instance = new SecureFileTransferContext();
}
return this.#instance;
}
#fileTransferSystem = null;
#active = false;
#securityLevel = 'high';
setFileTransferSystem(system) {
if (!(system instanceof EnhancedSecureFileTransfer)) {
throw new Error('Invalid file transfer system instance');
}
this.#fileTransferSystem = system;
this.#active = true;
}
getFileTransferSystem() {
return this.#fileTransferSystem;
}
isActive() {
return this.#active && this.#fileTransferSystem !== null;
}
deactivate() {
this.#active = false;
this.#fileTransferSystem = null;
}
getSecurityLevel() {
return this.#securityLevel;
}
setSecurityLevel(level) {
if (['low', 'medium', 'high'].includes(level)) {
this.#securityLevel = level;
}
}
}
// ============================================
// SECURITY ERROR HANDLER
// ============================================
class SecurityErrorHandler {
static #allowedErrors = new Set([
'File size exceeds maximum limit',
'Unsupported file type',
'Transfer timeout',
'Connection lost',
'Invalid file data',
'File transfer failed',
'Transfer cancelled',
'Network error',
'File not found',
'Permission denied'
]);
static sanitizeError(error) {
const message = error.message || error;
for (const allowed of this.#allowedErrors) {
if (message.includes(allowed)) {
return allowed;
}
}
console.error('🔒 Internal file transfer error:', {
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
});
return 'File transfer failed';
}
static logSecurityEvent(event, details = {}) {
console.warn('🔒 Security event:', {
event,
timestamp: new Date().toISOString(),
...details
});
}
}
// ============================================
// FILE METADATA SIGNATURE SYSTEM
// ============================================
class FileMetadataSigner {
static async signFileMetadata(metadata, privateKey) {
try {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify({
fileId: metadata.fileId,
fileName: metadata.fileName,
fileSize: metadata.fileSize,
fileHash: metadata.fileHash,
timestamp: metadata.timestamp,
version: metadata.version || '2.0'
}));
const signature = await crypto.subtle.sign(
'RSASSA-PKCS1-v1_5',
privateKey,
data
);
return Array.from(new Uint8Array(signature));
} catch (error) {
SecurityErrorHandler.logSecurityEvent('signature_failed', { error: error.message });
throw new Error('Failed to sign file metadata');
}
}
static async verifyFileMetadata(metadata, signature, publicKey) {
try {
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify({
fileId: metadata.fileId,
fileName: metadata.fileName,
fileSize: metadata.fileSize,
fileHash: metadata.fileHash,
timestamp: metadata.timestamp,
version: metadata.version || '2.0'
}));
const signatureBuffer = new Uint8Array(signature);
const isValid = await crypto.subtle.verify(
'RSASSA-PKCS1-v1_5',
publicKey,
signatureBuffer,
data
);
if (!isValid) {
SecurityErrorHandler.logSecurityEvent('invalid_signature', { fileId: metadata.fileId });
}
return isValid;
} catch (error) {
SecurityErrorHandler.logSecurityEvent('verification_failed', { error: error.message });
return false;
}
}
}
// ============================================
// ТОЧНЫЕ ИСПРАВЛЕНИЯ БЕЗОПАСНОСТИ
// ============================================
class MessageSizeValidator {
static MAX_MESSAGE_SIZE = 1024 * 1024; // 1MB
static isMessageSizeValid(message) {
const messageString = JSON.stringify(message);
const sizeInBytes = new Blob([messageString]).size;
if (sizeInBytes > this.MAX_MESSAGE_SIZE) {
SecurityErrorHandler.logSecurityEvent('message_too_large', {
size: sizeInBytes,
limit: this.MAX_MESSAGE_SIZE
});
throw new Error('Message too large');
}
return true;
}
}
class AtomicOperations {
constructor() {
this.locks = new Map();
}
async withLock(key, operation) {
if (this.locks.has(key)) {
await this.locks.get(key);
}
const lockPromise = (async () => {
try {
return await operation();
} finally {
this.locks.delete(key);
}
})();
this.locks.set(key, lockPromise);
return lockPromise;
}
}
// Rate limiting для защиты от спама
class RateLimiter {
constructor(maxRequests, windowMs) {
this.maxRequests = maxRequests;
this.windowMs = windowMs;
this.requests = new Map();
}
isAllowed(identifier) {
const now = Date.now();
const windowStart = now - this.windowMs;
if (!this.requests.has(identifier)) {
this.requests.set(identifier, []);
}
const userRequests = this.requests.get(identifier);
const validRequests = userRequests.filter(time => time > windowStart);
this.requests.set(identifier, validRequests);
if (validRequests.length >= this.maxRequests) {
SecurityErrorHandler.logSecurityEvent('rate_limit_exceeded', {
identifier,
requestCount: validRequests.length,
limit: this.maxRequests
});
return false;
}
validRequests.push(now);
return true;
}
}
class SecureMemoryManager {
static secureWipe(buffer) {
if (buffer instanceof ArrayBuffer) {
const view = new Uint8Array(buffer);
crypto.getRandomValues(view);
} else if (buffer instanceof Uint8Array) {
crypto.getRandomValues(buffer);
}
}
static secureDelete(obj, prop) {
if (obj[prop]) {
this.secureWipe(obj[prop]);
delete obj[prop];
}
}
}
class EnhancedSecureFileTransfer {
constructor(webrtcManager, onProgress, onComplete, onError, onFileReceived, onIncomingFileRequest) {
this.webrtcManager = webrtcManager;
this.onProgress = onProgress;
this.onComplete = onComplete;
this.onError = onError;
this.onFileReceived = onFileReceived;
this.onIncomingFileRequest = onIncomingFileRequest;
// Validate webrtcManager
if (!webrtcManager) {
throw new Error('webrtcManager is required for EnhancedSecureFileTransfer');
}
SecureFileTransferContext.getInstance().setFileTransferSystem(this);
this.atomicOps = new AtomicOperations();
this.rateLimiter = new RateLimiter(10, 60000);
this.signingKey = null;
this.verificationKey = null;
// Transfer settings
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
this.RETRY_ATTEMPTS = 3;
this.FILE_TYPE_RESTRICTIONS = {
pdf: {
extensions: ['.pdf'],
mimeTypes: ['application/pdf'],
maxSize: 50 * 1024 * 1024,
category: 'PDF',
description: 'PDF'
},
text: {
extensions: ['.txt'],
mimeTypes: ['text/plain'],
maxSize: 10 * 1024 * 1024,
category: 'Plain text',
description: 'TXT'
},
images: {
extensions: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp', '.ico'],
mimeTypes: [
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'image/bmp',
'image/x-icon'
],
maxSize: 25 * 1024 * 1024, // 25 MB
category: 'Images',
description: 'JPG, JPEG, PNG, GIF, WEBP, BMP, ICO'
},
archives: {
extensions: ['.zip'],
mimeTypes: ['application/zip'],
maxSize: 100 * 1024 * 1024, // 100 MB
category: 'Archives',
description: 'ZIP'
}
};
this.BLOCKED_EXTENSIONS = new Set([
'.exe', '.bat', '.cmd', '.sh', '.js', '.msi', '.dmg', '.app',
'.jar', '.scr', '.ps1', '.vbs', '.html', '.svg'
]);
// Active transfers tracking
this.activeTransfers = new Map(); // fileId -> transfer state
this.receivingTransfers = new Map(); // fileId -> receiving state
this.pendingIncomingTransfers = new Map(); // fileId -> validated metadata awaiting consent
this.transferQueue = []; // Queue for pending transfers
this.pendingChunks = new Map();
this.incomingOfferLimiter = new RateLimiter(5, 60000);
this.incomingChunkLimiter = new RateLimiter(240, 60000);
this.incomingTransferChunkLimiters = new Map();
this.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE = 120;
this.MAX_PENDING_INCOMING_TRANSFERS = 3;
// Session key derivation
this.sessionKeys = new Map(); // fileId -> derived session key
// 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.MAX_RETAINED_RECEIVED_FILE_BUFFERS = 3;
this.setupFileMessageHandlers();
if (this.webrtcManager) {
this.webrtcManager.fileTransferSystem = this;
}
}
// ============================================
// FILE TYPE VALIDATION SYSTEM
// ============================================
getFileType(file) {
const fileName = String(file?.name || '').toLowerCase();
const extensionIndex = fileName.lastIndexOf('.');
const fileExtension = extensionIndex >= 0 ? fileName.substring(extensionIndex) : '';
const mimeType = String(file?.type || '').toLowerCase();
for (const [typeKey, typeConfig] of Object.entries(this.FILE_TYPE_RESTRICTIONS)) {
const extensionAllowed = typeConfig.extensions.includes(fileExtension);
const mimeAllowed = typeConfig.mimeTypes.includes(mimeType);
if (extensionAllowed && mimeAllowed) {
return {
type: typeKey,
category: typeConfig.category,
description: typeConfig.description,
maxSize: typeConfig.maxSize,
allowed: true
};
}
}
return {
type: 'blocked',
category: 'Unsupported',
description: 'Allowed: JPG, JPEG, PNG, GIF, WEBP, BMP, ICO, PDF, TXT, ZIP',
maxSize: this.MAX_FILE_SIZE,
allowed: false,
extension: fileExtension,
mimeType
};
}
validateFile(file) {
const fileType = this.getFileType(file);
const errors = [];
const fileName = String(file?.name || '');
const lowerName = fileName.toLowerCase();
const extensionIndex = lowerName.lastIndexOf('.');
const fileExtension = extensionIndex >= 0 ? lowerName.substring(extensionIndex) : '';
const mimeType = String(file?.type || '').toLowerCase();
if (this.BLOCKED_EXTENSIONS.has(fileExtension)) {
errors.push(`File rejected: ${fileExtension} files are not allowed for security reasons.`);
}
if (!mimeType) {
errors.push('File rejected: missing MIME type is unsafe.');
}
if (file.size > fileType.maxSize) {
errors.push(`File size (${this.formatFileSize(file.size)}) exceeds maximum allowed for ${fileType.category} (${this.formatFileSize(fileType.maxSize)})`);
}
if (!fileType.allowed) {
if (mimeType && !this.BLOCKED_EXTENSIONS.has(fileExtension)) {
errors.push(`File rejected: extension and MIME type must match an allowed type. Supported types: ${fileType.description}`);
}
}
if (file.size > this.MAX_FILE_SIZE) {
errors.push(`File size (${this.formatFileSize(file.size)}) exceeds general limit (${this.formatFileSize(this.MAX_FILE_SIZE)})`);
}
return {
isValid: errors.length === 0,
errors: errors,
fileType: fileType,
fileSize: file.size,
formattedSize: this.formatFileSize(file.size)
};
}
normalizeDisplayFileName(fileName) {
return String(fileName || '')
.normalize('NFKC')
.replace(/[\u0000-\u001F\u007F]/g, '')
.replace(/[\\/]+/g, '_')
.trim()
.slice(0, 255);
}
validateIncomingMetadata(metadata) {
const errors = [];
if (!metadata || typeof metadata !== 'object') errors.push('Invalid file transfer metadata');
if (!metadata?.fileId || typeof metadata.fileId !== 'string') errors.push('Invalid file id');
if (!Number.isSafeInteger(metadata?.fileSize) || metadata.fileSize <= 0) errors.push('Invalid file size');
if (!Number.isSafeInteger(metadata?.totalChunks) || metadata.totalChunks <= 0) errors.push('Invalid chunk count');
if (!Number.isSafeInteger(metadata?.chunkSize) || metadata.chunkSize <= 0 || metadata.chunkSize > this.CHUNK_SIZE) errors.push('Invalid chunk size');
if (!Array.isArray(metadata?.salt) || metadata.salt.length !== 32) errors.push('Invalid salt');
const rawName = typeof metadata?.fileName === 'string' ? metadata.fileName : '';
const displayName = this.normalizeDisplayFileName(rawName);
const hasDangerousName =
!rawName ||
rawName !== rawName.trim() ||
/[\u0000-\u001F\u007F]/.test(rawName) ||
/[\\/]/.test(rawName) ||
rawName === '.' ||
rawName === '..' ||
displayName.length === 0;
if (hasDangerousName) errors.push('Dangerous file name');
if (errors.length === 0) {
const validation = this.validateFile({
name: displayName,
size: metadata.fileSize,
type: metadata.fileType || 'application/octet-stream'
});
if (!validation.isValid) errors.push(...validation.errors);
}
return { isValid: errors.length === 0, errors, displayName };
}
formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
getSupportedFileTypes() {
const supportedTypes = {};
for (const [typeKey, typeConfig] of Object.entries(this.FILE_TYPE_RESTRICTIONS)) {
supportedTypes[typeKey] = {
category: typeConfig.category,
description: typeConfig.description,
extensions: typeConfig.extensions,
maxSize: this.formatFileSize(typeConfig.maxSize),
maxSizeBytes: typeConfig.maxSize
};
}
return supportedTypes;
}
getFileTypeInfo() {
return {
supportedTypes: this.getSupportedFileTypes(),
generalMaxSize: this.formatFileSize(this.MAX_FILE_SIZE),
generalMaxSizeBytes: this.MAX_FILE_SIZE,
restrictions: this.FILE_TYPE_RESTRICTIONS
};
}
// ============================================
// 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 (_) {}
}
setupFileMessageHandlers() {
if (!this.webrtcManager.dataChannel) {
const setupRetry = setInterval(() => {
if (this.webrtcManager.dataChannel) {
clearInterval(setupRetry);
this.setupMessageInterception();
}
}, 100);
setTimeout(() => {
clearInterval(setupRetry);
}, 5000);
return;
}
// Если dataChannel уже готов, сразу настраиваем
this.setupMessageInterception();
}
setupMessageInterception() {
try {
if (!this.webrtcManager.dataChannel) {
return;
}
if (this.webrtcManager) {
this.webrtcManager.fileTransferSystem = this;
}
if (this.webrtcManager.dataChannel.onmessage) {
this.originalOnMessage = this.webrtcManager.dataChannel.onmessage;
}
this.webrtcManager.dataChannel.onmessage = async (event) => {
try {
if (event.data.length > MessageSizeValidator.MAX_MESSAGE_SIZE) {
console.warn('🔒 Message too large, ignoring');
SecurityErrorHandler.logSecurityEvent('oversized_message_blocked');
return;
}
if (typeof event.data === 'string') {
try {
const parsed = JSON.parse(event.data);
MessageSizeValidator.isMessageSizeValid(parsed);
if (this.isFileTransferMessage(parsed)) {
await this.handleFileMessage(parsed);
return;
}
} catch (parseError) {
if (parseError.message === 'Message too large') {
return;
}
}
}
if (this.originalOnMessage) {
return this.originalOnMessage.call(this.webrtcManager.dataChannel, event);
}
} catch (error) {
console.error('❌ Error in file system message interception:', error);
if (this.originalOnMessage) {
return this.originalOnMessage.call(this.webrtcManager.dataChannel, event);
}
}
};
} catch (error) {
console.error('❌ Failed to set up message interception:', error);
}
}
isFileTransferMessage(message) {
if (!message || typeof message !== 'object' || !message.type) {
return false;
}
const fileMessageTypes = [
'file_transfer_start',
'file_transfer_response',
'file_chunk',
'chunk_confirmation',
'file_transfer_complete',
'file_transfer_error'
];
return fileMessageTypes.includes(message.type);
}
async handleFileMessage(message) {
try {
if (!this.webrtcManager.fileTransferSystem) {
try {
if (typeof this.webrtcManager.initializeFileTransfer === 'function') {
this.webrtcManager.initializeFileTransfer();
let attempts = 0;
const maxAttempts = 50;
while (!this.webrtcManager.fileTransferSystem && attempts < maxAttempts) {
await new Promise(resolve => setTimeout(resolve, 100));
attempts++;
}
if (!this.webrtcManager.fileTransferSystem) {
throw new Error('File transfer system initialization timeout');
}
} else {
throw new Error('initializeFileTransfer method not available');
}
} catch (initError) {
console.error('❌ Failed to initialize file transfer system:', initError);
if (message.fileId) {
const errorMessage = {
type: 'file_transfer_error',
fileId: message.fileId,
error: 'File transfer system not available',
timestamp: Date.now()
};
await this.sendSecureMessage(errorMessage);
}
return;
}
}
switch (message.type) {
case 'file_transfer_start':
await this.handleFileTransferStart(message);
break;
case 'file_transfer_response':
this.handleTransferResponse(message);
break;
case 'file_chunk':
await this.handleFileChunk(message);
break;
case 'chunk_confirmation':
this.handleChunkConfirmation(message);
break;
case 'file_transfer_complete':
this.handleTransferComplete(message);
break;
case 'file_transfer_error':
this.handleTransferError(message);
break;
default:
console.warn('⚠️ Unknown file message type:', message.type);
}
} catch (error) {
console.error('❌ Error handling file message:', error);
if (message.fileId) {
const errorMessage = {
type: 'file_transfer_error',
fileId: message.fileId,
error: error.message,
timestamp: Date.now()
};
await this.sendSecureMessage(errorMessage);
}
}
}
// ============================================
// SIMPLIFIED KEY DERIVATION - USE SHARED DATA
// ============================================
async deriveFileSessionKey(fileId) {
try {
if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
throw new Error('WebRTC session data not available');
}
const fileSalt = crypto.getRandomValues(new Uint8Array(32));
const encoder = new TextEncoder();
const fingerprintData = encoder.encode(this.webrtcManager.keyFingerprint);
const fileIdData = encoder.encode(fileId);
const sessionSaltArray = new Uint8Array(this.webrtcManager.sessionSalt);
const combinedSeed = new Uint8Array(
fingerprintData.length +
sessionSaltArray.length +
fileSalt.length +
fileIdData.length
);
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);
const fileSessionKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
);
this.sessionKeys.set(fileId, {
key: fileSessionKey,
salt: Array.from(fileSalt),
created: Date.now()
});
return { key: fileSessionKey, salt: Array.from(fileSalt) };
} catch (error) {
console.error('❌ Failed to derive file session key:', error);
throw error;
}
}
async deriveFileSessionKeyFromSalt(fileId, saltArray) {
try {
if (!saltArray || !Array.isArray(saltArray) || saltArray.length !== 32) {
throw new Error(`Invalid salt: ${saltArray?.length || 0} bytes`);
}
if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
throw new Error('WebRTC session data not available');
}
const encoder = new TextEncoder();
const fingerprintData = encoder.encode(this.webrtcManager.keyFingerprint);
const fileIdData = encoder.encode(fileId);
const fileSalt = new Uint8Array(saltArray);
const sessionSaltArray = new Uint8Array(this.webrtcManager.sessionSalt);
const combinedSeed = new Uint8Array(
fingerprintData.length +
sessionSaltArray.length +
fileSalt.length +
fileIdData.length
);
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);
const fileSessionKey = await crypto.subtle.importKey(
'raw',
keyMaterial,
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
);
this.sessionKeys.set(fileId, {
key: fileSessionKey,
salt: saltArray,
created: Date.now()
});
return fileSessionKey;
} catch (error) {
console.error('❌ Failed to derive session key from salt:', error);
throw error;
}
}
// ============================================
// FILE TRANSFER IMPLEMENTATION
// ============================================
async sendFile(file) {
try {
// Validate webrtcManager
if (!this.webrtcManager) {
throw new Error('WebRTC Manager not initialized');
}
const clientId = this.getClientIdentifier();
if (!this.rateLimiter.isAllowed(clientId)) {
SecurityErrorHandler.logSecurityEvent('rate_limit_exceeded', { clientId });
throw new Error('Rate limit exceeded. Please wait before sending another file.');
}
if (!file || !file.size) {
throw new Error('Invalid file object');
}
const validation = this.validateFile(file);
if (!validation.isValid) {
const errorMessage = validation.errors.join('. ');
throw new Error(errorMessage);
}
if (this.activeTransfers.size >= this.MAX_CONCURRENT_TRANSFERS) {
throw new Error('Maximum concurrent transfers reached');
}
// Generate unique file ID
const fileId = `file_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// Calculate file hash for integrity verification
const fileHash = await this.calculateFileHash(file);
// Derive session key for this file
const keyResult = await this.deriveFileSessionKey(fileId);
const sessionKey = keyResult.key;
const salt = keyResult.salt;
// Create transfer state
const transferState = {
fileId: fileId,
file: file,
fileHash: fileHash,
sessionKey: sessionKey,
salt: salt,
totalChunks: Math.ceil(file.size / this.CHUNK_SIZE),
sentChunks: 0,
confirmedChunks: 0,
startTime: Date.now(),
status: 'preparing',
retryCount: 0,
lastChunkTime: Date.now()
};
this.activeTransfers.set(fileId, transferState);
this.transferNonces.set(fileId, 0);
const consentPromise = new Promise((resolve, reject) => {
transferState.resolveConsent = resolve;
transferState.rejectConsent = reject;
transferState.consentTimeout = setTimeout(() => {
transferState.consentTimeout = null;
reject(new Error('Transfer timeout'));
}, 30000);
});
// Send file metadata first
await this.sendFileMetadata(transferState);
// Wait for explicit receiver consent before any chunks are sent.
await consentPromise;
await this.startChunkTransmission(transferState);
return fileId;
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ File sending failed:', safeError);
if (this.onError) this.onError(safeError);
throw new Error(safeError);
}
}
async sendFileMetadata(transferState) {
try {
const metadata = {
type: 'file_transfer_start',
fileId: transferState.fileId,
fileName: transferState.file.name,
fileSize: transferState.file.size,
fileType: transferState.file.type || 'application/octet-stream',
fileHash: transferState.fileHash,
totalChunks: transferState.totalChunks,
chunkSize: this.CHUNK_SIZE,
salt: transferState.salt,
timestamp: Date.now(),
version: '2.0'
};
if (this.signingKey) {
try {
metadata.signature = await FileMetadataSigner.signFileMetadata(metadata, this.signingKey);
console.log('🔒 File metadata signed successfully');
} catch (signError) {
SecurityErrorHandler.logSecurityEvent('signature_failed', {
fileId: transferState.fileId,
error: signError.message
});
}
}
// Send metadata through secure channel
await this.sendSecureMessage(metadata);
transferState.status = 'metadata_sent';
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Failed to send file metadata:', safeError);
transferState.status = 'failed';
throw new Error(safeError);
}
}
async startChunkTransmission(transferState) {
try {
transferState.status = 'transmitting';
const file = transferState.file;
const totalChunks = transferState.totalChunks;
for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
const start = chunkIndex * this.CHUNK_SIZE;
const end = Math.min(start + this.CHUNK_SIZE, file.size);
// Read chunk from file
const chunkData = await this.readFileChunk(file, start, end);
// Send chunk (с учётом backpressure)
await this.sendFileChunk(transferState, chunkIndex, chunkData);
// Update progress
transferState.sentChunks++;
const progress = Math.round((transferState.sentChunks / totalChunks) * 95) + 5; // 5-100%
await this.waitForBackpressure();
}
transferState.status = 'waiting_confirmation';
// Timeout for completion confirmation
setTimeout(() => {
if (this.activeTransfers.has(transferState.fileId)) {
const state = this.activeTransfers.get(transferState.fileId);
if (state.status === 'waiting_confirmation') {
this.cleanupTransfer(transferState.fileId);
}
}
}, 30000);
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Chunk transmission failed:', safeError);
transferState.status = 'failed';
throw new Error(safeError);
}
}
async readFileChunk(file, start, end) {
try {
const blob = file.slice(start, end);
return await blob.arrayBuffer();
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Failed to read file chunk:', safeError);
throw new Error(safeError);
}
}
async sendFileChunk(transferState, chunkIndex, chunkData) {
try {
const sessionKey = transferState.sessionKey;
const nonce = crypto.getRandomValues(new Uint8Array(12));
// Encrypt chunk data
const encryptedChunk = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: nonce
},
sessionKey,
chunkData
);
// 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),
encryptedDataB64: encryptedB64,
chunkSize: chunkData.byteLength,
timestamp: Date.now()
};
await this.waitForBackpressure();
// Send chunk through secure channel
await this.sendSecureMessage(chunkMessage);
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Failed to send file chunk:', safeError);
throw new Error(safeError);
}
}
async sendSecureMessage(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;
}
}
}
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;
}
const softLimit = 4 * 1024 * 1024;
while (dc.bufferedAmount > softLimit) {
await new Promise(r => setTimeout(r, 20));
}
} catch (_) {
// ignore
}
}
async calculateFileHash(file) {
try {
const arrayBuffer = await file.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) {
console.error('❌ File hash calculation failed:', error);
throw error;
}
}
// ============================================
// MESSAGE HANDLERS
// ============================================
async handleFileTransferStart(metadata) {
try {
const clientId = this.getClientIdentifier();
if (!this.incomingOfferLimiter.isAllowed(clientId)) {
throw new Error('Incoming file request rate limit exceeded');
}
const validation = this.validateIncomingMetadata(metadata);
if (!validation.isValid) throw new Error(validation.errors.join('. '));
if (metadata.signature && this.verificationKey) {
try {
const isValid = await FileMetadataSigner.verifyFileMetadata(
metadata,
metadata.signature,
this.verificationKey
);
if (!isValid) {
SecurityErrorHandler.logSecurityEvent('invalid_metadata_signature', {
fileId: metadata.fileId
});
throw new Error('Invalid file metadata signature');
}
console.log('🔒 File metadata signature verified successfully');
} catch (verifyError) {
SecurityErrorHandler.logSecurityEvent('verification_failed', {
fileId: metadata.fileId,
error: verifyError.message
});
throw new Error('File metadata verification failed');
}
}
// Check if we already have this transfer
if (this.receivingTransfers.has(metadata.fileId) || this.pendingIncomingTransfers.has(metadata.fileId)) {
return;
}
if (this.pendingIncomingTransfers.size >= this.MAX_PENDING_INCOMING_TRANSFERS) {
throw new Error('Too many pending incoming file requests');
}
const pendingMetadata = {
...metadata,
fileName: validation.displayName,
receivedAt: Date.now()
};
this.pendingIncomingTransfers.set(metadata.fileId, pendingMetadata);
if (typeof this.onIncomingFileRequest === 'function') {
this.onIncomingFileRequest({
fileId: pendingMetadata.fileId,
fileName: pendingMetadata.fileName,
fileSize: pendingMetadata.fileSize,
mimeType: pendingMetadata.fileType || 'application/octet-stream'
});
} else {
await this.rejectIncomingFile(metadata.fileId, 'User consent unavailable');
}
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Failed to handle file transfer start:', safeError);
// Send error response
const errorResponse = {
type: 'file_transfer_response',
fileId: metadata.fileId,
accepted: false,
error: safeError,
timestamp: Date.now()
};
await this.sendSecureMessage(errorResponse);
}
}
async handleFileChunk(chunkMessage) {
return this.atomicOps.withLock(
`chunk-${chunkMessage.fileId}`,
async () => {
try {
let receivingState = this.receivingTransfers.get(chunkMessage.fileId);
// Never buffer chunks before explicit consent.
if (!receivingState) {
return;
}
if (!this._isIncomingChunkAllowed(chunkMessage.fileId)) {
console.warn('⚠️ Incoming file chunk rate limit exceeded; cleaning up transfer:', chunkMessage.fileId);
this.cleanupReceivingTransfer(chunkMessage.fileId);
return;
}
// Update last chunk time
receivingState.lastChunkTime = Date.now();
// Check if chunk already received
if (receivingState.receivedChunks.has(chunkMessage.chunkIndex)) {
return;
}
// Validate chunk
if (chunkMessage.chunkIndex < 0 || chunkMessage.chunkIndex >= receivingState.totalChunks) {
throw new Error(`Invalid chunk index: ${chunkMessage.chunkIndex}`);
}
// Decrypt chunk
const nonce = new Uint8Array(chunkMessage.nonce);
// 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');
}
const decryptedChunk = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: nonce
},
receivingState.sessionKey,
encryptedData
);
// Verify chunk size
if (decryptedChunk.byteLength !== chunkMessage.chunkSize) {
throw new Error(`Chunk size mismatch: expected ${chunkMessage.chunkSize}, got ${decryptedChunk.byteLength}`);
}
// Store chunk
receivingState.receivedChunks.set(chunkMessage.chunkIndex, decryptedChunk);
receivingState.receivedCount++;
// Send chunk confirmation
const confirmation = {
type: 'chunk_confirmation',
fileId: chunkMessage.fileId,
chunkIndex: chunkMessage.chunkIndex,
timestamp: Date.now()
};
await this.sendSecureMessage(confirmation);
// Check if all chunks received
if (receivingState.receivedCount === receivingState.totalChunks) {
await this.assembleFile(receivingState);
}
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Failed to handle file chunk:', safeError);
// Send error notification
const errorMessage = {
type: 'file_transfer_error',
fileId: chunkMessage.fileId,
error: safeError,
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: ${safeError}`);
}
}
}
);
}
_isIncomingChunkAllowed(fileId) {
const clientId = this.getClientIdentifier();
if (!this.incomingChunkLimiter.isAllowed(clientId)) {
SecurityErrorHandler.logSecurityEvent('incoming_chunk_aggregate_rate_limit_exceeded', {
clientId,
fileId
});
return false;
}
if (!this.incomingTransferChunkLimiters.has(fileId)) {
this.incomingTransferChunkLimiters.set(
fileId,
new RateLimiter(this.MAX_INCOMING_CHUNKS_PER_TRANSFER_PER_MINUTE, 60000)
);
}
const transferLimiter = this.incomingTransferChunkLimiters.get(fileId);
if (!transferLimiter.isAllowed(fileId)) {
SecurityErrorHandler.logSecurityEvent('incoming_chunk_transfer_rate_limit_exceeded', {
clientId,
fileId
});
return false;
}
return true;
}
async assembleFile(receivingState) {
try {
receivingState.status = 'assembling';
// Verify we have all chunks
for (let i = 0; i < receivingState.totalChunks; i++) {
if (!receivingState.receivedChunks.has(i)) {
throw new Error(`Missing chunk ${i}`);
}
}
// Combine all chunks in order
const chunks = [];
for (let i = 0; i < receivingState.totalChunks; i++) {
const chunk = receivingState.receivedChunks.get(i);
chunks.push(new Uint8Array(chunk));
}
// Calculate total size
const totalSize = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
// Verify total size matches expected
if (totalSize !== receivingState.fileSize) {
throw new Error(`File size mismatch: expected ${receivingState.fileSize}, got ${totalSize}`);
}
// Combine into single array
const fileData = new Uint8Array(totalSize);
let offset = 0;
for (const chunk of chunks) {
fileData.set(chunk, offset);
offset += chunk.length;
}
// Verify file integrity
const receivedHash = await this.calculateFileHashFromData(fileData);
if (receivedHash !== receivingState.fileHash) {
throw new Error('File integrity check failed - hash mismatch');
}
const fileBuffer = fileData.buffer;
const fileBlob = new Blob([fileBuffer], { type: receivingState.fileType });
receivingState.endTime = Date.now();
receivingState.status = 'completed';
this._storeReceivedFileBuffer(receivingState.fileId, {
buffer: fileBuffer,
type: receivingState.fileType,
name: receivingState.fileName,
size: receivingState.fileSize
});
if (this.onFileReceived) {
const getBlob = async () => {
const blob = await this.getBlob(receivingState.fileId);
if (!blob) {
throw new Error('This file is no longer available for download.');
}
return blob;
};
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,
mimeType: receivingState.fileType,
transferTime: receivingState.endTime - receivingState.startTime,
// backward-compatibility for existing UIs
fileBlob,
getBlob,
getObjectURL,
revokeObjectURL
});
}
// Send completion confirmation
const completionMessage = {
type: 'file_transfer_complete',
fileId: receivingState.fileId,
success: true,
timestamp: Date.now()
};
await this.sendSecureMessage(completionMessage);
// Cleanup
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);
} catch (error) {
console.error('❌ File assembly failed:', error);
receivingState.status = 'failed';
if (this.onError) {
this.onError(`File assembly failed: ${error.message}`);
}
// Send error notification
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);
}
}
async calculateFileHashFromData(data) {
try {
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) {
console.error('❌ Hash calculation failed:', error);
throw error;
}
}
handleTransferResponse(response) {
try {
const transferState = this.activeTransfers.get(response.fileId);
if (!transferState) {
return;
}
if (response.accepted) {
transferState.status = 'accepted';
if (transferState.consentTimeout) clearTimeout(transferState.consentTimeout);
transferState.consentTimeout = null;
transferState.resolveConsent?.();
transferState.resolveConsent = null;
transferState.rejectConsent = null;
} else {
transferState.status = 'rejected';
if (transferState.consentTimeout) clearTimeout(transferState.consentTimeout);
transferState.consentTimeout = null;
transferState.rejectConsent?.(new Error(response.error || 'Transfer rejected'));
transferState.rejectConsent = null;
transferState.resolveConsent = null;
if (this.onError) {
this.onError(`Transfer rejected: ${response.error || 'Unknown reason'}`);
}
this.cleanupTransfer(response.fileId);
}
} catch (error) {
console.error('❌ Failed to handle transfer response:', error);
}
}
handleChunkConfirmation(confirmation) {
try {
const transferState = this.activeTransfers.get(confirmation.fileId);
if (!transferState) {
return;
}
transferState.confirmedChunks++;
transferState.lastChunkTime = Date.now();
} catch (error) {
console.error('❌ Failed to handle chunk confirmation:', error);
}
}
handleTransferComplete(completion) {
try {
const transferState = this.activeTransfers.get(completion.fileId);
if (!transferState) {
return;
}
if (completion.success) {
transferState.status = 'completed';
transferState.endTime = Date.now();
if (this.onComplete) {
this.onComplete({
fileId: transferState.fileId,
fileName: transferState.file.name,
fileSize: transferState.file.size,
transferTime: transferState.endTime - transferState.startTime,
status: 'completed'
});
}
} else {
transferState.status = 'failed';
if (this.onError) {
this.onError(`Transfer failed: ${completion.error || 'Unknown error'}`);
}
}
this.cleanupTransfer(completion.fileId);
} catch (error) {
console.error('❌ Failed to handle transfer completion:', error);
}
}
handleTransferError(errorMessage) {
try {
const transferState = this.activeTransfers.get(errorMessage.fileId);
if (transferState) {
transferState.status = 'failed';
this.cleanupTransfer(errorMessage.fileId);
}
const receivingState = this.receivingTransfers.get(errorMessage.fileId);
if (receivingState) {
receivingState.status = 'failed';
this.cleanupReceivingTransfer(errorMessage.fileId);
}
if (this.onError) {
this.onError(`Transfer error: ${errorMessage.error || 'Unknown error'}`);
}
} catch (error) {
console.error('❌ Failed to handle transfer error:', error);
}
}
// ============================================
// UTILITY METHODS
// ============================================
getActiveTransfers() {
return Array.from(this.activeTransfers.values()).map(transfer => ({
fileId: transfer.fileId,
fileName: transfer.file?.name || 'Unknown',
fileSize: transfer.file?.size || 0,
progress: Math.round((transfer.sentChunks / transfer.totalChunks) * 100),
status: transfer.status,
startTime: transfer.startTime
}));
}
getReceivingTransfers() {
return Array.from(this.receivingTransfers.values()).map(transfer => ({
fileId: transfer.fileId,
fileName: transfer.fileName || 'Unknown',
fileSize: transfer.fileSize || 0,
progress: Math.round((transfer.receivedCount / transfer.totalChunks) * 100),
status: transfer.status,
startTime: transfer.startTime
}));
}
getPendingIncomingTransfers() {
return Array.from(this.pendingIncomingTransfers.values()).map(transfer => ({
fileId: transfer.fileId,
fileName: transfer.fileName,
fileSize: transfer.fileSize,
mimeType: transfer.fileType || 'application/octet-stream',
receivedAt: transfer.receivedAt
}));
}
async acceptIncomingFile(fileId) {
const metadata = this.pendingIncomingTransfers.get(fileId);
if (!metadata) return false;
const sessionKey = await this.deriveFileSessionKeyFromSalt(fileId, metadata.salt);
this.receivingTransfers.set(fileId, {
fileId,
fileName: metadata.fileName,
fileSize: metadata.fileSize,
fileType: metadata.fileType || 'application/octet-stream',
fileHash: metadata.fileHash,
totalChunks: metadata.totalChunks,
chunkSize: metadata.chunkSize || this.CHUNK_SIZE,
sessionKey,
salt: metadata.salt,
receivedChunks: new Map(),
receivedCount: 0,
startTime: Date.now(),
lastChunkTime: Date.now(),
status: 'receiving'
});
this.pendingIncomingTransfers.delete(fileId);
await this.sendSecureMessage({ type: 'file_transfer_response', fileId, accepted: true, timestamp: Date.now() });
return true;
}
async rejectIncomingFile(fileId, error = 'Rejected by user') {
if (!this.pendingIncomingTransfers.has(fileId)) return false;
this.pendingIncomingTransfers.delete(fileId);
await this.sendSecureMessage({ type: 'file_transfer_response', fileId, accepted: false, error, timestamp: Date.now() });
return true;
}
cancelTransfer(fileId) {
try {
if (this.activeTransfers.has(fileId)) {
this.cleanupTransfer(fileId);
return true;
}
if (this.receivingTransfers.has(fileId)) {
this.cleanupReceivingTransfer(fileId);
return true;
}
return false;
} catch (error) {
console.error('❌ Failed to cancel transfer:', error);
return false;
}
}
cleanupTransfer(fileId) {
const transferState = this.activeTransfers.get(fileId);
if (transferState) {
if (transferState.consentTimeout) {
clearTimeout(transferState.consentTimeout);
transferState.consentTimeout = null;
}
if (transferState.rejectConsent) {
transferState.rejectConsent(new Error('Transfer cancelled during cleanup or disconnect'));
transferState.rejectConsent = null;
transferState.resolveConsent = null;
}
}
this.activeTransfers.delete(fileId);
this.sessionKeys.delete(fileId);
this.transferNonces.delete(fileId);
this.incomingTransferChunkLimiters.delete(fileId);
// Remove processed chunk IDs for this transfer
for (const chunkId of this.processedChunks) {
if (chunkId.startsWith(fileId)) {
this.processedChunks.delete(chunkId);
}
}
}
_storeReceivedFileBuffer(fileId, entry) {
this.receivedFileBuffers.set(fileId, entry);
while (this.receivedFileBuffers.size > this.MAX_RETAINED_RECEIVED_FILE_BUFFERS) {
const oldestFileId = this.receivedFileBuffers.keys().next().value;
this._discardReceivedFileBuffer(oldestFileId);
}
}
_discardReceivedFileBuffer(fileId) {
const fileBuffer = this.receivedFileBuffers.get(fileId);
if (!fileBuffer) return;
try {
if (fileBuffer.buffer) {
SecureMemoryManager.secureWipe(fileBuffer.buffer);
new Uint8Array(fileBuffer.buffer).fill(0);
}
} catch (_) {
// Best-effort wipe; deletion must still proceed.
}
this.receivedFileBuffers.delete(fileId);
}
// ✅ УЛУЧШЕННАЯ безопасная очистка памяти для предотвращения use-after-free
cleanupReceivingTransfer(fileId) {
try {
// Безопасно очищаем pending chunks
this.pendingChunks.delete(fileId);
const receivingState = this.receivingTransfers.get(fileId);
if (receivingState) {
// ✅ БЕЗОПАСНАЯ очистка receivedChunks с дополнительной защитой
if (receivingState.receivedChunks && receivingState.receivedChunks.size > 0) {
for (const [index, chunk] of receivingState.receivedChunks) {
try {
// Дополнительная проверка на валидность chunk
if (chunk && (chunk instanceof ArrayBuffer || chunk instanceof Uint8Array)) {
SecureMemoryManager.secureWipe(chunk);
// Дополнительная очистка - заполняем нулями перед удалением
if (chunk instanceof ArrayBuffer) {
const view = new Uint8Array(chunk);
view.fill(0);
} else if (chunk instanceof Uint8Array) {
chunk.fill(0);
}
}
} catch (chunkError) {
console.warn('⚠️ Failed to securely wipe chunk:', chunkError);
}
}
receivingState.receivedChunks.clear();
}
// ✅ БЕЗОПАСНАЯ очистка session key
if (receivingState.sessionKey) {
try {
// Для CryptoKey нельзя безопасно очистить, но можем удалить ссылку
receivingState.sessionKey = null;
} catch (keyError) {
console.warn('⚠️ Failed to clear session key:', keyError);
}
}
// ✅ БЕЗОПАСНАЯ очистка других чувствительных данных
if (receivingState.salt) {
try {
if (Array.isArray(receivingState.salt)) {
receivingState.salt.fill(0);
}
receivingState.salt = null;
} catch (saltError) {
console.warn('⚠️ Failed to clear salt:', saltError);
}
}
// Очищаем все свойства receivingState
for (const [key, value] of Object.entries(receivingState)) {
if (value && typeof value === 'object') {
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
SecureMemoryManager.secureWipe(value);
} else if (Array.isArray(value)) {
value.fill(0);
}
receivingState[key] = null;
}
}
}
// Удаляем из основных коллекций
this.receivingTransfers.delete(fileId);
this.sessionKeys.delete(fileId);
this.incomingTransferChunkLimiters.delete(fileId);
// ✅ БЕЗОПАСНАЯ очистка финального буфера файла
const fileBuffer = this.receivedFileBuffers.get(fileId);
if (fileBuffer) {
try {
if (fileBuffer.buffer) {
SecureMemoryManager.secureWipe(fileBuffer.buffer);
// Дополнительная очистка - заполняем нулями
const view = new Uint8Array(fileBuffer.buffer);
view.fill(0);
}
// Очищаем все свойства fileBuffer
for (const [key, value] of Object.entries(fileBuffer)) {
if (value && typeof value === 'object') {
if (value instanceof ArrayBuffer || value instanceof Uint8Array) {
SecureMemoryManager.secureWipe(value);
}
fileBuffer[key] = null;
}
}
this.receivedFileBuffers.delete(fileId);
} catch (bufferError) {
console.warn('⚠️ Failed to securely clear file buffer:', bufferError);
// Принудительно удаляем даже при ошибке
this.receivedFileBuffers.delete(fileId);
}
}
// ✅ БЕЗОПАСНАЯ очистка processed chunks
const chunksToRemove = [];
for (const chunkId of this.processedChunks) {
if (chunkId.startsWith(fileId)) {
chunksToRemove.push(chunkId);
}
}
// Удаляем в отдельном цикле для избежания изменения коллекции во время итерации
for (const chunkId of chunksToRemove) {
this.processedChunks.delete(chunkId);
}
// Принудительная очистка памяти
if (typeof global !== 'undefined' && global.gc) {
try {
global.gc();
} catch (gcError) {
// Игнорируем ошибки GC
}
}
console.log(`🔒 Memory safely cleaned for file transfer: ${fileId}`);
} catch (error) {
console.error('❌ Error during secure memory cleanup:', error);
// Принудительная очистка даже при ошибке
this.receivingTransfers.delete(fileId);
this.sessionKeys.delete(fileId);
this.receivedFileBuffers.delete(fileId);
this.pendingChunks.delete(fileId);
throw new Error(`Memory cleanup failed: ${error.message}`);
}
}
getTransferStatus(fileId) {
if (this.activeTransfers.has(fileId)) {
const transfer = this.activeTransfers.get(fileId);
return {
type: 'sending',
fileId: transfer.fileId,
fileName: transfer.file.name,
progress: Math.round((transfer.sentChunks / transfer.totalChunks) * 100),
status: transfer.status,
startTime: transfer.startTime
};
}
if (this.receivingTransfers.has(fileId)) {
const transfer = this.receivingTransfers.get(fileId);
return {
type: 'receiving',
fileId: transfer.fileId,
fileName: transfer.fileName,
progress: Math.round((transfer.receivedCount / transfer.totalChunks) * 100),
status: transfer.status,
startTime: transfer.startTime
};
}
return null;
}
getSystemStatus() {
return {
initialized: true,
activeTransfers: this.activeTransfers.size,
receivingTransfers: this.receivingTransfers.size,
totalTransfers: this.activeTransfers.size + this.receivingTransfers.size,
maxConcurrentTransfers: this.MAX_CONCURRENT_TRANSFERS,
maxFileSize: this.MAX_FILE_SIZE,
chunkSize: this.CHUNK_SIZE,
hasWebrtcManager: !!this.webrtcManager,
isConnected: this.webrtcManager?.isConnected?.() || false,
hasDataChannel: !!this.webrtcManager?.dataChannel,
dataChannelState: this.webrtcManager?.dataChannel?.readyState,
isVerified: this.webrtcManager?.isVerified,
hasEncryptionKey: !!this.webrtcManager?.encryptionKey,
hasMacKey: !!this.webrtcManager?.macKey,
linkedToWebRTCManager: this.webrtcManager?.fileTransferSystem === this,
supportedFileTypes: this.getSupportedFileTypes(),
fileTypeInfo: this.getFileTypeInfo()
};
}
cleanup() {
SecureFileTransferContext.getInstance().deactivate();
if (this.webrtcManager && this.webrtcManager.dataChannel && this.originalOnMessage) {
this.webrtcManager.dataChannel.onmessage = this.originalOnMessage;
this.originalOnMessage = null;
}
if (this.webrtcManager && this.originalProcessMessage) {
this.webrtcManager.processMessage = this.originalProcessMessage;
this.originalProcessMessage = null;
}
if (this.webrtcManager && this.originalRemoveSecurityLayers) {
this.webrtcManager.removeSecurityLayers = this.originalRemoveSecurityLayers;
this.originalRemoveSecurityLayers = null;
}
// Cleanup all active transfers with secure memory wiping
for (const fileId of this.activeTransfers.keys()) {
this.cleanupTransfer(fileId);
}
for (const fileId of this.receivingTransfers.keys()) {
this.cleanupReceivingTransfer(fileId);
}
if (this.atomicOps) {
this.atomicOps.locks.clear();
}
if (this.rateLimiter) {
this.rateLimiter.requests.clear();
}
if (this.incomingChunkLimiter) {
this.incomingChunkLimiter.requests.clear();
}
this.incomingTransferChunkLimiters.clear();
// Clear all state
this.pendingChunks.clear();
this.pendingIncomingTransfers.clear();
this.activeTransfers.clear();
this.receivingTransfers.clear();
this.transferQueue.length = 0;
this.sessionKeys.clear();
this.transferNonces.clear();
this.processedChunks.clear();
for (const fileId of Array.from(this.receivedFileBuffers.keys())) {
this._discardReceivedFileBuffer(fileId);
}
this.clearKeys();
}
// ============================================
// SESSION UPDATE HANDLER - FIXED
// ============================================
onSessionUpdate(sessionData) {
// Clear session keys cache for resync
this.sessionKeys.clear();
}
// ============================================
// DEBUGGING AND DIAGNOSTICS
// ============================================
diagnoseFileTransferIssue() {
const diagnosis = {
timestamp: new Date().toISOString(),
fileTransferSystem: {
initialized: !!this,
hasWebrtcManager: !!this.webrtcManager,
webrtcManagerType: this.webrtcManager?.constructor?.name,
linkedToWebRTCManager: this.webrtcManager?.fileTransferSystem === this
},
webrtcManager: {
hasDataChannel: !!this.webrtcManager?.dataChannel,
dataChannelState: this.webrtcManager?.dataChannel?.readyState,
isConnected: this.webrtcManager?.isConnected?.() || false,
isVerified: this.webrtcManager?.isVerified,
hasEncryptionKey: !!this.webrtcManager?.encryptionKey,
hasMacKey: !!this.webrtcManager?.macKey,
hasKeyFingerprint: !!this.webrtcManager?.keyFingerprint,
hasSessionSalt: !!this.webrtcManager?.sessionSalt
},
securityContext: {
contextActive: SecureFileTransferContext.getInstance().isActive(),
securityLevel: SecureFileTransferContext.getInstance().getSecurityLevel(),
hasAtomicOps: !!this.atomicOps,
hasRateLimiter: !!this.rateLimiter
},
transfers: {
activeTransfers: this.activeTransfers.size,
receivingTransfers: this.receivingTransfers.size,
pendingChunks: this.pendingChunks.size,
sessionKeys: this.sessionKeys.size
},
fileTypeSupport: {
supportedTypes: this.getSupportedFileTypes(),
generalMaxSize: this.formatFileSize(this.MAX_FILE_SIZE),
restrictions: Object.keys(this.FILE_TYPE_RESTRICTIONS)
}
};
return diagnosis;
}
async debugKeyDerivation(fileId) {
try {
if (!this.webrtcManager.keyFingerprint || !this.webrtcManager.sessionSalt) {
throw new Error('Session data not available');
}
// Test sender derivation
const senderResult = await this.deriveFileSessionKey(fileId);
// Test receiver derivation with same salt
const receiverKey = await this.deriveFileSessionKeyFromSalt(fileId, senderResult.salt);
// Test encryption/decryption
const testData = new TextEncoder().encode('test data');
const nonce = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
senderResult.key,
testData
);
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: nonce },
receiverKey,
encrypted
);
const decryptedText = new TextDecoder().decode(decrypted);
if (decryptedText === 'test data') {
return { success: true, message: 'All tests passed' };
} else {
throw new Error('Decryption verification failed');
}
} catch (error) {
console.error('❌ Key derivation test failed:', error);
return { success: false, error: error.message };
}
}
// ============================================
// ALTERNATIVE METHOD OF INITIALIZING HANDLERS
// ============================================
registerWithWebRTCManager() {
if (!this.webrtcManager) {
throw new Error('WebRTC manager not available');
}
this.webrtcManager.fileTransferSystem = this;
this.webrtcManager.setFileMessageHandler = (handler) => {
this.webrtcManager._fileMessageHandler = handler;
};
this.webrtcManager.setFileMessageHandler((message) => {
return this.handleFileMessage(message);
});
}
static createFileMessageFilter(fileTransferSystem) {
return async (event) => {
try {
if (typeof event.data === 'string') {
const parsed = JSON.parse(event.data);
if (fileTransferSystem.isFileTransferMessage(parsed)) {
await fileTransferSystem.handleFileMessage(parsed);
return true;
}
}
} catch (error) {
}
return false;
};
}
// ============================================
// SECURITY KEY MANAGEMENT
// ============================================
setSigningKey(privateKey) {
if (!privateKey || !(privateKey instanceof CryptoKey)) {
throw new Error('Invalid private key for signing');
}
this.signingKey = privateKey;
console.log('🔒 Signing key set successfully');
}
setVerificationKey(publicKey) {
if (!publicKey || !(publicKey instanceof CryptoKey)) {
throw new Error('Invalid public key for verification');
}
this.verificationKey = publicKey;
console.log('🔒 Verification key set successfully');
}
async generateSigningKeyPair() {
try {
const keyPair = await crypto.subtle.generateKey(
{
name: 'RSASSA-PKCS1-v1_5',
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: 'SHA-256'
},
true, // extractable
['sign', 'verify']
);
this.signingKey = keyPair.privateKey;
this.verificationKey = keyPair.publicKey;
console.log('🔒 RSA key pair generated successfully');
return keyPair;
} catch (error) {
const safeError = SecurityErrorHandler.sanitizeError(error);
console.error('❌ Failed to generate signing key pair:', safeError);
throw new Error(safeError);
}
}
clearKeys() {
this.signingKey = null;
this.verificationKey = null;
console.log('🔒 Security keys cleared');
}
getSecurityStatus() {
return {
signingEnabled: this.signingKey !== null,
verificationEnabled: this.verificationKey !== null,
contextActive: SecureFileTransferContext.getInstance().isActive(),
securityLevel: SecureFileTransferContext.getInstance().getSecurityLevel()
};
}
getClientIdentifier() {
return this.webrtcManager?.connectionId ||
this.webrtcManager?.keyFingerprint?.substring(0, 16) ||
'default-client';
}
destroy() {
SecureFileTransferContext.getInstance().deactivate();
this.clearKeys();
console.log('🔒 File transfer system destroyed safely');
}
}
export { EnhancedSecureFileTransfer };