package diceware import ( "crypto/rand" "fmt" "io" "math" "math/big" ) // sides is the number of sides on a die. var sides = big.NewInt(6) var _ DicewareGenerator = (*Generator)(nil) // Generator is the stateful generator which can be used to customize the word // list and other generation options. type Generator struct { wordList WordList randReader io.Reader } // GeneratorInput is used as input to the NewGenerator function. type GeneratorInput struct { // WordList is the word list to use. There are built-in word lists like // WordListEffBig (default), WordListEffSmall, and WordListOriginal. You can // also bring your own word list by implementing the WordList interface. WordList WordList // RandReader is an optional reader to use in place of the default // (crypto/rand.Reader), which can be used to generate repeatable sets of // words RandReader io.Reader } // 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) } if i.WordList == nil { i.WordList = WordListEffLarge() } gen := &Generator{ wordList: i.WordList, randReader: i.RandReader, } if gen.randReader == nil { gen.randReader = rand.Reader } return gen, nil } // Generate generates a collection of diceware words, specified by the numWords // parameter. // // The algorithm is fast, but it's not designed to be performant, favoring // entropy over speed. // // This function is safe for concurrent use, but there is a possibility of // concurrent invocations generating overlapping words. To generate multiple // non-overlapping words, use a single invocation of the function and split the // resulting string list. func (g *Generator) Generate(numWords int) ([]string, error) { if typ, ok := g.wordList.(WordListNumWordser); ok { if l := typ.NumWords(); numWords > l { return nil, fmt.Errorf("number of requested words (%d) cannot exceed the size of the wordlist (%d)", numWords, l) } } list := make([]string, 0, numWords) seen := make(map[string]struct{}, numWords) for i := 0; i < numWords; i++ { n, err := g.RollWord(g.wordList.Digits()) if err != nil { return nil, err } word := g.wordList.WordAt(n) if _, ok := seen[word]; ok { i-- continue } list = append(list, word) seen[word] = struct{}{} } return list, nil } // MustGenerate is the same as Generate, but panics on error. func (g *Generator) MustGenerate(numWords int) []string { list, err := g.Generate(numWords) if err != nil { panic(err) } return list } // Generate - see Generator.Generate for usage. func Generate(numWords int) ([]string, error) { gen, err := NewGenerator(nil) if err != nil { return nil, err } return gen.Generate(numWords) } // MustGenerate - see Generator.MustGenerate for usage. func MustGenerate(numWords int) []string { gen, err := NewGenerator(nil) if err != nil { panic(err) } return gen.MustGenerate(numWords) } // GenerateWithWordList generates a list of the given number of words from the // given word list. func GenerateWithWordList(numWords int, wordList WordList) ([]string, error) { gen, err := NewGenerator(&GeneratorInput{ WordList: wordList, }) if err != nil { return nil, err } return gen.Generate(numWords) } // WordAt retrieves the word at the given index from EFF's large wordlist. // // Deprecated: Use WordList.WordAt instead. func WordAt(i int) string { return WordListEffLarge().WordAt(i) } // RollDie rolls a single 6-sided die and returns a value between [1,6]. // // Internally this creates a new Generator with a nil configuration and calls // Generator.RollDie. func RollDie() (int, error) { gen, err := NewGenerator(nil) if err != nil { return 0, err } return gen.RollDie() } // RollWord rolls and aggregates dice to represent one word in the list. The // result is the index of the word in the list. // // Internally this creates a new Generator with a nil configuration and calls // Generator.RollWord. func RollWord(d int) (int, error) { gen, err := NewGenerator(nil) if err != nil { return 0, err } return gen.RollWord(d) } // RollDie rolls a single 6-sided die and returns a value between [1,6]. func (g *Generator) RollDie() (int, error) { r, err := rand.Int(g.randReader, sides) if err != nil { return 0, fmt.Errorf("failed to generate a random number: %w", err) } return int(r.Int64()) + 1, nil } // RollWord rolls and aggregates dice to represent one word in the list. The // result is the index of the word in the list. func (g *Generator) RollWord(d int) (int, error) { var final int for i := d; i > 0; i-- { res, err := g.RollDie() if err != nil { return 0, err } final += res * int(math.Pow(10, float64(i-1))) } return final, nil }