mcrypt устарел, какова альтернатива?

103

Расширение mcrypt устарело и будет удалено в PHP 7.2 в соответствии с опубликованным здесь комментарием . Поэтому я ищу альтернативный способ шифрования паролей.

Сейчас я использую что-то вроде

mcrypt_encrypt(MCRYPT_RIJNDAEL_128, md5($key, true), $string, MCRYPT_MODE_CBC, $iv)

Мне нужно ваше мнение о лучшем / самом надежном способе шифрования паролей, зашифрованный пароль, конечно, должен поддерживаться PHP 7.xx и также должен быть дешифруемым, потому что мои клиенты действительно хотят иметь возможность `` восстанавливать '' свои пароли без создания нового один.

Пит
источник
9
Зачем нужно шифровать / расшифровывать пароли? Почему бы просто не хешировать их password_hashи не проверять password_verify?
Don't Panic
3
«зашифрованный пароль тоже должен поддаваться расшифровке» - почему? звучит не слишком безопасно. По какой-то особой причине?
Funk Forty Niner
24
«потому что мои клиенты действительно хотят иметь возможность« восстанавливать »свои пароли без создания нового». - Это небезопасно, и вместо этого им следует предоставить возможность сбросить свои пароли.
Funk Forty Niner
4
Не шифруйте пароли , когда злоумышленник получит БД, он также получит ключ шифрования. Обходите HMAC со случайной солью в течение примерно 100 мс и сохраните соль с помощью хеша. Используйте такие функции, как password_hash, PBKDF2, Bcrypt и аналогичные функции. Дело в том, чтобы злоумышленник тратил много времени на поиск паролей перебором.
zaph 08
2
Из руководства по php -> Эта функция УСТАРЕЛА с PHP 7.1.0. Полагаться на эту функцию крайне не рекомендуется. Альтернатива - натрий -> php.net/manual/en/book.sodium.php
MarcoZen

Ответы:

47

Лучше всего хешировать пароли, чтобы их нельзя было расшифровать. Это немного усложняет задачу злоумышленникам, которые могли получить доступ к вашей базе данных или файлам.

Если вам необходимо зашифровать свои данные и сделать их дешифруемыми, руководство по безопасному шифрованию / дешифрованию доступно по адресу https://paragonie.com/white-paper/2015-secure-php-data-encryption . Подводя итог этой ссылке:

  • Используйте Libsodium - расширение PHP
  • Если вы не можете использовать Libsodium, используйте defuse / php-encryption - Прямой код PHP
  • Если вы не можете использовать Libsodium или defuse / php-encryption, используйте OpenSSL - на многих серверах это уже установлено. Если нет, его можно скомпилировать с помощью --with-openssl [= DIR]
Фил
источник
1
Сначала следует попробовать openssl, потому что он очень распространен, а libsodium - нет. Raw php не следует использовать, если не используются все собственные расширения, если
JSON
Несмотря на то, что openssl очень распространен, похоже, что php 7 будет использовать libsodium для своей основной криптографической безопасности
.intelligence.com/news/…
1
Обратите внимание, что есть библиотека под названием Sodium-compat( github.com/paragonie/sodium_compat ), которая работает в PHP> = 5.2.4
RaelB
30

Как предлагает @rqLizard , вместо этого вы можете использовать функции openssl_encrypt/ openssl_decryptPHP, которые предоставляют гораздо лучшую альтернативу для реализации AES (расширенного стандарта шифрования), также известного как шифрование Rijndael.

Согласно следующему комментарию Скотта на php.net :

Если вы пишете код для шифрования / шифрования данных в 2015 году, вам следует использовать openssl_encrypt()и openssl_decrypt(). Базовая библиотека ( libmcrypt) заброшена с 2007 года и работает намного хуже, чем OpenSSL (который использует AES-NIсовременные процессоры и безопасен по времени кэширования).

Кроме того, MCRYPT_RIJNDAEL_256нет AES-256, это другой вариант блочного шифра Rijndael. Если вы хотите AES-256в mcrypt, вы должны использовать MCRYPT_RIJNDAEL_128с ключом 32 байта. OpenSSL делает более очевидным, какой режим вы используете (например, aes-128-cbcvs aes-256-ctr).

OpenSSL также использует заполнение PKCS7 в режиме CBC, а не заполнение байтов NULL в mcrypt. Таким образом, mcrypt с большей вероятностью сделает ваш код уязвимым для атак оракула заполнения, чем OpenSSL.

Наконец, если вы не аутентифицируете свои зашифрованные тексты (Encrypt Then MAC), вы делаете это неправильно.

Дальнейшее чтение:

Примеры кода

Пример # 1

Пример аутентифицированного шифрования AES в режиме GCM для PHP 7.1+

<?php
//$key should have been previously generated in a cryptographically safe way, like openssl_random_pseudo_bytes
$plaintext = "message to be encrypted";
$cipher = "aes-128-gcm";
if (in_array($cipher, openssl_get_cipher_methods()))
{
    $ivlen = openssl_cipher_iv_length($cipher);
    $iv = openssl_random_pseudo_bytes($ivlen);
    $ciphertext = openssl_encrypt($plaintext, $cipher, $key, $options=0, $iv, $tag);
    //store $cipher, $iv, and $tag for decryption later
    $original_plaintext = openssl_decrypt($ciphertext, $cipher, $key, $options=0, $iv, $tag);
    echo $original_plaintext."\n";
}
?>

Пример # 2

Пример аутентифицированного шифрования AES для PHP 5.6+

<?php
//$key previously generated safely, ie: openssl_random_pseudo_bytes
$plaintext = "message to be encrypted";
$ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC");
$iv = openssl_random_pseudo_bytes($ivlen);
$ciphertext_raw = openssl_encrypt($plaintext, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
$hmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true);
$ciphertext = base64_encode( $iv.$hmac.$ciphertext_raw );

//decrypt later....
$c = base64_decode($ciphertext);
$ivlen = openssl_cipher_iv_length($cipher="AES-128-CBC");
$iv = substr($c, 0, $ivlen);
$hmac = substr($c, $ivlen, $sha2len=32);
$ciphertext_raw = substr($c, $ivlen+$sha2len);
$original_plaintext = openssl_decrypt($ciphertext_raw, $cipher, $key, $options=OPENSSL_RAW_DATA, $iv);
$calcmac = hash_hmac('sha256', $ciphertext_raw, $key, $as_binary=true);
if (hash_equals($hmac, $calcmac))//PHP 5.6+ timing attack safe comparison
{
    echo $original_plaintext."\n";
}
?>

Пример # 3

На основе приведенных выше примеров я изменил следующий код, который нацелен на шифрование идентификатора сеанса пользователя:

class Session {

  /**
   * Encrypts the session ID and returns it as a base 64 encoded string.
   *
   * @param $session_id
   * @return string
   */
  public function encrypt($session_id) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Encrypt the session ID.
    $encrypt = mcrypt_encrypt(MCRYPT_RIJNDAEL_128, $key, $session_id, MCRYPT_MODE_CBC, $iv);
    // Base 64 encode the encrypted session ID.
    $encryptedSessionId = base64_encode($encrypt);
    // Return it.
    return $encryptedSessionId;
  }

  /**
   * Decrypts a base 64 encoded encrypted session ID back to its original form.
   *
   * @param $encryptedSessionId
   * @return string
   */
  public function decrypt($encryptedSessionId) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Decode the encrypted session ID from base 64.
    $decoded = base64_decode($encryptedSessionId);
    // Decrypt the string.
    $decryptedSessionId = mcrypt_decrypt(MCRYPT_RIJNDAEL_128, $key, $decoded, MCRYPT_MODE_CBC, $iv);
    // Trim the whitespace from the end.
    $session_id = rtrim($decryptedSessionId, "\0");
    // Return it.
    return $session_id;
  }

  public function _getIv() {
    return md5($this->_getSalt());
  }

  public function _getSalt() {
    return md5($this->drupal->drupalGetHashSalt());
  }

}

в:

class Session {

  const SESS_CIPHER = 'aes-128-cbc';

  /**
   * Encrypts the session ID and returns it as a base 64 encoded string.
   *
   * @param $session_id
   * @return string
   */
  public function encrypt($session_id) {
    // Get the MD5 hash salt as a key.
    $key = $this->_getSalt();
    // For an easy iv, MD5 the salt again.
    $iv = $this->_getIv();
    // Encrypt the session ID.
    $ciphertext = openssl_encrypt($session_id, self::SESS_CIPHER, $key, $options=OPENSSL_RAW_DATA, $iv);
    // Base 64 encode the encrypted session ID.
    $encryptedSessionId = base64_encode($ciphertext);
    // Return it.
    return $encryptedSessionId;
  }

