Files
2026-09-20 14:50:20 +03:00

149 lines
3.9 KiB
Go

// Package bip39 implements BIP-39 mnemonic generation, validation and seed
// derivation for the English wordlist. It builds on crypto/pbkdf2 and an
// embedded copy of the canonical 2048-word list.
package bip39
import (
"crypto/pbkdf2"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
_ "embed"
"errors"
"strings"
)
//go:embed english.txt
var data string
// ErrInvalidMnemonic is returned for malformed or checksum-failing phrases.
var ErrInvalidMnemonic = errors.New("bip39: invalid mnemonic")
// ErrInvalidWordsCount is returned for unsupported word counts.
var ErrInvalidWordsCount = errors.New("bip39: invalid words count")
var words []string
var wordIndex map[string]int
func init() {
words = strings.Fields(data)
if len(words) != 2048 {
panic("bip39: english wordlist must contain 2048 words")
}
wordIndex = make(map[string]int, len(words))
for i, w := range words {
wordIndex[w] = i
}
}
// Words returns the embedded wordlist.
func Words() []string {
return words
}
// ValidWordCount reports whether n is a supported BIP-39 word count.
func ValidWordCount(n int) bool {
_, ok := entropyBits[n]
return ok
}
// entropyBits maps a supported word count to its entropy size in bits.
var entropyBits = map[int]int{12: 128, 15: 160, 18: 192, 21: 224, 24: 256}
// NewMnemonic generates a mnemonic with the given number of words using
// cryptographically secure entropy. Supported counts are 12, 15, 18, 21, 24.
func NewMnemonic(numWords int) (string, error) {
bits, ok := entropyBits[numWords]
if !ok {
return "", ErrInvalidWordsCount
}
entropy := make([]byte, bits/8)
if _, err := rand.Read(entropy); err != nil {
return "", err
}
return entropyToMnemonic(entropy)
}
// entropyToMnemonic encodes entropy with its SHA-256 checksum into words.
func entropyToMnemonic(entropy []byte) (string, error) {
cs := sha256.Sum256(entropy)
csBits := len(entropy) * 8 / 32
bits := make([]bool, 0, len(entropy)*8+csBits)
for _, b := range entropy {
for i := 7; i >= 0; i-- {
bits = append(bits, (b>>i)&1 == 1)
}
}
for i := 0; i < csBits; i++ {
bits = append(bits, (cs[i/8]>>(7-uint(i)%8))&1 == 1)
}
out := make([]string, 0, (len(entropy)*8+csBits)/11)
for i := 0; i+11 <= len(bits); i += 11 {
idx := 0
for j := 0; j < 11; j++ {
idx = idx<<1 | boolToInt(bits[i+j])
}
out = append(out, words[idx])
}
return strings.Join(out, " "), nil
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
// Validate reports whether the mnemonic is well-formed and checksum-correct.
func Validate(mnemonic string) bool {
_, ok := mnemonicToEntropy(mnemonic)
return ok
}
// mnemonicToEntropy decodes a mnemonic back to entropy, verifying checksum.
func mnemonicToEntropy(mnemonic string) ([]byte, bool) {
fields := strings.Fields(mnemonic)
wc := len(fields)
if _, ok := entropyBits[wc]; !ok {
return nil, false
}
totalBits := wc * 11
entropyLen := totalBits * 32 / 33 / 8
bits := make([]bool, 0, totalBits)
for _, w := range fields {
idx, ok := wordIndex[w]
if !ok {
return nil, false
}
for i := 10; i >= 0; i-- {
bits = append(bits, (idx>>i)&1 == 1)
}
}
entropy := make([]byte, entropyLen)
for i := 0; i < entropyLen*8; i++ {
if bits[i] {
entropy[i/8] |= 1 << (7 - uint(i)%8)
}
}
csBits := totalBits - entropyLen*8
cs := sha256.Sum256(entropy)
for i := 0; i < csBits; i++ {
if byte(boolToInt(bits[entropyLen*8+i])) != (cs[i/8]>>(7-uint(i)%8))&1 {
return nil, false
}
}
return entropy, true
}
// Seed derives the 64-byte BIP-39 seed from a mnemonic and passphrase using
// PBKDF2-HMAC-SHA512 with 2048 iterations.
func Seed(mnemonic, passphrase string) []byte {
normalized := strings.Join(strings.Fields(mnemonic), " ")
key, err := pbkdf2.Key(sha512.New, normalized, []byte("mnemonic"+passphrase), 2048, 64)
if err != nil {
panic(err)
}
return key
}