openssl加密/解密不一致工作/失败


openssl encypt/decrypt inconsistently working/failing

我看到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是罪魁祸首。

这看起来可能是OpenSSL错误锁定。您应该确保在同一进程空间中的任何时候都只能使用一个OpenSSL对象。

要进行验证,请运行测试脚本,使其成为唯一使用OpenSSL的脚本。它仍然有50%的时间失败吗?还是只有在多次并发访问脚本时才会发生故障?

如果这种情况仍然发生,那么它几乎肯定是php-fpm中的一个错误——它正在实例化函数,并且在出现错误之前没有正确清除其数据区域。在这种情况下,我预计它每两次呼叫就会失败一次,而不是"平均50%",而是每偶数次呼叫都会失败一次。在情况下,我会尝试使用不同版本的OpenSSL。

要锁定openssl,可以尝试使用flock并实例化一个锁文件供SSL函数使用(首先检查锁是否可用,然后运行该函数并解锁)。试试这个,看看它是否有效。如果是这样,您可以寻找一种更有效的方法来实现它——例如,您可以使用MySQL LOCK(),或者信号量(如果可用的话)。

Spelunking

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函数。我发现这个简洁(很可能已经过时)的回复似乎表明食谱中有一些掌子汁。但这并不能解释为什么函数应该每两次失败一次。

函数在github上。它初始化OpenSSL,并通过一个辅助函数获得方法名称,该函数将返回一个指向非空内存的指针。

我有一个牵强的假设,即函数随机返回类似于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;这是因为我也偶然发现了这张纸条,它指向了一个与我想象的略有不同的资源,但距离足够近,足以让我做出反应:

似乎调用openssl的openssl_random_pseudo_bytes(),导致底层libcrypto调用一个回调以前由PostgreSQL库作为锁的一部分建立openssl多线程的可移植性回调。

有关该主题的一些信息可以在此处找到http://wiki.openssl.org/index.php/Manual:Threads(3)

如果HHVM openssl扩展没有建立这些相同的回调,则可能会导致调用错误的回调。

如果时间允许,我将要执行的下一个测试是在上面提到的故障点中放置警报(以静态syslog调用的形式),以准确地确定哪个测试失败。。。前提是我可以在虚拟机上安装与您相同的设置,并且我可以重现相同的怪异行为。

我可以向您展示我是如何使用openssl_encrypt和decrypt的,我使用的是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;

我们通过在服务器上"更新"openssl来解决这个问题-OpenSSL 1.0.1f 6 Jan 2014-是的,这正是我上面列出的版本,我也不明白。不过,这特别奇怪——因为在本地(在linux虚拟机上——我将其设置为与我们的生产环境相同),它运行得很好。我很抱歉任何遇到同样问题的人在你最黑暗的时刻偶然发现了这一点-我的解决方案对我有效,但我一点都不理解。