cosmos-sdk/client/keys/add.go

519 lines
14 KiB
Go

package keys
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"sort"
"github.com/tendermint/tendermint/crypto/multisig"
"github.com/cosmos/go-bip39"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/libs/cli"
"github.com/cosmos/cosmos-sdk/client"
ccrypto "github.com/cosmos/cosmos-sdk/crypto"
"github.com/cosmos/cosmos-sdk/crypto/keys"
"github.com/cosmos/cosmos-sdk/crypto/keys/hd"
sdk "github.com/cosmos/cosmos-sdk/types"
)
const (
flagInteractive = "interactive"
flagBIP44Path = "bip44-path"
flagRecover = "recover"
flagNoBackup = "no-backup"
flagDryRun = "dry-run"
flagAccount = "account"
flagIndex = "index"
flagMultisig = "multisig"
flagNoSort = "nosort"
)
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.
You can add a multisig key by passing the list of key names you want the public
key to be composed of to the --multisig flag and the minimum number of signatures
required through --multisig-threshold. The keys are sorted by address, unless
the flag --nosort is set.
`,
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(client.FlagUseLedger, false, "Store a local reference to a private key on a Ledger device")
cmd.Flags().String(flagBIP44Path, "44'/118'/0'/0/0", "BIP44 path from which to derive a private key")
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, "Index number for HD derivation")
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 keys.Keybase
var err error
var encryptPassword string
buf := client.BufferStdin()
name := args[0]
interactive := viper.GetBool(flagInteractive)
if viper.GetBool(flagDryRun) {
// we throw this away, so don't enforce args,
// we want to get a new random seed phrase quickly
kb = client.MockKeyBase()
encryptPassword = "throwing-this-key-away"
} else {
kb, err = GetKeyBaseWithWritePerm()
if err != nil {
return err
}
_, err := kb.Get(name)
if err == nil {
// account exists, ask for user confirmation
if response, err := client.GetConfirmation(
fmt.Sprintf("override the existing name %s", name), buf); err != nil || !response {
return err
}
}
multisigKeys := viper.GetStringSlice(flagMultisig)
if len(multisigKeys) != 0 {
var pks []crypto.PubKey
multisigThreshold := viper.GetInt(flagMultiSigThreshold)
if err := validateMultisigThreshold(multisigThreshold, len(multisigKeys)); err != nil {
return err
}
for _, keyname := range multisigKeys {
k, err := kb.Get(keyname)
if err != nil {
return err
}
pks = append(pks, k.GetPubKey())
}
// Handle --nosort
if !viper.GetBool(flagNoSort) {
sort.Slice(pks, func(i, j int) bool {
return bytes.Compare(pks[i].Address(), pks[j].Address()) < 0
})
}
pk := multisig.NewPubKeyMultisigThreshold(multisigThreshold, pks)
if _, err := kb.CreateOffline(name, pk); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "Key %q saved to disk.", name)
return nil
}
// ask for a password when generating a local key
if viper.GetString(FlagPublicKey) == "" && !viper.GetBool(client.FlagUseLedger) {
encryptPassword, err = client.GetCheckPassword(
"Enter a passphrase to encrypt your key to disk:",
"Repeat the passphrase:", buf)
if err != nil {
return err
}
}
}
if viper.GetString(FlagPublicKey) != "" {
pk, err := sdk.GetAccPubKeyBech32(viper.GetString(FlagPublicKey))
if err != nil {
return err
}
kb.CreateOffline(name, pk)
return nil
}
bipFlag := cmd.Flags().Lookup(flagBIP44Path)
bip44Params, err := getBIP44ParamsAndPath(bipFlag.Value.String(), bipFlag.Changed || !interactive)
if err != nil {
return err
}
// If we're using ledger, only thing we need is the path. So generate key and
// we're done.
if viper.GetBool(client.FlagUseLedger) {
account := uint32(viper.GetInt(flagAccount))
index := uint32(viper.GetInt(flagIndex))
path := ccrypto.DerivationPath{44, 118, account, 0, index}
info, err := kb.CreateLedger(name, path, keys.Secp256k1)
if err != nil {
return err
}
printCreate(info, "")
return nil
}
// Recover key from seed passphrase
if viper.GetBool(flagRecover) {
seed, err := client.GetSeed(
"Enter your recovery seed phrase:", buf)
if err != nil {
return err
}
info, err := kb.CreateKey(name, seed, encryptPassword)
if err != nil {
return err
}
// print out results without the seed phrase
viper.Set(flagNoBackup, true)
printCreate(info, "")
return nil
}
var mnemonic string
if interactive {
mnemonic, err = client.GetString("Enter your bip39 mnemonic, or hit enter to generate one.", buf)
if err != nil {
return err
}
}
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
}
}
// get bip39 passphrase
var bip39Passphrase string
if interactive {
bip39Passphrase, err = client.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, \"\"", buf)
if err != nil {
return err
}
// if they use one, make them re-enter it
if len(bip39Passphrase) != 0 {
p2, err := client.GetString("Repeat the passphrase:", buf)
if err != nil {
return err
}
if bip39Passphrase != p2 {
return errors.New("passphrases don't match")
}
}
}
info, err := kb.Derive(name, mnemonic, bip39Passphrase, encryptPassword, *bip44Params)
if err != nil {
return err
}
printCreate(info, mnemonic)
return nil
}
func getBIP44ParamsAndPath(path string, flagSet bool) (*hd.BIP44Params, error) {
buf := client.BufferStdin()
bip44Path := path
// if it wasn't set in the flag, give it a chance to overide interactively
if !flagSet {
var err error
bip44Path, err = client.GetString(fmt.Sprintf("Enter your bip44 path. Default is %s\n", path), buf)
if err != nil {
return nil, err
}
if len(bip44Path) == 0 {
bip44Path = path
}
}
bip44params, err := hd.NewParamsFromPath(bip44Path)
if err != nil {
return nil, err
}
return bip44params, nil
}
func printCreate(info keys.Info, seed string) {
output := viper.Get(cli.OutputFlag)
switch output {
case "text":
fmt.Fprintln(os.Stderr, "")
printKeyInfo(info, Bech32KeyOutput)
// print seed unless requested not to.
if !viper.GetBool(client.FlagUseLedger) && !viper.GetBool(flagNoBackup) {
fmt.Fprintln(os.Stderr, "\n**Important** write this seed phrase in a safe place.")
fmt.Fprintln(os.Stderr, "It is the only way to recover your account if you ever forget your password.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, seed)
}
case "json":
out, err := Bech32KeyOutput(info)
if err != nil {
panic(err)
}
if !viper.GetBool(flagNoBackup) {
out.Seed = seed
}
var jsonString []byte
if viper.GetBool(client.FlagIndentResponse) {
jsonString, err = cdc.MarshalJSONIndent(out, "", " ")
} else {
jsonString, err = cdc.MarshalJSON(out)
}
if err != nil {
panic(err) // really shouldn't happen...
}
fmt.Fprintln(os.Stderr, string(jsonString))
default:
panic(fmt.Sprintf("I can't speak: %s", output))
}
}
// function to just a new seed to display in the UI before actually persisting it in the keybase
func getSeed(algo keys.SigningAlgo) string {
kb := client.MockKeyBase()
pass := "throwing-this-key-away"
name := "inmemorykey"
_, seed, _ := kb.CreateMnemonic(name, keys.English, pass, algo)
return seed
}
func printPrefixed(msg string) {
fmt.Fprintln(os.Stderr, msg)
}
func printStep() {
printPrefixed("-------------------------------------")
}
/////////////////////////////
// REST
// new key request REST body
type NewKeyBody struct {
Name string `json:"name"`
Password string `json:"password"`
Seed string `json:"seed"`
}
// add new key REST handler
func AddNewKeyRequestHandler(indent bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var kb keys.Keybase
var m NewKeyBody
kb, err := GetKeyBaseWithWritePerm()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
err = json.Unmarshal(body, &m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if m.Name == "" {
w.WriteHeader(http.StatusBadRequest)
err = errMissingName()
w.Write([]byte(err.Error()))
return
}
if m.Password == "" {
w.WriteHeader(http.StatusBadRequest)
err = errMissingPassword()
w.Write([]byte(err.Error()))
return
}
// check if already exists
infos, err := kb.List()
for _, info := range infos {
if info.GetName() == m.Name {
w.WriteHeader(http.StatusConflict)
err = errKeyNameConflict(m.Name)
w.Write([]byte(err.Error()))
return
}
}
// create account
seed := m.Seed
if seed == "" {
seed = getSeed(keys.Secp256k1)
}
info, err := kb.CreateKey(m.Name, seed, m.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
keyOutput, err := Bech32KeyOutput(info)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
keyOutput.Seed = seed
PostProcessResponse(w, cdc, keyOutput, indent)
}
}
// Seed REST request handler
func SeedRequestHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
algoType := vars["type"]
// algo type defaults to secp256k1
if algoType == "" {
algoType = "secp256k1"
}
algo := keys.SigningAlgo(algoType)
seed := getSeed(algo)
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(seed))
}
// RecoverKeyBody is recover key request REST body
type RecoverKeyBody struct {
Password string `json:"password"`
Seed string `json:"seed"`
}
// RecoverRequestHandler performs key recover request
func RecoverRequestHandler(indent bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
var m RecoverKeyBody
body, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
err = cdc.UnmarshalJSON(body, &m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte(err.Error()))
return
}
if name == "" {
w.WriteHeader(http.StatusBadRequest)
err = errMissingName()
w.Write([]byte(err.Error()))
return
}
if m.Password == "" {
w.WriteHeader(http.StatusBadRequest)
err = errMissingPassword()
w.Write([]byte(err.Error()))
return
}
if m.Seed == "" {
w.WriteHeader(http.StatusBadRequest)
err = errMissingSeed()
w.Write([]byte(err.Error()))
return
}
kb, err := GetKeyBaseWithWritePerm()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
// check if already exists
infos, err := kb.List()
for _, info := range infos {
if info.GetName() == name {
w.WriteHeader(http.StatusConflict)
err = errKeyNameConflict(name)
w.Write([]byte(err.Error()))
return
}
}
info, err := kb.CreateKey(name, m.Seed, m.Password)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
keyOutput, err := Bech32KeyOutput(info)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(err.Error()))
return
}
PostProcessResponse(w, cdc, keyOutput, indent)
}
}