prysm-pulse/validator/accounts/wallet_recover.go

250 lines
8.0 KiB
Go
Raw Normal View History

package accounts
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/cmd/validator/flags"
"github.com/prysmaticlabs/prysm/io/prompt"
"github.com/prysmaticlabs/prysm/validator/accounts/userprompt"
"github.com/prysmaticlabs/prysm/validator/accounts/wallet"
"github.com/prysmaticlabs/prysm/validator/keymanager"
"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
"github.com/tyler-smith/go-bip39"
"github.com/tyler-smith/go-bip39/wordlists"
"github.com/urfave/cli/v2"
)
const (
phraseWordCount = 24
// #nosec G101 -- Not sensitive data
newMnemonicPassphraseYesNoText = "(Advanced) Do you want to setup a '25th word' passphrase for your mnemonic? [y/n]"
// #nosec G101 -- Not sensitive data
newMnemonicPassphrasePromptText = "(Advanced) Setup a passphrase '25th word' for your mnemonic " +
"(WARNING: You cannot recover your keys from your mnemonic if you forget this passphrase!)"
// #nosec G101 -- Not sensitive data
mnemonicPassphraseYesNoText = "(Advanced) Do you have an optional '25th word' passphrase for your mnemonic? [y/n]"
// #nosec G101 -- Not sensitive data
mnemonicPassphrasePromptText = "(Advanced) Enter the '25th word' passphrase for your mnemonic"
)
var (
ErrIncorrectWordNumber = errors.New("incorrect number of words provided")
ErrEmptyMnemonic = errors.New("phrase cannot be empty")
)
// RecoverWalletConfig to run the recover wallet function.
type RecoverWalletConfig struct {
WalletDir string
WalletPassword string
Mnemonic string
NumAccounts int
Mnemonic25thWord string
}
// RecoverWalletCli uses a menmonic seed phrase to recover a wallet into the path provided. This
// uses the CLI to extract necessary values to run the function.
func RecoverWalletCli(cliCtx *cli.Context) error {
mnemonic, err := inputMnemonic(cliCtx)
if err != nil {
return errors.Wrap(err, "could not get mnemonic phrase")
}
config := &RecoverWalletConfig{
Mnemonic: mnemonic,
}
skipMnemonic25thWord := cliCtx.IsSet(flags.SkipMnemonic25thWordCheckFlag.Name)
has25thWordFile := cliCtx.IsSet(flags.Mnemonic25thWordFileFlag.Name)
if !skipMnemonic25thWord && !has25thWordFile {
resp, err := prompt.ValidatePrompt(
os.Stdin, mnemonicPassphraseYesNoText, prompt.ValidateYesOrNo,
)
if err != nil {
return errors.Wrap(err, "could not validate choice")
}
if strings.EqualFold(resp, "y") {
mnemonicPassphrase, err := prompt.InputPassword(
cliCtx,
flags.Mnemonic25thWordFileFlag,
mnemonicPassphrasePromptText,
"Confirm mnemonic passphrase",
false, /* Should confirm password */
func(input string) error {
if strings.TrimSpace(input) == "" {
return errors.New("input cannot be empty")
}
return nil
},
)
if err != nil {
return err
}
config.Mnemonic25thWord = mnemonicPassphrase
}
}
walletDir, err := userprompt.InputDirectory(cliCtx, userprompt.WalletDirPromptText, flags.WalletDirFlag)
if err != nil {
return err
}
walletPassword, err := prompt.InputPassword(
cliCtx,
flags.WalletPasswordFileFlag,
wallet.NewWalletPasswordPromptText,
wallet.ConfirmPasswordPromptText,
true, /* Should confirm password */
prompt.ValidatePasswordInput,
)
if err != nil {
return err
}
numAccounts, err := inputNumAccounts(cliCtx)
if err != nil {
return errors.Wrap(err, "could not get number of accounts to recover")
}
config.WalletDir = walletDir
config.WalletPassword = walletPassword
config.NumAccounts = int(numAccounts)
if _, err = RecoverWallet(cliCtx.Context, config); err != nil {
return err
}
log.Infof(
"Successfully recovered HD wallet with accounts and saved configuration to disk",
)
return nil
}
// RecoverWallet uses a menmonic seed phrase to recover a wallet into the path provided.
func RecoverWallet(ctx context.Context, cfg *RecoverWalletConfig) (*wallet.Wallet, error) {
// Ensure that the wallet directory does not contain a wallet already
dirExists, err := wallet.Exists(cfg.WalletDir)
if err != nil {
return nil, err
}
if dirExists {
return nil, errors.New("a wallet already exists at this location. Please input an" +
" alternative location for the new wallet or remove the current wallet")
}
w := wallet.New(&wallet.Config{
WalletDir: cfg.WalletDir,
KeymanagerKind: keymanager.Derived,
WalletPassword: cfg.WalletPassword,
})
if err := w.SaveWallet(); err != nil {
return nil, errors.Wrap(err, "could not save wallet to disk")
}
km, err := derived.NewKeymanager(ctx, &derived.SetupConfig{
Wallet: w,
ListenForChanges: false,
})
if err != nil {
return nil, errors.Wrap(err, "could not make keymanager for given phrase")
}
if err := km.RecoverAccountsFromMnemonic(ctx, cfg.Mnemonic, cfg.Mnemonic25thWord, cfg.NumAccounts); err != nil {
return nil, err
}
log.WithField("wallet-path", w.AccountsDir()).Infof(
"Successfully recovered HD wallet with %d accounts. Please use `accounts list` to view details for your accounts",
cfg.NumAccounts,
)
return w, nil
}
func inputMnemonic(cliCtx *cli.Context) (mnemonicPhrase string, err error) {
if cliCtx.IsSet(flags.MnemonicFileFlag.Name) {
mnemonicFilePath := cliCtx.String(flags.MnemonicFileFlag.Name)
data, err := os.ReadFile(mnemonicFilePath) // #nosec G304 -- ReadFile is safe
if err != nil {
return "", err
}
enteredMnemonic := string(data)
Web Backend Recover Wallet (#8679) * recover wallet rpc support - first attempt * removed redundant check * separate createwallet into imported and derived. Recover is derived * added unit test for recover. Recover is the createWallet for derived * so proto does CamelCase! * fixed issues related to unit testing * expose ValidateMnemonic from accounts to be used by the rpc module * added Mnemonic25Support and test to ensure it is not et is not empty * added skipMnemonic25thword support and unit test * Update proto/validator/accounts/v2/web_api.proto Proper naming convention Co-authored-by: terence tsao <terence@prysmaticlabs.com> * ran goimports,changed variable to SkipMnemonic_25ThWord * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * fixed variable and text msgs naming convention as per the review * added unit test for strong password on recover * Update proto/validator/accounts/v2/web_api.proto Co-authored-by: Nishant Das <nish1993@hotmail.com> * Update validator/rpc/wallet.go Co-authored-by: Nishant Das <nish1993@hotmail.com> * goimports -w on root proto * added comments after exposing ValidateMnemonic to by used by rpc.RecoverWallet * language should be case insensitive * need to goimports before update protobuf scripts * Update validator/rpc/wallet.go Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * Update validator/rpc/wallet.go comments need to be consistant Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * Update validator/rpc/wallet_test.go consistent comments Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * fix comments * remove the skipMnemonic from rpc, just check if passphrase is empyt * defer call to set the writePassword flag on web boarding * gofmt * fixed the password write test after recovering a wallet. Needed to have two defers. One to set to true then one back to false * restore powchain.pb.go and finalized_block_root_container.pb.go * revert beacon messages.pb.go * revert unrelated files * revert unrelated files * revert unrelated files * unlreated files * restored the imports * Update validator/rpc/wallet_test.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> Co-authored-by: Nishant Das <nish1993@hotmail.com> Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
2021-04-05 20:42:03 +00:00
if err := ValidateMnemonic(enteredMnemonic); err != nil {
return "", errors.Wrap(err, "mnemonic phrase did not pass validation")
}
return enteredMnemonic, nil
}
allowedLanguages := map[string][]string{
"chinese_simplified": wordlists.ChineseSimplified,
"chinese_traditional": wordlists.ChineseTraditional,
"czech": wordlists.Czech,
"english": wordlists.English,
"french": wordlists.French,
"japanese": wordlists.Japanese,
"korean": wordlists.Korean,
"italian": wordlists.Italian,
"spanish": wordlists.Spanish,
}
languages := make([]string, 0)
for k := range allowedLanguages {
languages = append(languages, k)
}
sort.Strings(languages)
selectedLanguage, err := prompt.ValidatePrompt(
os.Stdin,
fmt.Sprintf("Enter the language of your seed phrase: %s", strings.Join(languages, ", ")),
func(input string) error {
if _, ok := allowedLanguages[input]; !ok {
return errors.New("input not in the list of allowed languages")
}
return nil
},
)
if err != nil {
return "", fmt.Errorf("could not get mnemonic language: %w", err)
}
bip39.SetWordList(allowedLanguages[selectedLanguage])
mnemonicPhrase, err = prompt.ValidatePrompt(
os.Stdin,
"Enter the seed phrase for the wallet you would like to recover",
Web Backend Recover Wallet (#8679) * recover wallet rpc support - first attempt * removed redundant check * separate createwallet into imported and derived. Recover is derived * added unit test for recover. Recover is the createWallet for derived * so proto does CamelCase! * fixed issues related to unit testing * expose ValidateMnemonic from accounts to be used by the rpc module * added Mnemonic25Support and test to ensure it is not et is not empty * added skipMnemonic25thword support and unit test * Update proto/validator/accounts/v2/web_api.proto Proper naming convention Co-authored-by: terence tsao <terence@prysmaticlabs.com> * ran goimports,changed variable to SkipMnemonic_25ThWord * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * fixed variable and text msgs naming convention as per the review * added unit test for strong password on recover * Update proto/validator/accounts/v2/web_api.proto Co-authored-by: Nishant Das <nish1993@hotmail.com> * Update validator/rpc/wallet.go Co-authored-by: Nishant Das <nish1993@hotmail.com> * goimports -w on root proto * added comments after exposing ValidateMnemonic to by used by rpc.RecoverWallet * language should be case insensitive * need to goimports before update protobuf scripts * Update validator/rpc/wallet.go Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * Update validator/rpc/wallet.go comments need to be consistant Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * Update validator/rpc/wallet_test.go consistent comments Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * fix comments * remove the skipMnemonic from rpc, just check if passphrase is empyt * defer call to set the writePassword flag on web boarding * gofmt * fixed the password write test after recovering a wallet. Needed to have two defers. One to set to true then one back to false * restore powchain.pb.go and finalized_block_root_container.pb.go * revert beacon messages.pb.go * revert unrelated files * revert unrelated files * revert unrelated files * unlreated files * restored the imports * Update validator/rpc/wallet_test.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> Co-authored-by: Nishant Das <nish1993@hotmail.com> Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
2021-04-05 20:42:03 +00:00
ValidateMnemonic)
if err != nil {
return "", fmt.Errorf("could not get mnemonic phrase: %w", err)
}
return mnemonicPhrase, nil
}
func inputNumAccounts(cliCtx *cli.Context) (int64, error) {
if cliCtx.IsSet(flags.NumAccountsFlag.Name) {
numAccounts := cliCtx.Int64(flags.NumAccountsFlag.Name)
if numAccounts <= 0 {
return 0, errors.New("must recover at least 1 account")
}
return numAccounts, nil
}
numAccounts, err := prompt.ValidatePrompt(os.Stdin, "Enter how many accounts you would like to generate from the mnemonic", prompt.ValidateNumber)
if err != nil {
return 0, err
}
numAccountsInt, err := strconv.Atoi(numAccounts)
if err != nil {
return 0, err
}
if numAccountsInt <= 0 {
return 0, errors.New("must recover at least 1 account")
}
return int64(numAccountsInt), nil
}
Web Backend Recover Wallet (#8679) * recover wallet rpc support - first attempt * removed redundant check * separate createwallet into imported and derived. Recover is derived * added unit test for recover. Recover is the createWallet for derived * so proto does CamelCase! * fixed issues related to unit testing * expose ValidateMnemonic from accounts to be used by the rpc module * added Mnemonic25Support and test to ensure it is not et is not empty * added skipMnemonic25thword support and unit test * Update proto/validator/accounts/v2/web_api.proto Proper naming convention Co-authored-by: terence tsao <terence@prysmaticlabs.com> * ran goimports,changed variable to SkipMnemonic_25ThWord * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * Update validator/rpc/wallet.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> * fixed variable and text msgs naming convention as per the review * added unit test for strong password on recover * Update proto/validator/accounts/v2/web_api.proto Co-authored-by: Nishant Das <nish1993@hotmail.com> * Update validator/rpc/wallet.go Co-authored-by: Nishant Das <nish1993@hotmail.com> * goimports -w on root proto * added comments after exposing ValidateMnemonic to by used by rpc.RecoverWallet * language should be case insensitive * need to goimports before update protobuf scripts * Update validator/rpc/wallet.go Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * Update validator/rpc/wallet.go comments need to be consistant Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * Update validator/rpc/wallet_test.go consistent comments Co-authored-by: Raul Jordan <raul@prysmaticlabs.com> * fix comments * remove the skipMnemonic from rpc, just check if passphrase is empyt * defer call to set the writePassword flag on web boarding * gofmt * fixed the password write test after recovering a wallet. Needed to have two defers. One to set to true then one back to false * restore powchain.pb.go and finalized_block_root_container.pb.go * revert beacon messages.pb.go * revert unrelated files * revert unrelated files * revert unrelated files * unlreated files * restored the imports * Update validator/rpc/wallet_test.go Co-authored-by: terence tsao <terence@prysmaticlabs.com> Co-authored-by: Nishant Das <nish1993@hotmail.com> Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
2021-04-05 20:42:03 +00:00
// ValidateMnemonic ensures that it is not empty and that the count of the words are
// as specified(currently 24).
func ValidateMnemonic(mnemonic string) error {
if strings.Trim(mnemonic, " ") == "" {
return ErrEmptyMnemonic
}
words := strings.Split(mnemonic, " ")
validWordCount := 0
for _, word := range words {
if strings.Trim(word, " ") == "" {
continue
}
validWordCount += 1
}
if validWordCount != phraseWordCount {
return errors.Wrapf(ErrIncorrectWordNumber, "phrase must be %d words, entered %d", phraseWordCount, validWordCount)
}
return nil
}