Skip to content

Commit 6e6eee6

Browse files
authored
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
1 parent 4dc6e35 commit 6e6eee6

File tree

6 files changed

+649
-2
lines changed

6 files changed

+649
-2
lines changed

cryptorand/numbers.go

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
package cryptorand
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/binary"
6+
7+
"golang.org/x/xerrors"
8+
)
9+
10+
// Most of this code is inspired by math/rand, so shares similar
11+
// functions and implementations, but uses crypto/rand to generate
12+
// random Int63 data.
13+
14+
// Int64 returns a non-negative random 63-bit integer as a int64.
15+
func Int63() (int64, error) {
16+
var i int64
17+
err := binary.Read(rand.Reader, binary.BigEndian, &i)
18+
if err != nil {
19+
return 0, xerrors.Errorf("read binary: %w", err)
20+
}
21+
22+
if i < 0 {
23+
return -i, nil
24+
}
25+
return i, nil
26+
}
27+
28+
// Uint64 returns a random 64-bit integer as a uint64.
29+
func Uint64() (uint64, error) {
30+
upper, err := Int63()
31+
if err != nil {
32+
return 0, xerrors.Errorf("read upper: %w", err)
33+
}
34+
35+
lower, err := Int63()
36+
if err != nil {
37+
return 0, xerrors.Errorf("read lower: %w", err)
38+
}
39+
40+
return uint64(lower)>>31 | uint64(upper)<<32, nil
41+
}
42+
43+
// Int31 returns a non-negative random 31-bit integer as a int32.
44+
func Int31() (int32, error) {
45+
i, err := Int63()
46+
if err != nil {
47+
return 0, err
48+
}
49+
50+
return int32(i >> 32), nil
51+
}
52+
53+
// Uint32 returns a 32-bit value as a uint32.
54+
func Uint32() (uint32, error) {
55+
i, err := Int63()
56+
if err != nil {
57+
return 0, err
58+
}
59+
60+
return uint32(i >> 31), nil
61+
}
62+
63+
// Int returns a non-negative random integer as a int.
64+
func Int() (int, error) {
65+
i, err := Int63()
66+
if err != nil {
67+
return 0, err
68+
}
69+
70+
if i < 0 {
71+
return int(-i), nil
72+
}
73+
return int(i), nil
74+
}
75+
76+
// Int63n returns a non-negative random integer in [0,n) as a int64.
77+
func Int63n(n int64) (int64, error) {
78+
if n <= 0 {
79+
panic("invalid argument to Int63n")
80+
}
81+
82+
max := int64((1 << 63) - 1 - (1<<63)%uint64(n))
83+
i, err := Int63()
84+
if err != nil {
85+
return 0, err
86+
}
87+
88+
for i > max {
89+
i, err = Int63()
90+
if err != nil {
91+
return 0, err
92+
}
93+
}
94+
95+
return i % n, nil
96+
}
97+
98+
// Int31n returns a non-negative integer in [0,n) as a int32.
99+
func Int31n(n int32) (int32, error) {
100+
i, err := Uint32()
101+
if err != nil {
102+
return 0, err
103+
}
104+
105+
return UnbiasedModulo32(i, n)
106+
}
107+
108+
// UnbiasedModulo32 uniformly modulos v by n over a sufficiently large data
109+
// set, regenerating v if necessary. n must be > 0. All input bits in v must be
110+
// fully random, you cannot cast a random uint8/uint16 for input into this
111+
// function.
112+
func UnbiasedModulo32(v uint32, n int32) (int32, error) {
113+
prod := uint64(v) * uint64(n)
114+
low := uint32(prod)
115+
if low < uint32(n) {
116+
thresh := uint32(-n) % uint32(n)
117+
for low < thresh {
118+
var err error
119+
v, err = Uint32()
120+
if err != nil {
121+
return 0, err
122+
}
123+
prod = uint64(v) * uint64(n)
124+
low = uint32(prod)
125+
}
126+
}
127+
return int32(prod >> 32), nil
128+
}
129+
130+
// Intn returns a non-negative integer in [0,n) as a int.
131+
func Intn(n int) (int, error) {
132+
if n <= 0 {
133+
panic("n must be a positive nonzero number")
134+
}
135+
136+
if n <= 1<<31-1 {
137+
i, err := Int31n(int32(n))
138+
if err != nil {
139+
return 0, err
140+
}
141+
142+
return int(i), nil
143+
}
144+
145+
i, err := Int63n(int64(n))
146+
if err != nil {
147+
return 0, err
148+
}
149+
150+
return int(i), nil
151+
}
152+
153+
// Float64 returns a random number in [0.0,1.0) as a float64.
154+
func Float64() (float64, error) {
155+
again:
156+
i, err := Int63n(1 << 53)
157+
if err != nil {
158+
return 0, err
159+
}
160+
161+
f := (float64(i) / (1 << 53))
162+
if f == 1 {
163+
goto again
164+
}
165+
166+
return f, nil
167+
}
168+
169+
// Float32 returns a random number in [0.0,1.0) as a float32.
170+
func Float32() (float32, error) {
171+
again:
172+
i, err := Float64()
173+
if err != nil {
174+
return 0, err
175+
}
176+
177+
f := float32(i)
178+
if f == 1 {
179+
goto again
180+
}
181+
182+
return f, nil
183+
}
184+
185+
// Bool returns a random true/false value as a bool.
186+
func Bool() (bool, error) {
187+
i, err := Uint64()
188+
if err != nil {
189+
return false, err
190+
}
191+
192+
// True if the least significant bit is 1
193+
return i&1 == 1, nil
194+
}

