Добрый день! Сегодня расскажу, как с помощью 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 бита (в теории можно увеличить, но пострадает производительность) и его практически невозможно восстановить без доступа к серверу в момент генерации.

Протестировать проект можно тут

Спасибо за внимание!

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


  1. Number571
    06.04.2026 20:08

    Всё бы хорошо, если бы не было так плохо. Пробовали ли вы считать энтропию от обычного ГПСЧ, который уже встроен в любые стандартные библиотеки любого языка программирования, включая PHP? Вы бы обнаружили, что при вычислении энтропии почти любая встроенная функция рандома будет всегда стремиться к 8 битам вне зависимости от криптостойкости генератора - даже обычный линейный конгруэнтный генератор по вашей функции будет стремиться к максимальному значению. А всё по тому, что некорректно проводить тестирование качества генератора по формуле вычисления энтропии. При вычислении энтропии мы должны брать во внимание не только те байты, которые уже имеются на руках, но также и априорные вероятности того, как и за счёт чего эти байты были получены. Из-за этого реальная энтропия вычисления будет куда ниже вычисляемой по сырым байтам.

    Вот как пример кода на языке Go, где я использую стандартный математический (не криптографический) ГПСЧ и получаю "энтропию" в 7.997064, хотя фактическая энтропия сводится только лишь к использованию time.Now().UnixNano() - и это вряд-ли достигнет даже пары бит.

    package main
    
    import (
    	"fmt"
    	"math"
    	"math/rand"
    	"time"
    )
    
    func entropy(b []byte) float64 {
    	freq := make(map[byte]uint, 256)
    	for _, v := range b {
    		freq[v]++
    	}
    	e := .0
    	total := uint(len(b))
    	for _, v := range freq {
    		if v > 0 {
    			p := float64(v) / float64(total)
    			e -= p * math.Log2(p)
    		}
    	}
    	return e
    }
    
    func main() {
    	r := rand.New(rand.NewSource(time.Now().UnixNano()))
    
    	buf := make([]byte, 65536)
    	if _, err := r.Read(buf); err != nil {
    		panic(err)
    	}
    
    	fmt.Printf("Entropy: %f\n", entropy(buf))
    }
    


    1. 3ball Автор
      06.04.2026 20:08

      Благодарю за уточнение. Да, вы правы в данном случае энтропия Шеннона будет иллюзией, а реальная энтропия от джиттера не будет превышать 1-2 бита. Но разве реальная случайность не зависит от наблюдателя? Для взломщика это скорее всего будут те самые 6-7 бит, ибо он не может восстановить внутреннее состояние таймеров. Даже если он знает алгоритм и начальный seed, он не знает, в какой момент времени были сделаны замеры (T1, T2), а джиттер вносит непредсказуемость. Это не классический PRNG с фиксированным состоянием, а гибрид, где время выступает источником скрытой энтропии. Поэтому для внешнего наблюдателя эффективная энтропия на байт действительно близка к 8, хотя для создателя, знающего состояние, она равна энтропии джиттера. Минус такого подхода - синхронизация почти невозможна, потому что запустить два компьютера и получить один и тот же результат не получится, выход всегда разный. И да, а вы не задумывались, что было бы если бы такой генератор построить на сверхточных часах? :)


      1. Number571
        06.04.2026 20:08

        Но разве реальная случайность не зависит от наблюдателя?

        Если не углубляться в философию наблюдателя и говорить именно что про +- реальную случайность, то в контексте криптографии - не зависит. Если случайность реальна, то и взломать или найти закономерности в ней будет теоретически невозможно / неосуществимо.

        Для взломщика это скорее всего будут те самые 6-7 бит, ибо он не может восстановить внутреннее состояние таймеров. Даже если он знает алгоритм и начальный seed, он не знает, в какой момент времени были сделаны замеры (T1, T2)

        Предположим, что это вообще единственная модель угроз, доступная криптоаналитику. Даже в этом случае, T1,T2 - это 64-битные числа и для правильного их нахождения потребуется потратить приблизительно 2^65 операций. Теперь примем во внимание, что сам timestamp - это не случайное число, мы чётко знаем его поведение - оно увеличивается, а некоторая его часть остаётся в основном неизменной (по типу префикса 17755... для текущего времени). Это говорит о том, что область вычислений на самом деле куда ниже 2^64. Итого, мы получаем, что даже если бы генератор был +- безопасным, он является небезопасным по выбранным параметрам - буквально уязвим к перебору при достаточных средствах злоумышленника.

        Поэтому для внешнего наблюдателя эффективная энтропия на байт действительно близка к 8, хотя для создателя, знающего состояние, она равна энтропии джиттера.

        Энтропия - это абсолютная величина, вы говорите про вычислительную сложность. Из более-менее близкого примера - это пароль и пароль проброшенный через KDF (с конфигурацией, например +2^20 вычислений). Если энтропия пароля 40-бит, то даже когда он будет проброшен через KDF, энтропия результата также будет 40-бит, но вычислительная сложность станет уже 60-бит. Энтропия - это фактически начальные условия в отрыве от какого-либо алгоритма. В вашем случае энтропией выступает timestamp, но вы решаете вычислять энтропию от результата алгоритма (который был произведён от метки времени). В этом и суть, что предоставленная вами энтропия - она ложная при любом сценарии, будь то практическом или теоретическом.

        Минус такого подхода - синхронизация почти невозможна, потому что запустить два компьютера и получить один и тот же результат не получится, выход всегда разный.

        Точно также, как если бы мы просто запустили rand(time.Now().UnixNano()) на двух машинах - т.к., мы физическим не сможем запустить их синхронно, даже если будем использовать для этого какие-нибудь автоматизированные скрипты или программы.

        И да, а вы не задумывались, что было бы если бы такой генератор построить на сверхточных часах?

        Мы бы потратили много денег на аренду сверхточных часов только для того, чтобы протестировать небезопасный ГПСЧ. К тому же, если мы говорим про ГСЧ построенные на хаосе, то скорее всего результат был бы вообще противоположным - тобишь генератор стал бы ещё хуже рандомить числа.


        1. 3ball Автор
          06.04.2026 20:08

          Да, вы правы. Но посмотрите внимательнее, как работает механизм.

          Т5/Т6 - срезы от 1000 до 10000 и выбор конкретного среза зависит от стейт, но момент времени - нет

          Т11/т12 - случайные вставки нулевого байта и паузы ( с разбросом 1-100 ) тактов не хранятся в state и не могут быть восстановлены.

          Т8/т9 - нелинейная зависимость, которая зависит от последнего байта строки - тоже не может быть восстановлена. В худшем случае, где нападающий знает все о времени и state, он не знает были ли паузы ( и сколько они длились ) и нулевые байты.

          Состояние для паузы ( извините, тут воспользовался подсказкой ) - 100^(n-1), где n = 32 (32 байта ключа), то есть 100^31 ≈ 10^62 ≈ 2^206 — это для пауз между 32 байтами (31 промежуток).

          Таким образом state это только первый шаг. Без асинхронных слоев он не дает взлома.


        1. 3ball Автор
          06.04.2026 20:08

          Я пересчитал через Qwen, теоретическая стойкость, как я указал выше, но практическая 2^80 - 2 ^100. Не могу проверить эти результаты, у меня изначально другие были, но имеет место быть. Если интересно, то могу скинуть, как это рассчитывалось, спасибо за критику