Accounts-V2: Replace promptui with shared/promptutil (#6759)

* Add promptutil to shared/promptutil
* fixes
* comment
* Implement promptutil into accounts-v2
* gaz
* Merge branch 'master' of github.com:prysmaticlabs/prysm into implement-promptutil
* Apply suggestions from code review
* gofmt and fix all scanned input
* Merge refs/heads/master into implement-promptutil
This commit is contained in:
Ivan Martinez
2020-07-29 00:55:26 -04:00
committed by GitHub
parent cbd731152e
commit fac5e19a17
9 changed files with 172 additions and 297 deletions

View File

@@ -1,3 +1,4 @@
load("@io_bazel_rules_go//go:def.bzl", "go_test")
load("@prysm//tools/go:def.bzl", "go_library")
go_library(
@@ -14,3 +15,9 @@ go_library(
"@org_golang_x_crypto//ssh/terminal:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["validate_test.go"],
embed = [":go_default_library"],
)

View File

@@ -1,6 +1,8 @@
package promptutil
import (
"bufio"
"errors"
"fmt"
"os"
"strings"
@@ -17,15 +19,17 @@ func ValidatePrompt(promptText string, validateFunc func(string) error) (string,
var response string
for !responseValid {
fmt.Printf("%s:\n", au.Bold(promptText))
_, err := fmt.Scanln(&response)
if err != nil && !strings.Contains(err.Error(), "unexpected newline") {
return "", err
}
response = strings.TrimRight(response, "\r\n")
if err := validateFunc(response); err != nil {
fmt.Printf("Entry not valid: %s\n", au.BrightRed(err))
scanner := bufio.NewScanner(os.Stdin)
if ok := scanner.Scan(); ok {
item := scanner.Text()
response = strings.TrimRight(item, "\r\n")
if err := validateFunc(response); err != nil {
fmt.Printf("Entry not valid: %s\n", au.BrightRed(err))
} else {
responseValid = true
}
} else {
responseValid = true
return "", errors.New("could not scan text input")
}
}
return response, nil
@@ -35,15 +39,16 @@ func ValidatePrompt(promptText string, validateFunc func(string) error) (string,
func DefaultPrompt(promptText string, defaultValue string) (string, error) {
var response string
fmt.Printf("%s %s:\n", promptText, fmt.Sprintf("(%s: %s)", au.BrightGreen("default"), defaultValue))
_, err := fmt.Scanln(&response)
if err != nil && !strings.Contains(err.Error(), "unexpected newline") {
return "", err
scanner := bufio.NewScanner(os.Stdin)
if ok := scanner.Scan(); ok {
item := scanner.Text()
response = strings.TrimRight(item, "\r\n")
if response == "" {
return defaultValue, nil
}
return response, nil
}
response = strings.TrimRight(response, "\r\n")
if response == "" {
return defaultValue, nil
}
return response, nil
return "", errors.New("could not scan text input")
}
// DefaultAndValidatePrompt prompts the user for any text and expects it to fulfill a validation function. If nothing is entered
@@ -53,16 +58,17 @@ func DefaultAndValidatePrompt(promptText string, defaultValue string, validateFu
var response string
for !responseValid {
fmt.Printf("%s %s:\n", promptText, fmt.Sprintf("(%s: %s)", au.BrightGreen("default"), defaultValue))
_, err := fmt.Scanln(&response)
if err != nil && !strings.Contains(err.Error(), "unexpected newline") {
return "", err
}
response = strings.TrimRight(response, "\r\n")
if response == "" {
return defaultValue, nil
}
if err := validateFunc(response); err != nil {
fmt.Printf("Entry not valid: %s\n", au.BrightRed(err))
scanner := bufio.NewScanner(os.Stdin)
if ok := scanner.Scan(); ok {
item := scanner.Text()
response = strings.TrimRight(item, "\r\n")
if err := validateFunc(response); err != nil {
fmt.Printf("Entry not valid: %s\n", au.BrightRed(err))
} else {
responseValid = true
}
} else {
return "", errors.New("could not scan text input")
}
}
return response, nil

View File

@@ -0,0 +1,85 @@
package promptutil
import "testing"
func TestValidatePasswordInput(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "no numbers nor special characters",
input: "abcdefghijklmnopqrs",
wantErr: true,
},
{
name: "number and letters but no special characters",
input: "abcdefghijklmnopqrs2020",
wantErr: true,
},
{
name: "numbers, letters, special characters, but too short",
input: "abc2$",
wantErr: true,
},
{
name: "proper length and strong password",
input: "%Str0ngpassword32kjAjsd22020$%",
wantErr: false,
},
{
name: "password format correct but weak entropy score",
input: "aaaaaaa1$",
wantErr: true,
},
{
name: "Unicode strings separated by a space character",
input: "x*329293@aAJSD i22903saj",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := ValidatePasswordInput(tt.input); (err != nil) != tt.wantErr {
t.Errorf("validatePasswordInput() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func TestIsValidUnicode(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{
name: "Regular alphanumeric",
input: "Someone23xx",
want: true,
},
{
name: "Unicode strings separated by a space character",
input: "x*329293@aAJSD i22903saj",
want: false,
},
{
name: "Japanese",
input: "僕は絵お見るのが好きです",
want: true,
},
{
name: "Other foreign",
input: "Etérium",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsValidUnicode(tt.input); got != tt.want {
t.Errorf("isValidUnicode() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -27,6 +27,7 @@ go_library(
"//shared/featureconfig:go_default_library",
"//shared/params:go_default_library",
"//shared/petnames:go_default_library",
"//shared/promptutil:go_default_library",
"//validator/flags:go_default_library",
"//validator/keymanager/v2:go_default_library",
"//validator/keymanager/v2/derived:go_default_library",
@@ -36,7 +37,6 @@ go_library(
"@com_github_dustinkirkland_golang_petname//:go_default_library",
"@com_github_logrusorgru_aurora//:go_default_library",
"@com_github_manifoldco_promptui//:go_default_library",
"@com_github_nbutton23_zxcvbn_go//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",
"@com_github_urfave_cli_v2//:go_default_library",

View File

@@ -52,85 +52,3 @@ func TestCreateAccount_Derived(t *testing.T) {
assert.NoError(t, err)
require.Equal(t, len(names), int(numAccounts))
}
func Test_validatePasswordInput(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
}{
{
name: "no numbers nor special characters",
input: "abcdefghijklmnopqrs",
wantErr: true,
},
{
name: "number and letters but no special characters",
input: "abcdefghijklmnopqrs2020",
wantErr: true,
},
{
name: "numbers, letters, special characters, but too short",
input: "abc2$",
wantErr: true,
},
{
name: "proper length and strong password",
input: "%Str0ngpassword32kjAjsd22020$%",
wantErr: false,
},
{
name: "password format correct but weak entropy score",
input: "aaaaaaa1$",
wantErr: true,
},
{
name: "Unicode strings separated by a space character",
input: "x*329293@aAJSD i22903saj",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := validatePasswordInput(tt.input); (err != nil) != tt.wantErr {
t.Errorf("validatePasswordInput() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
func Test_isValidUnicode(t *testing.T) {
tests := []struct {
name string
input string
want bool
}{
{
name: "Regular alphanumeric",
input: "Someone23xx",
want: true,
},
{
name: "Unicode strings separated by a space character",
input: "x*329293@aAJSD i22903saj",
want: false,
},
{
name: "Japanese",
input: "僕は絵お見るのが好きです",
want: true,
},
{
name: "Other foreign",
input: "Etérium",
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isValidUnicode(tt.input); got != tt.want {
t.Errorf("isValidUnicode() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -4,12 +4,11 @@ import (
"fmt"
"io/ioutil"
"strings"
"unicode"
"github.com/logrusorgru/aurora"
"github.com/manifoldco/promptui"
strongPasswords "github.com/nbutton23/zxcvbn-go"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/prysmaticlabs/prysm/validator/flags"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/remote"
"github.com/urfave/cli/v2"
@@ -30,17 +29,14 @@ const (
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.
// An enum to indicate to the prompt that confirming the password is not needed.
noConfirmPass passwordConfirm = iota
// An enum to indicate the prompt to confirm the password entered.
// An enum to indicate to the prompt to confirm the password entered.
confirmPass
)
var au = aurora.NewAurora(true)
func inputDirectory(cliCtx *cli.Context, promptText string, flag *cli.StringFlag) (string, error) {
directory := cliCtx.String(flag.Name)
if cliCtx.IsSet(flag.Name) {
@@ -54,7 +50,6 @@ func inputDirectory(cliCtx *cli.Context, promptText string, flag *cli.StringFlag
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
}
@@ -64,34 +59,21 @@ func inputDirectory(cliCtx *cli.Context, promptText string, flag *cli.StringFlag
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()
inputtedDir, err := promptutil.DefaultPrompt(au.Bold(promptText).String(), directory)
if err != nil {
return "", fmt.Errorf("could not determine directory: %v", formatPromptError(err))
return "", err
}
if inputtedDir == prompt.Default {
if inputtedDir == directory {
return directory, nil
}
return inputtedDir, nil
}
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,
passwordFileFlag *cli.StringFlag,
@@ -105,7 +87,7 @@ func inputPassword(
return "", errors.Wrap(err, "could not read password file")
}
enteredPassword := strings.TrimRight(string(data), "\r\n")
if err := validatePasswordInput(enteredPassword); err != nil {
if err := promptutil.ValidatePasswordInput(enteredPassword); err != nil {
return "", errors.Wrap(err, "password did not pass validation")
}
return enteredPassword, nil
@@ -114,35 +96,26 @@ func inputPassword(
var walletPassword string
var err error
for !hasValidPassword {
prompt := promptui.Prompt{
Label: promptText,
Validate: validatePasswordInput,
Mask: '*',
walletPassword, err = promptutil.PasswordPrompt(promptText, promptutil.ValidatePasswordInput)
if err != nil {
return "", fmt.Errorf("could not read account password: %v", err)
}
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()
passwordConfirmation, err := promptutil.PasswordPrompt(confirmPasswordPromptText, promptutil.ValidatePasswordInput)
if err != nil {
return "", fmt.Errorf("could not read password confirmation: %v", formatPromptError(err))
return "", fmt.Errorf("could not read password confirmation: %v", err)
}
if walletPassword != confirmPassword {
if walletPassword != passwordConfirmation {
log.Error("Passwords do not match")
continue
}
hasValidPassword = true
} else {
return strings.TrimRight(walletPassword, "\r\n"), nil
return walletPassword, nil
}
}
return strings.TrimRight(walletPassword, "\r\n"), nil
return walletPassword, nil
}
func inputWeakPassword(cliCtx *cli.Context, passwordFileFlag *cli.StringFlag, promptText string) (string, error) {
@@ -155,64 +128,11 @@ func inputWeakPassword(cliCtx *cli.Context, passwordFileFlag *cli.StringFlag, pr
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()
walletPassword, err := promptutil.PasswordPrompt(promptText, promptutil.NotEmpty)
if err != nil {
return "", fmt.Errorf("could not read account password: %v", formatPromptError(err))
return "", fmt.Errorf("could not read account password: %v", 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
return walletPassword, nil
}
func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) {
@@ -223,60 +143,36 @@ func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) {
log.Info("Input desired configuration")
var err error
if addr == "" {
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
},
}
addr, err = prompt.Run()
addr, err = promptutil.ValidatePrompt("Remote gRPC address (such as host.example.com:4000)", promptutil.NotEmpty)
if err != nil {
return nil, err
}
}
if crt == "" {
prompt := promptui.Prompt{
Label: "Path to TLS crt (such as /path/to/client.crt)",
Validate: validateCertPath,
}
crt, err = prompt.Run()
crt, err = promptutil.ValidatePrompt("Path to TLS crt (such as /path/to/client.crt)", validateCertPath)
if err != nil {
return nil, err
}
}
if key == "" {
prompt := promptui.Prompt{
Label: "Path to TLS key (such as /path/to/client.key)",
Validate: validateCertPath,
}
key, err = prompt.Run()
key, err = promptutil.ValidatePrompt("Path to TLS key (such as /path/to/client.key)", validateCertPath)
if err != nil {
return nil, err
}
}
if ca == "" {
prompt := promptui.Prompt{
Label: "Path to certificate authority (CA) crt (such as /path/to/ca.crt)",
Validate: validateCertPath,
}
ca, err = prompt.Run()
ca, err = promptutil.ValidatePrompt("Path to certificate authority (CA) crt (such as /path/to/ca.crt)", validateCertPath)
if err != nil {
return nil, err
}
}
newCfg := &remote.Config{
RemoteCertificate: &remote.CertificateConfig{
ClientCertPath: strings.TrimRight(crt, "\r\n"),
ClientKeyPath: strings.TrimRight(key, "\r\n"),
CACertPath: strings.TrimRight(ca, "\r\n"),
ClientCertPath: crt,
ClientKeyPath: key,
CACertPath: ca,
},
RemoteAddr: strings.TrimRight(addr, "\r\n"),
RemoteAddr: addr,
}
fmt.Printf("%s\n", newCfg)
return newCfg, nil
@@ -286,7 +182,7 @@ func validateCertPath(input string) error {
if input == "" {
return errors.New("crt path cannot be empty")
}
if !isValidUnicode(input) {
if !promptutil.IsValidUnicode(input) {
return errors.New("not valid unicode")
}
if !fileExists(input) {
@@ -307,18 +203,3 @@ func formatPromptError(err error) error {
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
}

View File

@@ -7,8 +7,8 @@ import (
"strconv"
"strings"
"github.com/manifoldco/promptui"
"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/prysmaticlabs/prysm/validator/flags"
v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2"
"github.com/prysmaticlabs/prysm/validator/keymanager/v2/derived"
@@ -99,15 +99,11 @@ func inputMnemonic(cliCtx *cli.Context) (string, error) {
}
return enteredMnemonic, nil
}
prompt := promptui.Prompt{
Label: "Enter the seed phrase for the wallet you would like to recover",
Validate: validateMnemonic,
}
menmonicPhrase, err := prompt.Run()
mnemonicPhrase, err := promptutil.ValidatePrompt("Enter the seed phrase for the wallet you would like to recover", validateMnemonic)
if err != nil {
return "", fmt.Errorf("could not determine wallet directory: %v", formatPromptError(err))
return "", fmt.Errorf("could not get mnemonic phrase: %v", err)
}
return menmonicPhrase, nil
return mnemonicPhrase, nil
}
func inputNumAccounts(cliCtx *cli.Context) (int64, error) {
@@ -115,20 +111,9 @@ func inputNumAccounts(cliCtx *cli.Context) (int64, error) {
numAccounts := cliCtx.Int64(flags.NumAccountsFlag.Name)
return numAccounts, nil
}
prompt := promptui.Prompt{
Label: "Enter how many accounts you would like to recover",
Validate: func(input string) error {
_, err := strconv.Atoi(input)
if err != nil {
return err
}
return nil
},
Default: "0",
}
numAccounts, err := prompt.Run()
numAccounts, err := promptutil.DefaultAndValidatePrompt("Enter how many accounts you would like to recover", "0", promptutil.ValidateNumber)
if err != nil {
return 0, formatPromptError(err)
return 0, err
}
numAccountsInt, err := strconv.Atoi(numAccounts)
if err != nil {

View File

@@ -18,12 +18,12 @@ go_library(
"//shared/bytesutil:go_default_library",
"//shared/depositutil:go_default_library",
"//shared/petnames:go_default_library",
"//shared/promptutil:go_default_library",
"//shared/rand:go_default_library",
"//shared/roughtime:go_default_library",
"//validator/accounts/v2/iface:go_default_library",
"//validator/keymanager/v2:go_default_library",
"@com_github_google_uuid//:go_default_library",
"@com_github_manifoldco_promptui//:go_default_library",
"@com_github_pkg_errors//:go_default_library",
"@com_github_prysmaticlabs_go_ssz//:go_default_library",
"@com_github_sirupsen_logrus//:go_default_library",

View File

@@ -2,12 +2,13 @@ package derived
import (
"fmt"
"strings"
"github.com/manifoldco/promptui"
"github.com/prysmaticlabs/prysm/shared/promptutil"
"github.com/tyler-smith/go-bip39"
)
const confirmationText = "Confirm you have written down the recovery words somewhere safe (offline) [y|Y]"
// SeedPhraseFactory defines a struct which
// can generate new seed phrases in human-readable
// format from a source of entropy in raw bytes. It
@@ -38,29 +39,21 @@ func (m *EnglishMnemonicGenerator) ConfirmAcknowledgement(phrase string) error {
"Write down the sentence below, as it is your only " +
"means of recovering your wallet",
)
fmt.Printf(`
=================Wallet Seed Recovery Phrase====================
fmt.Printf(
`=================Wallet Seed Recovery Phrase====================
%s
===================================================================
`, phrase)
===================================================================`,
phrase)
fmt.Println("")
if m.skipMnemonicConfirm {
return nil
}
// Confirm the user has written down the mnemonic phrase offline.
prompt := promptui.Prompt{
Label: "Confirm you have written down the recovery words somewhere safe (offline)",
IsConfirm: true,
}
expected := "y"
var result string
var err error
for strings.ToLower(result) != expected {
result, err = prompt.Run()
if err != nil {
log.Errorf("Could not confirm acknowledgement of prompt, please enter y")
}
_, err := promptutil.ValidatePrompt(confirmationText, promptutil.ValidateConfirmation)
if err != nil {
log.Errorf("Could not confirm acknowledgement of prompt, please enter y")
}
return nil
}