无法在iPhone上解密使用OpenSSL编码的数据

4

这是我之前问题的追踪: 如何从OpenSSL加密数据中获取初始化向量(IV)

我正在使用OpenSSL命令行实用程序加密字符串,然后尝试在iPhone上使用<CommonCrypto/CommonCryptor.h>解密字符串。使用Dropbox SDK,一个带有加密字符串的xml文件被加载到iPhone上,我的应用程序试图解析并解密此文件中的字符串。

这是OpenSSL命令的示例:

printf %s "Hello" | openssl enc -aes-128-cbc -K 00ff349830193845af43984758690213 -iv 0 -base64

上述的base64字符串被放置在一个XML文件中,然后由应用程序解析。
我正在使用Matt Gallagher的NSData这个扩展来解码base64文本。我假设它能正常工作;我目前还没有找到一个好的测试方法。(来源:http://cocoawithlove.com/2009/06/base64-encoding-options-on-mac-and.html)。
以下是解密加密字符串的方法。
在这种情况下,密钥是一个NSString,等于@"00ff349830193845af43984758690213"
+ (NSString *)string:(NSString *)encryptedString withAES128Key:(NSString *)key {

// decode base64, from Matt Gallagher's NSData category
NSData *b64DecodedData = [NSData dataFromBase64String:encryptedString];

NSData *keyData = [key dataUsingEncoding:NSUTF8StringEncoding];

// fyi, I plan to replace this later with a random iv
NSData *ivData = [@"00000000000000000000000000000000" dataUsingEncoding:NSUTF8StringEncoding];

// decrypt the string
NSData *decodedData = [self doCipher:b64DecodedData iv:ivData key:keyData context:kCCDecrypt];

NSString *unencryptedString = [[NSString alloc] initWithBytes:[decodedData bytes] length:[decodedData length] encoding:NSUTF8StringEncoding];

return [unencryptedString autorelease];
}

以下是实际解密的方法:(此方法的信用归功于另一个stackoverflow用户。)
+ (NSData *)doCipher:(NSData *)dataIn
              iv:(NSData *)iv
             key:(NSData *)symmetricKey
         context:(CCOperation)encryptOrDecrypt
{
CCCryptorStatus ccStatus   = kCCSuccess;
size_t          cryptBytes = 0;    // Number of bytes moved to buffer.
NSMutableData  *dataOut    = [NSMutableData dataWithLength:dataIn.length + kCCBlockSizeAES128];

ccStatus = CCCrypt( encryptOrDecrypt,
                   kCCAlgorithmAES128,
                   kCCOptionPKCS7Padding,
                   [symmetricKey bytes], 
                   kCCKeySizeAES128,
                   iv,
                   dataIn.bytes,
                   dataIn.length,
                   dataOut.mutableBytes,
                   dataOut.length,
                   &cryptBytes);

// error occurs here, error -4304 kCCDecodeError
if (ccStatus != kCCSuccess) {
    // Handle error
    NSLog(@"CCCrypt status: %d", ccStatus);
}

dataOut.length = cryptBytes;

return dataOut;
}

发生了一个错误,错误代码为-4304,即kCCDecodeError,因为ccStatus不等于kCCSuccess
我感觉密钥和iv没有被正确地设置为NSData对象。 OpenSSL要求密钥和iv是十六进制值,我已经这样做,并仔细将它们设置为恰好128位。 但是,我认为在将这些字符串转换为doCipher方法的NSData时漏掉了某些东西。
非常感谢任何帮助! 我一整天都在尝试解决这个问题。
3个回答

8

虽然iv处理不正确,但这只是问题的一部分。

解码错误听起来像是参数长度不正确,因为任何随机的iv、密钥和数据都应该是有效的输入。(我的妻子同意这一点,她职业上从事这方面的工作。)检查一下将它们转换为NSData后的密钥和数据长度。请注意,传递具有不正确或不兼容填充的加密数据也会导致解码错误。

编写一个Base64测试,针对你的iOS代码和openssl。

从简单的测试开始逐步推进解决方案。

例如,在获得加密最高工作效果之前先去掉base64。尝试使用简单的数据,比如所有位都为0的一个块长度,填充可能会是一个问题。尝试使用更简单的密钥,比如所有位都为0。你可以在Mac终端命令行中使用OPENSSL。

一旦基本的加密效果正常,再添加需要的功能。

对于来自命令行的openssl,请使用输入和输出文件,它们将处理二进制文件,因此您最初不必担心这个问题。以下是一个示例:

(file_orig.txt contains: "1234567890123456")

openssl enc -e -aes-128-cbc -K 00ff349830193845af43984758690213 -p -iv 0 -nosalt -in file_orig.txt -out file_aes.txt

该代码会打印出生成的密钥以及使用的初始化向量(iv):

key=00ff349830193845af43984758690213
iv =00000000000000000000000000000000

然后您可以在iOS方法中读取相同的数据文件。

这里是一个使用openssl创建的文件的iOS方法:
(将key openssl输出放入文件key-hex-openssl.txt中)

NSData *keyHexData = [@"00ff349830193845af43984758690213" dataUsingEncoding:NSUTF8StringEncoding];
NSData *testData   = [NSData dataWithContentsOfFile:@"yourDirectoryPath/file_aes.txt"];
NSData *clearData  = [NSData dataWithContentsOfFile:@"yourDirectoryPath/file_orig.txt"];

NSLog(@"keyHexData: %@", keyHexData);
NSLog(@"testData:   %@", testData);
NSLog(@"clearData:  %@", clearData);

unsigned char keyBytes[16];
unsigned char *hex = (uint8_t *)keyHexData.bytes;

char byte_chars[3] = {'\0','\0','\0'};
for (int i=0; i<16; i++) {
    byte_chars[0] = hex[i*2];
    byte_chars[1] = hex[(i*2)+1];
    keyBytes[i] = strtol(byte_chars, NULL, 16);
}
NSData *keyData = [NSData dataWithBytes:keyBytes length:16];
NSLog(@"keyData:    %@", keyData);

NSData *ivData = [NSData dataWithBytes:(char []){0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0} length:16];
NSLog(@"ivData:     %@", ivData);

CCCryptorStatus ccStatus   = kCCSuccess;
size_t          cryptBytes = 0;    // Number of bytes moved to buffer.
NSMutableData  *clearOut   = [NSMutableData dataWithLength:testData.length];

ccStatus = CCCrypt(kCCDecrypt,
                   kCCAlgorithmAES128,
                   kCCOptionPKCS7Padding,
                   keyData.bytes, 
                   kCCKeySizeAES128,
                   ivData.bytes,
                   testData.bytes,
                   testData.length,
                   clearOut.mutableBytes,
                   clearOut.length,
                   &cryptBytes);

if (ccStatus != kCCSuccess) {
    NSLog(@"CCCrypt status: %d", ccStatus);
}

clearOut.length = cryptBytes;
NSLog(@"clearOut:   %@", clearOut);
keyHexData: <41393641 34344436 31343245 43463546 33444339 30303038 46453941 34383838>
testData:   <86a8b306 0f33db02 01e77e66 af5bcb3a>
clearData:  <31323334 35363738 39303132 33343536>
keyData:    <a96a44d6 142ecf5f 3dc90008 fe9a4888>
ivData:     <00000000 00000000 00000000 00000000>
clearOut:   <31323334 35363738 39303132 33343536>

请注意,clearData 已恢复为 clearOut。
这演示了使用 OpenSSL 加密,并使用 CommonCrypto 解密的过程。需要解决的问题:
1) 需要添加 Base64。

