Skip to content

Commit

Permalink
crypto/rand: Implement module.
Browse files Browse the repository at this point in the history
The github.com/decred/dcrd/crypto/rand module provides an alternative to the
standard library's math/rand, math/rand/v2, and crypto/rand packages.  It
implements a package-global fast userspace CSPRNG that never errors after
initial seeding at init time with the ability to create additional PRNGs
without locking overhead if needed.  In addition to providing random bytes,
the PRNG is also capable of generating cryptographically secure integers with
uniform distribution, and provides a Fisher-Yates shuffle function that can be
used to shuffle slices with random indexes.
  • Loading branch information
jrick committed Jun 13, 2024
1 parent 4a5ac92 commit f467a37
Show file tree
Hide file tree
Showing 6 changed files with 518 additions and 0 deletions.
161 changes: 161 additions & 0 deletions crypto/rand/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package rand

import (
"io"
"sync"
"time"
)

// Reader returns the default cryptographically secure userspace PRNG that is
// periodically reseeded with entropy obtained from crypto/rand.
// The returned Reader is safe for concurrent access.
func Reader() io.Reader {
return globalRand
}

type lockingPRNG struct {
*PRNG
mu sync.Mutex
}

var globalRand *lockingPRNG

func init() {
p, err := NewPRNG()
if err != nil {
panic(err)
}
globalRand = &lockingPRNG{PRNG: p}
}

func (p *lockingPRNG) Read(s []byte) (n int, err error) {
p.mu.Lock()
defer p.mu.Unlock()

return p.PRNG.Read(s)
}

// Read fills b with random bytes obtained from the default userspace PRNG.
func Read(b []byte) {
// Mutex is acquired by (*lockingPRNG).Read.
globalRand.Read(b)
}

// Uint32 returns a uniform random uint32.
func Uint32() uint32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Uint32()
}

// Uint64 returns a uniform random uint64.
func Uint64() uint64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Uint64()
}

// Uint32n returns a random uint32 in range [0,n) without modulo bias.
func Uint32n(n uint32) uint32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Uint32n(n)
}

// Uint64n returns a random uint32 in range [0,n) without modulo bias.
func Uint64n(n uint64) uint64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Uint64n(n)
}

// Int32 returns a random 31-bit non-negative integer as an int32 without
// modulo bias.
func Int32() int32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Int32()
}

// Int32n returns, as an int32, a random 31-bit non-negative integer in [0,n)
// without modulo bias.
// Panics if n <= 0.
func Int32n(n int32) int32 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Int32n(n)
}

// Int64 returns a random 63-bit non-negative integer as an int64 without
// modulo bias.
func Int64() int64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Int64()
}

// Int64n returns, as an int64, a random 63-bit non-negative integer in [0,n)
// without modulo bias.
// Panics if n <= 0.
func Int64n(n int64) int64 {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Int64n(n)
}

// Int returns a non-negative integer without bias.
func Int() int {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Int()
}

// IntN returns, as an int, a random non-negative integer in [0,n) without
// modulo bias.
// Panics if n <= 0.
func IntN(n int) int {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.IntN(n)
}

// UintN returns, as an uint, a random integer in [0,n) without modulo bias.
func UintN(n uint) uint {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.UintN(n)
}

// Duration returns a random duration in [0,n) without modulo bias.
// Panics if n <= 0.
func Duration(n time.Duration) time.Duration {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

return globalRand.Duration(n)
}

// Shuffle randomizes the order of n elements by swapping the elements at
// indexes i and j.
// Panics if n < 0.
func Shuffle(n int, swap func(i, j int)) {
globalRand.mu.Lock()
defer globalRand.mu.Unlock()

globalRand.Shuffle(n, swap)
}
13 changes: 13 additions & 0 deletions crypto/rand/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

// Package rand implements a fast userspace CSPRNG that is periodically
// reseeded with entropy obtained from crypto/rand. The PRNG can be used to
// obtain random bytes as well as generating uniformly-distributed integers in
// a full or limited range.
//
// The default global PRNG will never panic after package init and is safe for
// concurrent access. Additional PRNGs which avoid the locking overhead can
// be created by calling NewPRNG.
package rand
7 changes: 7 additions & 0 deletions crypto/rand/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module github.com/decred/dcrd/crypto/rand

go 1.18

require golang.org/x/crypto v0.24.0

require golang.org/x/sys v0.21.0 // indirect
4 changes: 4 additions & 0 deletions crypto/rand/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
106 changes: 106 additions & 0 deletions crypto/rand/prng.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2024 The Decred developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

package rand

import (
cryptorand "crypto/rand"
"encoding/binary"
"math/bits"
"time"

"golang.org/x/crypto/chacha20"
)

const (
maxCipherRead = 4 * 1024 * 1024 // 4 MiB
maxCipherDuration = 20 * time.Second
)

// nonce implements a 12-byte little endian counter suitable for use as an
// incrementing ChaCha20 nonce.
type nonce [chacha20.NonceSize]byte

func (n *nonce) inc() {
n0 := binary.LittleEndian.Uint32(n[0:4])
n1 := binary.LittleEndian.Uint32(n[4:8])
n2 := binary.LittleEndian.Uint32(n[8:12])

var carry uint32
n0, carry = bits.Add32(n0, 1, carry)
n1, carry = bits.Add32(n1, 0, carry)
n2, _ = bits.Add32(n2, 0, carry)

binary.LittleEndian.PutUint32(n[0:4], n0)
binary.LittleEndian.PutUint32(n[4:8], n1)
binary.LittleEndian.PutUint32(n[8:12], n2)
}

// PRNG is a cryptographically secure pseudorandom number generator capable of
// generating random bytes and integers. PRNG methods are not safe for
// concurrent access.
type PRNG struct {
key [chacha20.KeySize]byte
nonce nonce
cipher chacha20.Cipher
read int
t time.Time
}

// NewPRNG returns a seeded PRNG.
func NewPRNG() (*PRNG, error) {
p := new(PRNG)
err := p.seed()
if err != nil {
return nil, err
}
return p, nil
}

// seed reseeds the prng with kernel and existing cipher entropy, if the
// cipher has been originally seeded.
// Only returns an error during initial seeding if a crypto/rand read errors.
func (p *PRNG) seed() error {
_, err := cryptorand.Read(p.key[:])
if err != nil && p.t.IsZero() {
return err
}
p.cipher.XORKeyStream(p.key[:], p.key[:])

// never errors with correct key and nonce sizes
cipher, _ := chacha20.NewUnauthenticatedCipher(p.key[:], p.nonce[:])
p.cipher = *cipher
p.nonce.inc()
p.read = 0
p.t = time.Now().Add(maxCipherDuration)
return nil
}

// Read fills s with len(s) of cryptographically-secure random bytes.
// Read never errors.
func (p *PRNG) Read(s []byte) (n int, err error) {
if time.Now().After(p.t) {
// Reseed the cipher.
// The panic will never be hit except by calling the Read
// method on the zero PRNG value and if crypto/rand read fails.
// Creating the PRNG properly with NewPRNG will return nil and an
// error if the first seeding fails.
// Later calls to seed will never return an error.
if err := p.seed(); err != nil {
panic(err)
}
}

for p.read+len(s) > maxCipherRead {
l := maxCipherRead - p.read
p.cipher.XORKeyStream(s[:l], s[:l])
p.seed()
n += l
s = s[l:]
}
p.cipher.XORKeyStream(s, s)
p.read += len(s)
n += len(s)
return
}
Loading

0 comments on commit f467a37

Please sign in to comment.