diff --git a/shared/cmd/BUILD.bazel b/shared/cmd/BUILD.bazel index 667431882b..1cc850f8b4 100644 --- a/shared/cmd/BUILD.bazel +++ b/shared/cmd/BUILD.bazel @@ -5,7 +5,6 @@ go_library( name = "go_default_library", srcs = [ "config.go", - "customflags.go", "defaults.go", "flags.go", "helpers.go", @@ -16,6 +15,7 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/shared/cmd", visibility = ["//visibility:public"], deps = [ + "//shared/fileutil:go_default_library", "//shared/params:go_default_library", "@com_github_golang_mock//gomock:go_default_library", "@com_github_pkg_errors//:go_default_library", @@ -31,7 +31,6 @@ go_test( size = "small", srcs = [ "config_test.go", - "customflags_test.go", "helpers_test.go", ], embed = [":go_default_library"], diff --git a/shared/cmd/customflags.go b/shared/cmd/customflags.go deleted file mode 100644 index a1d5bad6ff..0000000000 --- a/shared/cmd/customflags.go +++ /dev/null @@ -1,32 +0,0 @@ -package cmd - -import ( - "os" - "os/user" - "path" - "strings" -) - -// Expands a file path -// 1. replace tilde with users home dir -// 2. expands embedded environment variables -// 3. cleans the path, e.g. /a/b/../c -> /a/c -// Note, it has limitations, e.g. ~someuser/tmp will not be expanded -func expandPath(p string) string { - if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { - if home := homeDir(); home != "" { - p = home + p[1:] - } - } - return path.Clean(os.ExpandEnv(p)) -} - -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - if usr, err := user.Current(); err == nil { - return usr.HomeDir - } - return "" -} diff --git a/shared/cmd/defaults.go b/shared/cmd/defaults.go index b9c2b3f60d..e7538991d5 100644 --- a/shared/cmd/defaults.go +++ b/shared/cmd/defaults.go @@ -19,13 +19,15 @@ package cmd import ( "path/filepath" "runtime" + + "github.com/prysmaticlabs/prysm/shared/fileutil" ) // DefaultDataDir is the default data directory to use for the databases and other // persistence requirements. func DefaultDataDir() string { // Try to place the data folder in the user's home dir - home := homeDir() + home := fileutil.HomeDir() if home != "" { if runtime.GOOS == "darwin" { return filepath.Join(home, "Library", "Eth2") diff --git a/shared/fileutil/BUILD.bazel b/shared/fileutil/BUILD.bazel new file mode 100644 index 0000000000..454989ffc5 --- /dev/null +++ b/shared/fileutil/BUILD.bazel @@ -0,0 +1,20 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") +load("@prysm//tools/go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = ["fileutil.go"], + importpath = "github.com/prysmaticlabs/prysm/shared/fileutil", + visibility = ["//visibility:public"], + deps = ["@com_github_pkg_errors//:go_default_library"], +) + +go_test( + name = "go_default_test", + srcs = ["fileutil_test.go"], + embed = [":go_default_library"], + deps = [ + "//shared/testutil/assert:go_default_library", + "//shared/testutil/require:go_default_library", + ], +) diff --git a/shared/fileutil/fileutil.go b/shared/fileutil/fileutil.go new file mode 100644 index 0000000000..297fad87f3 --- /dev/null +++ b/shared/fileutil/fileutil.go @@ -0,0 +1,76 @@ +package fileutil + +import ( + "io/ioutil" + "os" + "os/user" + "path" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +// ExpandPath given a string which may be a relative path. +// 1. replace tilde with users home dir +// 2. expands embedded environment variables +// 3. cleans the path, e.g. /a/b/../c -> /a/c +// Note, it has limitations, e.g. ~someuser/tmp will not be expanded +func ExpandPath(p string) (string, error) { + if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { + if home := HomeDir(); home != "" { + p = home + p[1:] + } + } + return filepath.Abs(path.Clean(os.ExpandEnv(p))) +} + +// HomeDir for a user. +func HomeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + if usr, err := user.Current(); err == nil { + return usr.HomeDir + } + return "" +} + +// HasDir checks if a directory indeed exists at the specified path. +func HasDir(dirPath string) (bool, error) { + fullPath, err := ExpandPath(dirPath) + if err != nil { + return false, err + } + info, err := os.Stat(fullPath) + if os.IsNotExist(err) { + return false, nil + } + if info == nil { + return false, err + } + return info.IsDir(), err +} + +// FileExists returns true if a file is not a directory and exists +// at the specified path. +func FileExists(filename string) bool { + filePath, err := ExpandPath(filename) + if err != nil { + return false + } + info, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +// ReadFileAsBytes expands a file name's absolute path and reads it as bytes from disk. +func ReadFileAsBytes(filename string) ([]byte, error) { + filePath, err := ExpandPath(filename) + if err != nil { + return nil, errors.Wrap(err, "could not determine absolute path of password file") + } + return ioutil.ReadFile(filePath) +} diff --git a/shared/cmd/customflags_test.go b/shared/fileutil/fileutil_test.go similarity index 87% rename from shared/cmd/customflags_test.go rename to shared/fileutil/fileutil_test.go index 8b7893577e..3bf2a99edd 100644 --- a/shared/cmd/customflags_test.go +++ b/shared/fileutil/fileutil_test.go @@ -13,8 +13,7 @@ // // You should have received a copy of the GNU General Public License // along with go-ethereum. If not, see . - -package cmd +package fileutil import ( "os" @@ -22,6 +21,7 @@ import ( "testing" "github.com/prysmaticlabs/prysm/shared/testutil/assert" + "github.com/prysmaticlabs/prysm/shared/testutil/require" ) func TestPathExpansion(t *testing.T) { @@ -32,7 +32,6 @@ func TestPathExpansion(t *testing.T) { tests := map[string]string{ "/home/someuser/tmp": "/home/someuser/tmp", "~/tmp": user.HomeDir + "/tmp", - "~thisOtherUser/b/": "~thisOtherUser/b", "$DDDXXX/a/b": "/tmp/a/b", "/a/b/": "/a/b", } @@ -41,6 +40,8 @@ func TestPathExpansion(t *testing.T) { t.Error(err) } for test, expected := range tests { - assert.Equal(t, expected, expandPath(test)) + expanded, err := ExpandPath(test) + require.NoError(t, err) + assert.Equal(t, expected, expanded) } } diff --git a/tools/keystores/BUILD.bazel b/tools/keystores/BUILD.bazel index eda307ffd3..fefaeebd84 100644 --- a/tools/keystores/BUILD.bazel +++ b/tools/keystores/BUILD.bazel @@ -8,6 +8,7 @@ go_library( visibility = ["//visibility:private"], deps = [ "//shared/bls:go_default_library", + "//shared/fileutil:go_default_library", "//shared/params:go_default_library", "//shared/promptutil:go_default_library", "//validator/keymanager/v2:go_default_library", diff --git a/tools/keystores/main.go b/tools/keystores/main.go index 38aca6c558..db8a007767 100644 --- a/tools/keystores/main.go +++ b/tools/keystores/main.go @@ -11,8 +11,6 @@ import ( "io/ioutil" "log" "os" - "os/user" - "path" "path/filepath" "strings" @@ -20,6 +18,7 @@ import ( "github.com/logrusorgru/aurora" "github.com/pkg/errors" "github.com/prysmaticlabs/prysm/shared/bls" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/promptutil" v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2" @@ -92,7 +91,7 @@ func decrypt(cliCtx *cli.Context) error { if keystorePath == "" { return errors.New("--keystore must be set") } - fullPath, err := expandPath(keystorePath) + fullPath, err := fileutil.ExpandPath(keystorePath) if err != nil { return errors.Wrapf(err, "could not expand path: %s", keystorePath) } @@ -104,7 +103,7 @@ func decrypt(cliCtx *cli.Context) error { return nil }) } - isDir, err := hasDir(fullPath) + isDir, err := fileutil.HasDir(fullPath) if err != nil { return errors.Wrapf(err, "could not check if path exists: %s", fullPath) } @@ -149,15 +148,11 @@ func encrypt(cliCtx *cli.Context) error { if outputPath == "" { return errors.New("--output-path must be set") } - fullPath, err := expandPath(outputPath) + fullPath, err := fileutil.ExpandPath(outputPath) if err != nil { return errors.Wrapf(err, "could not expand path: %s", outputPath) } - exists, err := fileExists(fullPath) - if err != nil { - return errors.Wrapf(err, "could not check if file exists: %s", fullPath) - } - if exists { + if fileutil.FileExists(fullPath) { response, err := promptutil.ValidatePrompt( fmt.Sprintf("file at path %s already exists, are you sure you want to overwrite it? [y/n]", fullPath), func(s string) error { @@ -263,58 +258,3 @@ func readAndDecryptKeystore(fullPath string, password string) error { fmt.Printf("Pubkey: %#x\n", au.BrightGreen(pubKeyBytes)) return nil } - -// Checks if the item at the specified path exists and is a directory. -func hasDir(dirPath string) (bool, error) { - fullPath, err := expandPath(dirPath) - if err != nil { - return false, err - } - info, err := os.Stat(fullPath) - if err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return info.IsDir(), nil -} - -// Check if a file at the specified path exists. -func fileExists(filePath string) (bool, error) { - fullPath, err := expandPath(filePath) - if err != nil { - return false, err - } - if _, err := os.Stat(fullPath); err != nil { - if os.IsNotExist(err) { - return false, nil - } - return false, err - } - return true, nil -} - -// Expands a file path -// 1. replace tilde with users home dir -// 2. expands embedded environment variables -// 3. cleans the path, e.g. /a/b/../c -> /a/c -// Note, it has limitations, e.g. ~someuser/tmp will not be expanded -func expandPath(p string) (string, error) { - if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { - if home := homeDir(); home != "" { - p = home + p[1:] - } - } - return filepath.Abs(path.Clean(os.ExpandEnv(p))) -} - -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - if usr, err := user.Current(); err == nil { - return usr.HomeDir - } - return "" -} diff --git a/validator/accounts/v1/BUILD.bazel b/validator/accounts/v1/BUILD.bazel index 39460c2291..eee3c98121 100644 --- a/validator/accounts/v1/BUILD.bazel +++ b/validator/accounts/v1/BUILD.bazel @@ -16,6 +16,7 @@ go_library( "//contracts/deposit-contract:go_default_library", "//shared/cmd:go_default_library", "//shared/depositutil:go_default_library", + "//shared/fileutil:go_default_library", "//shared/keystore:go_default_library", "//shared/params:go_default_library", "//validator/db/kv:go_default_library", diff --git a/validator/accounts/v1/account.go b/validator/accounts/v1/account.go index a120927d54..c9f7a152ce 100644 --- a/validator/accounts/v1/account.go +++ b/validator/accounts/v1/account.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "os" - "os/user" "path/filepath" "runtime" "strings" @@ -17,6 +16,7 @@ import ( contract "github.com/prysmaticlabs/prysm/contracts/deposit-contract" "github.com/prysmaticlabs/prysm/shared/cmd" "github.com/prysmaticlabs/prysm/shared/depositutil" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/keystore" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/validator/db/kv" @@ -190,7 +190,7 @@ func PrintPublicAndPrivateKeys(path string, passphrase string) error { // DefaultValidatorDir returns OS-specific default keystore directory. func DefaultValidatorDir() string { // Try to place the data folder in the user's home dir - home := homeDir() + home := fileutil.HomeDir() if home != "" { if runtime.GOOS == "darwin" { return filepath.Join(home, "Library", "Eth2Validators") @@ -336,17 +336,6 @@ func changePasswordForKeyType(keystorePath string, filePrefix string, oldPasswor return nil } -// homeDir returns home directory path. -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - if usr, err := user.Current(); err == nil { - return usr.HomeDir - } - return "" -} - // ExtractPublicKeysFromKeyStore extracts only the public keys from the decrypted keys from the keystore. func ExtractPublicKeysFromKeyStore(keystorePath string, passphrase string) ([][]byte, error) { decryptedKeys, err := DecryptKeysFromKeystore(keystorePath, params.BeaconConfig().ValidatorPrivkeyFileName, passphrase) diff --git a/validator/accounts/v2/BUILD.bazel b/validator/accounts/v2/BUILD.bazel index 6e87bc7fde..e2670839de 100644 --- a/validator/accounts/v2/BUILD.bazel +++ b/validator/accounts/v2/BUILD.bazel @@ -23,8 +23,8 @@ go_library( "//validator:__subpackages__", ], deps = [ - "//shared/bytesutil:go_default_library", "//shared/featureconfig:go_default_library", + "//shared/fileutil:go_default_library", "//shared/params:go_default_library", "//shared/petnames:go_default_library", "//shared/promptutil:go_default_library", @@ -34,16 +34,13 @@ go_library( "//validator/keymanager/v2/direct:go_default_library", "//validator/keymanager/v2/remote:go_default_library", "@com_github_gofrs_flock//:go_default_library", - "@com_github_k0kubun_go_ansi//:go_default_library", "@com_github_logrusorgru_aurora//:go_default_library", "@com_github_manifoldco_promptui//:go_default_library", "@com_github_pkg_errors//:go_default_library", - "@com_github_schollz_progressbar_v3//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", "@com_github_tyler_smith_go_bip39//wordlists:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", - "@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library", ], ) @@ -53,7 +50,6 @@ go_test( "accounts_create_test.go", "accounts_import_test.go", "accounts_list_test.go", - "consts_test.go", "wallet_create_test.go", "wallet_edit_test.go", "wallet_recover_test.go", diff --git a/validator/accounts/v2/accounts_create.go b/validator/accounts/v2/accounts_create.go index 85be74b339..cc4640118b 100644 --- a/validator/accounts/v2/accounts_create.go +++ b/validator/accounts/v2/accounts_create.go @@ -26,7 +26,7 @@ func CreateAccount(cliCtx *cli.Context) error { return err } skipMnemonicConfirm := cliCtx.Bool(flags.SkipMnemonicConfirmFlag.Name) - keymanager, err := wallet.InitializeKeymanager(ctx, skipMnemonicConfirm) + keymanager, err := wallet.InitializeKeymanager(cliCtx, skipMnemonicConfirm) if err != nil && strings.Contains(err.Error(), "invalid checksum") { return errors.New("wrong wallet password entered") } diff --git a/validator/accounts/v2/accounts_create_test.go b/validator/accounts/v2/accounts_create_test.go index 3851fa094f..69b14e8e56 100644 --- a/validator/accounts/v2/accounts_create_test.go +++ b/validator/accounts/v2/accounts_create_test.go @@ -42,7 +42,7 @@ func TestCreateAccount_Derived(t *testing.T) { require.NoError(t, CreateAccount(cliCtx)) - keymanager, err := wallet.InitializeKeymanager(ctx, true) + keymanager, err := wallet.InitializeKeymanager(cliCtx, true) require.NoError(t, err) km, ok := keymanager.(*derived.Keymanager) if !ok { diff --git a/validator/accounts/v2/accounts_export.go b/validator/accounts/v2/accounts_export.go index f38972f5bf..c1d854a8d1 100644 --- a/validator/accounts/v2/accounts_export.go +++ b/validator/accounts/v2/accounts_export.go @@ -12,6 +12,7 @@ import ( "github.com/logrusorgru/aurora" "github.com/manifoldco/promptui" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/validator/flags" "github.com/urfave/cli/v2" @@ -94,7 +95,7 @@ func (w *Wallet) zipAccounts(accounts []string, targetPath string) error { if err := os.MkdirAll(targetPath, params.BeaconIoConfig().ReadWriteExecutePermissions); err != nil { return errors.Wrap(err, "could not create target folder") } - if fileExists(archivePath) { + if fileutil.FileExists(archivePath) { return errors.Errorf("Zip file already exists in directory: %s", archivePath) } zipfile, err := os.Create(archivePath) diff --git a/validator/accounts/v2/accounts_import.go b/validator/accounts/v2/accounts_import.go index 42348764f9..655ced068d 100644 --- a/validator/accounts/v2/accounts_import.go +++ b/validator/accounts/v2/accounts_import.go @@ -13,6 +13,7 @@ import ( "github.com/logrusorgru/aurora" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/validator/flags" v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct" @@ -91,7 +92,7 @@ func ImportAccount(cliCtx *cli.Context) error { if err != nil { return err } - km, err := direct.NewKeymanager(ctx, wallet, directCfg) + km, err := direct.NewKeymanager(cliCtx, wallet, directCfg) if err != nil { return err } @@ -99,10 +100,7 @@ func ImportAccount(cliCtx *cli.Context) error { if err != nil { return errors.Wrap(err, "could not parse keys directory") } - if err := wallet.SaveWallet(); err != nil { - return errors.Wrap(err, "could not save wallet") - } - isDir, err := hasDir(keysDir) + isDir, err := fileutil.HasDir(keysDir) if err != nil { return errors.Wrap(err, "could not determine if path is a directory") } @@ -131,14 +129,14 @@ func ImportAccount(cliCtx *cli.Context) error { // specify this value in their filename. sort.Sort(byDerivationPath(keystoreFileNames)) for _, name := range keystoreFileNames { - keystore, err := wallet.readKeystoreFile(ctx, filepath.Join(keysDir, name)) + keystore, err := readKeystoreFile(ctx, filepath.Join(keysDir, name)) if err != nil { return errors.Wrapf(err, "could not import keystore at path: %s", name) } keystoresImported = append(keystoresImported, keystore) } } else { - keystore, err := wallet.readKeystoreFile(ctx, keysDir) + keystore, err := readKeystoreFile(ctx, keysDir) if err != nil { return errors.Wrap(err, "could not import keystore") } @@ -156,7 +154,7 @@ func ImportAccount(cliCtx *cli.Context) error { return nil } -func (w *Wallet) readKeystoreFile(ctx context.Context, keystoreFilePath string) (*v2keymanager.Keystore, error) { +func readKeystoreFile(ctx context.Context, keystoreFilePath string) (*v2keymanager.Keystore, error) { keystoreBytes, err := ioutil.ReadFile(keystoreFilePath) if err != nil { return nil, errors.Wrap(err, "could not read keystore file") diff --git a/validator/accounts/v2/accounts_import_test.go b/validator/accounts/v2/accounts_import_test.go index 2a36678bde..db0ae90cfe 100644 --- a/validator/accounts/v2/accounts_import_test.go +++ b/validator/accounts/v2/accounts_import_test.go @@ -50,7 +50,7 @@ func TestImport_Noninteractive(t *testing.T) { require.NoError(t, err) require.NoError(t, wallet.WriteKeymanagerConfigToDisk(ctx, encodedCfg)) keymanager, err := direct.NewKeymanager( - ctx, + cliCtx, wallet, direct.DefaultConfig(), ) @@ -70,7 +70,7 @@ func TestImport_Noninteractive(t *testing.T) { wallet, err = OpenWallet(cliCtx) require.NoError(t, err) - km, err := wallet.InitializeKeymanager(ctx, true) + km, err := wallet.InitializeKeymanager(cliCtx, true) require.NoError(t, err) keys, err := km.FetchValidatingPublicKeys(ctx) require.NoError(t, err) @@ -105,7 +105,7 @@ func TestImport_Noninteractive_Filepath(t *testing.T) { require.NoError(t, err) require.NoError(t, wallet.WriteKeymanagerConfigToDisk(ctx, encodedCfg)) keymanager, err := direct.NewKeymanager( - ctx, + cliCtx, wallet, keymanagerCfg, ) @@ -120,7 +120,7 @@ func TestImport_Noninteractive_Filepath(t *testing.T) { wallet, err = OpenWallet(cliCtx) require.NoError(t, err) - km, err := wallet.InitializeKeymanager(ctx, true) + km, err := wallet.InitializeKeymanager(cliCtx, true) require.NoError(t, err) keys, err := km.FetchValidatingPublicKeys(ctx) require.NoError(t, err) diff --git a/validator/accounts/v2/accounts_list.go b/validator/accounts/v2/accounts_list.go index b238d71a24..7971ec51cd 100644 --- a/validator/accounts/v2/accounts_list.go +++ b/validator/accounts/v2/accounts_list.go @@ -20,14 +20,13 @@ import ( // ListAccounts displays all available validator accounts in a Prysm wallet. func ListAccounts(cliCtx *cli.Context) error { // Read the wallet from the specified path. - ctx := context.Background() wallet, err := OpenWallet(cliCtx) if errors.Is(err, ErrNoWalletFound) { return errors.Wrap(err, "no wallet found at path, create a new wallet with wallet-v2 create") } else if err != nil { return errors.Wrap(err, "could not open wallet") } - keymanager, err := wallet.InitializeKeymanager(ctx, true /* skip mnemonic confirm */) + keymanager, err := wallet.InitializeKeymanager(cliCtx, true /* skip mnemonic confirm */) if err != nil && strings.Contains(err.Error(), "invalid checksum") { return errors.New("wrong wallet password entered") } diff --git a/validator/accounts/v2/accounts_list_test.go b/validator/accounts/v2/accounts_list_test.go index fd0cc6a039..14ac384fbb 100644 --- a/validator/accounts/v2/accounts_list_test.go +++ b/validator/accounts/v2/accounts_list_test.go @@ -45,7 +45,7 @@ func TestListAccounts_DirectKeymanager(t *testing.T) { require.NoError(t, wallet.SaveWallet()) ctx := context.Background() keymanager, err := direct.NewKeymanager( - ctx, + cliCtx, wallet, direct.DefaultConfig(), ) @@ -108,20 +108,11 @@ func TestListAccounts_DerivedKeymanager(t *testing.T) { require.NoError(t, wallet.SaveWallet()) ctx := context.Background() - seedConfig, err := derived.InitializeWalletSeedFile(ctx, password, true /* skip confirm */) - require.NoError(t, err) - - // Create a new wallet seed file and write it to disk. - seedConfigFile, err := derived.MarshalEncryptedSeedFile(ctx, seedConfig) - require.NoError(t, err) - require.NoError(t, wallet.WriteFileAtPath(ctx, "", derived.EncryptedSeedFileName, seedConfigFile)) - keymanager, err := derived.NewKeymanager( - ctx, + cliCtx, wallet, derived.DefaultConfig(), true, /* skip confirm */ - password, ) require.NoError(t, err) diff --git a/validator/accounts/v2/consts_test.go b/validator/accounts/v2/consts_test.go deleted file mode 100644 index fbb783735e..0000000000 --- a/validator/accounts/v2/consts_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package v2 - -const ( - walletDirName = "wallet" - passwordDirName = "walletpasswords" - exportDirName = "export" - importDirName = "import" - importPasswordDirName = "importpasswords" - passwordFileName = "password.txt" - password = "OhWOWthisisatest42!$" - mnemonicFileName = "mnemonic.txt" - mnemonic = "garage car helmet trade salmon embrace market giant movie wet same champion dawn chair shield drill amazing panther accident puzzle garden mosquito kind arena" -) diff --git a/validator/accounts/v2/iface/wallet.go b/validator/accounts/v2/iface/wallet.go index 75eca5cf8c..8e1b458afe 100644 --- a/validator/accounts/v2/iface/wallet.go +++ b/validator/accounts/v2/iface/wallet.go @@ -12,7 +12,6 @@ type Wallet interface { // Methods to retrieve wallet and accounts metadata. AccountsDir() string ListDirs() ([]string, error) - Password() string // Read methods for important wallet and accounts-related files. ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error) ReadFileAtPath(ctx context.Context, filePath string, fileName string) ([]byte, error) diff --git a/validator/accounts/v2/prompt.go b/validator/accounts/v2/prompt.go index d09fb2b667..5fd36becbe 100644 --- a/validator/accounts/v2/prompt.go +++ b/validator/accounts/v2/prompt.go @@ -3,15 +3,12 @@ package v2 import ( "fmt" "io/ioutil" - "os" - "os/user" - "path" - "path/filepath" "strings" "github.com/logrusorgru/aurora" "github.com/manifoldco/promptui" "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/promptutil" "github.com/prysmaticlabs/prysm/validator/flags" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/remote" @@ -19,23 +16,8 @@ import ( ) const ( - importKeysDirPromptText = "Enter the directory or filepath where your keystores to import are located" - exportDirPromptText = "Enter a file location to write the exported account(s) to" - walletDirPromptText = "Enter a wallet directory" - newWalletPasswordPromptText = "New wallet password" - confirmPasswordPromptText = "Confirm password" - walletPasswordPromptText = "Wallet password" - newAccountPasswordPromptText = "New account password" - passwordForAccountPromptText = "Enter password for account with public key %#x" -) - -type passwordConfirm int - -const ( - // An enum to indicate to the prompt that confirming the password is not needed. - noConfirmPass passwordConfirm = iota - // An enum to indicate to the prompt to confirm the password entered. - confirmPass + importKeysDirPromptText = "Enter the directory or filepath where your keystores to import are located" + walletDirPromptText = "Enter a wallet directory" ) var au = aurora.NewAurora(true) @@ -43,11 +25,11 @@ 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) { - return expandPath(directory) + return fileutil.ExpandPath(directory) } // Append and log the appropriate directory name depending on the flag used. if flag.Name == flags.WalletDirFlag.Name { - ok, err := hasDir(directory) + ok, err := fileutil.HasDir(directory) if err != nil { return "", errors.Wrapf(err, "could not check if wallet dir %s exists", directory) } @@ -64,62 +46,13 @@ func inputDirectory(cliCtx *cli.Context, promptText string, flag *cli.StringFlag if inputtedDir == directory { return directory, nil } - return expandPath(inputtedDir) -} - -func inputPassword( - cliCtx *cli.Context, - passwordFileFlag *cli.StringFlag, - promptText string, - confirmPassword passwordConfirm, - passwordValidator func(input string) error, -) (string, error) { - if cliCtx.IsSet(passwordFileFlag.Name) { - passwordFilePathInput := cliCtx.String(passwordFileFlag.Name) - passwordFilePath, err := expandPath(passwordFilePathInput) - if err != nil { - return "", errors.Wrap(err, "could not determine absolute path of password file") - } - data, err := ioutil.ReadFile(passwordFilePath) - if err != nil { - return "", errors.Wrap(err, "could not read password file") - } - enteredPassword := strings.TrimRight(string(data), "\r\n") - if err := passwordValidator(enteredPassword); err != nil { - return "", errors.Wrap(err, "password did not pass validation") - } - return enteredPassword, nil - } - var hasValidPassword bool - var walletPassword string - var err error - for !hasValidPassword { - walletPassword, err = promptutil.PasswordPrompt(promptText, passwordValidator) - if err != nil { - return "", fmt.Errorf("could not read account password: %v", err) - } - - if confirmPassword == confirmPass { - passwordConfirmation, err := promptutil.PasswordPrompt(confirmPasswordPromptText, passwordValidator) - if err != nil { - return "", fmt.Errorf("could not read password confirmation: %v", err) - } - if walletPassword != passwordConfirmation { - log.Error("Passwords do not match") - continue - } - hasValidPassword = true - } else { - return walletPassword, nil - } - } - return walletPassword, nil + return fileutil.ExpandPath(inputtedDir) } func inputWeakPassword(cliCtx *cli.Context, passwordFileFlag *cli.StringFlag, promptText string) (string, error) { if cliCtx.IsSet(passwordFileFlag.Name) { passwordFilePathInput := cliCtx.String(passwordFileFlag.Name) - passwordFilePath, err := expandPath(passwordFilePathInput) + passwordFilePath, err := fileutil.ExpandPath(passwordFilePathInput) if err != nil { return "", errors.Wrap(err, "could not determine absolute path of password file") } @@ -167,15 +100,15 @@ func inputRemoteKeymanagerConfig(cliCtx *cli.Context) (*remote.Config, error) { return nil, err } } - crtPath, err := expandPath(strings.TrimRight(crt, "\r\n")) + crtPath, err := fileutil.ExpandPath(strings.TrimRight(crt, "\r\n")) if err != nil { return nil, errors.Wrapf(err, "could not determine absolute path for %s", crt) } - keyPath, err := expandPath(strings.TrimRight(key, "\r\n")) + keyPath, err := fileutil.ExpandPath(strings.TrimRight(key, "\r\n")) if err != nil { return nil, errors.Wrapf(err, "could not determine absolute path for %s", crt) } - caPath, err := expandPath(strings.TrimRight(ca, "\r\n")) + caPath, err := fileutil.ExpandPath(strings.TrimRight(ca, "\r\n")) if err != nil { return nil, errors.Wrapf(err, "could not determine absolute path for %s", crt) } @@ -198,7 +131,7 @@ func validateCertPath(input string) error { if !promptutil.IsValidUnicode(input) { return errors.New("not valid unicode") } - if !fileExists(input) { + if !fileutil.FileExists(input) { return fmt.Errorf("no crt found at path: %s", input) } return nil @@ -216,27 +149,3 @@ func formatPromptError(err error) error { return err } } - -// Expands a file path -// 1. replace tilde with users home dir -// 2. expands embedded environment variables -// 3. cleans the path, e.g. /a/b/../c -> /a/c -// Note, it has limitations, e.g. ~someuser/tmp will not be expanded -func expandPath(p string) (string, error) { - if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { - if home := homeDir(); home != "" { - p = home + p[1:] - } - } - return filepath.Abs(path.Clean(os.ExpandEnv(p))) -} - -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - if usr, err := user.Current(); err == nil { - return usr.HomeDir - } - return "" -} diff --git a/validator/accounts/v2/testing/mock.go b/validator/accounts/v2/testing/mock.go index 2fafeb8b4f..57450d3dd8 100644 --- a/validator/accounts/v2/testing/mock.go +++ b/validator/accounts/v2/testing/mock.go @@ -18,7 +18,6 @@ type Wallet struct { EncryptedSeedFile []byte AccountPasswords map[string]string UnlockAccounts bool - WalletPassword string lock sync.RWMutex } @@ -38,11 +37,6 @@ func (m *Wallet) AccountsDir() string { return m.InnerAccountsDir } -// Password -- -func (m *Wallet) Password() string { - return m.WalletPassword -} - // ListDirs -- func (m *Wallet) ListDirs() ([]string, error) { return m.Directories, nil diff --git a/validator/accounts/v2/wallet.go b/validator/accounts/v2/wallet.go index ff99f51f5e..4bb2727d69 100644 --- a/validator/accounts/v2/wallet.go +++ b/validator/accounts/v2/wallet.go @@ -2,32 +2,23 @@ package v2 import ( "context" - "encoding/json" "fmt" "io" "io/ioutil" "os" "path/filepath" - "strconv" - "strings" - "time" "github.com/gofrs/flock" - "github.com/k0kubun/go-ansi" - "github.com/logrusorgru/aurora" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/params" - "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" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/remote" - "github.com/schollz/progressbar/v3" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" - keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" ) const ( @@ -63,7 +54,6 @@ type Wallet struct { configFilePath string walletFileLock *flock.Flock keymanagerKind v2keymanager.Kind - walletPassword string } // NewWallet given a set of configuration options, will leverage @@ -76,7 +66,7 @@ func NewWallet( // Check if the user has a wallet at the specified path. // If a user does not have a wallet, we instantiate one // based on specified options. - walletExists, err := hasDir(walletDir) + walletExists, err := fileutil.HasDir(walletDir) if err != nil { return nil, errors.Wrap(err, "could not check if wallet exists") } @@ -90,25 +80,11 @@ func NewWallet( } } accountsPath := filepath.Join(walletDir, keymanagerKind.String()) - w := &Wallet{ + return &Wallet{ accountsPath: accountsPath, keymanagerKind: keymanagerKind, walletDir: walletDir, - } - if keymanagerKind == v2keymanager.Derived || keymanagerKind == v2keymanager.Direct { - walletPassword, err := inputPassword( - cliCtx, - flags.WalletPasswordFileFlag, - newWalletPasswordPromptText, - confirmPass, - promptutil.ValidatePasswordInput, - ) - if err != nil { - return nil, errors.Wrap(err, "could not get password") - } - w.walletPassword = walletPassword - } - return w, nil + }, nil } // OpenWallet instantiates a wallet from a specified path. It checks the @@ -120,7 +96,7 @@ func OpenWallet(cliCtx *cli.Context) (*Wallet, error) { if err != nil { return nil, err } - ok, err := hasDir(walletDir) + ok, err := fileutil.HasDir(walletDir) if err != nil { return nil, errors.Wrap(err, "could not parse wallet directory") } @@ -140,71 +116,12 @@ func OpenWallet(cliCtx *cli.Context) (*Wallet, error) { return nil, errors.Wrap(err, "could not read keymanager kind for wallet") } walletPath := filepath.Join(walletDir, keymanagerKind.String()) - w := &Wallet{ + log.Infof("%s %s", au.BrightMagenta("(wallet directory)"), walletDir) + return &Wallet{ walletDir: walletDir, accountsPath: walletPath, keymanagerKind: keymanagerKind, - } - // Check if the wallet is using the new, fast keystore format. - hasNewFormat, err := hasDir(filepath.Join(walletPath, direct.AccountsPath)) - if err != nil { - return nil, errors.Wrap(err, "could not read wallet dir") - } - log.Infof("%s %s", au.BrightMagenta("(wallet directory)"), w.walletDir) - if keymanagerKind == v2keymanager.Derived { - validateExistingPass := func(input string) error { - if input == "" { - return errors.New("password input cannot be empty") - } - return nil - } - walletPassword, err := inputPassword( - cliCtx, - flags.WalletPasswordFileFlag, - walletPasswordPromptText, - noConfirmPass, - validateExistingPass, - ) - if err != nil { - return nil, err - } - w.walletPassword = walletPassword - } - if keymanagerKind == v2keymanager.Direct { - var walletPassword string - if hasNewFormat { - validateExistingPass := func(input string) error { - if input == "" { - return errors.New("password input cannot be empty") - } - return nil - } - walletPassword, err = inputPassword( - cliCtx, - flags.WalletPasswordFileFlag, - walletPasswordPromptText, - noConfirmPass, - validateExistingPass, - ) - } else { - fmt.Println("\nWe have revamped how imported accounts work, improving speed significantly for your " + - "validators as well as reducing memory and CPU requirements. This unifies all your existing accounts " + - "into a single format protected by a strong password. You'll need to set a new password for this " + - "updated wallet format") - walletPassword, err = inputPassword( - cliCtx, - flags.WalletPasswordFileFlag, - newWalletPasswordPromptText, - confirmPass, - promptutil.ValidatePasswordInput, - ) - } - if err != nil { - return nil, err - } - w.walletPassword = walletPassword - } - return w, nil + }, nil } // SaveWallet persists the wallet's directories to disk. @@ -225,17 +142,13 @@ func (w *Wallet) AccountsDir() string { return w.accountsPath } -// Password for the wallet. -func (w *Wallet) Password() string { - return w.walletPassword -} - // InitializeKeymanager reads a keymanager config from disk at the wallet path, // unmarshals it based on the wallet's keymanager kind, and returns its value. func (w *Wallet) InitializeKeymanager( - ctx context.Context, + cliCtx *cli.Context, skipMnemonicConfirm bool, ) (v2keymanager.IKeymanager, error) { + ctx := context.Background() configFile, err := w.ReadKeymanagerConfigFromDisk(ctx) if err != nil { return nil, errors.Wrap(err, "could not read keymanager config") @@ -247,7 +160,7 @@ func (w *Wallet) InitializeKeymanager( if err != nil { return nil, errors.Wrap(err, "could not unmarshal keymanager config file") } - keymanager, err = direct.NewKeymanager(ctx, w, cfg) + keymanager, err = direct.NewKeymanager(cliCtx, w, cfg) if err != nil { return nil, errors.Wrap(err, "could not initialize direct keymanager") } @@ -256,7 +169,7 @@ func (w *Wallet) InitializeKeymanager( if err != nil { return nil, errors.Wrap(err, "could not unmarshal keymanager config file") } - keymanager, err = derived.NewKeymanager(ctx, w, cfg, skipMnemonicConfirm, w.walletPassword) + keymanager, err = derived.NewKeymanager(cliCtx, w, cfg, skipMnemonicConfirm) if err != nil { return nil, errors.Wrap(err, "could not initialize derived keymanager") } @@ -265,7 +178,7 @@ func (w *Wallet) InitializeKeymanager( if err != nil { return nil, errors.Wrap(err, "could not unmarshal keymanager config file") } - keymanager, err = remote.NewKeymanager(ctx, 100000000, cfg) + keymanager, err = remote.NewKeymanager(cliCtx, 100000000, cfg) if err != nil { return nil, errors.Wrap(err, "could not initialize remote keymanager") } @@ -295,7 +208,7 @@ func (w *Wallet) ListDirs() ([]string, error) { } dirNames := make([]string, 0) for _, item := range list { - ok, err := hasDir(filepath.Join(w.AccountsDir(), item)) + ok, err := fileutil.HasDir(filepath.Join(w.AccountsDir(), item)) if err != nil { return nil, errors.Wrapf(err, "could not parse directory: %v", err) } @@ -363,27 +276,11 @@ func (w *Wallet) FileNameAtPath(ctx context.Context, filePath string, fileName s return fullFileName, nil } -// AccountTimestamp retrieves the timestamp from a given keystore file name. -func AccountTimestamp(fileName string) (time.Time, error) { - timestampStart := strings.LastIndex(fileName, "-") + 1 - timestampEnd := strings.LastIndex(fileName, ".") - // Return an error if the text we expect cannot be found. - if timestampStart == -1 || timestampEnd == -1 { - return time.Unix(0, 0), fmt.Errorf("could not find timestamp in file name %s", fileName) - } - unixTimestampStr, err := strconv.ParseInt(fileName[timestampStart:timestampEnd], 10, 64) - if err != nil { - return time.Unix(0, 0), errors.Wrapf(err, "could not parse account created at timestamp: %s", fileName) - } - unixTimestamp := time.Unix(unixTimestampStr, 0) - return unixTimestamp, nil -} - // ReadKeymanagerConfigFromDisk opens a keymanager config file // for reading if it exists at the wallet path. func (w *Wallet) ReadKeymanagerConfigFromDisk(ctx context.Context) (io.ReadCloser, error) { configFilePath := filepath.Join(w.accountsPath, KeymanagerConfigFileName) - if !fileExists(configFilePath) { + if !fileutil.FileExists(configFilePath) { return nil, fmt.Errorf("no keymanager config file found at path: %s", w.accountsPath) } w.configFilePath = configFilePath @@ -431,7 +328,7 @@ func (w *Wallet) WriteKeymanagerConfigToDisk(ctx context.Context, encoded []byte // within the wallet path. func (w *Wallet) ReadEncryptedSeedFromDisk(ctx context.Context) (io.ReadCloser, error) { configFilePath := filepath.Join(w.accountsPath, derived.EncryptedSeedFileName) - if !fileExists(configFilePath) { + if !fileutil.FileExists(configFilePath) { return nil, fmt.Errorf("no encrypted seed file found at path: %s", w.accountsPath) } return os.Open(configFilePath) @@ -449,168 +346,6 @@ func (w *Wallet) WriteEncryptedSeedToDisk(ctx context.Context, encoded []byte) e return nil } -// enterPasswordForAccount checks if a user has a password specified for the new account -// either from a file or from stdin. Then, it saves the password to the wallet. -func (w *Wallet) enterPasswordForAccount(cliCtx *cli.Context, accountName string, pubKey []byte) error { - au := aurora.NewAurora(true) - var password string - var err error - if cliCtx.IsSet(flags.AccountPasswordFileFlag.Name) { - passwordFilePath := cliCtx.String(flags.AccountPasswordFileFlag.Name) - data, err := ioutil.ReadFile(passwordFilePath) - if err != nil { - return err - } - password = string(data) - err = w.checkPasswordForAccount(accountName, password) - if err != nil && strings.Contains(err.Error(), "invalid checksum") { - return fmt.Errorf("invalid password entered for account with public key %#x", pubKey) - } - if err != nil { - return err - } - } else { - pubKeyStr := fmt.Sprintf("%#x", bytesutil.Trunc(pubKey)) - attemptingPassword := true - // Loop asking for the password until the user enters it correctly. - for attemptingPassword { - // Ask the user for the password to their account. - password, err = inputWeakPassword( - cliCtx, - flags.AccountPasswordFileFlag, - fmt.Sprintf(passwordForAccountPromptText, au.BrightGreen(pubKeyStr)), - ) - if err != nil { - return errors.Wrap(err, "could not input password") - } - err = w.checkPasswordForAccount(accountName, password) - if err != nil && strings.Contains(err.Error(), "invalid checksum") { - fmt.Print(au.Red("X").Bold()) - fmt.Print(au.Red("\nIncorrect password entered, please try again")) - continue - } - if err != nil { - return err - } - attemptingPassword = false - fmt.Print(au.Green("✔️\n").Bold()) - } - } - return nil -} - -func (w *Wallet) enterPasswordForAllAccounts(cliCtx *cli.Context, accountNames []string, pubKeys [][]byte) error { - au := aurora.NewAurora(true) - var password string - var err error - if cliCtx.IsSet(flags.AccountPasswordFileFlag.Name) { - passwordFilePath := cliCtx.String(flags.AccountPasswordFileFlag.Name) - data, err := ioutil.ReadFile(passwordFilePath) - if err != nil { - return err - } - password = string(data) - for i := 0; i < len(accountNames); i++ { - err = w.checkPasswordForAccount(accountNames[i], password) - if err != nil && strings.Contains(err.Error(), "invalid checksum") { - return fmt.Errorf("invalid password for account with public key %#x", pubKeys[i]) - } - if err != nil { - return err - } - } - } else { - password, err = inputWeakPassword( - cliCtx, - flags.AccountPasswordFileFlag, - "Enter the password for your imported accounts", - ) - fmt.Println("Importing accounts, this may take a while...") - bar := progressbar.NewOptions( - len(accountNames), - progressbar.OptionFullWidth(), - progressbar.OptionSetWriter(ansi.NewAnsiStdout()), - progressbar.OptionEnableColorCodes(true), - progressbar.OptionSetTheme(progressbar.Theme{ - Saucer: "[green]=[reset]", - SaucerHead: "[green]>[reset]", - SaucerPadding: " ", - BarStart: "[", - BarEnd: "]", - }), - progressbar.OptionOnCompletion(func() { fmt.Println() }), - progressbar.OptionSetDescription("Importing accounts"), - ) - for i := 0; i < len(accountNames); i++ { - // We check if the individual account unlocks with the global password. - err = w.checkPasswordForAccount(accountNames[i], password) - if err != nil && strings.Contains(err.Error(), "invalid checksum") { - // If the password fails for an individual account, we ask the user to input - // that individual account's password until it succeeds. - _, err := w.askUntilPasswordConfirms(cliCtx, accountNames[i], pubKeys[i]) - if err != nil { - return err - } - if err := bar.Add(1); err != nil { - return errors.Wrap(err, "could not add to progress bar") - } - continue - } - if err != nil { - return err - } - fmt.Printf("Finished importing %#x\n", au.BrightMagenta(bytesutil.Trunc(pubKeys[i]))) - if err := bar.Add(1); err != nil { - return errors.Wrap(err, "could not add to progress bar") - } - } - } - return nil -} - -func (w *Wallet) askUntilPasswordConfirms(cliCtx *cli.Context, accountName string, pubKey []byte) (string, error) { - // Loop asking for the password until the user enters it correctly. - var password string - var err error - for { - password, err = inputWeakPassword( - cliCtx, - flags.AccountPasswordFileFlag, - fmt.Sprintf(passwordForAccountPromptText, bytesutil.Trunc(pubKey)), - ) - if err != nil { - return "", errors.Wrap(err, "could not input password") - } - err = w.checkPasswordForAccount(accountName, password) - if err != nil && strings.Contains(err.Error(), "invalid checksum") { - fmt.Println(au.Red("Incorrect password entered, please try again")) - continue - } - if err != nil { - return "", err - } - break - } - return password, nil -} - -func (w *Wallet) checkPasswordForAccount(accountName string, password string) error { - encoded, err := w.ReadFileAtPath(context.Background(), accountName, direct.KeystoreFileName) - if err != nil { - return errors.Wrap(err, "could not read keystore file") - } - keystoreJSON := &v2keymanager.Keystore{} - if err := json.Unmarshal(encoded, &keystoreJSON); err != nil { - return errors.Wrap(err, "could not decode json") - } - decryptor := keystorev4.New() - _, err = decryptor.Decrypt(keystoreJSON.Crypto, password) - if err != nil { - return errors.Wrap(err, "could not decrypt keystore") - } - return nil -} - func readKeymanagerKindFromWalletPath(walletPath string) (v2keymanager.Kind, error) { walletItem, err := os.Open(walletPath) if err != nil { @@ -638,7 +373,7 @@ func readKeymanagerKindFromWalletPath(walletPath string) (v2keymanager.Kind, err func createOrOpenWallet(cliCtx *cli.Context, creationFunc func(cliCtx *cli.Context) (*Wallet, error)) (*Wallet, error) { directory := cliCtx.String(flags.WalletDirFlag.Name) - ok, err := hasDir(directory) + ok, err := fileutil.HasDir(directory) if err != nil { return nil, errors.Wrapf(err, "could not check if wallet dir %s exists", directory) } @@ -654,40 +389,10 @@ func createOrOpenWallet(cliCtx *cli.Context, creationFunc func(cliCtx *cli.Conte return creationFunc(cliCtx) } -// Returns true if a file is not a directory and exists -// at the specified path. -func fileExists(filename string) bool { - filePath, err := expandPath(filename) - if err != nil { - return false - } - info, err := os.Stat(filePath) - if os.IsNotExist(err) { - return false - } - return !info.IsDir() -} - -// Checks if a directory indeed exists at the specified path. -func hasDir(dirPath string) (bool, error) { - fullPath, err := expandPath(dirPath) - if err != nil { - return false, err - } - info, err := os.Stat(fullPath) - if os.IsNotExist(err) { - return false, nil - } - if info == nil { - return false, err - } - return info.IsDir(), err -} - // isEmptyWallet checks if a folder consists key directory such as `derived`, `remote` or `direct`. // Returns true if exists, false otherwise. func isEmptyWallet(name string) (bool, error) { - expanded, err := expandPath(name) + expanded, err := fileutil.ExpandPath(name) if err != nil { return false, err } diff --git a/validator/accounts/v2/wallet_create.go b/validator/accounts/v2/wallet_create.go index 7ac209535d..1ec9b12746 100644 --- a/validator/accounts/v2/wallet_create.go +++ b/validator/accounts/v2/wallet_create.go @@ -4,7 +4,6 @@ import ( "context" "github.com/pkg/errors" - "github.com/prysmaticlabs/prysm/validator/flags" v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/derived" "github.com/prysmaticlabs/prysm/validator/keymanager/v2/direct" @@ -76,16 +75,7 @@ func createDirectKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error { } func createDerivedKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error { - skipMnemonicConfirm := cliCtx.Bool(flags.SkipMnemonicConfirmFlag.Name) ctx := context.Background() - seedConfig, err := derived.InitializeWalletSeedFile(ctx, wallet.walletPassword, skipMnemonicConfirm) - if err != nil { - return errors.Wrap(err, "could not initialize new wallet seed file") - } - seedConfigFile, err := derived.MarshalEncryptedSeedFile(ctx, seedConfig) - if err != nil { - return errors.Wrap(err, "could not marshal encrypted wallet seed file") - } keymanagerConfig, err := derived.MarshalConfigFile(ctx, derived.DefaultConfig()) if err != nil { return errors.Wrap(err, "could not marshal keymanager config file") @@ -96,9 +86,6 @@ func createDerivedKeymanagerWallet(cliCtx *cli.Context, wallet *Wallet) error { if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil { return errors.Wrap(err, "could not write keymanager config to disk") } - if err := wallet.WriteEncryptedSeedToDisk(ctx, seedConfigFile); err != nil { - return errors.Wrap(err, "could not write encrypted wallet seed config to disk") - } return nil } diff --git a/validator/accounts/v2/wallet_recover.go b/validator/accounts/v2/wallet_recover.go index 522de9fabb..45897642c9 100644 --- a/validator/accounts/v2/wallet_recover.go +++ b/validator/accounts/v2/wallet_recover.go @@ -31,14 +31,6 @@ func RecoverWallet(cliCtx *cli.Context) error { return errors.Wrap(err, "could not create new wallet") } ctx := context.Background() - seedConfig, err := derived.SeedFileFromMnemonic(ctx, mnemonic, wallet.walletPassword) - if err != nil { - return errors.Wrap(err, "could not initialize new wallet seed file") - } - seedConfigFile, err := derived.MarshalEncryptedSeedFile(ctx, seedConfig) - if err != nil { - return errors.Wrap(err, "could not marshal encrypted wallet seed file") - } keymanagerConfig, err := derived.MarshalConfigFile(ctx, derived.DefaultConfig()) if err != nil { return errors.Wrap(err, "could not marshal keymanager config file") @@ -49,10 +41,7 @@ func RecoverWallet(cliCtx *cli.Context) error { if err := wallet.WriteKeymanagerConfigToDisk(ctx, keymanagerConfig); err != nil { return errors.Wrap(err, "could not write keymanager config to disk") } - if err := wallet.WriteEncryptedSeedToDisk(ctx, seedConfigFile); err != nil { - return errors.Wrap(err, "could not write encrypted wallet seed config to disk") - } - keymanager, err := wallet.InitializeKeymanager(ctx, true) + keymanager, err := wallet.InitializeKeymanager(cliCtx, true) if err != nil { return err } @@ -60,7 +49,9 @@ func RecoverWallet(cliCtx *cli.Context) error { if !ok { return errors.New("not a derived keymanager") } - + if err := km.WriteEncryptedSeedToWallet(ctx, mnemonic); err != nil { + return err + } numAccounts, err := inputNumAccounts(cliCtx) if err != nil { return errors.Wrap(err, "could not get number of accounts to recover") diff --git a/validator/accounts/v2/wallet_recover_test.go b/validator/accounts/v2/wallet_recover_test.go index 8bd7e1395e..06d3f8b552 100644 --- a/validator/accounts/v2/wallet_recover_test.go +++ b/validator/accounts/v2/wallet_recover_test.go @@ -66,7 +66,7 @@ func TestRecoverDerivedWallet(t *testing.T) { wantCfg := derived.DefaultConfig() assert.DeepEqual(t, wantCfg, cfg) - keymanager, err := wallet.InitializeKeymanager(ctx, true) + keymanager, err := wallet.InitializeKeymanager(cliCtx, true) require.NoError(t, err) km, ok := keymanager.(*derived.Keymanager) if !ok { diff --git a/validator/accounts/v2/wallet_test.go b/validator/accounts/v2/wallet_test.go index 3d32a1808a..753c25be49 100644 --- a/validator/accounts/v2/wallet_test.go +++ b/validator/accounts/v2/wallet_test.go @@ -9,10 +9,8 @@ import ( "math/big" "os" "path/filepath" - "reflect" "strconv" "testing" - "time" "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/testutil" @@ -25,6 +23,16 @@ import ( "github.com/urfave/cli/v2" ) +const ( + walletDirName = "wallet" + passwordDirName = "walletpasswords" + exportDirName = "export" + passwordFileName = "password.txt" + password = "OhWOWthisisatest42!$" + mnemonicFileName = "mnemonic.txt" + mnemonic = "garage car helmet trade salmon embrace market giant movie wet same champion dawn chair shield drill amazing panther accident puzzle garden mosquito kind arena" +) + func init() { logrus.SetLevel(logrus.DebugLevel) logrus.SetOutput(ioutil.Discard) @@ -90,44 +98,6 @@ func setupWalletAndPasswordsDir(t testing.TB) (string, string, string) { return walletDir, passwordsDir, passwordFilePath } -func TestAccountTimestamp(t *testing.T) { - tests := []struct { - name string - fileName string - want time.Time - wantErr bool - }{ - { - name: "keystore with timestamp", - fileName: "keystore-1234567.json", - want: time.Unix(1234567, 0), - }, - { - name: "keystore with deriv path and timestamp", - fileName: "keystore-12313-313-00-0-5500550.json", - want: time.Unix(5500550, 0), - }, - { - name: "keystore with no timestamp", - fileName: "keystore.json", - want: time.Unix(0, 0), - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, err := AccountTimestamp(tt.fileName) - if (err != nil) != tt.wantErr { - t.Errorf("AccountTimestamp() error = %v, wantErr %v", err, tt.wantErr) - return - } - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("AccountTimestamp() got = %v, want %v", got, tt.want) - } - }) - } -} - func Test_IsEmptyWallet_RandomFiles(t *testing.T) { path := testutil.TempDir() walletDir := filepath.Join(path, "test") @@ -164,7 +134,7 @@ func Test_LockUnlockFile(t *testing.T) { ctx := context.Background() wallet, err := OpenWallet(cliCtx) defer unlock(t, wallet) - _, err = wallet.InitializeKeymanager(ctx, true) + _, err = wallet.InitializeKeymanager(cliCtx, true) require.NoError(t, err) assert.NoError(t, err) err = wallet.LockConfigFile(ctx) diff --git a/validator/flags/BUILD.bazel b/validator/flags/BUILD.bazel index c42ea848be..32eb50c9e4 100644 --- a/validator/flags/BUILD.bazel +++ b/validator/flags/BUILD.bazel @@ -9,6 +9,7 @@ go_library( importpath = "github.com/prysmaticlabs/prysm/validator/flags", visibility = ["//validator:__subpackages__"], deps = [ + "//shared/fileutil:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", ], diff --git a/validator/flags/flags.go b/validator/flags/flags.go index 8f6911333b..d61e6d03f6 100644 --- a/validator/flags/flags.go +++ b/validator/flags/flags.go @@ -3,12 +3,11 @@ package flags import ( - "os" - "os/user" "path/filepath" "runtime" "time" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/sirupsen/logrus" "github.com/urfave/cli/v2" ) @@ -16,8 +15,6 @@ import ( const ( // WalletDefaultDirName for accounts-v2. WalletDefaultDirName = "prysm-wallet-v2" - // PasswordsDefaultDirName where account-v2 passwords are stored. - PasswordsDefaultDirName = "prysm-wallet-v2-passwords" ) var log = logrus.WithField("prefix", "flags") @@ -248,7 +245,7 @@ func ComplainOnDeprecatedFlags(ctx *cli.Context) { // DefaultValidatorDir returns OS-specific default validator directory. func DefaultValidatorDir() string { // Try to place the data folder in the user's home dir - home := homeDir() + home := fileutil.HomeDir() if home != "" { if runtime.GOOS == "darwin" { return filepath.Join(home, "Library", "Eth2Validators") @@ -261,14 +258,3 @@ func DefaultValidatorDir() string { // As we cannot guess a stable location, return empty and handle later return "" } - -// homeDir returns home directory path. -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - if usr, err := user.Current(); err == nil { - return usr.HomeDir - } - return "" -} diff --git a/validator/keymanager/v2/derived/BUILD.bazel b/validator/keymanager/v2/derived/BUILD.bazel index 0cadc510ab..6faa8d75e7 100644 --- a/validator/keymanager/v2/derived/BUILD.bazel +++ b/validator/keymanager/v2/derived/BUILD.bazel @@ -17,14 +17,17 @@ go_library( "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", "//shared/depositutil:go_default_library", + "//shared/fileutil:go_default_library", "//shared/petnames:go_default_library", "//shared/promptutil:go_default_library", "//shared/rand:go_default_library", "//validator/accounts/v2/iface:go_default_library", + "//validator/flags:go_default_library", "@com_github_google_uuid//:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_tyler_smith_go_bip39//:go_default_library", + "@com_github_urfave_cli_v2//:go_default_library", "@com_github_wealdtech_go_eth2_util//:go_default_library", "@com_github_wealdtech_go_eth2_wallet_encryptor_keystorev4//:go_default_library", ], diff --git a/validator/keymanager/v2/derived/derived.go b/validator/keymanager/v2/derived/derived.go index b20654552d..796e0c8801 100644 --- a/validator/keymanager/v2/derived/derived.go +++ b/validator/keymanager/v2/derived/derived.go @@ -7,6 +7,8 @@ import ( "io" "io/ioutil" "path" + "path/filepath" + "strings" "sync" "github.com/google/uuid" @@ -15,11 +17,15 @@ import ( "github.com/prysmaticlabs/prysm/shared/bls" "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/depositutil" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/petnames" + "github.com/prysmaticlabs/prysm/shared/promptutil" "github.com/prysmaticlabs/prysm/shared/rand" "github.com/prysmaticlabs/prysm/validator/accounts/v2/iface" + "github.com/prysmaticlabs/prysm/validator/flags" "github.com/sirupsen/logrus" "github.com/tyler-smith/go-bip39" + "github.com/urfave/cli/v2" util "github.com/wealdtech/go-eth2-util" keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" ) @@ -38,7 +44,19 @@ const ( // m / purpose / coin_type / account_index / withdrawal_key / validating_key ValidatingKeyDerivationPathTemplate = "m/12381/3600/%d/0/0" // EncryptedSeedFileName for persisting a wallet's seed when using a derived keymanager. - EncryptedSeedFileName = "seed.encrypted.json" + EncryptedSeedFileName = "seed.encrypted.json" + newWalletPasswordPromptText = "New wallet password" + walletPasswordPromptText = "Wallet password" + confirmPasswordPromptText = "Confirm password" +) + +type passwordConfirm int + +const ( + // An enum to indicate to the prompt that confirming the password is not needed. + noConfirmPass passwordConfirm = iota + // An enum to indicate to the prompt to confirm the password entered. + confirmPass ) // Config for a derived keymanager. @@ -56,7 +74,7 @@ type Keymanager struct { lock sync.RWMutex seedCfg *SeedConfig seed []byte - walletPassword string + accountsPassword string } // SeedConfig json file representation as a Go struct. @@ -78,31 +96,80 @@ func DefaultConfig() *Config { // NewKeymanager instantiates a new derived keymanager from configuration options. func NewKeymanager( - ctx context.Context, + cliCtx *cli.Context, wallet iface.Wallet, cfg *Config, skipMnemonicConfirm bool, - password string, ) (*Keymanager, error) { - seedConfigFile, err := wallet.ReadEncryptedSeedFromDisk(ctx) + walletFiles, err := wallet.ListDirs() if err != nil { - return nil, errors.Wrap(err, "could not read encrypted seed file from disk") + return nil, err } - enc, err := ioutil.ReadAll(seedConfigFile) - if err != nil { - return nil, errors.Wrap(err, "could not read seed configuration file contents") - } - defer func() { - if err := seedConfigFile.Close(); err != nil { - log.Errorf("Could not close keymanager config file: %v", err) + var accountsPassword string + // If the user does not have any accounts in their wallet, we ask them to + // set a new wallet password, which will be used for encrypting/decrypting + // their wallet secret to and from disk. + if len(walletFiles) == 0 { + accountsPassword, err = inputPassword( + cliCtx, + flags.WalletPasswordFileFlag, + newWalletPasswordPromptText, + confirmPass, + promptutil.ValidatePasswordInput, + ) + } else { + validateExistingPass := func(input string) error { + if input == "" { + return errors.New("password input cannot be empty") + } + return nil } - }() + accountsPassword, err = inputPassword( + cliCtx, + flags.WalletPasswordFileFlag, + walletPasswordPromptText, + noConfirmPass, + validateExistingPass, + ) + } + + // Check if the wallet seed file exists. If it does not, we initialize one + // by creating a new mnemonic and writing the encrypted file to disk. + ctx := context.Background() + var encodedSeedFile []byte + if !fileutil.FileExists(filepath.Join(wallet.AccountsDir(), EncryptedSeedFileName)) { + seedConfig, err := initializeWalletSeedFile(accountsPassword, skipMnemonicConfirm) + if err != nil { + return nil, errors.Wrap(err, "could not initialize new wallet seed file") + } + encodedSeedFile, err = marshalEncryptedSeedFile(seedConfig) + if err != nil { + return nil, errors.Wrap(err, "could not marshal encrypted wallet seed file") + } + if err = wallet.WriteEncryptedSeedToDisk(ctx, encodedSeedFile); err != nil { + return nil, errors.Wrap(err, "could not write encrypted wallet seed config to disk") + } + } else { + seedConfigFile, err := wallet.ReadEncryptedSeedFromDisk(ctx) + if err != nil { + return nil, errors.Wrap(err, "could not read encrypted seed file from disk") + } + encodedSeedFile, err = ioutil.ReadAll(seedConfigFile) + if err != nil { + return nil, errors.Wrap(err, "could not read seed configuration file contents") + } + defer func() { + if err := seedConfigFile.Close(); err != nil { + log.Errorf("Could not close keymanager config file: %v", err) + } + }() + } seedConfig := &SeedConfig{} - if err := json.Unmarshal(enc, seedConfig); err != nil { + if err := json.Unmarshal(encodedSeedFile, seedConfig); err != nil { return nil, errors.Wrap(err, "could not unmarshal seed configuration") } decryptor := keystorev4.New() - seed, err := decryptor.Decrypt(seedConfig.Crypto, password) + seed, err := decryptor.Decrypt(seedConfig.Crypto, accountsPassword) if err != nil { return nil, errors.Wrap(err, "could not decrypt seed configuration with password") } @@ -112,10 +179,10 @@ func NewKeymanager( mnemonicGenerator: &EnglishMnemonicGenerator{ skipMnemonicConfirm: skipMnemonicConfirm, }, - seedCfg: seedConfig, - seed: seed, - walletPassword: password, - keysCache: make(map[[48]byte]bls.SecretKey), + seedCfg: seedConfig, + seed: seed, + accountsPassword: accountsPassword, + keysCache: make(map[[48]byte]bls.SecretKey), } // We initialize a cache of public key -> secret keys // used to retrieve secrets keys for the accounts via the unlocked wallet. @@ -150,72 +217,6 @@ func MarshalConfigFile(ctx context.Context, cfg *Config) ([]byte, error) { return json.MarshalIndent(cfg, "", "\t") } -// InitializeWalletSeedFile creates a new, encrypted seed using a password input -// and persists its encrypted file metadata to disk under the wallet path. -func InitializeWalletSeedFile(ctx context.Context, password string, skipMnemonicConfirm bool) (*SeedConfig, error) { - mnemonicRandomness := make([]byte, 32) - if _, err := rand.NewGenerator().Read(mnemonicRandomness); err != nil { - return nil, errors.Wrap(err, "could not initialize mnemonic source of randomness") - } - m := &EnglishMnemonicGenerator{ - skipMnemonicConfirm: skipMnemonicConfirm, - } - phrase, err := m.Generate(mnemonicRandomness) - if err != nil { - return nil, errors.Wrap(err, "could not generate wallet seed") - } - if err := m.ConfirmAcknowledgement(phrase); err != nil { - return nil, errors.Wrap(err, "could not confirm mnemonic acknowledgement") - } - walletSeed := bip39.NewSeed(phrase, "") - encryptor := keystorev4.New() - cryptoFields, err := encryptor.Encrypt(walletSeed, password) - if err != nil { - return nil, errors.Wrap(err, "could not encrypt seed phrase into keystore") - } - id, err := uuid.NewRandom() - if err != nil { - return nil, errors.Wrap(err, "could not generate unique UUID") - } - return &SeedConfig{ - Crypto: cryptoFields, - ID: id.String(), - NextAccount: 0, - Version: encryptor.Version(), - Name: encryptor.Name(), - }, nil -} - -// SeedFileFromMnemonic uses the provided mnemonic seed phrase to generate the -// appropriate seed file for recovering a derived wallets. -func SeedFileFromMnemonic(ctx context.Context, mnemonic string, password string) (*SeedConfig, error) { - if ok := bip39.IsMnemonicValid(mnemonic); !ok { - return nil, bip39.ErrInvalidMnemonic - } - walletSeed := bip39.NewSeed(mnemonic, "") - encryptor := keystorev4.New() - cryptoFields, err := encryptor.Encrypt(walletSeed, password) - if err != nil { - return nil, errors.Wrap(err, "could not encrypt seed phrase into keystore") - } - id, err := uuid.NewRandom() - if err != nil { - return nil, errors.Wrap(err, "could not generate unique UUID") - } - return &SeedConfig{ - Crypto: cryptoFields, - ID: id.String(), - NextAccount: 0, - Version: encryptor.Version(), - Name: encryptor.Name(), - }, nil -} - -// MarshalEncryptedSeedFile json encodes the seed configuration for a derived keymanager. -func MarshalEncryptedSeedFile(ctx context.Context, seedCfg *SeedConfig) ([]byte, error) { - return json.MarshalIndent(seedCfg, "", "\t") -} - // Config returns the derived keymanager configuration. func (dr *Keymanager) Config() *Config { return dr.cfg @@ -226,6 +227,23 @@ func (dr *Keymanager) NextAccountNumber(ctx context.Context) uint64 { return dr.seedCfg.NextAccount } +// WriteEncryptedSeedToWallet given a mnemonic phrase, is able to regenerate a wallet seed +// encrypt it, and write it to the wallet's path. +func (dr *Keymanager) WriteEncryptedSeedToWallet(ctx context.Context, mnemonic string) error { + seedConfig, err := seedFileFromMnemonic(mnemonic, dr.accountsPassword) + if err != nil { + return errors.Wrap(err, "could not initialize new wallet seed file") + } + seedConfigFile, err := marshalEncryptedSeedFile(seedConfig) + if err != nil { + return errors.Wrap(err, "could not marshal encrypted wallet seed file") + } + if err := dr.wallet.WriteEncryptedSeedToDisk(ctx, seedConfigFile); err != nil { + return errors.Wrap(err, "could not write encrypted wallet seed config to disk") + } + return nil +} + // ValidatingAccountNames for the derived keymanager. func (dr *Keymanager) ValidatingAccountNames(ctx context.Context) ([]string, error) { names := make([]string, 0) @@ -293,7 +311,7 @@ func (dr *Keymanager) CreateAccount(ctx context.Context, logAccountInfo bool) (s }).Info("Successfully created new validator account") } dr.seedCfg.NextAccount++ - encodedCfg, err := MarshalEncryptedSeedFile(ctx, dr.seedCfg) + encodedCfg, err := marshalEncryptedSeedFile(dr.seedCfg) if err != nil { return "", errors.Wrap(err, "could not marshal encrypted seed file") } @@ -412,3 +430,114 @@ func (dr *Keymanager) initializeSecretKeysCache() error { } return nil } + +func inputPassword( + cliCtx *cli.Context, + passwordFileFlag *cli.StringFlag, + promptText string, + confirmPassword passwordConfirm, + passwordValidator func(input string) error, +) (string, error) { + if cliCtx.IsSet(passwordFileFlag.Name) { + passwordFilePathInput := cliCtx.String(passwordFileFlag.Name) + data, err := fileutil.ReadFileAsBytes(passwordFilePathInput) + if err != nil { + return "", errors.Wrap(err, "could not read file as bytes") + } + enteredPassword := strings.TrimRight(string(data), "\r\n") + if err := passwordValidator(enteredPassword); err != nil { + return "", errors.Wrap(err, "password did not pass validation") + } + return enteredPassword, nil + } + var hasValidPassword bool + var walletPassword string + var err error + for !hasValidPassword { + walletPassword, err = promptutil.PasswordPrompt(promptText, passwordValidator) + if err != nil { + return "", fmt.Errorf("could not read account password: %v", err) + } + + if confirmPassword == confirmPass { + passwordConfirmation, err := promptutil.PasswordPrompt(confirmPasswordPromptText, passwordValidator) + if err != nil { + return "", fmt.Errorf("could not read password confirmation: %v", err) + } + if walletPassword != passwordConfirmation { + log.Error("Passwords do not match") + continue + } + hasValidPassword = true + } else { + return walletPassword, nil + } + } + return walletPassword, nil +} + +// Creates a new, encrypted seed using a password input +// and persists its encrypted file metadata to disk under the wallet path. +func initializeWalletSeedFile(password string, skipMnemonicConfirm bool) (*SeedConfig, error) { + mnemonicRandomness := make([]byte, 32) + if _, err := rand.NewGenerator().Read(mnemonicRandomness); err != nil { + return nil, errors.Wrap(err, "could not initialize mnemonic source of randomness") + } + m := &EnglishMnemonicGenerator{ + skipMnemonicConfirm: skipMnemonicConfirm, + } + phrase, err := m.Generate(mnemonicRandomness) + if err != nil { + return nil, errors.Wrap(err, "could not generate wallet seed") + } + if err := m.ConfirmAcknowledgement(phrase); err != nil { + return nil, errors.Wrap(err, "could not confirm mnemonic acknowledgement") + } + walletSeed := bip39.NewSeed(phrase, "") + encryptor := keystorev4.New() + cryptoFields, err := encryptor.Encrypt(walletSeed, password) + if err != nil { + return nil, errors.Wrap(err, "could not encrypt seed phrase into keystore") + } + id, err := uuid.NewRandom() + if err != nil { + return nil, errors.Wrap(err, "could not generate unique UUID") + } + return &SeedConfig{ + Crypto: cryptoFields, + ID: id.String(), + NextAccount: 0, + Version: encryptor.Version(), + Name: encryptor.Name(), + }, nil +} + +// Uses the provided mnemonic seed phrase to generate the +// appropriate seed file for recovering a derived wallets. +func seedFileFromMnemonic(mnemonic string, password string) (*SeedConfig, error) { + if ok := bip39.IsMnemonicValid(mnemonic); !ok { + return nil, bip39.ErrInvalidMnemonic + } + walletSeed := bip39.NewSeed(mnemonic, "") + encryptor := keystorev4.New() + cryptoFields, err := encryptor.Encrypt(walletSeed, password) + if err != nil { + return nil, errors.Wrap(err, "could not encrypt seed phrase into keystore") + } + id, err := uuid.NewRandom() + if err != nil { + return nil, errors.Wrap(err, "could not generate unique UUID") + } + return &SeedConfig{ + Crypto: cryptoFields, + ID: id.String(), + NextAccount: 0, + Version: encryptor.Version(), + Name: encryptor.Name(), + }, nil +} + +// marshalEncryptedSeedFile json encodes the seed configuration for a derived keymanager. +func marshalEncryptedSeedFile(seedCfg *SeedConfig) ([]byte, error) { + return json.MarshalIndent(seedCfg, "", "\t") +} diff --git a/validator/keymanager/v2/derived/derived_test.go b/validator/keymanager/v2/derived/derived_test.go index 9699a1357c..7533aa3f23 100644 --- a/validator/keymanager/v2/derived/derived_test.go +++ b/validator/keymanager/v2/derived/derived_test.go @@ -69,7 +69,7 @@ func TestDerivedKeymanager_CreateAccount(t *testing.T) { seedCfg: &SeedConfig{ NextAccount: 0, }, - walletPassword: password, + accountsPassword: password, } ctx := context.Background() accountName, err := dr.CreateAccount(ctx, true /*logAccountInfo*/) @@ -105,8 +105,8 @@ func TestDerivedKeymanager_FetchValidatingPublicKeys(t *testing.T) { seedCfg: &SeedConfig{ NextAccount: 0, }, - seed: make([]byte, 32), - walletPassword: "hello world", + seed: make([]byte, 32), + accountsPassword: "hello world", } // First, generate accounts and their keystore.json files. ctx := context.Background() @@ -154,7 +154,7 @@ func TestDerivedKeymanager_Sign(t *testing.T) { seedCfg: &SeedConfig{ NextAccount: 0, }, - walletPassword: "hello world", + accountsPassword: "hello world", } // First, generate some accounts. diff --git a/validator/keymanager/v2/direct/BUILD.bazel b/validator/keymanager/v2/direct/BUILD.bazel index 5c553b076a..60ad90b5b5 100644 --- a/validator/keymanager/v2/direct/BUILD.bazel +++ b/validator/keymanager/v2/direct/BUILD.bazel @@ -18,6 +18,7 @@ go_library( "//shared/bls:go_default_library", "//shared/bytesutil:go_default_library", "//shared/depositutil:go_default_library", + "//shared/fileutil:go_default_library", "//shared/petnames:go_default_library", "//shared/promptutil:go_default_library", "//validator/accounts/v2/iface:go_default_library", diff --git a/validator/keymanager/v2/direct/direct.go b/validator/keymanager/v2/direct/direct.go index fc8820e721..40f5942afc 100644 --- a/validator/keymanager/v2/direct/direct.go +++ b/validator/keymanager/v2/direct/direct.go @@ -16,11 +16,14 @@ import ( "github.com/prysmaticlabs/prysm/shared/bls" "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/prysmaticlabs/prysm/shared/depositutil" + "github.com/prysmaticlabs/prysm/shared/fileutil" "github.com/prysmaticlabs/prysm/shared/petnames" "github.com/prysmaticlabs/prysm/shared/promptutil" "github.com/prysmaticlabs/prysm/validator/accounts/v2/iface" + "github.com/prysmaticlabs/prysm/validator/flags" v2keymanager "github.com/prysmaticlabs/prysm/validator/keymanager/v2" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" keystorev4 "github.com/wealdtech/go-eth2-wallet-encryptor-keystorev4" ) @@ -31,13 +34,22 @@ const ( KeystoreFileName = "keystore-*.json" // KeystoreFileNameFormat exposes the filename the keystore should be formatted in. KeystoreFileNameFormat = "keystore-%d.json" - // PasswordFileSuffix for passwords persisted as text to disk. - PasswordFileSuffix = ".pass" // AccountsPath where all direct keymanager keystores are kept. - AccountsPath = "accounts" - accountsKeystoreFileName = "all-accounts.keystore.json" - accountsKeystoreFileNameFormat = "all-accounts.keystore.json" - eipVersion = "EIP-2335" + AccountsPath = "accounts" + accountsKeystoreFileName = "all-accounts.keystore.json" + eipVersion = "EIP-2335" + newWalletPasswordPromptText = "New wallet password" + walletPasswordPromptText = "Wallet password" + confirmPasswordPromptText = "Confirm password" +) + +type passwordConfirm int + +const ( + // An enum to indicate to the prompt that confirming the password is not needed. + noConfirmPass passwordConfirm = iota + // An enum to indicate to the prompt to confirm the password entered. + confirmPass ) // Config for a direct keymanager. @@ -47,11 +59,12 @@ type Config struct { // Keymanager implementation for direct keystores utilizing EIP-2335. type Keymanager struct { - wallet iface.Wallet - cfg *Config - keysCache map[[48]byte]bls.SecretKey - accountsStore *AccountStore - lock sync.RWMutex + wallet iface.Wallet + cfg *Config + keysCache map[[48]byte]bls.SecretKey + accountsStore *AccountStore + lock sync.RWMutex + accountsPassword string } // AccountStore -- @@ -68,7 +81,7 @@ func DefaultConfig() *Config { } // NewKeymanager instantiates a new direct keymanager from configuration options. -func NewKeymanager(ctx context.Context, wallet iface.Wallet, cfg *Config) (*Keymanager, error) { +func NewKeymanager(ctx *cli.Context, wallet iface.Wallet, cfg *Config) (*Keymanager, error) { k := &Keymanager{ wallet: wallet, cfg: cfg, @@ -76,6 +89,39 @@ func NewKeymanager(ctx context.Context, wallet iface.Wallet, cfg *Config) (*Keym accountsStore: &AccountStore{}, } + walletFiles, err := wallet.ListDirs() + if err != nil { + return nil, err + } + var accountsPassword string + if len(walletFiles) == 0 { + accountsPassword, err = inputPassword( + ctx, + flags.WalletPasswordFileFlag, + newWalletPasswordPromptText, + confirmPass, + promptutil.ValidatePasswordInput, + ) + } else { + validateExistingPass := func(input string) error { + if input == "" { + return errors.New("password input cannot be empty") + } + return nil + } + accountsPassword, err = inputPassword( + ctx, + flags.WalletPasswordFileFlag, + walletPasswordPromptText, + noConfirmPass, + validateExistingPass, + ) + } + if err != nil { + return nil, err + } + k.accountsPassword = accountsPassword + // If the wallet has the capability of unlocking accounts using // passphrases, then we initialize a cache of public key -> secret keys // used to retrieve secrets keys for the accounts via password unlock. @@ -238,8 +284,8 @@ func (dr *Keymanager) Sign(ctx context.Context, req *validatorpb.SignRequest) (b return secretKey.Sign(req.SigningRoot), nil } -func (dr *Keymanager) initializeSecretKeysCache(ctx context.Context) error { - encoded, err := dr.wallet.ReadFileAtPath(ctx, AccountsPath, accountsKeystoreFileName) +func (dr *Keymanager) initializeSecretKeysCache(cliCtx *cli.Context) error { + encoded, err := dr.wallet.ReadFileAtPath(context.Background(), AccountsPath, accountsKeystoreFileName) if err != nil && strings.Contains(err.Error(), "no files found") { // If there are no keys to initialize at all, just exit. return nil @@ -254,11 +300,11 @@ func (dr *Keymanager) initializeSecretKeysCache(ctx context.Context) error { // by utilizing the password and initialize a new BLS secret key from // its raw bytes. decryptor := keystorev4.New() - enc, err := decryptor.Decrypt(keystoreFile.Crypto, dr.wallet.Password()) + enc, err := decryptor.Decrypt(keystoreFile.Crypto, dr.accountsPassword) if err != nil && strings.Contains(err.Error(), "invalid checksum") { // If the password fails for an individual account, we ask the user to input // that individual account's password until it succeeds. - enc, _, err = dr.askUntilPasswordConfirms(decryptor, keystoreFile) + enc, dr.accountsPassword, err = dr.askUntilPasswordConfirms(decryptor, keystoreFile) if err != nil { return errors.Wrap(err, "could not confirm password via prompt") } @@ -336,7 +382,7 @@ func (dr *Keymanager) createAccountsKeystore( if err != nil { return nil, err } - cryptoFields, err := encryptor.Encrypt(encodedStore, dr.wallet.Password()) + cryptoFields, err := encryptor.Encrypt(encodedStore, dr.accountsPassword) if err != nil { return nil, errors.Wrap(err, "could not encrypt accounts") } @@ -375,3 +421,52 @@ func (dr *Keymanager) askUntilPasswordConfirms( } return secretKey, password, nil } + +func inputPassword( + cliCtx *cli.Context, + passwordFileFlag *cli.StringFlag, + promptText string, + confirmPassword passwordConfirm, + passwordValidator func(input string) error, +) (string, error) { + if cliCtx.IsSet(passwordFileFlag.Name) { + passwordFilePathInput := cliCtx.String(passwordFileFlag.Name) + passwordFilePath, err := fileutil.ExpandPath(passwordFilePathInput) + if err != nil { + return "", errors.Wrap(err, "could not determine absolute path of password file") + } + data, err := ioutil.ReadFile(passwordFilePath) + if err != nil { + return "", errors.Wrap(err, "could not read password file") + } + enteredPassword := strings.TrimRight(string(data), "\r\n") + if err := passwordValidator(enteredPassword); err != nil { + return "", errors.Wrap(err, "password did not pass validation") + } + return enteredPassword, nil + } + var hasValidPassword bool + var walletPassword string + var err error + for !hasValidPassword { + walletPassword, err = promptutil.PasswordPrompt(promptText, passwordValidator) + if err != nil { + return "", fmt.Errorf("could not read account password: %v", err) + } + + if confirmPassword == confirmPass { + passwordConfirmation, err := promptutil.PasswordPrompt(confirmPasswordPromptText, passwordValidator) + if err != nil { + return "", fmt.Errorf("could not read password confirmation: %v", err) + } + if walletPassword != passwordConfirmation { + log.Error("Passwords do not match") + continue + } + hasValidPassword = true + } else { + return walletPassword, nil + } + } + return walletPassword, nil +} diff --git a/validator/keymanager/v2/direct/direct_test.go b/validator/keymanager/v2/direct/direct_test.go index 965377be79..dfd832c917 100644 --- a/validator/keymanager/v2/direct/direct_test.go +++ b/validator/keymanager/v2/direct/direct_test.go @@ -22,13 +22,13 @@ func TestDirectKeymanager_CreateAccount(t *testing.T) { hook := logTest.NewGlobal() password := "secretPassw0rd$1999" wallet := &mock.Wallet{ - Files: make(map[string]map[string][]byte), - WalletPassword: password, + Files: make(map[string]map[string][]byte), } dr := &Keymanager{ - keysCache: make(map[[48]byte]bls.SecretKey), - wallet: wallet, - accountsStore: &AccountStore{}, + keysCache: make(map[[48]byte]bls.SecretKey), + wallet: wallet, + accountsStore: &AccountStore{}, + accountsPassword: password, } ctx := context.Background() accountName, err := dr.CreateAccount(ctx) @@ -66,13 +66,13 @@ func TestDirectKeymanager_CreateAccount(t *testing.T) { func TestDirectKeymanager_FetchValidatingPublicKeys(t *testing.T) { password := "secretPassw0rd$1999" wallet := &mock.Wallet{ - Files: make(map[string]map[string][]byte), - WalletPassword: password, + Files: make(map[string]map[string][]byte), } dr := &Keymanager{ - wallet: wallet, - keysCache: make(map[[48]byte]bls.SecretKey), - accountsStore: &AccountStore{}, + wallet: wallet, + keysCache: make(map[[48]byte]bls.SecretKey), + accountsStore: &AccountStore{}, + accountsPassword: password, } // First, generate accounts and their keystore.json files. ctx := context.Background() @@ -107,12 +107,12 @@ func TestDirectKeymanager_Sign(t *testing.T) { wallet := &mock.Wallet{ Files: make(map[string]map[string][]byte), AccountPasswords: make(map[string]string), - WalletPassword: password, } dr := &Keymanager{ - wallet: wallet, - accountsStore: &AccountStore{}, - keysCache: make(map[[48]byte]bls.SecretKey), + wallet: wallet, + accountsStore: &AccountStore{}, + keysCache: make(map[[48]byte]bls.SecretKey), + accountsPassword: password, } // First, generate accounts and their keystore.json files. @@ -136,7 +136,7 @@ func TestDirectKeymanager_Sign(t *testing.T) { // by utilizing the password and initialize a new BLS secret key from // its raw bytes. decryptor := keystorev4.New() - enc, err := decryptor.Decrypt(keystoreFile.Crypto, dr.wallet.Password()) + enc, err := decryptor.Decrypt(keystoreFile.Crypto, dr.accountsPassword) require.NoError(t, err) store := &AccountStore{} require.NoError(t, json.Unmarshal(enc, store)) diff --git a/validator/keymanager/v2/direct/import_test.go b/validator/keymanager/v2/direct/import_test.go index 65e10e640a..1b8d7d4116 100644 --- a/validator/keymanager/v2/direct/import_test.go +++ b/validator/keymanager/v2/direct/import_test.go @@ -62,11 +62,10 @@ func TestDirectKeymanager_CreateAccountsKeystore_NoDuplicates(t *testing.T) { privKeys[i] = priv.Marshal() pubKeys[i] = priv.PublicKey().Marshal() } - wallet := &mock.Wallet{ - WalletPassword: "Passw0rdz$%@49", - } + wallet := &mock.Wallet{} dr := &Keymanager{ - wallet: wallet, + wallet: wallet, + accountsPassword: "Mypassw0rdz932", } ctx := context.Background() _, err := dr.createAccountsKeystore(ctx, privKeys, pubKeys) @@ -122,12 +121,12 @@ func TestDirectKeymanager_ImportKeystores(t *testing.T) { // Setup the keymanager. wallet := &mock.Wallet{ - Files: make(map[string]map[string][]byte), - WalletPassword: password, + Files: make(map[string]map[string][]byte), } dr := &Keymanager{ - wallet: wallet, - accountsStore: &AccountStore{}, + wallet: wallet, + accountsStore: &AccountStore{}, + accountsPassword: password, } // Create several keystores and attempt to import them. diff --git a/validator/keymanager/v2/remote/BUILD.bazel b/validator/keymanager/v2/remote/BUILD.bazel index 82040e12ad..0a394b21a4 100644 --- a/validator/keymanager/v2/remote/BUILD.bazel +++ b/validator/keymanager/v2/remote/BUILD.bazel @@ -20,6 +20,7 @@ go_library( "@com_github_logrusorgru_aurora//: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", "@org_golang_google_grpc//:go_default_library", "@org_golang_google_grpc//credentials:go_default_library", ], @@ -37,5 +38,6 @@ go_test( "//shared/testutil:go_default_library", "//shared/testutil/require:go_default_library", "@com_github_golang_mock//gomock:go_default_library", + "@com_github_urfave_cli_v2//:go_default_library", ], ) diff --git a/validator/keymanager/v2/remote/remote.go b/validator/keymanager/v2/remote/remote.go index b8f0eec571..22d226f638 100644 --- a/validator/keymanager/v2/remote/remote.go +++ b/validator/keymanager/v2/remote/remote.go @@ -17,6 +17,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/bls" "github.com/prysmaticlabs/prysm/shared/bytesutil" "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" "google.golang.org/grpc" "google.golang.org/grpc/credentials" ) @@ -54,7 +55,7 @@ type Keymanager struct { } // NewKeymanager instantiates a new direct keymanager from configuration options. -func NewKeymanager(ctx context.Context, maxMessageSize int, cfg *Config) (*Keymanager, error) { +func NewKeymanager(cliCtx *cli.Context, maxMessageSize int, cfg *Config) (*Keymanager, error) { // Load the client certificates. if cfg.RemoteCertificate == nil { return nil, errors.New("certificates are required") diff --git a/validator/keymanager/v2/remote/remote_test.go b/validator/keymanager/v2/remote/remote_test.go index c59b4deb6f..55bcd1a60a 100644 --- a/validator/keymanager/v2/remote/remote_test.go +++ b/validator/keymanager/v2/remote/remote_test.go @@ -19,6 +19,7 @@ import ( "github.com/prysmaticlabs/prysm/shared/params" "github.com/prysmaticlabs/prysm/shared/testutil" "github.com/prysmaticlabs/prysm/shared/testutil/require" + "github.com/urfave/cli/v2" ) var validClientCert = `-----BEGIN CERTIFICATE----- @@ -173,7 +174,7 @@ func TestNewRemoteKeymanager(t *testing.T) { test.opts.RemoteCertificate.ClientKeyPath = clientKeyPath } } - _, err := NewKeymanager(context.Background(), 1, test.opts) + _, err := NewKeymanager(&cli.Context{}, 1, test.opts) if test.err == "" { require.NoError(t, err) } else { diff --git a/validator/node/node.go b/validator/node/node.go index 0237a720c5..457a4a48ff 100644 --- a/validator/node/node.go +++ b/validator/node/node.go @@ -91,7 +91,7 @@ func NewValidatorClient(cliCtx *cli.Context) (*ValidatorClient, error) { ValidatorClient.wallet = wallet ctx := context.Background() keyManagerV2, err = wallet.InitializeKeymanager( - ctx, false, /* skipMnemonicConfirm */ + cliCtx, false, /* skipMnemonicConfirm */ ) if err != nil { log.Fatalf("Could not read existing keymanager for wallet: %v", err)