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
This commit is contained in:
Kyle Carberry 2022-01-19 11:55:06 -06:00 committed by GitHub
parent 4dc6e35c24
commit 6e6eee633c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 649 additions and 2 deletions

194
cryptorand/numbers.go Normal file
View File

@ -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
cryptorand/numbers_test.go Normal file
View File

@ -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")
}

84
cryptorand/strings.go Normal file
View File

@ -0,0 +1,84 @@
package cryptorand
import (
"crypto/rand"
"encoding/binary"
"strings"
)
// Charsets
const (
// Numeric includes decimal numbers (0-9)
Numeric = "0123456789"
// Upper is uppercase characters in the Latin alphabet
Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// Lower is lowercase characters in the Latin alphabet
Lower = "abcdefghijklmnopqrstuvwxyz"
// Alpha is upper or lowercase alphabetic characters
Alpha = Upper + Lower
// Default is uppercase, lowercase, or numeric characters
Default = Numeric + Alpha
// Hex is hexadecimal lowercase characters
Hex = "0123456789abcdef"
// Human creates strings which are easily distinguishable from
// others created with the same charset. It contains most lowercase
// alphanumeric characters without 0,o,i,1,l.
Human = "23456789abcdefghjkmnpqrstuvwxyz"
)
// StringCharset generates a random string using the provided charset and size
func StringCharset(charSetStr string, size int) (string, error) {
charSet := []rune(charSetStr)
if len(charSet) == 0 || size == 0 {
return "", nil
}
// This buffer facilitates pre-emptively creation of random uint32s
// to reduce syscall overhead.
ibuf := make([]byte, 4*size)
_, err := rand.Read(ibuf)
if err != nil {
return "", err
}
var buf strings.Builder
buf.Grow(size)
for i := 0; i < size; i++ {
c, err := UnbiasedModulo32(
binary.BigEndian.Uint32(ibuf[i*4:(i+1)*4]),
int32(len(charSet)),
)
if err != nil {
return "", err
}
_, _ = buf.WriteRune(charSet[c])
}
return buf.String(), nil
}
// String returns a random string using Default.
func String(size int) (string, error) {
return StringCharset(Default, size)
}
// HexString returns a hexadecimal string of given length.
func HexString(size int) (string, error) {
return StringCharset(Hex, size)
}
// Sha1String returns a 40-character hexadecimal string, which matches
// the length of a SHA-1 hash (160 bits).
func Sha1String() (string, error) {
return StringCharset(Hex, 40)
}

209
cryptorand/strings_test.go Normal file
View File

