Криптографически стойкая генерация случайных чисел: пакет crypto/rand

Краткое описание

crypto/rand — пакет Go для генерации криптографически стойких случайных чисел, использующий системный источник энтропии операционной системы. В отличие от math/rand, который основан на детерминированном математическом алгоритме и может быть восстановлен при достаточном количестве образцов, crypto/rand создаёт действительно непредсказуемые числа. Используется для задач, где безопасность критична: генерация паролей, токенов, ключей шифрования, PIN-кодов, сессионных идентификаторов. Недостаток: работает ~40 раз медленнее math/rand из-за обращения к системному источнику энтропии. Требует импорта math/big для работы с типом big.Int. Синтаксис: rand.Int(rand.Reader, max) возвращает (*big.Int, error).


Проблема псевдослучайных чисел в math/rand

Как работают псевдослучайные генераторы

PRNG (Pseudo-Random Number Generator) основаны на математических формулах, где каждое следующее число зависит от предыдущего.

Пример: линейный конгруэнтный генератор (LCG)

Формула Википедии:

\[X_{n+1} = (a \cdot X_n + c) \mod m\]

Компоненты:

  • $X_0$ — начальное значение (seed)
  • $(X_n$ — предыдущее число
  • $X_{n+1}$ — следующее число
  • $a, c, m$ — константы алгоритма

Пример вычисления (a=5, c=3, m=16, seed=7):

Шаг 1: $X_0 = 7$ (seed)

Шаг 2: $X_1 = (5 \cdot 7 + 3) \mod 16 = 38 \mod 16 = 6$

Шаг 3: $X_2 = (5 \cdot 6 + 3) \mod 16 = 33 \mod 16 = 1$

Шаг 4: $X_3 = (5 \cdot 1 + 3) \mod 16 = 8 \mod 16 = 8$

Последовательность: 7 → 6 → 1 → 8 → …

Уязвимость: предсказуемость

Проблема: Зная алгоритм и имея достаточно образцов, можно:

  1. Восстановить seed
  2. Предсказать будущие числа
  3. Воспроизвести всю последовательность

Пример атаки:

// Атакующий видит несколько чисел из math/rand
// Числа: 81, 87, 47, 59, 81...
// Зная алгоритм, можно восстановить seed
// И предсказать следующие: 87, 47, 59...

Почему это опасно для безопасности

Сценарий 1: Генерация PIN-кода

// ❌ НЕБЕЗОПАСНО
pin := rand.Intn(10000)  // math/rand
// Атакующий может предсказать PIN по предыдущим

Сценарий 2: Токен сессии

// ❌ НЕБЕЗОПАСНО
sessionID := rand.Int()  // math/rand
// Атакующий может подобрать sessionID других пользователей

Сценарий 3: Ключ шифрования

// ❌ НЕБЕЗОПАСНО
key := rand.Intn(1000000)  // math/rand
// Атакующий может восстановить ключ

Решение: crypto/rand

Принцип работы

crypto/rand использует криптографически стойкий источник случайности операционной системы (CSPRNG — Cryptographically Secure PRNG):

  • Linux/Unix: /dev/urandom (системная энтропия)
  • Windows: CryptGenRandom API
  • macOS: arc4random / /dev/urandom

Откуда берётся энтропия

Источники системной энтропии:

  • Таймеры прерываний
  • Движения мыши
  • Нажатия клавиш
  • Сетевой трафик
  • Аппаратные генераторы шума (HWRNG)
  • Температура процессора
  • Время доступа к диску

Результат: Последовательность невозможно предсказать, даже зная алгоритм.

Сравнение: math/rand vs crypto/rand

Характеристика math/rand crypto/rand
Алгоритм Математический (детерминированный) Системная энтропия (CSPRNG)
Предсказуемость ❌ Предсказуем при известном seed ✅ Непредсказуем
Скорость ✅ Быстрый (~1x) ❌ Медленнее (~40x)
Безопасность ❌ НЕ криптостойкий ✅ Криптографически стойкий
Применение Игры, симуляции, тесты Пароли, токены, ключи, PIN
Воспроизводимость ✅ Да (с фиксированным seed) ❌ Нет
Импорт import "math/rand" import "crypto/rand"
Синтаксис rand.Intn(100) rand.Int(rand.Reader, big.NewInt(100))
Ошибки Нет (panic при n<=0) Да (error при проблемах с ОС)

Импорт и зависимости

Необходимые импорты

import (
    "crypto/rand"  // Криптостойкая генерация
    "math/big"     // Работа с big.Int
)

Почему нужен math/big

crypto/rand.Int() возвращает *big.Int, а не обычный int.

Причины:

  1. Произвольная точность — может генерировать числа любого размера
  2. Безопасность — стандартные типы могут переполниться
  3. Единообразие — один API для чисел любого размера

Тип big.Int:

  • Целое число произвольной точности
  • Может хранить числа больше int64
  • Используется в криптографии для больших простых чисел

Синтаксис crypto/rand.Int()

Базовая форма

randomBig, err := rand.Int(rand.Reader, max)

Компоненты

  1. rand.Reader — источник энтропии (интерфейс io.Reader)
  2. max — верхняя граница типа *big.Int
  3. Возврат:
    • randomBig — случайное число типа *big.Int
    • err — ошибка (если системный источник недоступен)

Диапазон значений

Полуинтервал: [0, max) — от 0 включительно до max не включая.

Аналогично math/rand.Intn(n) по логике диапазона.


Пример 1: Генерация криптостойкого числа

Задача

Сгенерировать криптографически стойкое число от 0 до 99 включительно.

Код

fmt.Println("--- Криптостойкое число [0, 99] ---")

// Создаём верхнюю границу (100, чтобы получить [0, 99])
max := big.NewInt(100)

// Генерируем число
randomBig, err := rand.Int(rand.Reader, max)

if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

// Преобразуем big.Int в обычный int64 для вывода
fmt.Printf("Криптостойкое число [0, 99]: %d\n", randomBig.Int64())

Пошаговое выполнение

Шаг 1: Создание big.Int со значением 100

max := big.NewInt(100)

Шаг 2: Обращение к rand.Reader (системный источник энтропии)

Шаг 3: Генерация случайного числа из [0, 100)

Шаг 4: Возврат randomBig (например, 42) и err = nil

Шаг 5: Проверка ошибки → err == nil → успех

Шаг 6: Преобразование *big.Intint64 через .Int64()

Шаг 7: Вывод: "Криптостойкое число [0, 99]: 42"

Результат (пример):

--- Криптостойкое число [0, 99] ---
Криптостойкое число [0, 99]: 73

Важные моменты

  1. Обработка ошибок обязательнаerr может быть не nil если системный источник недоступен
  2. big.NewInt(100) создаёт *big.Int, а не обычный int
  3. .Int64() преобразует *big.Int в int64 для удобства

Пример 2: Генерация безопасного 4-значного PIN-кода

Задача

Сгенерировать криптографически стойкий PIN-код из 4 цифр (диапазон 0000 - 9999).

Подход: генерация по цифрам

Логика:

  1. Генерируем 4 отдельные цифры [0, 9]
  2. Каждая цифра — независимый вызов crypto/rand
  3. Склеиваем в строку

Почему не одно число :

  • Проблема с ведущими нулями: 0042 превратится в 42
  • По цифрам: каждая цифра гарантированно 0-9

Код

fmt.Println("--- Генерация безопасного PIN-кода ---")
fmt.Println("Требование: 4-значный PIN (0000-9999)")
fmt.Println("Генерируем 4 отдельные цифры криптостойко:")

pin := ""
maxDigit := big.NewInt(10)  // Диапазон [0, 9]

// Генерируем 4 цифры
digit1, err := rand.Int(rand.Reader, maxDigit)
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

digit2, err := rand.Int(rand.Reader, maxDigit)
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

digit3, err := rand.Int(rand.Reader, maxDigit)
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

digit4, err := rand.Int(rand.Reader, maxDigit)
if err != nil {
    fmt.Println("Ошибка:", err)
    return
}

// Формируем PIN
pin = fmt.Sprintf("%d%d%d%d", digit1, digit2, digit3, digit4)

fmt.Printf("Сгенерированный PIN: %s\n", pin)

Пошаговое выполнение

Шаг 1: Создание maxDigit = big.NewInt(10) → диапазон [0, 9]

Шаг 2: Генерация первой цифры

  • rand.Int(rand.Reader, maxDigit) → например, 7
  • digit1 = 7

Шаг 3: Генерация второй цифры → например, 0

  • digit2 = 0

Шаг 4: Генерация третьей цифры → например, 5

  • digit3 = 5

Шаг 5: Генерация четвёртой цифры → например, 2

  • digit4 = 2

Шаг 6: Форматирование в строку: "7052"

Шаг 7: Вывод: "Сгенерированный PIN: 7052"

Результат (пример):

--- Генерация безопасного PIN-кода ---
Требование: 4-значный PIN (0000-9999)
Генерируем 4 отдельные цифры криптостойко:
Сгенерированный PIN: 7052

Улучшенная версия с циклом

pin := ""
maxDigit := big.NewInt(10)

for i := 0; i < 4; i++ {
    digit, err := rand.Int(rand.Reader, maxDigit)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    pin += digit.String()
}

fmt.Printf("PIN: %s\n", pin)

Почему это безопасно

math/rand (небезопасно):

// ❌ Предсказуемо
pin := rand.Intn(10000)
// Атакующий может восстановить seed
// и предсказать все будущие PIN-коды

crypto/rand (безопасно):

// ✅ Непредсказуемо
digit, _ := rand.Int(rand.Reader, big.NewInt(10))
// Каждая цифра использует системную энтропию
// Невозможно предсказать следующую цифру

Когда использовать math/rand vs crypto/rand

✅ Используйте math/rand когда:

1. Нужна скорость

// Генерация 1,000,000 чисел для симуляции
for i := 0; i < 1000000; i++ {
    value := rand.Intn(100)  // Быстро
    // обработка
}

2. Безопасность не критична

// Случайная позиция врага в игре
enemyX := rand.Intn(800)
enemyY := rand.Intn(600)

3. Нужна воспроизводимость (тестирование)

// Тест с фиксированным seed
rng := rand.New(rand.NewSource(42))
result := rng.Intn(100)  // Всегда 39 при seed=42

4. Демонстрация, обучение, прототипирование

// Пример для документации
dice := rand.Intn(6) + 1

5. Генерация тестовых данных

// Заполнение БД тестовыми пользователями
age := 18 + rand.Intn(47)  // Возраст 18-65

✅ Используйте crypto/rand когда:

1. Генерация паролей

// ✅ Безопасный пароль
password := generateSecurePassword()  // crypto/rand внутри

2. Токены доступа (API keys, JWT)

// ✅ JWT secret key
secretKey := make([]byte, 32)
rand.Read(secretKey)  // crypto/rand.Read()

3. PIN-коды для банковских карт

// ✅ Криптостойкий PIN
pin := generateSecurePIN()  // crypto/rand

4. Сессионные идентификаторы

// ✅ Уникальный session ID
sessionID := generateSessionID()  // crypto/rand

5. Ключи шифрования

// ✅ AES ключ
key := make([]byte, 32)  // 256-bit
rand.Read(key)  // crypto/rand

6. Одноразовые коды (OTP)

// ✅ Код подтверждения по SMS
code := generateOTP()  // crypto/rand

7. Salt для хеширования паролей

// ✅ Соль для bcrypt/scrypt
salt := make([]byte, 16)
rand.Read(salt)  // crypto/rand

Производительность: цена безопасности

Сравнение скорости

Тесты показывают: crypto/rand работает ~40 раз медленнее math/rand.

Benchmark (примерные данные):

math/rand.Intn(100):     10 ns/op
crypto/rand.Int(max):    400 ns/op  (~40x медленнее)

Почему медленнее

math/rand:

  • Вычисления в памяти
  • Простая арифметика
  • Нет системных вызовов

crypto/rand:

  • Обращение к ОС (системный вызов)
  • Чтение из /dev/urandom или API
  • Сбор энтропии
  • Криптографические преобразования

Когда производительность критична

Сценарий 1: Генерация миллионов чисел

// Симуляция Монте-Карло: 10,000,000 итераций
// math/rand: ~0.1 секунды
// crypto/rand: ~4 секунды
// Вывод: используйте math/rand

Сценарий 2: Генерация одного токена

// Создание API токена при регистрации
// crypto/rand: ~400 наносекунд
// Вывод: производительность некритична, используйте crypto/rand

Правило: Если генерируете < 1000 чисел в секунду и безопасность важна → используйте crypto/rand.


Применение в реальных задачах

Пример 1: Генерация JWT secret key

JWT (JSON Web Token) требует секретный ключ для подписи токенов.

import (
    "crypto/rand"
    "encoding/base64"
)

func generateJWTSecret() (string, error) {
    // Создаём 32-байтовый ключ (256 бит)
    key := make([]byte, 32)
    
    // Заполняем криптостойкими случайными байтами
    _, err := rand.Read(key)
    if err != nil {
        return "", err
    }
    
    // Кодируем в base64 для удобства хранения
    return base64.StdEncoding.EncodeToString(key), nil
}

// Использование:
secret, err := generateJWTSecret()
// secret = "Kv7xM9kF2nL8pQ3wR5sT1uY6zA4bC7dE..."

Почему crypto/rand:

  • JWT secret должен быть непредсказуем
  • Компрометация secret → компрометация всех токенов
  • Безопасность критична

Пример 2: Генерация session ID

func generateSessionID() (string, error) {
    // 16 байт = 128 бит энтропии
    sessionBytes := make([]byte, 16)
    
    _, err := rand.Read(sessionBytes)
    if err != nil {
        return "", err
    }
    
    // Кодируем в hex для удобства
    return fmt.Sprintf("%x", sessionBytes), nil
}

// Результат: "3a7f9c2e1b5d8f4a6c9e2d1f8b3a7c5e"

Пример 3: Генерация OTP (одноразовый код)

func generateOTP() (string, error) {
    // 6-значный код
    max := big.NewInt(1000000)
    
    num, err := rand.Int(rand.Reader, max)
    if err != nil {
        return "", err
    }
    
    // Форматируем с ведущими нулями
    return fmt.Sprintf("%06d", num.Int64()), nil
}

// Результат: "042753" (всегда 6 цифр)

Пример 4: Генерация соли для паролей

func generateSalt() ([]byte, error) {
    // 16 байт соли
    salt := make([]byte, 16)
    
    _, err := rand.Read(salt)
    if err != nil {
        return nil, err
    }
    
    return salt, nil
}

// Использование с bcrypt:
salt, _ := generateSalt()
hashedPassword := bcrypt.GenerateFromPassword(password, salt)

Работа с rand.Read() для байтов

Синтаксис rand.Read()

bytes := make([]byte, n)
_, err := rand.Read(bytes)

Возврат:

  • Количество прочитанных байт (всегда n)
  • Ошибка (если источник недоступен)

Пример: генерация 32 байт

import "crypto/rand"

func main() {
    // Создаём слайс на 32 байта
    randomBytes := make([]byte, 32)
    
    // Заполняем криптостойкими случайными байтами
    _, err := rand.Read(randomBytes)
    if err != nil {
        fmt.Println("Ошибка:", err)
        return
    }
    
    fmt.Printf("Случайные байты (hex): %x\n", randomBytes)
}

Результат (пример):

Случайные байты (hex): 7a3f9c2e1b5d8f4a6c9e2d1f8b3a7c5e9d4f1a2b3c5e6f7a8b9c0d1e2f3a4b5c

Когда использовать rand.Read()

  • Генерация ключей шифрования
  • Создание salt для хеширования
  • Генерация случайных токенов
  • Инициализация криптографических алгоритмов

Обработка ошибок

Почему ошибки возможны

crypto/rand зависит от системного источника энтропии:

  • /dev/urandom может быть недоступен (редко)
  • Системные ресурсы исчерпаны
  • Проблемы с HWRNG (аппаратным генератором)

Правильная обработка

❌ Плохо: игнорирование ошибок

num, _ := rand.Int(rand.Reader, big.NewInt(100))
// Если err != nil, num может быть nil → паника!

✅ Хорошо: проверка ошибок

num, err := rand.Int(rand.Reader, big.NewInt(100))
if err != nil {
    log.Fatal("Не удалось сгенерировать случайное число:", err)
    return
}
// Безопасно использовать num

Типичная ошибка

Error reading random: read /dev/urandom: resource temporarily unavailable

Решение: Повтор попытки или fallback на другой источник (редко нужно).


Best Practices

1. Всегда проверяйте ошибки

// ✅ Хорошо
num, err := rand.Int(rand.Reader, max)
if err != nil {
    return fmt.Errorf("генерация числа: %w", err)
}

2. Используйте crypto/rand для безопасности

// ❌ Плохо: токен предсказуем
token := rand.Int()  // math/rand

// ✅ Хорошо: криптостойкий токен
tokenBytes := make([]byte, 32)
rand.Read(tokenBytes)  // crypto/rand

3. Документируйте выбор генератора

// Генерация демо-данных для тестирования (не критично)
func generateTestAge() int {
    return 18 + rand.Intn(47)  // math/rand OK
}

// Генерация API ключа для продакшена (критично!)
func generateAPIKey() (string, error) {
    // crypto/rand для безопасности
    key := make([]byte, 32)
    _, err := rand.Read(key)
    // ...
}

4. Оптимизируйте частые вызовы

// ❌ Плохо: 1000 системных вызовов
for i := 0; i < 1000; i++ {
    num, _ := rand.Int(rand.Reader, big.NewInt(100))
    // ...
}

// ✅ Лучше: один вызов, много байт
randomBytes := make([]byte, 1000)
rand.Read(randomBytes)
for _, b := range randomBytes {
    // используем байты
}

5. Используйте достаточную длину ключей

// ❌ Плохо: 4 байта = 32 бита (слабо)
key := make([]byte, 4)
rand.Read(key)

// ✅ Хорошо: 32 байта = 256 бит (сильно)
key := make([]byte, 32)
rand.Read(key)

Типичные ошибки

Ошибка 1: Использование math/rand для паролей

// ❌ ОПАСНО: пароль предсказуем
password := rand.Intn(1000000)

// ✅ БЕЗОПАСНО: криптостойкий
passwordBytes := make([]byte, 16)
rand.Read(passwordBytes)
password := base64.StdEncoding.EncodeToString(passwordBytes)

Ошибка 2: Игнорирование ошибок

// ❌ Плохо: не проверяем err
num, _ := rand.Int(rand.Reader, big.NewInt(100))

// ✅ Хорошо: обрабатываем ошибку
num, err := rand.Int(rand.Reader, big.NewInt(100))
if err != nil {
    return err
}

Ошибка 3: Неправильное преобразование big.Int

// ❌ Плохо: может паниковать если num > int64
num, _ := rand.Int(rand.Reader, big.NewInt(100))
value := int(num.Int64())  // Небезопасно для больших чисел

// ✅ Хорошо: проверяем диапазон
if !num.IsInt64() {
    return errors.New("число слишком большое")
}
value := num.Int64()

Ошибка 4: Повторное использование max

// ❌ Неэффективно: создаём big.Int в цикле
for i := 0; i < 1000; i++ {
    num, _ := rand.Int(rand.Reader, big.NewInt(100))
}

// ✅ Эффективно: создаём max один раз
max := big.NewInt(100)
for i := 0; i < 1000; i++ {
    num, _ := rand.Int(rand.Reader, max)
}

Ключевые моменты

  1. crypto/rand — криптографически стойкий генератор на основе системной энтропии
  2. math/rand — быстрый, но предсказуемый генератор для некритичных задач
  3. Производительность — crypto/rand ~40x медленнее math/rand
  4. Безопасность — crypto/rand невозможно предсказать, math/rand можно
  5. Синтаксисrand.Int(rand.Reader, max) возвращает (*big.Int, error)
  6. Импорты — нужны crypto/rand и math/big
  7. Ошибки — всегда проверяйте err от crypto/rand
  8. Применение — пароли, токены, ключи, PIN требуют crypto/rand
  9. rand.Read() — для генерации случайных байт (ключи, соли)
  10. Правило выбора — безопасность критична → crypto/rand, иначе → math/rand

Что запомнить

  • crypto/rand для безопасности — пароли, токены, ключи, PIN
  • math/rand для скорости — игры, симуляции, тесты
  • ~40x медленнее — цена криптостойкости
  • Системная энтропия — источник непредсказуемости
  • big.NewInt(max) — создание верхней границы
  • rand.Int(rand.Reader, max) — генерация числа
  • Всегда проверять err — источник может быть недоступен
  • rand.Read(bytes) — для байтовых данных
  • Предсказуемость опасна — math/rand не для секретов
  • JWT, API keys, OTP — используйте crypto/rand

Итог: Пакет crypto/rand предоставляет криптографически стойкую генерацию случайных чисел через системные источники энтропии, что делает последовательность непредсказуемой даже при знании алгоритма. В отличие от math/rand, основанного на детерминированных математических формулах, crypto/rand использует /dev/urandom (Linux), CryptGenRandom (Windows) или аналоги для сбора энтропии из аппаратных событий. Цена безопасности — производительность: crypto/rand работает ~40 раз медленнее. Используйте crypto/rand для генерации паролей, токенов, ключей шифрования, PIN-кодов, сессионных ID и других критичных для безопасности данных. Для игр, симуляций и тестов достаточно math/rand. Синтаксис требует math/big для работы с big.Int, и обязательна проверка ошибок, так как системный источник энтропии может быть временно недоступен.

(https://pkg.go.dev/math/rand)