Основой всех безопасных случайных данных в Web Crypto API выступает
метод crypto.getRandomValues(). Он заполняет типизированные
массивы значениями, полученными из системного криптографического
генератора случайных чисел.
Ключевая особенность заключается в том, что результат не предсказуем и не зависит от времени, состояния памяти или алгоритмических последовательностей.
const buffer = new Uint8Array(32);
crypto.getRandomValues(buffer);
Используются только TypedArray-структуры:
Uint8Array, Uint16Array,
Uint32Array и их производные. Обычные массивы JavaScript не
поддерживаются, что связано с требованиями к низкоуровневому доступу к
памяти.
Прямое применение операций вроде остатка от деления приводит к статистическому смещению. Это особенно критично при генерации значений в ограниченном диапазоне.
Неправильный подход:
const x = crypto.getRandomValues(new Uint8Array(1))[0] % 10;
Распределение становится неравномерным, так как 256 не делится на 10 без остатка.
Корректный подход основан на методе отбрасывания значений (rejection sampling):
function randomInt(max) {
const range = 256 - (256 % max);
const buf = new Uint8Array(1);
let value;
do {
crypto.getRandomValues(buf);
value = buf[0];
} while (value >= range);
return value % max;
}
Для больших диапазонов используются Uint32Array и
соответствующее масштабирование с учётом переполнения 32-битного
пространства.
UUID версии 4 строится на случайных байтах с фиксированными битами версии и варианта. Web Crypto API позволяет формировать его без внешних зависимостей.
function uuidv4() {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes].map(b => b.toString(16).padStart(2, '0'));
return (
hex.slice(0, 4).join('') + '-' +
hex.slice(4, 6).join('') + '-' +
hex.slice(6, 8).join('') + '-' +
hex.slice(8, 10).join('') + '-' +
hex.slice(10, 16).join('')
);
}
Фиксация битов версии и варианта обеспечивает соответствие RFC 4122, при этом вся энтропия поступает исключительно из криптографического генератора.
Сессионные токены, CSRF-значения и одноразовые nonce требуют высокой энтропии и компактного представления. Часто используется кодирование в Base64URL.
function randomToken(byteLength = 32) {
const bytes = new Uint8Array(byteLength);
crypto.getRandomValues(bytes);
let binary = '';
for (const b of bytes) {
binary += String.fromCharCode(b);
}
return btoa(binary)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
}
Длина 32 байта обеспечивает 256 бит энтропии, что считается достаточным для большинства сценариев аутентификации и защиты от перебора.
При использовании SubtleCrypto для PBKDF2 или других
хеш-функций соль должна быть уникальной и непредсказуемой.
const salt = new Uint8Array(16);
crypto.getRandomValues(salt);
const keyMaterial = await crypto.subtle.importKey(
"raw",
new TextEncoder().encode("password"),
{ name: "PBKDF2" },
false,
["deriveBits"]
);
const derived = await crypto.subtle.deriveBits(
{
name: "PBKDF2",
salt,
iterations: 100000,
hash: "SHA-256"
},
keyMaterial,
256
);
Повторное использование соли для разных паролей приводит к деградации безопасности всей схемы.
При использовании AES-GCM критическим требованием является уникальность nonce. Стандартная длина — 12 байт.
function generateIv() {
const iv = new Uint8Array(12);
crypto.getRandomValues(iv);
return iv;
}
Повторное использование IV с одинаковым ключом приводит к утечке информации о зашифрованных данных. Генерация должна исключать любые детерминированные или счётчиковые подходы без строгого контроля состояния.
Алгоритм Фишера–Йейтса в сочетании с
crypto.getRandomValues позволяет создавать равномерно
распределённые перестановки.
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = randomInt(i + 1);
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
Любые реализации на основе Math.random() не обеспечивают
требуемого уровня энтропии и предсказуемы при частичном наблюдении
состояния.
При создании паролей, кодов подтверждения или invite-кодов используется ограниченный набор символов. Важно избегать смещений распределения.
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
function randomString(length) {
const result = [];
for (let i = 0; i < length; i++) {
const idx = randomInt(alphabet.length);
result.push(alphabet[idx]);
}
return result.join('');
}
При увеличении алфавита (например, добавлении символов) возрастает риск появления bias при некорректном масштабировании.
Web Crypto API оперирует байтами, поэтому преобразование в строки требует аккуратного подхода.
Hex-кодирование:
function toHex(buffer) {
return [...new Uint8Array(buffer)]
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
Base64URL часто используется для токенов, поскольку безопасен для URL и заголовков HTTP.
На практике встречаются систематические ошибки, приводящие к снижению криптостойкости:
Math.random() вместо
crypto.getRandomValuesЛюбая из этих ошибок может полностью разрушить модель безопасности даже при использовании сильных алгоритмов шифрования.
Web Crypto API доступен только в безопасных контекстах: HTTPS или
localhost. В небезопасных условиях объект crypto.subtle
может быть недоступен, а вызов getRandomValues ограничен
или запрещён.
Также важно учитывать, что генератор случайных чисел является системным ресурсом, и его поведение зависит от операционной системы. Алгоритмическая реализация скрыта и не подлежит контролю со стороны JavaScript.
crypto.getRandomValues() работает значительно быстрее
любых пользовательских реализаций PRNG, но имеет ограничение на размер
буфера. При необходимости генерации больших объёмов данных требуется
разбиение на чанки.
function fillLargeBuffer(size) {
const result = new Uint8Array(size);
for (let offset = 0; offset < size; offset += 65536) {
const chunk = result.subarray(offset, offset + 65536);
crypto.getRandomValues(chunk);
}
return result;
}
Ключевая характеристика криптографической случайности заключается в отсутствии воспроизводимости. Повторное выполнение кода не позволяет получить идентичные результаты даже при одинаковом состоянии приложения.
Это исключает возможность тестирования через фиксацию seed, что характерно для обычных PRNG. В криптографических сценариях такая детерминированность считается уязвимостью, а не преимуществом.