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

14

我在 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 6 Jan 2014 (在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进程阻碍了该函数——我已经添加了这些标记。

现在不太确定该往哪里走了...

第二次更新

我进行了更多测试。我将测试脚本放在运行php-fpm的nginx后面。奇怪的是,要么a)它100%失败,要么b)它没有失败,而不是两者都有一点结果。这使我相信是nginx或php-fpm是罪魁祸首。


密钥和IV的长度是否完全正确,其中密钥为128位、192位或256位之一,IV为16字节? - zaph
你可以看到我的IV有多长 - 它恰好是16个字节。我使用的密钥是128位(16个字符)。 - Tyler Sebastian
我将密钥设置为“abce…”,共16个字符。 - Tyler Sebastian
@zaph 我添加了额外的检查,openssl_random_pseudo_bytes(16) 不会返回 FALSE - Tyler Sebastian
在for循环中添加sleep(5);,看看是否仍然会有50%的失败率? - peterpeterson
显示剩余3条评论
3个回答

8
这似乎是一个OpenSSL错误。您应确保在同一进程空间中仅使用一个OpenSSL对象。要验证,请运行测试脚本,以便它是唯一使用OpenSSL的脚本。它是否仍然有50%的失败率?或者失败只发生在对脚本进行多个并发访问时?如果仍然发生,那么几乎肯定是php-fpm中的一个bug - 它实例化函数,并且直到出现错误才清除其数据区。在这种情况下,我期望它每两次调用一次失败,而不是"平均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函数。我发现这篇简短(很可能过时的)回复,似乎表明某些地方有一些facepalm juice(译注:意思是“脸掌声汁”,表示非常尴尬或愚蠢的情况)在配方中。但这并不能解释为什么该函数每两次就会失败一次。

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

我有一个牵强附会的假设,即函数返回类似于0或81的东西(因为这两个字符串都在您的cipherlist输出中,索引为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扩展没有建立这些相同的回调,则可能会导致调用错误的回调。
如果时间允许,我将进行下一轮测试,以在上述故障点中放置警报(以静态系统日志调用的形式),以精确地确定哪个测试失败...前提是我能够在虚拟机上安装与您相同的设置,并且我可以重现相同的奇怪行为。请注意保留HTML标签。

测试脚本在通过PHP CLI运行时从未失败。然而,我想尝试通过fastcgi运行它(我只是将其放在nginx后面),并且我正在经历与以前相同的行为 - 50%的时间失败。 - Tyler Sebastian
你能验证一下它是每两个调用失败一次,还是更随机吗?明天我可能会看一下PHP源代码。 - LSerni
请看下面的我的“解决方案” - 我给你这个赏金是因为你对此问题进行了深入的研究。 - Tyler Sebastian

4

我可以展示如何使用openssl_encrypt和decrypt,我正在使用Silex框架,目前没有任何问题。希望这能帮到你。

我知道这不是最好的解决方案,但也许能对你有所帮助。

$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;

抱歉 - 没有成功。我几乎逐行复制了加密部分(包括示例密钥/iv),但仍然出现错误。 - Tyler Sebastian
你在哪个平台上运行? - Tyler Sebastian
在 Windows 10 上使用 Apache XAMPP 和 PHP 5.6。这就是您需要的吗? - Slico
完美,谢谢。虽然这并没有帮助我,但我只是在想重叠的问题。 - Tyler Sebastian

0
我们通过在服务器上“更新”openssl来解决了这个问题 - OpenSSL 1.0.1f 6 Jan 2014 - 是的,那就是我上面列出的确切版本,我也不明白。然而,这更加奇怪 - 因为在本地(在一个Linux虚拟机上 - 我提供了与我们的生产环境完全相同的配置),它可以完美地工作。对于任何遇到相同问题的人,看到这篇文章时感到失落,我很抱歉 - 我的解决方案对我有效,但我并不理解其中的任何内容。

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