Привет, Хабр!
В версии Go 1.22 пакет math/rand/v2
претерпел значительные изменения, а в частности - переход на ChaCha8Rand. Этот новый генератор представляет собой модификацию широко известного и проверенного временем шифра ChaCha8, который используется в протоколах TLS и SSH.
Немного про сам генератор
ChaCha8Rand основывается на алгоритме ChaCha8, который сам по себе является облегченной версией шифра ChaCha20. ChaCha8 выполняет восемь раундов перестановок и замен ключа и блока данных, что гарантирует высокий уровень энтропии и защиту от различных криптографических атак.
ChaCha8Rand принимает 32-байтный ключ, его называют семенем, который используется в качестве начальной точки для генерации случайных чисел. Ключ устанавливает начальное состояние алгоритма, которое затем преобразуется в псевдослучайное состояние для генерации последовательности чисел.
ChaCha8Rand генерирует 64-битные псевдослучайные числа путем прямого вызова алгоритма ChaCha8. Внутри он инициализирует ChaCha8 с заданным ключом и использует блоки данных, которые алгоритм создает при каждом запросе нового случайного значения. Процесс итеративный: каждый вызов генератора ChaCha8Rand изменяет внутреннее состояние и формирует новое случайное число.
ChaCha8Rand поддерживает функции сериализации и десериализации состояния, что позволяет сохранять и восстанавливать текущее состояние генератора.
Реализация ChaCha8Rand в Go
В пакете math/rand/v2
, ChaCha8 представлен структурой ChaCha8
, которая хранит внутреннее состояние генератора и предоставляет методы для управления им. Состояние инициализируется 32-байтным семенем, передаваемым в функцию NewChaCha8
, а сам процесс генерации числа осуществляется методом Uint64()
, который возвращает следующее 64-битное псевдослучайное число.
Основные функции и методы, которые используют ChaCha8Rand, включают:
IntN и Int64N: функции генерируют псевдослучайное целое число в заданном диапазоне. Например,
IntN(n int) int
возвращает случайное число от 0 до n-1. Они обеспечивают равномерное распределение значений в указанном диапазоне.Float64: генерирует псевдослучайное число с плавающей точкой между 0.0 и 1.0, не включая 1.0.
Perm: создаёт случайную перестановку чисел от 0 до n-1.
Кроме того, в math/rand/v2
реализованы новые методы MarshalBinary и UnmarshalBinary для объектов, которые реализуют интерфейс Source, включая ChaCha8.
Генератор создается путем вызова NewChaCha8
с 32-байтным массивом начальных значений. Если массив не заполнен случайными данными, генератор не сможет обеспечить криптографическую стойкость.
После инициализации генератора метод Uint64()
возвращает следующее псевдослучайное 64-битное число на основе внутреннего состояния. Этот метод использует ChaCha8 для выполнения восемь раундов перестановок и замен.
Примеры
Инициализация генератора и генерация чисел:
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
// создаем 32-байтное семя для инициализации генератора
seed := [32]byte{'s', 'o', 'm', 'e', 'k', 'e', 'y', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f'}
// инициализируем новый генератор ChaCha8
generator := rand.NewChaCha8(seed)
// получаем несколько случайных чисел
fmt.Println(generator.Uint64())
fmt.Println(generator.Uint64())
}
Генерация криптографически безопасного случайного числа:
package main
import (
"fmt"
"math/rand/v2"
)
func main() {
seed := [32]byte{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v'}
generator := rand.NewChaCha8(seed)
fmt.Println("Secure random number:", generator.Uint64())
}
Генерация случайной строки для токенов или сессий:
package main
import (
"fmt"
"math/rand/v2"
)
func randomBytes(n int) []byte {
seed := [32]byte{'s', 'e', 'e', 'd', 'p', 'h', 'r', 'a', 's', 'e', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm'}
generator := rand.NewChaCha8(seed)
b := make([]byte, n)
for i := range b {
b[i] = byte(generator.Uint32() % 256)
}
return b
}
func main() {
fmt.Println("Random session token:", randomBytes(16))
}
Использование в шифровании:
package main
import (
"crypto/cipher"
"fmt"
"golang.org/x/crypto/chacha20"
"math/rand/v2"
)
func main() {
seed := [32]byte{'k', 'e', 'y', 'p', 'h', 'r', 'a', 's', 'e', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n'}
generator := rand.NewChaCha8(seed)
nonce := [12]byte{} // нормально было бы использовать случайные значения
key := [32]byte{}
for i := range key {
key[i] = byte(generator.Uint32() % 256)
}
cipher, err := chacha20.NewUnauthenticatedCipher(key[:], nonce[:])
if err != nil {
panic(err)
}
plaintext := []byte("This is a secret message.")
ciphertext := make([]byte, len(plaintext))
cipher.XORKeyStream(ciphertext, plaintext)
fmt.Println("Encrypted:", ciphertext)
fmt.Println("Decrypted message:")
cipher.XORKeyStream(ciphertext, ciphertext) // расшифровка производится тем же способом
fmt.Println(string(ciphertext))
}
Сравнение ChaCha8Rand с другими генераторами
Составили табличку:
Критерий |
ChaCha8Rand |
PCG |
Rand (Go) |
---|---|---|---|
Криптографическая безопасность |
Высокая (256 бит) |
Подходит не для всех задач и в целом низкая |
Низкая |
Производительность |
Высокая на |
Высокая, особенно в микробенчмарках |
Средняя |
Универсальность |
Подходит для криптографических приложений |
Ограниченное использование в криптографии |
Не рекомендуется для криптографических приложений |
Методы ускорения |
Не требуется |
Доступны методы "jump ahead" для быстрого пропуска значений |
Отсутствуют |
Размер состояния |
512 бит |
64 или 128 бит |
Зависит от реализации |
Ожидается, что сообщество Golang предложит дополнительные стратегии использования этого инструмента, будем следить за новостями!
Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке.
Комментарии (7)
vilgeforce
15.05.2024 14:57Надежность генератора не выше надежности seed. Допускается ли использование без явного задания seed, к слову?
Sly_tom_cat
15.05.2024 14:57В Go строгая типизация. Сказано на вход
rand.NewChaCha8
передать массив[32]byte
значит надо передать или код просто не соберется.
mrobespierre
15.05.2024 14:57Обычно ругаю ваши статьи, но тут спасибо. И всё же пара замечаний:
Seed обычно переводят как посев, а не как семя.
Посев не должен быть переменной. Он или константа (тогда, когда это необходимо), или псевдослучайное число, вычитанное из /dev/urandom. В Go для этого есть хелпер crypto/rand.Read()
И немного душноты: на Linux рандом работает только если запущен rngd. Он вычитывает энтропию из доступных устройств (TPM, процессор (в них сейчас есть выделенная инструкция), virtio, если мы внутри виртуалки, и т.д.) и "закачивает" её в urandom. В крайнем случае есть сурогатная альтернатива в виде Haveged. Без них конечно ничего не ломается, но всё что "crypto" работает медленно, а псевдослучайные числа, скажем так, не совсем случайные.
dso
15.05.2024 14:57+4Где это обычно переводят, как "посев"? Везде пишут "семя" (seeds - семена). Семя генерируемого мира, например.
mrobespierre
15.05.2024 14:57вбейте в гугл оба варианта, вот те тысячи, где "посев", там и переводят, а те четыре, где "семена" (seed и seeds вообще-то слова разные и не заменяемые просто так, но в вашем мгимо видимо не проходили)
shebdim
15.05.2024 14:57seed - в контексте генерации случайных чисел используют как исходную точку для генерации случайных чисел, каждое значение определяет всю цепочку случайных чисел. В таком контексте это буквально "семя". Посев тут очень сложно втиснуть. Если очень хочется, то можно и так
NewChaCha8(seed)
NewChaCha8 - посев
seed - семя
Хотя слишком аграрно на мой вкус :)
noRoman
Понятно на)