lib/rand: Break out random functions into separate package

The intention for this package is to provide a combination of the
security of crypto/rand and the convenience of math/rand. It should be
the first choice of random data unless ultimate performance is required
and the usage is provably irrelevant from a security standpoint.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3186
This commit is contained in:
Jakob Borg
2016-05-26 07:02:56 +00:00
committed by Audrius Butkevicius
parent 410d700ae3
commit e6b78e5d56
10 changed files with 42 additions and 31 deletions

View File

@@ -1,64 +0,0 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package util
import (
"crypto/md5"
cryptoRand "crypto/rand"
"encoding/binary"
"io"
mathRand "math/rand"
)
// randomCharset contains the characters that can make up a randomString().
const randomCharset = "2345679abcdefghijkmnopqrstuvwxyzACDEFGHJKLMNPQRSTUVWXYZ"
var (
// defaultSecureSource is a concurrency safe math/rand.Source with a
// cryptographically sound base.
defaltSecureSource = newSecureSource()
// defaultSecureRand is a math/rand.Rand based on the secure source.
defaultSecureRand = mathRand.New(defaltSecureSource)
)
func init() {
// The default RNG should be seeded with something good.
mathRand.Seed(RandomInt64())
}
// RandomString returns a strongly random string of characters (taken from
// randomCharset) of the specified length. The returned string contains ~5.8
// bits of entropy per character, due to the character set used.
func RandomString(l int) string {
bs := make([]byte, l)
for i := range bs {
bs[i] = randomCharset[defaultSecureRand.Intn(len(randomCharset))]
}
return string(bs)
}
// RandomInt64 returns a strongly random int64, slowly
func RandomInt64() int64 {
var bs [8]byte
_, err := io.ReadFull(cryptoRand.Reader, bs[:])
if err != nil {
panic("randomness failure: " + err.Error())
}
return SeedFromBytes(bs[:])
}
// SeedFromBytes calculates a weak 64 bit hash from the given byte slice,
// suitable for use a predictable random seed.
func SeedFromBytes(bs []byte) int64 {
h := md5.New()
h.Write(bs)
s := h.Sum(nil)
// The MD5 hash of the byte slice is 16 bytes long. We interpret it as two
// uint64s and XOR them together.
return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
}

View File

@@ -1,63 +0,0 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package util
import "testing"
func TestSeedFromBytes(t *testing.T) {
// should always return the same seed for the same bytes
tcs := []struct {
bs []byte
v int64
}{
{[]byte("hello world"), -3639725434188061933},
{[]byte("hello worlx"), -2539100776074091088},
}
for _, tc := range tcs {
if v := SeedFromBytes(tc.bs); v != tc.v {
t.Errorf("Unexpected seed value %d != %d", v, tc.v)
}
}
}
func TestRandomString(t *testing.T) {
for _, l := range []int{0, 1, 2, 3, 4, 8, 42} {
s := RandomString(l)
if len(s) != l {
t.Errorf("Incorrect length %d != %d", len(s), l)
}
}
strings := make([]string, 1000)
for i := range strings {
strings[i] = RandomString(8)
for j := range strings {
if i == j {
continue
}
if strings[i] == strings[j] {
t.Errorf("Repeated random string %q", strings[i])
}
}
}
}
func TestRandomInt64(t *testing.T) {
ints := make([]int64, 1000)
for i := range ints {
ints[i] = RandomInt64()
for j := range ints {
if i == j {
continue
}
if ints[i] == ints[j] {
t.Errorf("Repeated random int64 %d", ints[i])
}
}
}
}

View File

@@ -1,57 +0,0 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package util
import (
"bufio"
"crypto/rand"
"encoding/binary"
"io"
"sync"
)
// The secureSource is a math/rand.Source that reads bytes from
// crypto/rand.Reader. It means we can use the convenience functions
// provided by math/rand.Rand on top of a secure source of numbers. It is
// concurrency safe for ease of use.
type secureSource struct {
rd io.Reader
mut sync.Mutex
}
func newSecureSource() *secureSource {
return &secureSource{
// Using buffering on top of the rand.Reader increases our
// performance by about 20%, even though it means we must use
// locking.
rd: bufio.NewReader(rand.Reader),
}
}
func (s *secureSource) Seed(int64) {
panic("SecureSource is not seedable")
}
func (s *secureSource) Int63() int64 {
var buf [8]byte
// Read eight bytes of entropy from the buffered, secure random number
// generator. The buffered reader isn't concurrency safe, so we lock
// around that.
s.mut.Lock()
_, err := io.ReadFull(s.rd, buf[:])
s.mut.Unlock()
if err != nil {
panic("randomness failure: " + err.Error())
}
// Grab those bytes as an uint64
v := binary.BigEndian.Uint64(buf[:])
// Mask of the high bit and return the resulting int63
return int64(v & (1<<63 - 1))
}

View File

@@ -1,96 +0,0 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package util
import "testing"
func TestSecureSource(t *testing.T) {
// This is not a test to verify that the random numbers are secure,
// merely that the numbers look random at all and that we've haven't
// broken the implementation by masking off half the bits or always
// returning "4" (chosen by fair dice roll),
const nsamples = 10000
// Create a new source and sample values from it.
s := newSecureSource()
res0 := make([]int64, nsamples)
for i := range res0 {
res0[i] = s.Int63()
}
// Do it again
s = newSecureSource()
res1 := make([]int64, nsamples)
for i := range res1 {
res1[i] = s.Int63()
}
// There should (statistically speaking) be no repetition of the values,
// neither within the samples from a source nor between sources.
for _, v0 := range res0 {
for _, v1 := range res1 {
if v0 == v1 {
t.Errorf("Suspicious coincidence, %d repeated between res0/res1", v0)
}
}
}
for i, v0 := range res0 {
for _, v1 := range res0[i+1:] {
if v0 == v1 {
t.Errorf("Suspicious coincidence, %d repeated within res0", v0)
}
}
}
for i, v0 := range res1 {
for _, v1 := range res1[i+1:] {
if v0 == v1 {
t.Errorf("Suspicious coincidence, %d repeated within res1", v0)
}
}
}
// Count how many times each bit was set. On average each bit ought to
// be set in half of the samples, except the topmost bit which must
// never be set (int63). We raise an alarm if a single bit is set in
// fewer than 1/3 of the samples or more often than 2/3 of the samples.
var bits [64]int
for _, v := range res0 {
for i := range bits {
if v&1 == 1 {
bits[i]++
}
v >>= 1
}
}
for bit, count := range bits {
switch bit {
case 63:
// The topmost bit is never set
if count != 0 {
t.Errorf("The topmost bit was set %d times in %d samples (should be 0)", count, nsamples)
}
default:
if count < nsamples/3 {
t.Errorf("Bit %d was set only %d times out of %d", bit, count, nsamples)
}
if count > nsamples/3*2 {
t.Errorf("Bit %d was set fully %d times out of %d", bit, count, nsamples)
}
}
}
}
var sink int64
func BenchmarkSecureSource(b *testing.B) {
s := newSecureSource()
for i := 0; i < b.N; i++ {
sink = s.Int63()
}
b.ReportAllocs()
}