Merge different places of keyboard input to keyboardCreatePassword() and keyboardGetPassword().

Fix Windows problem with english words array.
Add checksum for english words array verification.
This commit is contained in:
cheatfate 2020-09-29 19:49:09 +03:00 committed by zah
parent d2f8cf9386
commit 9441e912cb
2 changed files with 170 additions and 157 deletions

View File

@ -1,5 +1,5 @@
import import
std/[os, strutils, terminal, wordwrap], std/[os, strutils, terminal, wordwrap, unicode],
chronicles, chronos, web3, stint, json_serialization, 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],
@ -116,6 +116,64 @@ proc checkFilePermissions*(filePath: string): bool =
else: else:
true true
proc keyboardCreatePassword(prompt: string, confirm: string): KsResult[string] =
while true:
let password =
try:
readPasswordFromStdin(prompt)
except IOError:
error "Could not read password from stdin"
return err("Could not read password from stdin")
# We treat `password` as UTF-8 encoded string.
if validateUtf8(password) == -1:
if runeLen(password) < minPasswordLen:
echo80 "The entered password should be at least " & $minPasswordLen &
" characters."
continue
elif password in mostCommonPasswords:
echo80 "The entered password is too commonly used and it would be " &
"easy to brute-force with automated tools."
continue
else:
echo80 "Entered password is not valid UTF-8 string"
continue
let confirmedPassword =
try:
readPasswordFromStdin(confirm)
except IOError:
error "Could not read password from stdin"
return err("Could not read password from stdin")
if password != confirmedPassword:
echo "Passwords don't match, please try again"
continue
return ok(password)
proc keyboardGetPassword[T](prompt: string, attempts: int,
pred: proc(p: string): KsResult[T] {.closure.}): KsResult[T] =
var
remainingAttempts = attempts
counter = 1
while remainingAttempts > 0:
let passphrase =
try:
readPasswordFromStdin(prompt)
except IOError as exc:
error "Could not read password from stdin"
return
os.sleep(1000 * counter)
let res = pred(passphrase)
if res.isOk():
return res
else:
inc(counter)
dec(remainingAttempts)
err("Failed to decrypt keystore")
proc loadKeystore(validatorsDir, secretsDir, keyName: string, proc loadKeystore(validatorsDir, secretsDir, keyName: string,
nonInteractive: bool): Option[ValidatorPrivKey] = nonInteractive: bool): Option[ValidatorPrivKey] =
let let
@ -155,21 +213,19 @@ proc loadKeystore(validatorsDir, secretsDir, keyName: string,
keyName, validatorsDir, secretsDir = secretsDir keyName, validatorsDir, secretsDir = secretsDir
return return
var remainingAttempts = 3 let prompt = "Please enter passphrase for key \"" &
var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\"\n" (validatorsDir / keyName) & "\": "
while remainingAttempts > 0: let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3,
let passphrase = KeystorePass: proc (password: string): KsResult[ValidatorPrivKey] =
try: readPasswordFromStdin(prompt) let decrypted = decryptKeystore(keystore, KeystorePass password)
except IOError: if decrypted.isErr():
error "STDIN not readable. Cannot obtain Keystore password" error "Keystore decryption failed. Please try again", keystorePath
return decrypted
)
let decrypted = decryptKeystore(keystore, passphrase) if res.isOk():
if decrypted.isOk: some(res.get())
return decrypted.get.some else:
else: return
prompt = "Keystore decryption failed. Please try again"
dec remainingAttempts
iterator validatorKeysFromDirs*(validatorsDir, secretsDir: string): ValidatorPrivKey = iterator validatorKeysFromDirs*(validatorsDir, secretsDir: string): ValidatorPrivKey =
try: try:
@ -244,67 +300,34 @@ proc loadNetKeystore*(keyStorePath: string,
error "Network keystore decryption failed", key_store = keyStorePath error "Network keystore decryption failed", key_store = keyStorePath
return return
else: else:
var remainingAttempts = 3 let prompt = "Please enter passphrase to unlock networking key: "
var counter = 0 let res = keyboardGetPassword[lcrypto.PrivateKey](prompt, 3,
var prompt = "Please enter passphrase to unlock networking key: " proc (password: string): KsResult[lcrypto.PrivateKey] =
while remainingAttempts > 0: let decrypted = decryptNetKeystore(keystore, KeystorePass password)
let passphrase = KeystorePass: if decrypted.isErr():
try: error "Keystore decryption failed. Please try again", keystorePath
readPasswordFromStdin(prompt) decrypted
except IOError: )
error "Could not read password from stdin" if res.isOk():
return some(res.get())
else:
let decrypted = decryptNetKeystore(keystore, passphrase) return
if decrypted.isOk:
return some(decrypted.get())
else:
dec remainingAttempts
inc counter
os.sleep(1000 * counter)
error "Network keystore decryption failed", key_store = keyStorePath
proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string, proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string,
netKey: lcrypto.PrivateKey, insecurePwd: Option[string] netKey: lcrypto.PrivateKey, insecurePwd: Option[string]
): Result[void, KeystoreGenerationError] = ): Result[void, KeystoreGenerationError] =
var password, confirmedPassword: TaintedString let password =
if insecurePwd.isSome(): if insecurePwd.isSome():
warn "Using insecure password to lock networking key", warn "Using insecure password to lock networking key",
key_path = keyStorePath key_path = keyStorePath
password = insecurePwd.get() insecurePwd.get()
else: else:
while true:
let prompt = "Please enter NEW password to lock network key storage: " let prompt = "Please enter NEW password to lock network key storage: "
let confirm = "Please confirm, network key storage password: "
password = let res = keyboardCreatePassword(prompt, confirm)
try: if res.isErr():
readPasswordFromStdin(prompt) return err(FailedToCreateKeystoreFile)
except IOError: res.get()
error "Could not read password from stdin"
return err(FailedToCreateKeystoreFile)
if len(password) < minPasswordLen:
echo "The entered password should be at least ", minPasswordLen,
" characters"
continue
elif password in mostCommonPasswords:
echo80 "The entered password is too commonly used and it would be " &
"easy to brute-force with automated tools."
continue
confirmedPassword =
try:
readPasswordFromStdin("Please confirm, network key storage " &
"password: ")
except IOError:
error "Could not read password from stdin"
return err(FailedToCreateKeystoreFile)
if password != confirmedPassword:
echo "Passwords don't match, please try again"
continue
break
let keyStore = createNetKeystore(kdfScrypt, rng, netKey, let keyStore = createNetKeystore(kdfScrypt, rng, netKey,
KeystorePass password) KeystorePass password)
@ -526,87 +549,64 @@ proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
"installation and can be changed at any time." "installation and can be changed at any time."
echo "" echo ""
while true: let password =
var password, confirmedPassword: TaintedString block:
try: let prompt = "Please enter a password: "
var firstTry = true let confirm = "Please repeat the password: "
let res = keyboardCreatePassword(prompt, confirm)
if res.isErr():
return err($res.error)
res.get()
template prompt: string = var name: WalletName
if firstTry: let outWalletName = config.outWalletName
"Please enter a password: " if outWalletName.isSome:
else: name = outWalletName.get
"Please enter a new password: " 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: while true:
if not readPasswordInput(prompt, password): var enteredName = ask "Wallet name"
return err "failure to read a password from stdin" if enteredName.len > 0:
name =
if password.len < minPasswordLen:
try: try:
echo "The entered password should be at least $1 characters." % WalletName.parseCmdArg(enteredName)
[$minPasswordLen] except CatchableError as err:
except ValueError: echo err.msg & ". Please try again."
raiseAssert "The format string above is correct" continue
elif password in mostCommonPasswords: break
echo80 "The entered password is too commonly used and it would be easy " &
"to brute-force with automated tools."
else:
break
firstTry = false let nextAccount =
if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore:
if not readPasswordInput("Please repeat the password:", confirmedPassword):
return err "failure to read a password from stdin"
if password != confirmedPassword:
echo "Passwords don't match, please try again"
continue
var name: WalletName
let outWalletName = config.outWalletName
if outWalletName.isSome:
name = outWalletName.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 nextAccount = 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 password) password = KeystorePass password)
let outWalletFileFlag = config.outWalletFile let outWalletFileFlag = config.outWalletFile
let outWalletFile = if outWalletFileFlag.isSome: let outWalletFile =
if outWalletFileFlag.isSome:
string outWalletFileFlag.get string outWalletFileFlag.get
else: else:
config.walletsDir / addFileExt(string wallet.uuid, "json") config.walletsDir / addFileExt(string wallet.uuid, "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)
notice "Wallet file written", path = outWalletFile
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
finally:
burnMem(password) burnMem(password)
burnMem(confirmedPassword) burnMem(confirmedPassword)
return err("failure to create wallet file due to " & status.error)
info "Wallet file written", path = outWalletFile
burnMem(password)
burnMem(confirmedPassword)
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
proc createWalletInteractively*( proc createWalletInteractively*(
rng: var BrHmacDrbgContext, rng: var BrHmacDrbgContext,
@ -665,7 +665,7 @@ proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
echo "To restore your wallet, please enter your backed-up seed phrase." echo "To restore your wallet, please enter your backed-up seed phrase."
while true: while true:
if not readPasswordInput("Seedphrase:", enteredMnemonic): if not readPasswordInput("Seedphrase: ", enteredMnemonic):
fatal "failure to read password from stdin" fatal "failure to read password from stdin"
quit 1 quit 1
@ -685,24 +685,26 @@ proc loadWallet*(fileName: string): Result[Wallet, string] =
err exc.msg err exc.msg
proc unlockWalletInteractively*(wallet: Wallet): Result[Mnemonic, string] = proc unlockWalletInteractively*(wallet: Wallet): Result[Mnemonic, string] =
let prompt = "Please enter the password for unlocking the wallet: "
echo "Please enter the password for unlocking the wallet" echo "Please enter the password for unlocking the wallet"
for i in 1..3: let res = keyboardGetPassword[Mnemonic](prompt, 3,
var password: TaintedString proc (password: string): KsResult[Mnemonic] =
try:
if not readPasswordInput("Password: ", password):
return err "failure to read password from stdin"
var secret = decryptCryptoField(wallet.crypto, KeystorePass password) var secret = decryptCryptoField(wallet.crypto, KeystorePass password)
if secret.len > 0: if len(secret) > 0:
defer: burnMem(secret) let mnemonic = Mnemonic(string.fromBytes(secret))
return ok Mnemonic(string.fromBytes(secret)) burnMem(secret)
ok(mnemonic)
else: else:
echo "Unlocking of the wallet failed. Please try again." let failed = "Unlocking of the wallet failed. Please try again"
finally: echo failed
burnMem(password) err(failed)
)
return err "failure to unlock wallet" if res.isOk():
ok(res.get())
else:
err "Unlocking of the wallet failed."
proc findWallet*(config: BeaconNodeConf, proc findWallet*(config: BeaconNodeConf,
name: WalletName): Result[WalletPathPair, string] = name: WalletName): Result[WalletPathPair, string] =

View File

@ -202,7 +202,7 @@ macro wordListArray*(filename: static string,
minWordLen: static int = 0, minWordLen: static int = 0,
maxWordLen: static int = high(int)): untyped = maxWordLen: static int = high(int)): untyped =
result = newTree(nnkBracket) result = newTree(nnkBracket)
var words = slurp(filename).split() var words = slurp(filename).splitLines()
for word in words: for word in words:
if word.len >= minWordLen and word.len <= maxWordLen: if word.len >= minWordLen and word.len <= maxWordLen:
result.add newCall("cstring", newLit(word)) result.add newCall("cstring", newLit(word))
@ -213,9 +213,21 @@ const
englishWords = wordListArray("english_word_list.txt", englishWords = wordListArray("english_word_list.txt",
maxWords = wordListLen, maxWords = wordListLen,
maxWordLen = maxWordLen) maxWordLen = maxWordLen)
englishWordsDigest =
"AD90BF3BEB7B0EB7E5ACD74727DC0DA96E0A280A258354E7293FB7E211AC03DB".toDigest
proc checkEnglishWords(): bool =
if len(englishWords) != wordListLen:
false
else:
var ctx: sha256
ctx.init()
for item in englishWords:
ctx.update($item)
ctx.finish() == englishWordsDigest
static: static:
doAssert englishWords.len == wordListLen doAssert(checkEnglishWords(), "English words array is corrupted!")
func append*(path: KeyPath, pathNode: Natural): KeyPath = func append*(path: KeyPath, pathNode: Natural): KeyPath =
KeyPath(path.string & "/" & $pathNode) KeyPath(path.string & "/" & $pathNode)
@ -225,7 +237,8 @@ func validateKeyPath*(path: TaintedString): Result[KeyPath, cstring] =
var number: BiggestUint var number: BiggestUint
try: try:
for elem in path.string.split("/"): for elem in path.string.split("/"):
# TODO: doesn't "m" have to be the first character and is it the only place where it is valid? # TODO: doesn't "m" have to be the first character and is it the only
# place where it is valid?
if elem == "m": if elem == "m":
continue continue
# parseBiggestUInt can raise if overflow # parseBiggestUInt can raise if overflow
@ -274,8 +287,6 @@ proc generateMnemonic*(
entropyParam: openarray[byte] = @[]): Mnemonic = entropyParam: openarray[byte] = @[]): Mnemonic =
## Generates a valid BIP-0039 mnenomic: ## Generates a valid BIP-0039 mnenomic:
## https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic ## https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
doAssert words.len == wordListLen
var entropy: seq[byte] var entropy: seq[byte]
if entropyParam.len == 0: if entropyParam.len == 0:
setLen(entropy, 32) setLen(entropy, 32)