Skip to content

Commit 0803d4b

Browse files
committed
Add keys new and keys mnemonic commands
Closes cosmos#2091.
1 parent fd8c1e5 commit 0803d4b

File tree

11 files changed

+456
-40
lines changed

11 files changed

+456
-40
lines changed

client/input.go

+15-8
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"strings"
88

99
"github.com/bgentry/speakeasy"
10-
isatty "github.com/mattn/go-isatty"
10+
"github.com/mattn/go-isatty"
1111
"github.com/pkg/errors"
1212
)
1313

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

4545
// GetSeed will request a seed phrase from stdin and trims off
4646
// leading/trailing spaces
47-
func GetSeed(prompt string, buf *bufio.Reader) (seed string, err error) {
48-
if inputIsTty() {
49-
fmt.Println(prompt)
50-
}
51-
seed, err = readLineFromBuf(buf)
52-
seed = strings.TrimSpace(seed)
53-
return
47+
func GetSeed(prompt string, buf *bufio.Reader) (string, error) {
48+
return GetString(prompt, buf)
5449
}
5550

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

98+
func GetString(prompt string, buf *bufio.Reader) (string, error) {
99+
if inputIsTty() && prompt != "" {
100+
fmt.Println(prompt)
101+
}
102+
103+
out, err := readLineFromBuf(buf)
104+
if err != nil {
105+
return "", err
106+
}
107+
return strings.TrimSpace(out), nil
108+
}
109+
103110
// inputIsTty returns true iff we have an interactive prompt,
104111
// where we can disable echo and request to repeat the password.
105112
// If false, we can optimize for piped input from another command

client/keys/mnemonic.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package keys
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/viper"
6+
"github.com/cosmos/cosmos-sdk/crypto/keys"
7+
"fmt"
8+
"os/signal"
9+
"os"
10+
"syscall"
11+
"bytes"
12+
"github.com/cosmos/cosmos-sdk/client"
13+
"bufio"
14+
"github.com/cosmos/cosmos-sdk/crypto/keys/bip39"
15+
)
16+
17+
const (
18+
flagEntropy = "user"
19+
)
20+
21+
func mnemonicCommand() *cobra.Command {
22+
cmd := &cobra.Command{
23+
Use: "mnemonic",
24+
Short: "Creates a new mnemonic for use in key generation. Uses system entropy by default.",
25+
RunE: runMnemonicCmd,
26+
}
27+
cmd.Flags().Bool(flagEntropy, false, "Prompt the use to enter entropy. Otherwise, use the system's entropy.")
28+
return cmd
29+
}
30+
31+
func runMnemonicCmd(cmd *cobra.Command, args []string) error {
32+
kb, err := GetKeyBase()
33+
if err != nil {
34+
return err
35+
}
36+
37+
if !viper.GetBool(flagEntropy) {
38+
return outputMnemonic(kb, nil)
39+
}
40+
41+
stdin := client.BufferStdin()
42+
var buf bytes.Buffer
43+
done := make(chan bool)
44+
sigs := make(chan os.Signal, 1)
45+
signal.Notify(sigs, syscall.SIGTERM)
46+
47+
// need below signal handling in order to prevent panics on SIGTERM
48+
go func() {
49+
<-sigs
50+
fmt.Println("Killed.")
51+
os.Exit(1)
52+
}()
53+
54+
go func() {
55+
fmt.Println("Please provide entropy using your keyboard and press enter.")
56+
scanner := bufio.NewScanner(stdin)
57+
for scanner.Scan() {
58+
buf.Write(scanner.Bytes())
59+
if buf.Len() >= bip39.FreshKeyEntropySize {
60+
done <- true
61+
return
62+
}
63+
64+
fmt.Println("Please provide additional entropy and press enter.")
65+
}
66+
}()
67+
68+
<-done
69+
if err != nil {
70+
return err
71+
}
72+
73+
buf.Truncate(bip39.FreshKeyEntropySize)
74+
return outputMnemonic(kb, buf.Bytes())
75+
76+
}
77+
78+
func outputMnemonic(kb keys.Keybase, entropy []byte) error {
79+
fmt.Println("Generating mnemonic...")
80+
mnemonic, err := kb.GenerateMnemonic(keys.English, entropy)
81+
if err != nil {
82+
return err
83+
}
84+
85+
fmt.Println(mnemonic)
86+
return nil
87+
}

