Skip to content

Commit

Permalink
Add keys new and keys mnemonic commands
Browse files Browse the repository at this point in the history
Closes cosmos#2091.
  • Loading branch information
mslipper committed Aug 29, 2018
1 parent fd8c1e5 commit 0803d4b
Show file tree
Hide file tree
Showing 11 changed files with 456 additions and 40 deletions.
23 changes: 15 additions & 8 deletions client/input.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"strings"

"github.com/bgentry/speakeasy"
isatty "github.com/mattn/go-isatty"
"github.com/mattn/go-isatty"
"github.com/pkg/errors"
)

Expand Down Expand Up @@ -44,13 +44,8 @@ func GetPassword(prompt string, buf *bufio.Reader) (pass string, err error) {

// GetSeed will request a seed phrase from stdin and trims off
// leading/trailing spaces
func GetSeed(prompt string, buf *bufio.Reader) (seed string, err error) {
if inputIsTty() {
fmt.Println(prompt)
}
seed, err = readLineFromBuf(buf)
seed = strings.TrimSpace(seed)
return
func GetSeed(prompt string, buf *bufio.Reader) (string, error) {
return GetString(prompt, buf)
}

// GetCheckPassword will prompt for a password twice to verify they
Expand Down Expand Up @@ -100,6 +95,18 @@ func GetConfirmation(prompt string, buf *bufio.Reader) (bool, error) {
}
}

func GetString(prompt string, buf *bufio.Reader) (string, error) {
if inputIsTty() && prompt != "" {
fmt.Println(prompt)
}

out, err := readLineFromBuf(buf)
if err != nil {
return "", err
}
return strings.TrimSpace(out), nil
}

// inputIsTty returns true iff we have an interactive prompt,
// where we can disable echo and request to repeat the password.
// If false, we can optimize for piped input from another command
Expand Down
87 changes: 87 additions & 0 deletions client/keys/mnemonic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package keys

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/cosmos-sdk/crypto/keys"
"fmt"
"os/signal"
"os"
"syscall"
"bytes"
"github.com/cosmos/cosmos-sdk/client"
"bufio"
"github.com/cosmos/cosmos-sdk/crypto/keys/bip39"
)

const (
flagEntropy = "user"
)

func mnemonicCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "mnemonic",
Short: "Creates a new mnemonic for use in key generation. Uses system entropy by default.",
RunE: runMnemonicCmd,
}
cmd.Flags().Bool(flagEntropy, false, "Prompt the use to enter entropy. Otherwise, use the system's entropy.")
return cmd
}

func runMnemonicCmd(cmd *cobra.Command, args []string) error {
kb, err := GetKeyBase()
if err != nil {
return err
}

if !viper.GetBool(flagEntropy) {
return outputMnemonic(kb, nil)
}

stdin := client.BufferStdin()
var buf bytes.Buffer
done := make(chan bool)
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGTERM)

// need below signal handling in order to prevent panics on SIGTERM
go func() {
<-sigs
fmt.Println("Killed.")
os.Exit(1)
}()

go func() {
fmt.Println("Please provide entropy using your keyboard and press enter.")
scanner := bufio.NewScanner(stdin)
for scanner.Scan() {
buf.Write(scanner.Bytes())
if buf.Len() >= bip39.FreshKeyEntropySize {
done <- true
return
}

fmt.Println("Please provide additional entropy and press enter.")
}
}()

<-done
if err != nil {
return err
}

buf.Truncate(bip39.FreshKeyEntropySize)
return outputMnemonic(kb, buf.Bytes())

}

func outputMnemonic(kb keys.Keybase, entropy []byte) error {
fmt.Println("Generating mnemonic...")
mnemonic, err := kb.GenerateMnemonic(keys.English, entropy)
if err != nil {
return err
}

fmt.Println(mnemonic)
return nil
}
123 changes: 123 additions & 0 deletions client/keys/new.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package keys

import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/pkg/errors"
"github.com/cosmos/cosmos-sdk/crypto/keys"
"fmt"
"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/crypto/keys/hd"
)

const (
flagDefault = "default"
)

func newCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "new <name>",
Args: cobra.ExactArgs(1),
Short: "Creates a new key interactively, or with sensible defaults.",
RunE: runNewCommand,
}
cmd.Flags().Bool(flagDefault, true, "Use system entropy to generate a new mnemonic and derive a key using default parameters")
return cmd
}

