Refactor Wallet Password Handling, Preventing Wallet Corruption (#6933)

* wallet should no longer deal with account passwords
* ensure tests are fixed
* Merge branch 'master' into corrupted-pass
* move mnemonic logic into right place
* rem fmts
* add fileutil
* gazelle
* imports
* move seed logic to derived
* fix tests
* imports
* gaz
* Merge refs/heads/master into corrupted-pass
* merge confs
* Merge refs/heads/master into corrupted-pass
* ivan's feedback
* Merge branch 'corrupted-pass' of github.com:prysmaticlabs/prysm into corrupted-pass
* gaz
* fix shared test
* Merge refs/heads/master into corrupted-pass
* resolve conflicts
* fix test build
This commit is contained in:
Raul Jordan
2020-08-10 13:54:40 -05:00
committed by GitHub
parent a7f4293eb3
commit cbc27e0f2e
40 changed files with 545 additions and 803 deletions

View File

@@ -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"],

View File

@@ -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 ""
}

View File

@@ -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")

View File

@@ -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",
],
)

View File

@@ -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)
}

View File

@@ -13,8 +13,7 @@
//
// You should have received a copy of the GNU General Public License
// along with go-ethereum. If not, see <http://www.gnu.org/licenses/>.
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)
}
}

View File

@@ -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",

View File

@@ -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 ""
}

View File

@@ -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",

View File

@@ -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)

View File

@@ -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",

View File

@@ -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")
}

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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")
}

View File

@@ -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)

View File

@@ -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"
)

View File

@@ -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)

View File

@@ -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 ""
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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",
],

View File

@@ -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 ""
}

View File

@@ -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",
],

View File

@@ -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")
}

View File

@@ -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.

View File

@@ -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",

View File

@@ -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
}

View File

@@ -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))

View File

@@ -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.

View File

@@ -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",
],
)

View File

@@ -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")

View File

@@ -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 {

View File

@@ -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)