CCCrypt decrypting in AES CBC works even without IV

18k views Asked by At

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;
}
4

There are 4 answers

4
zaph On BEST ANSWER

Used for formatting comment.

With iv:

clear data:   <4cd9b050 30c04bf9 2a0cb024 19010a31>
iv data:      <724a7fc0 0d8ac9d5 f09ff4c1 64d2d1bb>
key data:     <78656a1a 337fddd6 fa52e34d 9156d187>
crypt data:   <d2c2efee 54e43781 549eec03 9db688e1 7c4248e7 e2ac1d80 7105ffae 4043ffb3>
decrypt data: <4cd9b050 30c04bf9 2a0cb024 19010a31>

With iv of 0's:

clear data:   <4cd9b050 30c04bf9 2a0cb024 19010a31>
iv data:      <00000000 00000000 00000000 00000000>
key data:     <78656a1a 337fddd6 fa52e34d 9156d187>
crypt data:   <cf85cdbe 10a87309 a6fb4c4e ce640619 6be7b155 9db3f066 97e461e7 ced7960f>
decrypt data: <4cd9b050 30c04bf9 2a0cb024 19010a31>

It is clear that the iv is not being used in the OP code.

Code for above:

CCCryptorStatus ccStatus   = kCCSuccess;
size_t          cryptBytes = 0;
NSMutableData  *dataOut    = [NSMutableData dataWithLength:dataIn.length + kCCBlockSizeAES128];

ccStatus = CCCrypt( encryptOrDecrypt, // kCCEncrypt or kCCDecrypt
                   kCCAlgorithmAES128,
                   kCCOptionPKCS7Padding,
                   key.bytes, 
                   kCCKeySizeAES128,
                   iv.bytes,
                   dataIn.bytes,
                   dataIn.length,
                   dataOut.mutableBytes,
                   dataOut.length,
                   &cryptBytes);

dataOut.length = cryptBytes;
2
zaph On

The iv is only used for the first block on decryption, further blocks use the cipher text from the previous block so it is somewhat self-synchronizing.

Wikipedia image: http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation

From Wikipedia Block cipher mode of operation.

So, picking up decryption in the middle of a CBC encrypted stream on a block boundary works except for the first block.

3
ocgully On

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.

2
USTD On

You can read the correct answer here: http://www.remote-exploit.org/archives/2012/01/09/the_apple_idioten_vektor_iv/

Apple made an error in their Crypto Library that assumes that if the IV vector is not provided they automatically set the IV to a zero vector instead of returning an error. An IV should always be provided to ensure the best security and Apple should not be doing their zero assumption as it greatly weakens security and makes it vulnerable to attacks.