-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Add cryptorand package for random string and number generation #32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW, the
Must*
helpers are useful because I don't thinkrand.Reader
can actually fail since it uses urandom https://pkg.go.dev/crypto/rand#pkg-variablesThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could maybe simplify by removing the non-must variants and just panicing instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This package could be used on Mac and Windows too, where the guarantee of entropy isn't there!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess both are useful, since we don't care if tests panic/fail, but production code should check errors 🤷♂️