-
Notifications
You must be signed in to change notification settings - Fork 582
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add cryptorand package for random string and number generation (#…
…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
Showing
6 changed files
with
649 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
Oops, something went wrong.