Skip to content

Commit

Permalink
feat: Add cryptorand package for random string and number generation (#…
Browse files Browse the repository at this point in the history
…32)

* feat: Add cryptorand package for random string and number generation

This package is taken from the monorepo, and was renamed from crand
for improved clarity. It will be used for API key generation.

* Remove "Must" functions

There is little precedence of functions leading with Must being
idiomatic in Go code. Ignoring errors in favor of a panic is
dangerous in highly-reliable code.

* Remove unused must.go
  • Loading branch information
kylecarbs committed Jan 19, 2022
1 parent 4dc6e35 commit 6e6eee6
Show file tree
Hide file tree
Showing 6 changed files with 649 additions and 2 deletions.
194 changes: 194 additions & 0 deletions cryptorand/numbers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
package cryptorand

import (
"crypto/rand"
"encoding/binary"

"golang.org/x/xerrors"
)

// Most of this code is inspired by math/rand, so shares similar
// functions and implementations, but uses crypto/rand to generate
// random Int63 data.

// Int64 returns a non-negative random 63-bit integer as a int64.
func Int63() (int64, error) {
var i int64
err := binary.Read(rand.Reader, binary.BigEndian, &i)
if err != nil {
return 0, xerrors.Errorf("read binary: %w", err)
}

if i < 0 {
return -i, nil
}
return i, nil
}

// Uint64 returns a random 64-bit integer as a uint64.
func Uint64() (uint64, error) {
upper, err := Int63()
if err != nil {
return 0, xerrors.Errorf("read upper: %w", err)
}

lower, err := Int63()
if err != nil {
return 0, xerrors.Errorf("read lower: %w", err)
}

return uint64(lower)>>31 | uint64(upper)<<32, nil
}

// Int31 returns a non-negative random 31-bit integer as a int32.
func Int31() (int32, error) {
i, err := Int63()
if err != nil {
return 0, err
}

return int32(i >> 32), nil
}

// Uint32 returns a 32-bit value as a uint32.
func Uint32() (uint32, error) {
i, err := Int63()
if err != nil {
return 0, err
}

return uint32(i >> 31), nil
}

// Int returns a non-negative random integer as a int.
func Int() (int, error) {
i, err := Int63()
if err != nil {
return 0, err
}

if i < 0 {
return int(-i), nil
}
return int(i), nil
}

// Int63n returns a non-negative random integer in [0,n) as a int64.
func Int63n(n int64) (int64, error) {
if n <= 0 {
panic("invalid argument to Int63n")
}

max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
i, err := Int63()
if err != nil {
return 0, err
}

for i > max {
i, err = Int63()
if err != nil {
return 0, err
}
}

return i % n, nil
}

// Int31n returns a non-negative integer in [0,n) as a int32.
func Int31n(n int32) (int32, error) {
i, err := Uint32()
if err != nil {
return 0, err
}

return UnbiasedModulo32(i, n)
}

// UnbiasedModulo32 uniformly modulos v by n over a sufficiently large data
// set, regenerating v if necessary. n must be > 0. All input bits in v must be
// fully random, you cannot cast a random uint8/uint16 for input into this
// function.
func UnbiasedModulo32(v uint32, n int32) (int32, error) {
prod := uint64(v) * uint64(n)
low := uint32(prod)
if low < uint32(n) {
thresh := uint32(-n) % uint32(n)
for low < thresh {
var err error
v, err = Uint32()
if err != nil {
return 0, err
}
prod = uint64(v) * uint64(n)
low = uint32(prod)
}
}
return int32(prod >> 32), nil
}

// Intn returns a non-negative integer in [0,n) as a int.
func Intn(n int) (int, error) {
if n <= 0 {
panic("n must be a positive nonzero number")
}

if n <= 1<<31-1 {
i, err := Int31n(int32(n))
if err != nil {
return 0, err
}

return int(i), nil
}

i, err := Int63n(int64(n))
if err != nil {
return 0, err
}

return int(i), nil
}

