package bip39 import ( "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/binary" "errors" "math/big" "strings" "github.com/tyler-smith/go-bip39/wordlists" "golang.org/x/crypto/pbkdf2" ) var ( // Some bitwise operands for working with big.Ints last11BitsMask = big.NewInt(2047) rightShift11BitsDivider = big.NewInt(2048) bigOne = big.NewInt(1) bigTwo = big.NewInt(2) // wordList is the set of words to use wordList []string // wordMap is a reverse lookup map for wordList wordMap map[string]int ) var ( // ErrInvalidMnemonic is returned when trying to use a malformed mnemonic. ErrInvalidMnemonic = errors.New("Invalid menomic") // 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") // ErrValidatedSeedLengthMismatch is returned when a validated seed is not the // same size as the given seed. This should never happen is present only as a // sanity assertion. ErrValidatedSeedLengthMismatch = errors.New("Seed length does not match validated seed length") // ErrChecksumIncorrect is returned when entropy has the incorrect checksum. ErrChecksumIncorrect = errors.New("Checksum incorrect") ) func init() { SetWordList(wordlists.English) } // SetWordList sets the list of words to use for mnemonics. Currently the list // that is set is used package-wide. func SetWordList(list []string) { wordList = list wordMap = map[string]int{} for i, v := range wordList { wordMap[v] = i } } // NewEntropy will create random entropy bytes // so long as the requested size bitSize is an appropriate size. 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 } // 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 err := validateEntropyBitSize(entropyBitLength) if err != nil { return "", err } // 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, rightShift11BitsDivider) // 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 } // MnemonicToByteArray takes a mnemonic string and turns it into a byte array // suitable for creating another mnemonic. // An error is returned if the mnemonic is invalid. func MnemonicToByteArray(mnemonic string) ([]byte, error) { var ( mnemonicSlice = strings.Split(mnemonic, " ") entropyBitSize = len(mnemonicSlice) * 11 checksumBitSize = entropyBitSize % 32 fullByteSize = (entropyBitSize-checksumBitSize)/8 + 1 checksumByteSize = fullByteSize - (fullByteSize % 4) ) // Pre validate that the mnemonic is well formed and only contains words that // are present in the word list if !IsMnemonicValid(mnemonic) { return nil, ErrInvalidMnemonic } // Convert word indices to a `big.Int` representing the entropy checksummedEntropy := big.NewInt(0) modulo := big.NewInt(2048) for _, v := range mnemonicSlice { index := big.NewInt(int64(wordMap[v])) checksummedEntropy.Mul(checksummedEntropy, modulo) checksummedEntropy.Add(checksummedEntropy, index) } // Calculate the unchecksummed entropy so we can validate that the checksum is // correct checksumModulo := big.NewInt(0).Exp(bigTwo, big.NewInt(int64(checksumBitSize)), nil) rawEntropy := big.NewInt(0).Div(checksummedEntropy, checksumModulo) // Convert `big.Int`s to byte padded byte slices rawEntropyBytes := padByteSlice(rawEntropy.Bytes(), checksumByteSize) checksummedEntropyBytes := padByteSlice(checksummedEntropy.Bytes(), fullByteSize) // Validate that the checksum is correct newChecksummedEntropyBytes := padByteSlice(addChecksum(rawEntropyBytes), fullByteSize) if !compareByteSlices(checksummedEntropyBytes, newChecksummedEntropyBytes) { return nil, ErrChecksumIncorrect } return checksummedEntropyBytes, nil } // NewSeedWithErrorChecking creates a hashed seed output given the mnemonic string and a password. // An error is returned if the mnemonic is not convertible to a byte array. func NewSeedWithErrorChecking(mnemonic string, password string) ([]byte, error) { _, err := MnemonicToByteArray(mnemonic) if err != nil { return nil, err } return NewSeed(mnemonic, password), 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 { // Create a list of all the words in the mnemonic sentence words := strings.Fields(mnemonic) // Get word count wordCount := len(words) // The number of words should be 12, 15, 18, 21 or 24 if wordCount%3 != 0 || wordCount < 12 || wordCount > 24 { return false } // Check if all words belong in the wordlist for _, word := range words { if _, ok := wordMap[word]; !ok { return false } } return true } // 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 { // Get first byte of sha256 hasher := sha256.New() hasher.Write(data) hash := hasher.Sum(nil) 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 := uint(0); i < checksumBitLength; i++ { // Bitshift 1 left dataBigInt.Mul(dataBigInt, bigTwo) // Set rightmost bit if leftmost checksum bit is set if uint8(firstChecksumByte&(1<<(7-i))) > 0 { dataBigInt.Or(dataBigInt, bigOne) } } return dataBigInt.Bytes() } // 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 { if len(slice) >= length { return slice } newSlice := make([]byte, length-len(slice)) return append(newSlice, slice...) } // compareByteSlices returns true of the byte slices have equal contents and // returns false otherwise. func compareByteSlices(a, b []byte) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true }