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.
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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))
|
||||
}
|
||||
Reference in New Issue
Block a user