  /**
   * Decrypts a base 64 encoded encrypted session ID back to its original form.
   *
   * @param $encryptedSessionId
   * @return string
   */
  public function decrypt($encryptedSessionId) {
    // Get the Drupal hash salt as a key.
    $key = $this->_getSalt();
    // Get the iv.
    $iv = $this->_getIv();
    // Decode the encrypted session ID from base 64.
    $decoded = base64_decode($encryptedSessionId, TRUE);
    // Decrypt the string.
    $decryptedSessionId = openssl_decrypt($decoded, self::SESS_CIPHER, $key, $options=OPENSSL_RAW_DATA, $iv);
    // Trim the whitespace from the end.
    $session_id = rtrim($decryptedSessionId, '\0');
    // Return it.
    return $session_id;
  }

  public function _getIv() {
    $ivlen = openssl_cipher_iv_length(self::SESS_CIPHER);
    return substr(md5($this->_getSalt()), 0, $ivlen);
  }

  public function _getSalt() {
    return $this->drupal->drupalGetHashSalt();
  }

}

Чтобы уточнить, вышеупомянутое изменение не является истинным преобразованием, поскольку два шифрования используют разный размер блока и разные зашифрованные данные. Кроме того, заполнение по умолчанию отличается, MCRYPT_RIJNDAELподдерживается только нестандартное заполнение нулями. @zaph


Дополнительные примечания (из комментариев @ zaph):

  • Rijndael 128 ( MCRYPT_RIJNDAEL_128) это эквивалентно AES , однако Rijndael 256 ( MCRYPT_RIJNDAEL_256) не AES-256 , как 256 определяет размер блока 256 бит, в то время как AES имеет только один размер блока: 128 бит. Итак, в основном Rijndael с размером блока 256 бит ( MCRYPT_RIJNDAEL_256) был ошибочно назван из-за выбора разработчиков mcrypt . @zaph
  • Rijndael с размером блока 256 может быть менее безопасным, чем с размером блока 128 бит, потому что у последнего было гораздо больше обзоров и применений. Во-вторых, совместимость затрудняется тем, что в то время как AES обычно доступен, а Rijndael с размером блока 256 бит - нет.
  • Шифрование с разными размерами блоков для Rijndael производит разные зашифрованные данные.

    Например, MCRYPT_RIJNDAEL_256(не эквивалентно AES-256) определяет другой вариант блочного шифра Rijndael с размером 256 бит и размером ключа на основе переданного ключа, где aes-256-cbcRijndael с размером блока 128 бит с размером ключа 256 бит. Поэтому они используют разные размеры блоков, которые создают совершенно разные зашифрованные данные, поскольку mcrypt использует число для указания размера блока, а OpenSSL использует число для указания размера ключа (AES имеет только один размер блока из 128 бит). Итак, в основном AES - это Rijndael с размером блока 128 бит и размером ключа 128, 192 и 256 бит. Поэтому лучше использовать AES, который в OpenSSL называется Rijndael 128.

Kenorb
источник
1
В общем, использование Rijndael с размером блока 256 бит является ошибкой из-за выбора разработчиков mcrypt. Кроме того, Rijndael с размером блока 256 может быть менее безопасным, чем с размером блока 128 бит, потому что последний получил гораздо больше обзора и использования. Кроме того, взаимодействие затруднено тем, что, хотя AES обычно доступен, Rijndael с размером блока 256 бит не является.
zaph 05
Почему ты $session_id = rtrim($decryptedSessionId, "\0");? Можно ли openssl_decryptв конце вернуть нежелательные символы? Что, если зашифрованная переменная оканчивается на 0 (т.е. encrypt("abc0")?
hlscalon
@hiscalon "\0"- это не "0"просто символ NULL, код ASCII которого равен 0x00 (шестнадцатеричный 0).
Киамлалуно
11

Реализация Rijndael на чистом PHP существует с phpseclib, доступным как пакет composer, и работает на PHP 7.3 (проверено мной).

В документации phpseclib есть страница, которая генерирует образец кода после ввода основных переменных (шифр, режим, размер ключа, размер в битах). Он выводит следующее для Rijndael, ECB, 256, 256:

код с mycrypt

$decoded = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, ENCRYPT_KEY, $term, MCRYPT_MODE_ECB);

так работает с библиотекой

$rijndael = new \phpseclib\Crypt\Rijndael(\phpseclib\Crypt\Rijndael::MODE_ECB);
$rijndael->setKey(ENCRYPT_KEY);
$rijndael->setKeyLength(256);
$rijndael->disablePadding();
$rijndael->setBlockLength(256);

$decoded = $rijndael->decrypt($term);

* $termбылbase64_decoded

Pentium10
источник
11

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

Чтобы зашифровать:

function encrypt($key, $payload) {
  $iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
  $encrypted = openssl_encrypt($payload, 'aes-256-cbc', $key, 0, $iv);
  return base64_encode($encrypted . '::' . $iv);
}

Чтобы расшифровать:

