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

Комментарии (1)


  1. anonymous
    00.00.0000 00:00