// Float64 returns a random number in [0.0,1.0) as a float64.
func Float64() (float64, error) {
again:
i, err := Int63n(1 << 53)
if err != nil {
return 0, err
}

f := (float64(i) / (1 << 53))
if f == 1 {
goto again
}

return f, nil
}

// Float32 returns a random number in [0.0,1.0) as a float32.
func Float32() (float32, error) {
again:
i, err := Float64()
if err != nil {
return 0, err
}

f := float32(i)
if f == 1 {
goto again
}

return f, nil
}

// Bool returns a random true/false value as a bool.
func Bool() (bool, error) {
i, err := Uint64()
if err != nil {
return false, err
}

// True if the least significant bit is 1
return i&1 == 1, nil
}
159 changes: 159 additions & 0 deletions cryptorand/numbers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package cryptorand_test

import (
"crypto/rand"
"encoding/binary"
"testing"

"github.com/coder/coder/cryptorand"
"github.com/stretchr/testify/require"
)

func TestInt63(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Int63()
require.NoError(t, err, "unexpected error from Int63")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0, "values must be positive")
}
}

func TestUint64(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Uint64()
require.NoError(t, err, "unexpected error from Uint64")
t.Logf("value: %v <- random?", v)
}
}

func TestInt31(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Int31()
require.NoError(t, err, "unexpected error from Int31")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0, "values must be positive")
}
}

func TestUnbiasedModulo32(t *testing.T) {
t.Parallel()
const mod = 7
dist := [mod]uint32{}

for i := 0; i < 1000; i++ {
b := [4]byte{}
_, _ = rand.Read(b[:])
v, err := cryptorand.UnbiasedModulo32(binary.BigEndian.Uint32(b[:]), mod)
require.NoError(t, err, "unexpected error from UnbiasedModulo32")
dist[v]++
}

t.Logf("dist: %+v <- evenly distributed?", dist)
}

func TestUint32(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Uint32()
require.NoError(t, err, "unexpected error from Uint32")
t.Logf("value: %v <- random?", v)
}
}

func TestInt(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Int()
require.NoError(t, err, "unexpected error from Int")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0, "values must be positive")
}
}

func TestInt63n(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Int63n(1 << 35)
require.NoError(t, err, "unexpected error from Int63n")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0, "values must be positive")
require.True(t, v < 1<<35, "values must be less than 1<<35")
}
}

func TestInt31n(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Int31n(100)
require.NoError(t, err, "unexpected error from Int31n")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0, "values must be positive")
require.True(t, v < 100, "values must be less than 100")
}
}

func TestIntn(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Intn(100)
require.NoError(t, err, "unexpected error from Intn")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0, "values must be positive")
require.True(t, v < 100, "values must be less than 100")
}
}

func TestFloat64(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Float64()
require.NoError(t, err, "unexpected error from Float64")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0.0, "values must be positive")
require.True(t, v < 1.0, "values must be less than 1.0")
}
}

func TestFloat32(t *testing.T) {
t.Parallel()

for i := 0; i < 20; i++ {
v, err := cryptorand.Float32()
require.NoError(t, err, "unexpected error from Float32")
t.Logf("value: %v <- random?", v)
require.True(t, v >= 0.0, "values must be positive")
require.True(t, v < 1.0, "values must be less than 1.0")
}
}

func TestBool(t *testing.T) {
t.Parallel()

const iterations = 10000
trueCount := 0

for i := 0; i < iterations; i += 1 {
v, err := cryptorand.Bool()
require.NoError(t, err, "unexpected error from Bool")
if v {
trueCount++
}
}

percentage := (float64(trueCount) / iterations) * 100
t.Logf("number of true values: %d of %d total (%.2f%%)", trueCount, iterations, percentage)
require.True(t, percentage > 48, "expected more than 48 percent of values to be true")
require.True(t, percentage < 52, "expected less than 52 percent of values to be true")
}

0 comments on commit 6e6eee6

Please sign in to comment.