В этой статье мы рассмотрим проблемы, связанные с генерированием случайных чисел для различных криптографических задач. К сожалению, в РНР 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 приоритеты использования источников случайных чисел распределены иначе:
- По умолчанию:
fread() /dev/urandom
- Если первый вариант недоступен, то
mcrypt_create_iv($bytes, MCRYPT_CREATE_IV)
- План «В»:
COM('CAPICOM.Utilities.1')->GetRandom()
- Наконец, если не работают все предыдущие варианты:
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/
Добавить комментарий