В этой статье мы проанализируем проблемы, относящиеся к генерации случайных чисел, используемых в криптографии. 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(ожидаемый результат)
.График будет выглядеть так (чем ближе к нулю, тем лучше):
Даже несмотря на плохой результат с тремя шестерками и всю простоту теста, мы видим явное превосходство
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
использует несколько другие приоритеты:fread()
/dev/urandom
если доступноmcrypt_create_iv($bytes, MCRYPT_CREATE_IV)
COM('CAPICOM.Utilities.1')->GetRandom()
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)
lorc
07.12.2015 22:09+1А почему бы не скормить случайные числа в die hard и не получить более-менее надежные результаты теста?
anitspam
08.12.2015 05:19+3Функция random_bytes возвращает строку и принимает в качестве входных параметров int, задающий длину (в байтах) возвращаемого значения:
$bytes = random_bytes('10');
люблю пхп :)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
zaigraeff
Открыл R написал такой код:
Из 5 попыток запуска был результат как лучше чем у random_int так и хуже чем у rand :)
Собсно вопрос, какова роль удачи в приведенном в статье примере? Наверняка можно подобрать случай где результаты будут обратные.
ЗЫ: PHP я не знаю, да и R не очень знаю, строго не судите :)
CAH4A
Да, по-моему, автор что-то не то пишет.
Можно вообще написать такой генератор псевдослучайных чисел, который тест на выпадение шестёрок будет проходить идеально.
Например такй: seed ГПСЧ будет от 0 до 100 * 3 (100 раз по 3 рола), а само число выбираться case-ом. После броска: seed++.
Но генерировать пароли таким генератором явным образом не стоит) Всего 300 разных паролей.
Вопрос качества ГСПЧ — это вопрос энтропии источника, а не то, насколько он адекватно аппроксимирует равномерное распределение.