Add internal randstream and pwgen packages
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user