From fcd412f7a18348596ce28b7df190d7b7b1b13067 Mon Sep 17 00:00:00 2001 From: Zahary Karadjov Date: Tue, 14 Jul 2020 22:00:35 +0300 Subject: [PATCH] Finish the 'create wallet' command; Addresses #1319 --- .gitmodules | 5 + beacon_chain/beacon_node.nim | 108 ++----------------- beacon_chain/conf.nim | 27 +++-- beacon_chain/keystore_management.nim | 152 ++++++++++++++++++++++++++- beacon_chain/spec/keystore.nim | 13 ++- vendor/nimbus-security-resources | 1 + 6 files changed, 193 insertions(+), 113 deletions(-) create mode 160000 vendor/nimbus-security-resources diff --git a/.gitmodules b/.gitmodules index e5b67a336..e6466a949 100644 --- a/.gitmodules +++ b/.gitmodules @@ -183,3 +183,8 @@ url = https://github.com/eth2-clients/eth2-testnets.git ignore = dirty branch = master +[submodule "vendor/nimbus-security-resources"] + path = vendor/nimbus-security-resources + url = https://github.com/status-im/nimbus-security-resources.git + ignore = dirty + branch = master diff --git a/beacon_chain/beacon_node.nim b/beacon_chain/beacon_node.nim index 8d5085eba..0d2c4c83a 100644 --- a/beacon_chain/beacon_node.nim +++ b/beacon_chain/beacon_node.nim @@ -1016,107 +1016,6 @@ when hasPrompt: # var t: Thread[ptr Prompt] # createThread(t, processPromptCommands, addr p) -proc createWalletInteractively( - rng: var BrHmacDrbgContext, - conf: BeaconNodeConf): OutFile {.raises: [Defect].} = - if conf.nonInteractive: - fatal "Wallets can be created only in interactive mode" - quit 1 - - var mnemonic = generateMnemonic(rng) - defer: keystore_management.burnMem(mnemonic) - - template readLine: string = - try: stdin.readLine() - except IOError: - fatal "Failed to read data from stdin" - quit 1 - - echo "The created wallet will be protected with a password " & - "that applies only to the current Nimbus installation. " & - "In case you lose your wallet and you need to restore " & - "it on a different machine, you must use the following " & - "seed recovery phrase: \n" - - echo $mnemonic - - echo "Please back up the seed phrase now to a safe location as " & - "if you are protecting a sensitive password. The seed phrase " & - "be used to withdrawl funds from your wallet.\n" - - echo "Did you back up your seed recovery phrase? (please type 'yes' to continue or press enter to quit)" - while true: - let answer = readLine() - if answer == "": - quit 1 - elif answer != "yes": - echo "To continue, please type 'yes' (without the quotes) or press enter to quit" - else: - break - - echo "When you perform operations with your wallet such as withdrawals " & - "and additional deposits, you'll be asked to enter a password. " & - "Please note that this password is local to the current Nimbus " & - "installation and can be changed at any time." - - while true: - var password, confirmedPassword: TaintedString - try: - let status = try: - readPasswordFromStdin("Please enter a password:", password) and - readPasswordFromStdin("Please repeat the password:", confirmedPassword) - except IOError: - fatal "Failed to read password interactively" - quit 1 - - if status: - if password != confirmedPassword: - echo "Passwords don't match, please try again" - else: - var name: WalletName - if conf.createdWalletName.isSome: - name = conf.createdWalletName.get - else: - echo "For your convenience, the wallet can be identified with a name " & - "of your choice. Please enter a wallet name below or press ENTER " & - "to continue with a machine-generated name." - - while true: - var enteredName = readLine() - if enteredName.len > 0: - name = try: WalletName.parseCmdArg(enteredName) - except CatchableError as err: - echo err.msg & ". Please try again." - continue - break - - let (uuid, walletContent) = KdfPbkdf2.createWalletContent( - rng, mnemonic, name) - try: - var outWalletFile: OutFile - - if conf.createdWalletFile.isSome: - outWalletFile = conf.createdWalletFile.get - createDir splitFile(string outWalletFile).dir - else: - let walletsDir = conf.walletsDir - createDir walletsDir - outWalletFile = OutFile(walletsDir / addFileExt(string uuid, "json")) - - writeFile(string outWalletFile, string walletContent) - return outWalletFile - except CatchableError as err: - fatal "Failed to write wallet file", err = err.msg - quit 1 - - if not status: - fatal "Failed to read a password from stdin" - quit 1 - - finally: - keystore_management.burnMem(password) - keystore_management.burnMem(confirmedPassword) - programMain: var config = makeBannerAndConfig(clientId, BeaconNodeConf) @@ -1340,10 +1239,15 @@ programMain: of wallets: case config.walletsCmd: of WalletsCmd.create: - let walletFile = createWalletInteractively(rng[], config) + let status = createWalletInteractively(rng[], config) + if status.isErr: + echo status.error + quit 1 + of WalletsCmd.list: # TODO discard + of WalletsCmd.restore: # TODO discard diff --git a/beacon_chain/conf.nim b/beacon_chain/conf.nim index 41bcdf286..4bea3e0e9 100644 --- a/beacon_chain/conf.nim +++ b/beacon_chain/conf.nim @@ -268,6 +268,10 @@ type desc: "Initial value for the 'nextaccount' property of the wallet" name: "next-account" }: Option[Natural] + outWalletsDirFlag* {. + desc: "A directory containing wallet files" + name: "wallets-dir" }: Option[OutDir] + createdWalletFile* {. desc: "Output wallet file" name: "out" }: Option[OutFile] @@ -296,6 +300,11 @@ type desc: "Private key of the controlling (sending) account", name: "deposit-private-key" }: string + depositsDir* {. + defaultValue: "validators" + desc: "A folder with validator metadata created by the `deposits create` command" + name: "deposits-dir" }: string + case depositsCmd* {.command.}: DepositsCmd of DepositsCmd.create: totalDeposits* {. @@ -331,11 +340,6 @@ type name: "dont-send" .}: bool of DepositsCmd.send: - depositsDir* {. - defaultValue: "validators" - desc: "A folder with validator metadata created by the `deposits create` command" - name: "deposits-dir" }: string - minDelay* {. defaultValue: 0.0 desc: "Minimum possible delay between making two deposits (in seconds)" @@ -474,7 +478,18 @@ func secretsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = string conf.secretsDirFlag.get(InputDir(conf.dataDir / "secrets")) func walletsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = - string conf.walletsDirFlag.get(InputDir(conf.dataDir / "wallets")) + case conf.cmd + of noCommand: + if conf.walletsDirFlag.isSome: + return conf.walletsDirFlag.get.string + of wallets: + doAssert conf.walletsCmd == WalletsCmd.create + if conf.outWalletsDirFlag.isSome: + return conf.outWalletsDirFlag.get.string + else: + raiseAssert "Inappropraite call to walletsDir" + + return conf.dataDir / "wallets" func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string = conf.dataDir / "db" diff --git a/beacon_chain/keystore_management.nim b/beacon_chain/keystore_management.nim index 6ae7de039..703873554 100644 --- a/beacon_chain/keystore_management.nim +++ b/beacon_chain/keystore_management.nim @@ -1,5 +1,5 @@ import - os, strutils, terminal, + std/[os, strutils, terminal, wordwrap], stew/byteutils, chronicles, chronos, web3, stint, json_serialization, serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl, spec/[datatypes, digest, crypto, keystore], @@ -162,6 +162,156 @@ proc loadDeposits*(depositsDir: string): seq[Deposit] = path = depositsDir, err = err.msg quit 1 +const + minPasswordLen = 10 + + mostCommonPasswords = wordListArray( + currentSourcePath.parentDir / + "../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt", + minWordLength = minPasswordLen) + +proc createWalletInteractively*( + rng: var BrHmacDrbgContext, + conf: BeaconNodeConf): Result[OutFile, cstring] = + + if conf.nonInteractive: + return err "Wallets can be created only in interactive mode" + + var mnemonic = generateMnemonic(rng) + defer: burnMem(mnemonic) + + template ask(prompt: string): string = + try: + stdout.write prompt, ": " + stdin.readLine() + except IOError: + return err "Failed to read data from stdin" + + template echo80(msg: string) = + echo wrapWords(msg, 80) + + proc readPasswordInput(prompt: string, password: var TaintedString): bool = + try: readPasswordFromStdin(prompt, password) + except IOError: false + + echo80 "The generated wallet is uniquely identified by a seed phrase " & + "consisting of 24 words. In case you lose your wallet and you " & + "need to restore it on a different machine, you must use the " & + "words displayed below:" + + try: + echo "" + stdout.setStyle({styleBright}) + stdout.setForegroundColor fgCyan + echo80 $mnemonic + stdout.resetAttributes() + echo "" + except IOError, ValueError: + return err "Failed to write to the standard output" + + echo80 "Please back up the seed phrase now to a safe location as " & + "if you are protecting a sensitive password. The seed phrase " & + "can be used to withdrawl funds from your wallet." + + echo "" + echo "Did you back up your seed recovery phrase?\p" & + "(please type 'yes' to continue or press enter to quit)" + + while true: + let answer = ask "Answer" + if answer == "": + return err "Wallet creation aborted" + elif answer != "yes": + echo "To continue, please type 'yes' (without the quotes) or press enter to quit" + else: + break + + echo "" + echo80 "When you perform operations with your wallet such as withdrawals " & + "and additional deposits, you'll be asked to enter a password. " & + "Please note that this password is local to the current Nimbus " & + "installation and can be changed at any time." + echo "" + + while true: + var password, confirmedPassword: TaintedString + try: + var firstTry = true + + template prompt: string = + if firstTry: + "Please enter a password:" + else: + "Please enter a new password:" + + while true: + if not readPasswordInput(prompt, password): + return err "Failed to read a password from stdin" + + if password.len < minPasswordLen: + try: + echo "The entered password should be at least $1 characters." % + [$minPasswordLen] + except ValueError as err: + raiseAssert "The format string above is correct" + elif password in mostCommonPasswords: + echo80 "The entered password is too commonly used and it would be easy " & + "to brute-force with automated tools." + else: + break + + firstTry = false + + if not readPasswordInput("Please repeat the password:", confirmedPassword): + return err "Failed to read a password from stdin" + + if password != confirmedPassword: + echo "Passwords don't match, please try again" + continue + + var name: WalletName + if conf.createdWalletName.isSome: + name = conf.createdWalletName.get + else: + echo "" + echo80 "For your convenience, the wallet can be identified with a name " & + "of your choice. Please enter a wallet name below or press ENTER " & + "to continue with a machine-generated name." + + while true: + var enteredName = ask "Wallet name" + if enteredName.len > 0: + name = try: WalletName.parseCmdArg(enteredName) + except CatchableError as err: + echo err.msg & ". Please try again." + continue + break + + let (uuid, walletContent) = KdfPbkdf2.createWalletContent( + rng, mnemonic, + name = name, + password = KeyStorePass password) + try: + var outWalletFile: OutFile + + if conf.createdWalletFile.isSome: + outWalletFile = conf.createdWalletFile.get + createDir splitFile(string outWalletFile).dir + else: + let walletsDir = conf.walletsDir + createDir walletsDir + outWalletFile = OutFile(walletsDir / addFileExt(string uuid, "json")) + + writeFile(string outWalletFile, string walletContent) + echo "Wallet file written to ", outWalletFile + return ok outWalletFile + except CatchableError as err: + return err "Failed to write wallet file" + + finally: + burnMem(password) + burnMem(confirmedPassword) + {.pop.} # TODO: async functions should note take `seq` inputs because diff --git a/beacon_chain/spec/keystore.nim b/beacon_chain/spec/keystore.nim index 2b0c2150b..e74eadfad 100644 --- a/beacon_chain/spec/keystore.nim +++ b/beacon_chain/spec/keystore.nim @@ -137,15 +137,20 @@ proc getRandomBytes*(rng: var BrHmacDrbgContext, n: Natural): seq[byte] result = newSeq[byte](n) brHmacDrbgGenerate(rng, result) -macro wordListArray(filename: static string): array[wordListLen, cstring] = +macro wordListArray*(filename: static string, + maxWords: static int = 0, + minWordLength: static int = 0): untyped = result = newTree(nnkBracket) var words = slurp(filename).split() - words.setLen wordListLen for word in words: - result.add newCall("cstring", newLit(word)) + if word.len >= minWordLength: + result.add newCall("cstring", newLit(word)) + if maxWords > 0 and result.len >= maxWords: + return const - englishWords = wordListArray "english_word_list.txt" + englishWords = wordListArray("english_word_list.txt", + maxWords = wordListLen) iterator pathNodesImpl(path: string): Natural {.raises: [ValueError].} = diff --git a/vendor/nimbus-security-resources b/vendor/nimbus-security-resources new file mode 160000 index 000000000..145e12aaa --- /dev/null +++ b/vendor/nimbus-security-resources @@ -0,0 +1 @@ +Subproject commit 145e12aaad958cae8fce34e48be0da0111733bf6