From 49e0ae3375b10d02cef5bfd19f401e2f689e8b5b Mon Sep 17 00:00:00 2001 From: yahtoo Date: Thu, 28 Dec 2017 10:19:38 +0800 Subject: [PATCH] Add xprv export and import function (#218) * Save hd wallet path index to db * Add xprv export and import function * Optimized variable name definition --- blockchain/account/accounts.go | 37 ++++++++------- blockchain/pseudohsm/pseudohsm.go | 30 +++++++++++- blockchain/rpc_reactor.go | 4 +- blockchain/wallet.go | 31 +++++++++++++ blockchain/wallet/wallet.go | 97 ++++++++++++++++++++++++++++++++++++--- cmd/cobra/commands/bytomcli.go | 2 + cmd/cobra/commands/key.go | 90 ++++++++++++++++++++++++++++++++++++ node/node.go | 2 +- 8 files changed, 266 insertions(+), 27 deletions(-) create mode 100644 blockchain/wallet.go diff --git a/blockchain/account/accounts.go b/blockchain/account/accounts.go index a4322a1c..33c5098f 100755 --- a/blockchain/account/accounts.go +++ b/blockchain/account/accounts.go @@ -3,6 +3,7 @@ package account import ( "context" + "encoding/binary" "encoding/json" "fmt" "sync" @@ -29,6 +30,7 @@ const ( aliasPrefix = "ALI:" accountPrefix = "ACC:" accountCPPrefix = "ACP:" + keyNextIndex = "NextIndex" ) // pre-define errors for supporting bytom errorFormatter @@ -53,13 +55,18 @@ func CPKey(hash common.Hash) []byte { // NewManager creates a new account manager func NewManager(walletDB dbm.DB, chain *protocol.Chain) *Manager { + var nextIndex uint64 + if index := walletDB.Get([]byte(keyNextIndex)); index != nil { + nextIndex = uint64(binary.LittleEndian.Uint64(index)) + } return &Manager{ - db: walletDB, - chain: chain, - utxoDB: newReserver(chain, walletDB), - cache: lru.New(maxAccountCache), - aliasCache: lru.New(maxAccountCache), - delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram), + db: walletDB, + chain: chain, + utxoDB: newReserver(chain, walletDB), + cache: lru.New(maxAccountCache), + aliasCache: lru.New(maxAccountCache), + delayedACPs: make(map[*txbuilder.TemplateBuilder][]*CtrlProgram), + acpIndexNext: nextIndex, } } @@ -382,23 +389,19 @@ func (m *Manager) GetCoinbaseControlProgram(height uint64) ([]byte, error) { return script, nil } +func saveIndex(db dbm.DB, index uint64) { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, index) + db.Set([]byte(keyNextIndex), buf) +} + func (m *Manager) nextIndex(ctx context.Context) (uint64, error) { m.acpMu.Lock() defer m.acpMu.Unlock() - //TODO: fix this part, really serious security breach - if m.acpIndexNext >= m.acpIndexCap { - const incrby = 10000 // start 1,increments by 10,000 - if m.acpIndexCap <= incrby { - m.acpIndexCap = incrby + 1 - } else { - m.acpIndexCap += incrby - } - m.acpIndexNext = m.acpIndexCap - incrby - } - n := m.acpIndexNext m.acpIndexNext++ + saveIndex(m.db, m.acpIndexNext) return n, nil } diff --git a/blockchain/pseudohsm/pseudohsm.go b/blockchain/pseudohsm/pseudohsm.go index 96cba3d3..17f3d52f 100644 --- a/blockchain/pseudohsm/pseudohsm.go +++ b/blockchain/pseudohsm/pseudohsm.go @@ -15,6 +15,7 @@ import ( // listKeyMaxAliases limits the alias filter to a sane maximum size. const listKeyMaxAliases = 200 +// pre-define errors for supporting bytom errorFormatter var ( ErrDuplicateKeyAlias = errors.New("duplicate key alias") ErrInvalidAfter = errors.New("invalid after") @@ -117,7 +118,7 @@ func (h *HSM) ListKeys(after string, limit int) ([]XPub, string, error) { // xprv with the given path (but does not store the new xprv), and // signs the given msg. func (h *HSM) XSign(xpub chainkd.XPub, path [][]byte, msg []byte, auth string) ([]byte, error) { - xprv, err := h.loadChainKDKey(xpub, auth) + xprv, err := h.LoadChainKDKey(xpub, auth) if err != nil { return nil, err } @@ -127,7 +128,8 @@ func (h *HSM) XSign(xpub chainkd.XPub, path [][]byte, msg []byte, auth string) ( return xprv.Sign(msg), nil } -func (h *HSM) loadChainKDKey(xpub chainkd.XPub, auth string) (xprv chainkd.XPrv, err error) { +//LoadChainKDKey get xprv from xpub +func (h *HSM) LoadChainKDKey(xpub chainkd.XPub, auth string) (xprv chainkd.XPrv, err error) { h.cacheMu.Lock() defer h.cacheMu.Unlock() @@ -193,3 +195,27 @@ func (h *HSM) ResetPassword(xpub chainkd.XPub, auth, newAuth string) error { } return h.keyStore.StoreKey(xpb.File, xkey, newAuth) } + +//ImportXPrvKey import XPrv to chainkd +func (h *HSM) ImportXPrvKey(auth string, alias string, xprv chainkd.XPrv) (*XPub, bool, error) { + if ok := h.cache.hasAlias(alias); ok { + return nil, false, ErrDuplicateKeyAlias + } + + xpub := xprv.XPub() + id := uuid.NewRandom() + key := &XKey{ + ID: id, + KeyType: "bytom_kd", + XPub: xpub, + XPrv: xprv, + Alias: alias, + } + file := h.keyStore.JoinPath(keyFileName(key.ID.String())) + if err := h.keyStore.StoreKey(file, key, auth); err != nil { + return nil, false, errors.Wrap(err, "storing keys") + } + + h.cache.add(XPub{XPub: xpub, Alias: alias, File: file}) + return &XPub{XPub: xpub, Alias: alias, File: file}, true, nil +} diff --git a/blockchain/rpc_reactor.go b/blockchain/rpc_reactor.go index 6bf7205f..d100f3f7 100644 --- a/blockchain/rpc_reactor.go +++ b/blockchain/rpc_reactor.go @@ -29,7 +29,7 @@ func (bcr *BlockchainReactor) ServeHTTP(rw http.ResponseWriter, req *http.Reques bcr.handler.ServeHTTP(rw, req) } -// BuildHander is json rpc handler +//BuildHander build json rpc handler func (bcr *BlockchainReactor) BuildHander() { m := bcr.mux if bcr.accounts != nil && bcr.assets != nil { @@ -65,6 +65,8 @@ func (bcr *BlockchainReactor) BuildHander() { //hsm api m.Handle("/create-key", jsonHandler(bcr.pseudohsmCreateKey)) + m.Handle("/export-private-key", jsonHandler(bcr.walletExportKey)) + m.Handle("/import-private-key", jsonHandler(bcr.walletImportKey)) m.Handle("/list-keys", jsonHandler(bcr.pseudohsmListKeys)) m.Handle("/delete-key", jsonHandler(bcr.pseudohsmDeleteKey)) m.Handle("/sign-transactions", jsonHandler(bcr.pseudohsmSignTemplates)) diff --git a/blockchain/wallet.go b/blockchain/wallet.go new file mode 100644 index 00000000..181a5f27 --- /dev/null +++ b/blockchain/wallet.go @@ -0,0 +1,31 @@ +package blockchain + +import ( + "context" + + "github.com/bytom/crypto/ed25519/chainkd" +) + +func (bcr *BlockchainReactor) walletExportKey(ctx context.Context, in struct { + Password string + XPub chainkd.XPub +}) interface{} { + key, err := bcr.wallet.ExportAccountPrivKey(bcr.hsm, in.XPub, in.Password) + if err != nil { + return err.Error() + } + return key +} + +func (bcr *BlockchainReactor) walletImportKey(ctx context.Context, in struct { + Alias string + Password string + XPrv chainkd.XPrv + Index uint64 +}) interface{} { + xpub, err := bcr.wallet.ImportAccountPrivKey(bcr.hsm, in.XPrv, in.Alias, in.Password, in.Index) + if err != nil { + return err.Error() + } + return xpub +} diff --git a/blockchain/wallet/wallet.go b/blockchain/wallet/wallet.go index 8f9612e3..8066ec88 100755 --- a/blockchain/wallet/wallet.go +++ b/blockchain/wallet/wallet.go @@ -2,15 +2,25 @@ package wallet import ( "encoding/json" + "time" + "github.com/btcsuite/btcutil/base58" log "github.com/sirupsen/logrus" "github.com/tendermint/tmlibs/db" + "github.com/bytom/blockchain/account" + "github.com/bytom/blockchain/asset" + "github.com/bytom/blockchain/pseudohsm" + "github.com/bytom/crypto/ed25519/chainkd" + "github.com/bytom/crypto/sha3pool" "github.com/bytom/protocol" "github.com/bytom/protocol/bc" "github.com/bytom/protocol/bc/legacy" ) +//SINGLE single sign +const SINGLE = 1 + var walletKey = []byte("walletInfo") //StatusInfo is base valid block info to handle orphan block rollback @@ -21,14 +31,21 @@ type StatusInfo struct { //Wallet is related to storing account unspent outputs type Wallet struct { - DB db.DB - status StatusInfo + DB db.DB + status StatusInfo + AccountMgr *account.Manager + AssetReg *asset.Registry + chain *protocol.Chain + rescanProgress chan bool } //NewWallet return a new wallet instance -func NewWallet(walletDB db.DB) *Wallet { +func NewWallet(walletDB db.DB, account *account.Manager, asset *asset.Registry, chain *protocol.Chain) *Wallet { w := &Wallet{ - DB: walletDB, + DB: walletDB, + AccountMgr: account, + AssetReg: asset, + chain: chain, } walletInfo, err := w.GetWalletInfo() if err != nil { @@ -36,6 +53,8 @@ func NewWallet(walletDB db.DB) *Wallet { } w.status.Height = walletInfo.Height w.status.Hash = walletInfo.Hash + rescanProgress := make(chan bool) + w.rescanProgress = rescanProgress return w } @@ -83,7 +102,7 @@ func (w *Wallet) WalletUpdate(c *protocol.Chain) { storeBatch := w.DB.NewBatch() LOOP: - + getRescanNotification(w) for !c.InMainChain(w.status.Height, w.status.Hash) { if block, err = c.GetBlockByHash(&w.status.Hash); err != nil { log.WithField("err", err).Error("get block by hash") @@ -119,7 +138,6 @@ LOOP: //next loop will save w.status.Height = block.Height w.status.Hash = block.Hash() - indexTransactions(&storeBatch, block, w) buildAccountUTXOs(&storeBatch, block, w) @@ -132,3 +150,70 @@ LOOP: //goto next loop goto LOOP } + +func getRescanNotification(w *Wallet) { + select { + case <-w.rescanProgress: + w.status.Height = 1 + block, _ := w.chain.GetBlockByHeight(w.status.Height) + w.status.Hash = block.Hash() + default: + return + } +} + +// ExportAccountPrivKey exports the account private key as a WIF for encoding as a string +// in the Wallet Import Formt. +func (w *Wallet) ExportAccountPrivKey(hsm *pseudohsm.HSM, xpub chainkd.XPub, auth string) (*string, error) { + xprv, err := hsm.LoadChainKDKey(xpub, auth) + if err != nil { + return nil, err + } + var hashed [32]byte + sha3pool.Sum256(hashed[:], xprv[:]) + + tmp := append(xprv[:], hashed[:4]...) + res := base58.Encode(tmp) + return &res, nil +} + +// ImportAccountPrivKey imports the account key in the Wallet Import Formt. +func (w *Wallet) ImportAccountPrivKey(hsm *pseudohsm.HSM, xprv chainkd.XPrv, alias, auth string, index uint64) (*pseudohsm.XPub, error) { + xpub, _, err := hsm.ImportXPrvKey(auth, alias, xprv) + if err != nil { + return nil, err + } + var xpubs []chainkd.XPub + xpubs = append(xpubs, xpub.XPub) + account, err := w.AccountMgr.Create(nil, xpubs, SINGLE, alias, nil, "") + if err != nil { + return nil, err + } + if err := recoveryAccountWalletDB(w, account, &w.DB, xpub, index); err != nil { + return nil, err + } + return xpub, nil +} + +func recoveryAccountWalletDB(w *Wallet, account *account.Account, DB *db.DB, XPub *pseudohsm.XPub, index uint64) error { + if err := createProgram(w, account, DB, XPub, index); err != nil { + return err + } + rescanBlocks(w) + return nil +} + +func createProgram(w *Wallet, account *account.Account, DB *db.DB, XPub *pseudohsm.XPub, index uint64) error { + for i := uint64(0); i < index; i++ { + _, err := w.AccountMgr.CreateControlProgram(nil, account.ID, true, time.Now()) + if err != nil { + return err + } + } + return nil +} + +//WalletUpdate process every valid block and reverse every invalid block which need to rollback +func rescanBlocks(w *Wallet) { + w.rescanProgress <- true +} diff --git a/cmd/cobra/commands/bytomcli.go b/cmd/cobra/commands/bytomcli.go index 2a7c5f89..302eea0b 100644 --- a/cmd/cobra/commands/bytomcli.go +++ b/cmd/cobra/commands/bytomcli.go @@ -104,6 +104,8 @@ func AddCommands() { BytomcliCmd.AddCommand(createKeyCmd) BytomcliCmd.AddCommand(deleteKeyCmd) BytomcliCmd.AddCommand(listKeysCmd) + BytomcliCmd.AddCommand(exportPrivateCmd) + BytomcliCmd.AddCommand(importPrivateCmd) BytomcliCmd.AddCommand(isMiningCmd) diff --git a/cmd/cobra/commands/key.go b/cmd/cobra/commands/key.go index 2e47a92c..c3ac4a7b 100644 --- a/cmd/cobra/commands/key.go +++ b/cmd/cobra/commands/key.go @@ -1,14 +1,17 @@ package commands import ( + "bytes" "context" "encoding/hex" "strconv" + "github.com/btcsuite/btcutil/base58" "github.com/spf13/cobra" jww "github.com/spf13/jwalterweatherman" "github.com/bytom/crypto/ed25519/chainkd" + "github.com/bytom/crypto/sha3pool" ) var createKeyCmd = &cobra.Command{ @@ -94,3 +97,90 @@ var listKeysCmd = &cobra.Command{ } }, } + +var exportPrivateCmd = &cobra.Command{ + Use: "export-private-key", + Short: "export the private key", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 2 { + jww.ERROR.Println("error: export-private-key args not vaild export-private-key password xpub") + return + } + + type Key struct { + Password string + XPub chainkd.XPub + } + var key Key + xpub := new(chainkd.XPub) + data, err := hex.DecodeString(args[1]) + if err != nil { + jww.ERROR.Println("error: export-private-key args not vaild", err) + } + copy(xpub[:], data) + + key.Password = args[0] + key.XPub = *xpub + + var response interface{} + + client := mustRPCClient() + client.Call(context.Background(), "/export-private-key", &key, &response) + res := base58.Decode(response.(string)) + if len(res) != 68 { + jww.ERROR.Println("export private error") + return + } + var hashed [32]byte + sha3pool.Sum256(hashed[:], res[:64]) + rest := bytes.Compare(hashed[:4], res[64:]) + if rest == 0 { + jww.FEEDBACK.Printf("priv: %v\n", response) + return + } + jww.ERROR.Println("export private error") + return + }, +} + +var importPrivateCmd = &cobra.Command{ + Use: "import-private-key", + Short: "import the private key", + Run: func(cmd *cobra.Command, args []string) { + if len(args) != 4 { + jww.ERROR.Println("error: import-private-key args not vaild import-private-key Alias Password Priv Index") + return + } + + type Key struct { + Alias string + Password string + XPrv chainkd.XPrv + Index uint64 + } + + privhash := base58.Decode(args[2]) + if len(privhash) != 68 { + jww.ERROR.Println("wif priv length error") + return + } + var hashed [32]byte + + sha3pool.Sum256(hashed[:], privhash[:64]) + + if res := bytes.Compare(hashed[:4], privhash[64:]); res != 0 { + jww.ERROR.Println("wif priv hash error") + } + var key Key + key.Alias = args[0] + key.Password = args[1] + key.Index, _ = strconv.ParseUint(args[3], 10, 64) + copy(key.XPrv[:], privhash[:64]) + + var response interface{} + + client := mustRPCClient() + client.Call(context.Background(), "/import-private-key", &key, &response) + jww.FEEDBACK.Printf("%v\n", response) + }, +} diff --git a/node/node.go b/node/node.go index 91045ca5..45842d42 100755 --- a/node/node.go +++ b/node/node.go @@ -205,7 +205,7 @@ func NewNode(config *cfg.Config) *Node { accounts = account.NewManager(walletDB, chain) assets = asset.NewRegistry(walletDB, chain) - wallet = w.NewWallet(walletDB) + wallet = w.NewWallet(walletDB, accounts, assets, chain) go wallet.WalletUpdate(chain) -- 2.11.0