这是完成所需加密的起点。

1
原来PKCS5和PKCS7使用相同的算法,因此在填充方面,openssl和ComonCrypto是兼容的。 - zaph
2
有趣的是,许多只使用CommonCrypto的人并不真正知道发送到CCCrypt的密钥应该是原始字节。感谢这个答案。 - Tudor

4
你向 CCCrypt 提供了密钥和 IV 的 UTF8 表示,但该函数需要原始字节。如果你以字符串形式存储密钥和 IV,其中字符串是字节的十六进制表示,则需要将这个十六进制数字字符串转换回原始字节。
例如,让我们看看所有零字节的 IV。
你提供了 OpenSSL IV "00000000000000000000000000000000"。OpenSSL 将该字符串转换为每两个十六进制数字对应的一个字节,并得到这 16 个字节。
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  (This is an array of 16 zero bytes)

然而,您需要获取IV字符串并获取其UTF8表示。

// fyi, I plan to replace this later with a random iv
NSData *ivData = [@"00000000000000000000000000000000" dataUsingEncoding:NSUTF8StringEncoding];

您最终得到的是这些字节:
30 30 30 30 30 30 30 30 ...

因为UTF8中字符'0'的十六进制表示是0x30。所以把你的十六进制表示转换成字节,而不是UTF8字符,这样你的密钥和IV就会与OpenSSL的匹配。
OpenSSL有一个名为set_hex的函数,它可以将字符串转换为字节(在C中以unsigned char数组形式保存)。
#define BIO_printf fprintf
#define bio_err stderr
int set_hex(char *in, unsigned char *out, int size)
        {
        int i,n;
        unsigned char j;

        n=strlen(in);
        if (n > (size*2))
                {
                BIO_printf(bio_err,"hex string is too long\n");
                return(0);
                }
        memset(out,0,size);
        for (i=0; i<n; i++)
                {
                j=(unsigned char)*in;
                *(in++)='\0';
                if (j == 0) break;
                if ((j >= '0') && (j <= '9'))
                        j-='0';
                else if ((j >= 'A') && (j <= 'F'))
                        j=j-'A'+10;
                else if ((j >= 'a') && (j <= 'f'))
                        j=j-'a'+10;
                else
                        {
                        BIO_printf(bio_err,"non-hex digit\n");
                        return(0);
                        }
                if (i&1)
                        out[i/2]|=j;
                else
                        out[i/2]=(j<<4);
                }
        return(1);
        }

