262 lines
6.8 KiB
Go
262 lines
6.8 KiB
Go
// Package password provides a library for generating high-entropy random
|
|
// password strings via the crypto/rand package.
|
|
//
|
|
// res, err := Generate(64, 10, 10, false, false)
|
|
// if err != nil {
|
|
// log.Fatal(err)
|
|
// }
|
|
// log.Printf(res)
|
|
//
|
|
// Most functions are safe for concurrent use.
|
|
package password
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"io"
|
|
"math/big"
|
|
"strings"
|
|
)
|
|
|
|
// Built-time checks that the generators implement the interface.
|
|
var _ PasswordGenerator = (*Generator)(nil)
|
|
|
|
// PasswordGenerator is an interface that implements the Generate function. This
|
|
// is useful for testing where you can pass this interface instead of a real
|
|
// password generator to mock responses for predicability.
|
|
type PasswordGenerator interface {
|
|
Generate(int, int, int, bool, bool) (string, error)
|
|
MustGenerate(int, int, int, bool, bool) string
|
|
}
|
|
|
|
const (
|
|
// LowerLetters is the list of lowercase letters.
|
|
LowerLetters = "abcdefghijklmnopqrstuvwxyz"
|
|
|
|
// UpperLetters is the list of uppercase letters.
|
|
UpperLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
// Digits is the list of permitted digits.
|
|
Digits = "0123456789"
|
|
|
|
// Symbols is the list of symbols.
|
|
Symbols = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
|
|
)
|
|
|
|
var (
|
|
// ErrExceedsTotalLength is the error returned with the number of digits and
|
|
// symbols is greater than the total length.
|
|
ErrExceedsTotalLength = errors.New("number of digits and symbols must be less than total length")
|
|
|
|
// ErrLettersExceedsAvailable is the error returned with the number of letters
|
|
// exceeds the number of available letters and repeats are not allowed.
|
|
ErrLettersExceedsAvailable = errors.New("number of letters exceeds available letters and repeats are not allowed")
|
|
|
|
// ErrDigitsExceedsAvailable is the error returned with the number of digits
|
|
// exceeds the number of available digits and repeats are not allowed.
|
|
ErrDigitsExceedsAvailable = errors.New("number of digits exceeds available digits and repeats are not allowed")
|
|
|
|
// ErrSymbolsExceedsAvailable is the error returned with the number of symbols
|
|
// exceeds the number of available symbols and repeats are not allowed.
|
|
ErrSymbolsExceedsAvailable = errors.New("number of symbols exceeds available symbols and repeats are not allowed")
|
|
)
|
|
|
|
// Generator is the stateful generator which can be used to customize the list
|
|
// of letters, digits, and/or symbols.
|
|
type Generator struct {
|
|
lowerLetters string
|
|
upperLetters string
|
|
digits string
|
|
symbols string
|
|
reader io.Reader
|
|
}
|
|
|
|
// GeneratorInput is used as input to the NewGenerator function.
|
|
type GeneratorInput struct {
|
|
LowerLetters string
|
|
UpperLetters string
|
|
Digits string
|
|
Symbols string
|
|
Reader io.Reader // rand.Reader by default
|
|
}
|
|
|
|
// NewGenerator creates a new Generator from the specified configuration. If no
|
|
// input is given, all the default values are used. This function is safe for
|
|
// concurrent use.
|
|
func NewGenerator(i *GeneratorInput) (*Generator, error) {
|
|
if i == nil {
|
|
i = new(GeneratorInput)
|
|
}
|
|
|
|
g := &Generator{
|
|
lowerLetters: i.LowerLetters,
|
|
upperLetters: i.UpperLetters,
|
|
digits: i.Digits,
|
|
symbols: i.Symbols,
|
|
reader: i.Reader,
|
|
}
|
|
|
|
if g.lowerLetters == "" {
|
|
g.lowerLetters = LowerLetters
|
|
}
|
|
|
|
if g.upperLetters == "" {
|
|
g.upperLetters = UpperLetters
|
|
}
|
|
|
|
if g.digits == "" {
|
|
g.digits = Digits
|
|
}
|
|
|
|
if g.symbols == "" {
|
|
g.symbols = Symbols
|
|
}
|
|
|
|
if g.reader == nil {
|
|
g.reader = rand.Reader
|
|
}
|
|
|
|
return g, nil
|
|
}
|
|
|
|
// Generate generates a password with the given requirements. length is the
|
|
// total number of characters in the password. numDigits is the number of digits
|
|
// to include in the result. numSymbols is the number of symbols to include in
|
|
// the result. noUpper excludes uppercase letters from the results. allowRepeat
|
|
// allows characters to repeat.
|
|
//
|
|
// The algorithm is fast, but it's not designed to be performant; it favors
|
|
// entropy over speed. This function is safe for concurrent use.
|
|
func (g *Generator) Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) {
|
|
letters := g.lowerLetters
|
|
if !noUpper {
|
|
letters += g.upperLetters
|
|
}
|
|
|
|
chars := length - numDigits - numSymbols
|
|
if chars < 0 {
|
|
return "", ErrExceedsTotalLength
|
|
}
|
|
|
|
if !allowRepeat && chars > len(letters) {
|
|
return "", ErrLettersExceedsAvailable
|
|
}
|
|
|
|
if !allowRepeat && numDigits > len(g.digits) {
|
|
return "", ErrDigitsExceedsAvailable
|
|
}
|
|
|
|
if !allowRepeat && numSymbols > len(g.symbols) {
|
|
return "", ErrSymbolsExceedsAvailable
|
|
}
|
|
|
|
var result string
|
|
|
|
// Characters
|
|
for i := 0; i < chars; i++ {
|
|
ch, err := randomElement(g.reader, letters)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !allowRepeat && strings.Contains(result, ch) {
|
|
i--
|
|
continue
|
|
}
|
|
|
|
result, err = randomInsert(g.reader, result, ch)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Digits
|
|
for i := 0; i < numDigits; i++ {
|
|
d, err := randomElement(g.reader, g.digits)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !allowRepeat && strings.Contains(result, d) {
|
|
i--
|
|
continue
|
|
}
|
|
|
|
result, err = randomInsert(g.reader, result, d)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
// Symbols
|
|
for i := 0; i < numSymbols; i++ {
|
|
sym, err := randomElement(g.reader, g.symbols)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if !allowRepeat && strings.Contains(result, sym) {
|
|
i--
|
|
continue
|
|
}
|
|
|
|
result, err = randomInsert(g.reader, result, sym)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// MustGenerate is the same as Generate, but panics on error.
|
|
func (g *Generator) MustGenerate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) string {
|
|
res, err := g.Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// Generate is the package shortcut for Generator.Generate.
|
|
func Generate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) (string, error) {
|
|
gen, err := NewGenerator(nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return gen.Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
|
|
}
|
|
|
|
// MustGenerate is the package shortcut for Generator.MustGenerate.
|
|
func MustGenerate(length, numDigits, numSymbols int, noUpper, allowRepeat bool) string {
|
|
res, err := Generate(length, numDigits, numSymbols, noUpper, allowRepeat)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return res
|
|
}
|
|
|
|
// randomInsert randomly inserts the given value into the given string.
|
|
func randomInsert(reader io.Reader, s, val string) (string, error) {
|
|
if s == "" {
|
|
return val, nil
|
|
}
|
|
|
|
n, err := rand.Int(reader, big.NewInt(int64(len(s)+1)))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
i := n.Int64()
|
|
return s[0:i] + val + s[i:], nil
|
|
}
|
|
|
|
// randomElement extracts a random element from the given string.
|
|
func randomElement(reader io.Reader, s string) (string, error) {
|
|
n, err := rand.Int(reader, big.NewInt(int64(len(s))))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(s[n.Int64()]), nil
|
|
}
|