Криптографическая рандомизация в PHP

В этой статье мы проанализируем проблемы, относящиеся к генерации случайных чисел, используемых в криптографии. PHP5 не обеспечивает простой механизм генерации криптостойких случайных чисел, в то время как PHP7 решает эту проблему путем введения CSPRNG-функций.

Что такое CSPRNG?


Цитируя википедию, криптографически стойкий генератор псевдослучайных чисел (англ. Cryptographically secure pseudorandom number generator, CSPRNG) — это генератор псевдослучайных чисел с определёнными свойствами, позволяющими использовать его в криптографии.

CSPRNG в основном используется для следующих целей:
  • Генерация ключей (в том числе, генерация public/private ключей)
  • Создание случайных паролей для аккаунтов пользователей
  • Системы шифрования

Главным аспектом сохранения высокого уровня безопасности является высокое качество случайности.

CSPRNG в PHP7


PHP7 вводит две новых функции, которые могут быть использованы для CSPRNG: random_bytes и random_int.

Функция random_bytes возвращает строку и принимает в качестве входных параметров 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 будет использоваться системный getrandom(2).
  • Если все это терпит неудачу, в качестве финальной попытки PHP попробует задействовать /dev/urandom.
  • При невозможности использовать эти источники будет выброшена ошибка.

Простой тест


Хорошая система генерации случайных чисел определяется «качеством» генераций. Чтобы его проверить часто используется набор статистических тестов, позволяющих, не вникая в сложную тему статистики, сравнить известное эталонное поведение с результатом генератора и помочь в оценке его качества.

Один из самых простых тестов — игра в кости. Предполагаемая вероятность выпадения шестерки при одной кости — один к шести, в то же время, если я брошу три кости 100 раз, то ожидаемые выпадения 1, 2 и 3х шестерок примерно такие:
  • 0 шестерок = 57.9 раз
  • 1 шестерка = 34.7 раз
  • 2 шестерки = 6.9 раз
  • 3 шестерки = 0.5 раз

Вот код для воспроизведения броска костей 1 000 000 раз:

$times = 1000000;
$result = [];
for ($i=0; $i < $times; $i++) {
    $dieRoll = array(6 => 0); //initializes just the six counting to zero
    $dieRoll[roll()] += 1; //first die
    $dieRoll[roll()] += 1; //second die
    $dieRoll[roll()] += 1; //third die
    $result[$dieRoll[6]] += 1; //counts the sixes
}
function roll() {
    return random_int(1,6);
}
var_dump($result);

Прогонка кода в PHP7 c использованием random_int и простого rand выдаст следующие результаты:
Шестерки Ожидаемый результат random_int rand
0 579000 579430 578179
1 347000 346927 347620
2 69000 68985 69586
3 5000 4658 4615

Для лучшего сравнения rand и random_int построим график результатов, применяя формулу: результат PHPожидаемый результат / sqrt(ожидаемый результат).

График будет выглядеть так (чем ближе к нулю, тем лучше):
график test random

Даже несмотря на плохой результат с тремя шестерками и всю простоту теста, мы видим явное превосходство random_int над rand.

А что насчет PHP5?


По умолчанию, PHP5 не предусматривает каких-либо сильных псевдо-генераторов случайных чисел. Но на самом деле есть несколько вариантов, таких как openssl_random_pseudo_bytes(), mcrypt_create_iv() или непосредственное использование /dev/random или /dev/urandom с fread(). Есть также такие библиотеки, как RandomLib или libsodium.

Если вы хотите начать использовать хороший генератор случайных чисел и в то же время пока еще не готовы к переходу на PHP7, вы можете использовать библитеку random_compat от Paragon Initiative Enterprises. Она позволяет использовать random_bytes() и random_int() в PHP 5.х проектах.

Библиотеку можно установить через 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)

По сравнению с PHP7, 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()


Дополнительную информацию о том почему используется именно этот порядок вы можете прочитать в документации.

Пример генерации пароля с использованием библиотеки:

$passwordChar = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$passwordLength = 8;
$max = strlen($passwordChar) - 1;
$password = '';
for ($i = 0; $i < $passwordLength; ++$i) {
    $password .= $passwordChar[random_int(0, $max)];
}
echo $password;
//possible output: 7rgG8GHu

Краткий итог


Вы всегда должны применять криптографически стойкие генераторы псевдослучайных чисел, и random_compat является хорошим решением для этого.

Если же вам необходим надежный источник случайных данных, то посмотрите в сторону random_int и random_bytes.

Ссылки по теме


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


  1. zaigraeff
    07.12.2015 19:59
    +8

    Открыл R написал такой код:

    n <- 1000000
    a <- rep(0, 4)
    k <- NULL
    for (i in 1:n) {
      k <- sum(sample(6, size = 3, replace = TRUE) == 6)
      a[k+1] <- a[k+1] + 1
    }
    

    Из 5 попыток запуска был результат как лучше чем у random_int так и хуже чем у rand :)
    Собсно вопрос, какова роль удачи в приведенном в статье примере? Наверняка можно подобрать случай где результаты будут обратные.

    ЗЫ: PHP я не знаю, да и R не очень знаю, строго не судите :)


    1. CAH4A
      08.12.2015 10:41

      Да, по-моему, автор что-то не то пишет.

      Можно вообще написать такой генератор псевдослучайных чисел, который тест на выпадение шестёрок будет проходить идеально.
      Например такй: seed ГПСЧ будет от 0 до 100 * 3 (100 раз по 3 рола), а само число выбираться case-ом. После броска: seed++.

      Но генерировать пароли таким генератором явным образом не стоит) Всего 300 разных паролей.

      Вопрос качества ГСПЧ — это вопрос энтропии источника, а не то, насколько он адекватно аппроксимирует равномерное распределение.


  1. lorc
    07.12.2015 22:09
    +1

    А почему бы не скормить случайные числа в die hard и не получить более-менее надежные результаты теста?


  1. anitspam
    08.12.2015 05:19
    +3

    Функция random_bytes возвращает строку и принимает в качестве входных параметров int, задающий длину (в байтах) возвращаемого значения:
    $bytes = random_bytes('10');
    


    люблю пхп :)


    1. bolk
      08.12.2015 07:33
      +7

      Вы про строку '10'? Теперь есть строгая типизация:

      $ php7 -r "declare(strict_types=1); random_bytes('10');"
      
      Fatal error: Uncaught TypeError: random_bytes() expects parameter 1 to be integer, string given in Command line code:1
      Stack trace:
      #0 Command line code(1): random_bytes('10')
      #1 {main}
        thrown in Command line code on line 1