OSDN Git Service

Add xprv export and import function (#218)
authoryahtoo <yahtoo.ma@gmail.com>
Thu, 28 Dec 2017 02:19:38 +0000 (10:19 +0800)
committerPaladz <yzhu101@uottawa.ca>
Thu, 28 Dec 2017 02:19:38 +0000 (10:19 +0800)
* Save hd wallet path index to db

* Add xprv export and import function

* Optimized variable name definition

blockchain/account/accounts.go
blockchain/pseudohsm/pseudohsm.go
blockchain/rpc_reactor.go
blockchain/wallet.go [new file with mode: 0644]
blockchain/wallet/wallet.go
cmd/cobra/commands/bytomcli.go
cmd/cobra/commands/key.go
node/node.go

index a4322a1..33c5098 100755 (executable)
@@ -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
 }
 
index 96cba3d..17f3d52 100644 (file)
@@ -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
+}
index 6bf7205..d100f3f 100644 (file)
@@ -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 (file)
index 0000000..181a5f2
--- /dev/null
@@ -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
+}
index 8f9612e..8066ec8 100755 (executable)
@@ -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
+}
index 2a7c5f8..302eea0 100644 (file)
@@ -104,6 +104,8 @@ func AddCommands() {
        BytomcliCmd.AddCommand(createKeyCmd)
        BytomcliCmd.AddCommand(deleteKeyCmd)
        BytomcliCmd.AddCommand(listKeysCmd)
+       BytomcliCmd.AddCommand(exportPrivateCmd)
+       BytomcliCmd.AddCommand(importPrivateCmd)
 
        BytomcliCmd.AddCommand(isMiningCmd)
 
index 2e47a92..c3ac4a7 100644 (file)
@@ -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)
+       },
+}
index 91045ca..45842d4 100755 (executable)
@@ -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)