diff --git a/cmd/delete.go b/cmd/delete.go new file mode 100644 index 000000000..6c0185166 --- /dev/null +++ b/cmd/delete.go @@ -0,0 +1,49 @@ +// Copyright © 2017 Ethan Frey +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + + "github.com/pkg/errors" + + "github.com/spf13/cobra" +) + +// deleteCmd represents the delete command +var deleteCmd = &cobra.Command{ + Use: "delete ", + Short: "DANGER: Delete a private key from your system", + RunE: runDeleteCmd, +} + +func runDeleteCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a name for the key") + } + name := args[0] + + oldpass, err := getPassword("DANGER - enter password to permanently delete key:") + if err != nil { + return err + } + + err = GetKeyManager().Delete(name, oldpass) + if err != nil { + return err + } + fmt.Println("Password deleted forever (uh oh!)") + return nil +} diff --git a/cmd/new.go b/cmd/new.go index b48f9727a..2969a1042 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -15,14 +15,20 @@ package cmd import ( + "fmt" + "github.com/pkg/errors" + "github.com/tendermint/go-crypto/keys" + "github.com/tendermint/go-wire/data" + "github.com/tendermint/tmlibs/cli" "github.com/spf13/cobra" "github.com/spf13/viper" ) const ( - flagType = "type" + flagType = "type" + flagNoBackup = "no-backup" ) // newCmd represents the new command @@ -37,6 +43,7 @@ passed as a command line argument for security.`, func init() { newCmd.Flags().StringP(flagType, "t", "ed25519", "Type of key (ed25519|secp256k1)") + newCmd.Flags().Bool(flagNoBackup, false, "Don't print out seed phrase (if others are watching the terminal)") } func runNewCmd(cmd *cobra.Command, args []string) error { @@ -51,9 +58,37 @@ func runNewCmd(cmd *cobra.Command, args []string) error { return err } - info, _, err := GetKeyManager().Create(name, pass, algo) + info, seed, err := GetKeyManager().Create(name, pass, algo) if err == nil { - printInfo(info) + printCreate(info, seed) } return err } + +type NewOutput struct { + Key keys.Info `json:"key"` + Seed string `json:"seed"` +} + +func printCreate(info keys.Info, seed string) { + switch viper.Get(cli.OutputFlag) { + case "text": + printInfo(info) + // print seed unless requested not to. + if !viper.GetBool(flagNoBackup) { + fmt.Println("**Important** write this seed phrase in a safe place.") + fmt.Println("It is the only way to recover your account if you ever forget your password.\n") + fmt.Println(seed) + } + case "json": + out := NewOutput{Key: info} + if !viper.GetBool(flagNoBackup) { + out.Seed = seed + } + json, err := data.ToJSON(out) + if err != nil { + panic(err) // really shouldn't happen... + } + fmt.Println(string(json)) + } +} diff --git a/cmd/recover.go b/cmd/recover.go new file mode 100644 index 000000000..e3bd919b1 --- /dev/null +++ b/cmd/recover.go @@ -0,0 +1,53 @@ +// Copyright © 2017 Ethan Frey +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "github.com/pkg/errors" + + "github.com/spf13/cobra" +) + +// recoverCmd represents the recover command +var recoverCmd = &cobra.Command{ + Use: "recover ", + Short: "Change the password for a private key", + RunE: runRecoverCmd, +} + +func runRecoverCmd(cmd *cobra.Command, args []string) error { + if len(args) != 1 || len(args[0]) == 0 { + return errors.New("You must provide a name for the key") + } + name := args[0] + + pass, err := getPassword("Enter the new passphrase:") + if err != nil { + return err + } + + // not really a password... huh? + seed, err := getSeed("Enter your recovery seed phrase") + if err != nil { + return err + } + + info, err := GetKeyManager().Recover(name, pass, seed) + if err != nil { + return err + } + printInfo(info) + return nil +} diff --git a/cmd/root.go b/cmd/root.go index ad5a633fb..cfd3d1adf 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -41,6 +41,8 @@ func init() { RootCmd.AddCommand(listCmd) RootCmd.AddCommand(newCmd) RootCmd.AddCommand(updateCmd) + RootCmd.AddCommand(deleteCmd) + RootCmd.AddCommand(recoverCmd) } func RegisterServer() { diff --git a/cmd/utils.go b/cmd/utils.go index e54343b7a..7978f78a7 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -76,6 +76,12 @@ func getPassword(prompt string) (pass string, err error) { return pass, nil } +func getSeed(prompt string) (seed string, err error) { + seed, err = stdinPassword() + seed = strings.TrimSpace(seed) + return +} + func getCheckPassword(prompt, prompt2 string) (string, error) { // simple read on no-tty if !inputIsTty() { diff --git a/tests/keys.sh b/tests/keys.sh index 82c8083ec..c848f8dd2 100755 --- a/tests/keys.sh +++ b/tests/keys.sh @@ -12,8 +12,9 @@ oneTimeSetUp() { newKey(){ assertNotNull "keyname required" "$1" KEYPASS=${2:-qwertyuiop} - KEY=$(echo $KEYPASS | ${EXE} new $1) - assertTrue "created $1" $? + KEY=$(echo $KEYPASS | ${EXE} new $1 -o json) + if ! assertTrue "created $1" $?; then return 1; fi + assertEquals "$1" $(echo $KEY | jq .key.name | tr -d \") return $? } @@ -59,6 +60,52 @@ test02updateKeys() { assertTrue "takes new key after update" "updateKey $USER $PASS2 $PASS3" } +test03recoverKeys() { + USER=sleepy + PASS1=S4H.9j.D9S7hso + + USER2=easy + PASS2=1234567890 + + # make a user and check they exist + echo "create..." + KEY=$(echo $PASS1 | ${EXE} new $USER -o json) + if ! assertTrue "created $USER" $?; then return 1; fi + if [ -n "$DEBUG" ]; then echo $KEY; echo; fi + + SEED=$(echo $KEY | jq .seed | tr -d \") + ADDR=$(echo $KEY | jq .key.address | tr -d \") + PUBKEY=$(echo $KEY | jq .key.pubkey | tr -d \") + assertTrue "${EXE} get $USER > /dev/null" + + # let's delete this key + echo "delete..." + assertFalse "echo foo | ${EXE} delete $USER > /dev/null" + assertTrue "echo $PASS1 | ${EXE} delete $USER > /dev/null" + assertFalse "${EXE} get $USER > /dev/null" + + # fails on short password + echo "recover..." + assertFalse "echo foo; echo $SEED | ${EXE} recover $USER2 -o json > /dev/null" + # fails on bad seed + assertFalse "echo $PASS2; echo \"silly white whale tower bongo\" | ${EXE} recover $USER2 -o json > /dev/null" + # now we got it + KEY2=$((echo $PASS2; echo $SEED) | ${EXE} recover $USER2 -o json) + if ! assertTrue "recovery failed: $KEY2" $?; then return 1; fi + if [ -n "$DEBUG" ]; then echo $KEY2; echo; fi + + # make sure it looks the same + NAME2=$(echo $KEY2 | jq .name | tr -d \") + ADDR2=$(echo $KEY2 | jq .address | tr -d \") + PUBKEY2=$(echo $KEY2 | jq .pubkey | tr -d \") + assertEquals "wrong username" "$USER2" "$NAME2" + assertEquals "address doesn't match" "$ADDR" "$ADDR2" + assertEquals "pubkey doesn't match" "$PUBKEY" "$PUBKEY2" + + # and we can find the info + assertTrue "${EXE} get $USER2 > /dev/null" +} + # load and run these tests with shunit2! DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" #get this files directory . $DIR/shunit2