@ -0,0 +1,209 @@
package cryptorand_test
import (
"crypto/rand"
"encoding/binary"
"math/big"
"strings"
"testing"
"unicode/utf8"
"github.com/coder/coder/cryptorand"
"github.com/stretchr/testify/require"
)
func TestString(t *testing.T) {
t.Parallel()
for i := 0; i < 20; i++ {
rs, err := cryptorand.String(10)
require.NoError(t, err, "unexpected error from String")
t.Logf("value: %v <- random?", rs)
}
}
func TestStringCharset(t *testing.T) {
t.Parallel()
tests := []struct {
Name string
Charset string
HelperFunc func(int) (string, error)
Length int
}{
{
Name: "MultiByte-20",
Charset: "💓😘💓🌷",
Length: 20,
},
{
Name: "MultiByte-7",
Charset: "😇🥰😍🤩😘😗☺️😚😙🥲😋😛😜🤪😝🤑",
Length: 7,
},
{
Name: "MixedBytes",
Charset: "🍋🍌🍍🥭🍎🍏🍐🍑🍒🍓🫐🥝🍅🫒🥥🥑🍆🥔abcdefg1234",
Length: 10,
},
{
Name: "Empty",
Charset: cryptorand.Default,
Length: 0,
HelperFunc: cryptorand.String,
},
{
Name: "Numeric",
Charset: cryptorand.Numeric,
Length: 1,
},
{
Name: "Upper",
Charset: cryptorand.Upper,
Length: 3,
},
{
Name: "Lower",
Charset: cryptorand.Lower,
Length: 10,
},
{
Name: "Alpha",
Charset: cryptorand.Alpha,
Length: 20,
},
{
Name: "Default",
Charset: cryptorand.Default,
Length: 10,
},
{
Name: "Hex",
Charset: cryptorand.Hex,
Length: 15,
HelperFunc: cryptorand.HexString,
},
{
Name: "Human",
Charset: cryptorand.Human,
Length: 20,
},
}
for _, test := range tests {
test := test
t.Run(test.Name, func(t *testing.T) {
t.Parallel()
for i := 0; i < 5; i++ {
rs, err := cryptorand.StringCharset(test.Charset, test.Length)
require.NoError(t, err, "unexpected error from StringCharset")
require.Equal(t, test.Length, utf8.RuneCountInString(rs), "expected RuneCountInString to match requested")
if i == 0 {
t.Logf("value: %v <- random?", rs)
}
}
})
if test.HelperFunc != nil {
t.Run(test.Name+"HelperFunc", func(t *testing.T) {
t.Parallel()
for i := 0; i < 5; i++ {
rs, err := test.HelperFunc(test.Length)
require.NoError(t, err, "unexpected error from HelperFunc")
require.Equal(t, test.Length, utf8.RuneCountInString(rs), "expected RuneCountInString to match requested")
if i == 0 {
t.Logf("value: %v <- random?", rs)
}
}
})
}
}
}
func TestSha1String(t *testing.T) {
t.Parallel()
for i := 0; i < 20; i++ {
rs, err := cryptorand.Sha1String()
require.NoError(t, err, "unexpected error from String")
require.Equal(t, 40, utf8.RuneCountInString(rs), "expected RuneCountInString to match requested")
t.Logf("value: %v <- random?", rs)
}
}
func BenchmarkString20(b *testing.B) {
b.SetBytes(20)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = cryptorand.String(20)
}
}
func BenchmarkStringUnsafe20(b *testing.B) {
mkstring := func(charSetStr string, size int) (string, error) {
charSet := []rune(charSetStr)
// This buffer facilitates pre-emptively creation of random uint32s
// to reduce syscall overhead.
ibuf := make([]byte, 4*size)
_, err := rand.Read(ibuf)
if err != nil {
return "", err
}
var buf strings.Builder
buf.Grow(size)
for i := 0; i < size; i++ {
n := binary.BigEndian.Uint32(ibuf[i*4 : (i+1)*4])
_, _ = buf.WriteRune(charSet[n%uint32(len(charSet))])
}
return buf.String(), nil
}
b.SetBytes(20)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = mkstring(cryptorand.Default, 20)
}
}
func BenchmarkStringBigint20(b *testing.B) {
mkstring := func(charSetStr string, size int) (string, error) {
charSet := []rune(charSetStr)
var buf strings.Builder
buf.Grow(size)
bi := big.NewInt(int64(size))
for i := 0; i < size; i++ {
num, err := rand.Int(rand.Reader, bi)
if err != nil {
return "", err
}
_, _ = buf.WriteRune(charSet[num.Uint64()%uint64(len(charSet))])
}
return buf.String(), nil
}
b.SetBytes(20)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_, _ = mkstring(cryptorand.Default, 20)
}
}
func BenchmarkStringRuneCast(b *testing.B) {
s := strings.Repeat("0", 20)
b.SetBytes(int64(len(s)))
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = []rune(s)
}
}

4
go.mod
View File

@ -18,6 +18,7 @@ require (
github.com/hashicorp/hc-install v0.3.1
github.com/hashicorp/terraform-config-inspect v0.0.0-20211115214459-90acf1ca460f
github.com/hashicorp/terraform-exec v0.15.0
github.com/justinas/nosurf v1.1.1
github.com/lib/pq v1.10.4
github.com/ory/dockertest/v3 v3.8.1
github.com/pion/datachannel v1.5.2
@ -26,6 +27,7 @@ require (
github.com/pion/webrtc/v3 v3.1.13
github.com/spf13/cobra v1.3.0
github.com/stretchr/testify v1.7.0
github.com/unrolled/secure v1.0.9
go.uber.org/atomic v1.7.0
go.uber.org/goleak v1.1.12
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
@ -65,7 +67,6 @@ require (
github.com/hashicorp/terraform-json v0.13.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/justinas/nosurf v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
@ -91,7 +92,6 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/unrolled/secure v1.0.9 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect

1
go.sum
View File

@ -1159,6 +1159,7 @@ github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/
github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=