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