带有 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的主要内容,如果未能解决你的问题,请参考以下文章

AES 加密在 iOS 13.4 上无法正常工作

IOS AES加密之ECB128模式

swift 2.3 和 Xcode8.1 中的通用加密

在 iOS 上使用 CommonCrypto 的 PBKDF2

在 Swift 框架中导入 CommonCrypto

为啥带有 GCM 的 AES-256 会在密文大小上增加 16 个字节?