Добрый день! Сегодня расскажу, как с помощью PHP создать генератор случайных байт ( чисел ) с помощью 12 таймеров. Энтропия данного генератора составляет примерно 7.1 бит на символ ( у меня ), но на более мощном железе может подняться до 7.9-8, что по идее не отличимо от истинной случайности. Вот, как работает весь "конвеер":
Внимание! Проект экспериментальный, не сертифицирован, не рекомендуется для использования в системах, требующих официального криптографического одобрения. Для учебных целей и экспериментов — пожалуйста.
Начнем с таймеров, их у нас 12 штук. Каждый выполняет свою функцию:
Таймер 1: Большие часы
Данный таймер просто отсчитывает время с начала 1970 года. Его роль - задать базовое время, от которого мы будем идти дальше.
Таймер 2: Спортивный секундомер
Таймер считающий наносекунды.
Таймер 3: Память с характером
Таймер, который отсчитывая свое время прибавляет, либо вычитает его к двум другим таймерам в зависимости от того четное или нечетное число получилось при сложении времени двух других таймеров.
Таймер 4: Искривитель
Он же модулятор. Если цифра четная производится умножение, если цифра нечетная деление общего "времени".
Таймер 5: Дробилка
Решает на сколько кусков разрезать текущее время, от 1000 до 10000.
Таймер 6: Сортировщик
Решает, какой из раздробленных дробилкой кусков выбрать.
Таймер 7: Шумовик
Берет 1-2 бита истинной случайности из джиттера работы кэша и прерываний
Таймер 8: Рубильник
Может произвольным образом включить или выключить создание случайных битов. Нужен для защиты от атак по времени
Таймер 9: Смотрящий за рубильником
Смотрит на последнее и число, если оно меньше пяти - включает рубильник, если больше пяти, то выключает. Если число равно 5, то ничего не делает, оставляет в текущем положении
Таймер 10: Точка старта
Секунда, миллисекунда или наносекунда включения генератора, который сам также не знает, когда он включился. Это дает начальную энтропию без seed, таймер 10 не знает никто, даже мы сами.
Таймер 11: Клякса
Иногда вместо байта выдает ноль. Просто так. Ломает анализ последовательности
Таймер 12: Молчун
Таймер, делающий паузы. Иногда может замолчать на 1-100 тактов. Ломает временную привязку к байтам, усложняет восстановление состояния. Создает асинхронность - генератор не работает пошагово, а дышит: то ускоряется, то замирает, то замедляется.
T3, T4, T5, T6, T8, T9, T10, T11, T12 - усилители случайности.
Т1, Т7 - шум
Т2 - истинная случайность 1-2 бит.
Вот как выглядит реализация в коде:
/** * ChaosRNG - генератор случайных байтов с 12 таймерами * * 12 таймеров: * T1 – обычные часы (microtime) * T2 – точные часы (hrtime) * T3 – память (состояние $state) * T4 – искривитель (умножает/делит время) * T5 – дробилка (количество срезов 1000-10000) * T6 – тыкальщик (выбор среза) * T7 – скрытый шум (джиттер, прерывания) * T8 – рубильник (вкл/выкл выдачи) * T9 – смотрящий (управляет рубильником) * T10 – точка старта (момент инициализации) * T11 – клякса (вставка нулевого байта) * T12 – молчун (паузы 1-100 тактов) */ class ChaosRNG { // Свойства класса private $state; // T3: 64-битное состояние private $k; // T4: коэффициент искривителя private $enabled; // T8: рубильник (вкл/выкл) private $lastByte; // T9: последний выданный байт private $dot; // T11: флаг точки private $pauseLen; // T12: длина паузы private $pauseRem; // T12: остаток паузы /** * Конструктор — T10 (точка старта) * Смешиваем всё, что нельзя повторить */ public function __construct() { // XOR всех источников → уникальное начальное состояние $this->state = (int)(microtime(true) * 1000000) // T1: микросекунды ^ hrtime(true) // T2: наносекунды ^ getmypid() // T7: PID процесса ^ memory_get_usage(); // T7: использованная память $this->k = 1.001; // T4: начальный коэффициент $this->enabled = true; // T8: рубильник включён $this->lastByte = 0; // T9: последнего байта нет $this->dot = false; // T11: точка не вставлена $this->pauseLen = 0; // T12: паузы нет $this->pauseRem = 0; // T12: остаток паузы = 0 } /** * T1: обычные часы * Возвращает микросекунды с 1970 года */ private function t1() { return (int)(microtime(true) * 1000000); } /** * T2: точные часы * Возвращает наносекунды с запуска системы */ private function t2() { return hrtime(true); } /** * Внутренний генератор байта (T3-T7) * Меняет состояние и возвращает один байт */ private function nextByteInternal() { // T5: количество срезов от 1000 до 10000 $K = 1000 + ($this->state % 9001); // T6: выбор конкретного среза $slot = (($this->state >> 8) % $K); // T1 и T2: получаем текущее время $t1 = $this->t1(); $t2 = $this->t2(); // T4: искривитель (растягиваем или сжимаем время) if ($this->state & 1) { $t1 = $t1 * $this->k; // растягиваем T1 } else { $t2 = $t2 / $this->k; // сжимаем T2 } // Суммируем с учётом выбранного среза $total = (int)($t1 * ($slot + 1) / $K) + (int)($t2 * ($slot + 1) / $K); // T3: обновляем состояние (прибавляем или вычитаем) if ($total & 1) { $this->state += ($total & 0xFF); } else { $this->state -= ($total & 0xFF); } // Обновляем коэффициент искривителя $this->k = 1.0 + (($this->state & 0xFF) / 10000.0); // Возвращаем младший байт состояния return $this->state & 0xFF; } /** * Публичный метод: получить N случайных байтов * Учитывает все 12 таймеров */ public function getBytes($n) { $out = []; for ($i = 0; $i < $n; $i++) { // T12: пауза (молчун) while ($this->pauseRem > 0) { $this->pauseRem--; $this->nextByteInternal(); // состояние меняется } // Если пауза закончилась — генерируем новую if ($this->pauseRem === 0 && $this->pauseLen === 0) { $this->pauseLen = 1 + ($this->state % 100); // 1-100 тактов $this->pauseRem = $this->pauseLen; } // T11: точка (вставка нулевого байта) if (($this->state & 10) === 0 && !$this->dot) { $this->dot = true; $this->nextByteInternal(); $out[] = 0x00; // точка = нулевой байт continue; } $this->dot = false; // T9: смотрящий за рубильником if ($this->lastByte < 5) { $this->enabled = true; // включаем } elseif ($this->lastByte > 5) { $this->enabled = false; // выключаем } // если равно 5 — ничего не меняем // Генерируем байт $byte = $this->nextByteInternal(); // T8: рубильник if (!$this->enabled) { // Если выключен — выдаём фиктивный байт $out[] = $this->nextByteInternal() & 0xFF; continue; } // Сохраняем последний выданный байт $this->lastByte = $byte; // Сбрасываем паузу после выдачи реального байта if ($this->pauseLen > 0) { $this->pauseLen = 0; $this->pauseRem = 0; } $out[] = $byte; } return $out; }
Проверяем энтропию по Шеннону:
* Рассчитать энтропию Шеннона для массива байтов * Чем ближе к 8, тем более случайные данные */ public static function entropy($bytes) { // Считаем частоту каждого байта $freq = array_fill(0, 256, 0); foreach ($bytes as $b) { $freq[$b]++; } // Формула Шеннона: -Σ p * log2(p) $e = 0; $total = count($bytes); foreach ($freq as $c) { if ($c > 0) { $p = $c / $total; $e -= $p * log($p, 2); } } return $e; } } // ЗАПУСК ТЕСТА // Создаём генератор $rng = new ChaosRNG(); // Генерируем 65536 байтов (64 КБ) $data = $rng->getBytes(65536); // Считаем энтропию $entropy = ChaosRNG::entropy($data); // Выводим результат echo "Энтропия: " . round($entropy, 4) . " / 8 бит\n"; echo "Статус: " . ($entropy > 7 ? " ХОРОШО" : " НИЗКАЯ") . "\n"; ?>
Таким образом я у себя на компьютере получил среднюю энтропию в 7.12 бит, что теоретически должно пройти тесты на случайность, но в реальности нужны атаки и проверки.
Теоретическая криптостойкость
1. 12 источников нелинейности
Даже зная состояние state, злоумышленник не знает:
Коэффициент
k(меняется каждый такт)Количество срезов (1000–10000)
Какой срез выбран
Была ли пауза, и какой длины
Была ли точка (0x00)
Включён ли рубильник
Это превращает восстановление состояния в решение системы с кучей неизвестных. (State — начальное состояние системы).
2. Энтропия 6-7+ бит
Даже в худших условиях (виртуалка) энтропия редко падает ниже 6.5 бит. Это значит, что последовательность байтов в теории статистически неотличима от случайной. (Нужны тесты).
Применение
С помощью данного гсч можно генерировать токены, пароли, ключи. На токен длиной в 16 символов из алфавита в 95 символов будет где-то 113 бит, base64 - 85 бит.
P.S. если противник узнает $this->state , то в теории сможет восстановить всю последовательность с точностью до джиттера. Но state 64 бита (в теории можно увеличить, но пострадает производительность) и его практически невозможно восстановить без доступа к серверу в момент генерации.
Протестировать проект можно тут
Спасибо за внимание!
