From 11b9175dccd1c5eb18d6cca9c270388426977624 Mon Sep 17 00:00:00 2001 From: Josh Rickmar Date: Thu, 13 Jun 2024 19:05:15 +0000 Subject: [PATCH] crypto/rand: Import module 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. --- crypto/rand/default.go | 161 +++++++++++++++++++++++++++++ crypto/rand/doc.go | 13 +++ crypto/rand/go.mod | 7 ++ crypto/rand/go.sum | 4 + crypto/rand/prng.go | 106 +++++++++++++++++++ crypto/rand/uniform.go | 227 +++++++++++++++++++++++++++++++++++++++++ 6 files changed, 518 insertions(+) create mode 100644 crypto/rand/default.go create mode 100644 crypto/rand/doc.go create mode 100644 crypto/rand/go.mod create mode 100644 crypto/rand/go.sum create mode 100644 crypto/rand/prng.go create mode 100644 crypto/rand/uniform.go diff --git a/crypto/rand/default.go b/crypto/rand/default.go new file mode 100644 index 0000000000..481ee49018 --- /dev/null +++ b/crypto/rand/default.go @@ -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) +} diff --git a/crypto/rand/doc.go b/crypto/rand/doc.go new file mode 100644 index 0000000000..7290113d6b --- /dev/null +++ b/crypto/rand/doc.go @@ -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 diff --git a/crypto/rand/go.mod b/crypto/rand/go.mod new file mode 100644 index 0000000000..b458860c67 --- /dev/null +++ b/crypto/rand/go.mod @@ -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 diff --git a/crypto/rand/go.sum b/crypto/rand/go.sum new file mode 100644 index 0000000000..3983382181 --- /dev/null +++ b/crypto/rand/go.sum @@ -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= diff --git a/crypto/rand/prng.go b/crypto/rand/prng.go new file mode 100644 index 0000000000..3ca9a625d7 --- /dev/null +++ b/crypto/rand/prng.go @@ -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 +} diff --git a/crypto/rand/uniform.go b/crypto/rand/uniform.go new file mode 100644 index 0000000000..04f133374c --- /dev/null +++ b/crypto/rand/uniform.go @@ -0,0 +1,227 @@ +// 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. +// +// Uniform random algorithms modified from the Go math/rand/v2 package with +// the following license: +// +// Copyright (c) 2009 The Go Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +package rand + +import ( + "encoding/binary" + "math/bits" + "time" +) + +// Uint32 returns a uniform random uint32. +func (p *PRNG) Uint32() uint32 { + b := make([]byte, 4) + p.Read(b) + return binary.LittleEndian.Uint32(b) +} + +// Uint64 returns a uniform random uint64. +func (p *PRNG) Uint64() uint64 { + b := make([]byte, 8) + p.Read(b) + return binary.LittleEndian.Uint64(b) +} + +// Uint32n returns a random uint32 in range [0,n) without modulo bias. +func (p *PRNG) Uint32n(n uint32) uint32 { + if n&(n-1) == 0 { // n is power of two, can mask + return uint32(p.Uint64()) & (n - 1) + } + // On 64-bit systems we still use the uint64 code below because + // the probability of a random uint64 lo being < a uint32 n is near zero, + // meaning the unbiasing loop almost never runs. + // On 32-bit systems, here we need to implement that same logic in 32-bit math, + // both to preserve the exact output sequence observed on 64-bit machines + // and to preserve the optimization that the unbiasing loop almost never runs. + // + // We want to compute + // hi, lo := bits.Mul64(r.Uint64(), n) + // In terms of 32-bit halves, this is: + // x1:x0 := r.Uint64() + // 0:hi, lo1:lo0 := bits.Mul64(x1:x0, 0:n) + // Writing out the multiplication in terms of bits.Mul32 allows + // using direct hardware instructions and avoiding + // the computations involving these zeros. + x := p.Uint64() + lo1a, lo0 := bits.Mul32(uint32(x), n) + hi, lo1b := bits.Mul32(uint32(x>>32), n) + lo1, c := bits.Add32(lo1a, lo1b, 0) + hi += c + if lo1 == 0 && lo0 < n { + n64 := uint64(n) + thresh := uint32(-n64 % n64) + for lo1 == 0 && lo0 < thresh { + x := p.Uint64() + lo1a, lo0 = bits.Mul32(uint32(x), n) + hi, lo1b = bits.Mul32(uint32(x>>32), n) + lo1, c = bits.Add32(lo1a, lo1b, 0) + hi += c + } + } + return hi +} + +const is32bit = ^uint(0)>>32 == 0 + +// Uint64n returns a random uint32 in range [0,n) without modulo bias. +func (p *PRNG) Uint64n(n uint64) uint64 { + if is32bit && uint64(uint32(n)) == n { + return uint64(p.Uint32n(uint32(n))) + } + if n&(n-1) == 0 { // n is power of two, can mask + return p.Uint64() & (n - 1) + } + + // Suppose we have a uint64 x uniform in the range [0,2⁶⁴) + // and want to reduce it to the range [0,n) preserving exact uniformity. + // We can simulate a scaling arbitrary precision x * (n/2⁶⁴) by + // the high bits of a double-width multiply of x*n, meaning (x*n)/2⁶⁴. + // Since there are 2⁶⁴ possible inputs x and only n possible outputs, + // the output is necessarily biased if n does not divide 2⁶⁴. + // In general (x*n)/2⁶⁴ = k for x*n in [k*2⁶⁴,(k+1)*2⁶⁴). + // There are either floor(2⁶⁴/n) or ceil(2⁶⁴/n) possible products + // in that range, depending on k. + // But suppose we reject the sample and try again when + // x*n is in [k*2⁶⁴, k*2⁶⁴+(2⁶⁴%n)), meaning rejecting fewer than n possible + // outcomes out of the 2⁶⁴. + // Now there are exactly floor(2⁶⁴/n) possible ways to produce + // each output value k, so we've restored uniformity. + // To get valid uint64 math, 2⁶⁴ % n = (2⁶⁴ - n) % n = -n % n, + // so the direct implementation of this algorithm would be: + // + // hi, lo := bits.Mul64(r.Uint64(), n) + // thresh := -n % n + // for lo < thresh { + // hi, lo = bits.Mul64(r.Uint64(), n) + // } + // + // That still leaves an expensive 64-bit division that we would rather avoid. + // We know that thresh < n, and n is usually much less than 2⁶⁴, so we can + // avoid the last four lines unless lo < n. + // + // See also: + // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction + // https://lemire.me/blog/2016/06/30/fast-random-shuffling + hi, lo := bits.Mul64(p.Uint64(), n) + if lo < n { + thresh := -n % n + for lo < thresh { + hi, lo = bits.Mul64(p.Uint64(), n) + } + } + return hi +} + +// Int32 returns a random 31-bit non-negative integer as an int32 without +// modulo bias. +func (p *PRNG) Int32() int32 { + return int32(p.Uint32() & 0x7FFFFFFF) +} + +// Int32n returns, as an int32, a random 31-bit non-negative integer in [0,n) +// without modulo bias. +// Panics if n <= 0. +func (p *PRNG) Int32n(n int32) int32 { + if n <= 0 { + panic("rand: invalid argument to Int32n") + } + return int32(p.Uint32n(uint32(n))) +} + +// Int64 returns a random 63-bit non-negative integer as an int64 without +// modulo bias. +func (p *PRNG) Int64() int64 { + return int64(p.Uint64() & 0x7FFFFFFF_FFFFFFFF) +} + +// Int64n returns, as an int64, a random 63-bit non-negative integer in [0,n) +// without modulo bias. +// Panics if n <= 0. +func (p *PRNG) Int64n(n int64) int64 { + if n <= 0 { + panic("rand: invalid argument to Int64n") + } + return int64(p.Uint64n(uint64(n))) +} + +// Int returns a non-negative integer without bias. +func (p *PRNG) Int() int { + return int(uint(p.Uint64()) << 1 >> 1) +} + +// IntN returns, as an int, a random non-negative integer in [0,n) without +// modulo bias. +// Panics if n <= 0. +func (p *PRNG) IntN(n int) int { + if n <= 0 { + panic("rand: invalid argument to IntN") + } + return int(p.Uint64n(uint64(n))) +} + +// UintN returns, as an uint, a random integer in [0,n) without modulo bias. +func (p *PRNG) UintN(n uint) uint { + return uint(p.Uint64n(uint64(n))) +} + +// Duration returns a random duration in [0,n) without modulo bias. +// Panics if n <= 0. +func (p *PRNG) Duration(n time.Duration) time.Duration { + if n <= 0 { + panic("rand: invalid argument to Duration") + } + return time.Duration(p.Uint64n(uint64(n))) +} + +// Shuffle randomizes the order of n elements by swapping the elements at +// indexes i and j. +// Panics if n < 0. +func (p *PRNG) Shuffle(n int, swap func(i, j int)) { + if n < 0 { + panic("rand: invalid argument to Shuffle") + } + + // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + // Shuffle really ought not be called with n that doesn't fit in 32 bits. + // Not only will it take a very long time, but with 2³¹! possible permutations, + // there's no way that any PRNG can have a big enough internal state to + // generate even a minuscule percentage of the possible permutations. + // Nevertheless, the right API signature accepts an int n, so handle it as best we can. + for i := n - 1; i > 0; i-- { + j := int(p.Uint64n(uint64(i + 1))) + swap(i, j) + } +}