client/keys/new.go

+123
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package keys
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
"github.com/spf13/viper"
6+
"github.com/pkg/errors"
7+
"github.com/cosmos/cosmos-sdk/crypto/keys"
8+
"fmt"
9+
"github.com/cosmos/cosmos-sdk/client"
10+
"github.com/cosmos/cosmos-sdk/crypto/keys/hd"
11+
)
12+
13+
const (
14+
flagDefault = "default"
15+
)
16+
17+
func newCommand() *cobra.Command {
18+
cmd := &cobra.Command{
19+
Use: "new <name>",
20+
Args: cobra.ExactArgs(1),
21+
Short: "Creates a new key interactively, or with sensible defaults.",
22+
RunE: runNewCommand,
23+
}
24+
cmd.Flags().Bool(flagDefault, true, "Use system entropy to generate a new mnemonic and derive a key using default parameters")
25+
return cmd
26+
}
27+
28+
func runNewCommand(cmd *cobra.Command, args []string) error {
29+
name := args[0]
30+
31+
if name == "" {
32+
return errors.New("you must provide a name for the key")
33+
}
34+
35+
kb, err := GetKeyBase()
36+
if err != nil {
37+
return err
38+
}
39+
40+
isDefault := viper.GetBool(flagDefault)
41+
if isDefault {
42+
_, seed, err := kb.CreateMnemonic(name, keys.English, "", keys.Secp256k1)
43+
if err != nil {
44+
return err
45+
}
46+
fmt.Printf("Seed: %s\n", seed)
47+
fmt.Printf("Successfully wrote encrypted priv key named %s\n", name)
48+
return nil
49+
}
50+
51+
var mnemonic string
52+
var bip39Pw string
53+
var bip44Path string
54+
var encryptionPw string
55+
56+
stdin := client.BufferStdin()
57+
printPrefixed("Enter your bip39 mnemonic.")
58+
printPrefixed("If you don't have one, just hit enter and one will be generated for you.")
59+
mnemonic, err = client.GetSeed("", stdin)
60+
if err != nil {
61+
return err
62+
}
63+
64+
if mnemonic == "" {
65+
mnemonic, err = kb.GenerateMnemonic(keys.English, nil)
66+
if err != nil {
67+
return err
68+
}
69+
}
70+
71+
printStep()
72+
printPrefixed("Enter your bip39 passphrase.")
73+
printPrefixed("If you don't have one, just hit enter and the default \"\" will be used.")
74+
bip39Pw, err = client.GetString("", stdin)
75+
if err != nil {
76+
return err
77+
}
78+
79+
printStep()
80+
printPrefixed("Enter your bip44 path. If you press enter, the default of m/44'/0'/0'/0/0 will be used.")
81+
bip44Path, err = client.GetString("", stdin)
82+
if err != nil {
83+
return err
84+
}
85+
86+
if bip44Path == "" {
87+
bip44Path = "m/44'/0'/0'/0/0"
88+
}
89+
90+
printStep()
91+
printPrefixed("Enter a password to encrypt the derived private key with.")
92+
encryptionPw, err = client.GetString("", stdin)
93+
if err != nil {
94+
return err
95+
}
96+
97+
if encryptionPw == "" {
98+
return errors.New("you must define an encryption password")
99+
}
100+
101+
printStep()
102+
103+
params, err := hd.ParamsFromString(bip44Path)
104+
if err != nil {
105+
return err
106+
}
107+
_, err = kb.Derive(name, mnemonic, encryptionPw, bip39Pw, *params)
108+
if err != nil {
109+
return err
110+
}
111+
fmt.Printf("Mnemonic: %s\n", mnemonic)
112+
fmt.Printf("Successfully wrote encrypted priv key named %s\n", name)
113+
114+
return nil
115+
}
116+
117+
func printPrefixed(msg string) {
118+
fmt.Printf("> %s\n", msg)
119+
}
120+
121+
func printStep() {
122+
printPrefixed("-------------------------------------")
123+
}

