diff --git a/crypto/random/entropy_test.go b/crypto/random/entropy_test.go index 1bde202..cb71742 100644 --- a/crypto/random/entropy_test.go +++ b/crypto/random/entropy_test.go @@ -6,7 +6,6 @@ import ( ) func TestFeeder(t *testing.T) { - // wait for start / first round to complete time.Sleep(1 * time.Millisecond) diff --git a/crypto/random/get.go b/crypto/random/get.go index fb540c0..0b8463f 100644 --- a/crypto/random/get.go +++ b/crypto/random/get.go @@ -1,8 +1,10 @@ package random import ( + "encoding/binary" "errors" "io" + "math" "time" "github.com/Safing/portbase/config" @@ -13,10 +15,10 @@ var ( Reader io.Reader rngBytesRead int64 - rngLastFeed = time.Now() + rngLastFeed = time.Now() reseedAfterSeconds config.IntOption - reseedAfterBytes config.IntOption + reseedAfterBytes config.IntOption ) // reader provides an io.Reader interface @@ -53,13 +55,13 @@ func checkEntropy() (err error) { return errors.New("RNG is not ready yet") } if rngBytesRead > reseedAfterBytes() || - int64(time.Now().Sub(rngLastFeed).Seconds()) > reseedAfterSeconds() { + int64(time.Now().Sub(rngLastFeed).Seconds()) > reseedAfterSeconds() { select { case r := <-rngFeeder: rng.Reseed(r) rngBytesRead = 0 rngLastFeed = time.Now() - case <-time.After(1*time.Second): + case <-time.After(1 * time.Second): return errors.New("failed to get new entropy") } } @@ -94,3 +96,21 @@ func Bytes(n int) ([]byte, error) { return rng.PseudoRandomData(uint(n)), nil } + +// Number returns a random number from 0 to (incl.) max. +func Number(max uint64) (uint64, error) { + secureLimit := math.MaxUint64 - (math.MaxUint64 % max) + max++ + + for { + randomBytes, err := Bytes(8) + if err != nil { + return 0, err + } + + candidate := binary.LittleEndian.Uint64(randomBytes) + if candidate < secureLimit { + return candidate % max, nil + } + } +} diff --git a/crypto/random/get_test.go b/crypto/random/get_test.go new file mode 100644 index 0000000..eb5a3d6 --- /dev/null +++ b/crypto/random/get_test.go @@ -0,0 +1,35 @@ +package random + +import ( + "testing" +) + +func TestNumberRandomness(t *testing.T) { + if testing.Short() { + t.Skip() + } + + var subjects uint64 = 10 + var testSize uint64 = 10000 + + results := make([]uint64, int(subjects)) + for i := 0; i < int(subjects*testSize); i++ { + n, err := Number(subjects - 1) + if err != nil { + t.Fatal(err) + return + } + results[int(n)]++ + } + + // catch big mistakes in the number function, eg. massive % bias + lowerMargin := testSize - testSize/50 + upperMargin := testSize + testSize/50 + for subject, result := range results { + if result < lowerMargin || result > upperMargin { + t.Errorf("subject %d is outside of margins: %d", subject, result) + } + } + + t.Fatal(results) +} diff --git a/crypto/random/rng_test.go b/crypto/random/rng_test.go index 371ef93..aa3c0c9 100644 --- a/crypto/random/rng_test.go +++ b/crypto/random/rng_test.go @@ -19,14 +19,14 @@ func TestRNG(t *testing.T) { if err != nil { t.Errorf("failed to create aes cipher: %s", err) } - rng.Reseed(key) + rng.Reseed(key) config.SetConfigOption("random.rng_cipher", "serpent") _, err = newCipher(key) if err != nil { t.Errorf("failed to create serpent cipher: %s", err) } - rng.Reseed(key) + rng.Reseed(key) b := make([]byte, 32) _, err = Read(b)