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

孤街醉人 提交于 2019-12-07 23:15:49

问题


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.


回答1:


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.




回答2:


I can't reproduce your problem with 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 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 documentation:

Initialization vector, optional. Used for Cipher Block Chaining (CBC) mode. If present, must be the same length as the selected algorithm's block size. If CBC mode is selected (by the absence of any mode bits in the options flags) and no IV is present, a NULL (all zeroes) IV will be used. This is ignored if ECB mode is used or if a stream cipher algorithm is selected.

So IV is useless in ECB mode.




回答3:


Dead to rights it turns out that there is a subtle flaw with the manual PKCS#7 padding routine applied before PHP's mcrypt.

It is well-known that mcrypt uses null padding, so to make it PKCS7-compatible mcrypt required manually byte-aligning the data by appending n number of bytes containing the value n in each of them. 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 was required to focus on the iOS 7/8 problem. However, it turns out

$block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128); // null

does nothing when linked against newer versions of libmcrypt >= 2.4 (ref), but

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

properly returns the block size. In effect, no padding is being applied and padding reverts to null padding a la mcrypt. Credit goes to zaph for demonstrating

It is clear that the test data was not padded with PKCS#7 because it has null padding bytes. PKCS#7 passes with a bytes that are the length of the padding. fullData would be: 74686520 71756963 6b206272 6f776e20 666f7820 6a756d70 6564206f 76657220 74686520 66656e63 65070707 07070707

Now, there was mention of iOS 7 handling "bad padding" differently than iOS 8. I absolutely need a reference on that, but these two faults combined explain the behavior observed in the OP.



来源:https://stackoverflow.com/questions/31850550/aes128-truncated-decrypted-text-on-ios-7-no-problems-on-ios-8

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!