check / check (push) Waiting to run
go-bip39's repository no longer exists. The part of v1.1.0 that secret uses (NewEntropy, NewMnemonic, NewSeed, IsMnemonicValid and what they call; English word list only) now lives in internal/bip39 with upstream's LICENSE beside it, along with upstream's tests of that code and their vectors unchanged. Every import moves there, and the module leaves go.mod and go.sum. Beyond the trimming, only what the linter asked for changed. No derived key or mnemonic changes. Model: opus-5-5
286 lines
8.3 KiB
Go
286 lines
8.3 KiB
Go
// Package bip39 is the Golang implementation of the BIP39 spec.
|
|
//
|
|
// The official BIP39 spec can be found at
|
|
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
|
|
//
|
|
// It is a copy of github.com/tyler-smith/go-bip39 v1.1.0, trimmed to what
|
|
// secret uses.
|
|
//
|
|
//nolint:mnd // the numbers are BIP-39's own, written as upstream writes them
|
|
package bip39
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/sha512"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
|
|
"golang.org/x/crypto/pbkdf2"
|
|
)
|
|
|
|
var (
|
|
// ErrInvalidMnemonic is returned when trying to use a malformed mnemonic.
|
|
ErrInvalidMnemonic = errors.New("invalid mnenomic")
|
|
|
|
// ErrEntropyLengthInvalid is returned when trying to use an entropy set with
|
|
// an invalid size.
|
|
ErrEntropyLengthInvalid = errors.New(
|
|
"entropy length must be [128, 256] and a multiple of 32",
|
|
)
|
|
|
|
// ErrChecksumIncorrect is returned when entropy has the incorrect checksum.
|
|
ErrChecksumIncorrect = errors.New("checksum incorrect")
|
|
)
|
|
|
|
// NewEntropy will create random entropy bytes
|
|
// so long as the requested size bitSize is an appropriate size.
|
|
//
|
|
// bitSize has to be a multiple 32 and be within the inclusive range of {128, 256}
|
|
func NewEntropy(bitSize int) ([]byte, error) {
|
|
err := validateEntropyBitSize(bitSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
entropy := make([]byte, bitSize/8)
|
|
_, err = rand.Read(entropy)
|
|
|
|
return entropy, err
|
|
}
|
|
|
|
// EntropyFromMnemonic takes a mnemonic generated by this library,
|
|
// and returns the input entropy used to generate the given mnemonic.
|
|
// An error is returned if the given mnemonic is invalid.
|
|
func EntropyFromMnemonic(mnemonic string) ([]byte, error) {
|
|
mnemonicSlice, isValid := splitMnemonicWords(mnemonic)
|
|
if !isValid {
|
|
return nil, ErrInvalidMnemonic
|
|
}
|
|
|
|
// Some bitwise operands for working with big.Ints
|
|
shift11BitsMask := big.NewInt(2048)
|
|
bigOne := big.NewInt(1)
|
|
|
|
// used to isolate the checksum bits from the entropy+checksum byte array
|
|
wordLengthChecksumMasksMapping := map[int]*big.Int{
|
|
12: big.NewInt(15),
|
|
15: big.NewInt(31),
|
|
18: big.NewInt(63),
|
|
21: big.NewInt(127),
|
|
24: big.NewInt(255),
|
|
}
|
|
// used to use only the desired x of 8 available checksum bits.
|
|
// 256 bit (word length 24) requires all 8 bits of the checksum,
|
|
// and thus no shifting is needed for it (we would get a divByZero crash if we did)
|
|
wordLengthChecksumShiftMapping := map[int]*big.Int{
|
|
12: big.NewInt(16),
|
|
15: big.NewInt(8),
|
|
18: big.NewInt(4),
|
|
21: big.NewInt(2),
|
|
}
|
|
|
|
// wordMap is a reverse lookup map for the word list
|
|
wordMap := map[string]int{}
|
|
for i, v := range English() {
|
|
wordMap[v] = i
|
|
}
|
|
|
|
// Decode the words into a big.Int.
|
|
b := big.NewInt(0)
|
|
|
|
for _, v := range mnemonicSlice {
|
|
index, found := wordMap[v]
|
|
if !found {
|
|
return nil, fmt.Errorf(
|
|
"%w: word `%v` not found in reverse map", ErrInvalidMnemonic, v,
|
|
)
|
|
}
|
|
|
|
var wordBytes [2]byte
|
|
|
|
//nolint:gosec // the index of a word in the list is below 2048
|
|
binary.BigEndian.PutUint16(wordBytes[:], uint16(index))
|
|
|
|
b = b.Mul(b, shift11BitsMask)
|
|
b = b.Or(b, big.NewInt(0).SetBytes(wordBytes[:]))
|
|
}
|
|
|
|
// Build and add the checksum to the big.Int.
|
|
checksum := big.NewInt(0)
|
|
checksumMask := wordLengthChecksumMasksMapping[len(mnemonicSlice)]
|
|
checksum = checksum.And(b, checksumMask)
|
|
|
|
b.Div(b, big.NewInt(0).Add(checksumMask, bigOne))
|
|
|
|
// The entropy is the underlying bytes of the big.Int. Any upper bytes of
|
|
// all 0's are not returned so we pad the beginning of the slice with empty
|
|
// bytes if necessary.
|
|
entropy := b.Bytes()
|
|
entropy = padByteSlice(entropy, len(mnemonicSlice)/3*4)
|
|
|
|
// Generate the checksum and compare with the one we got from the mneomnic.
|
|
entropyChecksumBytes := computeChecksum(entropy)
|
|
entropyChecksum := big.NewInt(int64(entropyChecksumBytes[0]))
|
|
|
|
if l := len(mnemonicSlice); l != 24 {
|
|
checksumShift := wordLengthChecksumShiftMapping[l]
|
|
entropyChecksum.Div(entropyChecksum, checksumShift)
|
|
}
|
|
|
|
if checksum.Cmp(entropyChecksum) != 0 {
|
|
return nil, ErrChecksumIncorrect
|
|
}
|
|
|
|
return entropy, nil
|
|
}
|
|
|
|
// NewMnemonic will return a string consisting of the mnemonic words for
|
|
// the given entropy.
|
|
// If the provide entropy is invalid, an error will be returned.
|
|
func NewMnemonic(entropy []byte) (string, error) {
|
|
// Compute some lengths for convenience.
|
|
entropyBitLength := len(entropy) * 8
|
|
checksumBitLength := entropyBitLength / 32
|
|
sentenceLength := (entropyBitLength + checksumBitLength) / 11
|
|
|
|
// Validate that the requested size is supported.
|
|
err := validateEntropyBitSize(entropyBitLength)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Some bitwise operands for working with big.Ints
|
|
last11BitsMask := big.NewInt(2047)
|
|
shift11BitsMask := big.NewInt(2048)
|
|
|
|
// wordList is the set of words to use
|
|
wordList := English()
|
|
|
|
// Add checksum to entropy.
|
|
entropy = addChecksum(entropy)
|
|
|
|
// Break entropy up into sentenceLength chunks of 11 bits.
|
|
// For each word AND mask the rightmost 11 bits and find the word at that index.
|
|
// Then bitshift entropy 11 bits right and repeat.
|
|
// Add to the last empty slot so we can work with LSBs instead of MSB.
|
|
|
|
// Entropy as an int so we can bitmask without worrying about bytes slices.
|
|
entropyInt := new(big.Int).SetBytes(entropy)
|
|
|
|
// Slice to hold words in.
|
|
words := make([]string, sentenceLength)
|
|
|
|
// Throw away big.Int for AND masking.
|
|
word := big.NewInt(0)
|
|
|
|
for i := sentenceLength - 1; i >= 0; i-- {
|
|
// Get 11 right most bits and bitshift 11 to the right for next time.
|
|
word.And(entropyInt, last11BitsMask)
|
|
entropyInt.Div(entropyInt, shift11BitsMask)
|
|
|
|
// Get the bytes representing the 11 bits as a 2 byte slice.
|
|
wordBytes := padByteSlice(word.Bytes(), 2)
|
|
|
|
// Convert bytes to an index and add that word to the list.
|
|
words[i] = wordList[binary.BigEndian.Uint16(wordBytes)]
|
|
}
|
|
|
|
return strings.Join(words, " "), nil
|
|
}
|
|
|
|
// NewSeed creates a hashed seed output given a provided string and password.
|
|
// No checking is performed to validate that the string provided is a valid mnemonic.
|
|
func NewSeed(mnemonic string, password string) []byte {
|
|
return pbkdf2.Key([]byte(mnemonic), []byte("mnemonic"+password), 2048, 64, sha512.New)
|
|
}
|
|
|
|
// IsMnemonicValid attempts to verify that the provided mnemonic is valid.
|
|
// Validity is determined by both the number of words being appropriate,
|
|
// and that all the words in the mnemonic are present in the word list.
|
|
func IsMnemonicValid(mnemonic string) bool {
|
|
_, err := EntropyFromMnemonic(mnemonic)
|
|
|
|
return err == nil
|
|
}
|
|
|
|
// Appends to data the first (len(data) / 32)bits of the result of sha256(data)
|
|
// Currently only supports data up to 32 bytes
|
|
func addChecksum(data []byte) []byte {
|
|
// Some bitwise operands for working with big.Ints
|
|
bigOne := big.NewInt(1)
|
|
bigTwo := big.NewInt(2)
|
|
|
|
// Get first byte of sha256
|
|
hash := computeChecksum(data)
|
|
firstChecksumByte := hash[0]
|
|
|
|
// len() is in bytes so we divide by 4
|
|
checksumBitLength := uint(len(data) / 4)
|
|
|
|
// For each bit of check sum we want we shift the data one the left
|
|
// and then set the (new) right most bit equal to checksum bit at that index
|
|
// staring from the left
|
|
dataBigInt := new(big.Int).SetBytes(data)
|
|
for i := range checksumBitLength {
|
|
// Bitshift 1 left
|
|
dataBigInt.Mul(dataBigInt, bigTwo)
|
|
|
|
// Set rightmost bit if leftmost checksum bit is set
|
|
if firstChecksumByte&(1<<(7-i)) > 0 {
|
|
dataBigInt.Or(dataBigInt, bigOne)
|
|
}
|
|
}
|
|
|
|
return dataBigInt.Bytes()
|
|
}
|
|
|
|
func computeChecksum(data []byte) []byte {
|
|
hasher := sha256.New()
|
|
hasher.Write(data)
|
|
|
|
return hasher.Sum(nil)
|
|
}
|
|
|
|
// validateEntropyBitSize ensures that entropy is the correct size for being a
|
|
// mnemonic.
|
|
func validateEntropyBitSize(bitSize int) error {
|
|
if (bitSize%32) != 0 || bitSize < 128 || bitSize > 256 {
|
|
return ErrEntropyLengthInvalid
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// padByteSlice returns a byte slice of the given size with contents of the
|
|
// given slice left padded and any empty spaces filled with 0's.
|
|
func padByteSlice(slice []byte, length int) []byte {
|
|
offset := length - len(slice)
|
|
if offset <= 0 {
|
|
return slice
|
|
}
|
|
|
|
newSlice := make([]byte, length)
|
|
copy(newSlice[offset:], slice)
|
|
|
|
return newSlice
|
|
}
|
|
|
|
func splitMnemonicWords(mnemonic string) ([]string, bool) {
|
|
// Create a list of all the words in the mnemonic sentence
|
|
words := strings.Fields(mnemonic)
|
|
|
|
// Get num of words
|
|
numOfWords := len(words)
|
|
|
|
// The number of words should be 12, 15, 18, 21 or 24
|
|
if numOfWords%3 != 0 || numOfWords < 12 || numOfWords > 24 {
|
|
return nil, false
|
|
}
|
|
|
|
return words, true
|
|
}
|