function decrypt($key, $garble) {
    list($encrypted_data, $iv) = explode('::', base64_decode($garble), 2);
    return openssl_decrypt($encrypted_data, 'aes-256-cbc', $key, 0, $iv);
}

Справочная ссылка: https://www.shift8web.ca/2017/04/how-to-encrypt-and-execute-your-php-code-with-mcrypt/

Аристон Кордейро
источник
Много хорошей кармы тебе, друг! Только одно: если пароль, например, был зашифрован старым кодом, новый код дешифрования не сможет его проверить. Его необходимо повторно сохранить и зашифровать с помощью этого нового кода.
Lumis
Эту проблему решит простой сценарий миграции. Используйте старый способ дешифрования, а затем новый способ шифрования и хранения. Альтернативой является добавление флага в таблицу пользователя и выполнение сценария принудительного сброса пароля для всех учетных записей пользователей, которым требуется изменение пароля.
cecil merrel aka takerainfire
8

Вы можете использовать пакет phpseclib pollyfill. Вы не можете использовать open ssl или libsodium для шифрования / дешифрования с помощью rijndael 256. Другая проблема, вам не нужно заменять какой-либо код.

Ахмет Эркан ЧЕЛИК
источник
2
Это было очень полезно, спасибо. Пришлось удалить расширение php-mcrypt, и тогда это работает как шарм.
DannyB
Я установил mcrypt_compat, запустив, composer require phpseclib/mcrypt_compatно все еще получаю, PHP Fatal error: Uncaught Error: Call to undefined function mcrypt_get_key_size() in /app/kohana/classes/Kohana/Encrypt.php:124что использую php 7.2.26и фреймворк Kohana. Есть ли какие-либо дополнительные действия после его установки с помощью композитора?
М-Дахаб
Понял. Вы должны добавить require APPPATH . '/vendor/autoload.php';в конец bootstrap.php.
М-Дахаб
3

Вам следует использовать OpenSSL, mcryptпоскольку он активно развивается и поддерживается. Это обеспечивает лучшую безопасность, удобство обслуживания и портативность. Во-вторых, он намного быстрее выполняет шифрование / дешифрование AES. По умолчанию он использует заполнение PKCS7, но вы можете указать, OPENSSL_ZERO_PADDINGесли оно вам нужно. Для использования с 32-байтовым двоичным ключом вы можете указать, aes-256-cbcчто гораздо очевиднее, чем MCRYPT_RIJNDAEL_128.

Вот пример кода с использованием Mcrypt:

Неаутентифицированная библиотека шифрования AES-256-CBC, написанная на Mcrypt с заполнением PKCS7.

/**
 * This library is unsafe because it does not MAC after encrypting
 */
class UnsafeMcryptAES
{
    const CIPHER = MCRYPT_RIJNDAEL_128;

    public static function encrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = mcrypt_get_iv_size(self::CIPHER);
        $iv = mcrypt_create_iv($ivsize, MCRYPT_DEV_URANDOM);

        // Add PKCS7 Padding
        $block = mcrypt_get_block_size(self::CIPHER);
        $pad = $block - (mb_strlen($message, '8bit') % $block, '8bit');
        $message .= str_repeat(chr($pad), $pad);

        $ciphertext = mcrypt_encrypt(
            MCRYPT_RIJNDAEL_128,
            $key,
            $message,
            MCRYPT_MODE_CBC,
            $iv
        );

        return $iv . $ciphertext;
    }

    public static function decrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = mcrypt_get_iv_size(self::CIPHER);
        $iv = mb_substr($message, 0, $ivsize, '8bit');
        $ciphertext = mb_substr($message, $ivsize, null, '8bit');

        $plaintext = mcrypt_decrypt(
            MCRYPT_RIJNDAEL_128,
            $key,
            $ciphertext,
            MCRYPT_MODE_CBC,
            $iv
        );

        $len = mb_strlen($plaintext, '8bit');
        $pad = ord($plaintext[$len - 1]);
        if ($pad <= 0 || $pad > $block) {
            // Padding error!
            return false;
        }
        return mb_substr($plaintext, 0, $len - $pad, '8bit');
    }
}

А вот версия, написанная с использованием OpenSSL:

/**
 * This library is unsafe because it does not MAC after encrypting
 */
class UnsafeOpensslAES
{
    const METHOD = 'aes-256-cbc';

    public static function encrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = openssl_cipher_iv_length(self::METHOD);
        $iv = openssl_random_pseudo_bytes($ivsize);

        $ciphertext = openssl_encrypt(
            $message,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );

