Skip to content

Instantly share code, notes, and snippets.

@dopey
Forked from denisbrodbeck/main.go
Last active April 13, 2024 08:07
Show Gist options
  • Star 98 You must be signed in to star a gist
  • Fork 24 You must be signed in to fork a gist
  • Save dopey/c69559607800d2f2f90b1b1ed4e550fb to your computer and use it in GitHub Desktop.
Save dopey/c69559607800d2f2f90b1b1ed4e550fb to your computer and use it in GitHub Desktop.
How to generate secure random strings in golang with crypto/rand.
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)
}
@nanmu42
Copy link

nanmu42 commented Jan 17, 2018

Cool! 👍

@aaomidi
Copy link

aaomidi commented May 11, 2018

What is the licensing of this?

@shadez95
Copy link

I would like to know licensing of this as well

@terrabitz
Copy link

terrabitz commented Jul 30, 2020

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 the letters array is 63 characters. 255 % 63 is 3, which means the characters 0, 1, and 2, will have a slightly higher chance of showing up in your generated string. It could get even worse with other values for the letters 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 between 0 and len(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

@kaigedong
Copy link

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 the letters array is 63 characters. 255 % 63 is 3, which means the characters 0, 1, and 2, will have a slightly higher chance of showing up in your generated string. It could get even worse with other values for the letters 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 between 0 and len(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

@terrabitz
Copy link

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

@bubenkoff
Copy link

thanks

@dopey
Copy link
Author

dopey commented Jan 22, 2021

Thanks folks. Gist updated.

@dopey
Copy link
Author

dopey commented Jan 22, 2021

With regards to licensing, I'm not sure. As already stated, I forked from @denisbrodbeck. Not sure what licensing (if any) was intended.

@denisbrodbeck
Copy link

@dopey, @aaomidi, @shadez95

License is MIT.
My gist was just a note to myself, never thought of adding a license to it. Feel free to use it, just use the revised version which does not suffer from modulo bias 😄

@dopey
Copy link
Author

dopey commented Jan 23, 2021

Thanks @denisbrodbeck. Cheers!

@stonymahony
Copy link

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

@dopey
Copy link
Author

dopey commented Sep 2, 2021

@stonymahony is that something I should update the gist with?

@kyle-aoki
Copy link

Had to add "math/big" to imports to get this code to work FYI

@dopey
Copy link
Author

dopey commented Sep 24, 2021

@kyle-aoki added. thanks!

@imusmanmalik
Copy link

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

@unlessbamboo
Copy link

thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment