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?
Не панікуйте

3
"зашифрований пароль також має бути розшифрованим" - чому? не звучить занадто безпечно. Якась особлива причина?
Funk Forty Niner

24
"тому що мої клієнти хочуть мати можливість" відновити "свої паролі без створення нового." - Це не безпечно, і їм слід надати можливість скинути свої паролі.
Funk Forty Niner

4
Не шифруйте паролі , коли зловмисник отримує БД, він також отримає ключ шифрування. Ітераціюйте HMAC з випадковою сіллю протягом приблизно 100 мс і збережіть сіль з хешем. Використовуйте такі функції, як password_hash, PBKDF2, Bcrypt та подібні функції. Сенс у тому, щоб зловмисник витратив багато часу на пошук паролів грубою силою.
zaph

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-шифрування - Прямий PHP-код
  • Якщо ви не можете використовувати Libsodium або знешкодити / php-шифрування, використовуйте OpenSSL - Для багатьох серверів це вже буде встановлено. Якщо ні, то його можна скласти за допомогою --with-openssl [= DIR]

1
Спершу слід спробувати openssl, тому що це дуже часто, де лібсодіум не є. Не слід використовувати сирий php, якщо все запитання не
виникає

незважаючи на те, що openssl дуже поширений, схоже, що php 7 буде використовувати libsodium для своєї основної криптографії securityintelligence.com/news/…
shadi

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-cbcпроти aes-256-ctr).

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

Нарешті, якщо ви не автентифікуєте свої шифротексти (шифрувати потім 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) визначає інший варіант блокового шифру Ріндейла з розміром 256 біт і розміром ключа на основі переданого ключа, де aes-256-cbcзнаходиться Rijndael з розміром блоку 128 біт з розміром ключа 256-біт. Тому вони використовують різні розміри блоків, які створюють абсолютно різні зашифровані дані, оскільки mcrypt використовує число для визначення розміру блоку, де OpenSSL використовував число для визначення розміру ключа (AES має лише один розмір блоку - 128 біт). Тому в основному AES - це Rijndael, розмір блоку - 128 біт і розміри ключів 128, 192 та 256 біт. Тому краще використовувати AES, який називається Rijndael 128 у OpenSSL.


1
Взагалі використання Rijndael з розміром блоку 256 біт є помилкою через вибір розробниками mcrypt. Далі Rijndael з розміром блоку 256 може бути менш безпечним, ніж з розміром блоку 128 біт, тому що останній провів набагато більше огляду та використання. Додатково сумісність перешкоджає тому, що в той час як AES загальнодоступний Rijndael з розміром блоку в 256 біт, це не так.
zaph

Чому ти $session_id = rtrim($decryptedSessionId, "\0");? Чи можливо openssl_decryptповернутися до небажаних символів? Що робити, якщо зашифрована змінна закінчується на 0 (тобто encrypt("abc0")?
hlscalon

@hiscalon "\0"- це не "0"символ NULL, код ASCII якого становить 0x00 (шістнадцятковий 0).
kiamlaluno

11

Pure-PHP-реалізація Rijndael існує за допомогою phpseclib, доступного як пакет композиторів, і працює на 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


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/


Багато хорошої карми тобі, друже! Тільки одне: якщо пароль, наприклад, був зашифрований зі старим кодом, новий код розшифрування не зможе його перевірити. Його потрібно повторно зберегти та зашифрувати цим новим кодом.
Луміс

Простий сценарій міграції вирішив би цю проблему. Використовуйте старий спосіб дешифрування, а потім новий спосіб шифрування та зберігання. Альтернативою є додавання прапора до таблиці користувача та створення сценаріїв примусового скидання пароля для всіх облікових записів користувачів, які потребують змін пароля.
cecil merrel aka donerainfire

8

Можна використовувати пакет phpseclib pollyfill. Ви не можете використовувати відкритий 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 framwork. Чи є ще якісь кроки для виконання після його встановлення разом із композитором?
М-Дахаб

Зрозумів. Ви повинні додати 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-код, ви робите це неправильно .


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 автоматично використовуватимуть більш сильні алгоритми для генерування хешей паролів, не потребуючи оновлення коду.



0

Я зміг перекласти свій криптооб'єкт

  • Отримайте копію php з mcrypt, щоб розшифрувати старі дані. Я перейшов до http://php.net/get/php-7.1.12.tar.gz/from/a/mirror , скомпілював його, потім додав розширення ext / mcrypt (конфігурувати; зробити; зробити встановлення). Я думаю, що мені довелося додати рядок extenstion = 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. З того, що я прочитав, opensl_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 Це для бази даних паролів, а не для перевірки користувача.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.