Ethermint key generation (#78)

* WIP setting up Ethereum key CLI commands

* Functional key gen and showing Ethereum address

* Cleaned up changes

* WIP setting up Ethereum key CLI commands

* Functional key gen and showing Ethereum address

* Cleaned up changes

* Changed address to cosmos specific address

* Remove default bech32 prefixes and add basic add command test

* Changed Private key type to slice of bytes for compatibility and storability

* switch back to using cosmos crypto Keybase interfaces

* Changed key output to ethereum addressing instead of bitcoin and key generation to allow seeding from mnemonic and bip39 password

* Updated show command and added test

* Remove prefix requirement for showing keys and added existing keys commands to CLI temporarily

* Removed unnecessary duplicate code

* Readd prefixes for accounts temporarily

* Fix linting issue

* Remove TODO for setting PK to specific length of bytes (all functions use slice)

* Cleaned up descriptions to remove multi-sigs
This commit is contained in:
Austin Abell
2019-08-11 10:42:46 -04:00
committed by GitHub
parent 92dc7d9a59
commit cfac906f92
22 changed files with 1742 additions and 18 deletions
+258
View File
@@ -0,0 +1,258 @@
package keys
import (
"bufio"
"errors"
"fmt"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/client/input"
clientkeys "github.com/cosmos/cosmos-sdk/client/keys"
cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/ethermint/crypto/keys"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/cosmos/go-bip39"
"github.com/tendermint/tendermint/libs/cli"
)
const (
flagInteractive = "interactive"
flagRecover = "recover"
flagNoBackup = "no-backup"
// flagDryRun = "dry-run"
flagAccount = "account"
flagIndex = "index"
// flagNoSort = "nosort"
// DefaultKeyPass contains the default key password for genesis transactions
DefaultKeyPass = "12345678"
mnemonicEntropySize = 256
)
func addKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "add <name>",
Short: "Add an encrypted private key (either newly generated or recovered), encrypt it, and save to disk",
Long: `Derive a new private key and encrypt to disk.
Optionally specify a BIP39 mnemonic, a BIP39 passphrase to further secure the mnemonic,
and a bip32 HD path to derive a specific account. The key will be stored under the given name
and encrypted with the given password. The only input that is required is the encryption password.
If run with -i, it will prompt the user for BIP44 path, BIP39 mnemonic, and passphrase.
The flag --recover allows one to recover a key from a seed passphrase.
If run with --dry-run, a key would be generated (or recovered) but not stored to the
local keystore.
Use the --pubkey flag to add arbitrary public keys to the keystore for constructing
multisig transactions.
`,
Args: cobra.ExactArgs(1),
RunE: runAddCmd,
}
// cmd.Flags().StringSlice(flagMultisig, nil, "Construct and store a multisig public key (implies --pubkey)")
// cmd.Flags().Uint(flagMultiSigThreshold, 1, "K out of N required signatures. For use in conjunction with --multisig")
// cmd.Flags().Bool(flagNoSort, false, "Keys passed to --multisig are taken in the order they're supplied")
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(flags.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device")
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")
cmd.Flags().Uint32(flagAccount, 0, "Account number for HD derivation")
cmd.Flags().Uint32(flagIndex, 0, "Address index number for HD derivation")
cmd.Flags().Bool(flags.FlagIndentResponse, false, "Add indent to JSON response")
return cmd
}
/*
input
- bip39 mnemonic
- bip39 passphrase
- bip44 path
- local encryption password
output
- armor encrypted private key (saved to file)
*/
func runAddCmd(cmd *cobra.Command, args []string) error {
var kb cosmosKeys.Keybase
var err error
var encryptPassword string
inBuf := bufio.NewReader(cmd.InOrStdin())
name := args[0]
interactive := viper.GetBool(flagInteractive)
showMnemonic := !viper.GetBool(flagNoBackup)
kb, err = clientkeys.NewKeyBaseFromHomeFlag()
if err != nil {
return err
}
_, err = kb.Get(name)
if err == nil {
// account exists, ask for user confirmation
response, err2 := input.GetConfirmation(fmt.Sprintf("override the existing name %s", name), inBuf)
if err2 != nil {
return err2
}
if !response {
return errors.New("aborted")
}
}
// ask for a password when generating a local key
if viper.GetString(FlagPublicKey) == "" && !viper.GetBool(flags.FlagUseLedger) {
encryptPassword, err = input.GetCheckPassword(
"Enter a passphrase to encrypt your key to disk:",
"Repeat the passphrase:", inBuf)
if err != nil {
return err
}
}
// }
if viper.GetString(FlagPublicKey) != "" {
pk, err := sdk.GetAccPubKeyBech32(viper.GetString(FlagPublicKey))
if err != nil {
return err
}
_, err = kb.CreateOffline(name, pk)
if err != nil {
return err
}
return nil
}
account := uint32(viper.GetInt(flagAccount))
index := uint32(viper.GetInt(flagIndex))
// // If we're using ledger, only thing we need is the path and the bech32 prefix.
// if viper.GetBool(flags.FlagUseLedger) {
// bech32PrefixAccAddr := sdk.GetConfig().GetBech32AccountAddrPrefix()
// info, err := kb.CreateLedger(name, keys.Secp256k1, bech32PrefixAccAddr, account, index)
// if err != nil {
// return err
// }
// return printCreate(cmd, info, false, "")
// }
// 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."
}
mnemonic, err = input.GetString(bip39Message, inBuf)
if err != nil {
return err
}
if !bip39.IsMnemonicValid(mnemonic) {
return errors.New("invalid mnemonic")
}
}
if len(mnemonic) == 0 {
// read entropy seed straight from crypto.Rand and convert to mnemonic
entropySeed, err := bip39.NewEntropy(mnemonicEntropySize)
if err != nil {
return err
}
mnemonic, err = bip39.NewMnemonic(entropySeed[:])
if err != nil {
return err
}
}
// override bip39 passphrase
if interactive {
bip39Passphrase, err = input.GetString(
"Enter your bip39 passphrase. This is combined with the mnemonic to derive the seed. "+
"Most users should just hit enter to use the default, \"\"", inBuf)
if err != nil {
return err
}
// if they use one, make them re-enter it
if len(bip39Passphrase) != 0 {
p2, err := input.GetString("Repeat the passphrase:", inBuf)
if err != nil {
return err
}
if bip39Passphrase != p2 {
return errors.New("passphrases don't match")
}
}
}
info, err := kb.CreateAccount(name, mnemonic, bip39Passphrase, encryptPassword, account, index)
if err != nil {
return err
}
// Recover key from seed passphrase
if viper.GetBool(flagRecover) {
// Hide mnemonic from output
showMnemonic = false
mnemonic = ""
}
return printCreate(cmd, info, showMnemonic, mnemonic)
}
func printCreate(cmd *cobra.Command, info cosmosKeys.Info, showMnemonic bool, mnemonic string) error {
output := viper.Get(cli.OutputFlag)
switch output {
case clientkeys.OutputFormatText:
cmd.PrintErrln()
printKeyInfo(info, keys.Bech32KeyOutput)
// print mnemonic unless requested not to.
if showMnemonic {
cmd.PrintErrln("\n**Important** write this mnemonic phrase in a safe place.")
cmd.PrintErrln("It is the only way to recover your account if you ever forget your password.")
cmd.PrintErrln("")
cmd.PrintErrln(mnemonic)
}
case clientkeys.OutputFormatJSON:
out, err := keys.Bech32KeyOutput(info)
if err != nil {
return err
}
if showMnemonic {
out.Mnemonic = mnemonic
}
var jsonString []byte
if viper.GetBool(flags.FlagIndentResponse) {
jsonString, err = cdc.MarshalJSONIndent(out, "", " ")
} else {
jsonString, err = cdc.MarshalJSON(out)
}
if err != nil {
return err
}
cmd.PrintErrln(string(jsonString))
default:
return fmt.Errorf("I can't speak: %s", output)
}
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package keys
import (
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/tendermint/tendermint/libs/cli"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/tests"
)
func TestAddCommandBasic(t *testing.T) {
cmd := addKeyCommand()
assert.NotNil(t, cmd)
mockIn, _, _ := tests.ApplyMockIO(cmd)
kbHome, kbCleanUp := tests.NewTestCaseDir(t)
assert.NotNil(t, kbHome)
defer kbCleanUp()
viper.Set(flags.FlagHome, kbHome)
viper.Set(cli.OutputFlag, OutputFormatText)
mockIn.Reset("test1234\ntest1234\n")
err := runAddCmd(cmd, []string{"keyname1"})
assert.NoError(t, err)
viper.Set(cli.OutputFlag, OutputFormatText)
mockIn.Reset("test1234\ntest1234\n")
err = runAddCmd(cmd, []string{"keyname1"})
assert.Error(t, err)
viper.Set(cli.OutputFlag, OutputFormatText)
mockIn.Reset("y\ntest1234\ntest1234\n")
err = runAddCmd(cmd, []string{"keyname1"})
assert.NoError(t, err)
viper.Set(cli.OutputFlag, OutputFormatJSON)
mockIn.Reset("test1234\ntest1234\n")
err = runAddCmd(cmd, []string{"keyname2"})
assert.NoError(t, err)
}
+23
View File
@@ -0,0 +1,23 @@
package keys
import (
"github.com/cosmos/cosmos-sdk/codec"
)
var cdc *codec.Codec
func init() {
cdc = codec.New()
codec.RegisterCrypto(cdc)
cdc.Seal()
}
// marshal keys
func MarshalJSON(o interface{}) ([]byte, error) {
return cdc.MarshalJSON(o)
}
// unmarshal json
func UnmarshalJSON(bz []byte, ptr interface{}) error {
return cdc.UnmarshalJSON(bz, ptr)
}
+34
View File
@@ -0,0 +1,34 @@
package keys
import (
"github.com/spf13/cobra"
"github.com/cosmos/cosmos-sdk/client/flags"
)
// Commands registers a sub-tree of commands to interact with
// local private key storage.
func Commands() *cobra.Command {
cmd := &cobra.Command{
Use: "emintkeys",
Short: "Add or view local private keys",
Long: `Keys allows you to manage your local keystore for tendermint.
These keys may be in any format supported by go-crypto and can be
used by light-clients, full nodes, or any other application that
needs to sign with a private key.`,
}
cmd.AddCommand(
// mnemonicKeyCommand(),
addKeyCommand(),
// exportKeyCommand(),
// importKeyCommand(),
// listKeysCmd(),
showKeysCmd(),
flags.LineBreak,
// deleteKeyCommand(),
// updateKeyCommand(),
// parseKeyStringCommand(),
)
return cmd
}
+124
View File
@@ -0,0 +1,124 @@
package keys
import (
"errors"
"fmt"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/tendermint/libs/cli"
"github.com/cosmos/cosmos-sdk/client/flags"
"github.com/cosmos/cosmos-sdk/crypto"
cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys"
sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/ethermint/crypto/keys"
)
const (
// FlagAddress is the flag for the user's address on the command line.
FlagAddress = "address"
// FlagPublicKey represents the user's public key on the command line.
FlagPublicKey = "pubkey"
// FlagBechPrefix defines a desired Bech32 prefix encoding for a key.
FlagBechPrefix = "bech"
// FlagDevice indicates that the information should be shown in the device
FlagDevice = "device"
)
func showKeysCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "show <name>",
Short: "Show key info for the given name",
Long: `Return public details of a single local key.`,
Args: cobra.MinimumNArgs(1),
RunE: runShowCmd,
}
cmd.Flags().String(FlagBechPrefix, sdk.PrefixAccount, "The Bech32 prefix encoding for a key (acc|val|cons)")
cmd.Flags().BoolP(FlagAddress, "a", false, "Output the address only (overrides --output)")
cmd.Flags().BoolP(FlagPublicKey, "p", false, "Output the public key only (overrides --output)")
cmd.Flags().BoolP(FlagDevice, "d", false, "Output the address in a ledger device")
cmd.Flags().Bool(flags.FlagIndentResponse, false, "Add indent to JSON response")
return cmd
}
func runShowCmd(cmd *cobra.Command, args []string) (err error) {
var info cosmosKeys.Info
if len(args) == 1 {
info, err = GetKeyInfo(args[0])
if err != nil {
return err
}
} else {
return errors.New("Must provide only one name")
}
isShowAddr := viper.GetBool(FlagAddress)
isShowPubKey := viper.GetBool(FlagPublicKey)
isShowDevice := viper.GetBool(FlagDevice)
isOutputSet := false
tmp := cmd.Flag(cli.OutputFlag)
if tmp != nil {
isOutputSet = tmp.Changed
}
if isShowAddr && isShowPubKey {
return errors.New("cannot use both --address and --pubkey at once")
}
if isOutputSet && (isShowAddr || isShowPubKey) {
return errors.New("cannot use --output with --address or --pubkey")
}
keyOutputFunction := keys.Bech32KeyOutput
switch {
case isShowAddr:
printKeyAddress(info, keyOutputFunction)
case isShowPubKey:
printPubKey(info, keyOutputFunction)
default:
printKeyInfo(info, keyOutputFunction)
}
if isShowDevice {
if isShowPubKey {
return fmt.Errorf("the device flag (-d) can only be used for addresses not pubkeys")
}
if viper.GetString(FlagBechPrefix) != "acc" {
return fmt.Errorf("the device flag (-d) can only be used for accounts")
}
// Override and show in the device
if info.GetType() != cosmosKeys.TypeLedger {
return fmt.Errorf("the device flag (-d) can only be used for accounts stored in devices")
}
hdpath, err := info.GetPath()
if err != nil {
return nil
}
return crypto.LedgerShowAddress(*hdpath, info.GetPubKey())
}
return nil
}
// TODO: Determine if the different prefixes are necessary for ethermint
// func getBechKeyOut(bechPrefix string) (bechKeyOutFn, error) {
// switch bechPrefix {
// case sdk.PrefixAccount:
// return keys.Bech32KeyOutput, nil
// case sdk.PrefixValidator:
// return keys.Bech32ValKeyOutput, nil
// case sdk.PrefixConsensus:
// return keys.Bech32ConsKeyOutput, nil
// }
// return nil, fmt.Errorf("invalid Bech32 prefix encoding provided: %s", bechPrefix)
// }
+52
View File
@@ -0,0 +1,52 @@
package keys
import (
"testing"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/cosmos/cosmos-sdk/client/flags"
clientkeys "github.com/cosmos/cosmos-sdk/client/keys"
"github.com/cosmos/cosmos-sdk/tests"
)
func TestShowKeysCmd(t *testing.T) {
cmd := showKeysCmd()
assert.NotNil(t, cmd)
assert.Equal(t, "false", cmd.Flag(FlagAddress).DefValue)
assert.Equal(t, "false", cmd.Flag(FlagPublicKey).DefValue)
}
func TestRunShowCmd(t *testing.T) {
cmd := showKeysCmd()
err := runShowCmd(cmd, []string{"invalid"})
assert.EqualError(t, err, "Key invalid not found")
// Prepare a key base
// Now add a temporary keybase
kbHome, cleanUp := tests.NewTestCaseDir(t)
defer cleanUp()
viper.Set(flags.FlagHome, kbHome)
fakeKeyName1 := "runShowCmd_Key1"
fakeKeyName2 := "runShowCmd_Key2"
kb, err := clientkeys.NewKeyBaseFromHomeFlag()
assert.NoError(t, err)
_, err = kb.CreateAccount(fakeKeyName1, tests.TestMnemonic, "", "", 0, 0)
assert.NoError(t, err)
_, err = kb.CreateAccount(fakeKeyName2, tests.TestMnemonic, "", "", 0, 1)
assert.NoError(t, err)
// // Now try single key
// err = runShowCmd(cmd, []string{fakeKeyName1})
// assert.EqualError(t, err, "invalid Bech32 prefix encoding provided: ")
// // Now try single key - set bech to acc
// viper.Set(FlagBechPrefix, sdk.PrefixAccount)
err = runShowCmd(cmd, []string{fakeKeyName1})
assert.NoError(t, err)
err = runShowCmd(cmd, []string{fakeKeyName2})
assert.NoError(t, err)
}
+112
View File
@@ -0,0 +1,112 @@
package keys
import (
"fmt"
"github.com/spf13/viper"
"github.com/tendermint/tendermint/libs/cli"
"gopkg.in/yaml.v2"
"github.com/cosmos/cosmos-sdk/client/flags"
clientkeys "github.com/cosmos/cosmos-sdk/client/keys"
cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys"
)
// available output formats.
const (
OutputFormatText = "text"
OutputFormatJSON = "json"
)
type bechKeyOutFn func(keyInfo cosmosKeys.Info) (cosmosKeys.KeyOutput, error)
// GetKeyInfo returns key info for a given name. An error is returned if the
// keybase cannot be retrieved or getting the info fails.
func GetKeyInfo(name string) (cosmosKeys.Info, error) {
keybase, err := clientkeys.NewKeyBaseFromHomeFlag()
if err != nil {
return nil, err
}
return keybase.Get(name)
}
func printKeyInfo(keyInfo cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
ko, err := bechKeyOut(keyInfo)
if err != nil {
panic(err)
}
switch viper.Get(cli.OutputFlag) {
case OutputFormatText:
printTextInfos([]cosmosKeys.KeyOutput{ko})
case OutputFormatJSON:
var out []byte
var err error
if viper.GetBool(flags.FlagIndentResponse) {
out, err = cdc.MarshalJSONIndent(ko, "", " ")
} else {
out, err = cdc.MarshalJSON(ko)
}
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
}
// func printInfos(infos []keys.Info) {
// kos, err := keys.Bech32KeysOutput(infos)
// if err != nil {
// panic(err)
// }
// switch viper.Get(cli.OutputFlag) {
// case OutputFormatText:
// printTextInfos(kos)
// case OutputFormatJSON:
// var out []byte
// var err error
// if viper.GetBool(flags.FlagIndentResponse) {
// out, err = cdc.MarshalJSONIndent(kos, "", " ")
// } else {
// out, err = cdc.MarshalJSON(kos)
// }
// if err != nil {
// panic(err)
// }
// fmt.Printf("%s", out)
// }
// }
func printTextInfos(kos []cosmosKeys.KeyOutput) {
out, err := yaml.Marshal(&kos)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
func printKeyAddress(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
ko, err := bechKeyOut(info)
if err != nil {
panic(err)
}
fmt.Println(ko.Address)
}
func printPubKey(info cosmosKeys.Info, bechKeyOut bechKeyOutFn) {
ko, err := bechKeyOut(info)
if err != nil {
panic(err)
}
fmt.Println(ko.PubKey)
}