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
|
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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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].} =
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 145e12aaad958cae8fce34e48be0da0111733bf6
|
Loading…
Reference in New Issue