196 lines
5.1 KiB
Go
196 lines
5.1 KiB
Go
// Package pwgen generates memorable passwords from the system dictionary.
|
|
// It backs `sutil generate password`.
|
|
//
|
|
// A password is three dictionary words, each followed by one decimal
|
|
// digit, joined by a delimiter that is chosen at random from Delimiters
|
|
// and used in both positions:
|
|
//
|
|
// <word><digit><delim><word><digit><delim><word><digit>
|
|
//
|
|
// for example "marble7-sprout2-velvet9".
|
|
//
|
|
// Words come from the operating system's word list (/usr/share/dict/words
|
|
// on macOS and most Unix systems) through github.com/kavfixnel/words,
|
|
// filtered to words made only of lowercase ASCII letters whose length is
|
|
// within the configured bounds. That drops proper nouns, abbreviations,
|
|
// and anything awkward to type. All random choices use crypto/rand.
|
|
package pwgen
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/kavfixnel/words"
|
|
)
|
|
|
|
const (
|
|
// WordCount is the number of words in every generated password.
|
|
WordCount = 3
|
|
|
|
// Delimiters is the set of characters from which the delimiter that
|
|
// separates the words of a password is chosen.
|
|
Delimiters = ".-_|~+=:,!@#%^&*"
|
|
|
|
// DefaultMinWordLength is the shortest word used when
|
|
// Params.MinWordLength is zero.
|
|
DefaultMinWordLength = 4
|
|
|
|
// DefaultMaxWordLength is the longest word used when
|
|
// Params.MaxWordLength is zero.
|
|
DefaultMaxWordLength = 8
|
|
|
|
// digitCount is the number of decimal digits (0 through 9) one of
|
|
// which follows every word.
|
|
digitCount = 10
|
|
)
|
|
|
|
var (
|
|
// ErrNoUsableWords is returned by New when the word source yields no
|
|
// words within the length bounds. On a system without a word list,
|
|
// github.com/kavfixnel/words returns an empty list rather than an
|
|
// error, so this is also the error for "no system dictionary".
|
|
ErrNoUsableWords = errors.New("pwgen: no usable words in word list")
|
|
|
|
// ErrInvalidWordLength is returned by New when the word length bounds
|
|
// are negative or the minimum exceeds the maximum.
|
|
ErrInvalidWordLength = errors.New("pwgen: invalid word length bounds")
|
|
)
|
|
|
|
// Params configures New.
|
|
type Params struct {
|
|
// Words, when non-nil, is used instead of the system dictionary. It
|
|
// is filtered by the same length and character rules.
|
|
Words []string
|
|
|
|
// MinWordLength is the shortest word to use; zero means
|
|
// DefaultMinWordLength.
|
|
MinWordLength int
|
|
|
|
// MaxWordLength is the longest word to use; zero means
|
|
// DefaultMaxWordLength.
|
|
MaxWordLength int
|
|
}
|
|
|
|
// Generator produces passwords from a fixed set of usable words. It is
|
|
// safe for concurrent use.
|
|
type Generator struct {
|
|
words []string
|
|
}
|
|
|
|
// New loads and filters the word source described by p. It returns
|
|
// ErrNoUsableWords if no words survive filtering, which on a system
|
|
// without a word list is the normal outcome.
|
|
func New(p Params) (*Generator, error) {
|
|
minLength, maxLength := p.MinWordLength, p.MaxWordLength
|
|
|
|
if minLength == 0 {
|
|
minLength = DefaultMinWordLength
|
|
}
|
|
|
|
if maxLength == 0 {
|
|
maxLength = DefaultMaxWordLength
|
|
}
|
|
|
|
if minLength < 0 || maxLength < minLength {
|
|
return nil, fmt.Errorf(
|
|
"%w: min %d, max %d", ErrInvalidWordLength, minLength, maxLength,
|
|
)
|
|
}
|
|
|
|
source := p.Words
|
|
if source == nil {
|
|
systemWords, err := words.NewWordList(nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("pwgen: reading system word list: %w", err)
|
|
}
|
|
|
|
source = systemWords
|
|
}
|
|
|
|
usable := filterWords(source, minLength, maxLength)
|
|
if len(usable) == 0 {
|
|
return nil, ErrNoUsableWords
|
|
}
|
|
|
|
return &Generator{words: usable}, nil
|
|
}
|
|
|
|
// UsableWords returns the number of distinct words the generator chooses
|
|
// from, which determines the strength of its passwords.
|
|
func (g *Generator) UsableWords() int {
|
|
return len(g.words)
|
|
}
|
|
|
|
// Generate returns a new random password.
|
|
func (g *Generator) Generate() (string, error) {
|
|
delimiterIndex, err := randomIndex(len(Delimiters))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
delimiter := Delimiters[delimiterIndex]
|
|
|
|
var password strings.Builder
|
|
|
|
for i := range WordCount {
|
|
if i > 0 {
|
|
password.WriteByte(delimiter)
|
|
}
|
|
|
|
wordIndex, err := randomIndex(len(g.words))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
digit, err := randomIndex(digitCount)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
password.WriteString(g.words[wordIndex])
|
|
password.WriteString(strconv.Itoa(digit))
|
|
}
|
|
|
|
return password.String(), nil
|
|
}
|
|
|
|
// filterWords returns the entries of source that isUsableWord accepts.
|
|
func filterWords(source []string, minLength, maxLength int) []string {
|
|
usable := make([]string, 0, len(source))
|
|
|
|
for _, word := range source {
|
|
if isUsableWord(word, minLength, maxLength) {
|
|
usable = append(usable, word)
|
|
}
|
|
}
|
|
|
|
return usable
|
|
}
|
|
|
|
// isUsableWord reports whether word is made only of lowercase ASCII
|
|
// letters and has a length within the bounds.
|
|
func isUsableWord(word string, minLength, maxLength int) bool {
|
|
if len(word) < minLength || len(word) > maxLength {
|
|
return false
|
|
}
|
|
|
|
return !strings.ContainsFunc(word, func(r rune) bool {
|
|
return r < 'a' || r > 'z'
|
|
})
|
|
}
|
|
|
|
// randomIndex returns a uniformly random integer in [0, n) from
|
|
// crypto/rand.
|
|
func randomIndex(n int) (int, error) {
|
|
value, err := rand.Int(rand.Reader, big.NewInt(int64(n)))
|
|
if err != nil {
|
|
return 0, fmt.Errorf("pwgen: reading from crypto/rand: %w", err)
|
|
}
|
|
|
|
return int(value.Int64()), nil
|
|
}
|