package bip39 import ( "crypto/rand" "crypto/sha256" "crypto/sha512" "encoding/binary" "errors" "fmt" "math/big" "strings" "golang.org/x/crypto/pbkdf2" ) var ( // Some bitwise operands for working with big.Ints. last11BitsMask = big.NewInt(2047) shift11BitsMask = big.NewInt(2048) bigOne = big.NewInt(1) bigTwo = big.NewInt(2) // wordLengthChecksumMasksMapping is 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), } // wordLengthChecksumShiftMapping is used to lookup the number of operand // for shifting bits to handle checksums. wordLengthChecksumShiftMapping = map[int]*big.Int{ 12: big.NewInt(16), 15: big.NewInt(8), 18: big.NewInt(4), 21: 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 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") // 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(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 } } // GetWordList gets the list of words to use for mnemonics. func GetWordList() []string { return wordList } // GetWordIndex gets word index in wordMap. func GetWordIndex(word string) (int, bool) { idx, ok := wordMap[word] return idx, ok } // 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) { if err := validateEntropyBitSize(bitSize); err != nil { return nil, err } entropy := make([]byte, bitSize/8) _, _ = rand.Read(entropy) // err is always nil return entropy, nil } // 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 } // Decode the words into a big.Int. var ( wordBytes [2]byte b = big.NewInt(0) ) for _, v := range mnemonicSlice { index, found := wordMap[v] if !found { return nil, fmt.Errorf("word `%v` not found in reverse map", v) } binary.BigEndian.PutUint16(wordBytes[:], uint16(index)) b.Mul(b, shift11BitsMask) 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 } // 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 } // 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, raw ...bool) ([]byte, error) { var ( mnemonicSlice = strings.Split(mnemonic, " ") entropyBitSize = len(mnemonicSlice) * 11 checksumBitSize = entropyBitSize % 32 fullByteSize = (entropyBitSize-checksumBitSize)/8 + 1 ) // Turn into raw entropy. rawEntropyBytes, err := EntropyFromMnemonic(mnemonic) if err != nil { return nil, err } // If we want the raw entropy then we're done. if len(raw) > 0 && raw[0] { return rawEntropyBytes, nil } // Otherwise add the checksum before returning return padByteSlice(addChecksum(rawEntropyBytes), fullByteSize), 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 { _, 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 { // 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 := uint(0); i < checksumBitLength; i++ { // 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) // This error is guaranteed to be nil 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 } // 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 } 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 }