带有 CommonCrypto 的 AES 使用太多内存 - Objective-C
Posted
技术标签:
【中文标题】带有 CommonCrypto 的 AES 使用太多内存 - Objective-C【英文标题】:AES with CommonCrypto uses too much memory - Objective-C 【发布时间】:2011-06-29 21:56:47 【问题描述】:我的目标是能够在获得文件/文件夹和密码的情况下使用 Objective-C 在 AES 中对其进行加密和解密。我不是加密书呆子或任何东西,但我选择了 AES,因为我发现它非常标准且非常安全。我正在使用一个 NSMutableData 类别,它具有加密和解密其数据的方法。这里是:
- (NSInteger)AES256EncryptionWithKey: (NSString*)key
// The key should be 32 bytes for AES256, will be null-padded otherwise
char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
// Fetch key data
if (![key getCString: keyPtr maxLength: sizeof(keyPtr) encoding: NSUTF8StringEncoding])
return 2; // Length of 'key' is bigger than keyPtr
NSUInteger dataLength = [self length];
// See the doc: For block ciphers, the output size will always be less than or
// equal to the input size plus the size of one block.
// That's why we need to add the size of one block here
size_t bufferSize = dataLength + kCCBlockSizeAES128;
void* buffer = malloc(bufferSize);
size_t numBytesEncrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
keyPtr, kCCKeySizeAES256,
NULL , // initialization vector (optional)
[self bytes], dataLength, // input bytes and it's length
buffer, bufferSize, // output buffer and it's length
&numBytesEncrypted); // ??
if (cryptStatus == kCCSuccess)
// The returned NSData takes ownership of the buffer and will free it on deallocation
[self setData: [NSData dataWithBytesNoCopy: buffer length: numBytesEncrypted]];
return 0;
free(buffer); // Free the buffer;
return 1;
- (NSInteger)AES256DecryptionWithKey: (NSString*)key
// The key should be 32 bytes for AES256, will be null-padded otherwise
char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
// Fetch key data
if (![key getCString: keyPtr maxLength: sizeof(keyPtr) encoding: NSUTF8StringEncoding])
return 2; // Length of 'key' is bigger than keyPtr
NSUInteger dataLength = [self length];
// See the doc: For block ciphers, the output size will always be less than or
// equal to the input size plus the size of one block.
// That's why we need to add the size of one block here
size_t bufferSize = dataLength + kCCBlockSizeAES128;
void* buffer = malloc(bufferSize);
size_t numBytesDecrypted = 0;
CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
keyPtr, kCCKeySizeAES256,
NULL, // initialization vector (optional)
[self bytes], dataLength, // input
buffer, bufferSize, // output
&numBytesDecrypted);
if (cryptStatus == kCCSuccess)
// The returned NSData takes ownership of the buffer and will free it on deallocation
[self setData: [NSData dataWithBytesNoCopy: buffer length: numBytesDecrypted]];
return 0;
free(buffer); // Free the buffer;
return 1;
这段代码的问题在于它使用了 about !! 5!!内存中用户选择的文件(使用 NSMutableData 打开)大小的倍数。从用户的角度来看,这是完全不能接受的(想象加密内存中 2Gb - 10Gb 的文件??),但我真的很茫然。
您能建议任何可以解决此问题的修改吗?可能一次加密一个块(这样只有一两个块同时在内存中,而不是整个文件 * 5)。最大的问题是我不知道该怎么做。有什么想法吗?
谢谢
PS:当我使用这个类别时,我是这样做的:
NSMutableData* data = [NSMutableData dataWithContentsOfFile: @"filepath"];
[data AES256EncryptionWithKey: @"password"];
[data writeToFile: @"newname" atomically: NO];
仅这 3 行就造成了如此大的内存问题。
哦,顺便说一句:我需要一个初始化向量吗?我认为它更安全,或者什么,但我不知道。如果真的有需要,能告诉我怎么做吗?
编辑
这就是我现在正在做的事情:
NSMutableData* data = [NSMutableData dataWithContentsOfMappedFile: @"filepath"];
[data SafeAES256EncryptionWithKey: @"password"];
[data writeToFile: @"newname" atomically: NO];
以及类别中的新方法:
- (void)SafeAES256EncryptionWithKey: (NSString*)key
// The key should be 32 bytes for AES256, will be null-padded otherwise
char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
// Fetch key data
if (![key getCString: keyPtr maxLength: sizeof(keyPtr) encoding: NSUTF8StringEncoding])
return 2; // Length of 'key' is bigger than keyPtr
CCCryptorRef cryptor;
CCCryptorStatus cryptStatus = CCCryptorCreate(kCCEncrypt, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
keyPtr, kCCKeySizeAES256,
NULL, // IV - needed?
&cryptor);
if (cryptStatus != kCCSuccess)
; // Handle error here
NSInteger startByte;
size_t dataOutMoved;
size_t dataInLength = kChunkSizeBytes; // #define kChunkSizeBytes (16)
size_t dataOutLength = CCCryptorGetOutputLength(cryptor, dataInLength, FALSE);
const void* dataIn = malloc(dataInLength);
void* dataOut = malloc(dataOutLength);
for (startByte = 0; startByte <= [self length]; startByte += kChunkSizeBytes)
if ((startByte + kChunkSizeBytes) > [self length]) dataInLength = [self length] - startByte;
else dataInLength = kChunkSizeBytes;
NSRange bytesRange = NSMakeRange(startByte, (int)dataInLength);
[self getBytes: dataIn range: bytesRange];
CCCryptorUpdate(cryptor, dataIn, dataInLength, dataOut, dataOutLength, &dataOutMoved);
if (dataOutMoved != dataOutLength)
NSLog(@"dataOutMoved != dataOutLength");
[self replaceBytesInRange: bytesRange withBytes: dataOut];
CCCryptorFinal(cryptor, dataOut, dataOutLength, &dataOutMoved);
[self appendBytes: dataOut length: dataOutMoved];
CCCryptorRelease(cryptor);
我不明白为什么这有时有效而有时却无效。我在这里真的很茫然。有人可以检查此代码吗?
为了不一次将所有文件加载到内存中,我使用-dataWithContentsOfMappedFile
,然后调用-getBytes:range:
,因为我看到here那样它不会将所有文件加载到实际内存中一次,仅指定范围。
编辑 2
请看我的回答,了解我现在在做什么。
【问题讨论】:
见:***.com/questions/6127318/… 你不应该将整个文件加载到内存中,你应该只对小数据使用CCCrypt()
。
@Bavarious 所以,我现在有两个问题。首先,我如何只加载文件的一小部分数据(我无法加载所有文件,但需要一个 16 字节 - 或多个字节 - 文件块)。其次,我如何使用 CCCreate、Update 和 Finish。我记得在那儿看到过一个例子,但我再也找不到了。我真的无法理解您必须赋予这些功能的所有类型。当代码脱离 Objective-C 和 Cocoa 领域时,我会迷路。请帮忙!
@Bavarious 我已经用我现在正在做的事情更新了我的问题,但仍然无法正常工作。请检查一下。
我今天晚些时候再看看。
@Bavarious 我已经用我现在正在做的事情回答了这个问题,这似乎完美无缺。不过我不确定。
【参考方案1】:
我决定离开舒适的 Objc-C 领域,用 C 函数重写上面的第二个 NSMutableData 类别。我尽力了,但如果这段代码有缺陷,我也不会感到惊讶,所以请提出建议!我还删除了“方案”类别,并决定改用独立方法。这里:
// What do you think this number should be? 16B, 256B...? 1KB, 1MB? Please tell me
#define kChunkSizeBytes (1024*1024) // 1 MB
- (BOOL)cryptFile: (NSString*)oldFPath
toFile: (NSString*)newFPath
withPassword: (NSString*)password
andOperation: (CCOperation)operation
// READ PASSWORD
// The key should be 32 bytes for AES256, will be null-padded otherwise
char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
// Fetch key data
if (![password getCString: keyPtr maxLength: sizeof(keyPtr) encoding: NSUTF8StringEncoding])
return FALSE; // Length of 'key' is bigger than keyPtr
// CREATE CRYPTOR
CCCryptorRef cryptor;
CCCryptorStatus cryptStatus = CCCryptorCreate(operation, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
keyPtr, kCCKeySizeAES256,
NULL, // IV - needed?
&cryptor);
if (cryptStatus != kCCSuccess)
return FALSE; // Handle error here
// OPEN OLD FILE AND READ SIZE
FILE* oldFile = fopen([oldFPath UTF8String], "rb");
if(oldFile == NULL)
return FALSE; // Could not open old file
fseek(oldFile, 0, SEEK_END);
size_t oldFileSize = ftell(oldFile);
fseek(oldFile, 0, SEEK_SET);
// OPEN NEW FILE
FILE* newFile = fopen([newFPath UTF8String], "ab");
if(newFile == NULL)
return FALSE; // Could not open new file
// ..CRYPT
NSInteger byteOffset;
size_t dataOutMoved;
size_t dataInLength = kChunkSizeBytes;
size_t dataOutLength = CCCryptorGetOutputLength(cryptor, dataInLength, FALSE);
const void* dataIn = malloc(dataInLength);
void* dataOut = malloc(dataOutLength);
// ..crypt data one chunk at a time
for (byteOffset = 0; byteOffset <= oldFileSize; byteOffset += kChunkSizeBytes)
if ([[NSThread currentThread] isCancelled]) break;
if ((byteOffset + kChunkSizeBytes) > oldFileSize) dataInLength = oldFileSize - byteOffset;
else dataInLength = kChunkSizeBytes;
fseeko(oldFile, byteOffset, SEEK_SET);
fread(dataIn, 1, dataInLength, oldFile);
CCCryptorUpdate(cryptor, dataIn, dataInLength, dataOut, dataOutLength, &dataOutMoved);
fwrite(dataOut, 1, dataOutMoved, newFile);
// If thread hasn't been cancelled, finalize
if (![[NSThread currentThread] isCancelled])
CCCryptorFinal(cryptor, dataOut, dataOutLength, &dataOutMoved);
fwrite(dataOut, 1, dataOutMoved, newFile);
// CLOSE AND RELEASE
free(dataIn);
free(dataOut);
fclose(oldFile);
fclose(newFile);
CCCryptorRelease(cryptor);
return TRUE;
我知道'for'循环内部没有错误检查,其他地方也可能是这种情况。请对此提出建议!那里有一些代码可以检查线程是否已被取消。那是因为此代码在我的班级控制的单独线程上运行。每当用户单击“取消”按钮时,都会向我创建的线程发送取消消息。那些如果确保线程实际取消。随意提出建议(再次!)并在任何你喜欢的地方使用此代码:)
PS:我已经对此进行了测试,包括加密和解密,到目前为止它运行良好。我最初的问题(内存过多)似乎也解决了!
【讨论】:
不得不说,经过更多的测试,我发现它没有正确加密和解密一个大约2GB的文件(压缩后大约是1.5GB)。我在这个答案中发布的代码看起来不错,但我不能确定,因为我的知识很少。请告诉我你的意见!我真的希望这是完美的...... PS:这可能是由于我的驱动器空间不足,现在我想到了。但我仍然不能确定。 我正在解密一个 333mb 的文件,但由于内存问题而崩溃。@alex【参考方案2】:这是使用 NSMutableData 上的类别对 Alex 的原始代码的修改实现。它已经使用内存数据进行了彻底的单元测试,以及具有不同长度文件的[NSMutableData dataWithContentsOfMappedFile:"filePath"];
,从几个字节到 50 MB。
NSMutableData+Crypto.h
@interface NSMutableData (Crypto)
- (BOOL)encryptWithKey:(NSString *)key;
- (BOOL)decryptWithKey:(NSString *)key;
@end
还有 NSMutableData+Crypto.m
#define kChunkSizeBytes (1024 * 1024) // 1 MB
@implementation NSMutableData (Crypto)
/**
* Performs a cipher on an in-place buffer
*/
-(BOOL) doCipher:(NSString *)key operation: (CCOperation) operation
// The key should be 32 bytes for AES256, will be null-padded otherwise
char keyPtr[kCCKeySizeAES256 + 1]; // room for terminator (unused)
bzero(keyPtr, sizeof(keyPtr)); // fill with zeroes (for padding)
// Fetch key data
if (![key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding]) return FALSE; // Length of 'key' is bigger than keyPtr
CCCryptorRef cryptor;
CCCryptorStatus cryptStatus = CCCryptorCreate(operation, kCCAlgorithmAES128, kCCOptionPKCS7Padding,
keyPtr, kCCKeySizeAES256,
NULL, // IV - needed?
&cryptor);
if (cryptStatus != kCCSuccess) // Handle error here
return FALSE;
size_t dataOutMoved;
size_t dataInLength = kChunkSizeBytes; // #define kChunkSizeBytes (16)
size_t dataOutLength = CCCryptorGetOutputLength(cryptor, dataInLength, FALSE);
size_t totalLength = 0; // Keeps track of the total length of the output buffer
size_t filePtr = 0; // Maintains the file pointer for the output buffer
NSInteger startByte; // Maintains the file pointer for the input buffer
char *dataIn = malloc(dataInLength);
char *dataOut = malloc(dataOutLength);
for (startByte = 0; startByte <= [self length]; startByte += kChunkSizeBytes)
if ((startByte + kChunkSizeBytes) > [self length])
dataInLength = [self length] - startByte;
else
dataInLength = kChunkSizeBytes;
// Get the chunk to be ciphered from the input buffer
NSRange bytesRange = NSMakeRange((NSUInteger) startByte, (NSUInteger) dataInLength);
[self getBytes:dataIn range:bytesRange];
cryptStatus = CCCryptorUpdate(cryptor, dataIn, dataInLength, dataOut, dataOutLength, &dataOutMoved);
if (dataOutMoved != dataOutLength)
NSLog(@"dataOutMoved (%d) != dataOutLength (%d)", dataOutMoved, dataOutLength);
if ( cryptStatus != kCCSuccess)
NSLog(@"Failed CCCryptorUpdate: %d", cryptStatus);
// Write the ciphered buffer into the output buffer
bytesRange = NSMakeRange(filePtr, (NSUInteger) dataOutMoved);
[self replaceBytesInRange:bytesRange withBytes:dataOut];
totalLength += dataOutMoved;
filePtr += dataOutMoved;
// Finalize encryption/decryption.
cryptStatus = CCCryptorFinal(cryptor, dataOut, dataOutLength, &dataOutMoved);
totalLength += dataOutMoved;
if ( cryptStatus != kCCSuccess)
NSLog(@"Failed CCCryptorFinal: %d", cryptStatus);
// In the case of encryption, expand the buffer if it required some padding (an encrypted buffer will always be a multiple of 16).
// In the case of decryption, truncate our buffer in case the encrypted buffer contained some padding
[self setLength:totalLength];
// Finalize the buffer with data from the CCCryptorFinal call
NSRange bytesRange = NSMakeRange(filePtr, (NSUInteger) dataOutMoved);
[self replaceBytesInRange:bytesRange withBytes:dataOut];
CCCryptorRelease(cryptor);
free(dataIn);
free(dataOut);
return 1;
- (BOOL)encryptWithKey:(NSString *)key
return [self doCipher:key operation:kCCEncrypt];
- (BOOL)decryptWithKey:(NSString *)key
return [self doCipher:key operation:kCCDecrypt];
亚历克斯注意:您最初的尝试根本没有考虑到 CCCryptorUpdate 可能不会在输出上返回与输入相同的字节数。例如,解密 1024 字节通常返回 1008 字节,因为 16 字节用于填充。您使用 C 文件函数的示例通过写入新文件而不是就地替换内存来考虑这一点。我刚刚实现了一个类似于您的文件输出方法的文件位置指针。感谢您让我开始!
【讨论】:
这看起来不错!唯一的缺点是我需要能够对任何大小的文件进行操作(一次加载 2Gb 并不好,这就是让我首先开始这个问题的原因:)。看了我的回答,你有没有发现我的代码有什么缺陷? (除了缺乏错误处理)有了这个安全的东西,确保一切都是正确的总是很重要的...... 我已经用一个 700MB 的文件对其进行了测试,它运行良好,并且从不使用超过 18MB 的 RAM(在单元测试中 - 显然使用 GUI 会更多)。它在 800 MB 后开始剥落。以上是关于带有 CommonCrypto 的 AES 使用太多内存 - Objective-C的主要内容,如果未能解决你的问题,请参考以下文章