2

我试图解决的情况:在我的 Cocoa 应用程序中,我需要使用对称密码加密一个字符串,将其发布到 PHP,并让该脚本解码数据。该过程需要反向工作以返回答案(PHP 编码,Cocoa 解码)。

我遗漏了一些东西,因为即使我可以在 PHP 和 Cocoa 中使密钥和初始化向量 (iv) 相同,但当一个应用程序将其编码数据发送到另一个应用程序时,解码永远不会起作用。两者都可以很好地编码/解码自己的数据(经过验证以确保手头没有一些 PEBKAC 问题)。我怀疑某处存在填充问题,我只是没有看到。

我的 cocoa 应用程序使用 SSCrypto 进行编码(它只是 OpenSSL 函数的一个方便的包装器)。密码是 Blowfish,模式是 CBC。(请原谅内存泄漏,代码已被剥离为基本要素)

NSData *secretText = [@"secretTextToEncode" dataUsingEncoding:NSUTF8StringEncoding];
NSData *symmetricKey = [@"ThisIsMyKey" dataUsingEncoding:NSUTF8StringEncoding];

unsigned char *input = (unsigned char *)[secretText bytes];
unsigned char *outbuf;
int outlen, templen, inlen;
inlen = [secretText length];

unsigned char evp_key[EVP_MAX_KEY_LENGTH] = {"\0"};
int cipherMaxIVLength = EVP_MAX_IV_LENGTH;
EVP_CIPHER_CTX cCtx;
const EVP_CIPHER *cipher = EVP_bf_cbc();

cipherMaxIVLength = EVP_CIPHER_iv_length( cipher );
unsigned char iv[cipherMaxIVLength];

EVP_BytesToKey(cipher, EVP_md5(), NULL, [symmetricKey bytes], [symmetricKey length], 1, evp_key, iv);

NSData *initVector = [NSData dataWithBytes:iv length:cipherMaxIVLength];

EVP_CIPHER_CTX_init(&cCtx);

if (!EVP_EncryptInit_ex(&cCtx, cipher, NULL, evp_key, iv)) {
    EVP_CIPHER_CTX_cleanup(&cCtx);
    return nil;
}
int ctx_CipherKeyLength = EVP_CIPHER_CTX_key_length( &cCtx );
EVP_CIPHER_CTX_set_key_length(&cCtx, ctx_CipherKeyLength);

outbuf = (unsigned char *)calloc(inlen + EVP_CIPHER_CTX_block_size(&cCtx), sizeof(unsigned char));

if (!EVP_EncryptUpdate(&cCtx, outbuf, &outlen, input, inlen)){
    EVP_CIPHER_CTX_cleanup(&cCtx);
    return nil;
}
if (!EVP_EncryptFinal(&cCtx, outbuf + outlen, &templen)){
    EVP_CIPHER_CTX_cleanup(&cCtx);
    return nil;
}
outlen += templen;
EVP_CIPHER_CTX_cleanup(&cCtx);

NSData *cipherText = [NSData dataWithBytes:outbuf length:outlen];

NSString *base64String = [cipherText encodeBase64WithNewlines:NO];
NSString *iv = [initVector encodeBase64WithNewlines:NO];

然后将 base64String 和 iv 发布到 PHP 以尝试对其进行解码:

<?php

import_request_variables( "p", "p_" );

if( $p_data != "" && $p_iv != "" )
{
    $encodedData = base64_decode( $p_data, true );
    $iv = base64_decode( $p_iv, true );

    $td = mcrypt_module_open( MCRYPT_BLOWFISH, '', MCRYPT_MODE_CBC, '' );
    $keySize = mcrypt_enc_get_key_size( $td );
    $key = substr( md5( "ThisIsMyKey" ), 0, $keySize );

    $decodedData = mcrypt_decrypt(MCRYPT_BLOWFISH, $key, $encodedData, MCRYPT_MODE_CBC, $iv );
    mcrypt_module_close( $td );

    echo "decoded: " . $decodedData;
}
?>

decodedData 总是乱码。

我已经尝试反转该过程,将编码输出从 PHP 发送到 Cocoa,但 EVP_DecryptFinal() 失败,这让我相信某处存在 NULL 填充问题。我已经阅读并重新阅读了 PHP 和 OpenSSL 文档,但现在它们都模糊不清了,我没有想法可以尝试。

4

2 回答 2

3

