Finish the 'create wallet' command; Addresses #1319

This commit is contained in:
Zahary Karadjov 2020-07-14 22:00:35 +03:00 committed by zah
parent dad3dd5809
commit fcd412f7a1
6 changed files with 193 additions and 113 deletions

5
.gitmodules vendored
View File

@ -183,3 +183,8 @@
url = https://github.com/eth2-clients/eth2-testnets.git url = https://github.com/eth2-clients/eth2-testnets.git
ignore = dirty ignore = dirty
branch = master 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

View File

@ -1016,107 +1016,6 @@ when hasPrompt:
# var t: Thread[ptr Prompt] # var t: Thread[ptr Prompt]
# createThread(t, processPromptCommands, addr p) # 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: programMain:
var config = makeBannerAndConfig(clientId, BeaconNodeConf) var config = makeBannerAndConfig(clientId, BeaconNodeConf)
@ -1340,10 +1239,15 @@ programMain:
of wallets: of wallets:
case config.walletsCmd: case config.walletsCmd:
of WalletsCmd.create: of WalletsCmd.create:
let walletFile = createWalletInteractively(rng[], config) let status = createWalletInteractively(rng[], config)
if status.isErr:
echo status.error
quit 1
of WalletsCmd.list: of WalletsCmd.list:
# TODO # TODO
discard discard
of WalletsCmd.restore: of WalletsCmd.restore:
# TODO # TODO
discard discard

View File

@ -268,6 +268,10 @@ type
desc: "Initial value for the 'nextaccount' property of the wallet" desc: "Initial value for the 'nextaccount' property of the wallet"
name: "next-account" }: Option[Natural] name: "next-account" }: Option[Natural]
outWalletsDirFlag* {.
desc: "A directory containing wallet files"
name: "wallets-dir" }: Option[OutDir]
createdWalletFile* {. createdWalletFile* {.
desc: "Output wallet file" desc: "Output wallet file"
name: "out" }: Option[OutFile] name: "out" }: Option[OutFile]
@ -296,6 +300,11 @@ type
desc: "Private key of the controlling (sending) account", desc: "Private key of the controlling (sending) account",
name: "deposit-private-key" }: string 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 case depositsCmd* {.command.}: DepositsCmd
of DepositsCmd.create: of DepositsCmd.create:
totalDeposits* {. totalDeposits* {.
@ -331,11 +340,6 @@ type
name: "dont-send" .}: bool name: "dont-send" .}: bool
of DepositsCmd.send: of DepositsCmd.send:
depositsDir* {.
defaultValue: "validators"
desc: "A folder with validator metadata created by the `deposits create` command"
name: "deposits-dir" }: string
minDelay* {. minDelay* {.
defaultValue: 0.0 defaultValue: 0.0
desc: "Minimum possible delay between making two deposits (in seconds)" 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")) string conf.secretsDirFlag.get(InputDir(conf.dataDir / "secrets"))
func walletsDir*(conf: BeaconNodeConf|ValidatorClientConf): string = 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 = func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
conf.dataDir / "db" conf.dataDir / "db"

View File

@ -1,5 +1,5 @@
import import
os, strutils, terminal, std/[os, strutils, terminal, wordwrap],
stew/byteutils, chronicles, chronos, web3, stint, json_serialization, stew/byteutils, chronicles, chronos, web3, stint, json_serialization,
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl, serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
spec/[datatypes, digest, crypto, keystore], spec/[datatypes, digest, crypto, keystore],
@ -162,6 +162,156 @@ proc loadDeposits*(depositsDir: string): seq[Deposit] =
path = depositsDir, err = err.msg path = depositsDir, err = err.msg
quit 1 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.} {.pop.}
# TODO: async functions should note take `seq` inputs because # TODO: async functions should note take `seq` inputs because

View File

@ -137,15 +137,20 @@ proc getRandomBytes*(rng: var BrHmacDrbgContext, n: Natural): seq[byte]
result = newSeq[byte](n) result = newSeq[byte](n)
brHmacDrbgGenerate(rng, result) 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) result = newTree(nnkBracket)
var words = slurp(filename).split() var words = slurp(filename).split()
words.setLen wordListLen
for word in words: 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 const
englishWords = wordListArray "english_word_list.txt" englishWords = wordListArray("english_word_list.txt",
maxWords = wordListLen)
iterator pathNodesImpl(path: string): Natural iterator pathNodesImpl(path: string): Natural
{.raises: [ValueError].} = {.raises: [ValueError].} =

1
vendor/nimbus-security-resources vendored Submodule

@ -0,0 +1 @@
Subproject commit 145e12aaad958cae8fce34e48be0da0111733bf6