CCCrypt decrypting in AES CBC works even without IV

Dennis picture Dennis · Nov 19, 2014 · Viewed 15.4k times · Source

I have a confusing problem, where decrypting a file which was encrypted using CCCrypt's AES-CBC mode with a randomized, 16byte IV produces the exact same output whether I pass in the same correct IV used for encryption or none at all.

What I expect: using a NULL IV for decrypting should not result in a correct decryption. What I observe: using a NULL IV results in the same result as with the IV used for encryption.

Below for sake of completeness, here's the important code snippets, iv is passed in as 16-byte securely randomized NSData.

What am I not understanding here? Is CCCrypt somehow figuring out the IV from the encrypted data by itself? I couldn't find anything around that in the docs.

- (NSData *)encryptedDataForData:(NSData *)rawData
                         withKey:(NSData *)key
                              iv:(NSData *)iv
                           error:(NSError __autoreleasing**)error
{
    size_t outLength;
    NSMutableData *cipherData = [NSMutableData dataWithLength:rawData.length + kAlgorithmBlockSize];

    CCCryptorStatus result = CCCrypt(kCCEncrypt,
                                     kCCAlgorithmAES128,
                                     kCCOptionPKCS7Padding | kCCModeCBC,
                                     key.bytes,
                                     key.length,
                                     iv.bytes,
                                     rawData.bytes,
                                     rawData.length,
                                     cipherData.mutableBytes,
                                     cipherData.length,
                                     &outLength);
    if (result == kCCSuccess) {
        cipherData.length = outLength;
        return cipherData;
    } else {
        return nil;
    }
}

- (NSData *)decryptedDataForData:(NSData *)encryptedData withKey:(NSData *)key error:(NSError __autoreleasing**)error
{
    size_t outLength;
    NSMutableData *decryptedData = [NSMutableData dataWithLength:encryptedData.length];

    // this line is just to illustrate how setting the exact same iv here - if this one
    // was used for encryption - results in same output as when passing iv = NULL
    NSData *iv = [Cryptor dataForHex:@"724a7fc0 0d8ac9d5 f09ff4c1 64d2d1bb"];

    CCCryptorStatus result = CCCrypt(kCCDecrypt,
                                     kCCAlgorithmAES128,
                                     kCCOptionPKCS7Padding | kCCModeCBC,
                                     key.bytes,
                                     key.length,
                                     iv.bytes, // iv OR NULL --> same result o_O
                                     encryptedData.bytes,
                                     encryptedData.length,
                                     decryptedData.mutableBytes,
                                     decryptedData.length,
                                     &outLength);
    if (result == kCCSuccess) {
        decryptedData.length = outLength;
        return decryptedData;
    } else {
        return nil;
    }
}

EDIT:

To elaborate on this, no matter which IV I use for decryption (tried out a couple of different randomized IV's) I always do get byte for byte the identical results. Even when I decrypt only some partial chunk of the encrypted file somewhere from the middle of the encrypted file.

Is this maybe related to the actual data I am en/decrypting (mp3 files)?

When I just pass some arbitrary chunk of the actual encrypted file to the decryptor, shouldn't it require the block right before that chunk of data (which I do not provide explicitly as the IV) to do the proper decryption? The only explanation I could think of here personally is that CCCrypt just always uses the first 16-bytes as the IV and does not decrypt those but drops them in the output.

EDIT 2:

Output of en/decryption, showing the first two blocks of in and output data, the key and the iv:

# encryption
data <4cd9b050 30c04bf9 2a0cb024 19010a31 400c2261 0069196a d77bcae6 9799ae26>
iv <724a7fc0 0d8ac9d5 f09ff4c1 64d2d1bb>
key <78656a1a 337fddd6 fa52e34d 9156d187>
encrypted <cf85cdbe 10a87309 a6fb4c4e ce640619 8f445b70 3738018a e78291a7 b4ea26ce>

# decryption with correct IV
data <cf85cdbe 10a87309 a6fb4c4e ce640619 8f445b70 3738018a e78291a7 b4ea26ce>
iv <724a7fc0 0d8ac9d5 f09ff4c1 64d2d1bb>
key <78656a1a 337fddd6 fa52e34d 9156d187>
decrypted <4cd9b050 30c04bf9 2a0cb024 19010a31 400c2261 0069196a d77bcae6 9799ae26>

# decryption with zero IV
data <cf85cdbe 10a87309 a6fb4c4e ce640619 8f445b70 3738018a e78291a7 b4ea26ce>
iv <00000000 00000000 00000000 00000000>
key <78656a1a 337fddd6 fa52e34d 9156d187>
decrypted <4cd9b050 30c04bf9 2a0cb024 19010a31 400c2261 0069196a d77bcae6 9799ae26>

# decryption with different IV
data <cf85cdbe 10a87309 a6fb4c4e ce640619 8f445b70 3738018a e78291a7 b4ea26ce>
iv <12345678 9abcdef1 23456789 abcdef12>
key <78656a1a 337fddd6 fa52e34d 9156d187>
decrypted <4cd9b050 30c04bf9 2a0cb024 19010a31 400c2261 0069196a d77bcae6 9799ae26>

EDIT 3:

The code for -dataForHex: is:

+ (NSData *)dataForHex:(NSString *)hex
{
    NSString *hexNoSpaces = [[[hex stringByReplacingOccurrencesOfString:@" " withString:@""]
            stringByReplacingOccurrencesOfString:@"<" withString:@""]
            stringByReplacingOccurrencesOfString:@">" withString:@""];

    NSMutableData *data = [[NSMutableData alloc] init];
    unsigned char whole_byte = 0;
    char byte_chars[3] = {'\0','\0','\0'};
    for (NSUInteger i = 0; i < [hexNoSpaces length] / 2; i++) {
        byte_chars[0] = (unsigned char) [hexNoSpaces characterAtIndex:(NSUInteger) (i * 2)];
        byte_chars[1] = (unsigned char) [hexNoSpaces characterAtIndex:(NSUInteger) (i * 2 + 1)];
        whole_byte = (unsigned char)strtol(byte_chars, NULL, 16);
        [data appendBytes:&whole_byte length:1];
    }
    return data;
}

Answer

ocgully picture ocgully · Mar 5, 2015

It's also worth pointing out that one should never include the mode with the padding options. I've seen this around quite a bit, and I've actually fallen into the same pitfall trying to be a "explicit as possible".

kCCOptionPKCS7Padding | kCCModeCBC

Should be:

kCCOptionPKCS7Padding

Fun fact 1: Both kCCModeEBC and kCCOptionPKCS7Padding share the same value: 1, and would actually evaluate to using kCCOptionPKCS7Padding, which would then default to kCCModeCBC.

Fun fact 2: Using kCCOptionPKCS7Padding | kCCModeCBC evaluates to both kCCOptionPKCS7Padding flag and kCCOptionECBMode being set, which actually results in kCCModeEBC being used.