2018-12-19 12:58:53 +00:00
|
|
|
import
|
2020-09-29 16:49:09 +00:00
|
|
|
std/[os, strutils, terminal, wordwrap, unicode],
|
2020-08-26 06:42:26 +00:00
|
|
|
chronicles, chronos, web3, stint, json_serialization,
|
2020-07-08 12:36:03 +00:00
|
|
|
serialization, blscurve, eth/common/eth_types, eth/keys, confutils, bearssl,
|
2020-07-09 22:08:54 +00:00
|
|
|
spec/[datatypes, digest, crypto, keystore],
|
2020-08-26 06:42:26 +00:00
|
|
|
stew/[byteutils, io2], libp2p/crypto/crypto as lcrypto,
|
2020-08-24 16:06:41 +00:00
|
|
|
nimcrypto/utils as ncrutils,
|
2020-07-17 20:59:50 +00:00
|
|
|
conf, ssz/merkleization, network_metadata
|
2018-12-19 12:58:53 +00:00
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
export
|
|
|
|
keystore
|
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
{.push raises: [Defect].}
|
2020-10-06 11:38:18 +00:00
|
|
|
{.localPassC: "-fno-lto".} # no LTO for crypto
|
2019-07-12 14:24:11 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
const
|
|
|
|
keystoreFileName* = "keystore.json"
|
2020-08-24 16:06:41 +00:00
|
|
|
netKeystoreFileName* = "network_keystore.json"
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-03-24 11:13:07 +00:00
|
|
|
type
|
2020-08-21 19:36:42 +00:00
|
|
|
WalletPathPair* = object
|
|
|
|
wallet*: Wallet
|
|
|
|
path*: string
|
|
|
|
|
|
|
|
CreatedWallet* = object
|
|
|
|
walletPath*: WalletPathPair
|
2020-07-17 20:59:50 +00:00
|
|
|
mnemonic*: Mnemonic
|
2019-07-12 14:24:11 +00:00
|
|
|
|
2020-08-24 16:06:41 +00:00
|
|
|
const
|
|
|
|
minPasswordLen = 10
|
|
|
|
|
|
|
|
mostCommonPasswords = wordListArray(
|
|
|
|
currentSourcePath.parentDir /
|
|
|
|
"../vendor/nimbus-security-resources/passwords/10-million-password-list-top-100000.txt",
|
|
|
|
minWordLen = minPasswordLen)
|
|
|
|
|
|
|
|
template echo80(msg: string) =
|
|
|
|
echo wrapWords(msg, 80)
|
|
|
|
|
2020-08-26 06:42:26 +00:00
|
|
|
proc checkAndCreateDataDir*(dataDir: string): bool =
|
|
|
|
## Checks `conf.dataDir`.
|
|
|
|
## If folder exists, procedure will check it for access and
|
|
|
|
## permissions `0750 (rwxr-x---)`, if folder do not exists it will be created
|
|
|
|
## with permissions `0750 (rwxr-x---)`.
|
2020-08-27 13:24:30 +00:00
|
|
|
let amask = {AccessFlags.Read, AccessFlags.Write, AccessFlags.Execute}
|
2020-08-26 06:42:26 +00:00
|
|
|
when defined(posix):
|
|
|
|
if fileAccessible(dataDir, amask):
|
|
|
|
let gmask = {UserRead, UserWrite, UserExec, GroupRead, GroupExec}
|
|
|
|
let pmask = {OtherRead, OtherWrite, OtherExec, GroupWrite}
|
|
|
|
let pres = getPermissionsSet(dataDir)
|
|
|
|
if pres.isErr():
|
|
|
|
fatal "Could not check data folder permissions",
|
|
|
|
data_dir = dataDir, errorCode = $pres.error,
|
|
|
|
errorMsg = ioErrorMsg(pres.error)
|
2020-08-27 13:24:30 +00:00
|
|
|
false
|
|
|
|
else:
|
|
|
|
let insecurePermissions = pres.get() * pmask
|
|
|
|
if insecurePermissions != {}:
|
|
|
|
fatal "Data folder has insecure permissions",
|
|
|
|
data_dir = dataDir,
|
|
|
|
insecure_permissions = $insecurePermissions,
|
|
|
|
current_permissions = pres.get().toString(),
|
|
|
|
required_permissions = gmask.toString()
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
2020-08-26 06:42:26 +00:00
|
|
|
else:
|
|
|
|
let res = createPath(dataDir, 0o750)
|
|
|
|
if res.isErr():
|
|
|
|
fatal "Could not create data folder", data_dir = dataDir,
|
|
|
|
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
|
2020-08-27 13:24:30 +00:00
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
2020-08-26 06:42:26 +00:00
|
|
|
elif defined(windows):
|
2020-08-27 13:24:30 +00:00
|
|
|
if fileAccessible(dataDir, amask):
|
|
|
|
let res = createPath(dataDir, 0o750)
|
|
|
|
if res.isErr():
|
|
|
|
fatal "Could not create data folder", data_dir = dataDir,
|
|
|
|
errorMsg = ioErrorMsg(res.error), errorCode = $res.error
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
true
|
2020-08-26 06:42:26 +00:00
|
|
|
else:
|
|
|
|
fatal "Unsupported operation system"
|
|
|
|
return false
|
|
|
|
|
2020-09-30 11:47:42 +00:00
|
|
|
proc checkSensitiveFilePermissions*(filePath: string): bool =
|
2020-08-27 13:24:30 +00:00
|
|
|
## Check if ``filePath`` has only "(600) rw-------" permissions.
|
|
|
|
## Procedure returns ``false`` if permissions are different
|
|
|
|
when defined(windows):
|
|
|
|
# Windows do not support per-user/group/other permissions,
|
|
|
|
# skiping verification part.
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
let allowedMask = {UserRead, UserWrite}
|
|
|
|
let mask = {UserExec,
|
|
|
|
GroupRead, GroupWrite, GroupExec,
|
|
|
|
OtherRead, OtherWrite, OtherExec}
|
|
|
|
let pres = getPermissionsSet(filePath)
|
|
|
|
if pres.isErr():
|
|
|
|
error "Could not check file permissions",
|
|
|
|
key_path = filePath, errorCode = $pres.error,
|
|
|
|
errorMsg = ioErrorMsg(pres.error)
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
let insecurePermissions = pres.get() * mask
|
|
|
|
if insecurePermissions != {}:
|
|
|
|
error "File has insecure permissions",
|
|
|
|
key_path = filePath,
|
|
|
|
insecure_permissions = $insecurePermissions,
|
|
|
|
current_permissions = pres.get().toString(),
|
|
|
|
required_permissions = allowedMask.toString()
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
true
|
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
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")
|
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
proc loadKeystore(validatorsDir, secretsDir, keyName: string,
|
|
|
|
nonInteractive: bool): Option[ValidatorPrivKey] =
|
2020-06-01 19:48:20 +00:00
|
|
|
let
|
|
|
|
keystorePath = validatorsDir / keyName / keystoreFileName
|
2020-08-02 17:26:57 +00:00
|
|
|
keystore =
|
|
|
|
try: Json.loadFile(keystorePath, Keystore)
|
2020-06-01 19:48:20 +00:00
|
|
|
except IOError as err:
|
|
|
|
error "Failed to read keystore", err = err.msg, path = keystorePath
|
|
|
|
return
|
2020-08-02 17:26:57 +00:00
|
|
|
except SerializationError as err:
|
|
|
|
error "Invalid keystore", err = err.formatMsg(keystorePath)
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
let passphrasePath = secretsDir / keyName
|
2020-06-03 11:52:36 +00:00
|
|
|
if fileExists(passphrasePath):
|
2020-09-30 11:47:42 +00:00
|
|
|
if not(checkSensitiveFilePermissions(passphrasePath)):
|
2020-08-27 13:24:30 +00:00
|
|
|
error "Password file has insecure permissions", key_path = keyStorePath
|
|
|
|
return
|
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
let passphrase = KeystorePass.init:
|
2020-08-27 13:24:30 +00:00
|
|
|
try:
|
|
|
|
readFile(passphrasePath)
|
|
|
|
except IOError as err:
|
|
|
|
error "Failed to read passphrase file", err = err.msg,
|
|
|
|
path = passphrasePath
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
let res = decryptKeystore(keystore, passphrase)
|
2020-06-03 11:52:36 +00:00
|
|
|
if res.isOk:
|
|
|
|
return res.get.some
|
|
|
|
else:
|
|
|
|
error "Failed to decrypt keystore", keystorePath, passphrasePath
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
if nonInteractive:
|
2020-06-01 19:48:20 +00:00
|
|
|
error "Unable to load validator key store. Please ensure matching passphrase exists in the secrets dir",
|
2020-09-01 13:44:40 +00:00
|
|
|
keyName, validatorsDir, secretsDir = secretsDir
|
2020-06-01 19:48:20 +00:00
|
|
|
return
|
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let prompt = "Please enter passphrase for key \"" &
|
|
|
|
(validatorsDir / keyName) & "\": "
|
|
|
|
let res = keyboardGetPassword[ValidatorPrivKey](prompt, 3,
|
|
|
|
proc (password: string): KsResult[ValidatorPrivKey] =
|
2020-10-02 15:46:05 +00:00
|
|
|
let decrypted = decryptKeystore(keystore, KeystorePass.init password)
|
2020-09-29 16:49:09 +00:00
|
|
|
if decrypted.isErr():
|
|
|
|
error "Keystore decryption failed. Please try again", keystorePath
|
|
|
|
decrypted
|
|
|
|
)
|
2020-10-02 15:46:05 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
if res.isOk():
|
|
|
|
some(res.get())
|
|
|
|
else:
|
|
|
|
return
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-09-01 13:44:40 +00:00
|
|
|
iterator validatorKeysFromDirs*(validatorsDir, secretsDir: string): ValidatorPrivKey =
|
|
|
|
try:
|
|
|
|
for kind, file in walkDir(validatorsDir):
|
|
|
|
if kind == pcDir:
|
|
|
|
let keyName = splitFile(file).name
|
|
|
|
let key = loadKeystore(validatorsDir, secretsDir, keyName, true)
|
|
|
|
if key.isSome:
|
|
|
|
yield key.get
|
|
|
|
else:
|
|
|
|
quit 1
|
|
|
|
except OSError:
|
|
|
|
quit 1
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPrivKey =
|
|
|
|
for validatorKeyFile in conf.validators:
|
|
|
|
try:
|
|
|
|
yield validatorKeyFile.load
|
|
|
|
except CatchableError as err:
|
|
|
|
error "Failed to load validator private key",
|
|
|
|
file = validatorKeyFile.string, err = err.msg
|
|
|
|
quit 1
|
|
|
|
|
2020-06-03 11:52:36 +00:00
|
|
|
let validatorsDir = conf.validatorsDir
|
2020-06-01 19:48:20 +00:00
|
|
|
try:
|
|
|
|
for kind, file in walkDir(validatorsDir):
|
|
|
|
if kind == pcDir:
|
|
|
|
let keyName = splitFile(file).name
|
2020-09-01 13:44:40 +00:00
|
|
|
let key = loadKeystore(validatorsDir, conf.secretsDir, keyName, conf.nonInteractive)
|
2020-06-01 19:48:20 +00:00
|
|
|
if key.isSome:
|
|
|
|
yield key.get
|
|
|
|
else:
|
|
|
|
quit 1
|
|
|
|
except OSError as err:
|
|
|
|
error "Validator keystores directory not accessible",
|
|
|
|
path = validatorsDir, err = err.msg
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
type
|
2020-08-02 17:26:57 +00:00
|
|
|
KeystoreGenerationError = enum
|
2020-06-01 19:48:20 +00:00
|
|
|
RandomSourceDepleted,
|
2020-08-02 18:47:15 +00:00
|
|
|
FailedToCreateValidatorDir
|
|
|
|
FailedToCreateSecretsDir
|
2020-06-01 19:48:20 +00:00
|
|
|
FailedToCreateSecretFile
|
|
|
|
FailedToCreateKeystoreFile
|
|
|
|
|
2020-08-25 10:16:31 +00:00
|
|
|
proc loadNetKeystore*(keyStorePath: string,
|
|
|
|
insecurePwd: Option[string]): Option[lcrypto.PrivateKey] =
|
2020-08-24 16:06:41 +00:00
|
|
|
|
2020-09-30 11:47:42 +00:00
|
|
|
if not(checkSensitiveFilePermissions(keystorePath)):
|
2020-08-27 13:24:30 +00:00
|
|
|
error "Network keystorage file has insecure permissions",
|
|
|
|
key_path = keyStorePath
|
|
|
|
return
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
let keyStore =
|
|
|
|
try:
|
|
|
|
Json.loadFile(keystorePath, NetKeystore)
|
|
|
|
except IOError as err:
|
|
|
|
error "Failed to read network keystore", err = err.msg,
|
|
|
|
path = keystorePath
|
|
|
|
return
|
|
|
|
except SerializationError as err:
|
|
|
|
error "Invalid network keystore", err = err.formatMsg(keystorePath)
|
|
|
|
return
|
|
|
|
|
2020-08-25 10:16:31 +00:00
|
|
|
if insecurePwd.isSome():
|
|
|
|
warn "Using insecure password to unlock networking key"
|
2020-10-02 15:46:05 +00:00
|
|
|
let decrypted = decryptNetKeystore(keystore, KeystorePass.init insecurePwd.get)
|
2020-08-24 16:06:41 +00:00
|
|
|
if decrypted.isOk:
|
|
|
|
return some(decrypted.get())
|
|
|
|
else:
|
|
|
|
error "Network keystore decryption failed", key_store = keyStorePath
|
2020-08-25 10:16:31 +00:00
|
|
|
return
|
|
|
|
else:
|
2020-09-29 16:49:09 +00:00
|
|
|
let prompt = "Please enter passphrase to unlock networking key: "
|
|
|
|
let res = keyboardGetPassword[lcrypto.PrivateKey](prompt, 3,
|
|
|
|
proc (password: string): KsResult[lcrypto.PrivateKey] =
|
2020-10-02 15:46:05 +00:00
|
|
|
let decrypted = decryptNetKeystore(keystore, KeystorePass.init password)
|
2020-09-29 16:49:09 +00:00
|
|
|
if decrypted.isErr():
|
|
|
|
error "Keystore decryption failed. Please try again", keystorePath
|
|
|
|
decrypted
|
|
|
|
)
|
|
|
|
if res.isOk():
|
|
|
|
some(res.get())
|
|
|
|
else:
|
|
|
|
return
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
proc saveNetKeystore*(rng: var BrHmacDrbgContext, keyStorePath: string,
|
2020-08-25 10:16:31 +00:00
|
|
|
netKey: lcrypto.PrivateKey, insecurePwd: Option[string]
|
|
|
|
): Result[void, KeystoreGenerationError] =
|
2020-09-29 16:49:09 +00:00
|
|
|
let password =
|
|
|
|
if insecurePwd.isSome():
|
|
|
|
warn "Using insecure password to lock networking key",
|
|
|
|
key_path = keyStorePath
|
|
|
|
insecurePwd.get()
|
|
|
|
else:
|
2020-08-25 10:16:31 +00:00
|
|
|
let prompt = "Please enter NEW password to lock network key storage: "
|
2020-09-29 16:49:09 +00:00
|
|
|
let confirm = "Please confirm, network key storage password: "
|
|
|
|
let res = keyboardCreatePassword(prompt, confirm)
|
|
|
|
if res.isErr():
|
|
|
|
return err(FailedToCreateKeystoreFile)
|
|
|
|
res.get()
|
2020-08-24 16:06:41 +00:00
|
|
|
|
|
|
|
let keyStore = createNetKeystore(kdfScrypt, rng, netKey,
|
2020-10-02 15:46:05 +00:00
|
|
|
KeystorePass.init password)
|
2020-08-24 16:06:41 +00:00
|
|
|
var encodedStorage: string
|
|
|
|
try:
|
|
|
|
encodedStorage = Json.encode(keyStore)
|
|
|
|
except SerializationError:
|
2020-08-25 12:49:05 +00:00
|
|
|
error "Could not serialize network key storage", key_path = keyStorePath
|
2020-08-24 16:06:41 +00:00
|
|
|
return err(FailedToCreateKeystoreFile)
|
|
|
|
|
|
|
|
let res = writeFile(keyStorePath, encodedStorage, 0o600)
|
|
|
|
if res.isOk():
|
|
|
|
ok()
|
|
|
|
else:
|
2020-08-25 12:49:05 +00:00
|
|
|
error "Could not write to network key storage file", key_path = keyStorePath
|
2020-08-24 16:06:41 +00:00
|
|
|
err(FailedToCreateKeystoreFile)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc saveKeystore(rng: var BrHmacDrbgContext,
|
|
|
|
validatorsDir, secretsDir: string,
|
|
|
|
signingKey: ValidatorPrivKey, signingPubKey: ValidatorPubKey,
|
|
|
|
signingKeyPath: KeyPath): Result[void, KeystoreGenerationError] =
|
|
|
|
let
|
2020-08-06 18:14:44 +00:00
|
|
|
keyName = "0x" & $signingPubKey
|
2020-08-02 17:26:57 +00:00
|
|
|
validatorDir = validatorsDir / keyName
|
|
|
|
|
|
|
|
if not existsDir(validatorDir):
|
2020-10-02 15:46:05 +00:00
|
|
|
var password = KeystorePass.init ncrutils.toHex(getRandomBytes(rng, 32))
|
2020-08-02 17:26:57 +00:00
|
|
|
defer: burnMem(password)
|
|
|
|
|
|
|
|
let
|
|
|
|
keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
|
|
|
|
password, signingKeyPath)
|
|
|
|
keystoreFile = validatorDir / keystoreFileName
|
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
var encodedStorage: string
|
|
|
|
try:
|
|
|
|
encodedStorage = Json.encode(keyStore)
|
|
|
|
except SerializationError:
|
|
|
|
error "Could not serialize keystorage", key_path = keystoreFile
|
|
|
|
return err(FailedToCreateKeystoreFile)
|
|
|
|
|
|
|
|
let vres = createPath(validatorDir, 0o750)
|
|
|
|
if vres.isErr():
|
|
|
|
return err(FailedToCreateValidatorDir)
|
2020-08-02 18:47:15 +00:00
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
let sres = createPath(secretsDir, 0o750)
|
|
|
|
if sres.isErr():
|
|
|
|
return err(FailedToCreateSecretsDir)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
let swres = writeFile(secretsDir / keyName, password.str, 0o600)
|
2020-08-27 13:24:30 +00:00
|
|
|
if swres.isErr():
|
|
|
|
return err(FailedToCreateSecretFile)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
let kwres = writeFile(keystoreFile, encodedStorage, 0o600)
|
|
|
|
if kwres.isErr():
|
|
|
|
return err(FailedToCreateKeystoreFile)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
ok()
|
|
|
|
|
2020-07-07 23:02:14 +00:00
|
|
|
proc generateDeposits*(preset: RuntimePreset,
|
2020-07-08 12:36:03 +00:00
|
|
|
rng: var BrHmacDrbgContext,
|
2020-08-21 19:36:42 +00:00
|
|
|
mnemonic: Mnemonic,
|
|
|
|
firstValidatorIdx, totalNewValidators: int,
|
2020-06-01 19:48:20 +00:00
|
|
|
validatorsDir: string,
|
2020-08-02 17:26:57 +00:00
|
|
|
secretsDir: string): Result[seq[DepositData], KeystoreGenerationError] =
|
2020-07-17 20:59:50 +00:00
|
|
|
var deposits: seq[DepositData]
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-10-01 18:56:42 +00:00
|
|
|
notice "Generating deposits", totalNewValidators, validatorsDir, secretsDir
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
let withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
|
|
|
|
# TODO: Explain why we are using an empty password
|
2020-10-02 15:46:05 +00:00
|
|
|
var withdrawalKey = keyFromPath(mnemonic, KeystorePass.init "", withdrawalKeyPath)
|
2020-07-17 20:59:50 +00:00
|
|
|
defer: burnMem(withdrawalKey)
|
|
|
|
let withdrawalPubKey = withdrawalKey.toPubKey
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
for i in 0 ..< totalNewValidators:
|
|
|
|
let keyStoreIdx = firstValidatorIdx + i
|
2020-07-17 20:59:50 +00:00
|
|
|
let signingKeyPath = withdrawalKeyPath.append keyStoreIdx
|
|
|
|
var signingKey = deriveChildKey(withdrawalKey, keyStoreIdx)
|
|
|
|
defer: burnMem(signingKey)
|
|
|
|
let signingPubKey = signingKey.toPubKey
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
? saveKeystore(rng, validatorsDir, secretsDir,
|
|
|
|
signingKey, signingPubKey, signingKeyPath)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
deposits.add preset.prepareDeposit(withdrawalPubKey, signingKey, signingPubKey)
|
2020-04-15 07:59:47 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
ok deposits
|
2020-04-15 07:59:47 +00:00
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
|
2020-08-27 13:24:30 +00:00
|
|
|
let walletDir = splitFile(outWalletPath).dir
|
|
|
|
var encodedWallet: string
|
|
|
|
try:
|
|
|
|
encodedWallet = Json.encode(wallet, pretty = true)
|
|
|
|
except SerializationError:
|
|
|
|
return err("Could not serialize wallet")
|
|
|
|
let pres = createPath(walletDir, 0o750)
|
|
|
|
if pres.isErr():
|
|
|
|
return err("Could not create wallet directory [" & walletDir & "]")
|
|
|
|
let wres = writeFile(outWalletPath, encodedWallet, 0o600)
|
|
|
|
if wres.isErr():
|
|
|
|
return err("Could not write wallet to file [" & outWalletPath & "]")
|
2020-07-17 20:59:50 +00:00
|
|
|
ok()
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
proc saveWallet*(wallet: WalletPathPair): Result[void, string] =
|
|
|
|
saveWallet(wallet.wallet, wallet.path)
|
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
proc readPasswordInput(prompt: string, password: var TaintedString): bool =
|
2020-08-21 09:47:35 +00:00
|
|
|
try:
|
|
|
|
when defined(windows):
|
|
|
|
# readPasswordFromStdin() on Windows always returns `false`.
|
|
|
|
# https://github.com/nim-lang/Nim/issues/15207
|
|
|
|
discard readPasswordFromStdin(prompt, password)
|
|
|
|
true
|
|
|
|
else:
|
|
|
|
readPasswordFromStdin(prompt, password)
|
2020-09-08 11:32:43 +00:00
|
|
|
except IOError:
|
2020-08-21 09:47:35 +00:00
|
|
|
false
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
proc setStyleNoError(styles: set[Style]) =
|
|
|
|
when defined(windows):
|
|
|
|
try: stdout.setStyle(styles)
|
|
|
|
except: discard
|
|
|
|
else:
|
|
|
|
try: stdout.setStyle(styles)
|
|
|
|
except IOError, ValueError: discard
|
|
|
|
|
|
|
|
proc setForegroundColorNoError(color: ForegroundColor) =
|
|
|
|
when defined(windows):
|
|
|
|
try: stdout.setForegroundColor(color)
|
|
|
|
except: discard
|
|
|
|
else:
|
|
|
|
try: stdout.setForegroundColor(color)
|
|
|
|
except IOError, ValueError: discard
|
|
|
|
|
|
|
|
proc resetAttributesNoError() =
|
|
|
|
when defined(windows):
|
|
|
|
try: stdout.resetAttributes()
|
|
|
|
except: discard
|
|
|
|
else:
|
|
|
|
try: stdout.resetAttributes()
|
|
|
|
except IOError: discard
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc importKeystoresFromDir*(rng: var BrHmacDrbgContext,
|
|
|
|
importedDir, validatorsDir, secretsDir: string) =
|
|
|
|
var password: TaintedString
|
|
|
|
defer: burnMem(password)
|
|
|
|
|
|
|
|
try:
|
|
|
|
for file in walkDirRec(importedDir):
|
|
|
|
let ext = splitFile(file).ext
|
|
|
|
if toLowerAscii(ext) != ".json":
|
|
|
|
continue
|
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
let keystore =
|
|
|
|
try:
|
|
|
|
Json.loadFile(file, Keystore)
|
|
|
|
except SerializationError as e:
|
|
|
|
warn "Invalid keystore", err = e.formatMsg(file)
|
|
|
|
continue
|
|
|
|
except IOError as e:
|
|
|
|
warn "Failed to read keystore file", file, err = e.msg
|
|
|
|
continue
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
var firstDecryptionAttempt = true
|
|
|
|
|
|
|
|
while true:
|
2020-10-02 15:46:05 +00:00
|
|
|
var secret = decryptCryptoField(keystore.crypto, KeystorePass.init password)
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
if secret.len == 0:
|
|
|
|
if firstDecryptionAttempt:
|
|
|
|
try:
|
2020-08-02 18:47:15 +00:00
|
|
|
const msg = "Please enter the password for decrypting '$1' " &
|
|
|
|
"or press ENTER to skip importing this keystore"
|
|
|
|
echo msg % [file]
|
2020-08-02 17:26:57 +00:00
|
|
|
except ValueError:
|
|
|
|
raiseAssert "The format string above is correct"
|
|
|
|
else:
|
|
|
|
echo "The entered password was incorrect. Please try again."
|
|
|
|
firstDecryptionAttempt = false
|
|
|
|
|
|
|
|
if not readPasswordInput("Password: ", password):
|
|
|
|
echo "System error while entering password. Please try again."
|
|
|
|
|
|
|
|
if password.len == 0:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
let privKey = ValidatorPrivKey.fromRaw(secret)
|
|
|
|
if privKey.isOk:
|
|
|
|
let pubKey = privKey.value.toPubKey
|
|
|
|
let status = saveKeystore(rng, validatorsDir, secretsDir,
|
|
|
|
privKey.value, pubKey,
|
|
|
|
keystore.path)
|
|
|
|
if status.isOk:
|
2020-10-01 18:56:42 +00:00
|
|
|
notice "Keystore imported", file
|
2020-08-02 17:26:57 +00:00
|
|
|
else:
|
|
|
|
error "Failed to import keystore", file, err = status.error
|
|
|
|
else:
|
|
|
|
error "Imported keystore holds invalid key", file, err = privKey.error
|
|
|
|
break
|
|
|
|
except OSError:
|
|
|
|
fatal "Failed to access the imported deposits directory"
|
|
|
|
quit 1
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
template ask(prompt: string): string =
|
2020-07-14 19:00:35 +00:00
|
|
|
try:
|
2020-08-21 19:36:42 +00:00
|
|
|
stdout.write prompt, ": "
|
|
|
|
stdin.readLine()
|
|
|
|
except IOError:
|
|
|
|
return err "failure to read data from stdin"
|
|
|
|
|
|
|
|
proc pickPasswordAndSaveWallet(rng: var BrHmacDrbgContext,
|
|
|
|
config: BeaconNodeConf,
|
|
|
|
mnemonic: Mnemonic): Result[WalletPathPair, string] =
|
2020-07-14 19:00:35 +00:00
|
|
|
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 ""
|
|
|
|
|
2020-10-05 15:27:05 +00:00
|
|
|
var password =
|
2020-09-29 16:49:09 +00:00
|
|
|
block:
|
|
|
|
let prompt = "Please enter a password: "
|
|
|
|
let confirm = "Please repeat the password: "
|
|
|
|
let res = keyboardCreatePassword(prompt, confirm)
|
|
|
|
if res.isErr():
|
|
|
|
return err($res.error)
|
|
|
|
res.get()
|
2020-10-01 19:18:56 +00:00
|
|
|
defer: burnMem(password)
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
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."
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
while true:
|
|
|
|
var enteredName = ask "Wallet name"
|
|
|
|
if enteredName.len > 0:
|
|
|
|
name =
|
2020-07-14 19:00:35 +00:00
|
|
|
try:
|
2020-09-29 16:49:09 +00:00
|
|
|
WalletName.parseCmdArg(enteredName)
|
|
|
|
except CatchableError as err:
|
|
|
|
echo err.msg & ". Please try again."
|
|
|
|
continue
|
|
|
|
break
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let nextAccount =
|
|
|
|
if config.cmd == wallets and config.walletsCmd == WalletsCmd.restore:
|
2020-08-21 19:36:42 +00:00
|
|
|
config.restoredDepositsCount
|
|
|
|
else:
|
|
|
|
none Natural
|
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let wallet = createWallet(kdfPbkdf2, rng, mnemonic,
|
|
|
|
name = name,
|
|
|
|
nextAccount = nextAccount,
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init password)
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let outWalletFileFlag = config.outWalletFile
|
|
|
|
let outWalletFile =
|
|
|
|
if outWalletFileFlag.isSome:
|
2020-07-17 20:59:50 +00:00
|
|
|
string outWalletFileFlag.get
|
|
|
|
else:
|
2020-10-01 19:18:56 +00:00
|
|
|
config.walletsDir / addFileExt(string wallet.name, "json")
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let status = saveWallet(wallet, outWalletFile)
|
|
|
|
if status.isErr:
|
|
|
|
return err("failure to create wallet file due to " & status.error)
|
|
|
|
|
2020-10-01 19:18:56 +00:00
|
|
|
echo "\nWallet file successfully written to \"", outWalletFile, "\""
|
2020-09-29 16:49:09 +00:00
|
|
|
return ok WalletPathPair(wallet: wallet, path: outWalletFile)
|
2020-07-14 19:00:35 +00:00
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
proc createWalletInteractively*(
|
|
|
|
rng: var BrHmacDrbgContext,
|
|
|
|
config: BeaconNodeConf): Result[CreatedWallet, string] =
|
|
|
|
|
|
|
|
if config.nonInteractive:
|
|
|
|
return err "not running in interactive mode"
|
|
|
|
|
|
|
|
var mnemonic = generateMnemonic(rng)
|
|
|
|
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:
|
|
|
|
echo ""
|
|
|
|
setStyleNoError({styleBright})
|
|
|
|
setForegroundColorNoError fgCyan
|
|
|
|
echo80 $mnemonic
|
|
|
|
resetAttributesNoError()
|
|
|
|
echo ""
|
|
|
|
except IOError, ValueError:
|
|
|
|
return err "failure 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 " &
|
2020-09-28 18:43:09 +00:00
|
|
|
"can be used to withdraw funds from your wallet."
|
2020-08-21 19:36:42 +00:00
|
|
|
|
|
|
|
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 "aborted wallet creation"
|
|
|
|
elif answer != "yes":
|
|
|
|
echo "To continue, please type 'yes' (without the quotes) or press enter to quit"
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
|
|
|
|
let walletPath = ? pickPasswordAndSaveWallet(rng, config, mnemonic)
|
|
|
|
return ok CreatedWallet(walletPath: walletPath, mnemonic: mnemonic)
|
|
|
|
|
|
|
|
proc restoreWalletInteractively*(rng: var BrHmacDrbgContext,
|
|
|
|
config: BeaconNodeConf) =
|
|
|
|
var
|
|
|
|
enteredMnemonic: TaintedString
|
|
|
|
validatedMnemonic: Mnemonic
|
|
|
|
|
|
|
|
defer:
|
|
|
|
burnMem enteredMnemonic
|
|
|
|
burnMem validatedMnemonic
|
|
|
|
|
|
|
|
echo "To restore your wallet, please enter your backed-up seed phrase."
|
|
|
|
while true:
|
2020-09-29 16:49:09 +00:00
|
|
|
if not readPasswordInput("Seedphrase: ", enteredMnemonic):
|
2020-08-21 19:36:42 +00:00
|
|
|
fatal "failure to read password from stdin"
|
|
|
|
quit 1
|
|
|
|
|
|
|
|
if validateMnemonic(enteredMnemonic, validatedMnemonic):
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
echo "The entered mnemonic was not valid. Please try again."
|
|
|
|
|
|
|
|
discard pickPasswordAndSaveWallet(rng, config, validatedMnemonic)
|
|
|
|
|
|
|
|
proc unlockWalletInteractively*(wallet: Wallet): Result[Mnemonic, string] =
|
2020-09-29 16:49:09 +00:00
|
|
|
let prompt = "Please enter the password for unlocking the wallet: "
|
2020-07-17 20:59:50 +00:00
|
|
|
echo "Please enter the password for unlocking the wallet"
|
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
let res = keyboardGetPassword[Mnemonic](prompt, 3,
|
|
|
|
proc (password: string): KsResult[Mnemonic] =
|
2020-10-02 15:46:05 +00:00
|
|
|
var secret = decryptCryptoField(wallet.crypto, KeystorePass.init password)
|
2020-09-29 16:49:09 +00:00
|
|
|
if len(secret) > 0:
|
|
|
|
let mnemonic = Mnemonic(string.fromBytes(secret))
|
|
|
|
burnMem(secret)
|
|
|
|
ok(mnemonic)
|
2020-07-17 20:59:50 +00:00
|
|
|
else:
|
2020-09-29 16:49:09 +00:00
|
|
|
let failed = "Unlocking of the wallet failed. Please try again"
|
|
|
|
echo failed
|
|
|
|
err(failed)
|
|
|
|
)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-09-29 16:49:09 +00:00
|
|
|
if res.isOk():
|
|
|
|
ok(res.get())
|
|
|
|
else:
|
|
|
|
err "Unlocking of the wallet failed."
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-10-02 13:38:32 +00:00
|
|
|
proc loadWallet*(fileName: string): Result[Wallet, string] =
|
|
|
|
try:
|
|
|
|
ok Json.loadFile(fileName, Wallet)
|
|
|
|
except SerializationError as err:
|
|
|
|
err "Invalid wallet syntax: " & err.formatMsg(fileName)
|
|
|
|
except IOError as err:
|
|
|
|
err "Error accessing wallet file \"" & fileName & "\": " & err.msg
|
|
|
|
|
2020-08-27 13:24:30 +00:00
|
|
|
proc findWallet*(config: BeaconNodeConf,
|
2020-10-02 13:38:32 +00:00
|
|
|
name: WalletName): Result[Option[WalletPathPair], string] =
|
2020-07-17 20:59:50 +00:00
|
|
|
var walletFiles = newSeq[string]()
|
|
|
|
try:
|
|
|
|
for kind, walletFile in walkDir(config.walletsDir):
|
|
|
|
if kind != pcFile: continue
|
2020-08-13 11:32:10 +00:00
|
|
|
let walletId = splitFile(walletFile).name
|
|
|
|
if cmpIgnoreCase(walletId, name.string) == 0:
|
2020-08-21 19:36:42 +00:00
|
|
|
let wallet = ? loadWallet(walletFile)
|
2020-10-02 13:38:32 +00:00
|
|
|
return ok some WalletPathPair(wallet: wallet, path: walletFile)
|
2020-08-13 11:32:10 +00:00
|
|
|
walletFiles.add walletFile
|
2020-10-02 13:38:32 +00:00
|
|
|
except OSError as err:
|
|
|
|
return err("Error accessing the wallets directory \"" &
|
|
|
|
config.walletsDir & "\": " & err.msg)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
for walletFile in walletFiles:
|
2020-08-21 19:36:42 +00:00
|
|
|
let wallet = ? loadWallet(walletFile)
|
2020-10-01 19:18:56 +00:00
|
|
|
if cmpIgnoreCase(wallet.name.string, name.string) == 0 or
|
|
|
|
cmpIgnoreCase(wallet.uuid.string, name.string) == 0:
|
2020-10-02 13:38:32 +00:00
|
|
|
return ok some WalletPathPair(wallet: wallet, path: walletFile)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
2020-10-02 13:38:32 +00:00
|
|
|
return ok none(WalletPathPair)
|
2020-07-17 20:59:50 +00:00
|
|
|
|
|
|
|
type
|
|
|
|
# This is not particularly well-standardized yet.
|
|
|
|
# Some relevant code for generating (1) and validating (2) the data can be found below:
|
|
|
|
# 1) https://github.com/ethereum/eth2.0-deposit-cli/blob/dev/eth2deposit/credentials.py
|
|
|
|
# 2) https://github.com/ethereum/eth2.0-deposit/blob/dev/src/pages/UploadValidator/validateDepositKey.ts
|
|
|
|
LaunchPadDeposit* = object
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
withdrawal_credentials*: Eth2Digest
|
|
|
|
amount*: Gwei
|
|
|
|
signature*: ValidatorSig
|
|
|
|
deposit_message_root*: Eth2Digest
|
|
|
|
deposit_data_root*: Eth2Digest
|
|
|
|
fork_version*: Version
|
|
|
|
|
|
|
|
func init*(T: type LaunchPadDeposit,
|
|
|
|
preset: RuntimePreset, d: DepositData): T =
|
|
|
|
T(pubkey: d.pubkey,
|
|
|
|
withdrawal_credentials: d.withdrawal_credentials,
|
|
|
|
amount: d.amount,
|
|
|
|
signature: d.signature,
|
|
|
|
deposit_message_root: hash_tree_root(d as DepositMessage),
|
|
|
|
deposit_data_root: hash_tree_root(d),
|
|
|
|
fork_version: preset.GENESIS_FORK_VERSION)
|
|
|
|
|
|
|
|
func `as`*(copied: LaunchPadDeposit, T: type DepositData): T =
|
|
|
|
T(pubkey: copied.pubkey,
|
|
|
|
withdrawal_credentials: copied.withdrawal_credentials,
|
|
|
|
amount: copied.amount,
|
|
|
|
signature: copied.signature)
|