From cfac906f9248bd517bbe3404684ca16aa9d1e9d6 Mon Sep 17 00:00:00 2001 From: Austin Abell Date: Sun, 11 Aug 2019 10:42:46 -0400 Subject: [PATCH] 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 --- app/test_utils.go | 2 +- cmd/emintcli/main.go | 11 +- cmd/emintd/main.go | 1 + crypto/codec.go | 4 +- crypto/keys/codec.go | 25 ++ crypto/keys/keybase.go | 516 ++++++++++++++++++++++++++++++++++++ crypto/keys/keys.go | 13 + crypto/keys/lazy_keybase.go | 222 ++++++++++++++++ crypto/keys/output.go | 85 ++++++ crypto/keys/types.go | 156 +++++++++++ crypto/keys/types_test.go | 46 ++++ crypto/secp256k1.go | 21 +- crypto/secp256k1_test.go | 2 +- go.mod | 3 +- keys/add.go | 258 ++++++++++++++++++ keys/add_test.go | 48 ++++ keys/codec.go | 23 ++ keys/root.go | 34 +++ keys/show.go | 124 +++++++++ keys/show_test.go | 52 ++++ keys/utils.go | 112 ++++++++ x/evm/types/utils.go | 2 +- 22 files changed, 1742 insertions(+), 18 deletions(-) create mode 100644 crypto/keys/codec.go create mode 100644 crypto/keys/keybase.go create mode 100644 crypto/keys/keys.go create mode 100644 crypto/keys/lazy_keybase.go create mode 100644 crypto/keys/output.go create mode 100644 crypto/keys/types.go create mode 100644 crypto/keys/types_test.go create mode 100644 keys/add.go create mode 100644 keys/add_test.go create mode 100644 keys/codec.go create mode 100644 keys/root.go create mode 100644 keys/show.go create mode 100644 keys/show_test.go create mode 100644 keys/utils.go diff --git a/app/test_utils.go b/app/test_utils.go index 31eb4a8d..4a630492 100644 --- a/app/test_utils.go +++ b/app/test_utils.go @@ -90,7 +90,7 @@ func newTestStdFee() auth.StdFee { // GenerateAddress generates an Ethereum address. func newTestAddrKey() (sdk.AccAddress, tmcrypto.PrivKey) { privkey, _ := crypto.GenerateKey() - addr := ethcrypto.PubkeyToAddress(privkey.PublicKey) + addr := ethcrypto.PubkeyToAddress(privkey.ToECDSA().PublicKey) return sdk.AccAddress(addr.Bytes()), privkey } diff --git a/cmd/emintcli/main.go b/cmd/emintcli/main.go index abac4fec..a09ee935 100644 --- a/cmd/emintcli/main.go +++ b/cmd/emintcli/main.go @@ -1,15 +1,17 @@ package main import ( - "github.com/cosmos/ethermint/rpc" - "github.com/tendermint/go-amino" "os" "path" - "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/keys" + "github.com/cosmos/ethermint/rpc" + "github.com/tendermint/go-amino" + + "github.com/cosmos/cosmos-sdk/client" sdkrpc "github.com/cosmos/cosmos-sdk/client/rpc" sdk "github.com/cosmos/cosmos-sdk/types" + emintkeys "github.com/cosmos/ethermint/keys" emintapp "github.com/cosmos/ethermint/app" "github.com/spf13/cobra" @@ -24,6 +26,7 @@ func main() { // Read in the configuration file for the sdk config := sdk.GetConfig() + // TODO: Remove or change prefix if usable to generate Ethereum address config.SetBech32PrefixForAccount(sdk.Bech32PrefixAccAddr, sdk.Bech32PrefixAccPub) config.SetBech32PrefixForValidator(sdk.Bech32PrefixValAddr, sdk.Bech32PrefixValPub) config.SetBech32PrefixForConsensusNode(sdk.Bech32PrefixConsAddr, sdk.Bech32PrefixConsPub) @@ -49,7 +52,9 @@ func main() { // TODO: Set up rest routes (if included, different from web3 api) rpc.Web3RpcCmd(cdc), client.LineBreak, + // TODO: Remove these commands once ethermint keys and genesis set up keys.Commands(), + emintkeys.Commands(), client.LineBreak, ) diff --git a/cmd/emintd/main.go b/cmd/emintd/main.go index acb9ad31..83626d5c 100644 --- a/cmd/emintd/main.go +++ b/cmd/emintd/main.go @@ -28,6 +28,7 @@ func main() { cdc := emintapp.MakeCodec() config := sdk.GetConfig() + // TODO: Remove or change prefix if usable to generate Ethereum address config.SetBech32PrefixForAccount(sdk.Bech32PrefixAccAddr, sdk.Bech32PrefixAccPub) config.SetBech32PrefixForValidator(sdk.Bech32PrefixValAddr, sdk.Bech32PrefixValPub) config.SetBech32PrefixForConsensusNode(sdk.Bech32PrefixConsAddr, sdk.Bech32PrefixConsPub) diff --git a/crypto/codec.go b/crypto/codec.go index 3c827a6b..219585e2 100644 --- a/crypto/codec.go +++ b/crypto/codec.go @@ -1,6 +1,8 @@ package crypto -import "github.com/cosmos/cosmos-sdk/codec" +import ( + "github.com/cosmos/cosmos-sdk/codec" +) var cryptoCodec = codec.New() diff --git a/crypto/keys/codec.go b/crypto/keys/codec.go new file mode 100644 index 00000000..ed71ff51 --- /dev/null +++ b/crypto/keys/codec.go @@ -0,0 +1,25 @@ +package keys + +import ( + cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" + + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + emintCrypto "github.com/cosmos/ethermint/crypto" + cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +var cdc *codec.Codec + +func init() { + cdc = codec.New() + cryptoAmino.RegisterAmino(cdc) + cdc.RegisterInterface((*cosmosKeys.Info)(nil), nil) + emintCrypto.RegisterCodec(cdc) + cdc.RegisterConcrete(hd.BIP44Params{}, "crypto/keys/hd/BIP44Params", nil) + cdc.RegisterConcrete(localInfo{}, "crypto/keys/localInfo", nil) + cdc.RegisterConcrete(ledgerInfo{}, "crypto/keys/ledgerInfo", nil) + cdc.RegisterConcrete(offlineInfo{}, "crypto/keys/offlineInfo", nil) + // cdc.RegisterConcrete(multiInfo{}, "crypto/keys/multiInfo", nil) + cdc.Seal() +} diff --git a/crypto/keys/keybase.go b/crypto/keys/keybase.go new file mode 100644 index 00000000..c9c0e430 --- /dev/null +++ b/crypto/keys/keybase.go @@ -0,0 +1,516 @@ +package keys + +import ( + "bufio" + "fmt" + "os" + "reflect" + "strings" + + "github.com/pkg/errors" + + "github.com/cosmos/cosmos-sdk/crypto" + cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" + "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" + + bip39 "github.com/cosmos/go-bip39" + + emintCrypto "github.com/cosmos/ethermint/crypto" + tmcrypto "github.com/tendermint/tendermint/crypto" + cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" + dbm "github.com/tendermint/tendermint/libs/db" +) + +var _ cosmosKeys.Keybase = dbKeybase{} + +// Language is a language to create the BIP 39 mnemonic in. +// Currently, only english is supported though. +// 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. + English Language = iota + 1 + // Japanese is currently not supported. + Japanese + // Korean is currently not supported. + Korean + // Spanish is currently not supported. + Spanish + // ChineseSimplified is currently not supported. + ChineseSimplified + // ChineseTraditional is currently not supported. + ChineseTraditional + // French is currently not supported. + French + // Italian is currently not supported. + Italian + addressSuffix = "address" + infoSuffix = "info" +) + +const ( + // used for deriving seed from mnemonic + DefaultBIP39Passphrase = "" + + // bits of entropy to draw when creating a mnemonic + defaultEntropySize = 256 +) + +var ( + // ErrUnsupportedSigningAlgo is raised when the caller tries to use a + // different signing scheme than secp256k1. + ErrUnsupportedSigningAlgo = errors.New("unsupported signing algo: only secp256k1 is supported") + + // ErrUnsupportedLanguage is raised when the caller tries to use a + // different language than english for creating a mnemonic sentence. + ErrUnsupportedLanguage = errors.New("unsupported language: only english is supported") +) + +// dbKeybase combines encryption and storage implementation to provide +// a full-featured key manager +type dbKeybase struct { + db dbm.DB +} + +// newDbKeybase creates a new keybase instance using the passed DB for reading and writing keys. +func newDbKeybase(db dbm.DB) cosmosKeys.Keybase { + return dbKeybase{ + db: db, + } +} + +// NewInMemory creates a transient keybase on top of in-memory storage +// instance useful for testing purposes and on-the-fly key generation. +func NewInMemory() cosmosKeys.Keybase { return dbKeybase{dbm.NewMemDB()} } + +// CreateMnemonic generates a new key and persists it to storage, encrypted +// using the provided password. +// It returns the generated mnemonic and the key Info. +// It returns an error if it fails to +// generate a key for the given algo type, or if another key is +// already stored under the same name. +func (kb dbKeybase) CreateMnemonic(name string, language cosmosKeys.Language, passwd string, algo cosmosKeys.SigningAlgo) (info cosmosKeys.Info, mnemonic string, err error) { + if language != cosmosKeys.English { + return nil, "", ErrUnsupportedLanguage + } + if algo != Secp256k1 { + err = ErrUnsupportedSigningAlgo + return + } + + // default number of words (24): + // this generates a mnemonic directly from the number of words by reading system entropy. + entropy, err := bip39.NewEntropy(defaultEntropySize) + if err != nil { + return + } + mnemonic, err = bip39.NewMnemonic(entropy) + if err != nil { + return + } + + seed := bip39.NewSeed(mnemonic, DefaultBIP39Passphrase) + fullFundraiserPath := types.GetConfig().GetFullFundraiserPath() + info, err = kb.persistDerivedKey(seed, passwd, name, 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) (cosmosKeys.Info, error) { + coinType := types.GetConfig().GetCoinType() + hdPath := hd.NewFundraiserParams(account, coinType, index) + return kb.Derive(name, mnemonic, bip39Passwd, encryptPasswd, *hdPath) +} + +func (kb dbKeybase) Derive(name, mnemonic, bip39Passphrase, encryptPasswd string, params hd.BIP44Params) (info cosmosKeys.Info, err error) { + seed, err := bip39.NewSeedWithErrorChecking(mnemonic, bip39Passphrase) + if err != nil { + return + } + + info, err = kb.persistDerivedKey(seed, encryptPasswd, name, params.String()) + return info, err +} + +// 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, algo cosmosKeys.SigningAlgo, hrp string, account, index uint32) (cosmosKeys.Info, error) { + if algo != Secp256k1 { + return nil, ErrUnsupportedSigningAlgo + } + + coinType := types.GetConfig().GetCoinType() + hdPath := hd.NewFundraiserParams(account, coinType, index) + priv, _, err := crypto.NewPrivKeyLedgerSecp256k1(*hdPath, hrp) + if err != nil { + return nil, err + } + pub := priv.PubKey() + + // Note: Once Cosmos App v1.3.1 is compulsory, it could be possible to check that pubkey and addr match + return kb.writeLedgerKey(name, pub, *hdPath), nil +} + +// CreateOffline creates a new reference to an offline keypair. It returns the +// created key info. +func (kb dbKeybase) CreateOffline(name string, pub tmcrypto.PubKey) (cosmosKeys.Info, error) { + return kb.writeOfflineKey(name, pub), nil +} + +// CreateMulti creates a new reference to a multisig (offline) keypair. It +// returns the created key info. +func (kb dbKeybase) CreateMulti(name string, pub tmcrypto.PubKey) (cosmosKeys.Info, error) { + return nil, nil +} + +func (kb *dbKeybase) persistDerivedKey(seed []byte, passwd, name, fullHdPath string) (info cosmosKeys.Info, err error) { + // create master key and derive first key: + masterPriv, ch := hd.ComputeMastersFromSeed(seed) + derivedPriv, err := hd.DerivePrivateKeyForPath(masterPriv, ch, fullHdPath) + if err != nil { + return + } + + // if we have a password, use it to encrypt the private key and store it + // else store the public key only + if passwd != "" { + info = kb.writeLocalKey(name, emintCrypto.PrivKeySecp256k1(derivedPriv[:]), passwd) + } else { + pubk := emintCrypto.PrivKeySecp256k1(derivedPriv[:]).PubKey() + info = kb.writeOfflineKey(name, pubk) + } + return info, nil +} + +// List returns the keys from storage in alphabetical order. +func (kb dbKeybase) List() ([]cosmosKeys.Info, error) { + var res []cosmosKeys.Info + iter := kb.db.Iterator(nil, nil) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + key := string(iter.Key()) + + // need to include only keys in storage that have an info suffix + if strings.HasSuffix(key, infoSuffix) { + info, err := readInfo(iter.Value()) + if err != nil { + return nil, err + } + res = append(res, info) + } + } + return res, nil +} + +// Get returns the public information about one key. +func (kb dbKeybase) Get(name string) (cosmosKeys.Info, error) { + bs := kb.db.Get(infoKey(name)) + if len(bs) == 0 { + return nil, keyerror.NewErrKeyNotFound(name) + } + return readInfo(bs) +} + +func (kb dbKeybase) GetByAddress(address types.AccAddress) (cosmosKeys.Info, error) { + ik := kb.db.Get(addrKey(address)) + if len(ik) == 0 { + return nil, fmt.Errorf("key with address %s not found", address) + } + bs := kb.db.Get(ik) + return readInfo(bs) +} + +// Sign signs the msg with the named key. +// It returns an error if the key doesn't exist or the decryption fails. +func (kb dbKeybase) Sign(name, passphrase string, msg []byte) (sig []byte, pub tmcrypto.PubKey, err error) { + info, err := kb.Get(name) + if err != nil { + return + } + + var priv tmcrypto.PrivKey + + switch info.(type) { + case localInfo: + linfo := info.(localInfo) + if linfo.PrivKeyArmor == "" { + err = fmt.Errorf("private key not available") + return + } + + priv, err = mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) + if err != nil { + return nil, nil, err + } + + case ledgerInfo: + linfo := info.(ledgerInfo) + priv, err = crypto.NewPrivKeyLedgerSecp256k1Unsafe(linfo.Path) + if err != nil { + return + } + + // case offlineInfo, multiInfo: + case offlineInfo: + _, err := fmt.Fprintf(os.Stderr, "Message to sign:\n\n%s\n", msg) + if err != nil { + return nil, nil, err + } + + buf := bufio.NewReader(os.Stdin) + _, err = fmt.Fprintf(os.Stderr, "\nEnter Amino-encoded signature:\n") + if err != nil { + return nil, nil, err + } + + // Will block until user inputs the signature + signed, err := buf.ReadString('\n') + if err != nil { + return nil, nil, err + } + + if err := cdc.UnmarshalBinaryLengthPrefixed([]byte(signed), sig); err != nil { + return nil, nil, errors.Wrap(err, "failed to decode signature") + } + + return sig, info.GetPubKey(), nil + } + + sig, err = priv.Sign(msg) + if err != nil { + return nil, nil, err + } + + pub = priv.PubKey() + return sig, pub, nil +} + +func (kb dbKeybase) ExportPrivateKeyObject(name string, passphrase string) (tmcrypto.PrivKey, error) { + info, err := kb.Get(name) + if err != nil { + return nil, err + } + + var priv tmcrypto.PrivKey + + switch info.(type) { + case localInfo: + linfo := info.(localInfo) + if linfo.PrivKeyArmor == "" { + err = fmt.Errorf("private key not available") + return nil, err + } + priv, err = mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase) + if err != nil { + return nil, err + } + + // case ledgerInfo, offlineInfo, multiInfo: + case ledgerInfo, offlineInfo: + return nil, errors.New("only works on local private keys") + } + + return priv, nil +} + +func (kb dbKeybase) Export(name string) (armor string, err error) { + bz := kb.db.Get(infoKey(name)) + if bz == nil { + return "", fmt.Errorf("no key to export with name %s", name) + } + return mintkey.ArmorInfoBytes(bz), nil +} + +// ExportPubKey returns public keys in ASCII armored format. +// Retrieve a Info object by its name and return the public key in +// a portable format. +func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) { + bz := kb.db.Get(infoKey(name)) + if bz == nil { + return "", fmt.Errorf("no key to export with name %s", name) + } + info, err := readInfo(bz) + if err != nil { + return + } + return mintkey.ArmorPubKeyBytes(info.GetPubKey().Bytes()), nil +} + +// ExportPrivKey returns a private key in ASCII armored format. +// It returns an error if the key does not exist or a wrong encryption passphrase is supplied. +func (kb dbKeybase) ExportPrivKey(name string, decryptPassphrase string, + encryptPassphrase string) (armor string, err error) { + priv, err := kb.ExportPrivateKeyObject(name, decryptPassphrase) + if err != nil { + return "", err + } + + return mintkey.EncryptArmorPrivKey(priv, encryptPassphrase), nil +} + +// ImportPrivKey imports a private key in ASCII armor format. +// It returns an error if a key with the same name exists or a wrong encryption passphrase is +// supplied. +func (kb dbKeybase) ImportPrivKey(name string, armor string, passphrase string) error { + if _, err := kb.Get(name); err == nil { + return errors.New("Cannot overwrite key " + name) + } + + privKey, err := mintkey.UnarmorDecryptPrivKey(armor, passphrase) + if err != nil { + return errors.Wrap(err, "couldn't import private key") + } + + kb.writeLocalKey(name, privKey, passphrase) + return nil +} + +func (kb dbKeybase) Import(name string, armor string) (err error) { + bz := kb.db.Get(infoKey(name)) + if len(bz) > 0 { + return errors.New("Cannot overwrite data for name " + name) + } + infoBytes, err := mintkey.UnarmorInfoBytes(armor) + if err != nil { + return + } + kb.db.Set(infoKey(name), infoBytes) + return nil +} + +// ImportPubKey imports ASCII-armored public keys. +// Store a new Info object holding a public key only, i.e. it will +// not be possible to sign with it as it lacks the secret key. +func (kb dbKeybase) ImportPubKey(name string, armor string) (err error) { + bz := kb.db.Get(infoKey(name)) + if len(bz) > 0 { + return errors.New("Cannot overwrite data for name " + name) + } + pubBytes, err := mintkey.UnarmorPubKeyBytes(armor) + if err != nil { + return + } + pubKey, err := cryptoAmino.PubKeyFromBytes(pubBytes) + if err != nil { + return + } + kb.writeOfflineKey(name, pubKey) + return +} + +// Delete removes key forever, but we must present the +// proper passphrase before deleting it (for security). +// It returns an error if the key doesn't exist or +// passphrases don't match. +// Passphrase is ignored when deleting references to +// offline and Ledger / HW wallet keys. +func (kb dbKeybase) Delete(name, passphrase string, skipPass bool) error { + // verify we have the proper password before deleting + info, err := kb.Get(name) + if err != nil { + return err + } + if linfo, ok := info.(localInfo); ok && !skipPass { + if _, err = mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, passphrase); err != nil { + return err + } + } + kb.db.DeleteSync(addrKey(info.GetAddress())) + kb.db.DeleteSync(infoKey(name)) + return nil +} + +// Update changes the passphrase with which an already stored key is +// encrypted. +// +// oldpass must be the current passphrase used for encryption, +// getNewpass is a function to get the passphrase to permanently replace +// the current passphrase +func (kb dbKeybase) Update(name, oldpass string, getNewpass func() (string, error)) error { + info, err := kb.Get(name) + if err != nil { + return err + } + switch info.(type) { + case localInfo: + linfo := info.(localInfo) + key, err := mintkey.UnarmorDecryptPrivKey(linfo.PrivKeyArmor, oldpass) + if err != nil { + return err + } + newpass, err := getNewpass() + if err != nil { + return err + } + kb.writeLocalKey(name, key, newpass) + return nil + default: + return fmt.Errorf("locally stored key required. Received: %v", reflect.TypeOf(info).String()) + } +} + +// CloseDB releases the lock and closes the storage backend. +func (kb dbKeybase) CloseDB() { + kb.db.Close() +} + +func (kb dbKeybase) writeLocalKey(name string, priv tmcrypto.PrivKey, passphrase string) cosmosKeys.Info { + privkey, ok := priv.(emintCrypto.PrivKeySecp256k1) + if !ok { + panic(fmt.Sprintf("invalid private key type: %T", priv)) + } + // encrypt private key using passphrase + privArmor := mintkey.EncryptArmorPrivKey(privkey, passphrase) + // make Info + pub := privkey.PubKey() + pubkey, ok := pub.(emintCrypto.PubKeySecp256k1) + if !ok { + panic(fmt.Sprintf("invalid public key type: %T", pub)) + } + info := newLocalInfo(name, pubkey, privArmor) + kb.writeInfo(name, info) + return info +} + +func (kb dbKeybase) writeLedgerKey(name string, pub tmcrypto.PubKey, path hd.BIP44Params) cosmosKeys.Info { + pubkey, ok := pub.(emintCrypto.PubKeySecp256k1) + if !ok { + panic(fmt.Sprintf("invalid public key type: %T", pub)) + } + info := newLedgerInfo(name, pubkey, path) + kb.writeInfo(name, info) + return info +} + +func (kb dbKeybase) writeOfflineKey(name string, pub tmcrypto.PubKey) cosmosKeys.Info { + pubkey, ok := pub.(emintCrypto.PubKeySecp256k1) + if !ok { + panic(fmt.Sprintf("invalid public key type: %T", pub)) + } + info := newOfflineInfo(name, pubkey) + kb.writeInfo(name, info) + return info +} + +func (kb dbKeybase) writeInfo(name string, info cosmosKeys.Info) { + // write the info by key + key := infoKey(name) + serializedInfo := writeInfo(info) + kb.db.SetSync(key, serializedInfo) + // store a pointer to the infokey by address for fast lookup + kb.db.SetSync(addrKey(info.GetAddress()), key) +} + +func addrKey(address types.AccAddress) []byte { + return []byte(fmt.Sprintf("%s.%s", address.String(), addressSuffix)) +} + +func infoKey(name string) []byte { + return []byte(fmt.Sprintf("%s.%s", name, infoSuffix)) +} diff --git a/crypto/keys/keys.go b/crypto/keys/keys.go new file mode 100644 index 00000000..72c14ca1 --- /dev/null +++ b/crypto/keys/keys.go @@ -0,0 +1,13 @@ +package keys + +import ( + cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +// SigningAlgo defines an algorithm to derive key-pairs which can be used for cryptographic signing. +type SigningAlgo string + +const ( + // Secp256k1 uses the Bitcoin secp256k1 ECDSA parameters. + Secp256k1 = cosmosKeys.SigningAlgo("emintsecp256k1") +) diff --git a/crypto/keys/lazy_keybase.go b/crypto/keys/lazy_keybase.go new file mode 100644 index 00000000..a38805b6 --- /dev/null +++ b/crypto/keys/lazy_keybase.go @@ -0,0 +1,222 @@ +package keys + +import ( + "fmt" + + "github.com/tendermint/tendermint/crypto" + cmn "github.com/tendermint/tendermint/libs/common" + + cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + sdk "github.com/cosmos/cosmos-sdk/types" +) + +var _ cosmosKeys.Keybase = lazyKeybase{} + +type lazyKeybase struct { + name string + dir string +} + +// New creates a new instance of a lazy keybase. +func New(name, dir string) cosmosKeys.Keybase { + if err := cmn.EnsureDir(dir, 0700); err != nil { + panic(fmt.Sprintf("failed to create Keybase directory: %s", err)) + } + + return lazyKeybase{name: name, dir: dir} +} + +func (lkb lazyKeybase) List() ([]cosmosKeys.Info, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).List() +} + +func (lkb lazyKeybase) Get(name string) (cosmosKeys.Info, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).Get(name) +} + +func (lkb lazyKeybase) GetByAddress(address sdk.AccAddress) (cosmosKeys.Info, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).GetByAddress(address) +} + +func (lkb lazyKeybase) Delete(name, passphrase string, skipPass bool) error { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + + return newDbKeybase(db).Delete(name, passphrase, skipPass) +} + +func (lkb lazyKeybase) Sign(name, passphrase string, msg []byte) ([]byte, crypto.PubKey, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, nil, err + } + defer db.Close() + + return newDbKeybase(db).Sign(name, passphrase, msg) +} + +func (lkb lazyKeybase) CreateMnemonic(name string, language cosmosKeys.Language, passwd string, algo cosmosKeys.SigningAlgo) (info cosmosKeys.Info, seed string, err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, "", err + } + defer db.Close() + + return newDbKeybase(db).CreateMnemonic(name, language, passwd, algo) +} + +func (lkb lazyKeybase) CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (cosmosKeys.Info, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd, account, index) +} + +func (lkb lazyKeybase) Derive(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (cosmosKeys.Info, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).Derive(name, mnemonic, bip39Passwd, encryptPasswd, params) +} + +func (lkb lazyKeybase) CreateLedger(name string, algo cosmosKeys.SigningAlgo, hrp string, account, index uint32) (info cosmosKeys.Info, err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).CreateLedger(name, algo, hrp, account, index) +} + +func (lkb lazyKeybase) CreateOffline(name string, pubkey crypto.PubKey) (info cosmosKeys.Info, err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).CreateOffline(name, pubkey) +} + +func (lkb lazyKeybase) CreateMulti(name string, pubkey crypto.PubKey) (info cosmosKeys.Info, err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).CreateMulti(name, pubkey) +} + +func (lkb lazyKeybase) Update(name, oldpass string, getNewpass func() (string, error)) error { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + + return newDbKeybase(db).Update(name, oldpass, getNewpass) +} + +func (lkb lazyKeybase) Import(name string, armor string) (err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + + return newDbKeybase(db).Import(name, armor) +} + +func (lkb lazyKeybase) ImportPrivKey(name string, armor string, passphrase string) error { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + + return newDbKeybase(db).ImportPrivKey(name, armor, passphrase) +} + +func (lkb lazyKeybase) ImportPubKey(name string, armor string) (err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + + return newDbKeybase(db).ImportPubKey(name, armor) +} + +func (lkb lazyKeybase) Export(name string) (armor string, err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return "", err + } + defer db.Close() + + return newDbKeybase(db).Export(name) +} + +func (lkb lazyKeybase) ExportPubKey(name string) (armor string, err error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return "", err + } + defer db.Close() + + return newDbKeybase(db).ExportPubKey(name) +} + +func (lkb lazyKeybase) ExportPrivateKeyObject(name string, passphrase string) (crypto.PrivKey, error) { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return nil, err + } + defer db.Close() + + return newDbKeybase(db).ExportPrivateKeyObject(name, passphrase) +} + +func (lkb lazyKeybase) ExportPrivKey(name string, decryptPassphrase string, + encryptPassphrase string) (armor string, err error) { + + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return "", err + } + defer db.Close() + + return newDbKeybase(db).ExportPrivKey(name, decryptPassphrase, encryptPassphrase) +} + +func (lkb lazyKeybase) CloseDB() {} diff --git a/crypto/keys/output.go b/crypto/keys/output.go new file mode 100644 index 00000000..30e7a302 --- /dev/null +++ b/crypto/keys/output.go @@ -0,0 +1,85 @@ +package keys + +import ( + "encoding/hex" + + cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" +) + +// KeyOutput defines a structure wrapping around an Info object used for output +// functionality. +type KeyOutput struct { + Name string `json:"name"` + Type string `json:"type"` + Address string `json:"address"` + PubKey string `json:"pubkey"` + Mnemonic string `json:"mnemonic,omitempty"` + Threshold uint `json:"threshold,omitempty"` +} + +// Bech32KeysOutput returns a slice of KeyOutput objects, each with the "acc" +// Bech32 prefixes, given a slice of Info objects. It returns an error if any +// call to Bech32KeyOutput fails. +func Bech32KeysOutput(infos []cosmosKeys.Info) ([]cosmosKeys.KeyOutput, error) { + kos := make([]cosmosKeys.KeyOutput, len(infos)) + for i, info := range infos { + ko, err := Bech32KeyOutput(info) + if err != nil { + return nil, err + } + kos[i] = ko + } + + return kos, nil +} + +// Bech32ConsKeyOutput create a KeyOutput in with "cons" Bech32 prefixes. +func Bech32ConsKeyOutput(keyInfo cosmosKeys.Info) (cosmosKeys.KeyOutput, error) { + consAddr := keyInfo.GetPubKey().Address() + bytes := keyInfo.GetPubKey().Bytes() + + // bechPubKey, err := sdk.Bech32ifyConsPub(keyInfo.GetPubKey()) + // if err != nil { + // return KeyOutput{}, err + // } + + return cosmosKeys.KeyOutput{ + Name: keyInfo.GetName(), + Type: keyInfo.GetType().String(), + Address: consAddr.String(), + PubKey: hex.EncodeToString(bytes), + }, nil +} + +// Bech32ValKeyOutput create a KeyOutput in with "val" Bech32 prefixes. +func Bech32ValKeyOutput(keyInfo cosmosKeys.Info) (cosmosKeys.KeyOutput, error) { + valAddr := keyInfo.GetPubKey().Address() + bytes := keyInfo.GetPubKey().Bytes() + + // bechPubKey, err := sdk.Bech32ifyValPub(keyInfo.GetPubKey()) + // if err != nil { + // return KeyOutput{}, err + // } + + return cosmosKeys.KeyOutput{ + Name: keyInfo.GetName(), + Type: keyInfo.GetType().String(), + Address: valAddr.String(), + PubKey: hex.EncodeToString(bytes), + }, nil +} + +// Bech32KeyOutput create a KeyOutput in with "acc" Bech32 prefixes. +func Bech32KeyOutput(info cosmosKeys.Info) (cosmosKeys.KeyOutput, error) { + accAddr := info.GetPubKey().Address() + bytes := info.GetPubKey().Bytes() + + ko := cosmosKeys.KeyOutput{ + Name: info.GetName(), + Type: info.GetType().String(), + Address: accAddr.String(), + PubKey: hex.EncodeToString(bytes), + } + + return ko, nil +} diff --git a/crypto/keys/types.go b/crypto/keys/types.go new file mode 100644 index 00000000..69fc63b1 --- /dev/null +++ b/crypto/keys/types.go @@ -0,0 +1,156 @@ +package keys + +import ( + "fmt" + + cosmosKeys "github.com/cosmos/cosmos-sdk/crypto/keys" + emintCrypto "github.com/cosmos/ethermint/crypto" + "github.com/tendermint/tendermint/crypto" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/cosmos-sdk/types" +) + +// KeyType reflects a human-readable type for key listing. +type KeyType uint + +// Info KeyTypes +const ( + TypeLocal KeyType = 0 + TypeLedger KeyType = 1 + TypeOffline KeyType = 2 + TypeMulti KeyType = 3 +) + +var keyTypes = map[KeyType]string{ + TypeLocal: "local", + TypeLedger: "ledger", + TypeOffline: "offline", + TypeMulti: "multi", +} + +// String implements the stringer interface for KeyType. +func (kt KeyType) String() string { + return keyTypes[kt] +} + +var ( + _ cosmosKeys.Info = &localInfo{} + _ cosmosKeys.Info = &ledgerInfo{} + _ cosmosKeys.Info = &offlineInfo{} +) + +// localInfo is the public information about a locally stored key +type localInfo struct { + Name string `json:"name"` + PubKey emintCrypto.PubKeySecp256k1 `json:"pubkey"` + PrivKeyArmor string `json:"privkey.armor"` +} + +func newLocalInfo(name string, pub emintCrypto.PubKeySecp256k1, privArmor string) cosmosKeys.Info { + return &localInfo{ + Name: name, + PubKey: pub, + PrivKeyArmor: privArmor, + } +} + +func (i localInfo) GetType() cosmosKeys.KeyType { + return cosmosKeys.TypeLocal +} + +func (i localInfo) GetName() string { + return i.Name +} + +func (i localInfo) GetPubKey() crypto.PubKey { + return i.PubKey +} + +func (i localInfo) GetAddress() types.AccAddress { + return i.PubKey.Address().Bytes() +} + +func (i localInfo) GetPath() (*hd.BIP44Params, error) { + return nil, fmt.Errorf("BIP44 Paths are not available for this type") +} + +// ledgerInfo is the public information about a Ledger key +type ledgerInfo struct { + Name string `json:"name"` + PubKey emintCrypto.PubKeySecp256k1 `json:"pubkey"` + Path hd.BIP44Params `json:"path"` +} + +func newLedgerInfo(name string, pub emintCrypto.PubKeySecp256k1, path hd.BIP44Params) cosmosKeys.Info { + return &ledgerInfo{ + Name: name, + PubKey: pub, + Path: path, + } +} + +func (i ledgerInfo) GetType() cosmosKeys.KeyType { + return cosmosKeys.TypeLedger +} + +func (i ledgerInfo) GetName() string { + return i.Name +} + +func (i ledgerInfo) GetPubKey() crypto.PubKey { + return i.PubKey +} + +func (i ledgerInfo) GetAddress() types.AccAddress { + return i.PubKey.Address().Bytes() +} + +func (i ledgerInfo) GetPath() (*hd.BIP44Params, error) { + tmp := i.Path + return &tmp, nil +} + +// offlineInfo is the public information about an offline key +type offlineInfo struct { + Name string `json:"name"` + PubKey emintCrypto.PubKeySecp256k1 `json:"pubkey"` +} + +func newOfflineInfo(name string, pub emintCrypto.PubKeySecp256k1) cosmosKeys.Info { + return &offlineInfo{ + Name: name, + PubKey: pub, + } +} + +func (i offlineInfo) GetType() cosmosKeys.KeyType { + return cosmosKeys.TypeOffline +} + +func (i offlineInfo) GetName() string { + return i.Name +} + +func (i offlineInfo) GetPubKey() crypto.PubKey { + return i.PubKey +} + +func (i offlineInfo) GetAddress() types.AccAddress { + return i.PubKey.Address().Bytes() +} + +func (i offlineInfo) GetPath() (*hd.BIP44Params, error) { + return nil, fmt.Errorf("BIP44 Paths are not available for this type") +} + +// encoding info +func writeInfo(i cosmosKeys.Info) []byte { + return cdc.MustMarshalBinaryLengthPrefixed(i) +} + +// decoding info +func readInfo(bz []byte) (info cosmosKeys.Info, err error) { + err = cdc.UnmarshalBinaryLengthPrefixed(bz, &info) + return +} diff --git a/crypto/keys/types_test.go b/crypto/keys/types_test.go new file mode 100644 index 00000000..f4e9cfba --- /dev/null +++ b/crypto/keys/types_test.go @@ -0,0 +1,46 @@ +package keys + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keys/hd" + "github.com/cosmos/ethermint/crypto" + "github.com/stretchr/testify/assert" +) + +func TestWriteReadInfo(t *testing.T) { + tmpKey, err := crypto.GenerateKey() + assert.NoError(t, err) + pkey := tmpKey.PubKey() + pubkey, ok := pkey.(crypto.PubKeySecp256k1) + assert.True(t, ok) + + info := newOfflineInfo("offline", pubkey) + bytes := writeInfo(info) + assert.NotNil(t, bytes) + + regeneratedKey, err := readInfo(bytes) + assert.NoError(t, err) + assert.Equal(t, info.GetPubKey(), regeneratedKey.GetPubKey()) + assert.Equal(t, info.GetName(), regeneratedKey.GetName()) + + info = newLocalInfo("local", pubkey, "testarmor") + bytes = writeInfo(info) + assert.NotNil(t, bytes) + + regeneratedKey, err = readInfo(bytes) + assert.NoError(t, err) + assert.Equal(t, info.GetPubKey(), regeneratedKey.GetPubKey()) + assert.Equal(t, info.GetName(), regeneratedKey.GetName()) + + info = newLedgerInfo("ledger", pubkey, + hd.BIP44Params{Purpose: 1, CoinType: 1, Account: 1, Change: false, AddressIndex: 1}) + bytes = writeInfo(info) + assert.NotNil(t, bytes) + + regeneratedKey, err = readInfo(bytes) + assert.NoError(t, err) + assert.Equal(t, info.GetPubKey(), regeneratedKey.GetPubKey()) + assert.Equal(t, info.GetName(), regeneratedKey.GetName()) + +} diff --git a/crypto/secp256k1.go b/crypto/secp256k1.go index f57b1400..6087d922 100644 --- a/crypto/secp256k1.go +++ b/crypto/secp256k1.go @@ -17,7 +17,7 @@ var _ tmcrypto.PrivKey = PrivKeySecp256k1{} // PrivKeySecp256k1 defines a type alias for an ecdsa.PrivateKey that implements // Tendermint's PrivateKey interface. -type PrivKeySecp256k1 ecdsa.PrivateKey +type PrivKeySecp256k1 []byte // GenerateKey generates a new random private key. It returns an error upon // failure. @@ -27,17 +27,18 @@ func GenerateKey() (PrivKeySecp256k1, error) { return PrivKeySecp256k1{}, err } - return PrivKeySecp256k1(*priv), nil + return PrivKeySecp256k1(ethcrypto.FromECDSA(priv)), nil } // PubKey returns the ECDSA private key's public key. func (privkey PrivKeySecp256k1) PubKey() tmcrypto.PubKey { - return PubKeySecp256k1{privkey.PublicKey} + ecdsaPKey := privkey.ToECDSA() + return PubKeySecp256k1(ethcrypto.FromECDSAPub(&ecdsaPKey.PublicKey)) } // Bytes returns the raw ECDSA private key bytes. func (privkey PrivKeySecp256k1) Bytes() []byte { - return ethcrypto.FromECDSA(privkey.ToECDSA()) + return privkey } // Sign creates a recoverable ECDSA signature on the secp256k1 curve over the @@ -58,7 +59,8 @@ func (privkey PrivKeySecp256k1) Equals(other tmcrypto.PrivKey) bool { // ToECDSA returns the ECDSA private key as a reference to ecdsa.PrivateKey type. func (privkey PrivKeySecp256k1) ToECDSA() *ecdsa.PrivateKey { - return (*ecdsa.PrivateKey)(&privkey) + key, _ := ethcrypto.ToECDSA(privkey.Bytes()) + return key } // ---------------------------------------------------------------------------- @@ -68,18 +70,17 @@ var _ tmcrypto.PubKey = (*PubKeySecp256k1)(nil) // PubKeySecp256k1 defines a type alias for an ecdsa.PublicKey that implements // Tendermint's PubKey interface. -type PubKeySecp256k1 struct { - pubkey ecdsa.PublicKey -} +type PubKeySecp256k1 []byte // Address returns the address of the ECDSA public key. func (key PubKeySecp256k1) Address() tmcrypto.Address { - return tmcrypto.Address(ethcrypto.PubkeyToAddress(key.pubkey).Bytes()) + pubk, _ := ethcrypto.UnmarshalPubkey(key) + return tmcrypto.Address(ethcrypto.PubkeyToAddress(*pubk).Bytes()) } // Bytes returns the raw bytes of the ECDSA public key. func (key PubKeySecp256k1) Bytes() []byte { - return ethcrypto.FromECDSAPub(&key.pubkey) + return key } // VerifyBytes verifies that the ECDSA public key created a given signature over diff --git a/crypto/secp256k1_test.go b/crypto/secp256k1_test.go index 5c80cdd4..1e16dec2 100644 --- a/crypto/secp256k1_test.go +++ b/crypto/secp256k1_test.go @@ -24,7 +24,7 @@ func TestPrivKeySecp256k1PrivKey(t *testing.T) { // validate Ethereum address equality addr := privKey.PubKey().Address() - expectedAddr := ethcrypto.PubkeyToAddress(privKey.PublicKey) + expectedAddr := ethcrypto.PubkeyToAddress(privKey.ToECDSA().PublicKey) require.Equal(t, expectedAddr.Bytes(), addr.Bytes()) // validate we can sign some bytes diff --git a/go.mod b/go.mod index 83b84c11..92dc635d 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8 // indirect github.com/cespare/cp v1.1.1 // indirect github.com/cosmos/cosmos-sdk v0.28.2-0.20190711105643-280734d0e37f - github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d // indirect + github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d github.com/deckarep/golang-set v1.7.1 // indirect github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect github.com/elastic/gosigar v0.10.3 // indirect @@ -58,4 +58,5 @@ require ( google.golang.org/genproto v0.0.0-20190708153700-3bdd9d9f5532 // indirect google.golang.org/grpc v1.22.0 // indirect gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect + gopkg.in/yaml.v2 v2.2.2 ) diff --git a/keys/add.go b/keys/add.go new file mode 100644 index 00000000..ec69d783 --- /dev/null +++ b/keys/add.go @@ -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 ", + 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 +} diff --git a/keys/add_test.go b/keys/add_test.go new file mode 100644 index 00000000..e158cf04 --- /dev/null +++ b/keys/add_test.go @@ -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) +} diff --git a/keys/codec.go b/keys/codec.go new file mode 100644 index 00000000..eae45446 --- /dev/null +++ b/keys/codec.go @@ -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) +} diff --git a/keys/root.go b/keys/root.go new file mode 100644 index 00000000..1fc55ea6 --- /dev/null +++ b/keys/root.go @@ -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 +} diff --git a/keys/show.go b/keys/show.go new file mode 100644 index 00000000..ddccf56b --- /dev/null +++ b/keys/show.go @@ -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 ", + 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) +// } diff --git a/keys/show_test.go b/keys/show_test.go new file mode 100644 index 00000000..6732006c --- /dev/null +++ b/keys/show_test.go @@ -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) +} diff --git a/keys/utils.go b/keys/utils.go new file mode 100644 index 00000000..3cc3df21 --- /dev/null +++ b/keys/utils.go @@ -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) +} diff --git a/x/evm/types/utils.go b/x/evm/types/utils.go index 8c9e4599..de7973ba 100644 --- a/x/evm/types/utils.go +++ b/x/evm/types/utils.go @@ -19,7 +19,7 @@ func GenerateEthAddress() ethcmn.Address { panic(err) } - return ethcrypto.PubkeyToAddress(priv.PublicKey) + return ethcrypto.PubkeyToAddress(priv.ToECDSA().PublicKey) } // ValidateSigner attempts to validate a signer for a given slice of bytes over