E.g.,

    char iv_str[] = "12345678901234567890123456789012";
    unsigned char iv[16];
    if( !set_hex(iv_str, iv, sizeof(iv)) )
    {
        // Handle error where string was not a well-formed IV
    }
    printf("IV: "); for(int i=0;i<sizeof(iv); ++i) { printf("%02x", iv[i]); } printf("\n");

是的!这正是发生的事情。那么我该如何将十六进制字符串转换为正确的字节呢? - David Nix
@galacticfury:我已经添加了OpenSSL使用的函数。他们的实现相当古老,但如果你想自己实现它,算法很简单。对于字符串中的每个十六进制字符,将字符转换为十进制(0-15)。如果十六进制字符是上半字节,则将其乘以16。如果是下半字节,则将该值添加到上半字节中,这样就得到了一个完整的字节。 - indiv
不幸的是,这个例程不能工作并且会收到一个“EXE_BAD_ACCESS”信号。CocoaFu示例中有五行代码用于转换密钥。 - zaph
1
@CocoaFu:例程已经可用。我在编辑中修复了我的“如何使用”示例,因此不再传递不可变的输入字符串(const char *)。因为OpenSSL代码在转换后会清除输入数组,以便不会在内存中留下明文密钥和IV的副本。因此,如果您遇到EXC_BAD_ACCESS错误,例如,您无法在崩溃转储或核心文件上运行“strings”实用程序以获取方便打印的加密密钥副本(理论上)。 OpenSSL解决方案只是十六进制基数转换的实现。 - indiv
@indiv:抱歉,在你和CocoaFu之间做出了艰难的选择。虽然还无法解码,但已经成功转换为十六进制。CocoaFu的十六进制转换方法更简单。现在我会像CocoaFu建议的那样,使用简单数据进行增量测试,这也是我一开始应该做的。非常感谢你的回答! - David Nix

2

对我来说,你的00ff349830193845af43984758690213看起来更像是十六进制数字,而不是Base64。由于有16个字节的十六进制数据,很可能这是以十六进制表示的IV向量,而不是Base64。


00ff349830193845af43984758690213是密钥,它是十六进制的。IV也是十六进制的。我正在尝试解密的字符串是Base64编码的。 - David Nix

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接