Подтвердить что ты не робот

Openssl encypt/decrypt непостоянно работает/не работает

Я вижу какое-то странное поведение с методами openssl_* в PHP. В 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 января 2014 года (версия, указанная в phpinfo, такая же). Если это актуально, весь этот код работает под инфраструктурой Silex через nginx/php-fpm.

Обновление: немного больше информации...

Я провел еще несколько тестов. Я написал небольшой script, который просто зацикливается на 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, который препятствует функции - я добавил теги.

Не совсем уверен, куда идти отсюда, теперь, хотя...

Второе обновление

Я сделал немного больше тестов. Я запустил тест script за nginx, запустив php-fpm. Странная вещь здесь заключается в том, что либо: а) она терпит неудачу в 100% случаев, либо b) она не работает 0 раз, а не немного из обоих результатов. Это заставляет меня думать, что это nginx или php-fpm, что виновник.

4b9b3361

Ответ 1

Похоже, что это может быть ошибка блокировки OpenSSL. Вы должны убедиться, что только один объект OpenSSL когда-либо используется в одно время в одном и том же пространстве процесса.

Чтобы проверить, запустите тест script, чтобы он был единственным, использующим OpenSSL. Он по-прежнему не работает в 50% случаев? Или это происходит только при одновременном доступе к script?

Если это все еще случается, оно почти должно быть ошибкой в ​​php-fpm - оно создает экземпляр функции и не очищает ее области данных до тех пор, пока не произойдет ошибка. В этом случае я ожидаю, что он будет терпеть неудачу один раз каждые два вызова, а не "50% в среднем", но ровно один раз каждый четный вызов. В этом случае я попробую использовать другую версию OpenSSL.

Чтобы заблокировать openssl, вы можете попробовать flock и создать экземпляр файла блокировки для использования функцией SSL (сначала вы проверите блокировку доступно, затем запустите функцию и разблокируйте). Попробуйте это и посмотрите, работает ли он. Если это так, вы можете изучить более эффективный способ сделать это - например, вы можете использовать семафор MySQL LOCK() или , если он доступен.

Спелеология

Неисправную функцию в 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 в рецепте где-то. Но это не объясняет, почему функция должна терпеть неудачу один раз в два.

Функция здесь, на 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_random_pseudo_bytes(), который вызывает openssl, заставляет базовый libcrypto вызывать обратный вызов, который был установленный ранее библиотекой PostgreSQL как часть блокировки обратные вызовы переносимости для многопоточности openssl.

Некоторые сведения по этой теме можно найти здесь http://wiki.openssl.org/index.php/Manual:Threads(3)

Если расширение HHVM openssl не устанавливает эти же обратные вызовы, это может вызывать неправильные обратные вызовы.

Следующие тесты, которые я собираюсь выполнить, если позволяют время, размещать предупреждения (в виде статических вызовов syslog) в вышеупомянутых точках отказа, чтобы точно определить, какой тест терпит неудачу... при условии, что я могу установить та же самая настройка, что и на виртуальной машине, и я могу воспроизвести одно и то же странное поведение.

Ответ 2

Я могу показать вам, как я использую 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;

Ответ 3

Мы вроде как решили это путем "обновления" openssl на наших серверах - OpenSSL 1.0.1f 6 Jan 2014 - да, что точная версия, указанная выше, я тоже не понимаю. Тем не менее это странно, потому что локально (на виртуальной машине Linux, которую я предусмотрел, чтобы быть идентичной нашей производственной среде), он работал отлично. Мне жаль, что кто-то испытывает те же проблемы, которые наткнулись на это в самый тёмный час - мое решение сработало для меня, но я ничего не понимаю.