我正在尝试解决的问题:在我的Cocoa应用程序中,我需要使用对称密码加密字符串,将其POST到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;
}
?>
decodeData总是乱七八糟。
我试过反转过程,将编码输出从PHP发送到Cocoa但EVP_DecryptFinal()失败,这导致我相信某处存在NULL填充问题。我已经阅读并重新阅读了PHP和OpenSSL文档,但现在它们都在一起模糊,我没有想法尝试。
答案 0 :(得分:3)
我认为你的问题是从密钥字符串中导出原始加密密钥的方法在双方是不同的。 php md5()函数返回一个十六进制字符串,即'a476c3 ...',你正在砍掉密钥大小,而EVP_BytesToKey()是一个相当复杂的哈希例程,它返回一个原始字节字符串。它可能,提供的参数简化为原始MD5哈希,但我不能说。无论哪种方式,它都会与php哈希不同。
如果将php更改为md5(“ThisIsMyKey”,TRUE),则会为您提供原始md5哈希值。在可可方面,SSCrypto的+ getMD5ForData:方法应该为相同的字符串生成相同的字符串(文本编码问题除外)。
编辑1:如果php字符串和Cocoa数据打印相同,它们在字节级别仍然不同。 php字符串是十六进制编码的(即只包含字符0-9和a-f),而cocoa数据是原始字节(尽管NSData在NSLogged时有用地打印出其内容的十六进制编码字符串)。您仍然需要将第二个TRUE参数添加到php的md5()函数中以获取原始字节字符串。
编辑2:OpenSSL 1.1.0c changed the digest algorithm在某些内部组件中使用。以前使用MD5,1.1.0切换到SHA256。请注意,更改不会影响EVP_BytesToKey
和openssl enc
等命令。
答案 1 :(得分:1)
我发现了我的问题。简短的回答:在Cocoa和PHP下使用的密钥长度不同。答案很长......
我最初的询问是使用Blowfish / CBC,这是一个从16个字节到56个的可变密钥长度密码。由于Boaz的想法是关键是某种方式应该责备,我切换到TripleDES作为密码,因为它使用固定密钥长度为24个字节。然后我发现了一个问题:Cocoa / EVP_BytesToKey()返回的密钥长度为24个字节,但是md5()哈希值返回的值只有16个。
问题的解决方案是让PHP以与EVP_BytesToKey
相同的方式创建密钥,直到输出长度至少为(cipherKeyLength + cipherIVLength)。以下PHP就是这样做的(忽略任何salt或count迭代)
$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填充。查看pkcs5_pad
和pkcs5_unpad
的评论http://us3.php.net/manual/en/ref.mcrypt.php,以添加和删除所述填充。
基本上,取密钥的原始md5值,如果不够长,则将密钥附加到md5结果,然后再将md5附加到该字符串。洗涤,冲洗,重复。 EVP_BytesToKey()的手册页解释了它实际做了什么,并显示了如果需要,可以放置盐值的位置。这种重新生成密钥的方法也正确地重新生成初始化向量(iv),因此没有必要将其传递。
但是Blowfish怎么样?
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可以满足我的需求。