package main | |
import ( | |
"crypto/rand" | |
"encoding/base64" | |
"fmt" | |
"io" | |
"math/big" | |
) | |
// Adapted from https://elithrar.github.io/article/generating-secure-random-numbers-crypto-rand/ | |
func init() { | |
assertAvailablePRNG() | |
} | |
func assertAvailablePRNG() { | |
// Assert that a cryptographically secure PRNG is available. | |
// Panic otherwise. | |
buf := make([]byte, 1) | |
_, err := io.ReadFull(rand.Reader, buf) | |
if err != nil { | |
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err)) | |
} | |
} | |
// GenerateRandomBytes returns securely generated random bytes. | |
// It will return an error if the system's secure random | |
// number generator fails to function correctly, in which | |
// case the caller should not continue. | |
func GenerateRandomBytes(n int) ([]byte, error) { | |
b := make([]byte, n) | |
_, err := rand.Read(b) | |
// Note that err == nil only if we read len(b) bytes. | |
if err != nil { | |
return nil, err | |
} | |
return b, nil | |
} | |
// GenerateRandomString returns a securely generated random string. | |
// It will return an error if the system's secure random | |
// number generator fails to function correctly, in which | |
// case the caller should not continue. | |
func GenerateRandomString(n int) (string, error) { | |
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" | |
ret := make([]byte, n) | |
for i := 0; i < n; i++ { | |
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) | |
if err != nil { | |
return "", err | |
} | |
ret[i] = letters[num.Int64()] | |
} | |
return string(ret), nil | |
} | |
// GenerateRandomStringURLSafe returns a URL-safe, base64 encoded | |
// securely generated random string. | |
// It will return an error if the system's secure random | |
// number generator fails to function correctly, in which | |
// case the caller should not continue. | |
func GenerateRandomStringURLSafe(n int) (string, error) { | |
b, err := GenerateRandomBytes(n) | |
return base64.URLEncoding.EncodeToString(b), err | |
} | |
func main() { | |
// Example: this will give us a 44 byte, base64 encoded output | |
token, err := GenerateRandomStringURLSafe(32) | |
if err != nil { | |
// Serve an appropriately vague error to the | |
// user, but log the details internally. | |
panic(err) | |
} | |
fmt.Println(token) | |
// Example: this will give us a 32 byte output | |
token, err = GenerateRandomString(32) | |
if err != nil { | |
// Serve an appropriately vague error to the | |
// user, but log the details internally. | |
panic(err) | |
} | |
fmt.Println(token) | |
} |
One thing to note is that it looks like this code suffers from modulo bias. On line 53, the following code is used:
bytes[i] = letters[b%byte(len(letters))]
b
is a random byte between 0 and 255 inclusive, while theletters
array is 63 characters.255 % 63
is 3, which means the characters0
,1
, and2
, will have a slightly higher chance of showing up in your generated string. It could get even worse with other values for theletters
constant`A better way of implementing this to avoid statistical bias would probably be via Golang's
crypto.Int
function. That way, you can generate a random number between0
andlen(letters) - 1
with statistical uniformity. Something along the lines of:func GenerateRandomString(n int) (string, error) { const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-" ret := make([]byte, n) for i := 0; i < n; i++ { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) if err != nil { return "", err } ret = append(ret, letters[num.Int64()]) } return string(ret), nil }EDIT
Actually, it appears this exact same criticism was given for the code this was forked from: https://gist.github.com/denisbrodbeck/635a644089868a51eccd6ae22b2eb800#gistcomment-2227109
ret := make([]byte,n) // len(ret) = n
...
ret = append(ret, letters[num.Int64()]) // len(ret) = 2n
@kaigedong You're right, that's my bad. I constantly forget when I pre-allocate my slices :)
Here's the fixed version
func GenerateRandomString(n int) (string, error) {
const letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-"
ret := make([]byte, n)
for i := 0; i < n; i++ {
num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters))))
if err != nil {
return "", err
}
ret[i] = letters[num.Int64()]
}
return string(ret), nil
}
thanks
Thanks folks. Gist updated.
With regards to licensing, I'm not sure. As already stated, I forked from @denisbrodbeck. Not sure what licensing (if any) was intended.
Thanks @denisbrodbeck. Cheers!
Thanks for the useful snippets!
For me, the supposedly URLsafe string generated with GenerateRandomStringURLSafe()
unfortunately contained "=".
I now use base64.RawURLEncoding
instead of base64.URLEncoding
, so the strings are really URLsafe ;)
Cheers
@stonymahony is that something I should update the gist with?
Had to add "math/big"
to imports to get this code to work FYI
@kyle-aoki added. thanks!
Maybe give this a shot and improve it for others to use as well ? I had similar usecase sometime ago https://pkg.go.dev/github.com/imusmanmalik/randomizer
thanks
Can't you just do rand.read and get random bytes, then encode it to base64?
One thing to note is that it looks like this code suffers from modulo bias. On line 53, the following code is used:
b
is a random byte between 0 and 255 inclusive, while theletters
array is 63 characters.255 % 63
is 3, which means the characters0
,1
, and2
, will have a slightly higher chance of showing up in your generated string. It could get even worse with other values for theletters
constant`A better way of implementing this to avoid statistical bias would probably be via Golang's
crypto.Int
function. That way, you can generate a random number between0
andlen(letters) - 1
with statistical uniformity. Something along the lines of:EDIT
Actually, it appears this exact same criticism was given for the code this was forked from: https://gist.github.com/denisbrodbeck/635a644089868a51eccd6ae22b2eb800#gistcomment-2227109