Fix #1855; Add support for wallet recovery passwords
This commit is contained in:
parent
63173ab2c1
commit
f76679810b
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import
|
||||
# Standard library
|
||||
tables, random, strutils, os, typetraits,
|
||||
tables, random, strutils, typetraits,
|
||||
|
||||
# Nimble packages
|
||||
chronos, confutils/defs,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
Loading…
Reference in New Issue