upd
This commit is contained in:
+148
@@ -0,0 +1,148 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package bip39
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestEntropyToMnemonicVectors uses the canonical BIP-39 English test vectors,
|
||||||
|
// which the tronpy-adjacent `mnemonic` package also satisfies.
|
||||||
|
func TestEntropyToMnemonicVectors(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
entropy string
|
||||||
|
mnemonic string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"00000000000000000000000000000000",
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||||
|
"legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"80808080808080808080808080808080",
|
||||||
|
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ffffffffffffffffffffffffffffffff",
|
||||||
|
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"000000000000000000000000000000000000000000000000",
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range cases {
|
||||||
|
entropy, err := hex.DecodeString(tc.entropy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode entropy: %v", err)
|
||||||
|
}
|
||||||
|
got, err := entropyToMnemonic(entropy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("entropyToMnemonic: %v", err)
|
||||||
|
}
|
||||||
|
if got != tc.mnemonic {
|
||||||
|
t.Fatalf("mnemonic = %q, want %q", got, tc.mnemonic)
|
||||||
|
}
|
||||||
|
if !Validate(got) {
|
||||||
|
t.Fatalf("Validate rejected generated mnemonic %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRejectsBadChecksum(t *testing.T) {
|
||||||
|
// Replace the final word with a different valid word to break the checksum.
|
||||||
|
bad := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon"
|
||||||
|
if Validate(bad) {
|
||||||
|
t.Fatalf("expected invalid checksum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRejectsUnknownWord(t *testing.T) {
|
||||||
|
bad := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon notaword"
|
||||||
|
if Validate(bad) {
|
||||||
|
t.Fatalf("expected unknown word rejection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateRejectsWrongCount(t *testing.T) {
|
||||||
|
if Validate("abandon abandon abandon") {
|
||||||
|
t.Fatalf("expected wrong-count rejection")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewMnemonicRoundTrips(t *testing.T) {
|
||||||
|
for _, wc := range []int{12, 15, 18, 21, 24} {
|
||||||
|
m, err := NewMnemonic(wc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewMnemonic(%d): %v", wc, err)
|
||||||
|
}
|
||||||
|
if fields := len(splitFields(m)); fields != wc {
|
||||||
|
t.Fatalf("NewMnemonic(%d) produced %d words", wc, fields)
|
||||||
|
}
|
||||||
|
if !Validate(m) {
|
||||||
|
t.Fatalf("Validate rejected a freshly generated mnemonic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := NewMnemonic(13); err == nil {
|
||||||
|
t.Fatalf("expected error for unsupported word count")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWordlistShape(t *testing.T) {
|
||||||
|
if got := len(Words()); got != 2048 {
|
||||||
|
t.Fatalf("wordlist size = %d, want 2048", got)
|
||||||
|
}
|
||||||
|
if !ValidWordCount(12) || ValidWordCount(13) {
|
||||||
|
t.Fatalf("ValidWordCount misreported")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitFields is a tiny local helper to avoid importing strings twice.
|
||||||
|
func splitFields(s string) []string {
|
||||||
|
var out []string
|
||||||
|
cur := ""
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] == ' ' {
|
||||||
|
if cur != "" {
|
||||||
|
out = append(out, cur)
|
||||||
|
cur = ""
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cur += string(s[i])
|
||||||
|
}
|
||||||
|
if cur != "" {
|
||||||
|
out = append(out, cur)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
+2048
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user