Довольны ли вы случайными числами в РНР?

от автора

В этой статье мы рассмотрим проблемы, связанные с генерированием случайных чисел для различных криптографических задач. К сожалению, в РНР 5 отсутствует простой механизм генерирования криптографически стойких случайных чисел. Однако в РНР 7 эта задача решена за счёт пары функций, выполняющих роль CSPRNG-генераторов.

Что такое CSPRNG?

Согласно Википедии, CSPRNG — это «обычный» генератор случайных чисел, обладающий свойствами, благодаря которым его можно использовать для криптографических нужд. Первое, что приходит на ум:

  • Генерирование сложных ключей
  • Создание случайных паролей для новых пользователей
  • Разработка систем шифрования

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

CSPRNG в PHP 7

В PHP 7 появились две функции, которые можно использовать в качестве CSPRNG: random_bytes и random_int. Первая функция принимает на вход целочисленное значение, а в ответ возвращает строковое значение, длина которого в байтах равняется полученному на входе.

Например:

$bytes = random_bytes('10'); var_dump(bin2hex($bytes)); //possible ouput: string(20) "7dfab0af960d359388e6"   

Функция random_int возвращает целочисленное значение из заданного диапазона:

var_dump(random_int(1, 100)); //possible output: 27 

Под капотом

У этих функций разные источники случайных чисел, и они зависят от окружения:

  • В Windows всегда используется CryptGenRandom().
  • На других платформах по мере возможности задействуется arc4random_buf() (производные от BSD и системы с libbsd).
  • На Linux, если не получается использовать arc4random_buf(), применяется системный вызов getrandom(2).
  • Если оба предыдущих варианта не работают, то используется /dev/urandom.
  • Наконец, при недоступности всех вышеописанных источников мы получаем ошибку исполнения.

Простой тест

Проверить «качество» работы генератора случайных чисел можно с помощью серии статистических тестов. Можно не вдаваться в сложные статистические расчёты, а сравнить известное поведение с результатами работы генератора. Пример простого теста — игральные кости. Будем считать, что вероятность выкинуть на одном кубике шестёрку равна один к шести. Тогда, если 100 раз кинуть одновременно по три кубика, примерная вероятность выпадения шестёрок будет следующей:

  • Ни одной — 57,9 раза
  • Одна шестёрка — 34,7 раза
  • Две шестёрки — 6,9 раза
  • Три шестёрки — 0,3 раза

Вот пример кода, эмулирующего кидание кубиков 1 млн. раз:

$times = 1000000; $result = []; for ($i=0; $i<$times; $i++){     $dieRoll = array(6 => 0); //Запускает отсчёт от 6 до нуля     $dieRoll[roll()] += 1; //Первый кубик     $dieRoll[roll()] += 1; //Второй кубик     $dieRoll[roll()] += 1; //Третий кубик     $result[$dieRoll[6]] += 1; //Подсчёт шестёрок } function roll(){     return random_int(1,6); } var_dump($result); 

Если протестировать в РНР 7 работу этого кода на random_int и простой функции rand, то можно получить такие результаты:

0 579 000 579 430 578 179
1 347 000 346 927 347 620
2 69 000 68 985 69 586
3 5 000 4 658 4 615

Чтобы ещё лучше оценить разницу между rand и random_int, можно построить диаграмму, для усиления разницы применив формулу: php result - expected result / sqrt(expected).

Чем ближе к 0, тем лучше:

Несмотря на посредственные результаты для комбинации из трёх шестёрок и то, что тест слишком прост по сравнению с реальными задачами, очевидно, что предпочтительнее использовать random_int. Кроме того, если работа генератора будет менее предсказуемой, то это положительно скажется на безопасности всего приложения.

Что насчёт PHP 5?

По умолчанию в PHP 5 отсутствуют какие-либо криптографически стойкие генераторы случайных чисел. Однако есть варианты использования таких инструментов, как openssl_random_pseudo_bytes() и mcrypt_create_iv(). С помощью fread() можно напрямую обращаться к /dev/random или /dev/urandom
. Также вы можете применять пакеты RandomLib и libsodium.

Если вам нужен хороший генератор, но при этом код должен быть готов к переходу на РНР 7, то можно обратиться к библиотеке random_compat, разработанной в недрах Paragon Initiative Enterprises. Эта библиотека позволяет использовать random_bytes() и random_int() в проектах, написанных на PHP 5.x. Установить её можно с помощью Composer:

composer require paragonie/random_compat require 'vendor/autoload.php'; $string = random_bytes(32); var_dump(bin2hex($string)); // string(64) "8757a27ce421b3b9363b7825104f8bc8cf27c4c3036573e5f0d4a91ad2aaec6f" $int = random_int(0,255); var_dump($int); // int(81) 

По сравнению с PHP 7, в random_compat приоритеты использования источников случайных чисел распределены иначе:

  1. По умолчанию: fread() /dev/urandom
  2. Если первый вариант недоступен, то mcrypt_create_iv($bytes, MCRYPT_CREATE_IV)
  3. План «В»: COM('CAPICOM.Utilities.1')->GetRandom()
  4. Наконец, если не работают все предыдущие варианты: openssl_random_pseudo_bytes()

Если вам любопытно, почему очерёдность именно такая, то можете изучить документацию к библиотеке. Простой пример использования random_compat для генерирования паролей:

$passwordChar = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $passwordLength = 8; $max = strlen($passwordChar) - 1; $password = ''; for ($i = 0; $i < $passwordLength; ++$i) {     $password .= $passwordChar[random_int(0, $max)]; } echo $password; //Вариант выходных данных: 7rgG8GHu

Заключение

Старайтесь всегда использовать наиболее стойкий из доступных вам генераторов. Хороший вариант — библиотека random_compat. Если же время горит, то применяйте хотя бы random_int или random_bytes.

ссылка на оригинал статьи https://habrahabr.ru/post/281030/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *