Fix #1855; Add support for wallet recovery passwords

This commit is contained in:
Zahary Karadjov 2020-10-19 22:02:48 +03:00 committed by zah
parent 63173ab2c1
commit f76679810b
6 changed files with 167 additions and 53 deletions

View File

@ -1137,8 +1137,8 @@ programMain:
of deposits:
case config.depositsCmd
of DepositsCmd.create:
var mnemonic: Mnemonic
defer: burnMem(mnemonic)
var seed: KeySeed
defer: burnMem(seed)
var walletPath: WalletPathPair
if config.existingWalletId.isSome:
@ -1154,7 +1154,7 @@ programMain:
var unlocked = unlockWalletInteractively(walletPath.wallet)
if unlocked.isOk:
swap(mnemonic, unlocked.get)
swap(seed, unlocked.get)
else:
# The failure will be reported in `unlockWalletInteractively`.
quit 1
@ -1164,7 +1164,7 @@ programMain:
fatal "Unable to create wallet", err = walletRes.error
quit 1
else:
swap(mnemonic, walletRes.get.mnemonic)
swap(seed, walletRes.get.seed)
walletPath = walletRes.get.walletPath
let vres = secureCreatePath(config.outValidatorsDir)
@ -1180,7 +1180,7 @@ programMain:
let deposits = generateDeposits(
config.runtimePreset,
rng[],
mnemonic,
seed,
walletPath.wallet.nextAccount,
config.totalDeposits,
config.outValidatorsDir,
@ -1238,7 +1238,7 @@ programMain:
if walletRes.isErr:
fatal "Unable to create wallet", err = walletRes.error
quit 1
burnMem(walletRes.get.mnemonic)
burnMem(walletRes.get.seed)
of WalletsCmd.list:
for kind, walletFile in walkDir(config.walletsDir):

View File

@ -176,6 +176,7 @@ proc main() {.async.} =
if cfg.cmd == StartUpCommand.generateSimulationDeposits:
let
mnemonic = generateMnemonic(rng[])
seed = getSeed(mnemonic, KeyStorePass.init "")
runtimePreset = getRuntimePresetForNetwork(cfg.eth2Network)
let vres = secureCreatePath(string cfg.outValidatorsDir)
@ -191,7 +192,7 @@ proc main() {.async.} =
let deposits = generateDeposits(
runtimePreset,
rng[],
mnemonic,
seed,
0, cfg.simulationDepositsCount,
string cfg.outValidatorsDir,
string cfg.outSecretsDir)

View File

@ -3,7 +3,7 @@ import
chronicles, chronos, web3, stint, json_serialization, zxcvbn,
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
spec/[datatypes, digest, crypto, keystore],
stew/[byteutils, io2], libp2p/crypto/crypto as lcrypto,
stew/io2, libp2p/crypto/crypto as lcrypto,
nimcrypto/utils as ncrutils,
conf, ssz/merkleization, network_metadata, filepath
@ -27,7 +27,7 @@ type
CreatedWallet* = object
walletPath*: WalletPathPair
mnemonic*: Mnemonic
seed*: KeySeed
const
minPasswordLen = 12
@ -143,7 +143,9 @@ proc checkSensitiveFilePermissions*(filePath: string): bool =
else:
true
proc keyboardCreatePassword(prompt: string, confirm: string): KsResult[string] =
proc keyboardCreatePassword(prompt: string,
confirm: string,
allowEmpty = false): KsResult[string] =
while true:
let password =
try:
@ -152,6 +154,9 @@ proc keyboardCreatePassword(prompt: string, confirm: string): KsResult[string] =
error "Could not read password from stdin"
return err("Could not read password from stdin")
if password.len == 0 and allowEmpty:
return ok("")
# We treat `password` as UTF-8 encoded string.
if validateUtf8(password) == -1:
if runeLen(password) < minPasswordLen:
@ -418,7 +423,7 @@ proc saveKeystore(rng: var BrHmacDrbgContext,
proc generateDeposits*(preset: RuntimePreset,
rng: var BrHmacDrbgContext,
mnemonic: Mnemonic,
seed: KeySeed,
firstValidatorIdx, totalNewValidators: int,
validatorsDir: string,
secretsDir: string): Result[seq[DepositData], KeystoreGenerationError] =
@ -426,23 +431,30 @@ proc generateDeposits*(preset: RuntimePreset,
notice "Generating deposits", totalNewValidators, validatorsDir, secretsDir
let withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
# TODO: Explain why we are using an empty password
var withdrawalKey = keyFromPath(mnemonic, KeystorePass.init "", withdrawalKeyPath)
defer: burnMem(withdrawalKey)
let withdrawalPubKey = withdrawalKey.toPubKey
# We'll reuse a single variable here to make the secret
# scrubbing (burnMem) easier to handle:
var baseKey = deriveMasterKey(seed)
defer: burnMem(baseKey)
baseKey = deriveChildKey(baseKey, baseKeyPath)
for i in 0 ..< totalNewValidators:
let keyStoreIdx = firstValidatorIdx + i
let signingKeyPath = withdrawalKeyPath.append keyStoreIdx
var signingKey = deriveChildKey(withdrawalKey, keyStoreIdx)
defer: burnMem(signingKey)
let signingPubKey = signingKey.toPubKey
let validatorIdx = firstValidatorIdx + i
# We'll reuse a single variable here to make the secret
# scrubbing (burnMem) easier to handle:
var derivedKey = baseKey
defer: burnMem(derivedKey)
derivedKey = deriveChildKey(derivedKey, validatorIdx)
derivedKey = deriveChildKey(derivedKey, 0) # This is witdrawal key
let withdrawalPubKey = derivedKey.toPubKey
derivedKey = deriveChildKey(derivedKey, 0) # This is the signing key
let signingPubKey = derivedKey.toPubKey
? saveKeystore(rng, validatorsDir, secretsDir,
signingKey, signingPubKey, signingKeyPath)
derivedKey, signingPubKey,
makeKeyPath(validatorIdx, signingKeyKind))
deposits.add preset.prepareDeposit(withdrawalPubKey, signingKey, signingPubKey)
deposits.add preset.prepareDeposit(withdrawalPubKey, derivedKey, signingPubKey)
ok deposits
@ -585,11 +597,11 @@ template ask(prompt: string): string =
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
config: BeaconNodeConf,
mnemonic: Mnemonic): Result[WalletPathPair, string] =
seed: KeySeed): Result[WalletPathPair, string] =
echoP "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 machine " &
"and you can change it at any time."
"and additional deposits, you'll be asked to enter a signing " &
"password. Please note that this password is local to the current " &
"machine and you can change it at any time."
echo ""
var password =
@ -629,7 +641,7 @@ proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
else:
none Natural
let wallet = createWallet(kdfPbkdf2, rng, mnemonic,
let wallet = createWallet(kdfPbkdf2, rng, seed,
name = name,
nextAccount = nextAccount,
password = KeystorePass.init password)
@ -732,8 +744,34 @@ proc createWalletInteractively*(
clearScreen()
let walletPath = ? pickPasswordAndSaveWallet(rng, config, mnemonic)
return ok CreatedWallet(walletPath: walletPath, mnemonic: mnemonic)
var mnenomicPassword = KeystorePass.init ""
defer: burnMem(mnenomicPassword)
echoP "The recovery of your wallet can be additionally protected by a" &
"recovery password. Since the seed phrase itself can be considered " &
"a password, setting such an additional password is optional. " &
"To ensure the strongest possible security, we recommend writing " &
"down your seed phrase and remembering your recovery password. " &
"If you don'n want to set a recovery password, just press ENTER."
var recoveryPassword = keyboardCreatePassword(
"Recovery password: ", "Confirm password: ", allowEmpty = true)
defer:
if recoveryPassword.isOk:
burnMem(recoveryPassword.get)
if recoveryPassword.isErr:
fatal "Failed to read password from stdin"
quit 1
var keystorePass = KeystorePass.init recoveryPassword.get
defer: burnMem(keystorePass)
var seed = getSeed(mnemonic, keystorePass)
defer: burnMem(seed)
let walletPath = ? pickPasswordAndSaveWallet(rng, config, seed)
return ok CreatedWallet(walletPath: walletPath, seed: seed)
proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
config: BeaconNodeConf) =
@ -756,21 +794,39 @@ proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
else:
echo "The entered mnemonic was not valid. Please try again."
discard pickPasswordAndSaveWallet(rng, config, validatedMnemonic)
echoP "If your seed phrase was protected with a recovery password, " &
"please enter it below. Please ENTER to attempt to restore " &
"the wallet without a recovery password."
proc unlockWalletInteractively*(wallet: Wallet): Result[Mnemonic, string] =
let prompt = "Please enter the password for unlocking the wallet: "
var recoveryPassword = keyboardCreatePassword(
"Recovery password: ", "Confirm password: ", allowEmpty = true)
defer:
if recoveryPassword.isOk:
burnMem(recoveryPassword.get)
if recoveryPassword.isErr:
fatal "Failed to read password from stdin"
quit 1
var keystorePass = KeystorePass.init recoveryPassword.get
defer: burnMem(keystorePass)
var seed = getSeed(validatedMnemonic, keystorePass)
defer: burnMem(seed)
discard pickPasswordAndSaveWallet(rng, config, seed)
proc unlockWalletInteractively*(wallet: Wallet): Result[KeySeed, string] =
echo "Please enter the password for unlocking the wallet"
let res = keyboardGetPassword[Mnemonic](prompt, 3,
proc (password: string): KsResult[Mnemonic] =
let res = keyboardGetPassword[KeySeed]("Password: ", 3,
proc (password: string): KsResult[KeySeed] =
var secret: seq[byte]
defer: burnMem(secret)
let status = decryptCryptoField(wallet.crypto, KeystorePass.init password, secret)
case status
of Success:
let mnemonic = Mnemonic(string.fromBytes(secret))
ok(mnemonic)
ok(KeySeed secret)
else:
# TODO Handle InvalidKeystore in a special way here
let failed = "Unlocking of the wallet failed. Please try again"

View File

@ -9,7 +9,7 @@
import
# Standard library
tables, random, strutils, os, typetraits,
tables, random, strutils, typetraits,
# Nimble packages
chronos, confutils/defs,

View File

@ -144,7 +144,6 @@ type
signingKey*: ValidatorPrivKey
withdrawalKey*: ValidatorPrivKey
SensitiveStrings = Mnemonic|KeySeed
SimpleHexEncodedTypes = ScryptSalt|ChecksumBytes|CipherBytes
const
@ -166,6 +165,7 @@ const
# https://eips.ethereum.org/EIPS/eip-2334
eth2KeyPurpose = 12381
eth2CoinType* = 3600
baseKeyPath* = [Natural eth2KeyPurpose, eth2CoinType]
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
wordListLen = 2048
@ -189,14 +189,15 @@ template `==`*(lhs, rhs: WalletName): bool =
template `$`*(x: WalletName): string =
string(x)
template burnMem*(m: var (SensitiveStrings|TaintedString)) =
# TODO: `burnMem` in nimcrypto could use distinctBase
# to make its usage less error-prone.
# TODO: `burnMem` in nimcrypto could use distinctBase
# to make its usage less error-prone.
template burnMem*(m: var (Mnemonic|TaintedString)) =
ncrutils.burnMem(string m)
template burnMem*(m: var KeySeed) =
ncrutils.burnMem(distinctBase m)
template burnMem*(m: var KeystorePass) =
# TODO: `burnMem` in nimcrypto could use distinctBase
# to make its usage less error-prone.
ncrutils.burnMem(m.str)
func longName*(wallet: Wallet): string =
@ -242,9 +243,6 @@ proc checkEnglishWords(): bool =
static:
doAssert(checkEnglishWords(), "English words array is corrupted!")
func append*(path: KeyPath, pathNode: Natural): KeyPath =
KeyPath(path.string & "/" & $pathNode)
func validateKeyPath*(path: TaintedString): Result[KeyPath, cstring] =
var digitCount: int
var number: BiggestUint
@ -398,6 +396,12 @@ proc deriveChildKey*(masterKey: ValidatorPrivKey,
path: KeyPath): ValidatorPrivKey =
result = masterKey
for idx in pathNodes(path):
result = deriveChildKey(result, idx)
proc deriveChildKey*(masterKey: ValidatorPrivKey,
path: openArray[Natural]): ValidatorPrivKey =
result = masterKey
for idx in path:
# TODO: we have exceptions in pathNodes unless `validateKeyPath`
# was called,
# and this iterator is used to derive secret keys
@ -721,7 +725,7 @@ proc createKeystore*(kdfKind: KdfKind,
proc createWallet*(kdfKind: KdfKind,
rng: var BrHmacDrbgContext,
mnemonic: Mnemonic,
seed: KeySeed,
name = WalletName "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[],
@ -730,10 +734,6 @@ proc createWallet*(kdfKind: KdfKind,
pretty = true): Wallet =
let
uuid = UUID $(uuidGenerate().expect("Random bytes should be available"))
# Please note that we are passing an empty password here because
# we want the wallet restoration procedure to depend only on the
# mnemonic (the user is asked to treat the mnemonic as a password).
seed = getSeed(mnemonic, KeystorePass.init "")
crypto = createCryptoField(kdfKind, rng, distinctBase seed,
password, salt, iv)
Wallet(

View File

@ -8,7 +8,7 @@
{.used.}
import
json, unittest,
json, unittest, typetraits,
stew/byteutils, blscurve, eth/keys, json_serialization,
libp2p/crypto/crypto as lcrypto,
nimcrypto/utils as ncrutils,
@ -292,3 +292,60 @@ suiteReport "KeyStorage testing suite":
check decryptKeystore(JsonString $badKdf,
KeystorePass.init password).iserr
suite "eth2.0-deposits-cli compatibility":
test "restoring mnemonic without password":
var mnemonic = Mnemonic "camera dad smile sail injury warfare grid kiwi report minute fold slot before stem firm wet vague shove version medal one alley vibrant mushroom"
let seed = getSeed(mnemonic, KeystorePass.init "")
check byteutils.toHex(distinctBase seed) == "60043d6e1efe0eea2ef1c8e7d4bb2d79cb27d3403e992b6058998c27c373cfb6fe047b11405360bb224803726fd6b0ee9e3335ae7d9032e6cb49baf08697cf2a"
let masterKey = deriveMasterKey(seed)
check masterKey.toHex == "54aea900840c22ee821ca4f67ba57392d7c3e3d4fc54a6343940c12404226eb7"
let
v1SK = deriveChildKey(masterKey, makeKeyPath(0, signingKeyKind))
v1WK = deriveChildKey(masterKey, makeKeyPath(0, withdrawalKeyKind))
v2SK = deriveChildKey(masterKey, makeKeyPath(1, signingKeyKind))
v2WK = deriveChildKey(masterKey, makeKeyPath(1, withdrawalKeyKind))
v3SK = deriveChildKey(masterKey, makeKeyPath(2, signingKeyKind))
v3WK = deriveChildKey(masterKey, makeKeyPath(2, withdrawalKeyKind))
check:
v1SK.toHex == "261610f7cb44fd17da74b1d0018db0bf311cfb0d30fd6bc7879d3db022a1ac7d"
v1WK.toHex == "0924b5928633a6712a392a8172bd0b3ce6b591491ed4b448d51b460d293258e1"
v2SK.toHex == "3ee523f969f9e0eed10ec62a4b816d94e28947fc1c55ba791555b83baef23b43"
v2WK.toHex == "4925c51f41cd275c70ec878a35a6640e69d1d9360f3dcf6400692a670bda27c2"
v3SK.toHex == "05935491479f8ad8887c4bf64e69fddf9c2d42848bb8a98170a5fe41e94c4122"
v3WK.toHex == "56b158b3b170e9c339b94b895afc28964a0b6d7a0809a39b558ca8b6688487cd"
test "restoring mnemonic with password":
var mnemonic = Mnemonic "swear umbrella lesson couch void gentle rocket valley distance match floor rocket flag solve muscle common modify target city youth pottery predict flip ghost"
let seed = getSeed(mnemonic, KeystorePass.init "abracadabra!@#$%^7890")
check byteutils.toHex(distinctBase seed) == "f129c3ac003a07e54974d8dbeb08d20c2343fc516e0e3704570c500a4b6ed98bad2e6fec6a3b9a88076c17feaa0d01163855578cb08bae53860d0ae2558cf03e"
let
masterKey = deriveMasterKey(seed)
v1SK = deriveChildKey(masterKey, makeKeyPath(0, signingKeyKind))
v1WK = deriveChildKey(masterKey, makeKeyPath(0, withdrawalKeyKind))
v2SK = deriveChildKey(masterKey, makeKeyPath(1, signingKeyKind))
v2WK = deriveChildKey(masterKey, makeKeyPath(1, withdrawalKeyKind))
v3SK = deriveChildKey(masterKey, makeKeyPath(2, signingKeyKind))
v3WK = deriveChildKey(masterKey, makeKeyPath(2, withdrawalKeyKind))
check:
v1SK.toHex == "16059302897bc6ecdb9cdac9bb27f34cc996e04b75143c73742aa5975bfaeae7"
v1WK.toHex == "1c28b8e41e5cb2f983780eabb77c927e804d1f7aaffcaaf5593538885a658e8a"
v2SK.toHex == "49a5fa9536ebb96253d420a4a9e9f054dc872d2a49884d46995b39b8147fd5e3"
v2WK.toHex == "70068f12a854370d18284884df62d3911af2f85d0be29cb071ec78c6ec564695"
v3SK.toHex == "1445cec3861d7cbf80e409d79aeee131622dcb0c815ff97ceab2515e14c41a1a"
v3WK.toHex == "1ccd5dce4c842bd3f65bbd59a382662e689fcf01ddc39aaaf2dcc7d073f11a93"