Files
securebit-chat/src/transfer/EnhancedSecureFileTransfer.js
T

2176 lines
81 KiB
JavaScript
Raw Normal View History

// ============================================
// 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);
2026-05-17 23:05:43 -04:00
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;
}
2026-05-17 23:05:43 -04:00
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}`);
}
}
}
);
}
2026-05-17 23:05:43 -04:00
_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);
2026-05-17 23:05:43 -04:00
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);
}
2025-08-24 16:30:06 -04:00
// ✅ УЛУЧШЕННАЯ безопасная очистка памяти для предотвращения use-after-free
cleanupReceivingTransfer(fileId) {
2025-08-24 16:30:06 -04:00
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;
}
}
}
2025-08-24 16:30:06 -04:00
// Удаляем из основных коллекций
this.receivingTransfers.delete(fileId);
this.sessionKeys.delete(fileId);
2026-05-17 23:05:43 -04:00
this.incomingTransferChunkLimiters.delete(fileId);
2025-08-24 16:30:06 -04:00
// ✅ БЕЗОПАСНАЯ очистка финального буфера файла
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);
}
}
2025-08-24 16:30:06 -04:00
// ✅ БЕЗОПАСНАЯ очистка 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);
}
2025-08-24 16:30:06 -04:00
// Принудительная очистка памяти
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();
}
2026-05-17 23:05:43 -04:00
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 };