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) + } +}