Files
sneak 02386a6ff1 Rename module to sneak.berlin/go/x and rework bip39wordlist
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.
2026-10-31 10:03:06 -07:00

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