Безопасная генерация случайных чисел: основа сильной криптографии
Безопасная генерация случайных чисел критична для шифрования. Как работает crypto.getRandomValues и почему слабая случайность ломает безопасность.
Безопасная генерация случайных чисел — фундамент, на котором держится вся криптография. В JavaScript crypto.getRandomValues(buffer) заполняет TypedArray криптографически безопасными случайными байтами из CSPRNG операционной системы (/dev/urandom на Linux/macOS, BCryptGenRandom на Windows, SecRandomCopyBytes на iOS/macOS). Не используйте Math.random() ни в чём, связанном с безопасностью: это Mulberry32 или xorshift-подобный PRNG, разработанный для скорости, а не непредсказуемости, и его вывод предсказуем после наблюдения нескольких значений. Слабый RNG ломает AES-ключи, TLS-рукопожатия, уникальность nonce в GCM, непредсказуемость токенов и каждый другой примитив безопасности, зависящий от непредсказуемых битов.
Разница между случайным и криптографически случайным
PRNG (генератор псевдослучайных чисел) создаёт детерминированный поток из начального значения (seed). Зная seed и алгоритм, можно воспроизвести каждый вывод. Подходит для игр, симуляций и методов Монте-Карло. Катастрофически для криптографии.
CSPRNG (криптографически стойкий PRNG) инициализируется из источника истинной энтропии (тепловой шум, тайминг прерываний, аппаратные инструкции RNG — например, Intel RDSEED), и его конструкция гарантирует, что вывод вычислительно неотличим от истинной случайности, а наблюдение предыдущего вывода не помогает предсказать будущий.
JavaScript предоставляет оба варианта. Math.random() — это PRNG. crypto.getRandomValues() — обёртка над CSPRNG операционной системы. Одна строка кода разницы, колоссальная разница в безопасности.
Канонически правильное использование
// Генерация случайных байт для 256-битного AES-ключа
const keyBytes = crypto.getRandomValues(new Uint8Array(32));
// Генерация 96-битного GCM nonce
const nonce = crypto.getRandomValues(new Uint8Array(12));
// Генерация 128-битной соли
const salt = crypto.getRandomValues(new Uint8Array(16));
// Генерация URL-безопасного случайного токена
const tokenBytes = crypto.getRandomValues(new Uint8Array(32));
const token = btoa(String.fromCharCode(...tokenBytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
crypto.getRandomValues() — синхронный вызов, заполняет буфер на месте, возвращает буфер. Максимальный размер запроса — 65 536 байт в одном вызове (квота, установленная спецификацией для предотвращения блокировки). Для бо́льшего количества случайного материала вызывайте повторно.
Эквиваленты в Node.js
const { randomBytes, randomFillSync, webcrypto } = require('crypto');
const keyBytes = randomBytes(32); // Возвращает Buffer
// Или совместимый с Web Crypto
const nonce = webcrypto.getRandomValues(new Uint8Array(12));
randomBytes в Node использует тот же базовый CSPRNG, что и Web Crypto. Используйте стиль API, подходящий вашему коду. Для изоморфного кода, работающего в обоих окружениях, webcrypto.getRandomValues точно соответствует браузерному.
Почему Math.random не подходит
V8 (Chrome/Node), SpiderMonkey (Firefox) и JavaScriptCore (Safari) реализуют Math.random() как быстрый PRNG без криптографических гарантий. V8 использует вариант xorshift128+. Исследователи продемонстрировали, что после наблюдения ~5 выводов атакующий может восстановить внутреннее состояние и предсказать все будущие выводы. В 2015 году Майк Паунд с коллегами обратили состояние Math.random V8 в реальных программах поиска уязвимостей.
Если вы используете Math.random() для генерации сессионных токенов, ссылок сброса пароля, nonce для шифрования или идентификаторов общего доступа, атакующие, наблюдающие несколько из них, могут предсказать остальные. Это не теория — распространённый класс ошибок, обнаруживаемых при аудитах.
Распространённые неправильные применения
Инициализация PRNG библиотеки через Math.random():
// НЕПРАВИЛЬНО
const seed = Math.floor(Math.random() * 2**32);
Ничто после не может быть более случайным, чем seed. Используйте crypto.getRandomValues(new Uint32Array(1))[0] вместо этого.
Использование Date.now() как источника энтропии: время угадываемо в узких временных окнах. Даже в сочетании с небольшим случайным фактором временны́е метки утекают достаточно битов для атакующих.
Написание собственного генератора через XOR-смешивание источников: не делайте этого. Системные CSPRNG уже смешивают все полезные источники энтропии. Добавление собственного перемешивания, как правило, снижает энтропию, а не увеличивает.
Смещение по модулю при генерации диапазонов: randomBytes[0] % 10 не распределён равномерно по 0-9, поскольку 256 не кратно 10. Для равномерных случайных целых в диапазоне используйте отклоняющую выборку:
function randomInt(max) {
const range = new Uint32Array(1);
const threshold = 2**32 - (2**32 % max);
do {
crypto.getRandomValues(range);
} while (range[0] >= threshold);
return range[0] % max;
}
Источники энтропии и проблемы при старте системы
На Linux /dev/urandom всегда безопасен после раннего этапа загрузки. В течение первых нескольких секунд загрузки на системах без аппаратного RNG пул ядра может быть недостаточно инициализирован. Это было использовано в баге Debian OpenSSL 2008 года, когда патч удалил смешивание энтропии, оставив только PID процесса как seed. Ключи, генерированные в этом окне, имели только 2^15 возможных значений — перебираемых за секунды.
Современные системы инициализируют CSPRNG ядра из: RDSEED на x86-64 (при наличии), инструкций RNG ARMv8.5-A, теплового шума различных периферийных устройств, тайминга прерываний, клавиатуры/мыши при интерактивной работе. На серверах с Intel Ice Lake или AMD Zen 3+ CSPRNG инициализируется в течение микросекунд после загрузки.
Для контейнеров Docker: /dev/urandom хоста передаётся по умолчанию. Никаких дополнительных действий не требуется. Для serverless (AWS Lambda, Cloudflare Workers) среда выполнения обрабатывает инициализацию энтропии для каждого вызова.
Сессионные токены и идентификаторы общего доступа
Для сервиса передачи файлов вы генерируете случайные идентификаторы для:
- Идентификаторов файлов в URL (атакующие не должны угадать валидные ID)
- Токенов общего доступа для защищённых паролем ссылок
- CSRF-токенов
- Ключей шифрования (AES-ключи на каждый файл)
- Nonce для GCM
Минимальная длина: 128 бит (16 байт) для устойчивости к коллизиям и непредсказуемости, 256 бит (32 байта) для ключей. URL-безопасное кодирование через base64url добавляет ~33% длины; шестнадцатеричное — 100%.
32-байтный токен в кодировке base64url содержит 43 символа и практически свободен от коллизий при энтропии 2^256.
Тестирование на слабый RNG
Признаки того, что ваш RNG сломан или слаб:
- Идентичные токены от разных запросов (коллизия в том, что должно быть огромным пространством)
- Вывод проходит визуальные тесты, но проваливается в статистических батареях
dieharderилиPractRand - Повторное использование seed после перезапуска процесса — каждое развёртывание использует одно начальное состояние
- Сгенерированные ключи попадают в паттерны (например, первые 4 байта варьируются, но последние 28 идентичны)
В продакшне вы, скорее всего, не заметите этих признаков, если только что-то катастрофически не пошло не так. Режим отказа обычно молчаливый: атаки просто становятся практичными в том, что должно быть пространством 2^256.
Аудит: каждый вызов Math.random() в кодовой базе должен быть проверен. Поиск Math.random по исходному дереву — хорошая еженедельная гигиеническая проверка. Преобразование любого security-релевантного вызова в crypto.getRandomValues занимает минуты и предотвращает реальные уязвимости.
Случайные строки и UUID
Для удобочитаемых идентификаторов crypto.randomUUID() возвращает UUID v4 (122 бита случайности) в стандартном формате:
const id = crypto.randomUUID();
// "f47ac10b-58cc-4372-a567-0e02b2c3d479"
Поддерживается в Chrome 92+, Firefox 95+, Safari 15.4+, Node 14.17+. Подходит для первичных ключей баз данных, идентификаторов API-запросов и некритичных для безопасности уникальных идентификаторов. Используйте явный getRandomValues для всего, что требует пользовательских форматов или повышенной энтропии.
На серверах: избегайте собственных пулов RNG
Некоторые серверные фреймворки предлагают собственные пулы случайности, заявляя о «смешивании» системного CSPRNG с энтропией на уровне приложения. Относитесь к этому с подозрением. Пользовательское смешивание редко улучшает вывод ядра и может молча снижать энтропию при наличии ошибок.
При использовании Node или крупной среды выполнения встроенный crypto.randomBytes корректен и быстр. Не заменяйте его сторонними смесителями.
Итог
Каждый криптографический элемент приложения для передачи файлов зависит от непредсказуемых случайных байтов. AES-ключи, GCM nonce, соли PBKDF2, токены общего доступа, CSRF-токены, идентификаторы сессий — всем нужен один и тот же примитив: crypto.getRandomValues() в браузерах, crypto.randomBytes() или webcrypto.getRandomValues в Node. Используйте именно их. Никогда Math.random(). Никогда временны́е метки. Никогда собственных смесителей.
HexaTransfer выводит каждый AES-ключ на файл, nonce и URL-идентификатор из crypto.getRandomValues() на клиенте. Идентификаторы общего доступа на стороне сервера получаются из crypto.randomBytes. Один API, согласованное поведение, никакого способа случайно внести предсказуемые биты в систему.
Примитив прост именно потому, что должен быть таким. Прост, корректен и доступен везде — именно так должны работать криптографические основы.
Попробуйте на hexatransfer.com — бесплатно, без регистрации, до 10 ГБ.
Безопасная отправка больших файлов со сквозным шифрованием
Передавайте файлы до 10 ГБ бесплатно со сквозным шифрованием. Регистрация не требуется. Ваши файлы шифруются в браузере перед загрузкой — никто другой не может их прочитать.
Отправить файл