Практические паттерны безопасной генерации случайных данных

Основой всех безопасных случайных данных в 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 без сторонних библиотек

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
);

Повторное использование соли для разных паролей приводит к деградации безопасности всей схемы.


Генерация IV и nonce для симметричного шифрования

При использовании 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
  • применение остатка от деления без rejection sampling
  • повторное использование IV в AES-GCM
  • фиксированная соль для разных пользователей
  • генерация случайных значений через временные метки или PID
  • усечение байтов без контроля распределения

Любая из этих ошибок может полностью разрушить модель безопасности даже при использовании сильных алгоритмов шифрования.


Ограничения и поведение в различных окружениях

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. В криптографических сценариях такая детерминированность считается уязвимостью, а не преимуществом.