client/keys/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ func Commands() *cobra.Command {
2525
client.LineBreak,
2626
deleteKeyCommand(),
2727
updateKeyCommand(),
28+
mnemonicCommand(),
29+
newCommand(),
2830
)
2931
return cmd
3032
}

cmd/gaia/cli_test/cli_test.go

+35
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/cosmos/cosmos-sdk/x/auth"
2323
"github.com/cosmos/cosmos-sdk/x/gov"
2424
"github.com/cosmos/cosmos-sdk/x/stake"
25+
"strings"
2526
)
2627

2728
var (
@@ -276,6 +277,31 @@ func TestGaiaCLISubmitProposal(t *testing.T) {
276277
require.Equal(t, " 2 - Apples", proposalsQuery)
277278
}
278279

280+
func TestGaiaCLIKeysNew(t *testing.T) {
281+
keyName := "testKey"
282+
tests.ExecuteT(t, fmt.Sprintf("gaiad --home=%s unsafe-reset-all", gaiadHome), "")
283+
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s foo", gaiacliHome), app.DefaultKeyPass)
284+
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s bar", gaiacliHome), app.DefaultKeyPass)
285+
286+
// happy path with defaults
287+
executeInit(t, fmt.Sprintf("gaiad init -o --name=foo --home=%s --home-client=%s", gaiadHome, gaiacliHome))
288+
executeWrite(t, fmt.Sprintf("gaiacli --home=%s keys new --default=true %s", gaiacliHome, keyName))
289+
keyAddr, _ := executeGetAddrPK(t, fmt.Sprintf("gaiacli keys show %s --output=json --home=%s", keyName, gaiacliHome))
290+
require.NotNil(t, keyAddr)
291+
}
292+
293+
func TestGaiaCLIMnemonic(t *testing.T) {
294+
tests.ExecuteT(t, fmt.Sprintf("gaiad --home=%s unsafe-reset-all", gaiadHome), "")
295+
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s foo", gaiacliHome), app.DefaultKeyPass)
296+
executeWrite(t, fmt.Sprintf("gaiacli keys delete --home=%s bar", gaiacliHome), app.DefaultKeyPass)
297+
executeMnemonic(t, fmt.Sprintf("gaiacli keys mnemonic"), "", "")
298+
executeMnemonic(t,
299+
fmt.Sprintf("gaiacli keys mnemonic --user"),
300+
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
301+
"donor anxiety expect little beef pass agree choice donor anxiety expect little beef pass agree choice donor anxiety expect little beef pass agree cattle",
302+
)
303+
}
304+
279305
//___________________________________________________________________________________
280306
// helper methods
281307

@@ -428,3 +454,12 @@ func executeGetVotes(t *testing.T, cmdStr string) []gov.Vote {
428454
require.NoError(t, err, "out %v\n, err %v", out, err)
429455
return votes
430456
}
457+
458+
func executeMnemonic(t *testing.T, cmdStr string, entropy string, expectedMnemonic string) {
459+
out := tests.ExecuteT(t, cmdStr, entropy)
460+
require.True(t, len(strings.Split(out, " ")) > 24)
461+
462+
if expectedMnemonic != "" {
463+
require.Contains(t, out, expectedMnemonic)
464+
}
465+
}

0 commit comments

Comments
 (0)