我认为您的问题是从密钥字符串派生原始加密密钥的方法在两侧不同。php md5() 函数返回一个十六进制字符串,即“a476c3...”,您将其缩减为密钥大小,而 EVP_BytesToKey() 是一个相当复杂的哈希例程,它返回一个原始字节字符串。它可能会通过提供的参数简化为原始 MD5 哈希,但我无法确定。无论哪种方式,它都将不同于 php 哈希。

如果您将 php 更改为 md5("ThisIsMyKey", TRUE),这将为您提供原始 md5 哈希。在可可方面,SSCrypto 的 +getMD5ForData: 方法应该为相同的字符串生成相同的字符串(文本编码问题除外)。

编辑 1:如果 php 字符串和 Cocoa 数据打印出来相同,它们在字节级别仍然不同。php 字符串是十六进制编码的(即仅包含字符 0-9 和 af),而可可数据是原始字节(尽管 NSData 在 NSLogged 时会打印出其内容的十六进制编码字符串)。您仍然需要将第二个 TRUE 参数添加到 php 的 md5() 函数以获取原始字节字符串。

编辑 2:OpenSSL 1.1.0c 更改了一些内部组件中使用的摘要算法。以前使用MD5,1.1.0改用SHA256。请注意,更改不会影响您EVP_BytesToKeyopenssl enc.

于 2008-11-26T17:58:31.357 回答
1

我发现了我的问题。简短的回答:在 Cocoa 和 PHP 下使用的密钥长度不同。长答案...

我最初的查询是使用 Blowfish/CBC,它是一个从 16 字节到 56 字节的可变密钥长度密码。出于 Boaz 认为密钥在某种程度上应该受到责备的想法,我切换到 TripleDES 作为密码,因为它使用 24 的固定密钥长度字节。这时我注意到一个问题:Cocoa/EVP_BytesToKey() 返回的密钥长度为 24 字节,但 md5() 对我的密钥进行散列后返回的值只有 16。

该问题的解决方案是让 PHP 以相同的方式创建一个密钥,EVP_BytesToKey直到输出长度至少为 (cipherKeyLength + cipherIVLength)。以下 PHP 就是这样做的(忽略任何盐或计数迭代)

$cipher = MCRYPT_TRIPLEDES;
$cipherMode = MCRYPT_MODE_CBC;

$keySize   = mcrypt_get_key_size( $cipher, $cipherMode );
$ivSize    = mcrypt_get_iv_size( $cipher, $cipherMode );

$rawKey = "ThisIsMyKey";
$genKeyData = '';
do
{
    $genKeyData = $genKeyData.md5( $genKeyData.$rawKey, true );
} while( strlen( $genKeyData ) < ($keySize + $ivSize) );

$generatedKey = substr( $genKeyData, 0, $keySize );
$generatedIV  = substr( $genKeyData, $keySize, $ivSize );

$output = mcrypt_decrypt( $cipher, $generatedKey, $encodedData, $cipherMode, $generatedIV );

echo "output (hex)" . bin2hex($output);

请注意,该输出的末尾很可能有 PKCS#5 填充。在这里查看评论http://us3.php.net/manual/en/ref.mcrypt.php用于添加pkcs5_padpkcs5_unpad删除所述填充。

基本上,获取密钥的原始 md5 值,如果不够长,则将密钥附加到 md5 结果并再次 md5 该字符串。洗涤,冲洗,重复。EVP_BytesToKey() 的手册页解释了它实际在做什么,并在需要时显示了将盐值放在哪里。这种重新生成密钥的方法也正确地重新生成了初始化向量 (iv),因此没有必要传递它。

但是河豚呢?

EVP_BytesToKey()返回密码可能的最小密钥,因为它不接受作为密钥大小基础的上下文。所以默认大小就是你得到的全部,对于 Blowfish 来说是 16 字节。 mcrypt_get_key_size(),另一方面,返回最大可能的密钥大小。所以我的原始代码中的以下几行:

$keySize = mcrypt_enc_get_key_size( $td );
$key = substr( md5( "ThisIsMyKey" ), 0, $keySize );

将始终返回 32 个字符的密钥,因为 $keySize 设置为 56。将上面的代码更改为:

$cipher = MCRYPT_BLOWFISH;
$cipherMode = MCRYPT_MODE_CBC;

$keySize   = 16;

允许河豚正确解码,但几乎破坏了可变长度密钥的好处。总而言之,EVP_BytesToKey()当涉及到可变密钥长度密码时,它被打破了。使用可变密钥密码时,您需要以不同的方式创建密钥/iv。我没有深入研究它,因为 3DES 可以满足我的需要。

于 2008-12-01T18:27:00.897 回答