        return $iv . $ciphertext;
    }

    public static function decrypt($message, $key)
    {
        if (mb_strlen($key, '8bit') !== 32) {
            throw new Exception("Needs a 256-bit key!");
        }
        $ivsize = openssl_cipher_iv_length(self::METHOD);
        $iv = mb_substr($message, 0, $ivsize, '8bit');
        $ciphertext = mb_substr($message, $ivsize, null, '8bit');

        return openssl_decrypt(
            $ciphertext,
            self::METHOD,
            $key,
            OPENSSL_RAW_DATA,
            $iv
        );
    }
}

Источник: Если вы вводите слово MCRYPT в свой PHP-код, значит, вы делаете это неправильно .

Kenorb
источник
2

Я использую это на PHP 7.2.x, у меня он отлично работает:

public function make_hash($userStr){
        try{
            /** 
             * Used and tested on PHP 7.2x, Salt has been removed manually, it is now added by PHP 
             */
             return password_hash($userStr, PASSWORD_BCRYPT);
            }catch(Exception $exc){
                $this->tempVar = $exc->getMessage();
                return false;
            }
        }

а затем аутентифицируйте хэш с помощью следующей функции:

public function varify_user($userStr,$hash){
        try{
            if (password_verify($userStr, $hash)) {
                 return true;
                }
            else {
                return false;
                }
            }catch(Exception $exc){
                $this->tempVar = $exc->getMessage();
                return false;
            }
        }

Пример:

  //create hash from user string

 $user_password = $obj->make_hash2($user_key);

и для аутентификации этого хеша используйте следующий код:

if($obj->varify_user($key, $user_key)){
      //this is correct, you can proceed with  
    }

Вот и все.

Абдул Рахман
источник
1

Как уже отмечалось, вы не должны хранить пароли своих пользователей в расшифровываемом формате. Обратимое шифрование предоставляет хакерам простой способ узнать пароли ваших пользователей, что распространяется на подвергание опасности учетных записей ваших пользователей на других сайтах, если они будут использовать там тот же пароль.

PHP предоставляет пару мощных функций для одностороннего хэш-шифрования со случайной солью - password_hash()и password_verify(). Поскольку хэш автоматически получает случайное значение, хакеры не могут использовать предварительно скомпилированные таблицы хешей паролей для обратного проектирования пароля. Установите этот PASSWORD_DEFAULTпараметр, и будущие версии PHP будут автоматически использовать более надежные алгоритмы для генерации хэшей паролей без необходимости обновления кода.

Тораций Аппотит
источник
1

Вам следует использовать openssl_encrypt()функцию.

rqЯщерица
источник
Есть ли у openssl encrypt в php 7 "heartbleed"?
TheCrazyProfessor
13
почему OP должен использовать openssl_encrypt? Приведите некоторые детали и предысторию
Мартин
0

Мне удалось перевести свой объект Crypto

  • Получите копию php с помощью mcrypt, чтобы расшифровать старые данные. Я зашел на http://php.net/get/php-7.1.12.tar.gz/from/a/mirror , скомпилировал его, а затем добавил расширение ext / mcrypt (configure; make; make install). Думаю, мне также пришлось добавить строку extension = mcrypt.so в php.ini. Серия скриптов для создания промежуточных версий данных с незашифрованными данными.

  • Создайте открытый и закрытый ключи для openssl

    openssl genrsa -des3 -out pkey.pem 2048
    (set a password)
    openssl rsa -in pkey.pem -out pkey-pub.pem -outform PEM -pubout
  • Для шифрования (с использованием открытого ключа) используйте openssl_seal. Из того, что я читал, openssl_encrypt с использованием ключа RSA ограничен на 11 байтов меньше длины ключа (см. Http://php.net/manual/en/function.openssl-public-encrypt.php комментарий Томаса Хорстена)

    $pubKey = openssl_get_publickey(file_get_contents('./pkey-pub.pem'));
    openssl_seal($pwd, $sealed, $ekeys, [ $pubKey ]);
    $encryptedPassword = base64_encode($sealed);
    $key = base64_encode($ekeys[0]);

Вероятно, вы могли бы сохранить необработанный двоичный файл.

  • Расшифровать (используя закрытый ключ)

    $passphrase="passphrase here";
    $privKey = openssl_get_privatekey(file_get_contents('./pkey.pem'), $passphrase);
    // I base64_decode() from my db columns
    openssl_open($encryptedPassword, $plain, $key, $privKey);
    echo "<h3>Password=$plain</h3>";

PS Нельзя зашифровать пустую строку ("")

PPS Это для базы данных паролей, а не для проверки пользователем.

Джошуа Гольдштейн
источник