Search code examples
iosobjective-cencryptionaescommoncrypto

AES128 truncated decrypted text on iOS 7, no problems on iOS 8


Using ciphertext encrypted with AES128 using ECB mode (this is toy encryption) and PKCS7 padding, the following code block results in the complete plaintext being recovered under iOS 8.

Running the same code block under iOS 7 results in the correct plaintext, but truncated. Why is this?

#import "NSData+AESCrypt.h" // <-- a category with the below function
#import <CommonCrypto/CommonCryptor.h>

- (NSData *)AES128Operation:(CCOperation)operation key:(NSString *)key iv:(NSString *)iv
{
    char keyPtr[kCCKeySizeAES128 + 1];
    bzero(keyPtr, sizeof(keyPtr));
    [key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];

    char ivPtr[kCCBlockSizeAES128 + 1];
    bzero(ivPtr, sizeof(ivPtr));
    if (iv) {
        [iv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
    }

    NSUInteger dataLength = [self length];                      
    size_t bufferSize = dataLength + kCCBlockSizeAES128;        
    void *buffer = malloc(bufferSize);

    size_t numBytesEncrypted = 0;
    CCCryptorStatus cryptStatus = CCCrypt(operation,
                                          kCCAlgorithmAES128,
                                          kCCOptionPKCS7Padding | kCCOptionECBMode,
                                          keyPtr,               
                                          kCCBlockSizeAES128,   
                                          ivPtr,                
                                          [self bytes],
                                          dataLength,           
                                          buffer,
                                          bufferSize,           
                                          &numBytesEncrypted);  
    if (cryptStatus == kCCSuccess) {
        return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
    }
    free(buffer);
    return nil;
}

I've added a self-contained test harness below with results.

Test harness:

NSString *key = @"1234567890ABCDEF";
NSString *ciphertext = @"I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydBkWGlujVnzRHvBNvSVbcKh";

NSData *encData = [[NSData alloc]initWithBase64EncodedString:ciphertext options:0];
NSData *plainData = [encData AES128Operation:kCCDecrypt key:key iv:nil];

NSString *plaintext = [NSString stringWithUTF8String:[plainData bytes]];

DLog(@"key: %@\nciphertext: %@\nplaintext: %@", key, ciphertext, plaintext);

iOS 8 results:

key: 1234567890ABCDEF
ciphertext: I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydBkWGlujVnzRHvBNvSVbcKh
plaintext: the quick brown fox jumped over the fence

iOS 7 results:

key: 1234567890ABCDEF
ciphertext: I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydBkWGlujVnzRHvBNvSVbcKh
plaintext: the quick brown fox jumped over 0

and subsequent results:

plaintext: the quick brown fox jumped over 
plaintext: the quick brown fox jumped over *

Update: Riddle me this: when I change

kCCOptionPKCS7Padding | kCCOptionECBMode ⇒ kCCOptionECBMode

the results in iOS 7 are as expected. Why is this?? I know the number of bytes are block aligned because the ciphertext is padded with PKCS7 padding so this makes sense, but why does setting kCCOptionPKCS7Padding | kCCOptionECBMode cause the truncated behavior in iOS 7 only?


Edit: The test ciphertext above was generated from both this web site, and independently using PHP's mcrypt with manual PKCS7 padding in the following function:

function encryptAES128WithPKCS7($message, $key)
{
    if (mb_strlen($key, '8bit') !== 16) {
        throw new Exception("Needs a 128-bit key!");
    }

    // Add PKCS7 Padding
    $block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128);
    $pad = $block - (mb_strlen($message, '8bit') % $block);
    $message .= str_repeat(chr($pad), $pad);

    $ciphertext = mcrypt_encrypt(
        MCRYPT_RIJNDAEL_128,
        $key,
        $message,
        MCRYPT_MODE_ECB
    );

    return $ciphertext;
}

// Demonstration encryption
echo base64_encode(encryptAES128WithPKCS7("the quick brown fox jumped over the fence", "1234567890ABCDEF"));

Out:

I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydBkWGlujVnzRHvBNvSVbcKh


Update: The properly PKCS#7-padded ciphertext would be

I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydA6aE5a3JrRst9Gn3sb3heC

Here is why is wasn't.


Solution

  • The data is not encrypted with PKCS#7 padding but with null padding. You can tell this by logging plainData:

    NSData *fullData = [NSData dataWithBytes:buffer length:dataLength];
    NSLog(@"\nfullData: %@", fullData);
    

    Output:
    plainData: 74686520 71756963 6b206272 6f776e20 666f7820 6a756d70 6564206f 76657220 74686520 66656e63 65000000 00000000

    The PHP mcrypt method does this, it is non-standard.

    mcrypt(), while popular was written by some bozos and uses non-standard null padding which is both insecure and will not work if the last byte of the data is 0x00.

    Early versions of CCCrypt would return an error if the padding was obviously incorrect, that was a security error which was later corrected. IIRC iOS7 was the last version that reported bad padding as an error.

    The solution is to add PKCS#7 padding prior to encryption:

    PKCS#7 padding always adds padding. The padding is a series by bytes with the value of the number of padding bytes added. The length of the padding is the block_size - (length(data) % block_size.

    For AES where the block is is 16-bytes (and hoping the php is valid, it had been a while):

    $pad_count = 16 - (strlen($data) % 16);
    $data .= str_repeat(chr($pad_count), $pad_count);
    

    Or remove trailing 0x00 bytes after decryption.

    See PKCS7.

    Early versions of CCCrypt would return an error if the padding was obviously incorrect, that was a security error which was later corrected. This was covered several times in the Apple forums, Quinn was in many of the discussions. But that is a security weakness so the parity check was removed and several developers were upset/hostile. Now if there is incorrect parity no error is reported.