Add BIP39 key derivation

pull/17/head
Oliver Gugger 4 years ago
parent beb99948db
commit eac127d4ef
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -0,0 +1,137 @@
// Package bip39 is the Golang implementation of the BIP39 spec.
// This code was copied from https://github.com/tyler-smith/go-bip39 which is
// also MIT licensed.
//
// The official BIP39 spec can be found at
// https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
package bip39
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"math/big"
"strings"
)
var (
// 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),
}
)
var (
// ErrInvalidMnemonic is returned when trying to use a malformed mnemonic.
ErrInvalidMnemonic = errors.New("invalid mnenomic")
// ErrChecksumIncorrect is returned when entropy has the incorrect checksum.
ErrChecksumIncorrect = errors.New("checksum incorrect")
)
// 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
}
wordMap := make(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 == false {
return nil, fmt.Errorf("word `%v` not found in " +
"reverse map", v)
}
var wordBytes [2]byte
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
}
func computeChecksum(data []byte) []byte {
hasher := sha256.New()
hasher.Write(data)
return hasher.Sum(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
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,112 @@
package btc
import (
"bufio"
"crypto/sha512"
"encoding/hex"
"errors"
"fmt"
"os"
"strings"
"syscall"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/bip39"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/ssh/terminal"
)
func ReadMnemonicFromTerminal(params *chaincfg.Params) (*hdkeychain.ExtendedKey,
error) {
// We'll now prompt the user to enter in their 12 to 24 word mnemonic.
fmt.Printf("Input your 12 to 24 word mnemonic separated by spaces: ")
reader := bufio.NewReader(os.Stdin)
mnemonicStr, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
fmt.Println()
// We'll trim off extra spaces, and ensure the mnemonic is all
// lower case, then populate our request.
mnemonicStr = strings.TrimSpace(mnemonicStr)
mnemonicStr = strings.ToLower(mnemonicStr)
mnemonicWords := strings.Split(mnemonicStr, " ")
if len(mnemonicWords) < 12 || len(mnemonicWords) > 24 {
return nil, errors.New("wrong cipher seed mnemonic length: " +
"must be between 12 and 24 words")
}
// Additionally, the user may have a passphrase, that will also
// need to be provided so the daemon can properly decipher the
// cipher seed.
fmt.Printf("Input your cipher seed passphrase (press enter if " +
"your seed doesn't have a passphrase): ")
passphrase, err := terminal.ReadPassword(int(syscall.Stdin)) // nolint
if err != nil {
return nil, err
}
fmt.Println()
// Check that the mnemonic is valid.
_, err = bip39.EntropyFromMnemonic(mnemonicStr)
if err != nil {
return nil, err
}
var seed []byte
fmt.Printf("Please choose passphrase mode:\n" +
" 0 - Default BIP39\n" +
" 1 - Passphrase to hex\n" +
" 2 - Digital Bitbox (extra round of PBKDF2)\n" +
"\n" +
"Choice [default 0]: ")
choice, err := reader.ReadString('\n')
if err != nil {
return nil, err
}
fmt.Println()
switch strings.TrimSpace(choice) {
case "", "0":
seed = pbkdf2.Key(
[]byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase...,
), 2048, 64, sha512.New,
)
case "1":
passphrase = []byte(hex.EncodeToString(passphrase))
seed = pbkdf2.Key(
[]byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase...,
), 2048, 64, sha512.New,
)
case "2":
passphrase = pbkdf2.Key(
passphrase, []byte("Digital Bitbox"), 20480, 64,
sha512.New,
)
passphrase = []byte(hex.EncodeToString(passphrase))
seed = pbkdf2.Key(
[]byte(mnemonicStr), append(
[]byte("mnemonic"), passphrase...,
), 2048, 64, sha512.New,
)
default:
return nil, fmt.Errorf("invalid mode selected: %v",
choice)
}
rootKey, err := hdkeychain.NewMaster(seed, params)
if err != nil {
return nil, fmt.Errorf("failed to derive master extended "+
"key: %v", err)
}
return rootKey, nil
}