func runNewCommand(cmd *cobra.Command, args []string) error {
name := args[0]

if name == "" {
return errors.New("you must provide a name for the key")
}

kb, err := GetKeyBase()
if err != nil {
return err
}

isDefault := viper.GetBool(flagDefault)
if isDefault {
_, seed, err := kb.CreateMnemonic(name, keys.English, "", keys.Secp256k1)
if err != nil {
return err
}
fmt.Printf("Seed: %s\n", seed)
fmt.Printf("Successfully wrote encrypted priv key named %s\n", name)
return nil
}

var mnemonic string
var bip39Pw string
var bip44Path string
var encryptionPw string

stdin := client.BufferStdin()
printPrefixed("Enter your bip39 mnemonic.")
printPrefixed("If you don't have one, just hit enter and one will be generated for you.")
mnemonic, err = client.GetSeed("", stdin)
if err != nil {
return err
}

if mnemonic == "" {
mnemonic, err = kb.GenerateMnemonic(keys.English, nil)
if err != nil {
return err
}
}

printStep()
printPrefixed("Enter your bip39 passphrase.")
printPrefixed("If you don't have one, just hit enter and the default \"\" will be used.")
bip39Pw, err = client.GetString("", stdin)
if err != nil {
return err
}

printStep()
printPrefixed("Enter your bip44 path. If you press enter, the default of m/44'/0'/0'/0/0 will be used.")
bip44Path, err = client.GetString("", stdin)
if err != nil {
return err
}

if bip44Path == "" {
bip44Path = "m/44'/0'/0'/0/0"
}

printStep()
printPrefixed("Enter a password to encrypt the derived private key with.")
encryptionPw, err = client.GetString("", stdin)
if err != nil {
return err
}

if encryptionPw == "" {
return errors.New("you must define an encryption password")
}

printStep()

params, err := hd.ParamsFromString(bip44Path)
if err != nil {
return err
}
_, err = kb.Derive(name, mnemonic, encryptionPw, bip39Pw, *params)
if err != nil {
return err
}
fmt.Printf("Mnemonic: %s\n", mnemonic)
fmt.Printf("Successfully wrote encrypted priv key named %s\n", name)

return nil
}

func printPrefixed(msg string) {
fmt.Printf("> %s\n", msg)
}

func printStep() {
printPrefixed("-------------------------------------")
}
2 changes: 2 additions & 0 deletions client/keys/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ func Commands() *cobra.Command {
client.LineBreak,
deleteKeyCommand(),
updateKeyCommand(),
mnemonicCommand(),
newCommand(),
)
return cmd
}
Expand Down
35 changes: 35 additions & 0 deletions cmd/gaia/cli_test/cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/cosmos/cosmos-sdk/x/auth"
"github.com/cosmos/cosmos-sdk/x/gov"
"github.com/cosmos/cosmos-sdk/x/stake"
"strings"
)

var (
Expand Down Expand Up @@ -276,6 +277,31 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
require.Equal(t, " 2 - Apples", proposalsQuery)
}

func TestGaiaCLIKeysNew(t *testing.T) {
keyName := "testKey"
tests.ExecuteT(t, fmt.Sprintf("gaiad --home=%s unsafe-reset-all", gaiadHome), "")
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s foo", gaiacliHome), app.DefaultKeyPass)
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s bar", gaiacliHome), app.DefaultKeyPass)

// happy path with defaults
executeInit(t, fmt.Sprintf("gaiad init -o --name=foo --home=%s --home-client=%s", gaiadHome, gaiacliHome))
executeWrite(t, fmt.Sprintf("gaiacli --home=%s keys new --default=true %s", gaiacliHome, keyName))
keyAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show %s --output=json --home=%s", keyName, gaiacliHome))
require.NotNil(t, keyAddr)
}

func TestGaiaCLIMnemonic(t *testing.T) {
tests.ExecuteT(t, fmt.Sprintf("gaiad --home=%s unsafe-reset-all", gaiadHome), "")
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s foo", gaiacliHome), app.DefaultKeyPass)
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s bar", gaiacliHome), app.DefaultKeyPass)
executeMnemonic(t, fmt.Sprintf("gaiacli keys mnemonic"), "", "")
executeMnemonic(t,
fmt.Sprintf("gaiacli keys mnemonic --user"),
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
"donor anxiety expect little beef pass agree choice donor anxiety expect little beef pass agree choice donor anxiety expect little beef pass agree cattle",
)
}

//___________________________________________________________________________________
// helper methods

Expand Down Expand Up @@ -428,3 +454,12 @@ func executeGetVotes(t *testing.T, cmdStr string) []gov.Vote {
require.NoError(t, err, "out %v\n, err %v", out, err)
return votes
}

func executeMnemonic(t *testing.T, cmdStr string, entropy string, expectedMnemonic string) {
out := tests.ExecuteT(t, cmdStr, entropy)
require.True(t, len(strings.Split(out, " ")) > 24)

if expectedMnemonic != "" {
require.Contains(t, out, expectedMnemonic)
}
}
Loading

0 comments on commit 0803d4b

Please sign in to comment.