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 }