github.com/prysmaticlabs/prysm@v1.4.4/validator/accounts/wallet/wallet.go (about)

     1  package wallet
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  
    12  	"github.com/pkg/errors"
    13  	"github.com/prysmaticlabs/prysm/cmd/validator/flags"
    14  	"github.com/prysmaticlabs/prysm/shared/fileutil"
    15  	"github.com/prysmaticlabs/prysm/shared/promptutil"
    16  	"github.com/prysmaticlabs/prysm/validator/accounts/iface"
    17  	"github.com/prysmaticlabs/prysm/validator/accounts/prompt"
    18  	"github.com/prysmaticlabs/prysm/validator/keymanager"
    19  	"github.com/prysmaticlabs/prysm/validator/keymanager/derived"
    20  	"github.com/prysmaticlabs/prysm/validator/keymanager/imported"
    21  	"github.com/prysmaticlabs/prysm/validator/keymanager/remote"
    22  	"github.com/sirupsen/logrus"
    23  	"github.com/urfave/cli/v2"
    24  )
    25  
    26  const (
    27  	// KeymanagerConfigFileName for the keymanager used by the wallet: imported, derived, or remote.
    28  	KeymanagerConfigFileName = "keymanageropts.json"
    29  	// NewWalletPasswordPromptText for wallet creation.
    30  	NewWalletPasswordPromptText = "New wallet password"
    31  	// PasswordPromptText for wallet unlocking.
    32  	PasswordPromptText = "Wallet password"
    33  	// ConfirmPasswordPromptText for confirming a wallet password.
    34  	ConfirmPasswordPromptText = "Confirm password"
    35  	// DefaultWalletPasswordFile used to store a wallet password with appropriate permissions
    36  	// if a user signs up via the Prysm web UI via RPC.
    37  	DefaultWalletPasswordFile = "walletpassword.txt"
    38  	// CheckExistsErrMsg for when there is an error while checking for a wallet
    39  	CheckExistsErrMsg = "could not check if wallet exists"
    40  	// CheckValidityErrMsg for when there is an error while checking wallet validity
    41  	CheckValidityErrMsg = "could not check if wallet is valid"
    42  	// InvalidWalletErrMsg for when a directory does not contain a valid wallet
    43  	InvalidWalletErrMsg = "directory does not contain valid wallet"
    44  )
    45  
    46  var (
    47  	// ErrNoWalletFound signifies there was no wallet directory found on-disk.
    48  	ErrNoWalletFound = errors.New(
    49  		"no wallet found. You can create a new wallet with `validator wallet create`. " +
    50  			"If you already did, perhaps you created a wallet in a custom directory, which you can specify using " +
    51  			"`--wallet-dir=/path/to/my/wallet`",
    52  	)
    53  	// KeymanagerKindSelections as friendly text.
    54  	KeymanagerKindSelections = map[keymanager.Kind]string{
    55  		keymanager.Imported: "Imported Wallet (Recommended)",
    56  		keymanager.Derived:  "HD Wallet",
    57  		keymanager.Remote:   "Remote Signing Wallet (Advanced)",
    58  	}
    59  	// ValidateExistingPass checks that an input cannot be empty.
    60  	ValidateExistingPass = func(input string) error {
    61  		if input == "" {
    62  			return errors.New("password input cannot be empty")
    63  		}
    64  		return nil
    65  	}
    66  )
    67  
    68  // Config to open a wallet programmatically.
    69  type Config struct {
    70  	WalletDir      string
    71  	KeymanagerKind keymanager.Kind
    72  	WalletPassword string
    73  }
    74  
    75  // Wallet is a primitive in Prysm's account management which
    76  // has the capability of creating new accounts, reading existing accounts,
    77  // and providing secure access to Ethereum proof of stake secrets depending on an
    78  // associated keymanager (either imported, derived, or remote signing enabled).
    79  type Wallet struct {
    80  	walletDir      string
    81  	accountsPath   string
    82  	configFilePath string
    83  	walletPassword string
    84  	keymanagerKind keymanager.Kind
    85  }
    86  
    87  // New creates a struct from config values.
    88  func New(cfg *Config) *Wallet {
    89  	accountsPath := filepath.Join(cfg.WalletDir, cfg.KeymanagerKind.String())
    90  	return &Wallet{
    91  		walletDir:      cfg.WalletDir,
    92  		accountsPath:   accountsPath,
    93  		keymanagerKind: cfg.KeymanagerKind,
    94  		walletPassword: cfg.WalletPassword,
    95  	}
    96  }
    97  
    98  // Exists checks if directory at walletDir exists
    99  func Exists(walletDir string) (bool, error) {
   100  	dirExists, err := fileutil.HasDir(walletDir)
   101  	if err != nil {
   102  		return false, errors.Wrap(err, "could not parse wallet directory")
   103  	}
   104  	isValid, err := IsValid(walletDir)
   105  	if errors.Is(err, ErrNoWalletFound) {
   106  		return false, nil
   107  	} else if err != nil {
   108  		return false, errors.Wrap(err, "could not check if dir is valid")
   109  	}
   110  	return dirExists && isValid, nil
   111  }
   112  
   113  // IsValid checks if a folder contains a single key directory such as `derived`, `remote` or `imported`.
   114  // Returns true if one of those subdirectories exist, false otherwise.
   115  func IsValid(walletDir string) (bool, error) {
   116  	expanded, err := fileutil.ExpandPath(walletDir)
   117  	if err != nil {
   118  		return false, err
   119  	}
   120  	f, err := os.Open(expanded)
   121  	if err != nil {
   122  		if strings.Contains(err.Error(), "no such file") ||
   123  			strings.Contains(err.Error(), "cannot find the file") ||
   124  			strings.Contains(err.Error(), "cannot find the path") {
   125  			return false, nil
   126  		}
   127  		return false, err
   128  	}
   129  	defer func() {
   130  		if err := f.Close(); err != nil {
   131  			log.Debugf("Could not close directory: %s", expanded)
   132  		}
   133  	}()
   134  	names, err := f.Readdirnames(-1)
   135  	if err != nil {
   136  		return false, err
   137  	}
   138  
   139  	if len(names) == 0 {
   140  		return false, ErrNoWalletFound
   141  	}
   142  
   143  	// Count how many wallet types we have in the directory
   144  	numWalletTypes := 0
   145  	for _, name := range names {
   146  		// Nil error means input name is `derived`, `remote` or `imported`
   147  		_, err = keymanager.ParseKind(name)
   148  		if err == nil {
   149  			numWalletTypes++
   150  		}
   151  	}
   152  	return numWalletTypes == 1, nil
   153  }
   154  
   155  // OpenWalletOrElseCli tries to open the wallet and if it fails or no wallet
   156  // is found, invokes a callback function.
   157  func OpenWalletOrElseCli(cliCtx *cli.Context, otherwise func(cliCtx *cli.Context) (*Wallet, error)) (*Wallet, error) {
   158  	exists, err := Exists(cliCtx.String(flags.WalletDirFlag.Name))
   159  	if err != nil {
   160  		return nil, errors.Wrap(err, CheckExistsErrMsg)
   161  	}
   162  	if !exists {
   163  		return otherwise(cliCtx)
   164  	}
   165  	isValid, err := IsValid(cliCtx.String(flags.WalletDirFlag.Name))
   166  	if errors.Is(err, ErrNoWalletFound) {
   167  		return otherwise(cliCtx)
   168  	}
   169  	if err != nil {
   170  		return nil, errors.Wrap(err, CheckValidityErrMsg)
   171  	}
   172  	if !isValid {
   173  		return nil, errors.New(InvalidWalletErrMsg)
   174  	}
   175  
   176  	walletDir, err := prompt.InputDirectory(cliCtx, prompt.WalletDirPromptText, flags.WalletDirFlag)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  	walletPassword, err := InputPassword(
   181  		cliCtx,
   182  		flags.WalletPasswordFileFlag,
   183  		PasswordPromptText,
   184  		false, /* Do not confirm password */
   185  		ValidateExistingPass,
   186  	)
   187  	if err != nil {
   188  		return nil, err
   189  	}
   190  	return OpenWallet(cliCtx.Context, &Config{
   191  		WalletDir:      walletDir,
   192  		WalletPassword: walletPassword,
   193  	})
   194  }
   195  
   196  // OpenWallet instantiates a wallet from a specified path. It checks the
   197  // type of keymanager associated with the wallet by reading files in the wallet
   198  // path, if applicable. If a wallet does not exist, returns an appropriate error.
   199  func OpenWallet(_ context.Context, cfg *Config) (*Wallet, error) {
   200  	exists, err := Exists(cfg.WalletDir)
   201  	if err != nil {
   202  		return nil, errors.Wrap(err, CheckExistsErrMsg)
   203  	}
   204  	if !exists {
   205  		return nil, ErrNoWalletFound
   206  	}
   207  	valid, err := IsValid(cfg.WalletDir)
   208  	// ErrNoWalletFound represents both a directory that does not exist as well as an empty directory
   209  	if errors.Is(err, ErrNoWalletFound) {
   210  		return nil, ErrNoWalletFound
   211  	}
   212  	if err != nil {
   213  		return nil, errors.Wrap(err, CheckValidityErrMsg)
   214  	}
   215  	if !valid {
   216  		return nil, errors.New(InvalidWalletErrMsg)
   217  	}
   218  
   219  	keymanagerKind, err := readKeymanagerKindFromWalletPath(cfg.WalletDir)
   220  	if err != nil {
   221  		return nil, errors.Wrap(err, "could not read keymanager kind for wallet")
   222  	}
   223  	accountsPath := filepath.Join(cfg.WalletDir, keymanagerKind.String())
   224  	return &Wallet{
   225  		walletDir:      cfg.WalletDir,
   226  		accountsPath:   accountsPath,
   227  		keymanagerKind: keymanagerKind,
   228  		walletPassword: cfg.WalletPassword,
   229  	}, nil
   230  }
   231  
   232  // SaveWallet persists the wallet's directories to disk.
   233  func (w *Wallet) SaveWallet() error {
   234  	if err := fileutil.MkdirAll(w.accountsPath); err != nil {
   235  		return errors.Wrap(err, "could not create wallet directory")
   236  	}
   237  	return nil
   238  }
   239  
   240  // KeymanagerKind used by the wallet.
   241  func (w *Wallet) KeymanagerKind() keymanager.Kind {
   242  	return w.keymanagerKind
   243  }
   244  
   245  // AccountsDir for the wallet.
   246  func (w *Wallet) AccountsDir() string {
   247  	return w.accountsPath
   248  }
   249  
   250  // Password for the wallet.
   251  func (w *Wallet) Password() string {
   252  	return w.walletPassword
   253  }
   254  
   255  // InitializeKeymanager reads a keymanager config from disk at the wallet path,
   256  // unmarshals it based on the wallet's keymanager kind, and returns its value.
   257  func (w *Wallet) InitializeKeymanager(ctx context.Context, cfg iface.InitKeymanagerConfig) (keymanager.IKeymanager, error) {
   258  	var km keymanager.IKeymanager
   259  	var err error
   260  	switch w.KeymanagerKind() {
   261  	case keymanager.Imported:
   262  		km, err = imported.NewKeymanager(ctx, &imported.SetupConfig{
   263  			Wallet:           w,
   264  			ListenForChanges: cfg.ListenForChanges,
   265  		})
   266  		if err != nil {
   267  			return nil, errors.Wrap(err, "could not initialize imported keymanager")
   268  		}
   269  	case keymanager.Derived:
   270  		km, err = derived.NewKeymanager(ctx, &derived.SetupConfig{
   271  			Wallet:           w,
   272  			ListenForChanges: cfg.ListenForChanges,
   273  		})
   274  		if err != nil {
   275  			return nil, errors.Wrap(err, "could not initialize derived keymanager")
   276  		}
   277  	case keymanager.Remote:
   278  		configFile, err := w.ReadKeymanagerConfigFromDisk(ctx)
   279  		if err != nil {
   280  			return nil, errors.Wrap(err, "could not read keymanager config")
   281  		}
   282  		opts, err := remote.UnmarshalOptionsFile(configFile)
   283  		if err != nil {
   284  			return nil, errors.Wrap(err, "could not unmarshal keymanager config file")
   285  		}
   286  		km, err = remote.NewKeymanager(ctx, &remote.SetupConfig{
   287  			Opts:           opts,
   288  			MaxMessageSize: 100000000,
   289  		})
   290  		if err != nil {
   291  			return nil, errors.Wrap(err, "could not initialize remote keymanager")
   292  		}
   293  	default:
   294  		return nil, fmt.Errorf("keymanager kind not supported: %s", w.keymanagerKind)
   295  	}
   296  	return km, nil
   297  }
   298  
   299  // WriteFileAtPath within the wallet directory given the desired path, filename, and raw data.
   300  func (w *Wallet) WriteFileAtPath(_ context.Context, filePath, fileName string, data []byte) error {
   301  	accountPath := filepath.Join(w.accountsPath, filePath)
   302  	hasDir, err := fileutil.HasDir(accountPath)
   303  	if err != nil {
   304  		return err
   305  	}
   306  	if !hasDir {
   307  		if err := fileutil.MkdirAll(accountPath); err != nil {
   308  			return errors.Wrapf(err, "could not create path: %s", accountPath)
   309  		}
   310  	}
   311  	fullPath := filepath.Join(accountPath, fileName)
   312  	if err := fileutil.WriteFile(fullPath, data); err != nil {
   313  		return errors.Wrapf(err, "could not write %s", filePath)
   314  	}
   315  	log.WithFields(logrus.Fields{
   316  		"path":     fullPath,
   317  		"fileName": fileName,
   318  	}).Debug("Wrote new file at path")
   319  	return nil
   320  }
   321  
   322  // ReadFileAtPath within the wallet directory given the desired path and filename.
   323  func (w *Wallet) ReadFileAtPath(_ context.Context, filePath, fileName string) ([]byte, error) {
   324  	accountPath := filepath.Join(w.accountsPath, filePath)
   325  	hasDir, err := fileutil.HasDir(accountPath)
   326  	if err != nil {
   327  		return nil, err
   328  	}
   329  	if !hasDir {
   330  		if err := fileutil.MkdirAll(accountPath); err != nil {
   331  			return nil, errors.Wrapf(err, "could not create path: %s", accountPath)
   332  		}
   333  	}
   334  	fullPath := filepath.Join(accountPath, fileName)
   335  	matches, err := filepath.Glob(fullPath)
   336  	if err != nil {
   337  		return []byte{}, errors.Wrap(err, "could not find file")
   338  	}
   339  	if len(matches) == 0 {
   340  		return []byte{}, fmt.Errorf("no files found in path: %s", fullPath)
   341  	}
   342  	rawData, err := ioutil.ReadFile(matches[0])
   343  	if err != nil {
   344  		return nil, errors.Wrapf(err, "could not read path: %s", filePath)
   345  	}
   346  	return rawData, nil
   347  }
   348  
   349  // FileNameAtPath return the full file name for the requested file. It allows for finding the file
   350  // with a regex pattern.
   351  func (w *Wallet) FileNameAtPath(_ context.Context, filePath, fileName string) (string, error) {
   352  	accountPath := filepath.Join(w.accountsPath, filePath)
   353  	if err := fileutil.MkdirAll(accountPath); err != nil {
   354  		return "", errors.Wrapf(err, "could not create path: %s", accountPath)
   355  	}
   356  	fullPath := filepath.Join(accountPath, fileName)
   357  	matches, err := filepath.Glob(fullPath)
   358  	if err != nil {
   359  		return "", errors.Wrap(err, "could not find file")
   360  	}
   361  	if len(matches) == 0 {
   362  		return "", fmt.Errorf("no files found in path: %s", fullPath)
   363  	}
   364  	fullFileName := filepath.Base(matches[0])
   365  	return fullFileName, nil
   366  }
   367  
   368  // ReadKeymanagerConfigFromDisk opens a keymanager config file
   369  // for reading if it exists at the wallet path.
   370  func (w *Wallet) ReadKeymanagerConfigFromDisk(_ context.Context) (io.ReadCloser, error) {
   371  	configFilePath := filepath.Join(w.accountsPath, KeymanagerConfigFileName)
   372  	if !fileutil.FileExists(configFilePath) {
   373  		return nil, fmt.Errorf("no keymanager config file found at path: %s", w.accountsPath)
   374  	}
   375  	w.configFilePath = configFilePath
   376  	return os.Open(configFilePath)
   377  
   378  }
   379  
   380  // WriteKeymanagerConfigToDisk takes an encoded keymanager config file
   381  // and writes it to the wallet path.
   382  func (w *Wallet) WriteKeymanagerConfigToDisk(_ context.Context, encoded []byte) error {
   383  	configFilePath := filepath.Join(w.accountsPath, KeymanagerConfigFileName)
   384  	// Write the config file to disk.
   385  	if err := fileutil.WriteFile(configFilePath, encoded); err != nil {
   386  		return errors.Wrapf(err, "could not write config to path: %s", configFilePath)
   387  	}
   388  	log.WithField("configFilePath", configFilePath).Debug("Wrote keymanager config file to disk")
   389  	return nil
   390  }
   391  
   392  func readKeymanagerKindFromWalletPath(walletPath string) (keymanager.Kind, error) {
   393  	walletItem, err := os.Open(walletPath)
   394  	if err != nil {
   395  		return 0, err
   396  	}
   397  	defer func() {
   398  		if err := walletItem.Close(); err != nil {
   399  			log.WithField(
   400  				"path", walletPath,
   401  			).Errorf("Could not close wallet directory: %v", err)
   402  		}
   403  	}()
   404  	list, err := walletItem.Readdirnames(0) // 0 to read all files and folders.
   405  	if err != nil {
   406  		return 0, fmt.Errorf("could not read files in directory: %s", walletPath)
   407  	}
   408  	for _, n := range list {
   409  		keymanagerKind, err := keymanager.ParseKind(n)
   410  		if err == nil {
   411  			return keymanagerKind, nil
   412  		}
   413  	}
   414  	return 0, errors.New("no keymanager folder (imported, remote, derived) found in wallet path")
   415  }
   416  
   417  // InputPassword prompts for a password and optionally for password confirmation.
   418  // The password is validated according to custom rules.
   419  func InputPassword(
   420  	cliCtx *cli.Context,
   421  	passwordFileFlag *cli.StringFlag,
   422  	promptText string,
   423  	confirmPassword bool,
   424  	passwordValidator func(input string) error,
   425  ) (string, error) {
   426  	if cliCtx.IsSet(passwordFileFlag.Name) {
   427  		passwordFilePathInput := cliCtx.String(passwordFileFlag.Name)
   428  		data, err := fileutil.ReadFileAsBytes(passwordFilePathInput)
   429  		if err != nil {
   430  			return "", errors.Wrap(err, "could not read file as bytes")
   431  		}
   432  		enteredPassword := strings.TrimRight(string(data), "\r\n")
   433  		if err := passwordValidator(enteredPassword); err != nil {
   434  			return "", errors.Wrap(err, "password did not pass validation")
   435  		}
   436  		return enteredPassword, nil
   437  	}
   438  	var hasValidPassword bool
   439  	var walletPassword string
   440  	var err error
   441  	for !hasValidPassword {
   442  		walletPassword, err = promptutil.PasswordPrompt(promptText, passwordValidator)
   443  		if err != nil {
   444  			return "", fmt.Errorf("could not read account password: %w", err)
   445  		}
   446  
   447  		if confirmPassword {
   448  			passwordConfirmation, err := promptutil.PasswordPrompt(ConfirmPasswordPromptText, passwordValidator)
   449  			if err != nil {
   450  				return "", fmt.Errorf("could not read password confirmation: %w", err)
   451  			}
   452  			if walletPassword != passwordConfirmation {
   453  				log.Error("Passwords do not match")
   454  				continue
   455  			}
   456  			hasValidPassword = true
   457  		} else {
   458  			return walletPassword, nil
   459  		}
   460  	}
   461  	return walletPassword, nil
   462  }