Address #1689 and #1517 Usability and security improvements in wallet creation

This commit is contained in:
Zahary Karadjov 2020-10-09 23:38:06 +03:00 committed by zah
parent 02a3770803
commit 69e498dc00
1 changed files with 100 additions and 55 deletions

View File

@ -34,7 +34,9 @@ const
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt", "../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
minWordLen = minPasswordLen) minWordLen = minPasswordLen)
template echo80(msg: string) = proc echoP(msg: string) =
## Prints a paragraph aligned to 80 columns
echo ""
echo wrapWords(msg, 80) echo wrapWords(msg, 80)
proc checkAndCreateDataDir*(dataDir: string): bool = proc checkAndCreateDataDir*(dataDir: string): bool =
@ -129,15 +131,18 @@ proc keyboardCreatePassword(prompt: string, confirm: string): KsResult[string] =
# We treat `password` as UTF-8 encoded string. # We treat `password` as UTF-8 encoded string.
if validateUtf8(password) == -1: if validateUtf8(password) == -1:
if runeLen(password) < minPasswordLen: if runeLen(password) < minPasswordLen:
echo80 "The entered password should be at least " & $minPasswordLen & echoP "The entered password should be at least " & $minPasswordLen &
" characters." " characters."
echo ""
continue continue
elif password in mostCommonPasswords: elif password in mostCommonPasswords:
echo80 "The entered password is too commonly used and it would be " & echoP "The entered password is too commonly used and it would be " &
"easy to brute-force with automated tools." "easy to brute-force with automated tools."
echo ""
continue continue
else: else:
echo80 "Entered password is not valid UTF-8 string" echoP "Entered password is not valid UTF-8 string"
echo ""
continue continue
let confirmedPassword = let confirmedPassword =
@ -148,7 +153,7 @@ proc keyboardCreatePassword(prompt: string, confirm: string): KsResult[string] =
return err("Could not read password from stdin") return err("Could not read password from stdin")
if password != confirmedPassword: if password != confirmedPassword:
echo "Passwords don't match, please try again" echo "Passwords don't match, please try again\n"
continue continue
return ok(password) return ok(password)
@ -539,11 +544,10 @@ template ask(prompt: string): string =
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext, proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
config: BeaconNodeConf, config: BeaconNodeConf,
mnemonic: Mnemonic): Result[WalletPathPair, string] = mnemonic: Mnemonic): Result[WalletPathPair, string] =
echo "" echoP "When you perform operations with your wallet such as withdrawals " &
echo80 "When you perform operations with your wallet such as withdrawals " & "and additional deposits, you'll be asked to enter a password. " &
"and additional deposits, you'll be asked to enter a password. " & "Please note that this password is local to the current machine " &
"Please note that this password is local to the current Nimbus " & "and you can change it at any time."
"installation and can be changed at any time."
echo "" echo ""
var password = var password =
@ -561,10 +565,9 @@ proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
if outWalletName.isSome: if outWalletName.isSome:
name = outWalletName.get name = outWalletName.get
else: else:
echo "" echoP "For your convenience, the wallet can be identified with a name " &
echo80 "For your convenience, the wallet can be identified with a name " & "of your choice. Please enter a wallet name below or press ENTER " &
"of your choice. Please enter a wallet name below or press ENTER " & "to continue with a machine-generated name."
"to continue with a machine-generated name."
while true: while true:
var enteredName = ask "Wallet name" var enteredName = ask "Wallet name"
@ -577,30 +580,38 @@ proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
continue continue
break break
let nextAccount = let nextAccount =
if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore: if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore:
config.restoredDepositsCount config.restoredDepositsCount
else: else:
none Natural none Natural
let wallet = createWallet(kdfPbkdf2, rng, mnemonic, let wallet = createWallet(kdfPbkdf2, rng, mnemonic,
name = name, name = name,
nextAccount = nextAccount, nextAccount = nextAccount,
password = KeystorePass.init password) password = KeystorePass.init password)
let outWalletFileFlag = config.outWalletFile let outWalletFileFlag = config.outWalletFile
let outWalletFile = let outWalletFile =
if outWalletFileFlag.isSome: if outWalletFileFlag.isSome:
string outWalletFileFlag.get string outWalletFileFlag.get
else: else:
config.walletsDir / addFileExt(string wallet.name, "json") config.walletsDir / addFileExt(string wallet.name, "json")
let status = saveWallet(wallet, outWalletFile) let status = saveWallet(wallet, outWalletFile)
if status.isErr: if status.isErr:
return err("failure to create wallet file due to " & status.error) return err("failure to create wallet file due to " & status.error)
echo "\nWallet file successfully written to \"", outWalletFile, "\"" echo "\nWallet file successfully written to \"", outWalletFile, "\""
return ok WalletPathPair(wallet: wallet, path: outWalletFile) return ok WalletPathPair(wallet: wallet, path: outWalletFile)
proc safeEraseScreen() =
try:
stdout.eraseScreen
except IOError:
discard
discard execShellCmd(when defined(windows): "cls" else: "clear")
proc createWalletInteractively*( proc createWalletInteractively*(
rng: var BrHmacDrbgContext, rng: var BrHmacDrbgContext,
@ -609,40 +620,74 @@ proc createWalletInteractively*(
if config.nonInteractive: if config.nonInteractive:
return err "not running in interactive mode" return err "not running in interactive mode"
echoP "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 can use the " &
"seed phrase to re-generate your signing and withdrawal keys."
echoP "The seed phrase should be kept secretly in a safe location as if " &
"you are protecting a sensitive password. It can be used to withdraw " &
"funds from your wallet."
echoP "We will display the seed phrase on the next screen. Please make sure " &
"you are in a safe environment and there are no cameras or potentially " &
"unwanted eye witnesses around you. Please prepare everything necessary " &
"to copy the seed phrase to a safe location and type 'continue' in " &
"the prompt below to proceed to the next screen or 'q' to exit now."
echo ""
while true:
let answer = ask "Action"
if answer.len > 0 and answer[0] == 'q': quit 1
if answer == "continue": break
echoP "To proceed to your seed phrase, please type 'continue' (without the quotes). " &
"Type 'q' to exit now."
echo ""
var mnemonic = generateMnemonic(rng) var mnemonic = generateMnemonic(rng)
defer: burnMem(mnemonic) defer: burnMem(mnemonic)
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: try:
echo "" echoP "Your seed phrase is:"
setStyleNoError({styleBright}) setStyleNoError({styleBright})
setForegroundColorNoError fgCyan setForegroundColorNoError fgCyan
echo80 $mnemonic echoP $mnemonic
resetAttributesNoError() resetAttributesNoError()
echo ""
except IOError, ValueError: except IOError, ValueError:
return err "failure to write to the standard output" return err "failure to write to the standard output"
echo80 "Please back up the seed phrase now to a safe location as " & echoP "Press any key to continue."
"if you are protecting a sensitive password. The seed phrase " & try:
"can be used to withdraw funds from your wallet." discard stdin.readChar()
except IOError as err:
fatal "Failed to read a key from stdin", err = err.msg
quit 1
safeEraseScreen()
echoP "To confirm that you've saved the seed phrase, please enter the first and the " &
"last three words of it. In case you've saved the seek phrase in your clipboard, " &
"we strongly advice clearing the clipboard now."
echo "" echo ""
echo "Did you back up your seed recovery phrase?\p" &
"(please type 'yes' to continue or press enter to quit)"
while true: for i in countdown(2, 0):
let answer = ask "Answer" let answer = ask "Answer"
if answer == "": let parts = answer.split(' ', maxsplit = 1)
return err "aborted wallet creation" if parts.len == 2:
elif answer != "yes": if count(parts[1], ' ') == 2 and
echo "To continue, please type 'yes' (without the quotes) or press enter to quit" mnemonic.string.startsWith(parts[0]) and
mnemonic.string.endsWith(parts[1]):
break
else: else:
break doAssert parts.len == 1
if i > 0:
echo "\nYour answer was not correct. You have ", i, " more attempts"
echoP "Please enter 4 words separated with a single space " &
"(the first word from the seed phrase, followed by the last 3)"
echo ""
else:
quit 1
safeEraseScreen()
let walletPath = ? pickPasswordAndSaveWallet(rng, config, mnemonic) let walletPath = ? pickPasswordAndSaveWallet(rng, config, mnemonic)
return ok CreatedWallet(walletPath: walletPath, mnemonic: mnemonic) return ok CreatedWallet(walletPath: walletPath, mnemonic: mnemonic)