Генерация случайных токенов и идентификаторов

Генерация случайных данных в контексте безопасности отличается от обычной псевдослучайности, используемой в математических библиотеках или игровых алгоритмах. Основное требование здесь — непредсказуемость. В WebCrypto API для этой цели используется источник криптографически стойкой энтропии, предоставляемый окружением браузера.

Ключевой механизм — метод crypto.getRandomValues(), входящий в интерфейс Crypto. Он заполняет заранее созданный буфер случайными значениями, полученными из системного генератора случайных чисел операционной системы.


Базовый механизм crypto.getRandomValues

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

const array = new Uint8Array(16);
crypto.getRandomValues(array);

console.log(array);

В этом примере создаётся массив из 16 байт, каждый из которых заполняется случайным значением от 0 до 255.

Поддерживаемые типы:

  • Int8Array
  • Uint8Array
  • Int16Array
  • Uint16Array
  • Int32Array
  • Uint32Array

Не допускаются типы с плавающей точкой (Float32Array, Float64Array), так как их формат не гарантирует равномерного распределения битовой энтропии.


Генерация случайных токенов фиксированной длины

На практике часто требуется токен для сессий, CSRF-защиты или временной авторизации. Один из распространённых подходов — преобразование байтов в строку.

Пример генерации hex-токена

function generateHexToken(length = 32) {
    const array = new Uint8Array(length / 2);
    crypto.getRandomValues(array);

    return Array.from(array, byte =>
        byte.toString(16).padStart(2, '0')
    ).join('');
}

const token = generateHexToken(32);
console.log(token);

Особенности:

  • каждый байт преобразуется в два символа hex
  • итоговая длина строки = length
  • равномерность распределения сохраняется на уровне байтов

Генерация base64 токенов

Hex-формат не всегда эффективен по размеру. Более компактное представление — Base64.

function generateBase64Token(byteLength = 32) {
    const array = new Uint8Array(byteLength);
    crypto.getRandomValues(array);

    let binary = '';
    for (let i = 0; i < array.length; i++) {
        binary += String.fromCharCode(array[i]);
    }

    return btoa(binary);
}

const token = generateBase64Token(32);
console.log(token);

Особенности:

  • компактнее hex примерно на 33%
  • требует btoa, работающего со строками бинарного представления
  • может содержать символы +, /, =

Для URL-safe варианта обычно применяется замена символов:

function base64Url(token) {
    return token
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
}

Генерация идентификаторов UUID

Современные браузеры поддерживают встроенный метод:

const id = crypto.randomUUID();
console.log(id);

Формат результата соответствует UUID v4:

xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx

Особенности:

  • гарантированная криптографическая стойкость
  • отсутствие необходимости ручной реализации алгоритма
  • встроенная нормализация версии 4

UUID используется в:

  • идентификаторах сущностей в базе данных
  • ключах кеширования
  • распределённых системах

Ручная генерация UUID v4 через getRandomValues

Несмотря на наличие встроенного метода, полезно понимать структуру UUID.

function generateUUID() {
    const bytes = new Uint8Array(16);
    crypto.getRandomValues(bytes);

    // версия 4
    bytes[6] = (bytes[6] & 0x0f) | 0x40;
    // variant RFC4122
    bytes[8] = (bytes[8] & 0x3f) | 0x80;

    const hex = Array.from(bytes, 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('')
    );
}

console.log(generateUUID());

Структура UUID:

  • 128 бит данных
  • фиксированные биты версии и варианта
  • стандарт RFC 4122

Ошибки при генерации случайных идентификаторов

При работе с WebCrypto часто допускаются критические ошибки:

1. Использование Math.random()

const id = Math.random().toString(36);

Недостатки:

  • предсказуемость
  • отсутствие криптографической стойкости
  • зависимость от seed генератора JS-движка

2. Повторное использование буфера

const buffer = new Uint8Array(16);

function badToken() {
    return crypto.getRandomValues(buffer);
}

Проблема:

  • возможна утечка состояния между вызовами
  • риск гонок в асинхронном коде

Правильный подход — создавать новый буфер каждый раз.


3. Неправильное преобразование байтов

String.fromCharCode(byte)

Используется только как промежуточный шаг. Нельзя интерпретировать байты как UTF-16 символы напрямую для криптографических целей.


Производительность генерации

crypto.getRandomValues() работает синхронно и оптимизирован на уровне браузера и ОС.

Сравнение:

  • Math.random() — быстрее, но небезопасно
  • getRandomValues() — безопасно, но чуть дороже по стоимости вызова

Практически разница заметна только при массовой генерации миллионов значений.


Генерация токенов заданного алфавита

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

function generateToken(length = 10) {
    const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
    const array = new Uint8Array(length);
    crypto.getRandomValues(array);

    return Array.from(array, x =>
        chars[x % chars.length]
    ).join('');
}

console.log(generateToken());

Особенности:

  • исключены похожие символы (O/0, I/1)
  • равномерное распределение достигается через модуль

Использование в сессионных идентификаторах

Сессионные токены требуют:

  • высокой энтропии
  • уникальности
  • невозможности предсказания

Типичная схема:

function sessionId() {
    const bytes = new Uint8Array(24);
    crypto.getRandomValues(bytes);

    return btoa(String.fromCharCode(...bytes))
        .replace(/\+/g, '-')
        .replace(/\//g, '_')
        .replace(/=+$/, '');
}

Размер 24 байта даёт 192 бита энтропии, что достаточно для большинства веб-приложений.


Связь с Web Crypto API

Хотя getRandomValues() формально не является криптографической функцией шифрования или подписи, он является частью Web Crypto API и служит фундаментом для:

  • генерации ключей
  • создания nonce (чисел, используемых один раз)
  • соли для хеширования паролей
  • токенов аутентификации

Требования безопасности к токенам

При проектировании идентификаторов учитываются:

  • энтропия (не менее 128 бит для сессионных токенов)
  • отсутствие предсказуемых шаблонов
  • уникальность в распределённой системе
  • ограниченный срок жизни

Использование в криптографических протоколах

Случайные значения часто используются как:

  • IV (initialization vector)
  • nonce в AES-GCM
  • соль в PBKDF2 / Argon2 (в браузере через сторонние реализации)
  • challenge в authentication flow

Пример nonce для AES-GCM:

const iv = crypto.getRandomValues(new Uint8Array(12));

12 байт — рекомендуемый размер для GCM-режима.


Ограничения и особенности браузерной реализации

  • невозможность контролировать источник энтропии
  • отсутствие детерминированного режима генерации
  • блокировка в небезопасных контекстах (HTTP вместо HTTPS)
  • зависимость от реализации ОС

Практическая модель выбора формата токена

  • hex — простота и читаемость
  • base64 — компактность
  • base64url — безопасная передача в URL
  • UUID — стандартизация и совместимость
  • кастомный алфавит — пользовательские сценарии (коды, приглашения)