[Перевод] Случайность в PHP7 – Повезет ли мне?

Криптографическая рандомизация в 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.

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


© Habrahabr.ru