Add internal randstream and pwgen packages

This commit is contained in:
2026-10-31 10:03:06 -07:00
parent b7028fd7e3
commit c9ce5ff442
4 changed files with 558 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
// Package randstream provides a fast, unbounded stream of pseudorandom
// bytes as an io.Reader. It backs `sutil generate random`.
//
// It exists because reading /dev/urandom is slow: operating system random
// devices are built for small requests of key material, not for filling
// disks or producing gigabytes of test data. A Reader instead expands a
// 32-byte key with the ChaCha8 generator from math/rand/v2, which runs at
// close to memory bandwidth and never blocks.
//
// When Params.Seed is set, the key is SHA-256 of the seed string and the
// stream is fully deterministic: the same seed always produces the same
// bytes, which makes test data reproducible. When Seed is empty, the key
// is read from crypto/rand (the operating system's secure random source,
// /dev/urandom or its equivalent) and the stream is unpredictable.
//
// The output is not meant to be used as cryptographic key material; use
// crypto/rand for that.
package randstream
import (
"crypto/rand"
"crypto/sha256"
"fmt"
mathrand "math/rand/v2"
)
// KeySize is the size in bytes of the key that seeds the stream.
const KeySize = 32
// Params configures New.
type Params struct {
// Seed, when non-empty, makes the stream deterministic: the same seed
// always yields the same byte sequence. When empty, the stream is
// keyed from crypto/rand and differs on every call to New.
Seed string
}
// Reader is an unbounded stream of pseudorandom bytes. Read never returns
// an error and never returns fewer bytes than requested. A Reader is not
// safe for concurrent use.
type Reader struct {
generator *mathrand.ChaCha8
}
// New returns a Reader configured by p.
func New(p Params) (*Reader, error) {
key, err := keyFromParams(p)
if err != nil {
return nil, err
}
return &Reader{generator: mathrand.NewChaCha8(key)}, nil
}
// Read fills b with pseudorandom bytes. It always returns len(b) and a nil
// error.
func (r *Reader) Read(b []byte) (int, error) {
return r.generator.Read(b)
}
// keyFromParams derives the ChaCha8 key: SHA-256 of the seed string, or
// fresh bytes from crypto/rand when no seed is given.
func keyFromParams(p Params) ([KeySize]byte, error) {
if p.Seed != "" {
return sha256.Sum256([]byte(p.Seed)), nil
}
var key [KeySize]byte
_, err := rand.Read(key[:])
if err != nil {
return key, fmt.Errorf("randstream: reading key from crypto/rand: %w", err)
}
return key, nil
}
+111
View File
@@ -0,0 +1,111 @@
package randstream_test
import (
"io"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"sneak.berlin/go/x/internal/randstream"
)
const (
sampleSize = 1 << 20
helloSeed = "hello"
)
func TestSeededStreamIsDeterministic(t *testing.T) {
t.Parallel()
first := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
second := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
assert.Equal(t, first, second)
}
func TestDifferentSeedsDiffer(t *testing.T) {
t.Parallel()
hello := readSample(t, randstream.Params{Seed: helloSeed}, sampleSize)
world := readSample(t, randstream.Params{Seed: "world"}, sampleSize)
assert.NotEqual(t, hello, world)
}
func TestUnseededStreamsDiffer(t *testing.T) {
t.Parallel()
first := readSample(t, randstream.Params{Seed: ""}, sampleSize)
second := readSample(t, randstream.Params{Seed: ""}, sampleSize)
assert.NotEqual(t, first, second)
}
func TestReadAlwaysFillsBuffer(t *testing.T) {
t.Parallel()
stream, err := randstream.New(randstream.Params{Seed: "fill"})
require.NoError(t, err)
for _, size := range []int{0, 1, 7, 64, 65, 4096, sampleSize} {
buffer := make([]byte, size)
n, err := stream.Read(buffer)
require.NoError(t, err)
assert.Equal(t, size, n)
}
}
func TestStreamIsContinuousAcrossReads(t *testing.T) {
t.Parallel()
const headSize, tailSize = 300, 700
whole := readSample(t, randstream.Params{Seed: "continuous"}, headSize+tailSize)
stream, err := randstream.New(randstream.Params{Seed: "continuous"})
require.NoError(t, err)
head := make([]byte, headSize)
tail := make([]byte, tailSize)
_, err = io.ReadFull(stream, head)
require.NoError(t, err)
_, err = io.ReadFull(stream, tail)
require.NoError(t, err)
assert.Equal(t, whole[:headSize], head)
assert.Equal(t, whole[headSize:], tail)
}
func TestOutputIsNotTriviallyPatterned(t *testing.T) {
t.Parallel()
sample := readSample(t, randstream.Params{Seed: "entropy"}, sampleSize)
var seen [256]bool
for _, b := range sample {
seen[b] = true
}
for value, wasSeen := range seen {
assert.True(t, wasSeen, "byte value %d never appeared", value)
}
}
// readSample reads size bytes from a new Reader configured by p.
func readSample(t *testing.T, p randstream.Params, size int) []byte {
t.Helper()
stream, err := randstream.New(p)
require.NoError(t, err)
sample := make([]byte, size)
_, err = io.ReadFull(stream, sample)
require.NoError(t, err)
return sample
}