prysm-pulse/validator/accounts/v2/prompt.go
Ivan Martinez 607d5fdf4e
Accounts-V2: Change accounts-v2 import to use directory of keystores (#6742)
* Finish implementation of importing dir of keystores

* Fix

* Fix importing

* Finish testing import account

* Update validator/accounts/v2/accounts_import.go

* Fix

* Fix

* fix

* Fix prompt

Co-authored-by: Raul Jordan <raul@prysmaticlabs.com>
2020-07-28 09:18:22 -05:00

352 lines
10 KiB
Go

package v2
import (
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"unicode"
"github.com/logrusorgru/aurora"
"github.com/manifoldco/promptui"
strongPasswords "github.com/nbutton23/zxcvbn-go"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/remote"
"github.com/urfave/cli/v2"
)
const (
importDirPromptText = "Enter the file location of the exported wallet zip to import"
importKeysDirPromptText = "Enter the directory where your keystores to import are located"
exportDirPromptText = "Enter a file location to write the exported wallet to"
walletDirPromptText = "Enter a wallet directory"
passwordsDirPromptText = "Directory where passwords will be stored"
newWalletPasswordPromptText = "New wallet password"
confirmPasswordPromptText = "Confirm password"
walletPasswordPromptText = "Wallet password"
newAccountPasswordPromptText = "New account password"
passwordForAccountPromptText = "Enter password for account %s"
)
type passwordConfirm int
const (
// Constants for passwords.
minPasswordLength = 8
// Min password score of 3 out of 5 based on the https://github.com/nbutton23/zxcvbn-go
// library for strong-entropy password computation.
minPasswordScore = 3
// An enum to indicate the prompt that confirming the password is not needed.
noConfirmPass passwordConfirm = iota
// An enum to indicate the prompt to confirm the password entered.
confirmPass
)
func inputDirectory(cliCtx *cli.Context, promptText string, flag *cli.StringFlag) (string, error) {
directory := appendDirName(cliCtx.String(flag.Name), flag.Name)
if cliCtx.IsSet(flag.Name) {
return directory, nil
}
// Append and log the appropriate directory name depending on the flag used.
if flag.Name == flags.WalletDirFlag.Name {
ok, err := hasDir(directory)
if err != nil {
return "", errors.Wrapf(err, "could not check if wallet dir %s exists", directory)
}
if ok {
au := aurora.NewAurora(true)
log.Infof("%s %s", au.BrightMagenta("(wallet path)"), directory)
return directory, nil
}
} else if flag.Name == flags.WalletPasswordsDirFlag.Name {
ok, err := hasDir(directory)
if err != nil {
return "", errors.Wrapf(err, "could not check if passwords dir %s exists", directory)
}
if ok {
au := aurora.NewAurora(true)
log.Infof("%s %s", au.BrightMagenta("(account passwords path)"), directory)
return directory, nil
}
}
prompt := promptui.Prompt{
Label: promptText,
Validate: validateDirectoryPath,
Default: directory,
}
inputtedDir, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not determine directory: %v", formatPromptError(err))
}
if inputtedDir == prompt.Default {
return directory, nil
}
return appendDirName(inputtedDir, flag.Name), nil
}
func appendDirName(inputtedDir string, flagName string) string {
switch flagName {
case flags.WalletDirFlag.Name:
inputtedDir = filepath.Join(inputtedDir, WalletDefaultDirName)
case flags.WalletPasswordsDirFlag.Name:
inputtedDir = filepath.Join(inputtedDir, PasswordsDefaultDirName)
}
return inputtedDir
}
func validateDirectoryPath(input string) error {
if len(input) == 0 {
return errors.New("directory path must not be empty")
}
return nil
}
func inputPassword(cliCtx *cli.Context, promptText string, confirmPassword passwordConfirm) (string, error) {
if cliCtx.IsSet(flags.PasswordFileFlag.Name) {
passwordFilePath := cliCtx.String(flags.PasswordFileFlag.Name)
data, err := ioutil.ReadFile(passwordFilePath)
if err != nil {
return "", errors.Wrap(err, "could not read password file")
}
enteredPassword := string(data)
if err := validatePasswordInput(enteredPassword); err != nil {
return "", errors.Wrap(err, "password did not pass validation")
}
return strings.TrimRight(enteredPassword, "\r\n"), nil
}
var hasValidPassword bool
var walletPassword string
var err error
for !hasValidPassword {
prompt := promptui.Prompt{
Label: promptText,
Validate: validatePasswordInput,
Mask: '*',
}
walletPassword, err = prompt.Run()
if err != nil {
return "", fmt.Errorf("could not read account password: %v", formatPromptError(err))
}
if confirmPassword == confirmPass {
prompt = promptui.Prompt{
Label: confirmPasswordPromptText,
Mask: '*',
}
confirmPassword, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not read password confirmation: %v", formatPromptError(err))
}
if walletPassword != confirmPassword {
log.Error("Passwords do not match")
continue
}
hasValidPassword = true
} else {
return strings.TrimRight(walletPassword, "\r\n"), nil
}
}
return strings.TrimRight(walletPassword, "\r\n"), nil
}
func inputWeakPassword(cliCtx *cli.Context, promptText string) (string, error) {
if cliCtx.IsSet(flags.PasswordFileFlag.Name) {
passwordFilePath := cliCtx.String(flags.PasswordFileFlag.Name)
data, err := ioutil.ReadFile(passwordFilePath)
if err != nil {
return "", errors.Wrap(err, "could not read password file")
}
return strings.TrimRight(string(data), "\r\n"), nil
}
prompt := promptui.Prompt{
Label: promptText,
Validate: func(input string) error {
if input == "" {
return errors.New("password cannot be empty")
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
return nil
},
Mask: '*',
}
walletPassword, err := prompt.Run()
if err != nil {
return "", fmt.Errorf("could not read account password: %v", formatPromptError(err))
}
return strings.TrimRight(walletPassword, "\r\n"), nil
}
// Validate a strong password input for new accounts,
// including a min length, at least 1 number and at least
// 1 special character.
func validatePasswordInput(input string) error {
var (
hasMinLen = false
hasLetter = false
hasNumber = false
hasSpecial = false
)
if len(input) >= minPasswordLength {
hasMinLen = true
}
for _, char := range input {
switch {
case !(unicode.IsLetter(char) || unicode.IsNumber(char) || unicode.IsPunct(char) || unicode.IsSymbol(char)):
return errors.New("password must only contain alphanumeric characters, punctuation, or symbols")
case unicode.IsLetter(char):
hasLetter = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
if !(hasMinLen && hasLetter && hasNumber && hasSpecial) {
return errors.New(
"password must have more than 8 characters, at least 1 special character, and 1 number",
)
}
strength := strongPasswords.PasswordStrength(input, nil)
if strength.Score < minPasswordScore {
return errors.New(
"password is too easy to guess, try a stronger password",
)
}
return nil
}
func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) {
addr := cliCtx.String(flags.GrpcRemoteAddressFlag.Name)
crt := cliCtx.String(flags.RemoteSignerCertPathFlag.Name)
key := cliCtx.String(flags.RemoteSignerKeyPathFlag.Name)
ca := cliCtx.String(flags.RemoteSignerCACertPathFlag.Name)
if addr != "" && crt != "" && key != "" && ca != "" {
if !(isValidUnicode(addr) && isValidUnicode(crt) && isValidUnicode(key) && isValidUnicode(ca)) {
return nil, errors.New("flag inputs contain non-unicode characters")
}
newCfg := &remote.Config{
RemoteCertificate: &remote.CertificateConfig{
ClientCertPath: strings.TrimRight(crt, "\r\n"),
ClientKeyPath: strings.TrimRight(key, "\r\n"),
CACertPath: strings.TrimRight(ca, "\r\n"),
},
RemoteAddr: strings.TrimRight(addr, "\r\n"),
}
log.Infof("New configuration")
fmt.Printf("%s\n", newCfg)
return newCfg, nil
}
log.Infof("Input desired configuration")
prompt := promptui.Prompt{
Label: "Remote gRPC address (such as host.example.com:4000)",
Validate: func(input string) error {
if input == "" {
return errors.New("remote host address cannot be empty")
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
return nil
},
}
remoteAddr, err := prompt.Run()
if err != nil {
return nil, err
}
prompt = promptui.Prompt{
Label: "Path to TLS crt (such as /path/to/client.crt)",
Validate: validateCertPath,
}
clientCrtPath, err := prompt.Run()
if err != nil {
return nil, err
}
prompt = promptui.Prompt{
Label: "Path to TLS key (such as /path/to/client.key)",
Validate: validateCertPath,
}
clientKeyPath, err := prompt.Run()
if err != nil {
return nil, err
}
prompt = promptui.Prompt{
Label: "(Optional) Path to certificate authority (CA) crt (such as /path/to/ca.crt)",
Validate: validateCACertPath,
}
caCrtPath, err := prompt.Run()
if err != nil {
return nil, err
}
newCfg := &remote.Config{
RemoteCertificate: &remote.CertificateConfig{
ClientCertPath: strings.TrimRight(clientCrtPath, "\r\n"),
ClientKeyPath: strings.TrimRight(clientKeyPath, "\r\n"),
CACertPath: strings.TrimRight(caCrtPath, "\r\n"),
},
RemoteAddr: strings.TrimRight(remoteAddr, "\r\n"),
}
fmt.Printf("%s\n", newCfg)
return newCfg, nil
}
func validateCertPath(input string) error {
if input == "" {
return errors.New("crt path cannot be empty")
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
if !fileExists(input) {
return fmt.Errorf("no crt found at path: %s", input)
}
return nil
}
func validateCACertPath(input string) error {
if input != "" && !fileExists(input) {
return fmt.Errorf("no crt found at path: %s", input)
}
if !isValidUnicode(input) {
return errors.New("not valid unicode")
}
return nil
}
func formatPromptError(err error) error {
switch err {
case promptui.ErrAbort:
return errors.New("wallet creation aborted, closing")
case promptui.ErrInterrupt:
return errors.New("keyboard interrupt, closing")
case promptui.ErrEOF:
return errors.New("no input received, closing")
default:
return err
}
}
// Checks if an input string is a valid unicode string comprised of only
// letters, numbers, punctuation, or symbols.
func isValidUnicode(input string) bool {
for _, char := range input {
if !(unicode.IsLetter(char) ||
unicode.IsNumber(char) ||
unicode.IsPunct(char) ||
unicode.IsSymbol(char)) {
log.Info(char)
return false
}
}
return true
}