Move to Go 1.26, embed the wordlist from english.txt, add ValidateWordlist, NormalizeWordlist, and GenerateMnemonic with tests, and remove the old root-level CLI that used the previous module path.
119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
package bip39wordlist
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/tyler-smith/go-bip39"
|
|
)
|
|
|
|
const (
|
|
// MinWordCount is the number of words in the shortest valid mnemonic,
|
|
// which encodes 128 bits of entropy.
|
|
MinWordCount = 12
|
|
|
|
// MaxWordCount is the number of words in the longest valid mnemonic,
|
|
// which encodes 256 bits of entropy.
|
|
MaxWordCount = 24
|
|
|
|
// WordCountStep is the granularity of valid word counts. Every valid
|
|
// mnemonic has a multiple of three words, because each group of three
|
|
// words carries 32 bits of entropy plus one checksum bit.
|
|
WordCountStep = 3
|
|
|
|
// entropyBitsPerWordGroup is the entropy carried by each group of
|
|
// WordCountStep words.
|
|
entropyBitsPerWordGroup = 32
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidWordCount is returned when a mnemonic does not have 12,
|
|
// 15, 18, 21, or 24 words.
|
|
ErrInvalidWordCount = errors.New(
|
|
"bip39wordlist: mnemonic must have 12, 15, 18, 21, or 24 words",
|
|
)
|
|
|
|
// ErrUnknownWord is returned when a mnemonic contains a word that is
|
|
// not in the wordlist. The returned error names the word and its
|
|
// one-based position.
|
|
ErrUnknownWord = errors.New("bip39wordlist: word is not in the wordlist")
|
|
|
|
// ErrInvalidChecksum is returned when every word is valid but the
|
|
// checksum bits encoded in the final word do not match the entropy.
|
|
ErrInvalidChecksum = errors.New("bip39wordlist: mnemonic checksum mismatch")
|
|
)
|
|
|
|
// ValidWordCount reports whether n is a valid number of words for a
|
|
// mnemonic: a multiple of three from 12 to 24 inclusive.
|
|
func ValidWordCount(n int) bool {
|
|
return n >= MinWordCount && n <= MaxWordCount && n%WordCountStep == 0
|
|
}
|
|
|
|
// ValidateWordlist checks that mnemonic is a valid BIP39 English mnemonic:
|
|
// a valid number of words, every word in the wordlist, and a correct
|
|
// checksum. Letter case and the amount of whitespace between words are
|
|
// ignored, so any phrase ValidateWordlist accepts can be canonicalized with
|
|
// NormalizeWordlist. It returns nil for a valid mnemonic and otherwise an
|
|
// error wrapping ErrInvalidWordCount, ErrUnknownWord, or
|
|
// ErrInvalidChecksum.
|
|
func ValidateWordlist(mnemonic string) error {
|
|
words := splitWords(mnemonic)
|
|
|
|
if !ValidWordCount(len(words)) {
|
|
return fmt.Errorf("%w: got %d", ErrInvalidWordCount, len(words))
|
|
}
|
|
|
|
for i, word := range words {
|
|
if !Contains(word) {
|
|
return fmt.Errorf("%w: %q at position %d", ErrUnknownWord, word, i+1)
|
|
}
|
|
}
|
|
|
|
if !bip39.IsMnemonicValid(strings.Join(words, " ")) {
|
|
return ErrInvalidChecksum
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// NormalizeWordlist returns mnemonic in canonical form: lowercase, with
|
|
// the words separated by exactly one space and no leading or trailing
|
|
// whitespace. It refuses invalid input: if mnemonic fails
|
|
// ValidateWordlist, that error is returned and the string is empty.
|
|
func NormalizeWordlist(mnemonic string) (string, error) {
|
|
err := ValidateWordlist(mnemonic)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return strings.Join(splitWords(mnemonic), " "), nil
|
|
}
|
|
|
|
// GenerateMnemonic returns a new random mnemonic of wordCount words, using
|
|
// entropy from crypto/rand. wordCount must satisfy ValidWordCount.
|
|
func GenerateMnemonic(wordCount int) (string, error) {
|
|
if !ValidWordCount(wordCount) {
|
|
return "", fmt.Errorf("%w: got %d", ErrInvalidWordCount, wordCount)
|
|
}
|
|
|
|
entropyBits := wordCount / WordCountStep * entropyBitsPerWordGroup
|
|
|
|
entropy, err := bip39.NewEntropy(entropyBits)
|
|
if err != nil {
|
|
return "", fmt.Errorf("bip39wordlist: generating entropy: %w", err)
|
|
}
|
|
|
|
mnemonic, err := bip39.NewMnemonic(entropy)
|
|
if err != nil {
|
|
return "", fmt.Errorf("bip39wordlist: encoding mnemonic: %w", err)
|
|
}
|
|
|
|
return mnemonic, nil
|
|
}
|
|
|
|
// splitWords lowercases mnemonic and splits it on any run of whitespace.
|
|
func splitWords(mnemonic string) []string {
|
|
return strings.Fields(strings.ToLower(mnemonic))
|
|
}
|