Криптографически стойкая генерация случайных чисел: пакет 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 → …
Уязвимость: предсказуемость
Проблема: Зная алгоритм и имея достаточно образцов, можно:
- Восстановить seed
- Предсказать будущие числа
- Воспроизвести всю последовательность
Пример атаки:
// Атакующий видит несколько чисел из 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:
CryptGenRandomAPI - 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.
Причины:
- Произвольная точность — может генерировать числа любого размера
- Безопасность — стандартные типы могут переполниться
- Единообразие — один API для чисел любого размера
Тип big.Int:
- Целое число произвольной точности
- Может хранить числа больше
int64 - Используется в криптографии для больших простых чисел
Синтаксис crypto/rand.Int()
Базовая форма
randomBig, err := rand.Int(rand.Reader, max)
Компоненты
rand.Reader— источник энтропии (интерфейсio.Reader)max— верхняя граница типа*big.Int- Возврат:
randomBig— случайное число типа*big.Interr— ошибка (если системный источник недоступен)
Диапазон значений
Полуинтервал: [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.Int → int64 через .Int64()
Шаг 7: Вывод: "Криптостойкое число [0, 99]: 42"
Результат (пример):
--- Криптостойкое число [0, 99] ---
Криптостойкое число [0, 99]: 73
Важные моменты
- Обработка ошибок обязательна —
errможет быть неnilесли системный источник недоступен - big.NewInt(100) создаёт
*big.Int, а не обычныйint .Int64()преобразует*big.Intвint64для удобства
Пример 2: Генерация безопасного 4-значного PIN-кода
Задача
Сгенерировать криптографически стойкий PIN-код из 4 цифр (диапазон 0000 - 9999).
Подход: генерация по цифрам
Логика:
- Генерируем 4 отдельные цифры
[0, 9] - Каждая цифра — независимый вызов
crypto/rand - Склеиваем в строку
Почему не одно число :
- Проблема с ведущими нулями:
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)→ например,7digit1 = 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)
}
Ключевые моменты
- crypto/rand — криптографически стойкий генератор на основе системной энтропии
- math/rand — быстрый, но предсказуемый генератор для некритичных задач
- Производительность — crypto/rand ~40x медленнее math/rand
- Безопасность — crypto/rand невозможно предсказать, math/rand можно
- Синтаксис —
rand.Int(rand.Reader, max)возвращает(*big.Int, error) - Импорты — нужны
crypto/randиmath/big - Ошибки — всегда проверяйте
errот crypto/rand - Применение — пароли, токены, ключи, PIN требуют crypto/rand
- rand.Read() — для генерации случайных байт (ключи, соли)
- Правило выбора — безопасность критична → 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)