我在PHP中使用openssl_*
方法看到了一些奇怪的行为。 50%的时间,它会失败,抛出Unknown cipher algorithm
,另外50%的时间,它将正确编码我的数据。这是我的代码中的相关代码段:
$iv = openssl_random_pseudo_bytes(16);
$hash = openssl_encrypt($raw, "AES-128-CBC", $hashing_secret, OPENSSL_RAW_DATA, $iv);
// send $iv.$hash
使用openssl_get_cipher_methods
给了我:
[0] => AES-128-CBC
...
[81] => aes-128-cbc
所以我知道密码可用。此外,$ openssl ciphers
将AES-128-CBC列为系统级别的可用密码(但是,我已经被告知PHP捆绑的openssl是独立的)
我正在运行Ubuntu 14.04,php5.5.9-1ubuntu4.14,openssl 1.0.1f 2014年1月6日(phpinfo中列出的版本是相同的)。如果相关,则所有这些代码都通过nginx / php-fpm在Silex框架下运行。
更新:更多信息......
我做了更多测试。我写了一个小脚本,它只循环了x次,编码了一些数据。
set_error_handler(function() use (&$errorCount) {
$errorCount++;
});
for ($i = 0; $i < $numTests; $i++) {
$hash = openssl_encrypt($data, "AES-128-CBC", $hashing_secret, OPENSSL_RAW_DATA, $iv);
}
如果我在同一台服务器上运行(通过php test.php
),它会一直运行 - 即每次$errorCount == 0
。这让我相信它:a)silex或b)阻碍函数的fastcgi进程 - 我已经添加了这些标记。
现在不确定从哪里开始,但是......
第二次更新
我做了一些测试。我把测试脚本放在nginx后面,运行php-fpm。这里奇怪的是,a)它100%失败或b)失败0次,而不是两次结果。这让我相信它的nginx或php-fpm是罪魁祸首。
答案 0 :(得分:7)
这看起来可能是OpenSSL误锁定错误。您应确保在同一进程空间中一次只能使用一个OpenSSL对象。
要验证,请运行测试脚本,以便它是唯一使用OpenSSL的脚本。 50%的时间是否仍然失败?或者仅在对脚本进行多次并发访问时才会发生故障?
如果它仍然发生,它几乎必须是php-fpm中的错误 - 它正在实例化该函数,并且在发生错误之前不能正确清除其数据区域。在那种情况下,我预计每两次通话就会失败一次,而不是“平均50%”,而是每次偶数通话一次。在那个的情况下,我会尝试使用不同版本的OpenSSL。
要锁定openssl,您可以尝试使用flock并实例化一个锁定文件以供SSL功能使用(首先检查锁定是否可用,然后运行该功能并解锁)。试试这个,看它是否有效。如果是这样,您可以查看更有效的方法 - 例如,您可以使用MySQL LOCK(),或semaphore(如果可用)。
5.5.9中的行为不当函数可以在ext/openssl/openssl.c
中找到,并且抛出的错误是初步检查之一。还没有惊喜:
/* {{{ proto string openssl_encrypt(string data, string method, string password [, long options=0 [, string $iv='']])
Encrypts given data with given method and key, returns raw or base64 encoded string */
PHP_FUNCTION(openssl_encrypt)
{
long options = 0;
char *data, *method, *password, *iv = "";
int data_len, method_len, password_len, iv_len = 0, max_iv_len;
const EVP_CIPHER *cipher_type;
EVP_CIPHER_CTX cipher_ctx;
int i=0, outlen, keylen;
unsigned char *outbuf, *key;
zend_bool free_iv;
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sss|ls", &data, &data_len, &method, &method_len, &password, &password_len, &options, &iv, &iv_len) == FAILURE) {
return;
}
cipher_type = EVP_get_cipherbyname(method);
if (!cipher_type) {
php_error_docref(NULL TSRMLS_CC, E_WARNING, "Unknown cipher algorithm");
RETURN_FALSE;
}
所以我们可以假设EVP_get_cipherbyname(method)
正在回归虚假。
除了它是标准的SSL功能。我发现这个简洁(很可能已经过时)reply这似乎表明在某个地方的配方中有一些facepalm果汁。但这并不能解释为什么函数每两次失败一次。
该功能为here on github。它初始化OpenSSL,并通过ancillary function获取方法名称,该名称将返回指向非空内存的指针。
我有一个牵强的假设,即函数随机返回类似于0或81的东西(因为两个字符串都在你的密码输出中,索引为0和81),0等于NULL,因此失败了。看来它不能像那样工作,它也应该在CLI中这样做。 但只是为了确保,请验证是否只有那个特定的密码失败(例如AES-256-CBC正常工作)。
另一种可能性是OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS, NULL)
调用失败了。如果此测试(在Ubuntu上;其他平台的行为不同)失败,则会发生这种情况:
int CRYPTO_THREAD_run_once(CRYPTO_ONCE *once, void (*init)(void))
{
if (pthread_once(once, init) != 0)
return 0;
return 1;
}
这再次表明libcrypto内部存在一些共享资源冲突。
作为另一个测试,我建议你不要调用随机字节IV初始化并尝试使用固定的IV;那是因为我偶然发现了this note,这指的是与我想的略有不同的资源,但足够接近让我做出反应:
看来openssl_random_pseudo_bytes()调用openssl, 导致底层libcrypto调用一个回调 之前由PostgreSQL库建立的锁定的一部分 openssl的多线程的可移植性回调。
可在此处找到有关该主题的一些信息 http://wiki.openssl.org/index.php/Manual:Threads(3)
如果HHVM openssl扩展没有建立这些相同的回调,则可能导致调用错误的回调。
在时间允许的情况下,我要执行的下一个测试是在上述故障点中发出警报(以静态系统日志调用的形式),以准确地确定哪个测试失败...如果我可以在VM上安装相同的设置,我可以重现相同的奇怪行为。
答案 1 :(得分:3)
我可以告诉你我如何使用openssl_encrypt并解密我正在使用Silex Framework,而且我现在没有任何问题。我希望它有所帮助。
我知道这不是最好的解决方案,但也许可以帮助你
$encrypt_method = "aes128";
$secret_key = 'd2ae49e3b63ed418b9fc25105cd964d4';
$secret_iv = 'fb68e879fab1db2a2ce30dbf6f9b3743';
$key = hash('sha256', $secret_key);
$iv = substr(hash('sha256', $secret_iv), 0, 16);
$output = openssl_encrypt($str, $encrypt_method, $key, 0, $iv);
return base64_encode($output);
$output = openssl_decrypt(base64_decode($str), $encrypt_method, $key, 0, $iv);
return $output;
答案 2 :(得分:0)
我们通过在服务器上“更新”openssl来解决这个问题 - OpenSSL 1.0.1f 6 Jan 2014
- 是的,这就是我上面列出的确切版本,我也不明白。但这有点奇怪 - 因为本地(在Linux VM上 - 我配置为与我们的生产环境相同)它完美地工作。我很抱歉遇到同样问题的人在最黑暗的时刻偶然发现 - 我的解决方案对我有用,但我不明白。