@ -2,12 +2,15 @@ package main
import (
"fmt"
"github.com/btcsuite/btcutil"
"github.com/guggero/chantools/btc"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/lnd"
)
type deriveKeyCommand struct {
BIP39 bool `long:"bip39" description:"Read a classic BIP39 seed and passphrase from the terminal instead of asking for the lnd seed format or providing the --rootkey flag."`
RootKey string `long:"rootkey" description:"BIP32 HD root key to derive the key from."`
Path string `long:"path" description:"The BIP32 derivation path to derive. Must start with \"m/\"."`
Neuter bool `long:"neuter" description:"Do not output the private key, just the public key."`
@ -21,8 +24,11 @@ func (c *deriveKeyCommand) Execute(_ []string) error {
err error
)
// Check that root key is valid or fall back to console input.
// Check that root key is valid or fall back to terminal input.
switch {
case c.BIP39:
extendedKey, err = btc.ReadMnemonicFromTerminal(chainParams)
case c.RootKey != "":
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
@ -48,11 +54,26 @@ func deriveKey(extendedKey *hdkeychain.ExtendedKey, path string,
if err != nil {
return fmt.Errorf("could not neuter child key: %v", err)
}
fmt.Printf("Public key: %x\n", pubKey.SerializeCompressed())
fmt.Printf("\nPublic key: %x\n", pubKey.SerializeCompressed())
fmt.Printf("Extended public key (xpub): %s\n", neutered.String())
// Print the address too.
hash160 := btcutil.Hash160(pubKey.SerializeCompressed())
addrP2PKH, err := btcutil.NewAddressPubKeyHash(hash160, chainParams)
if err != nil {
return fmt.Errorf("could not create address: %v", err)
}
addrP2WKH, err := btcutil.NewAddressWitnessPubKeyHash(
hash160, chainParams,
)
if err != nil {
return fmt.Errorf("could not create address: %v", err)
}
fmt.Printf("Address: %s\n", addrP2WKH)
fmt.Printf("Legacy address: %s\n", addrP2PKH)
if !neuter {
fmt.Printf("Private key (WIF): %s\n", wif.String())
fmt.Printf("\nPrivate key (WIF): %s\n", wif.String())
fmt.Printf("Extended private key (xprv): %s\n", child.String())
}

@ -3,19 +3,35 @@ package main
import (
"fmt"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
)
type showRootKeyCommand struct{}
type showRootKeyCommand struct {
BIP39 bool `long:"bip39" description:"Read a classic BIP39 seed and passphrase from the terminal instead of asking for the lnd seed format or providing the --rootkey flag."`
}
func (c *showRootKeyCommand) Execute(_ []string) error {
setupChainParams(cfg)
rootKey, _, err := lnd.ReadAezeedFromTerminal(chainParams)
var (
extendedKey *hdkeychain.ExtendedKey
err error
)
// Check that root key is valid or fall back to terminal input.
switch {
case c.BIP39:
extendedKey, err = btc.ReadMnemonicFromTerminal(chainParams)
default:
extendedKey, _, err = lnd.ReadAezeedFromTerminal(chainParams)
}
if err != nil {
return fmt.Errorf("failed to read root key from console: %v",
err)
return fmt.Errorf("error reading root key: %v", err)
}
fmt.Printf("\nYour BIP32 HD root key is: %s\n", rootKey.String())
fmt.Printf("\nYour BIP32 HD root key is: %s\n", extendedKey.String())
return nil
}

@ -15,6 +15,7 @@ require (
github.com/lightningnetwork/lnd v0.8.0-beta-rc3.0.20191224233846-f289a39c1a00
github.com/ltcsuite/ltcd v0.0.0-20191228044241-92166e412499 // indirect
github.com/miekg/dns v1.1.26 // indirect
github.com/prometheus/common v0.4.0
golang.org/x/crypto v0.0.0-20191227163750-53104e6ec876
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 // indirect
golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 // indirect

Loading…
Cancel
Save