iOS 7 上的 AES128 截断解密文本,iOS 8 上没有问题

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

使用 ECB 模式(这是玩具加密)和 PKCS7 填充的 AES128 加密密文,以下代码块导致在 iOS 8.

下恢复完整的明文

运行 iOS 7 下的相同代码块生成正确的明文,但被截断了。这是为什么?

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

我在下面添加了一个独立的测试工具和结果。

测试工具:

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 个结果:

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

iOS 7 个结果:

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

及后续结果:

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

更新: 猜谜语:当我改变时

kCCOptionPKCS7Padding | kCCOptionECBMode ⇒ kCCOptionECBMode

iOS7 中的结果符合预期。为什么是这样??我知道字节数是块对齐的,因为密文是用 PKCS7 填充填充的,所以这是有道理的,但为什么设置 kCCOptionPKCS7Padding | kCCOptionECBMode 只会导致 iOS 7 中的截断行为?


编辑: 上面的测试密文是从 this web site 生成的,并在以下函数中独立使用 PHP 的 mcrypt 和手动 PKCS7 填充:

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"));

输出:

I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydBkWGlujVnzRHvBNvSVbcKh


更新:正确的 PKCS#7 填充密文将是

I9JIk5BskZMZKJFB/EAs+N2AYzkVR15DoBbUL7cBydA6aE5a3JrRst9Gn3sb3heC

这就是为什么不是。

我无法使用以下代码重现您的问题:

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

结果如下:

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)

也来自文档:

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.

所以 IV 在 ECB 模式下没有用。

数据未使用 PKCS#7 填充加密,而是使用空填充加密。您可以通过记录 plainData:

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

输出:
纯数据:74686520 71756963 6b206272 6f776e20 666f7820 6a756d70 6564206f 76657220 74686520 66656e63 65000000 00000000

PHP mcrypt 方法就是这样做的,它是非标准的。

mcrypt(),虽然流行是由一些笨蛋编写的,并使用非标准的空填充,但如果数据的最后一个字节是 0x00 则既不安全又无法工作。

如果填充明显不正确,早期版本的 CCCrypt 会 return 出错,这是一个安全错误,后来得到纠正。 IIRC iOS7 是最后一个将错误填充报告为错误的版本。

解决方案是在加密之前添加 PKCS#7 填充:

PKCS#7 填充总是添加填充。填充是一系列字节,其值是添加的填充字节数。填充的长度是 block_size - (length(data) % block_size.

对于块为 16 字节的 AES(并希望 php 有效,已经有一段时间了):

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

或者解密后删除尾随的 0x00 字节。

PKCS7

如果填充明显不正确,早期版本的 CCCrypt 会 return 出错,这是一个安全错误,后来得到纠正。这在 Apple 论坛中被多次提及,Quinn 参与了很多讨论。但这是一个安全漏洞,因此奇偶校验被删除,一些开发人员 upset/hostile。现在,如果奇偶校验不正确,则不会报告错误。

事实证明,在 PHP 的 mcrypt 之前应用的 手册 PKCS#7 填充例程存在细微缺陷。

mcrypt 使用空填充是 well-known,因此要使其与 PKCS7 兼容,mcrypt 需要通过附加 n 个包含值 n 的字节数来手动对齐数据] 在他们每个人身上。在这个问题的情况下,它是用这个 gem 代码完成的:

// 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
);

我们在这里做的是对的,不是吗? 我需要专注于 iOS 7/8 问题。然而,结果

$block = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128); // null

链接到较新版本的 libmcrypt >= 2.4 (ref) 时不执行任何操作,但是

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

正确returns 块大小。实际上,没有应用任何填充,并且填充恢复为空填充 a la mcrypt。感谢 的演示

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

现在,有人提到 iOS 7 处理 "bad padding" 的方式不同于 iOS 8。我 绝对 需要参考,但是这两个错误结合起来解释了在 OP 中观察到的行为。