2025-08-24 16:04:49 -04:00
// ============================================
// 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 ] ;
}
}
}
2025-08-18 21:45:50 -04:00
class EnhancedSecureFileTransfer {
2026-05-17 14:48:52 -04:00
constructor ( webrtcManager , onProgress , onComplete , onError , onFileReceived , onIncomingFileRequest ) {
2025-08-18 21:45:50 -04:00
this . webrtcManager = webrtcManager ;
this . onProgress = onProgress ;
this . onComplete = onComplete ;
this . onError = onError ;
this . onFileReceived = onFileReceived ;
2026-05-17 14:48:52 -04:00
this . onIncomingFileRequest = onIncomingFileRequest ;
2025-08-18 21:45:50 -04:00
// Validate webrtcManager
if ( ! webrtcManager ) {
throw new Error ( 'webrtcManager is required for EnhancedSecureFileTransfer' ) ;
}
2025-08-24 16:04:49 -04:00
SecureFileTransferContext . getInstance ( ) . setFileTransferSystem ( this ) ;
2025-08-20 18:19:42 -04:00
2025-08-24 16:04:49 -04:00
this . atomicOps = new AtomicOperations ( ) ;
this . rateLimiter = new RateLimiter ( 10 , 60000 ) ;
2025-08-21 17:40:17 -04:00
2025-08-24 16:04:49 -04:00
this . signingKey = null ;
this . verificationKey = null ;
2025-08-18 21:45:50 -04:00
// Transfer settings
2025-08-21 04:07:16 -04:00
this . CHUNK _SIZE = 64 * 1024 ; // 64 KB
2025-08-18 21:45:50 -04:00
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 ;
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
this . FILE _TYPE _RESTRICTIONS = {
2026-05-17 14:48:52 -04:00
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'
2025-08-21 17:40:17 -04:00
} ,
images : {
2026-05-17 14:48:52 -04:00
extensions : [ '.jpg' , '.jpeg' , '.png' , '.gif' , '.webp' , '.bmp' , '.ico' ] ,
2025-08-21 17:40:17 -04:00
mimeTypes : [
'image/jpeg' ,
'image/png' ,
'image/gif' ,
'image/webp' ,
'image/bmp' ,
'image/x-icon'
] ,
maxSize : 25 * 1024 * 1024 , // 25 MB
category : 'Images' ,
2026-05-17 14:48:52 -04:00
description : 'JPG, JPEG, PNG, GIF, WEBP, BMP, ICO'
2025-08-21 17:40:17 -04:00
} ,
archives : {
2026-05-17 14:48:52 -04:00
extensions : [ '.zip' ] ,
mimeTypes : [ 'application/zip' ] ,
2025-08-21 17:40:17 -04:00
maxSize : 100 * 1024 * 1024 , // 100 MB
category : 'Archives' ,
2026-05-17 14:48:52 -04:00
description : 'ZIP'
2025-08-21 17:40:17 -04:00
}
} ;
2026-05-17 14:48:52 -04:00
this . BLOCKED _EXTENSIONS = new Set ( [
'.exe' , '.bat' , '.cmd' , '.sh' , '.js' , '.msi' , '.dmg' , '.app' ,
'.jar' , '.scr' , '.ps1' , '.vbs' , '.html' , '.svg'
] ) ;
2025-08-21 17:40:17 -04:00
2025-08-18 21:45:50 -04:00
// Active transfers tracking
this . activeTransfers = new Map ( ) ; // fileId -> transfer state
this . receivingTransfers = new Map ( ) ; // fileId -> receiving state
2026-05-17 14:48:52 -04:00
this . pendingIncomingTransfers = new Map ( ) ; // fileId -> validated metadata awaiting consent
2025-08-18 21:45:50 -04:00
this . transferQueue = [ ] ; // Queue for pending transfers
this . pendingChunks = new Map ( ) ;
2026-05-17 14:48:52 -04:00
this . incomingOfferLimiter = new RateLimiter ( 5 , 60000 ) ;
this . MAX _PENDING _INCOMING _TRANSFERS = 3 ;
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Session key derivation
2025-08-18 21:45:50 -04:00
this . sessionKeys = new Map ( ) ; // fileId -> derived session key
// Security
this . processedChunks = new Set ( ) ; // Prevent replay attacks
this . transferNonces = new Map ( ) ; // fileId -> current nonce counter
2025-08-21 04:07:16 -04:00
this . receivedFileBuffers = new Map ( ) ; // fileId -> { buffer:ArrayBuffer, type:string, name:string, size:number }
2026-05-17 14:48:52 -04:00
this . MAX _RETAINED _RECEIVED _FILE _BUFFERS = 3 ;
2025-08-24 16:04:49 -04:00
2025-08-18 21:45:50 -04:00
this . setupFileMessageHandlers ( ) ;
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
if ( this . webrtcManager ) {
this . webrtcManager . fileTransferSystem = this ;
}
}
// ============================================
// FILE TYPE VALIDATION SYSTEM
// ============================================
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
getFileType ( file ) {
2026-05-17 14:48:52 -04:00
const fileName = String ( file ? . name || '' ) . toLowerCase ( ) ;
const extensionIndex = fileName . lastIndexOf ( '.' ) ;
const fileExtension = extensionIndex >= 0 ? fileName . substring ( extensionIndex ) : '' ;
const mimeType = String ( file ? . type || '' ) . toLowerCase ( ) ;
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
for ( const [ typeKey , typeConfig ] of Object . entries ( this . FILE _TYPE _RESTRICTIONS ) ) {
2026-05-17 14:48:52 -04:00
const extensionAllowed = typeConfig . extensions . includes ( fileExtension ) ;
const mimeAllowed = typeConfig . mimeTypes . includes ( mimeType ) ;
if ( extensionAllowed && mimeAllowed ) {
2025-08-21 17:40:17 -04:00
return {
type : typeKey ,
category : typeConfig . category ,
description : typeConfig . description ,
maxSize : typeConfig . maxSize ,
allowed : true
} ;
}
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
return {
2026-05-17 14:48:52 -04:00
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
2025-08-21 17:40:17 -04:00
} ;
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
validateFile ( file ) {
const fileType = this . getFileType ( file ) ;
const errors = [ ] ;
2026-05-17 14:48:52 -04:00
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.' ) ;
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
if ( file . size > fileType . maxSize ) {
errors . push ( ` File size ( ${ this . formatFileSize ( file . size ) } ) exceeds maximum allowed for ${ fileType . category } ( ${ this . formatFileSize ( fileType . maxSize ) } ) ` ) ;
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
if ( ! fileType . allowed ) {
2026-05-17 14:48:52 -04:00
if ( mimeType && ! this . BLOCKED _EXTENSIONS . has ( fileExtension ) ) {
errors . push ( ` File rejected: extension and MIME type must match an allowed type. Supported types: ${ fileType . description } ` ) ;
}
2025-08-21 17:40:17 -04:00
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
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 )
} ;
}
2025-08-24 16:04:49 -04:00
2026-05-17 14:48:52 -04:00
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 } ;
}
2025-08-21 17:40:17 -04:00
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 ] ;
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
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 ;
}
2025-08-24 16:04:49 -04:00
2025-08-21 17:40:17 -04:00
getFileTypeInfo ( ) {
return {
supportedTypes : this . getSupportedFileTypes ( ) ,
generalMaxSize : this . formatFileSize ( this . MAX _FILE _SIZE ) ,
generalMaxSizeBytes : this . MAX _FILE _SIZE ,
restrictions : this . FILE _TYPE _RESTRICTIONS
} ;
2025-08-18 21:45:50 -04:00
}
2025-08-21 04:07:16 -04:00
// ============================================
// 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 ( _ ) { }
}
2025-08-20 18:19:42 -04:00
setupFileMessageHandlers ( ) {
if ( ! this . webrtcManager . dataChannel ) {
const setupRetry = setInterval ( ( ) => {
if ( this . webrtcManager . dataChannel ) {
clearInterval ( setupRetry ) ;
this . setupMessageInterception ( ) ;
}
} , 100 ) ;
2025-08-24 16:04:49 -04:00
2025-08-20 18:19:42 -04:00
setTimeout ( ( ) => {
clearInterval ( setupRetry ) ;
} , 5000 ) ;
return ;
}
// Если dataChannel уже готов, сразу настраиваем
this . setupMessageInterception ( ) ;
}
setupMessageInterception ( ) {
try {
if ( ! this . webrtcManager . dataChannel ) {
return ;
}
2025-08-21 17:40:17 -04:00
if ( this . webrtcManager ) {
this . webrtcManager . fileTransferSystem = this ;
}
2025-08-20 18:19:42 -04:00
if ( this . webrtcManager . dataChannel . onmessage ) {
this . originalOnMessage = this . webrtcManager . dataChannel . onmessage ;
}
this . webrtcManager . dataChannel . onmessage = async ( event ) => {
try {
2025-08-24 16:04:49 -04:00
if ( event . data . length > MessageSizeValidator . MAX _MESSAGE _SIZE ) {
console . warn ( '🔒 Message too large, ignoring' ) ;
SecurityErrorHandler . logSecurityEvent ( 'oversized_message_blocked' ) ;
return ;
}
2025-08-20 18:19:42 -04:00
if ( typeof event . data === 'string' ) {
try {
const parsed = JSON . parse ( event . data ) ;
2025-08-24 16:04:49 -04:00
MessageSizeValidator . isMessageSizeValid ( parsed ) ;
2025-08-20 18:19:42 -04:00
if ( this . isFileTransferMessage ( parsed ) ) {
await this . handleFileMessage ( parsed ) ;
2025-08-24 16:04:49 -04:00
return ;
2025-08-20 18:19:42 -04:00
}
} catch ( parseError ) {
2025-08-24 16:04:49 -04:00
if ( parseError . message === 'Message too large' ) {
return ;
}
2025-08-20 18:19:42 -04:00
}
}
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'
] ;
2025-08-21 17:40:17 -04:00
return fileMessageTypes . includes ( message . type ) ;
2025-08-20 18:19:42 -04:00
}
async handleFileMessage ( message ) {
try {
2025-08-21 17:40:17 -04:00
if ( ! this . webrtcManager . fileTransferSystem ) {
try {
if ( typeof this . webrtcManager . initializeFileTransfer === 'function' ) {
this . webrtcManager . initializeFileTransfer ( ) ;
let attempts = 0 ;
2025-08-24 16:04:49 -04:00
const maxAttempts = 50 ;
2025-08-21 17:40:17 -04:00
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 ;
}
}
2025-08-20 18:19:42 -04:00
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 ) ;
2025-08-24 16:04:49 -04:00
2025-08-20 18:19:42 -04:00
if ( message . fileId ) {
const errorMessage = {
type : 'file_transfer_error' ,
fileId : message . fileId ,
error : error . message ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorMessage ) ;
}
}
}
2025-08-18 21:45:50 -04:00
// ============================================
2025-08-18 23:56:10 -04:00
// SIMPLIFIED KEY DERIVATION - USE SHARED DATA
2025-08-18 21:45:50 -04:00
// ============================================
2025-08-18 23:56:10 -04:00
async deriveFileSessionKey ( fileId ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-18 23:56:10 -04:00
if ( ! this . webrtcManager . keyFingerprint || ! this . webrtcManager . sessionSalt ) {
throw new Error ( 'WebRTC session data not available' ) ;
2025-08-18 21:45:50 -04:00
}
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const fileSalt = crypto . getRandomValues ( new Uint8Array ( 32 ) ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const encoder = new TextEncoder ( ) ;
const fingerprintData = encoder . encode ( this . webrtcManager . keyFingerprint ) ;
const fileIdData = encoder . encode ( fileId ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const sessionSaltArray = new Uint8Array ( this . webrtcManager . sessionSalt ) ;
const combinedSeed = new Uint8Array (
fingerprintData . length +
sessionSaltArray . length +
fileSalt . length +
fileIdData . length
) ;
2025-08-18 21:45:50 -04:00
let offset = 0 ;
2025-08-18 23:56:10 -04:00
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 ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const keyMaterial = await crypto . subtle . digest ( 'SHA-256' , combinedSeed ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const fileSessionKey = await crypto . subtle . importKey (
2025-08-18 21:45:50 -04:00
'raw' ,
2025-08-18 23:56:10 -04:00
keyMaterial ,
{ name : 'AES-GCM' } ,
2025-08-18 21:45:50 -04:00
false ,
[ 'encrypt' , 'decrypt' ]
) ;
this . sessionKeys . set ( fileId , {
key : fileSessionKey ,
2025-08-18 23:56:10 -04:00
salt : Array . from ( fileSalt ) ,
2025-08-18 21:45:50 -04:00
created : Date . now ( )
} ) ;
2025-08-18 23:56:10 -04:00
return { key : fileSessionKey , salt : Array . from ( fileSalt ) } ;
2025-08-18 21:45:50 -04:00
} catch ( error ) {
console . error ( '❌ Failed to derive file session key:' , error ) ;
throw error ;
}
}
2025-08-18 23:56:10 -04:00
async deriveFileSessionKeyFromSalt ( fileId , saltArray ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-18 23:56:10 -04:00
if ( ! saltArray || ! Array . isArray ( saltArray ) || saltArray . length !== 32 ) {
throw new Error ( ` Invalid salt: ${ saltArray ? . length || 0 } bytes ` ) ;
2025-08-18 21:45:50 -04:00
}
2025-08-18 23:56:10 -04:00
if ( ! this . webrtcManager . keyFingerprint || ! this . webrtcManager . sessionSalt ) {
throw new Error ( 'WebRTC session data not available' ) ;
}
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const encoder = new TextEncoder ( ) ;
const fingerprintData = encoder . encode ( this . webrtcManager . keyFingerprint ) ;
const fileIdData = encoder . encode ( fileId ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const fileSalt = new Uint8Array ( saltArray ) ;
const sessionSaltArray = new Uint8Array ( this . webrtcManager . sessionSalt ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const combinedSeed = new Uint8Array (
fingerprintData . length +
sessionSaltArray . length +
fileSalt . length +
fileIdData . length
2025-08-18 21:45:50 -04:00
) ;
2025-08-18 23:56:10 -04:00
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 ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const keyMaterial = await crypto . subtle . digest ( 'SHA-256' , combinedSeed ) ;
2025-08-24 16:04:49 -04:00
2025-08-18 23:56:10 -04:00
const fileSessionKey = await crypto . subtle . importKey (
'raw' ,
keyMaterial ,
{ name : 'AES-GCM' } ,
2025-08-18 21:45:50 -04:00
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' ) ;
}
2025-08-24 16:04:49 -04:00
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.' ) ;
}
2025-08-18 21:45:50 -04:00
if ( ! file || ! file . size ) {
throw new Error ( 'Invalid file object' ) ;
}
2025-08-21 17:40:17 -04:00
const validation = this . validateFile ( file ) ;
if ( ! validation . isValid ) {
const errorMessage = validation . errors . join ( '. ' ) ;
throw new Error ( errorMessage ) ;
2025-08-18 21:45:50 -04:00
}
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 ) ;
2025-08-18 23:56:10 -04:00
// Derive session key for this file
const keyResult = await this . deriveFileSessionKey ( fileId ) ;
2025-08-18 21:45:50 -04:00
const sessionKey = keyResult . key ;
const salt = keyResult . salt ;
// Create transfer state
const transferState = {
fileId : fileId ,
file : file ,
fileHash : fileHash ,
sessionKey : sessionKey ,
2025-08-24 16:04:49 -04:00
salt : salt ,
2025-08-18 21:45:50 -04:00
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 ) ;
2026-05-17 14:48:52 -04:00
const consentPromise = new Promise ( ( resolve , reject ) => {
transferState . resolveConsent = resolve ;
transferState . rejectConsent = reject ;
transferState . consentTimeout = setTimeout ( ( ) => {
transferState . consentTimeout = null ;
reject ( new Error ( 'Transfer timeout' ) ) ;
} , 30000 ) ;
} ) ;
2025-08-18 21:45:50 -04:00
// Send file metadata first
await this . sendFileMetadata ( transferState ) ;
2026-05-17 14:48:52 -04:00
// Wait for explicit receiver consent before any chunks are sent.
await consentPromise ;
2025-08-18 21:45:50 -04:00
await this . startChunkTransmission ( transferState ) ;
return fileId ;
} catch ( error ) {
2025-08-24 16:04:49 -04:00
const safeError = SecurityErrorHandler . sanitizeError ( error ) ;
console . error ( '❌ File sending failed:' , safeError ) ;
if ( this . onError ) this . onError ( safeError ) ;
throw new Error ( safeError ) ;
2025-08-18 21:45:50 -04:00
}
}
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 ,
2025-08-24 16:04:49 -04:00
salt : transferState . salt ,
2025-08-18 21:45:50 -04:00
timestamp : Date . now ( ) ,
2025-08-18 23:56:10 -04:00
version : '2.0'
2025-08-18 21:45:50 -04:00
} ;
2025-08-24 16:04:49 -04:00
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
} ) ;
}
}
2025-08-18 21:45:50 -04:00
// Send metadata through secure channel
await this . sendSecureMessage ( metadata ) ;
transferState . status = 'metadata_sent' ;
} catch ( error ) {
2025-08-24 16:04:49 -04:00
const safeError = SecurityErrorHandler . sanitizeError ( error ) ;
console . error ( '❌ Failed to send file metadata:' , safeError ) ;
2025-08-18 21:45:50 -04:00
transferState . status = 'failed' ;
2025-08-24 16:04:49 -04:00
throw new Error ( safeError ) ;
2025-08-18 21:45:50 -04:00
}
}
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 ) ;
2025-08-21 04:07:16 -04:00
// Send chunk (с учётом backpressure)
2025-08-18 21:45:50 -04:00
await this . sendFileChunk ( transferState , chunkIndex , chunkData ) ;
// Update progress
transferState . sentChunks ++ ;
const progress = Math . round ( ( transferState . sentChunks / totalChunks ) * 95 ) + 5 ; // 5-100%
2025-08-24 16:04:49 -04:00
2025-08-21 04:07:16 -04:00
await this . waitForBackpressure ( ) ;
2025-08-18 21:45:50 -04:00
}
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 ) {
2025-08-24 16:04:49 -04:00
const safeError = SecurityErrorHandler . sanitizeError ( error ) ;
console . error ( '❌ Chunk transmission failed:' , safeError ) ;
2025-08-18 21:45:50 -04:00
transferState . status = 'failed' ;
2025-08-24 16:04:49 -04:00
throw new Error ( safeError ) ;
2025-08-18 21:45:50 -04:00
}
}
async readFileChunk ( file , start , end ) {
try {
const blob = file . slice ( start , end ) ;
return await blob . arrayBuffer ( ) ;
} catch ( error ) {
2025-08-24 16:04:49 -04:00
const safeError = SecurityErrorHandler . sanitizeError ( error ) ;
console . error ( '❌ Failed to read file chunk:' , safeError ) ;
throw new Error ( safeError ) ;
2025-08-18 21:45:50 -04:00
}
}
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
) ;
2025-08-21 04:07:16 -04:00
// Use Base64 to drastically reduce JSON overhead
const encryptedB64 = this . arrayBufferToBase64 ( new Uint8Array ( encryptedChunk ) ) ;
2025-08-18 21:45:50 -04:00
const chunkMessage = {
type : 'file_chunk' ,
fileId : transferState . fileId ,
chunkIndex : chunkIndex ,
totalChunks : transferState . totalChunks ,
nonce : Array . from ( nonce ) ,
2025-08-21 04:07:16 -04:00
encryptedDataB64 : encryptedB64 ,
2025-08-18 21:45:50 -04:00
chunkSize : chunkData . byteLength ,
timestamp : Date . now ( )
} ;
2025-08-24 16:04:49 -04:00
2025-08-21 04:07:16 -04:00
await this . waitForBackpressure ( ) ;
2025-08-18 21:45:50 -04:00
// Send chunk through secure channel
await this . sendSecureMessage ( chunkMessage ) ;
} catch ( error ) {
2025-08-24 16:04:49 -04:00
const safeError = SecurityErrorHandler . sanitizeError ( error ) ;
console . error ( '❌ Failed to send file chunk:' , safeError ) ;
throw new Error ( safeError ) ;
2025-08-18 21:45:50 -04:00
}
}
async sendSecureMessage ( message ) {
2025-08-24 16:04:49 -04:00
2025-08-21 04:07:16 -04:00
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 ( ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-21 04:07:16 -04:00
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 ;
2025-08-18 21:45:50 -04:00
}
2025-08-21 04:07:16 -04:00
const softLimit = 4 * 1024 * 1024 ;
while ( dc . bufferedAmount > softLimit ) {
await new Promise ( r => setTimeout ( r , 20 ) ) ;
}
} catch ( _ ) {
// ignore
2025-08-18 21:45:50 -04:00
}
}
async calculateFileHash ( file ) {
try {
const arrayBuffer = await file . arrayBuffer ( ) ;
2025-08-18 23:56:10 -04:00
const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , arrayBuffer ) ;
2025-08-18 21:45:50 -04:00
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 {
2026-05-17 14:48:52 -04:00
const clientId = this . getClientIdentifier ( ) ;
if ( ! this . incomingOfferLimiter . isAllowed ( clientId ) ) {
throw new Error ( 'Incoming file request rate limit exceeded' ) ;
2025-08-18 21:45:50 -04:00
}
2025-08-24 16:04:49 -04:00
2026-05-17 14:48:52 -04:00
const validation = this . validateIncomingMetadata ( metadata ) ;
if ( ! validation . isValid ) throw new Error ( validation . errors . join ( '. ' ) ) ;
2025-08-24 16:04:49 -04:00
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' ) ;
}
}
2025-08-18 21:45:50 -04:00
// Check if we already have this transfer
2026-05-17 14:48:52 -04:00
if ( this . receivingTransfers . has ( metadata . fileId ) || this . pendingIncomingTransfers . has ( metadata . fileId ) ) {
2025-08-18 21:45:50 -04:00
return ;
}
2026-05-17 14:48:52 -04:00
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' ) ;
2025-08-18 21:45:50 -04:00
}
} catch ( error ) {
2025-08-24 16:04:49 -04:00
const safeError = SecurityErrorHandler . sanitizeError ( error ) ;
console . error ( '❌ Failed to handle file transfer start:' , safeError ) ;
2025-08-18 21:45:50 -04:00
// Send error response
2025-08-18 23:56:10 -04:00
const errorResponse = {
type : 'file_transfer_response' ,
fileId : metadata . fileId ,
accepted : false ,
2025-08-24 16:04:49 -04:00
error : safeError ,
2025-08-18 23:56:10 -04:00
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorResponse ) ;
2025-08-18 21:45:50 -04:00
}
}
async handleFileChunk ( chunkMessage ) {
2025-08-24 16:04:49 -04:00
return this . atomicOps . withLock (
` chunk- ${ chunkMessage . fileId } ` ,
async ( ) => {
try {
let receivingState = this . receivingTransfers . get ( chunkMessage . fileId ) ;
2025-08-18 21:45:50 -04:00
2026-05-17 14:48:52 -04:00
// Never buffer chunks before explicit consent.
2025-08-24 16:04:49 -04:00
if ( ! receivingState ) {
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 } ` ) ;
}
}
2025-08-18 21:45:50 -04:00
}
2025-08-24 16:04:49 -04:00
) ;
2025-08-18 21:45:50 -04:00
}
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' ) ;
}
2025-08-24 16:04:49 -04:00
2025-08-21 04:07:16 -04:00
const fileBuffer = fileData . buffer ;
const fileBlob = new Blob ( [ fileBuffer ] , { type : receivingState . fileType } ) ;
2025-08-18 21:45:50 -04:00
receivingState . endTime = Date . now ( ) ;
receivingState . status = 'completed' ;
2025-08-24 16:04:49 -04:00
2026-05-17 14:48:52 -04:00
this . _storeReceivedFileBuffer ( receivingState . fileId , {
2025-08-21 04:07:16 -04:00
buffer : fileBuffer ,
type : receivingState . fileType ,
name : receivingState . fileName ,
size : receivingState . fileSize
} ) ;
2025-08-18 21:45:50 -04:00
if ( this . onFileReceived ) {
2026-05-17 14:48:52 -04:00
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 ;
} ;
2025-08-21 04:07:16 -04:00
const getObjectURL = async ( ) => {
const blob = await getBlob ( ) ;
return URL . createObjectURL ( blob ) ;
} ;
const revokeObjectURL = ( url ) => {
try { URL . revokeObjectURL ( url ) ; } catch ( _ ) { }
} ;
2025-08-18 21:45:50 -04:00
this . onFileReceived ( {
fileId : receivingState . fileId ,
fileName : receivingState . fileName ,
fileSize : receivingState . fileSize ,
2025-08-21 04:07:16 -04:00
mimeType : receivingState . fileType ,
transferTime : receivingState . endTime - receivingState . startTime ,
// backward-compatibility for existing UIs
fileBlob ,
getBlob ,
getObjectURL ,
revokeObjectURL
2025-08-18 21:45:50 -04:00
} ) ;
}
// Send completion confirmation
const completionMessage = {
type : 'file_transfer_complete' ,
fileId : receivingState . fileId ,
success : true ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( completionMessage ) ;
// Cleanup
2025-08-21 04:07:16 -04:00
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 ) ;
2025-08-18 21:45:50 -04:00
} catch ( error ) {
console . error ( '❌ File assembly failed:' , error ) ;
receivingState . status = 'failed' ;
if ( this . onError ) {
this . onError ( ` File assembly failed: ${ error . message } ` ) ;
}
// Send error notification
2025-08-18 23:56:10 -04:00
const errorMessage = {
type : 'file_transfer_complete' ,
fileId : receivingState . fileId ,
success : false ,
error : error . message ,
timestamp : Date . now ( )
} ;
await this . sendSecureMessage ( errorMessage ) ;
2025-08-18 21:45:50 -04:00
// Cleanup failed transfer
this . cleanupReceivingTransfer ( receivingState . fileId ) ;
}
}
async calculateFileHashFromData ( data ) {
try {
2025-08-18 23:56:10 -04:00
const hashBuffer = await crypto . subtle . digest ( 'SHA-256' , data ) ;
2025-08-18 21:45:50 -04:00
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' ;
2026-05-17 14:48:52 -04:00
if ( transferState . consentTimeout ) clearTimeout ( transferState . consentTimeout ) ;
transferState . consentTimeout = null ;
transferState . resolveConsent ? . ( ) ;
transferState . resolveConsent = null ;
transferState . rejectConsent = null ;
2025-08-18 21:45:50 -04:00
} else {
transferState . status = 'rejected' ;
2026-05-17 14:48:52 -04:00
if ( transferState . consentTimeout ) clearTimeout ( transferState . consentTimeout ) ;
transferState . consentTimeout = null ;
transferState . rejectConsent ? . ( new Error ( response . error || 'Transfer rejected' ) ) ;
transferState . rejectConsent = null ;
transferState . resolveConsent = null ;
2025-08-18 21:45:50 -04:00
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
} ) ) ;
}
2026-05-17 14:48:52 -04:00
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 ;
}
2025-08-18 21:45:50 -04:00
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 ) {
2026-05-17 14:48:52 -04:00
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 ;
}
}
2025-08-18 21:45:50 -04:00
this . activeTransfers . delete ( fileId ) ;
this . sessionKeys . delete ( fileId ) ;
this . transferNonces . delete ( fileId ) ;
// Remove processed chunk IDs for this transfer
for ( const chunkId of this . processedChunks ) {
if ( chunkId . startsWith ( fileId ) ) {
this . processedChunks . delete ( chunkId ) ;
}
}
}
2026-05-17 14:48:52 -04:00
_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
2025-08-18 21:45:50 -04:00
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:04:49 -04:00
}
}
2025-08-24 16:30:06 -04:00
// Удаляем из основных коллекций
this . receivingTransfers . delete ( fileId ) ;
this . sessionKeys . 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 ) ;
}
2025-08-24 16:04:49 -04:00
}
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 ) {
2025-08-18 21:45:50 -04:00
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 } ` ) ;
2025-08-18 21:45:50 -04:00
}
}
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 ,
2025-08-21 17:40:17 -04:00
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 ( )
2025-08-18 21:45:50 -04:00
} ;
}
cleanup ( ) {
2025-08-24 16:04:49 -04:00
SecureFileTransferContext . getInstance ( ) . deactivate ( ) ;
2025-08-20 18:19:42 -04:00
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 ;
}
2025-08-24 16:04:49 -04:00
// Cleanup all active transfers with secure memory wiping
2025-08-18 21:45:50 -04:00
for ( const fileId of this . activeTransfers . keys ( ) ) {
this . cleanupTransfer ( fileId ) ;
}
for ( const fileId of this . receivingTransfers . keys ( ) ) {
this . cleanupReceivingTransfer ( fileId ) ;
}
2025-08-24 16:04:49 -04:00
if ( this . atomicOps ) {
this . atomicOps . locks . clear ( ) ;
}
if ( this . rateLimiter ) {
this . rateLimiter . requests . clear ( ) ;
}
2025-08-18 21:45:50 -04:00
// Clear all state
this . pendingChunks . clear ( ) ;
2026-05-17 14:48:52 -04:00
this . pendingIncomingTransfers . clear ( ) ;
2025-08-18 21:45:50 -04:00
this . activeTransfers . clear ( ) ;
this . receivingTransfers . clear ( ) ;
this . transferQueue . length = 0 ;
this . sessionKeys . clear ( ) ;
this . transferNonces . clear ( ) ;
this . processedChunks . clear ( ) ;
2025-08-24 16:04:49 -04:00
2026-05-17 14:48:52 -04:00
for ( const fileId of Array . from ( this . receivedFileBuffers . keys ( ) ) ) {
this . _discardReceivedFileBuffer ( fileId ) ;
}
2025-08-24 16:04:49 -04:00
this . clearKeys ( ) ;
2025-08-18 23:56:10 -04:00
}
// ============================================
// SESSION UPDATE HANDLER - FIXED
// ============================================
onSessionUpdate ( sessionData ) {
// Clear session keys cache for resync
this . sessionKeys . clear ( ) ;
2025-08-18 21:45:50 -04:00
}
// ============================================
// DEBUGGING AND DIAGNOSTICS
// ============================================
2025-08-21 17:40:17 -04:00
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
} ,
2025-08-24 16:04:49 -04:00
securityContext : {
contextActive : SecureFileTransferContext . getInstance ( ) . isActive ( ) ,
securityLevel : SecureFileTransferContext . getInstance ( ) . getSecurityLevel ( ) ,
hasAtomicOps : ! ! this . atomicOps ,
hasRateLimiter : ! ! this . rateLimiter
2025-08-21 17:40:17 -04:00
} ,
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 ;
}
2025-08-18 23:56:10 -04:00
async debugKeyDerivation ( fileId ) {
2025-08-18 21:45:50 -04:00
try {
2025-08-20 18:19:42 -04:00
if ( ! this . webrtcManager . keyFingerprint || ! this . webrtcManager . sessionSalt ) {
throw new Error ( 'Session data not available' ) ;
2025-08-18 23:56:10 -04:00
}
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Test sender derivation
const senderResult = await this . deriveFileSessionKey ( fileId ) ;
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Test receiver derivation with same salt
const receiverKey = await this . deriveFileSessionKeyFromSalt ( fileId , senderResult . salt ) ;
2025-08-18 21:45:50 -04:00
2025-08-18 23:56:10 -04:00
// Test encryption/decryption
const testData = new TextEncoder ( ) . encode ( 'test data' ) ;
2025-08-18 21:45:50 -04:00
const nonce = crypto . getRandomValues ( new Uint8Array ( 12 ) ) ;
const encrypted = await crypto . subtle . encrypt (
{ name : 'AES-GCM' , iv : nonce } ,
2025-08-18 23:56:10 -04:00
senderResult . key ,
testData
2025-08-18 21:45:50 -04:00
) ;
const decrypted = await crypto . subtle . decrypt (
{ name : 'AES-GCM' , iv : nonce } ,
2025-08-18 23:56:10 -04:00
receiverKey ,
2025-08-18 21:45:50 -04:00
encrypted
) ;
const decryptedText = new TextDecoder ( ) . decode ( decrypted ) ;
2025-08-18 23:56:10 -04:00
if ( decryptedText === 'test data' ) {
return { success : true , message : 'All tests passed' } ;
2025-08-18 21:45:50 -04:00
} else {
throw new Error ( 'Decryption verification failed' ) ;
}
} catch ( error ) {
2025-08-18 23:56:10 -04:00
console . error ( '❌ Key derivation test failed:' , error ) ;
2025-08-18 21:45:50 -04:00
return { success : false , error : error . message } ;
}
}
2025-08-20 18:19:42 -04:00
// ============================================
2025-08-24 16:04:49 -04:00
// ALTERNATIVE METHOD OF INITIALIZING HANDLERS
2025-08-20 18:19:42 -04:00
// ============================================
2025-08-24 16:04:49 -04:00
2025-08-20 18:19:42 -04:00
registerWithWebRTCManager ( ) {
if ( ! this . webrtcManager ) {
throw new Error ( 'WebRTC manager not available' ) ;
}
2025-08-24 16:04:49 -04:00
2025-08-20 18:19:42 -04:00
this . webrtcManager . fileTransferSystem = this ;
2025-08-24 16:04:49 -04:00
2025-08-20 18:19:42 -04:00
this . webrtcManager . setFileMessageHandler = ( handler ) => {
this . webrtcManager . _fileMessageHandler = handler ;
} ;
2025-08-24 16:04:49 -04:00
2025-08-20 18:19:42 -04:00
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 ) ;
2025-08-24 16:04:49 -04:00
return true ;
2025-08-20 18:19:42 -04:00
}
}
} catch ( error ) {
}
2025-08-24 16:04:49 -04:00
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 ( )
2025-08-20 18:19:42 -04:00
} ;
}
2025-08-24 16:04:49 -04:00
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' ) ;
}
2025-08-18 21:45:50 -04:00
}
2026-05-17 14:48:52 -04:00
export { EnhancedSecureFileTransfer } ;