AES128 truncated decrypted text on iOS 7, no problem with iOS 8

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

Running the same code block in iOS 7 results in the correct plaintext but is 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 have added a standalone test harness below with the results.

Wire 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: Guess Me: When I Change

kCCOptionPKCS7Padding | kCCOptionECBMode ⇒ kCCOptionECBMode

results in iOS 7 are expected. Why is this?? I know the byte count is block aligned because the ciphertext is padded with PKCS7 padding, so it makes sense, but why does the setting kCCOptionPKCS7Padding | kCCOptionECBMode

cause truncated behavior in iOS 7?


Edit: The test ciphertext above was generated both from this website and independently using PHP mcrypt with manual PKCS7 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"));

      

Of:

I9JIk5BskZMZKJFB / EA + N2AYzkVR15DoBbUL7cBydBkWGlujVnzRHvBNvSVbcKh


Update: Correct PICCS # 7 cipher ciphertext would be

I9JIk5BskZMZKJFB / EA + N2AYzkVR15DoBbUL7cBydA6aE5a3JrRst9Gn3sb3heC

This is why this is not the case.

+2


source to share


3 answers


Data is not encrypted with PKCS # 7 padding but zero padding. You can find out about this by registering 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

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

mcrypt () while popularity was written by some bozos and uses non-standard zero padding which is insecure and won't work if the last data byte is 0x00.

Earlier versions of CCCrypt will return an error if the padding was clearly incorrect, it was a security bug that was later fixed. IIRC iOS7 was the last version to report bad padding as a bug.

The solution is to add a PKCS # 7 padding before encryption:



PKCS # 7 padding always adds padding. Padding is a byte sequence with the added number of padding bytes added. Padding length - block_size - (length (data)% block_size.

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

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

      

Or remove the trailing 0x00 byte after decryption.

See PKCS7 .

Earlier versions of CCCrypt will return an error if the padding was clearly incorrect, it was a security bug that was later fixed. This has been highlighted several times on the Apple forums, Quinn has been in many discussions. But this is a security weakness, so the parity check was removed and several developers were upset / hostile. Now, if there is invalid parity, no error is reported.

+2


source


I cannot reproduce your problem with the following code:

@implementation ViewController
{
    NSData *_data;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSLog(@"system version: %@", [[UIDevice currentDevice] systemVersion]);

    NSMutableString *text = [NSMutableString string];
    for (int i = 0; i < 80; i+=4)
    {
        [text appendFormat:@"ABCD"];
    }
    _data = [text dataUsingEncoding:NSUTF8StringEncoding];

    NSString *key = @"password";
    NSString *iv = @"12345678";
    NSData *encrypted = [self AES128Operation:kCCEncrypt key:key iv:iv];
    NSLog(@"encrypted: %@", encrypted);

    _data = encrypted;
    NSData *decrypted = [self AES128Operation:kCCDecrypt key:key iv:iv];
    NSLog(@"decrypted: %@ (%@)", decrypted, [[NSString alloc] initWithData:decrypted encoding:NSUTF8StringEncoding]);
}

- (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 = [_data length];
    size_t bufferSize = dataLength + kCCBlockSizeAES128;
    void *buffer = malloc(bufferSize);

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

      

Here are the results:

2015-08-06 12:39:29.716 Test[37788:13220246] system version: 8.4
2015-08-06 12:39:29.717 Test[37788:13220246] encrypted: <17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 c6b4234e 1d0709c9 45113e4f 2a9607f7>
2015-08-06 12:39:29.717 Test[37788:13220246] decrypted: <41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344> (ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD)


2015-08-06 13:39:50.270 Test[37841:607] system version: 7.1
2015-08-06 13:39:50.272 Test[37841:607] encrypted: <17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 17445b45 da8b6f93 7787e80a 3feb6948 c6b4234e 1d0709c9 45113e4f 2a9607f7>
2015-08-06 13:39:50.273 Test[37841:607] decrypted: <41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344 41424344> (ABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCDABCD)

      



Also from the documentation:

Initialization vector, optional. Used for Cipher Chaining (CBC) Mode. If present, must be the same length as the selected algorithm and the block size. If CBC mode is selected (due to missing any mode bits in the option flags) and no IV is present, a NULL (all zeros) IV will be used. This is ignored if ECB mode or stream encryption algorithm is used.

So the IV is useless in ECB mode.

+1


source


Dead rights , it turns out there is a subtle bug with manual PKCS # 7 applied before PHP mcrypt.

known that mcrypt uses zero padding, so for its PKCS7 compatibility, mcrypt requires manually byte-aligning the data by adding the n

number of bytes containing the value n

in each one. In the case of this question, it was done with this gem of code:

// Add PKCS#7 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
);

      

We're doing the right thing here, aren't we? I had to focus on the iOS 7/8 issue. However, it turns out that

$block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128); // null

      

doesn't do anything related to newer versions of libmcrypt> = 2.4 ( ref ), but

$block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, 'ecb'); // 16

      

correctly returns the block size. Essentially no padding is applied and the padding reverts to zero padding a la mcrypt. Credit goes to zaph for a demo

It is understood that the test data has not been padded with PKCS # 7 because it has zero padding bytes. PKCS # 7 comes with bytes, the length of which is shims. fullData will be: 74686520 71756963 6b206272 6f776e20 666f7820 6a756d70 6564206f 76657220 74686520 66656e63 65070707 07070707

There has now been a mention that iOS 7 handles "bad padding" differently than iOS 8. I absolutely need a link to this, but these two bugs combine to explain the behavior observed in the OP.

0


source







All Articles