From f5ada58780213458b48d89d75a6256d26afc6446 Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Tue, 5 Feb 2019 17:22:56 +0100 Subject: [PATCH] Merge PR #3461: GaiaCLI - refactor/fix --account and --index --- client/input.go | 6 - client/keys/add.go | 349 ++++++++++++++---------------- client/keys/delete.go | 4 +- client/keys/errors.go | 18 +- client/keys/mnemonic.go | 7 +- client/keys/types.go | 38 ++++ client/keys/utils.go | 9 - client/lcd/lcd_test.go | 96 ++++++-- client/lcd/test_helpers.go | 103 ++++----- client/rest/types.go | 59 +++++ cmd/gaia/cli_test/cli_test.go | 19 +- cmd/gaia/cli_test/test_helpers.go | 8 +- crypto/keys/hd/hdpath.go | 40 ++-- crypto/keys/hd/hdpath_test.go | 70 +++++- crypto/keys/keybase.go | 59 ++--- crypto/keys/keybase_test.go | 2 +- crypto/keys/types.go | 34 ++- crypto/ledger_secp256k1.go | 12 +- crypto/ledger_test.go | 10 +- 19 files changed, 547 insertions(+), 396 deletions(-) create mode 100644 client/keys/types.go diff --git a/client/input.go b/client/input.go index 46c838e2e4e4..631e13c69320 100644 --- a/client/input.go +++ b/client/input.go @@ -42,12 +42,6 @@ func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) { return pass, nil } -// GetSeed will request a seed phrase from stdin and trims off -// leading/trailing spaces -func GetSeed(prompt string, buf *bufio.Reader) (string, error) { - return GetString(prompt, buf) -} - // GetCheckPassword will prompt for a password twice to verify they // match (for creating a new password). // It enforces the password length. Only parses password once if diff --git a/client/keys/add.go b/client/keys/add.go index b3af588dc8b4..ca5b58da4267 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -9,26 +9,24 @@ import ( "os" "sort" - "github.com/tendermint/tendermint/crypto/multisig" + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/cmd/gaia/app" + "github.com/cosmos/cosmos-sdk/crypto/keys" + sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/go-bip39" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/multisig" "github.com/tendermint/tendermint/libs/cli" - - "github.com/cosmos/cosmos-sdk/client" - ccrypto "github.com/cosmos/cosmos-sdk/crypto" - "github.com/cosmos/cosmos-sdk/crypto/keys" - "github.com/cosmos/cosmos-sdk/crypto/keys/hd" - sdk "github.com/cosmos/cosmos-sdk/types" ) const ( flagInteractive = "interactive" - flagBIP44Path = "bip44-path" flagRecover = "recover" flagNoBackup = "no-backup" flagDryRun = "dry-run" @@ -38,6 +36,11 @@ const ( flagNoSort = "nosort" ) +const ( + maxValidAccountValue = int(0x80000000 - 1) + maxValidIndexalue = int(0x80000000 - 1) +) + func addKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "add ", @@ -68,7 +71,6 @@ the flag --nosort is set. cmd.Flags().String(FlagPublicKey, "", "Parse a public key in bech32 format and save it to disk") cmd.Flags().BoolP(flagInteractive, "i", false, "Interactively prompt user for BIP39 passphrase and mnemonic") cmd.Flags().Bool(client.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device") - cmd.Flags().String(flagBIP44Path, "44'/118'/0'/0/0", "BIP44 path from which to derive a private key") cmd.Flags().Bool(flagRecover, false, "Provide seed phrase to recover existing key instead of creating") cmd.Flags().Bool(flagNoBackup, false, "Don't print out seed phrase (if others are watching the terminal)") cmd.Flags().Bool(flagDryRun, false, "Perform action, but don't add key to local keystore") @@ -95,24 +97,25 @@ func runAddCmd(cmd *cobra.Command, args []string) error { name := args[0] interactive := viper.GetBool(flagInteractive) + showMnemonic := viper.GetBool(flagNoBackup) if viper.GetBool(flagDryRun) { // we throw this away, so don't enforce args, // we want to get a new random seed phrase quickly kb = client.MockKeyBase() - encryptPassword = "throwing-this-key-away" + encryptPassword = app.DefaultKeyPass } else { kb, err = GetKeyBaseWithWritePerm() if err != nil { return err } - _, err := kb.Get(name) + _, err = kb.Get(name) if err == nil { // account exists, ask for user confirmation - if response, err := client.GetConfirmation( - fmt.Sprintf("override the existing name %s", name), buf); err != nil || !response { - return err + if response, err2 := client.GetConfirmation( + fmt.Sprintf("override the existing name %s", name), buf); err2 != nil || !response { + return err2 } } @@ -144,6 +147,7 @@ func runAddCmd(cmd *cobra.Command, args []string) error { if _, err := kb.CreateOffline(name, pk); err != nil { return err } + fmt.Fprintf(os.Stderr, "Key %q saved to disk.", name) return nil } @@ -164,51 +168,37 @@ func runAddCmd(cmd *cobra.Command, args []string) error { if err != nil { return err } - kb.CreateOffline(name, pk) + _, err = kb.CreateOffline(name, pk) + if err != nil { + return err + } return nil } - bipFlag := cmd.Flags().Lookup(flagBIP44Path) - bip44Params, err := getBIP44ParamsAndPath(bipFlag.Value.String(), bipFlag.Changed || !interactive) - if err != nil { - return err - } + account := uint32(viper.GetInt(flagAccount)) + index := uint32(viper.GetInt(flagIndex)) - // If we're using ledger, only thing we need is the path. So generate key and - // we're done. + // If we're using ledger, only thing we need is the path. So generate key and we're done. if viper.GetBool(client.FlagUseLedger) { - account := uint32(viper.GetInt(flagAccount)) - index := uint32(viper.GetInt(flagIndex)) - path := ccrypto.DerivationPath{44, 118, account, 0, index} - info, err := kb.CreateLedger(name, path, keys.Secp256k1) + info, err := kb.CreateLedger(name, keys.Secp256k1, account, index) if err != nil { return err } - printCreate(info, "") - return nil + return printCreate(info, false, "") } - // Recover key from seed passphrase - if viper.GetBool(flagRecover) { - seed, err := client.GetSeed( - "Enter your recovery seed phrase:", buf) - if err != nil { - return err - } - info, err := kb.CreateKey(name, seed, encryptPassword) - if err != nil { - return err + // Get bip39 mnemonic + var mnemonic string + var bip39Passphrase string + + if interactive || viper.GetBool(flagRecover) { + bip39Message := "Enter your bip39 mnemonic" + if !viper.GetBool(flagRecover) { + bip39Message = "Enter your bip39 mnemonic, or hit enter to generate one." } - // print out results without the seed phrase - viper.Set(flagNoBackup, true) - printCreate(info, "") - return nil - } - var mnemonic string - if interactive { - mnemonic, err = client.GetString("Enter your bip39 mnemonic, or hit enter to generate one.", buf) + mnemonic, err = client.GetString(bip39Message, buf) if err != nil { return err } @@ -227,8 +217,12 @@ func runAddCmd(cmd *cobra.Command, args []string) error { } } - // get bip39 passphrase - var bip39Passphrase string + if !bip39.IsMnemonicValid(mnemonic) { + fmt.Fprintf(os.Stderr, "Error: Mnemonic is not valid") + return nil + } + + // override bip39 passphrase if interactive { bip39Passphrase, err = client.GetString( "Enter your bip39 passphrase. This is combined with the mnemonic to derive the seed. "+ @@ -250,173 +244,158 @@ func runAddCmd(cmd *cobra.Command, args []string) error { } } - info, err := kb.Derive(name, mnemonic, bip39Passphrase, encryptPassword, *bip44Params) + info, err := kb.CreateAccount(name, mnemonic, keys.DefaultBIP39Passphrase, encryptPassword, account, index) if err != nil { return err } - printCreate(info, mnemonic) - return nil -} - -func getBIP44ParamsAndPath(path string, flagSet bool) (*hd.BIP44Params, error) { - buf := client.BufferStdin() - bip44Path := path - - // if it wasn't set in the flag, give it a chance to overide interactively - if !flagSet { - var err error - - bip44Path, err = client.GetString(fmt.Sprintf("Enter your bip44 path. Default is %s\n", path), buf) - if err != nil { - return nil, err - } - - if len(bip44Path) == 0 { - bip44Path = path - } - } - bip44params, err := hd.NewParamsFromPath(bip44Path) - if err != nil { - return nil, err + // Recover key from seed passphrase + if viper.GetBool(flagRecover) { + // Hide mnemonic from output + showMnemonic = false + mnemonic = "" } - return bip44params, nil + return printCreate(info, showMnemonic, mnemonic) } -func printCreate(info keys.Info, seed string) { +func printCreate(info keys.Info, showMnemonic bool, mnemonic string) error { output := viper.Get(cli.OutputFlag) + switch output { case "text": - fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr) printKeyInfo(info, Bech32KeyOutput) - // print seed unless requested not to. - if !viper.GetBool(client.FlagUseLedger) && !viper.GetBool(flagNoBackup) { - fmt.Fprintln(os.Stderr, "\n**Important** write this seed phrase in a safe place.") + // print mnemonic unless requested not to. + if showMnemonic { + fmt.Fprintln(os.Stderr, "\n**Important** write this mnemonic phrase in a safe place.") fmt.Fprintln(os.Stderr, "It is the only way to recover your account if you ever forget your password.") - fmt.Fprintln(os.Stderr) - fmt.Fprintln(os.Stderr, seed) + fmt.Fprintln(os.Stderr, "") + fmt.Fprintln(os.Stderr, mnemonic) } case "json": out, err := Bech32KeyOutput(info) if err != nil { - panic(err) + return err } - if !viper.GetBool(flagNoBackup) { - out.Seed = seed + + if showMnemonic { + out.Mnemonic = mnemonic } + var jsonString []byte if viper.GetBool(client.FlagIndentResponse) { jsonString, err = cdc.MarshalJSONIndent(out, "", " ") } else { jsonString, err = cdc.MarshalJSON(out) } + if err != nil { - panic(err) // really shouldn't happen... + return err } fmt.Fprintln(os.Stderr, string(jsonString)) default: - panic(fmt.Sprintf("I can't speak: %s", output)) + return errors.Errorf("I can't speak: %s", output) } + + return nil } -// function to just a new seed to display in the UI before actually persisting it in the keybase -func getSeed(algo keys.SigningAlgo) string { +///////////////////////////// +// REST + +// function to just create a new seed to display in the UI before actually persisting it in the keybase +func generateMnemonic(algo keys.SigningAlgo) string { kb := client.MockKeyBase() - pass := "throwing-this-key-away" + pass := app.DefaultKeyPass name := "inmemorykey" _, seed, _ := kb.CreateMnemonic(name, keys.English, pass, algo) return seed } -func printPrefixed(msg string) { - fmt.Fprintln(os.Stderr, msg) -} - -func printStep() { - printPrefixed("-------------------------------------") -} - -///////////////////////////// -// REST - -// new key request REST body -type NewKeyBody struct { - Name string `json:"name"` - Password string `json:"password"` - Seed string `json:"seed"` +// CheckAndWriteErrorResponse will check for errors and return +// a given error message when corresponding +//TODO: Move to utils/rest or similar +func CheckAndWriteErrorResponse(w http.ResponseWriter, httpErr int, err error) bool { + if err != nil { + w.WriteHeader(httpErr) + _, _ = w.Write([]byte(err.Error())) + return true + } + return false } // add new key REST handler func AddNewKeyRequestHandler(indent bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var kb keys.Keybase - var m NewKeyBody + var m AddNewKey kb, err := GetKeyBaseWithWritePerm() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } body, err := ioutil.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + err = json.Unmarshal(body, &m) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + + // Check parameters if m.Name == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingName() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingName()) return } if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingPassword() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingPassword()) return } - // check if already exists - infos, err := kb.List() - for _, info := range infos { - if info.GetName() == m.Name { - w.WriteHeader(http.StatusConflict) - err = errKeyNameConflict(m.Name) - w.Write([]byte(err.Error())) - return - } + mnemonic := m.Mnemonic + // if mnemonic is empty, generate one + if mnemonic == "" { + mnemonic = generateMnemonic(keys.Secp256k1) + } + if !bip39.IsMnemonicValid(mnemonic) { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidMnemonic()) } - // create account - seed := m.Seed - if seed == "" { - seed = getSeed(keys.Secp256k1) + if m.Account < 0 || m.Account > maxValidAccountValue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidAccountNumber()) + return } - info, err := kb.CreateKey(m.Name, seed, m.Password) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + + if m.Index < 0 || m.Index > maxValidIndexalue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidIndexNumber()) + return + } + + _, err = kb.Get(m.Name) + if err == nil { + CheckAndWriteErrorResponse(w, http.StatusConflict, errKeyNameConflict(m.Name)) + return + } + + // create account + account := uint32(m.Account) + index := uint32(m.Index) + info, err := kb.CreateAccount(m.Name, mnemonic, keys.DefaultBIP39Passphrase, m.Password, account, index) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } keyOutput, err := Bech32KeyOutput(info) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } - keyOutput.Seed = seed + keyOutput.Mnemonic = mnemonic PostProcessResponse(w, cdc, keyOutput, indent) } @@ -426,22 +405,17 @@ func AddNewKeyRequestHandler(indent bool) http.HandlerFunc { func SeedRequestHandler(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) algoType := vars["type"] + // algo type defaults to secp256k1 if algoType == "" { algoType = "secp256k1" } - algo := keys.SigningAlgo(algoType) - seed := getSeed(algo) + algo := keys.SigningAlgo(algoType) + seed := generateMnemonic(algo) w.Header().Set("Content-Type", "application/json") - w.Write([]byte(seed)) -} - -// RecoverKeyBody is recover key request REST body -type RecoverKeyBody struct { - Password string `json:"password"` - Seed string `json:"seed"` + _, _ = w.Write([]byte(seed)) } // RecoverRequestHandler performs key recover request @@ -449,67 +423,66 @@ func RecoverRequestHandler(indent bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) name := vars["name"] - var m RecoverKeyBody + var m RecoverKey + body, err := ioutil.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + err = cdc.UnmarshalJSON(body, &m) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusBadRequest, err) { return } + kb, err := GetKeyBaseWithWritePerm() + CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) + if name == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingName() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingName()) return } if m.Password == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingPassword() - w.Write([]byte(err.Error())) + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingPassword()) return } - if m.Seed == "" { - w.WriteHeader(http.StatusBadRequest) - err = errMissingSeed() - w.Write([]byte(err.Error())) + + mnemonic := m.Mnemonic + if !bip39.IsMnemonicValid(mnemonic) { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidMnemonic()) + } + + if m.Mnemonic == "" { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errMissingMnemonic()) return } - kb, err := GetKeyBaseWithWritePerm() - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if m.Account < 0 || m.Account > maxValidAccountValue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidAccountNumber()) return } - // check if already exists - infos, err := kb.List() - for _, info := range infos { - if info.GetName() == name { - w.WriteHeader(http.StatusConflict) - err = errKeyNameConflict(name) - w.Write([]byte(err.Error())) - return - } + + if m.Index < 0 || m.Index > maxValidIndexalue { + CheckAndWriteErrorResponse(w, http.StatusBadRequest, errInvalidIndexNumber()) + return } - info, err := kb.CreateKey(name, m.Seed, m.Password) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + _, err = kb.Get(name) + if err == nil { + CheckAndWriteErrorResponse(w, http.StatusConflict, errKeyNameConflict(name)) + return + } + + account := uint32(m.Account) + index := uint32(m.Index) + + info, err := kb.CreateAccount(name, mnemonic, keys.DefaultBIP39Passphrase, m.Password, account, index) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } keyOutput, err := Bech32KeyOutput(info) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(err.Error())) + if CheckAndWriteErrorResponse(w, http.StatusInternalServerError, err) { return } diff --git a/client/keys/delete.go b/client/keys/delete.go index 5f3ff4f095c4..0f8392270c06 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -13,8 +13,8 @@ import ( "github.com/gorilla/mux" "github.com/cosmos/cosmos-sdk/client" - keys "github.com/cosmos/cosmos-sdk/crypto/keys" - keyerror "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" "github.com/spf13/cobra" ) diff --git a/client/keys/errors.go b/client/keys/errors.go index 9c6139d7a753..a603b1d1a65b 100644 --- a/client/keys/errors.go +++ b/client/keys/errors.go @@ -3,7 +3,7 @@ package keys import "fmt" func errKeyNameConflict(name string) error { - return fmt.Errorf("acount with name %s already exists", name) + return fmt.Errorf("account with name %s already exists", name) } func errMissingName() error { @@ -14,6 +14,18 @@ func errMissingPassword() error { return fmt.Errorf("you have to specify a password for the locally stored account") } -func errMissingSeed() error { - return fmt.Errorf("you have to specify seed for key recover") +func errMissingMnemonic() error { + return fmt.Errorf("you have to specify a mnemonic for key recovery") +} + +func errInvalidMnemonic() error { + return fmt.Errorf("the mnemonic is invalid") +} + +func errInvalidAccountNumber() error { + return fmt.Errorf("the account number is invalid") +} + +func errInvalidIndexNumber() error { + return fmt.Errorf("the index number is invalid") } diff --git a/client/keys/mnemonic.go b/client/keys/mnemonic.go index 32a6856bbdea..ffa72fff4097 100644 --- a/client/keys/mnemonic.go +++ b/client/keys/mnemonic.go @@ -4,11 +4,9 @@ import ( "crypto/sha256" "fmt" - "github.com/spf13/cobra" - - "github.com/cosmos/cosmos-sdk/client" - bip39 "github.com/bartekn/go-bip39" + "github.com/cosmos/cosmos-sdk/client" + "github.com/spf13/cobra" ) const ( @@ -58,7 +56,6 @@ func runMnemonicCmd(cmd *cobra.Command, args []string) error { // hash input entropy to get entropy seed hashedEntropy := sha256.Sum256([]byte(inputEntropy)) entropySeed = hashedEntropy[:] - printStep() } else { // read entropy seed straight from crypto.Rand var err error diff --git a/client/keys/types.go b/client/keys/types.go new file mode 100644 index 000000000000..d4a1032c2d13 --- /dev/null +++ b/client/keys/types.go @@ -0,0 +1,38 @@ +package keys + +// used for outputting keys.Info over REST +type KeyOutput struct { + Name string `json:"name"` + Type string `json:"type"` + Address string `json:"address"` + PubKey string `json:"pub_key"` + Mnemonic string `json:"mnemonic,omitempty"` +} + +// AddNewKey request a new key +type AddNewKey struct { + Name string `json:"name"` + Password string `json:"password"` + Mnemonic string `json:"mnemonic"` + Account int `json:"account,string,omitempty"` + Index int `json:"index,string,omitempty"` +} + +// RecoverKeyBody recovers a key +type RecoverKey struct { + Password string `json:"password"` + Mnemonic string `json:"mnemonic"` + Account int `json:"account,string,omitempty"` + Index int `json:"index,string,omitempty"` +} + +// UpdateKeyReq requests updating a key +type UpdateKeyReq struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +// DeleteKeyReq requests deleting a key +type DeleteKeyReq struct { + Password string `json:"password"` +} diff --git a/client/keys/utils.go b/client/keys/utils.go index 78899f5f8af4..5f9bcfa59795 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -118,15 +118,6 @@ func SetKeyBase(kb keys.Keybase) { keybase = kb } -// used for outputting keys.Info over REST -type KeyOutput struct { - Name string `json:"name"` - Type string `json:"type"` - Address string `json:"address"` - PubKey string `json:"pub_key"` - Seed string `json:"seed,omitempty"` -} - // create a list of KeyOutput in bech32 format func Bech32KeysOutput(infos []keys.Info) ([]KeyOutput, error) { kos := make([]KeyOutput, len(infos)) diff --git a/client/lcd/lcd_test.go b/client/lcd/lcd_test.go index c9842c7ebac8..25860f79e041 100644 --- a/client/lcd/lcd_test.go +++ b/client/lcd/lcd_test.go @@ -12,10 +12,9 @@ import ( "github.com/spf13/viper" "github.com/stretchr/testify/require" - client "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/rest" "github.com/cosmos/cosmos-sdk/cmd/gaia/app" - "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" @@ -47,41 +46,100 @@ func init() { version.Version = os.Getenv("VERSION") } -func TestKeys(t *testing.T) { +func TestSeedsAreDifferent(t *testing.T) { addr, _ := CreateAddr(t, name1, pw, GetKeyBase(t)) cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr}) defer cleanup() - // get new seed - seed := getKeysSeed(t, port) + mnemonic1 := getKeysSeed(t, port) + mnemonic2 := getKeysSeed(t, port) + + require.NotEqual(t, mnemonic1, mnemonic2) +} + +func TestKeyRecover(t *testing.T) { + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + defer cleanup() + + myName1 := "TestKeyRecover_1" + myName2 := "TestKeyRecover_2" + + mnemonic := getKeysSeed(t, port) + expectedInfo, _ := GetKeyBase(t).CreateAccount(myName1, mnemonic, "", pw, 0, 0) + expectedAddress := expectedInfo.GetAddress().String() + expectedPubKey := sdk.MustBech32ifyAccPub(expectedInfo.GetPubKey()) // recover key - doRecoverKey(t, port, name2, pw, seed) + doRecoverKey(t, port, myName2, pw, mnemonic, 0, 0) + + keys := getKeys(t, port) + + require.Equal(t, expectedAddress, keys[0].Address) + require.Equal(t, expectedPubKey, keys[0].PubKey) +} + +func TestKeyRecoverHDPath(t *testing.T) { + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{}) + defer cleanup() + + mnemonic := getKeysSeed(t, port) + + for account := uint32(0); account < 50; account += 13 { + for index := uint32(0); index < 50; index += 15 { + name1Idx := fmt.Sprintf("name1_%d_%d", account, index) + name2Idx := fmt.Sprintf("name2_%d_%d", account, index) + + expectedInfo, _ := GetKeyBase(t).CreateAccount(name1Idx, mnemonic, "", pw, account, index) + expectedAddress := expectedInfo.GetAddress().String() + expectedPubKey := sdk.MustBech32ifyAccPub(expectedInfo.GetPubKey()) + + // recover key + doRecoverKey(t, port, name2Idx, pw, mnemonic, account, index) + + keysName2Idx := getKey(t, port, name2Idx) + + require.Equal(t, expectedAddress, keysName2Idx.Address) + require.Equal(t, expectedPubKey, keysName2Idx.PubKey) + } + } +} + +func TestKeys(t *testing.T) { + addr1, _ := CreateAddr(t, name1, pw, GetKeyBase(t)) + addr1Bech32 := addr1.String() + + cleanup, _, _, port := InitializeTestLCD(t, 1, []sdk.AccAddress{addr1}) + defer cleanup() + + // get new seed & recover key + mnemonic2 := getKeysSeed(t, port) + doRecoverKey(t, port, name2, pw, mnemonic2, 0, 0) // add key - resp := doKeysPost(t, port, name3, pw, seed) + mnemonic3 := mnemonic2 + resp := doKeysPost(t, port, name3, pw, mnemonic3, 0, 0) - addrBech32 := addr.String() - addr2Bech32 := resp.Address - _, err := sdk.AccAddressFromBech32(addr2Bech32) + addr3Bech32 := resp.Address + _, err := sdk.AccAddressFromBech32(addr3Bech32) require.NoError(t, err, "Failed to return a correct bech32 address") // test if created account is the correct account - expectedInfo, _ := GetKeyBase(t).CreateKey(name3, seed, pw) - expectedAccount := sdk.AccAddress(expectedInfo.GetPubKey().Address().Bytes()) - require.Equal(t, expectedAccount.String(), addr2Bech32) + expectedInfo3, _ := GetKeyBase(t).CreateAccount(name3, mnemonic3, "", pw, 0, 0) + expectedAddress3 := sdk.AccAddress(expectedInfo3.GetPubKey().Address()).String() + require.Equal(t, expectedAddress3, addr3Bech32) // existing keys - keys := getKeys(t, port) - require.Equal(t, name1, keys[0].Name, "Did not serve keys name correctly") - require.Equal(t, addrBech32, keys[0].Address, "Did not serve keys Address correctly") - require.Equal(t, name2, keys[1].Name, "Did not serve keys name correctly") - require.Equal(t, addr2Bech32, keys[1].Address, "Did not serve keys Address correctly") + require.Equal(t, name1, getKey(t, port, name1).Name, "Did not serve keys name correctly") + require.Equal(t, addr1Bech32, getKey(t, port, name1).Address, "Did not serve keys Address correctly") + require.Equal(t, name2, getKey(t, port, name2).Name, "Did not serve keys name correctly") + require.Equal(t, addr3Bech32, getKey(t, port, name2).Address, "Did not serve keys Address correctly") + require.Equal(t, name3, getKey(t, port, name3).Name, "Did not serve keys name correctly") + require.Equal(t, addr3Bech32, getKey(t, port, name3).Address, "Did not serve keys Address correctly") // select key key := getKey(t, port, name3) require.Equal(t, name3, key.Name, "Did not serve keys name correctly") - require.Equal(t, addr2Bech32, key.Address, "Did not serve keys Address correctly") + require.Equal(t, addr3Bech32, key.Address, "Did not serve keys Address correctly") // update key updateKey(t, port, name3, pw, altPw, false) diff --git a/client/lcd/test_helpers.go b/client/lcd/test_helpers.go index 58b6319a63ae..2c6223de3b98 100644 --- a/client/lcd/test_helpers.go +++ b/client/lcd/test_helpers.go @@ -15,15 +15,6 @@ import ( "strings" "testing" - "github.com/tendermint/tendermint/crypto/secp256k1" - ctypes "github.com/tendermint/tendermint/rpc/core/types" - - cryptoKeys "github.com/cosmos/cosmos-sdk/crypto/keys" - authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" - "github.com/cosmos/cosmos-sdk/x/gov" - "github.com/cosmos/cosmos-sdk/x/slashing" - stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/keys" "github.com/cosmos/cosmos-sdk/client/rest" @@ -32,12 +23,23 @@ import ( gapp "github.com/cosmos/cosmos-sdk/cmd/gaia/app" "github.com/cosmos/cosmos-sdk/codec" crkeys "github.com/cosmos/cosmos-sdk/crypto/keys" + cryptoKeys "github.com/cosmos/cosmos-sdk/crypto/keys" "github.com/cosmos/cosmos-sdk/server" "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth" + authRest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + authrest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" + txbuilder "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" + bankRest "github.com/cosmos/cosmos-sdk/x/bank/client/rest" + "github.com/cosmos/cosmos-sdk/x/gov" + govRest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" gcutils "github.com/cosmos/cosmos-sdk/x/gov/client/utils" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingRest "github.com/cosmos/cosmos-sdk/x/slashing/client/rest" "github.com/cosmos/cosmos-sdk/x/staking" + stakingRest "github.com/cosmos/cosmos-sdk/x/staking/client/rest" + stakingTypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/spf13/viper" "github.com/stretchr/testify/require" @@ -46,6 +48,7 @@ import ( tmcfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/tendermint/tendermint/libs/cli" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" @@ -53,18 +56,12 @@ import ( "github.com/tendermint/tendermint/p2p" pvm "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" + ctypes "github.com/tendermint/tendermint/rpc/core/types" tmrpc "github.com/tendermint/tendermint/rpc/lib/server" tmtypes "github.com/tendermint/tendermint/types" - txbuilder "github.com/cosmos/cosmos-sdk/x/auth/client/txbuilder" - - authRest "github.com/cosmos/cosmos-sdk/x/auth/client/rest" - bankRest "github.com/cosmos/cosmos-sdk/x/bank/client/rest" distr "github.com/cosmos/cosmos-sdk/x/distribution" distrRest "github.com/cosmos/cosmos-sdk/x/distribution/client/rest" - govRest "github.com/cosmos/cosmos-sdk/x/gov/client/rest" - slashingRest "github.com/cosmos/cosmos-sdk/x/slashing/client/rest" - stakingRest "github.com/cosmos/cosmos-sdk/x/staking/client/rest" ) // makePathname creates a unique pathname for each test. It will panic if it @@ -145,14 +142,6 @@ func CreateAddr(t *testing.T, name, password string, kb crkeys.Keybase) (sdk.Acc return sdk.AccAddress(info.GetPubKey().Address()), seed } -// Type that combines an Address with the pnemonic of the private key to that address -type AddrSeed struct { - Address sdk.AccAddress - Seed string - Name string - Password string -} - // CreateAddr adds multiple address to the key store and returns the addresses and associated seeds in lexographical order by address. // It also requires that the keys could be created. func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.AccAddress, seeds, names, passwords []string) { @@ -169,7 +158,7 @@ func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.Acc password := "1234567890" info, seed, err = kb.CreateMnemonic(name, crkeys.English, password, crkeys.Secp256k1) require.NoError(t, err) - addrSeeds = append(addrSeeds, AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name, Password: password}) + addrSeeds = append(addrSeeds, rest.AddrSeed{Address: sdk.AccAddress(info.GetPubKey().Address()), Seed: seed, Name: name, Password: password}) } sort.Sort(addrSeeds) @@ -184,14 +173,14 @@ func CreateAddrs(t *testing.T, kb crkeys.Keybase, numAddrs int) (addrs []sdk.Acc return addrs, seeds, names, passwords } -// implement `Interface` in sort package. -type AddrSeedSlice []AddrSeed +// AddrSeedSlice implements `Interface` in sort package. +type AddrSeedSlice []rest.AddrSeed func (b AddrSeedSlice) Len() int { return len(b) } -// Sorts lexographically by Address +// Less sorts lexicographically by Address func (b AddrSeedSlice) Less(i, j int) bool { // bytes package already implements Comparable for []byte. switch bytes.Compare(b[i].Address.Bytes(), b[j].Address.Bytes()) { @@ -547,10 +536,11 @@ func getKeys(t *testing.T, port string) []keys.KeyOutput { } // POST /keys Create a new account locally -func doKeysPost(t *testing.T, port, name, password, seed string) keys.KeyOutput { - pk := postKeys{name, password, seed} +func doKeysPost(t *testing.T, port, name, password, mnemonic string, account int, index int) keys.KeyOutput { + pk := keys.AddNewKey{name, password, mnemonic, account, index} req, err := cdc.MarshalJSON(pk) require.NoError(t, err) + res, body := Request(t, port, "POST", "/keys", req) require.Equal(t, http.StatusOK, res.StatusCode, body) @@ -560,12 +550,6 @@ func doKeysPost(t *testing.T, port, name, password, seed string) keys.KeyOutput return resp } -type postKeys struct { - Name string `json:"name"` - Password string `json:"password"` - Seed string `json:"seed"` -} - // GET /keys/seed Create a new seed to create a new account defaultValidFor func getKeysSeed(t *testing.T, port string) string { res, body := Request(t, port, "GET", "/keys/seed", nil) @@ -577,14 +561,17 @@ func getKeysSeed(t *testing.T, port string) string { return body } -// POST /keys/{name}/recover Recover a account from a seed -func doRecoverKey(t *testing.T, port, recoverName, recoverPassword, seed string) { - jsonStr := []byte(fmt.Sprintf(`{"password":"%s", "seed":"%s"}`, recoverPassword, seed)) - res, body := Request(t, port, "POST", fmt.Sprintf("/keys/%s/recover", recoverName), jsonStr) +// POST /keys/{name}/recove Recover a account from a seed +func doRecoverKey(t *testing.T, port, recoverName, recoverPassword, mnemonic string, account uint32, index uint32) { + pk := keys.RecoverKey{recoverPassword, mnemonic, int(account), int(index)} + req, err := cdc.MarshalJSON(pk) + require.NoError(t, err) + + res, body := Request(t, port, "POST", fmt.Sprintf("/keys/%s/recover", recoverName), req) require.Equal(t, http.StatusOK, res.StatusCode, body) var resp keys.KeyOutput - err := codec.Cdc.UnmarshalJSON([]byte(body), &resp) + err = codec.Cdc.UnmarshalJSON([]byte(body), &resp) require.Nil(t, err, body) addr1Bech32 := resp.Address @@ -604,7 +591,7 @@ func getKey(t *testing.T, port, name string) keys.KeyOutput { // PUT /keys/{name} Update the password for this account in the KMS func updateKey(t *testing.T, port, name, oldPassword, newPassword string, fail bool) { - kr := updateKeyReq{oldPassword, newPassword} + kr := keys.UpdateKeyReq{oldPassword, newPassword} req, err := cdc.MarshalJSON(kr) require.NoError(t, err) keyEndpoint := fmt.Sprintf("/keys/%s", name) @@ -616,14 +603,9 @@ func updateKey(t *testing.T, port, name, oldPassword, newPassword string, fail b require.Equal(t, http.StatusOK, res.StatusCode, body) } -type updateKeyReq struct { - OldPassword string `json:"old_password"` - NewPassword string `json:"new_password"` -} - // DELETE /keys/{name} Remove an account func deleteKey(t *testing.T, port, name, password string) { - dk := deleteKeyReq{password} + dk := keys.DeleteKeyReq{password} req, err := cdc.MarshalJSON(dk) require.NoError(t, err) keyEndpoint := fmt.Sprintf("/keys/%s", name) @@ -631,10 +613,6 @@ func deleteKey(t *testing.T, port, name, password string) { require.Equal(t, http.StatusOK, res.StatusCode, body) } -type deleteKeyReq struct { - Password string `json:"password"` -} - // GET /auth/accounts/{address} Get the account information on blockchain func getAccount(t *testing.T, port string, addr sdk.AccAddress) auth.Account { res, body := Request(t, port, "GET", fmt.Sprintf("/auth/accounts/%s", addr.String()), nil) @@ -668,7 +646,7 @@ func doSign(t *testing.T, port, name, password, chainID string, accnum, sequence // POST /tx/broadcast Send a signed Tx func doBroadcast(t *testing.T, port string, msg auth.StdTx) sdk.TxResponse { - tx := broadcastReq{Tx: msg, Return: "block"} + tx := rest.BroadcastReq{Tx: msg, Return: "block"} req, err := cdc.MarshalJSON(tx) require.Nil(t, err) res, body := Request(t, port, "POST", "/tx/broadcast", req) @@ -678,11 +656,6 @@ func doBroadcast(t *testing.T, port string, msg auth.StdTx) sdk.TxResponse { return resultTx } -type broadcastReq struct { - Tx auth.StdTx `json:"tx"` - Return string `json:"return"` -} - // GET /bank/balances/{address} Get the account balances // POST /bank/accounts/{address}/transfers Send coins (build -> sign -> send) @@ -726,7 +699,7 @@ func doTransferWithGas( generateOnly, simulate, ) - sr := sendReq{ + sr := rest.SendReq{ Amount: sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 1)}, BaseReq: baseReq, } @@ -759,7 +732,7 @@ func doTransferWithGasAccAuto( fmt.Sprintf("%f", gasAdjustment), 0, 0, fees, nil, generateOnly, simulate, ) - sr := sendReq{ + sr := rest.SendReq{ Amount: sdk.Coins{sdk.NewInt64Coin(stakingTypes.DefaultBondDenom, 1)}, BaseReq: baseReq, } @@ -859,7 +832,7 @@ func doBeginRedelegation(t *testing.T, port, name, password string, chainID := viper.GetString(client.FlagChainID) baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - msg := msgBeginRedelegateInput{ + msg := rest.MsgBeginRedelegateInput{ BaseReq: baseReq, DelegatorAddr: delAddr, ValidatorSrcAddr: valSrcAddr, @@ -1090,7 +1063,7 @@ func doSubmitProposal(t *testing.T, port, seed, name, password string, proposerA chainID := viper.GetString(client.FlagChainID) baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - pr := postProposalReq{ + pr := rest.PostProposalReq{ Title: "Test", Description: "test", ProposalType: "Text", @@ -1186,7 +1159,7 @@ func doDeposit(t *testing.T, port, seed, name, password string, proposerAddr sdk chainID := viper.GetString(client.FlagChainID) baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - dr := depositReq{ + dr := rest.DepositReq{ Depositor: proposerAddr, Amount: sdk.Coins{sdk.NewCoin(stakingTypes.DefaultBondDenom, sdk.NewInt(amount))}, BaseReq: baseReq, @@ -1240,7 +1213,7 @@ func doVote(t *testing.T, port, seed, name, password string, proposerAddr sdk.Ac chainID := viper.GetString(client.FlagChainID) baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", accnum, sequence, fees, nil, false, false) - vr := voteReq{ + vr := rest.VoteReq{ Voter: proposerAddr, Option: option, BaseReq: baseReq, @@ -1372,7 +1345,7 @@ func doUnjail(t *testing.T, port, seed, name, password string, chainID := viper.GetString(client.FlagChainID) baseReq := rest.NewBaseReq(name, password, "", chainID, "", "", 1, 1, fees, nil, false, false) - ur := unjailReq{ + ur := rest.UnjailReq{ BaseReq: baseReq, } req, err := cdc.MarshalJSON(ur) diff --git a/client/rest/types.go b/client/rest/types.go index cb409627db99..0595042ba749 100644 --- a/client/rest/types.go +++ b/client/rest/types.go @@ -8,6 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/codec" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" ) // GasEstimateResponse defines a response definition for tx gas estimation. @@ -126,3 +127,61 @@ func ReadRESTReq(w http.ResponseWriter, r *http.Request, cdc *codec.Codec, req i return true } + +// AddrSeed combines an Address with the mnemonic of the private key to that address +type AddrSeed struct { + Address sdk.AccAddress + Seed string + Name string + Password string +} + +// SendReq requests sending an amount of coins +type SendReq struct { + Amount sdk.Coins `json:"amount"` + BaseReq BaseReq `json:"base_req"` +} + +// MsgBeginRedelegateInput request to begin a redelegation +type MsgBeginRedelegateInput struct { + BaseReq BaseReq `json:"base_req"` + DelegatorAddr sdk.AccAddress `json:"delegator_addr"` // in bech32 + ValidatorSrcAddr sdk.ValAddress `json:"validator_src_addr"` // in bech32 + ValidatorDstAddr sdk.ValAddress `json:"validator_dst_addr"` // in bech32 + SharesAmount sdk.Dec `json:"shares"` +} + +// PostProposalReq requests a proposals +type PostProposalReq struct { + BaseReq BaseReq `json:"base_req"` + Title string `json:"title"` // Title of the proposal + Description string `json:"description"` // Description of the proposal + ProposalType string `json:"proposal_type"` // Type of proposal. Initial set {PlainTextProposal, SoftwareUpgradeProposal} + Proposer sdk.AccAddress `json:"proposer"` // Address of the proposer + InitialDeposit sdk.Coins `json:"initial_deposit"` // Coins to add to the proposal's deposit +} + +// BroadcastReq requests broadcasting a transaction +type BroadcastReq struct { + Tx auth.StdTx `json:"tx"` + Return string `json:"return"` +} + +// DepositReq requests a deposit of an amount of coins +type DepositReq struct { + BaseReq BaseReq `json:"base_req"` + Depositor sdk.AccAddress `json:"depositor"` // Address of the depositor + Amount sdk.Coins `json:"amount"` // Coins to add to the proposal's deposit +} + +// VoteReq requests sending a vote +type VoteReq struct { + BaseReq BaseReq `json:"base_req"` + Voter sdk.AccAddress `json:"voter"` // address of the voter + Option string `json:"option"` // option from OptionSet chosen by the voter +} + +// UnjailReq request unjailing +type UnjailReq struct { + BaseReq BaseReq `json:"base_req"` +} diff --git a/cmd/gaia/cli_test/cli_test.go b/cmd/gaia/cli_test/cli_test.go index 6c58b6beebd8..2139f1fd00a2 100644 --- a/cmd/gaia/cli_test/cli_test.go +++ b/cmd/gaia/cli_test/cli_test.go @@ -48,7 +48,24 @@ func TestGaiaCLIKeysAddRecover(t *testing.T) { f := InitFixtures(t) f.KeysAddRecover("test-recover", "dentist task convince chimney quality leave banana trade firm crawl eternal easily") - require.Equal(t, f.KeyAddress("test-recover").String(), "cosmos1qcfdf69js922qrdr4yaww3ax7gjml6pdds46f4") + require.Equal(t, "cosmos1qcfdf69js922qrdr4yaww3ax7gjml6pdds46f4", f.KeyAddress("test-recover").String()) +} + +func TestGaiaCLIKeysAddRecoverHDPath(t *testing.T) { + t.Parallel() + f := InitFixtures(t) + + f.KeysAddRecoverHDPath("test-recoverHD1", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 0, 0) + require.Equal(t, "cosmos1qcfdf69js922qrdr4yaww3ax7gjml6pdds46f4", f.KeyAddress("test-recoverHD1").String()) + + f.KeysAddRecoverHDPath("test-recoverH2", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 1, 5) + require.Equal(t, "cosmos1pdfav2cjhry9k79nu6r8kgknnjtq6a7rykmafy", f.KeyAddress("test-recoverH2").String()) + + f.KeysAddRecoverHDPath("test-recoverH3", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 1, 17) + require.Equal(t, "cosmos1909k354n6wl8ujzu6kmh49w4d02ax7qvlkv4sn", f.KeyAddress("test-recoverH3").String()) + + f.KeysAddRecoverHDPath("test-recoverH4", "dentist task convince chimney quality leave banana trade firm crawl eternal easily", 2, 17) + require.Equal(t, "cosmos1v9plmhvyhgxk3th9ydacm7j4z357s3nhtwsjat", f.KeyAddress("test-recoverH4").String()) } func TestGaiaCLIMinimumFees(t *testing.T) { diff --git a/cmd/gaia/cli_test/test_helpers.go b/cmd/gaia/cli_test/test_helpers.go index 93fae3689643..5a206a997807 100644 --- a/cmd/gaia/cli_test/test_helpers.go +++ b/cmd/gaia/cli_test/test_helpers.go @@ -225,12 +225,18 @@ func (f *Fixtures) KeysAdd(name string, flags ...string) { executeWriteCheckErr(f.T, addFlags(cmd, flags), app.DefaultKeyPass) } -// KeysAdd is gaiacli keys add --recover +// KeysAddRecover prepares gaiacli keys add --recover func (f *Fixtures) KeysAddRecover(name, mnemonic string, flags ...string) { cmd := fmt.Sprintf("gaiacli keys add --home=%s --recover %s", f.GCLIHome, name) executeWriteCheckErr(f.T, addFlags(cmd, flags), app.DefaultKeyPass, mnemonic) } +// KeysAddRecoverHDPath prepares gaiacli keys add --recover --account --index +func (f *Fixtures) KeysAddRecoverHDPath(name, mnemonic string, account uint32, index uint32, flags ...string) { + cmd := fmt.Sprintf("gaiacli keys add --home=%s --recover %s --account %d --index %d", f.GCLIHome, name, account, index) + executeWriteCheckErr(f.T, addFlags(cmd, flags), app.DefaultKeyPass, mnemonic) +} + // KeysShow is gaiacli keys show func (f *Fixtures) KeysShow(name string, flags ...string) keys.KeyOutput { cmd := fmt.Sprintf("gaiacli keys show --home=%s %s", f.GCLIHome, name) diff --git a/crypto/keys/hd/hdpath.go b/crypto/keys/hd/hdpath.go index 112abe0b662a..050b0a39e39e 100644 --- a/crypto/keys/hd/hdpath.go +++ b/crypto/keys/hd/hdpath.go @@ -14,6 +14,7 @@ package hd import ( "crypto/hmac" "crypto/sha512" + "encoding/binary" "errors" "fmt" @@ -62,19 +63,7 @@ func NewParamsFromPath(path string) (*BIP44Params, error) { return nil, fmt.Errorf("path length is wrong. Expected 5, got %d", len(spl)) } - if spl[0] != "44'" { - return nil, fmt.Errorf("first field in path must be 44', got %v", spl[0]) - } - - if !isHardened(spl[1]) || !isHardened(spl[2]) { - return nil, - fmt.Errorf("second and third field in path must be hardened (ie. contain the suffix ', got %v and %v", spl[1], spl[2]) - } - if isHardened(spl[3]) || isHardened(spl[4]) { - return nil, - fmt.Errorf("fourth and fifth field in path must not be hardened (ie. not contain the suffix ', got %v and %v", spl[3], spl[4]) - } - + // Check items can be parsed purpose, err := hardenedInt(spl[0]) if err != nil { return nil, err @@ -91,15 +80,30 @@ func NewParamsFromPath(path string) (*BIP44Params, error) { if err != nil { return nil, err } - if !(change == 0 || change == 1) { - return nil, fmt.Errorf("change field can only be 0 or 1") - } addressIdx, err := hardenedInt(spl[4]) if err != nil { return nil, err } + // Confirm valid values + if spl[0] != "44'" { + return nil, fmt.Errorf("first field in path must be 44', got %v", spl[0]) + } + + if !isHardened(spl[1]) || !isHardened(spl[2]) { + return nil, + fmt.Errorf("second and third field in path must be hardened (ie. contain the suffix ', got %v and %v", spl[1], spl[2]) + } + if isHardened(spl[3]) || isHardened(spl[4]) { + return nil, + fmt.Errorf("fourth and fifth field in path must not be hardened (ie. not contain the suffix ', got %v and %v", spl[3], spl[4]) + } + + if !(change == 0 || change == 1) { + return nil, fmt.Errorf("change field can only be 0 or 1") + } + return &BIP44Params{ purpose: purpose, coinType: coinType, @@ -132,7 +136,7 @@ func NewFundraiserParams(account uint32, addressIdx uint32) *BIP44Params { return NewParams(44, 118, account, false, addressIdx) } -// Return the BIP44 fields as an array. +// DerivationPath returns the BIP44 fields as an array. func (p BIP44Params) DerivationPath() []uint32 { change := uint32(0) if p.change { @@ -251,8 +255,10 @@ func i64(key []byte, data []byte) (IL [32]byte, IR [32]byte) { mac := hmac.New(sha512.New, key) // sha512 does not err _, _ = mac.Write(data) + I := mac.Sum(nil) copy(IL[:], I[:32]) copy(IR[:], I[32:]) + return } diff --git a/crypto/keys/hd/hdpath_test.go b/crypto/keys/hd/hdpath_test.go index f310fc3555cb..275b714ceaf3 100644 --- a/crypto/keys/hd/hdpath_test.go +++ b/crypto/keys/hd/hdpath_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/stretchr/testify/assert" - "github.com/cosmos/go-bip39" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var defaultBIP39Passphrase = "" @@ -21,7 +21,27 @@ func mnemonicToSeed(mnemonic string) []byte { func ExampleStringifyPathParams() { path := NewParams(44, 0, 0, false, 0) fmt.Println(path.String()) - // Output: 44'/0'/0'/0/0 + path = NewParams(44, 33, 7, true, 9) + fmt.Println(path.String()) + // Output: + // 44'/0'/0'/0/0 + // 44'/33'/7'/1/9 +} + +func TestStringifyFundraiserPathParams(t *testing.T) { + path := NewFundraiserParams(4, 22) + require.Equal(t, "44'/118'/4'/0/22", path.String()) + + path = NewFundraiserParams(4, 57) + require.Equal(t, "44'/118'/4'/0/57", path.String()) +} + +func TestPathToArray(t *testing.T) { + path := NewParams(44, 118, 1, false, 4) + require.Equal(t, "[44 118 1 0 4]", fmt.Sprintf("%v", path.DerivationPath())) + + path = NewParams(44, 118, 2, true, 15) + require.Equal(t, "[44 118 2 1 15]", fmt.Sprintf("%v", path.DerivationPath())) } func TestParamsFromPath(t *testing.T) { @@ -60,6 +80,11 @@ func TestParamsFromPath(t *testing.T) { {"44'/0'/0'/0/0'"}, // fifth field must not have ' {"44'/-1'/0'/0/0"}, // no negatives {"44'/0'/0'/-1/0"}, // no negatives + {"a'/0'/0'/-1/0"}, // valid values + {"0/X/0'/-1/0"}, // valid values + {"44'/0'/X/-1/0"}, // valid values + {"44'/0'/0'/%/0"}, // valid values + {"44'/0'/0'/0/%"}, // valid values } for i, c := range badCases { @@ -80,14 +105,39 @@ func ExampleSomeBIP32TestVecs() { fmt.Println("keys from fundraiser test-vector (cosmos, bitcoin, ether)") fmt.Println() // cosmos - priv, _ := DerivePrivateKeyForPath(master, ch, FullFundraiserPath) - fmt.Println(hex.EncodeToString(priv[:])) + priv, err := DerivePrivateKeyForPath(master, ch, FullFundraiserPath) + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } // bitcoin - priv, _ = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0") - fmt.Println(hex.EncodeToString(priv[:])) + priv, err = DerivePrivateKeyForPath(master, ch, "44'/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } // ether - priv, _ = DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0") - fmt.Println(hex.EncodeToString(priv[:])) + priv, err = DerivePrivateKeyForPath(master, ch, "44'/60'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + // INVALID + priv, err = DerivePrivateKeyForPath(master, ch, "X/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } + priv, err = DerivePrivateKeyForPath(master, ch, "-44/0'/0'/0/0") + if err != nil { + fmt.Println("INVALID") + } else { + fmt.Println(hex.EncodeToString(priv[:])) + } fmt.Println() fmt.Println("keys generated via https://coinomi.com/recovery-phrase-tool.html") @@ -121,6 +171,8 @@ func ExampleSomeBIP32TestVecs() { // bfcb217c058d8bbafd5e186eae936106ca3e943889b0b4a093ae13822fd3170c // e77c3de76965ad89997451de97b95bb65ede23a6bf185a55d80363d92ee37c3d // 7fc4d8a8146dea344ba04c593517d3f377fa6cded36cd55aee0a0bb968e651bc + // INVALID + // INVALID // // keys generated via https://coinomi.com/recovery-phrase-tool.html // diff --git a/crypto/keys/keybase.go b/crypto/keys/keybase.go index e202cd6d816f..635a62f772e5 100644 --- a/crypto/keys/keybase.go +++ b/crypto/keys/keybase.go @@ -8,19 +8,18 @@ import ( "github.com/pkg/errors" - "github.com/cosmos/go-bip39" - "github.com/cosmos/cosmos-sdk/crypto" "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/go-bip39" + tmcrypto "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/encoding/amino" "github.com/tendermint/tendermint/crypto/secp256k1" dbm "github.com/tendermint/tendermint/libs/db" - - "github.com/cosmos/cosmos-sdk/crypto/keys/keyerror" ) var _ Keybase = dbKeybase{} @@ -30,6 +29,7 @@ var _ Keybase = dbKeybase{} // Find a list of all supported languages in the BIP 39 spec (word lists). type Language int +//noinspection ALL const ( // English is the default language to create a mnemonic. // It is the only supported language by this package. @@ -54,7 +54,7 @@ const ( const ( // used for deriving seed from mnemonic - defaultBIP39Passphrase = "" + DefaultBIP39Passphrase = "" // bits of entropy to draw when creating a mnemonic defaultEntropySize = 256 @@ -109,41 +109,15 @@ func (kb dbKeybase) CreateMnemonic(name string, language Language, passwd string return } - seed := bip39.NewSeed(mnemonic, defaultBIP39Passphrase) + seed := bip39.NewSeed(mnemonic, DefaultBIP39Passphrase) info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) return } -// TEMPORARY METHOD UNTIL WE FIGURE OUT USER FACING HD DERIVATION API -func (kb dbKeybase) CreateKey(name, mnemonic, passwd string) (info Info, err error) { - words := strings.Split(mnemonic, " ") - if len(words) != 12 && len(words) != 24 { - err = fmt.Errorf("recovering only works with 12 word (fundraiser) or 24 word mnemonics, got: %v words", len(words)) - return - } - seed, err := bip39.NewSeedWithErrorChecking(mnemonic, defaultBIP39Passphrase) - if err != nil { - return - } - info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) - return -} - -// CreateFundraiserKey converts a mnemonic to a private key and persists it, -// encrypted with the given password. -// TODO(ismail) -func (kb dbKeybase) CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) { - words := strings.Split(mnemonic, " ") - if len(words) != 12 { - err = fmt.Errorf("recovering only works with 12 word (fundraiser), got: %v words", len(words)) - return - } - seed, err := bip39.NewSeedWithErrorChecking(mnemonic, defaultBIP39Passphrase) - if err != nil { - return - } - info, err = kb.persistDerivedKey(seed, passwd, name, hd.FullFundraiserPath) - return +// CreateAccount converts a mnemonic to a private key and persists it, encrypted with the given password. +func (kb dbKeybase) CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) { + hdPath := hd.NewFundraiserParams(account, index) + return kb.Derive(name, mnemonic, bip39Passwd, encryptPasswd, *hdPath) } func (kb dbKeybase) Derive(name, mnemonic, bip39Passphrase, encryptPasswd string, params hd.BIP44Params) (info Info, err error) { @@ -151,23 +125,26 @@ func (kb dbKeybase) Derive(name, mnemonic, bip39Passphrase, encryptPasswd string if err != nil { return } - info, err = kb.persistDerivedKey(seed, encryptPasswd, name, params.String()) + info, err = kb.persistDerivedKey(seed, encryptPasswd, name, params.String()) return } // CreateLedger creates a new locally-stored reference to a Ledger keypair // It returns the created key info and an error if the Ledger could not be queried -func (kb dbKeybase) CreateLedger(name string, path crypto.DerivationPath, algo SigningAlgo) (Info, error) { +func (kb dbKeybase) CreateLedger(name string, algo SigningAlgo, account uint32, index uint32) (Info, error) { if algo != Secp256k1 { return nil, ErrUnsupportedSigningAlgo } - priv, err := crypto.NewPrivKeyLedgerSecp256k1(path) + + hdPath := hd.NewFundraiserParams(account, index) + priv, err := crypto.NewPrivKeyLedgerSecp256k1(*hdPath) if err != nil { return nil, err } pub := priv.PubKey() - return kb.writeLedgerKey(pub, path, name), nil + + return kb.writeLedgerKey(pub, *hdPath, name), nil } // CreateOffline creates a new reference to an offline keypair @@ -432,7 +409,7 @@ func (kb dbKeybase) writeLocalKey(priv tmcrypto.PrivKey, name, passphrase string return info } -func (kb dbKeybase) writeLedgerKey(pub tmcrypto.PubKey, path crypto.DerivationPath, name string) Info { +func (kb dbKeybase) writeLedgerKey(pub tmcrypto.PubKey, path hd.BIP44Params, name string) Info { info := newLedgerInfo(name, pub, path) kb.writeInfo(info, name) return info diff --git a/crypto/keys/keybase_test.go b/crypto/keys/keybase_test.go index bc7783b67ce3..36a05d3741ff 100644 --- a/crypto/keys/keybase_test.go +++ b/crypto/keys/keybase_test.go @@ -344,7 +344,7 @@ func TestSeedPhrase(t *testing.T) { // let us re-create it from the mnemonic-phrase params := *hd.NewFundraiserParams(0, 0) - newInfo, err := cstore.Derive(n2, mnemonic, defaultBIP39Passphrase, p2, params) + newInfo, err := cstore.Derive(n2, mnemonic, DefaultBIP39Passphrase, p2, params) require.NoError(t, err) require.Equal(t, n2, newInfo.GetName()) require.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address()) diff --git a/crypto/keys/types.go b/crypto/keys/types.go index 14d050961135..52ef88b4b94b 100644 --- a/crypto/keys/types.go +++ b/crypto/keys/types.go @@ -3,8 +3,6 @@ package keys import ( "github.com/tendermint/tendermint/crypto" - ccrypto "github.com/cosmos/cosmos-sdk/crypto" - "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/cosmos/cosmos-sdk/types" ) @@ -23,20 +21,20 @@ type Keybase interface { // CreateMnemonic creates a new mnemonic, and derives a hierarchical deterministic // key from that. CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error) - // CreateKey takes a mnemonic and derives, a password. This method is temporary - CreateKey(name, mnemonic, passwd string) (info Info, err error) - // CreateFundraiserKey takes a mnemonic and derives, a password - CreateFundraiserKey(name, mnemonic, passwd string) (info Info, err error) - // Compute a BIP39 seed from th mnemonic and bip39Passwd. + + // CreateAccount creates an account based using the BIP44 path (44'/118'/{account}'/0/{index} + CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) + + // Derive computes a BIP39 seed from th mnemonic and bip39Passwd. // Derive private key from the seed using the BIP44 params. // Encrypt the key to disk using encryptPasswd. // See https://github.com/cosmos/cosmos-sdk/issues/2095 - Derive(name, mnemonic, bip39Passwd, - encryptPasswd string, params hd.BIP44Params) (Info, error) - // Create, store, and return a new Ledger key reference - CreateLedger(name string, path ccrypto.DerivationPath, algo SigningAlgo) (info Info, err error) + Derive(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (Info, error) + + // CreateLedger creates, stores, and returns a new Ledger key reference + CreateLedger(name string, algo SigningAlgo, account uint32, index uint32) (info Info, err error) - // Create, store, and return a new offline key reference + // CreateOffline creates, stores, and returns a new offline key reference CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error) // The following operations will *only* work on locally-stored keys @@ -46,10 +44,10 @@ type Keybase interface { Export(name string) (armor string, err error) ExportPubKey(name string) (armor string, err error) - // *only* works on locally-stored keys. Temporary method until we redo the exporting API + // ExportPrivateKeyObject *only* works on locally-stored keys. Temporary method until we redo the exporting API ExportPrivateKeyObject(name string, passphrase string) (crypto.PrivKey, error) - // Close closes the database. + // CloseDB closes the database. CloseDB() } @@ -123,12 +121,12 @@ func (i localInfo) GetAddress() types.AccAddress { // ledgerInfo is the public information about a Ledger key type ledgerInfo struct { - Name string `json:"name"` - PubKey crypto.PubKey `json:"pubkey"` - Path ccrypto.DerivationPath `json:"path"` + Name string `json:"name"` + PubKey crypto.PubKey `json:"pubkey"` + Path hd.BIP44Params `json:"path"` } -func newLedgerInfo(name string, pub crypto.PubKey, path ccrypto.DerivationPath) Info { +func newLedgerInfo(name string, pub crypto.PubKey, path hd.BIP44Params) Info { return &ledgerInfo{ Name: name, PubKey: pub, diff --git a/crypto/ledger_secp256k1.go b/crypto/ledger_secp256k1.go index ff05d31bae66..8f5c9ab8138b 100644 --- a/crypto/ledger_secp256k1.go +++ b/crypto/ledger_secp256k1.go @@ -6,6 +6,7 @@ import ( "github.com/pkg/errors" secp256k1 "github.com/btcsuite/btcd/btcec" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" tmcrypto "github.com/tendermint/tendermint/crypto" tmsecp256k1 "github.com/tendermint/tendermint/crypto/secp256k1" ) @@ -22,9 +23,6 @@ type ( // dependencies when Ledger support is potentially not enabled. discoverLedgerFn func() (LedgerSECP256K1, error) - // DerivationPath represents a Ledger derivation path. - DerivationPath []uint32 - // LedgerSECP256K1 reflects an interface a Ledger API must implement for // the SECP256K1 scheme. LedgerSECP256K1 interface { @@ -39,7 +37,7 @@ type ( // go-amino so we can view the address later, even without having the // ledger attached. CachedPubKey tmcrypto.PubKey - Path DerivationPath + Path hd.BIP44Params ledger LedgerSECP256K1 } ) @@ -49,7 +47,7 @@ type ( // // CONTRACT: The ledger device, ledgerDevice, must be loaded and set prior to // any creation of a PrivKeyLedgerSecp256k1. -func NewPrivKeyLedgerSecp256k1(path DerivationPath) (tmcrypto.PrivKey, error) { +func NewPrivKeyLedgerSecp256k1(path hd.BIP44Params) (tmcrypto.PrivKey, error) { if discoverLedger == nil { return nil, errors.New("no Ledger discovery function defined") } @@ -138,11 +136,11 @@ func (pkl PrivKeyLedgerSecp256k1) getPubKey() (key tmcrypto.PubKey, err error) { } func (pkl PrivKeyLedgerSecp256k1) signLedgerSecp256k1(msg []byte) ([]byte, error) { - return pkl.ledger.SignSECP256K1(pkl.Path, msg) + return pkl.ledger.SignSECP256K1(pkl.Path.DerivationPath(), msg) } func (pkl PrivKeyLedgerSecp256k1) pubkeyLedgerSecp256k1() (pub tmcrypto.PubKey, err error) { - key, err := pkl.ledger.GetPublicKeySECP256K1(pkl.Path) + key, err := pkl.ledger.GetPublicKeySECP256K1(pkl.Path.DerivationPath()) if err != nil { return nil, fmt.Errorf("error fetching public key: %v", err) } diff --git a/crypto/ledger_test.go b/crypto/ledger_test.go index 1aae158eff95..cf14db74b583 100644 --- a/crypto/ledger_test.go +++ b/crypto/ledger_test.go @@ -5,6 +5,8 @@ import ( "os" "testing" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/encoding/amino" ) @@ -16,9 +18,9 @@ func TestRealLedgerSecp256k1(t *testing.T) { t.Skip(fmt.Sprintf("Set '%s' to run code on a real ledger", ledgerEnabledEnv)) } msg := []byte("{\"account_number\":\"3\",\"chain_id\":\"1234\",\"fee\":{\"amount\":[{\"amount\":\"150\",\"denom\":\"atom\"}],\"gas\":\"5000\"},\"memo\":\"memo\",\"msgs\":[[\"%s\"]],\"sequence\":\"6\"}") - path := DerivationPath{44, 60, 0, 0, 0} + path := hd.NewParams(44, 60, 0, false, 0) - priv, err := NewPrivKeyLedgerSecp256k1(path) + priv, err := NewPrivKeyLedgerSecp256k1(*path) require.Nil(t, err, "%s", err) pub := priv.PubKey() @@ -58,7 +60,7 @@ func TestRealLedgerErrorHandling(t *testing.T) { // first, try to generate a key, must return an error // (no panic) - path := DerivationPath{44, 60, 0, 0, 0} - _, err := NewPrivKeyLedgerSecp256k1(path) + path := hd.NewParams(44, 60, 0, false, 0) + _, err := NewPrivKeyLedgerSecp256k1(*path) require.Error(t, err) }