Finish the 'create wallet' command; Addresses #1319
This commit is contained in:
parent
dad3dd5809
commit
fcd412f7a1
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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].} =
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 145e12aaad958cae8fce34e48be0da0111733bf6
|
Loading…
Reference in New Issue