diff --git a/android/src/main/java/chat/rocket/mobilecrypto/algorithms/AESCrypto.kt b/android/src/main/java/chat/rocket/mobilecrypto/algorithms/AESCrypto.kt index 57add07..107d82c 100644 --- a/android/src/main/java/chat/rocket/mobilecrypto/algorithms/AESCrypto.kt +++ b/android/src/main/java/chat/rocket/mobilecrypto/algorithms/AESCrypto.kt @@ -115,9 +115,15 @@ object AESCrypto { return if (mode == "decrypt") { // Overwrite the input file with the decrypted file - val targetPath = if (inputFile.startsWith("file://")) inputFile.substring(7) else inputFile + val targetUri = Uri.parse(inputFile) FileInputStream(outputFileObj).use { inputStream -> - FileOutputStream(targetPath).use { fos -> + val outputStream = if (targetUri.scheme == null || targetUri.scheme == "file") { + FileOutputStream(normalizeFilePath(inputFile)) + } else { + reactContext.contentResolver.openOutputStream(targetUri) + ?: throw IllegalArgumentException("Cannot open output stream for URI: $targetUri") + } + outputStream.use { fos -> val buffer = ByteArray(BUFFER_SIZE) var numBytesRead: Int @@ -194,10 +200,32 @@ object AESCrypto { val uri = Uri.parse(filePath) return if (uri.scheme == null || uri.scheme == "file") { - FileInputStream(uri.path ?: filePath) + // Use the decoded path for FileInputStream + val normalizedPath = normalizeFilePath(filePath) + FileInputStream(normalizedPath) } else { reactContext.contentResolver.openInputStream(uri) ?: throw IllegalArgumentException("Cannot open input stream for URI: $uri") } } + + /** + * Normalize file path by removing file:// prefix and decoding URL-encoded characters + * (e.g., %20 for spaces, %D0%9D for Cyrillic chars) + */ + private fun normalizeFilePath(filePath: String): String { + var path = filePath + + // Remove file:// prefix if present + if (path.startsWith("file://")) { + path = path.substring(7) + return try { + Uri.decode(path) + } catch (e: Exception) { + path + } + } + + return path + } } diff --git a/ios/algorithms/AESCrypto.m b/ios/algorithms/AESCrypto.m index fb73e43..4895b8a 100644 --- a/ios/algorithms/AESCrypto.m +++ b/ios/algorithms/AESCrypto.m @@ -1,5 +1,6 @@ #import "AESCrypto.h" #import "CryptoUtils.h" +#import "FileUtils.h" #import #import @@ -87,15 +88,36 @@ + (nullable NSString *)processFile:(NSString *)filePath operation:(CCOperation)o NSData *keyData = [CryptoUtils decodeBase64:keyBase64]; NSData *ivData = [CryptoUtils decodeBase64:base64Iv]; - NSString *normalizedFilePath = [filePath stringByReplacingOccurrencesOfString:@"file://" withString:@""]; + NSString *normalizedFilePath = [FileUtils normalizeFilePath:filePath]; + + // Check if input file exists + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:normalizedFilePath]) { + return nil; + } + NSString *outputFileName = [@"processed_" stringByAppendingString:[normalizedFilePath lastPathComponent]]; NSString *outputFilePath = [[normalizedFilePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:outputFileName]; - + NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:normalizedFilePath]; NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:outputFilePath append:NO]; + + // Validate stream creation + if (!inputStream || !outputStream) { + return nil; + } + [inputStream open]; [outputStream open]; + // Validate stream opening + if ([inputStream streamStatus] != NSStreamStatusOpen || [outputStream streamStatus] != NSStreamStatusOpen) { + [inputStream close]; + [outputStream close]; + [fileManager removeItemAtPath:outputFilePath error:nil]; + return nil; + } + size_t bufferSize = 4096; uint8_t buffer[bufferSize]; CCCryptorRef cryptor = NULL; @@ -104,43 +126,47 @@ + (nullable NSString *)processFile:(NSString *)filePath operation:(CCOperation)o NSLog(@"Failed to create cryptor: %d", status); [inputStream close]; [outputStream close]; + [fileManager removeItemAtPath:outputFilePath error:nil]; return nil; } + BOOL loopSuccess = YES; + while ([inputStream hasBytesAvailable]) { NSInteger bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)]; if (bytesRead > 0) { + size_t dataOutMoved; status = CCCryptorUpdate(cryptor, buffer, bytesRead, buffer, bufferSize, &dataOutMoved); if (status == kCCSuccess) { - [outputStream write:buffer maxLength:dataOutMoved]; + NSInteger bytesWritten = [outputStream write:buffer maxLength:dataOutMoved]; + if (bytesWritten != (NSInteger)dataOutMoved) { + loopSuccess = NO; + break; + } } else { NSLog(@"Cryptor update failed: %d", status); - CCCryptorRelease(cryptor); - [inputStream close]; - [outputStream close]; - return nil; + loopSuccess = NO; + break; } } else if (bytesRead < 0) { NSLog(@"Input stream read error"); - CCCryptorRelease(cryptor); - [inputStream close]; - [outputStream close]; - return nil; + loopSuccess = NO; + break; } } - if (status == kCCSuccess) { + if (loopSuccess) { size_t finalBytesOut; status = CCCryptorFinal(cryptor, buffer, bufferSize, &finalBytesOut); - if (status == kCCSuccess) { - [outputStream write:buffer maxLength:finalBytesOut]; - } else { + if (status == kCCSuccess && finalBytesOut > 0) { + NSInteger finalBytesWritten = [outputStream write:buffer maxLength:finalBytesOut]; + if (finalBytesWritten != (NSInteger)finalBytesOut) { + NSLog(@"Output stream write error on final block"); + loopSuccess = NO; + } + } else if (status != kCCSuccess) { NSLog(@"Cryptor final failed: %d", status); - CCCryptorRelease(cryptor); - [inputStream close]; - [outputStream close]; - return nil; } } @@ -148,24 +174,31 @@ + (nullable NSString *)processFile:(NSString *)filePath operation:(CCOperation)o [inputStream close]; [outputStream close]; - if (status == kCCSuccess) { + if (status != kCCSuccess || !loopSuccess) { + [[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil]; + return nil; + } + + if (operation == kCCDecrypt) { + // For decrypt: atomically replace the original file with decrypted content NSURL *originalFileURL = [NSURL fileURLWithPath:normalizedFilePath]; NSURL *outputFileURL = [NSURL fileURLWithPath:outputFilePath]; NSError *error = nil; - [[NSFileManager defaultManager] replaceItemAtURL:originalFileURL - withItemAtURL:outputFileURL - backupItemName:nil - options:NSFileManagerItemReplacementUsingNewMetadataOnly - resultingItemURL:nil - error:&error]; - if (error) { + BOOL success = [[NSFileManager defaultManager] replaceItemAtURL:originalFileURL + withItemAtURL:outputFileURL + backupItemName:nil + options:NSFileManagerItemReplacementUsingNewMetadataOnly + resultingItemURL:nil + error:&error]; + if (!success) { NSLog(@"Failed to replace original file: %@", error); + [[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil]; return nil; } return [NSString stringWithFormat:@"file://%@", normalizedFilePath]; } else { - [[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil]; - return nil; + // For encrypt: return the processed file path + return [NSString stringWithFormat:@"file://%@", outputFilePath]; } } diff --git a/ios/algorithms/FileUtils.m b/ios/algorithms/FileUtils.m index 847dc60..ceee41d 100644 --- a/ios/algorithms/FileUtils.m +++ b/ios/algorithms/FileUtils.m @@ -220,10 +220,15 @@ + (BOOL)verifyFileIntegrity:(NSString *)filePath expectedChecksum:(NSString *)ex } + (NSString *)normalizeFilePath:(NSString *)filePath { - if ([filePath hasPrefix:@"file://"]) { - return [filePath substringFromIndex:7]; + NSString *path = filePath; + + if ([path hasPrefix:@"file://"]) { + path = [path substringFromIndex:7]; + NSString *decodedPath = [path stringByRemovingPercentEncoding]; + return decodedPath ?: path; } - return filePath; + + return path; } @end diff --git a/package.json b/package.json index fd230e9..fb0e0ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/mobile-crypto", - "version": "0.2.1", + "version": "0.2.2", "description": "Rocket.Chat Mobile Crypto", "main": "./lib/module/index.js", "module": "./lib/module/index.js",