Skip to content

Commit 57deaff

Browse files
authored
fix: decode URL-encoded characters in E2E file paths (#13)
1 parent b75e261 commit 57deaff

4 files changed

Lines changed: 102 additions & 36 deletions

File tree

android/src/main/java/chat/rocket/mobilecrypto/algorithms/AESCrypto.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,15 @@ object AESCrypto {
115115

116116
return if (mode == "decrypt") {
117117
// Overwrite the input file with the decrypted file
118-
val targetPath = if (inputFile.startsWith("file://")) inputFile.substring(7) else inputFile
118+
val targetUri = Uri.parse(inputFile)
119119
FileInputStream(outputFileObj).use { inputStream ->
120-
FileOutputStream(targetPath).use { fos ->
120+
val outputStream = if (targetUri.scheme == null || targetUri.scheme == "file") {
121+
FileOutputStream(normalizeFilePath(inputFile))
122+
} else {
123+
reactContext.contentResolver.openOutputStream(targetUri)
124+
?: throw IllegalArgumentException("Cannot open output stream for URI: $targetUri")
125+
}
126+
outputStream.use { fos ->
121127
val buffer = ByteArray(BUFFER_SIZE)
122128
var numBytesRead: Int
123129

@@ -194,10 +200,32 @@ object AESCrypto {
194200
val uri = Uri.parse(filePath)
195201

196202
return if (uri.scheme == null || uri.scheme == "file") {
197-
FileInputStream(uri.path ?: filePath)
203+
// Use the decoded path for FileInputStream
204+
val normalizedPath = normalizeFilePath(filePath)
205+
FileInputStream(normalizedPath)
198206
} else {
199207
reactContext.contentResolver.openInputStream(uri)
200208
?: throw IllegalArgumentException("Cannot open input stream for URI: $uri")
201209
}
202210
}
211+
212+
/**
213+
* Normalize file path by removing file:// prefix and decoding URL-encoded characters
214+
* (e.g., %20 for spaces, %D0%9D for Cyrillic chars)
215+
*/
216+
private fun normalizeFilePath(filePath: String): String {
217+
var path = filePath
218+
219+
// Remove file:// prefix if present
220+
if (path.startsWith("file://")) {
221+
path = path.substring(7)
222+
return try {
223+
Uri.decode(path)
224+
} catch (e: Exception) {
225+
path
226+
}
227+
}
228+
229+
return path
230+
}
203231
}

ios/algorithms/AESCrypto.m

Lines changed: 62 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#import "AESCrypto.h"
22
#import "CryptoUtils.h"
3+
#import "FileUtils.h"
34
#import <CommonCrypto/CommonCryptor.h>
45

56
#import <MobileCrypto/MobileCrypto-Swift.h>
@@ -87,15 +88,36 @@ + (nullable NSString *)processFile:(NSString *)filePath operation:(CCOperation)o
8788
NSData *keyData = [CryptoUtils decodeBase64:keyBase64];
8889
NSData *ivData = [CryptoUtils decodeBase64:base64Iv];
8990

90-
NSString *normalizedFilePath = [filePath stringByReplacingOccurrencesOfString:@"file://" withString:@""];
91+
NSString *normalizedFilePath = [FileUtils normalizeFilePath:filePath];
92+
93+
// Check if input file exists
94+
NSFileManager *fileManager = [NSFileManager defaultManager];
95+
if (![fileManager fileExistsAtPath:normalizedFilePath]) {
96+
return nil;
97+
}
98+
9199
NSString *outputFileName = [@"processed_" stringByAppendingString:[normalizedFilePath lastPathComponent]];
92100
NSString *outputFilePath = [[normalizedFilePath stringByDeletingLastPathComponent] stringByAppendingPathComponent:outputFileName];
93-
101+
94102
NSInputStream *inputStream = [NSInputStream inputStreamWithFileAtPath:normalizedFilePath];
95103
NSOutputStream *outputStream = [NSOutputStream outputStreamToFileAtPath:outputFilePath append:NO];
104+
105+
// Validate stream creation
106+
if (!inputStream || !outputStream) {
107+
return nil;
108+
}
109+
96110
[inputStream open];
97111
[outputStream open];
98112

113+
// Validate stream opening
114+
if ([inputStream streamStatus] != NSStreamStatusOpen || [outputStream streamStatus] != NSStreamStatusOpen) {
115+
[inputStream close];
116+
[outputStream close];
117+
[fileManager removeItemAtPath:outputFilePath error:nil];
118+
return nil;
119+
}
120+
99121
size_t bufferSize = 4096;
100122
uint8_t buffer[bufferSize];
101123
CCCryptorRef cryptor = NULL;
@@ -104,68 +126,79 @@ + (nullable NSString *)processFile:(NSString *)filePath operation:(CCOperation)o
104126
NSLog(@"Failed to create cryptor: %d", status);
105127
[inputStream close];
106128
[outputStream close];
129+
[fileManager removeItemAtPath:outputFilePath error:nil];
107130
return nil;
108131
}
109132

133+
BOOL loopSuccess = YES;
134+
110135
while ([inputStream hasBytesAvailable]) {
111136
NSInteger bytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
112137
if (bytesRead > 0) {
138+
113139
size_t dataOutMoved;
114140
status = CCCryptorUpdate(cryptor, buffer, bytesRead, buffer, bufferSize, &dataOutMoved);
115141
if (status == kCCSuccess) {
116-
[outputStream write:buffer maxLength:dataOutMoved];
142+
NSInteger bytesWritten = [outputStream write:buffer maxLength:dataOutMoved];
143+
if (bytesWritten != (NSInteger)dataOutMoved) {
144+
loopSuccess = NO;
145+
break;
146+
}
117147
} else {
118148
NSLog(@"Cryptor update failed: %d", status);
119-
CCCryptorRelease(cryptor);
120-
[inputStream close];
121-
[outputStream close];
122-
return nil;
149+
loopSuccess = NO;
150+
break;
123151
}
124152
} else if (bytesRead < 0) {
125153
NSLog(@"Input stream read error");
126-
CCCryptorRelease(cryptor);
127-
[inputStream close];
128-
[outputStream close];
129-
return nil;
154+
loopSuccess = NO;
155+
break;
130156
}
131157
}
132158

133-
if (status == kCCSuccess) {
159+
if (loopSuccess) {
134160
size_t finalBytesOut;
135161
status = CCCryptorFinal(cryptor, buffer, bufferSize, &finalBytesOut);
136-
if (status == kCCSuccess) {
137-
[outputStream write:buffer maxLength:finalBytesOut];
138-
} else {
162+
if (status == kCCSuccess && finalBytesOut > 0) {
163+
NSInteger finalBytesWritten = [outputStream write:buffer maxLength:finalBytesOut];
164+
if (finalBytesWritten != (NSInteger)finalBytesOut) {
165+
NSLog(@"Output stream write error on final block");
166+
loopSuccess = NO;
167+
}
168+
} else if (status != kCCSuccess) {
139169
NSLog(@"Cryptor final failed: %d", status);
140-
CCCryptorRelease(cryptor);
141-
[inputStream close];
142-
[outputStream close];
143-
return nil;
144170
}
145171
}
146172

147173
CCCryptorRelease(cryptor);
148174
[inputStream close];
149175
[outputStream close];
150176

151-
if (status == kCCSuccess) {
177+
if (status != kCCSuccess || !loopSuccess) {
178+
[[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil];
179+
return nil;
180+
}
181+
182+
if (operation == kCCDecrypt) {
183+
// For decrypt: atomically replace the original file with decrypted content
152184
NSURL *originalFileURL = [NSURL fileURLWithPath:normalizedFilePath];
153185
NSURL *outputFileURL = [NSURL fileURLWithPath:outputFilePath];
154186
NSError *error = nil;
155-
[[NSFileManager defaultManager] replaceItemAtURL:originalFileURL
156-
withItemAtURL:outputFileURL
157-
backupItemName:nil
158-
options:NSFileManagerItemReplacementUsingNewMetadataOnly
159-
resultingItemURL:nil
160-
error:&error];
161-
if (error) {
187+
BOOL success = [[NSFileManager defaultManager] replaceItemAtURL:originalFileURL
188+
withItemAtURL:outputFileURL
189+
backupItemName:nil
190+
options:NSFileManagerItemReplacementUsingNewMetadataOnly
191+
resultingItemURL:nil
192+
error:&error];
193+
if (!success) {
162194
NSLog(@"Failed to replace original file: %@", error);
195+
[[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil];
163196
return nil;
164197
}
165198
return [NSString stringWithFormat:@"file://%@", normalizedFilePath];
166199
} else {
167-
[[NSFileManager defaultManager] removeItemAtPath:outputFilePath error:nil];
168-
return nil;
200+
// For encrypt: return the processed file path
201+
return [NSString stringWithFormat:@"file://%@", outputFilePath];
169202
}
170203
}
171204

ios/algorithms/FileUtils.m

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,15 @@ + (BOOL)verifyFileIntegrity:(NSString *)filePath expectedChecksum:(NSString *)ex
220220
}
221221

222222
+ (NSString *)normalizeFilePath:(NSString *)filePath {
223-
if ([filePath hasPrefix:@"file://"]) {
224-
return [filePath substringFromIndex:7];
223+
NSString *path = filePath;
224+
225+
if ([path hasPrefix:@"file://"]) {
226+
path = [path substringFromIndex:7];
227+
NSString *decodedPath = [path stringByRemovingPercentEncoding];
228+
return decodedPath ?: path;
225229
}
226-
return filePath;
230+
231+
return path;
227232
}
228233

229234
@end

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rocket.chat/mobile-crypto",
3-
"version": "0.2.1",
3+
"version": "0.2.2",
44
"description": "Rocket.Chat Mobile Crypto",
55
"main": "./lib/module/index.js",
66
"module": "./lib/module/index.js",

0 commit comments

Comments
 (0)