cryptorand/numbers_test.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
package cryptorand_test
2+
3+
import (
4+
"crypto/rand"
5+
"encoding/binary"
6+
"testing"
7+
8+
"github.com/coder/coder/cryptorand"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestInt63(t *testing.T) {
13+
t.Parallel()
14+
15+
for i := 0; i < 20; i++ {
16+
v, err := cryptorand.Int63()
17+
require.NoError(t, err, "unexpected error from Int63")
18+
t.Logf("value: %v <- random?", v)
19+
require.True(t, v >= 0, "values must be positive")
20+
}
21+
}
22+
23+
func TestUint64(t *testing.T) {
24+
t.Parallel()
25+
26+
for i := 0; i < 20; i++ {
27+
v, err := cryptorand.Uint64()
28+
require.NoError(t, err, "unexpected error from Uint64")
29+
t.Logf("value: %v <- random?", v)
30+
}
31+
}
32+
33+
func TestInt31(t *testing.T) {
34+
t.Parallel()
35+
36+
for i := 0; i < 20; i++ {
37+
v, err := cryptorand.Int31()
38+
require.NoError(t, err, "unexpected error from Int31")
39+
t.Logf("value: %v <- random?", v)
40+
require.True(t, v >= 0, "values must be positive")
41+
}
42+
}
43+
44+
func TestUnbiasedModulo32(t *testing.T) {
45+
t.Parallel()
46+
const mod = 7
47+
dist := [mod]uint32{}
48+
49+
for i := 0; i < 1000; i++ {
50+
b := [4]byte{}
51+
_, _ = rand.Read(b[:])
52+
v, err := cryptorand.UnbiasedModulo32(binary.BigEndian.Uint32(b[:]), mod)
53+
require.NoError(t, err, "unexpected error from UnbiasedModulo32")
54+
dist[v]++
55+
}
56+
57+
t.Logf("dist: %+v <- evenly distributed?", dist)
58+
}
59+
60+
func TestUint32(t *testing.T) {
61+
t.Parallel()
62+
63+
for i := 0; i < 20; i++ {
64+
v, err := cryptorand.Uint32()
65+
require.NoError(t, err, "unexpected error from Uint32")
66+
t.Logf("value: %v <- random?", v)
67+
}
68+
}
69+
70+
func TestInt(t *testing.T) {
71+
t.Parallel()
72+
73+
for i := 0; i < 20; i++ {
74+
v, err := cryptorand.Int()
75+
require.NoError(t, err, "unexpected error from Int")
76+
t.Logf("value: %v <- random?", v)
77+
require.True(t, v >= 0, "values must be positive")
78+
}
79+
}
80+
81+
func TestInt63n(t *testing.T) {
82+
t.Parallel()
83+
84+
for i := 0; i < 20; i++ {
85+
v, err := cryptorand.Int63n(1 << 35)
86+
require.NoError(t, err, "unexpected error from Int63n")
87+
t.Logf("value: %v <- random?", v)
88+
require.True(t, v >= 0, "values must be positive")
89+
require.True(t, v < 1<<35, "values must be less than 1<<35")
90+
}
91+
}
92+
93+
func TestInt31n(t *testing.T) {
94+
t.Parallel()
95+
96+
for i := 0; i < 20; i++ {
97+
v, err := cryptorand.Int31n(100)
98+
require.NoError(t, err, "unexpected error from Int31n")
99+
t.Logf("value: %v <- random?", v)
100+
require.True(t, v >= 0, "values must be positive")
101+
require.True(t, v < 100, "values must be less than 100")
102+
}
103+
}
104+
105+
func TestIntn(t *testing.T) {
106+
t.Parallel()
107+
108+
for i := 0; i < 20; i++ {
109+
v, err := cryptorand.Intn(100)
110+
require.NoError(t, err, "unexpected error from Intn")
111+
t.Logf("value: %v <- random?", v)
112+
require.True(t, v >= 0, "values must be positive")
113+
require.True(t, v < 100, "values must be less than 100")
114+
}
115+
}
116+
117+
func TestFloat64(t *testing.T) {
118+
t.Parallel()
119+
120+
for i := 0; i < 20; i++ {
121+
v, err := cryptorand.Float64()
122+
require.NoError(t, err, "unexpected error from Float64")
123+
t.Logf("value: %v <- random?", v)
124+
require.True(t, v >= 0.0, "values must be positive")
125+
require.True(t, v < 1.0, "values must be less than 1.0")
126+
}
127+
}
128+
129+
func TestFloat32(t *testing.T) {
130+
t.Parallel()
131+
132+
for i := 0; i < 20; i++ {
133+
v, err := cryptorand.Float32()
134+
require.NoError(t, err, "unexpected error from Float32")
135+
t.Logf("value: %v <- random?", v)
136+
require.True(t, v >= 0.0, "values must be positive")
137+
require.True(t, v < 1.0, "values must be less than 1.0")
138+
}
139+
}
140+
141+
func TestBool(t *testing.T) {
142+
t.Parallel()
143+
144+
const iterations = 10000
145+
trueCount := 0
146+
147+
for i := 0; i < iterations; i += 1 {
148+
v, err := cryptorand.Bool()
149+
require.NoError(t, err, "unexpected error from Bool")
150+
if v {
151+
trueCount++
152+
}
153+
}
154+
155+
percentage := (float64(trueCount) / iterations) * 100
156+
t.Logf("number of true values: %d of %d total (%.2f%%)", trueCount, iterations, percentage)
157+
require.True(t, percentage > 48, "expected more than 48 percent of values to be true")
158+
require.True(t, percentage < 52, "expected less than 52 percent of values to be true")
159+
}

0 commit comments

Comments
 (0)