diff --git a/cryptorand/numbers.go b/cryptorand/numbers.go new file mode 100644 index 0000000000000..e685aa41d0d63 --- /dev/null +++ b/cryptorand/numbers.go @@ -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 +} diff --git a/cryptorand/numbers_test.go b/cryptorand/numbers_test.go new file mode 100644 index 0000000000000..612ec5b7f03fc --- /dev/null +++ b/cryptorand/numbers_test.go @@ -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") +} diff --git a/cryptorand/strings.go b/cryptorand/strings.go new file mode 100644 index 0000000000000..897a91d1058e2 --- /dev/null +++ b/cryptorand/strings.go @@ -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) +} diff --git a/cryptorand/strings_test.go b/cryptorand/strings_test.go new file mode 100644 index 0000000000000..df81a85320119 --- /dev/null +++ b/cryptorand/strings_test.go @@ -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) + } +} diff --git a/go.mod b/go.mod index 577e7278aa438..1bf9c4108b520 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 009f6283c4c39..65f858c429c8f 100644 --- a/go.sum +++ b/go.sum @@ -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=