Привет, Хабр!

В версии 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, включают:

  1. IntN и Int64N: функции генерируют псевдослучайное целое число в заданном диапазоне. Например, IntN(n int) int возвращает случайное число от 0 до n-1. Они обеспечивают равномерное распределение значений в указанном диапазоне.

  2. Float64: генерирует псевдослучайное число с плавающей точкой между 0.0 и 1.0, не включая 1.0.

  3. 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)


  1. noRoman
    15.05.2024 14:57
    +8

    Производительность Высокая на

    Понятно на)


  1. vilgeforce
    15.05.2024 14:57

    Надежность генератора не выше надежности seed. Допускается ли использование без явного задания seed, к слову?


    1. Sly_tom_cat
      15.05.2024 14:57

      В Go строгая типизация. Сказано на вход rand.NewChaCha8 передать массив [32]byte значит надо передать или код просто не соберется.


  1. mrobespierre
    15.05.2024 14:57

    Обычно ругаю ваши статьи, но тут спасибо. И всё же пара замечаний:

    1. Seed обычно переводят как посев, а не как семя.

    2. Посев не должен быть переменной. Он или константа (тогда, когда это необходимо), или псевдослучайное число, вычитанное из /dev/urandom. В Go для этого есть хелпер crypto/rand.Read()

    И немного душноты: на Linux рандом работает только если запущен rngd. Он вычитывает энтропию из доступных устройств (TPM, процессор (в них сейчас есть выделенная инструкция), virtio, если мы внутри виртуалки, и т.д.) и "закачивает" её в urandom. В крайнем случае есть сурогатная альтернатива в виде Haveged. Без них конечно ничего не ломается, но всё что "crypto" работает медленно, а псевдослучайные числа, скажем так, не совсем случайные.


    1. dso
      15.05.2024 14:57
      +4

      Где это обычно переводят, как "посев"? Везде пишут "семя" (seeds - семена). Семя генерируемого мира, например.


      1. mrobespierre
        15.05.2024 14:57

        вбейте в гугл оба варианта, вот те тысячи, где "посев", там и переводят, а те четыре, где "семена" (seed и seeds вообще-то слова разные и не заменяемые просто так, но в вашем мгимо видимо не проходили)


        1. shebdim
          15.05.2024 14:57

          seed - в контексте генерации случайных чисел используют как исходную точку для генерации случайных чисел, каждое значение определяет всю цепочку случайных чисел. В таком контексте это буквально "семя". Посев тут очень сложно втиснуть. Если очень хочется, то можно и так

          NewChaCha8(seed)

          NewChaCha8 - посев
          seed - семя

          Хотя слишком аграрно на мой вкус :)