mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-02-19 17:58:23 +00:00
Add 'deposits import' command; Switch to NJS when loading the keystores and improve the data validation
This commit is contained in:
parent
d58668157a
commit
c293254ded
@ -1249,6 +1249,12 @@ programMain:
|
|||||||
error "Failed to create launchpad deposit data file", err = err.msg
|
error "Failed to create launchpad deposit data file", err = err.msg
|
||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
|
of DepositsCmd.`import`:
|
||||||
|
importKeystoresFromDir(
|
||||||
|
rng[],
|
||||||
|
config.importedDepositsDir.string,
|
||||||
|
config.validatorsDir, config.secretsDir)
|
||||||
|
|
||||||
of DepositsCmd.status:
|
of DepositsCmd.status:
|
||||||
echo "The status command is not implemented yet"
|
echo "The status command is not implemented yet"
|
||||||
quit 1
|
quit 1
|
||||||
|
@ -26,8 +26,9 @@ type
|
|||||||
list = "Lists details about all wallets"
|
list = "Lists details about all wallets"
|
||||||
|
|
||||||
DepositsCmd* {.pure.} = enum
|
DepositsCmd* {.pure.} = enum
|
||||||
create = "Creates validator keystores and deposits"
|
create = "Creates validator keystores and deposits"
|
||||||
status = "Displays status information about all deposits"
|
`import` = "Imports password-protected keystores interactively"
|
||||||
|
status = "Displays status information about all deposits"
|
||||||
|
|
||||||
VCStartUpCmd* = enum
|
VCStartUpCmd* = enum
|
||||||
VCNoCommand
|
VCNoCommand
|
||||||
@ -58,6 +59,10 @@ type
|
|||||||
desc: "A directory containing validator keystores"
|
desc: "A directory containing validator keystores"
|
||||||
name: "validators-dir" }: Option[InputDir]
|
name: "validators-dir" }: Option[InputDir]
|
||||||
|
|
||||||
|
secretsDirFlag* {.
|
||||||
|
desc: "A directory containing validator keystore passwords"
|
||||||
|
name: "secrets-dir" }: Option[InputDir]
|
||||||
|
|
||||||
walletsDirFlag* {.
|
walletsDirFlag* {.
|
||||||
desc: "A directory containing wallet files"
|
desc: "A directory containing wallet files"
|
||||||
name: "wallets-dir" }: Option[InputDir]
|
name: "wallets-dir" }: Option[InputDir]
|
||||||
@ -125,10 +130,6 @@ type
|
|||||||
abbr: "v"
|
abbr: "v"
|
||||||
name: "validator" }: seq[ValidatorKeyPath]
|
name: "validator" }: seq[ValidatorKeyPath]
|
||||||
|
|
||||||
secretsDirFlag* {.
|
|
||||||
desc: "A directory containing validator keystore passwords"
|
|
||||||
name: "secrets-dir" }: Option[InputDir]
|
|
||||||
|
|
||||||
stateSnapshot* {.
|
stateSnapshot* {.
|
||||||
desc: "SSZ file specifying a recent state snapshot"
|
desc: "SSZ file specifying a recent state snapshot"
|
||||||
abbr: "s"
|
abbr: "s"
|
||||||
@ -317,12 +318,17 @@ type
|
|||||||
|
|
||||||
newWalletNameFlag* {.
|
newWalletNameFlag* {.
|
||||||
desc: "An easy-to-remember name for the wallet of your choice"
|
desc: "An easy-to-remember name for the wallet of your choice"
|
||||||
name: "new-wallet-name"}: Option[WalletName]
|
name: "new-wallet-name" }: Option[WalletName]
|
||||||
|
|
||||||
newWalletFileFlag* {.
|
newWalletFileFlag* {.
|
||||||
desc: "Output wallet file"
|
desc: "Output wallet file"
|
||||||
name: "new-wallet-file" }: Option[OutFile]
|
name: "new-wallet-file" }: Option[OutFile]
|
||||||
|
|
||||||
|
of DepositsCmd.`import`:
|
||||||
|
importedDepositsDir* {.
|
||||||
|
argument
|
||||||
|
desc: "A directory with keystores to import" }: InputDir
|
||||||
|
|
||||||
of DepositsCmd.status:
|
of DepositsCmd.status:
|
||||||
discard
|
discard
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import
|
import
|
||||||
std/[os, json, strutils, terminal, wordwrap],
|
std/[os, strutils, terminal, wordwrap],
|
||||||
stew/byteutils, chronicles, chronos, web3, stint, json_serialization,
|
stew/byteutils, 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],
|
||||||
@ -18,26 +18,29 @@ type
|
|||||||
mnemonic*: Mnemonic
|
mnemonic*: Mnemonic
|
||||||
nextAccount*: Natural
|
nextAccount*: Natural
|
||||||
|
|
||||||
proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf,
|
proc loadKeystore(conf: BeaconNodeConf|ValidatorClientConf,
|
||||||
validatorsDir, keyName: string): Option[ValidatorPrivKey] =
|
validatorsDir, keyName: string): Option[ValidatorPrivKey] =
|
||||||
let
|
let
|
||||||
keystorePath = validatorsDir / keyName / keystoreFileName
|
keystorePath = validatorsDir / keyName / keystoreFileName
|
||||||
keystoreContents = KeyStoreContent:
|
keystore =
|
||||||
try: readFile(keystorePath)
|
try: Json.loadFile(keystorePath, Keystore)
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
error "Failed to read keystore", err = err.msg, path = keystorePath
|
error "Failed to read keystore", err = err.msg, path = keystorePath
|
||||||
return
|
return
|
||||||
|
except SerializationError as err:
|
||||||
|
error "Invalid keystore", err = err.formatMsg(keystorePath)
|
||||||
|
return
|
||||||
|
|
||||||
let passphrasePath = conf.secretsDir / keyName
|
let passphrasePath = conf.secretsDir / keyName
|
||||||
if fileExists(passphrasePath):
|
if fileExists(passphrasePath):
|
||||||
let
|
let
|
||||||
passphrase = KeyStorePass:
|
passphrase = KeystorePass:
|
||||||
try: readFile(passphrasePath)
|
try: readFile(passphrasePath)
|
||||||
except IOError as err:
|
except IOError as err:
|
||||||
error "Failed to read passphrase file", err = err.msg, path = passphrasePath
|
error "Failed to read passphrase file", err = err.msg, path = passphrasePath
|
||||||
return
|
return
|
||||||
|
|
||||||
let res = decryptKeystore(keystoreContents, passphrase)
|
let res = decryptKeystore(keystore, passphrase)
|
||||||
if res.isOk:
|
if res.isOk:
|
||||||
return res.get.some
|
return res.get.some
|
||||||
else:
|
else:
|
||||||
@ -52,13 +55,13 @@ proc loadKeyStore(conf: BeaconNodeConf|ValidatorClientConf,
|
|||||||
var remainingAttempts = 3
|
var remainingAttempts = 3
|
||||||
var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\"\n"
|
var prompt = "Please enter passphrase for key \"" & validatorsDir/keyName & "\"\n"
|
||||||
while remainingAttempts > 0:
|
while remainingAttempts > 0:
|
||||||
let passphrase = KeyStorePass:
|
let passphrase = KeystorePass:
|
||||||
try: readPasswordFromStdin(prompt)
|
try: readPasswordFromStdin(prompt)
|
||||||
except IOError:
|
except IOError:
|
||||||
error "STDIN not readable. Cannot obtain KeyStore password"
|
error "STDIN not readable. Cannot obtain Keystore password"
|
||||||
return
|
return
|
||||||
|
|
||||||
let decrypted = decryptKeystore(keystoreContents, passphrase)
|
let decrypted = decryptKeystore(keystore, passphrase)
|
||||||
if decrypted.isOk:
|
if decrypted.isOk:
|
||||||
return decrypted.get.some
|
return decrypted.get.some
|
||||||
else:
|
else:
|
||||||
@ -79,7 +82,7 @@ iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPriv
|
|||||||
for kind, file in walkDir(validatorsDir):
|
for kind, file in walkDir(validatorsDir):
|
||||||
if kind == pcDir:
|
if kind == pcDir:
|
||||||
let keyName = splitFile(file).name
|
let keyName = splitFile(file).name
|
||||||
let key = loadKeyStore(conf, validatorsDir, keyName)
|
let key = loadKeystore(conf, validatorsDir, keyName)
|
||||||
if key.isSome:
|
if key.isSome:
|
||||||
yield key.get
|
yield key.get
|
||||||
else:
|
else:
|
||||||
@ -90,55 +93,66 @@ iterator validatorKeys*(conf: BeaconNodeConf|ValidatorClientConf): ValidatorPriv
|
|||||||
quit 1
|
quit 1
|
||||||
|
|
||||||
type
|
type
|
||||||
GenerateDepositsError = enum
|
KeystoreGenerationError = enum
|
||||||
RandomSourceDepleted,
|
RandomSourceDepleted,
|
||||||
FailedToCreateValidatoDir
|
FailedToCreateValidatoDir
|
||||||
FailedToCreateSecretFile
|
FailedToCreateSecretFile
|
||||||
FailedToCreateKeystoreFile
|
FailedToCreateKeystoreFile
|
||||||
|
|
||||||
proc generateDeposits*(preset: RuntimePreset,
|
proc saveKeystore(rng: var BrHmacDrbgContext,
|
||||||
rng: var BrHmacDrbgContext,
|
validatorsDir, secretsDir: string,
|
||||||
walletData: WalletDataForDeposits,
|
signingKey: ValidatorPrivKey, signingPubKey: ValidatorPubKey,
|
||||||
totalValidators: int,
|
signingKeyPath: KeyPath): Result[void, KeystoreGenerationError] =
|
||||||
validatorsDir: string,
|
let
|
||||||
secretsDir: string): Result[seq[DepositData], GenerateDepositsError] =
|
keyName = $signingPubKey
|
||||||
var deposits: seq[DepositData]
|
validatorDir = validatorsDir / keyName
|
||||||
|
|
||||||
info "Generating deposits", totalValidators, validatorsDir, secretsDir
|
if not existsDir(validatorDir):
|
||||||
|
var password = KeystorePass getRandomBytes(rng, 32).toHex
|
||||||
|
defer: burnMem(password)
|
||||||
|
|
||||||
let withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
|
|
||||||
# TODO: Explain why we are using an empty password
|
|
||||||
var withdrawalKey = keyFromPath(walletData.mnemonic, KeyStorePass"", withdrawalKeyPath)
|
|
||||||
defer: burnMem(withdrawalKey)
|
|
||||||
let withdrawalPubKey = withdrawalKey.toPubKey
|
|
||||||
|
|
||||||
for i in 0 ..< totalValidators:
|
|
||||||
let keyStoreIdx = walletData.nextAccount + i
|
|
||||||
let password = KeyStorePass getRandomBytes(rng, 32).toHex
|
|
||||||
|
|
||||||
let signingKeyPath = withdrawalKeyPath.append keyStoreIdx
|
|
||||||
var signingKey = deriveChildKey(withdrawalKey, keyStoreIdx)
|
|
||||||
defer: burnMem(signingKey)
|
|
||||||
let signingPubKey = signingKey.toPubKey
|
|
||||||
|
|
||||||
let keyStore = encryptKeystore(KdfPbkdf2, rng, signingKey,
|
|
||||||
password, signingKeyPath)
|
|
||||||
let
|
let
|
||||||
keyName = $signingPubKey
|
keyStore = createKeystore(kdfPbkdf2, rng, signingKey,
|
||||||
validatorDir = validatorsDir / keyName
|
password, signingKeyPath)
|
||||||
keystoreFile = validatorDir / keystoreFileName
|
keystoreFile = validatorDir / keystoreFileName
|
||||||
|
|
||||||
if existsDir(validatorDir):
|
|
||||||
continue
|
|
||||||
|
|
||||||
try: createDir validatorDir
|
try: createDir validatorDir
|
||||||
except OSError, IOError: return err FailedToCreateValidatoDir
|
except OSError, IOError: return err FailedToCreateValidatoDir
|
||||||
|
|
||||||
try: writeFile(secretsDir / keyName, password.string)
|
try: writeFile(secretsDir / keyName, password.string)
|
||||||
except IOError: return err FailedToCreateSecretFile
|
except IOError: return err FailedToCreateSecretFile
|
||||||
|
|
||||||
try: writeFile(keystoreFile, keyStore.string)
|
try: Json.saveFile(keystoreFile, keyStore)
|
||||||
except IOError: return err FailedToCreateKeystoreFile
|
except IOError, SerializationError:
|
||||||
|
return err FailedToCreateKeystoreFile
|
||||||
|
|
||||||
|
ok()
|
||||||
|
|
||||||
|
proc generateDeposits*(preset: RuntimePreset,
|
||||||
|
rng: var BrHmacDrbgContext,
|
||||||
|
walletData: WalletDataForDeposits,
|
||||||
|
totalValidators: int,
|
||||||
|
validatorsDir: string,
|
||||||
|
secretsDir: string): Result[seq[DepositData], KeystoreGenerationError] =
|
||||||
|
var deposits: seq[DepositData]
|
||||||
|
|
||||||
|
info "Generating deposits", totalValidators, validatorsDir, secretsDir
|
||||||
|
|
||||||
|
let withdrawalKeyPath = makeKeyPath(0, withdrawalKeyKind)
|
||||||
|
# TODO: Explain why we are using an empty password
|
||||||
|
var withdrawalKey = keyFromPath(walletData.mnemonic, KeystorePass"", withdrawalKeyPath)
|
||||||
|
defer: burnMem(withdrawalKey)
|
||||||
|
let withdrawalPubKey = withdrawalKey.toPubKey
|
||||||
|
|
||||||
|
for i in 0 ..< totalValidators:
|
||||||
|
let keyStoreIdx = walletData.nextAccount + i
|
||||||
|
let signingKeyPath = withdrawalKeyPath.append keyStoreIdx
|
||||||
|
var signingKey = deriveChildKey(withdrawalKey, keyStoreIdx)
|
||||||
|
defer: burnMem(signingKey)
|
||||||
|
let signingPubKey = signingKey.toPubKey
|
||||||
|
|
||||||
|
? saveKeystore(rng, validatorsDir, secretsDir,
|
||||||
|
signingKey, signingPubKey, signingKeyPath)
|
||||||
|
|
||||||
deposits.add preset.prepareDeposit(withdrawalPubKey, signingKey, signingPubKey)
|
deposits.add preset.prepareDeposit(withdrawalPubKey, signingKey, signingPubKey)
|
||||||
|
|
||||||
@ -153,18 +167,18 @@ const
|
|||||||
minWordLength = minPasswordLen)
|
minWordLength = minPasswordLen)
|
||||||
|
|
||||||
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
|
proc saveWallet*(wallet: Wallet, outWalletPath: string): Result[void, string] =
|
||||||
let
|
|
||||||
uuid = wallet.uuid
|
|
||||||
walletContent = WalletContent Json.encode(wallet, pretty = true)
|
|
||||||
|
|
||||||
try: createDir splitFile(outWalletPath).dir
|
try: createDir splitFile(outWalletPath).dir
|
||||||
except OSError, IOError:
|
except OSError, IOError:
|
||||||
let e = getCurrentException()
|
let e = getCurrentException()
|
||||||
return err("failure to create wallet directory: " & e.msg)
|
return err("failure to create wallet directory: " & e.msg)
|
||||||
|
|
||||||
try: writeFile(outWalletPath, string walletContent)
|
try: Json.saveFile(outWalletPath, wallet, pretty = true)
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return err("failure to write file: " & e.msg)
|
return err("failure to write file: " & e.msg)
|
||||||
|
except SerializationError as e:
|
||||||
|
# TODO: Saving a wallet should not produce SerializationErrors.
|
||||||
|
# Investigate the source of this exception.
|
||||||
|
return err("failure to serialize wallet: " & e.formatMsg("wallet"))
|
||||||
|
|
||||||
ok()
|
ok()
|
||||||
|
|
||||||
@ -196,6 +210,65 @@ proc resetAttributesNoError() =
|
|||||||
try: stdout.resetAttributes()
|
try: stdout.resetAttributes()
|
||||||
except IOError: discard
|
except IOError: discard
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
var firstDecryptionAttempt = true
|
||||||
|
|
||||||
|
while true:
|
||||||
|
var secret = decryptCryptoField(keystore.crypto, KeystorePass password)
|
||||||
|
|
||||||
|
if secret.len == 0:
|
||||||
|
if firstDecryptionAttempt:
|
||||||
|
try:
|
||||||
|
echo "Please enter the password for decrypting '$1' " &
|
||||||
|
"or press ENTER to skip importing this keystore" % [file]
|
||||||
|
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:
|
||||||
|
info "Keystore imported", file
|
||||||
|
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
|
||||||
|
|
||||||
proc createWalletInteractively*(
|
proc createWalletInteractively*(
|
||||||
rng: var BrHmacDrbgContext,
|
rng: var BrHmacDrbgContext,
|
||||||
conf: BeaconNodeConf): Result[WalletDataForDeposits, string] =
|
conf: BeaconNodeConf): Result[WalletDataForDeposits, string] =
|
||||||
@ -310,9 +383,8 @@ proc createWalletInteractively*(
|
|||||||
continue
|
continue
|
||||||
break
|
break
|
||||||
|
|
||||||
let wallet = KdfPbkdf2.createWallet(rng, mnemonic,
|
let wallet = createWallet(kdfPbkdf2, rng, mnemonic,
|
||||||
name = name,
|
name = name, password = KeystorePass password)
|
||||||
password = KeyStorePass password)
|
|
||||||
|
|
||||||
let outWalletFileFlag = conf.outWalletFile
|
let outWalletFileFlag = conf.outWalletFile
|
||||||
let outWalletFile = if outWalletFileFlag.isSome:
|
let outWalletFile = if outWalletFileFlag.isSome:
|
||||||
@ -338,27 +410,19 @@ proc loadWallet*(fileName: string): Result[Wallet, string] =
|
|||||||
err e.msg
|
err e.msg
|
||||||
|
|
||||||
proc unlockWalletInteractively*(wallet: Wallet): Result[WalletDataForDeposits, string] =
|
proc unlockWalletInteractively*(wallet: Wallet): Result[WalletDataForDeposits, string] =
|
||||||
var json: JsonNode
|
|
||||||
try:
|
|
||||||
json = parseJson wallet.crypto.string
|
|
||||||
except Exception as e: # TODO: parseJson shouldn't raise general `Exception`
|
|
||||||
if e[] of Defect: raise (ref Defect)(e)
|
|
||||||
else: return err "failure to parse crypto field"
|
|
||||||
|
|
||||||
var password: TaintedString
|
|
||||||
|
|
||||||
echo "Please enter the password for unlocking the wallet"
|
echo "Please enter the password for unlocking the wallet"
|
||||||
|
|
||||||
for i in 1..3:
|
for i in 1..3:
|
||||||
|
var password: TaintedString
|
||||||
try:
|
try:
|
||||||
if not readPasswordInput("Password: ", password):
|
if not readPasswordInput("Password: ", password):
|
||||||
return err "failure to read password from stdin"
|
return err "failure to read password from stdin"
|
||||||
|
|
||||||
var status = decryptoCryptoField(json, KeyStorePass password)
|
var secret = decryptCryptoField(wallet.crypto, KeystorePass password)
|
||||||
if status.isOk:
|
if secret.len > 0:
|
||||||
defer: burnMem(status.value)
|
defer: burnMem(secret)
|
||||||
return ok WalletDataForDeposits(
|
return ok WalletDataForDeposits(
|
||||||
mnemonic: Mnemonic string.fromBytes(status.value))
|
mnemonic: Mnemonic string.fromBytes(secret))
|
||||||
else:
|
else:
|
||||||
echo "Unlocking of the wallet failed. Please try again."
|
echo "Unlocking of the wallet failed. Please try again."
|
||||||
finally:
|
finally:
|
||||||
|
@ -6,43 +6,69 @@
|
|||||||
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
||||||
|
|
||||||
import
|
import
|
||||||
json, math, strutils, strformat, typetraits, bearssl,
|
math, strutils, strformat, typetraits, bearssl,
|
||||||
stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros,
|
stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros,
|
||||||
eth/keyfile/uuid, blscurve, json_serialization,
|
eth/keyfile/uuid, blscurve, faststreams/textio, json_serialization,
|
||||||
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, utils],
|
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, utils],
|
||||||
./datatypes, ./crypto, ./digest, ./signatures
|
./datatypes, ./crypto, ./digest, ./signatures
|
||||||
|
|
||||||
export
|
export
|
||||||
results, burnMem
|
results, burnMem, writeValue, readValue
|
||||||
|
|
||||||
{.push raises: [Defect].}
|
{.push raises: [Defect].}
|
||||||
|
|
||||||
type
|
type
|
||||||
ChecksumParams = object
|
ChecksumFunctionKind* = enum
|
||||||
|
sha256Checksum = "sha256"
|
||||||
|
|
||||||
Checksum = object
|
Sha256Params* = object
|
||||||
function: string
|
Sha256Digest* = MDigest[256]
|
||||||
params: ChecksumParams
|
|
||||||
message: string
|
|
||||||
|
|
||||||
CipherParams = object
|
ChecksumBytes* = distinct seq[byte]
|
||||||
iv: string
|
|
||||||
|
|
||||||
Cipher = object
|
Checksum* = object
|
||||||
function: string
|
case function*: ChecksumFunctionKind
|
||||||
params: CipherParams
|
of sha256Checksum:
|
||||||
message: string
|
params*: Sha256Params
|
||||||
|
message*: Sha256Digest
|
||||||
|
|
||||||
KdfScrypt* = object
|
Aes128CtrIv* = distinct seq[byte]
|
||||||
|
|
||||||
|
Aes128CtrParams* = object
|
||||||
|
iv*: Aes128CtrIv
|
||||||
|
|
||||||
|
CipherFunctionKind* = enum
|
||||||
|
aes128CtrCipher = "aes-128-ctr"
|
||||||
|
|
||||||
|
CipherBytes* = distinct seq[byte]
|
||||||
|
|
||||||
|
Cipher* = object
|
||||||
|
case function*: CipherFunctionKind
|
||||||
|
of aes128ctrCipher:
|
||||||
|
params*: Aes128CtrParams
|
||||||
|
message*: CipherBytes
|
||||||
|
|
||||||
|
KdfKind* = enum
|
||||||
|
kdfPbkdf2 = "pbkdf2"
|
||||||
|
kdfScrypt = "scrypt"
|
||||||
|
|
||||||
|
ScryptSalt* = distinct seq[byte]
|
||||||
|
|
||||||
|
ScryptParams* = object
|
||||||
dklen: int
|
dklen: int
|
||||||
n, p, r: int
|
n, p, r: int
|
||||||
salt: string
|
salt: ScryptSalt
|
||||||
|
|
||||||
KdfPbkdf2* = object
|
Pbkdf2Salt* = distinct seq[byte]
|
||||||
dklen: int
|
|
||||||
c: int
|
PrfKind* = enum # Pseudo-random-function Kind
|
||||||
prf: string
|
HmacSha256 = "hmac-sha256"
|
||||||
salt: string
|
|
||||||
|
Pbkdf2Params* = object
|
||||||
|
dklen*: int
|
||||||
|
c*: int
|
||||||
|
prf*: PrfKind
|
||||||
|
salt*: Pbkdf2Salt
|
||||||
|
|
||||||
# https://github.com/ethereum/EIPs/blob/4494da0966afa7318ec0157948821b19c4248805/EIPS/eip-2386.md#specification
|
# https://github.com/ethereum/EIPs/blob/4494da0966afa7318ec0157948821b19c4248805/EIPS/eip-2386.md#specification
|
||||||
Wallet* = object
|
Wallet* = object
|
||||||
@ -52,29 +78,31 @@ type
|
|||||||
walletType* {.serializedFieldName: "type"}: string
|
walletType* {.serializedFieldName: "type"}: string
|
||||||
# TODO: The use of `JsonString` can be removed once we
|
# TODO: The use of `JsonString` can be removed once we
|
||||||
# solve the serialization problem for `Crypto[T]`
|
# solve the serialization problem for `Crypto[T]`
|
||||||
crypto*: JsonString
|
crypto*: Crypto
|
||||||
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
|
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
|
||||||
|
|
||||||
KdfParams = KdfPbkdf2 | KdfScrypt
|
Kdf* = object
|
||||||
|
case function*: KdfKind
|
||||||
|
of kdfPbkdf2:
|
||||||
|
pbkdf2Params* {.serializedFieldName: "params".}: Pbkdf2Params
|
||||||
|
of kdfScrypt:
|
||||||
|
scryptParams* {.serializedFieldName: "params".}: ScryptParams
|
||||||
|
message*: string
|
||||||
|
|
||||||
Kdf[T: KdfParams] = object
|
Crypto* = object
|
||||||
function: string
|
kdf*: Kdf
|
||||||
params: T
|
checksum*: Checksum
|
||||||
message: string
|
cipher*: Cipher
|
||||||
|
|
||||||
Crypto[T: KdfParams] = object
|
Keystore* = object
|
||||||
kdf: Kdf[T]
|
crypto*: Crypto
|
||||||
checksum: Checksum
|
description*: string
|
||||||
cipher: Cipher
|
pubkey*: ValidatorPubKey
|
||||||
|
path*: KeyPath
|
||||||
|
uuid*: string
|
||||||
|
version*: int
|
||||||
|
|
||||||
Keystore[T: KdfParams] = object
|
KsResult*[T] = Result[T, string]
|
||||||
crypto: Crypto[T]
|
|
||||||
pubkey: string
|
|
||||||
path: string
|
|
||||||
uuid: string
|
|
||||||
version: int
|
|
||||||
|
|
||||||
KsResult*[T] = Result[T, cstring]
|
|
||||||
|
|
||||||
Eth2KeyKind* = enum
|
Eth2KeyKind* = enum
|
||||||
signingKeyKind # Also known as voting key
|
signingKeyKind # Also known as voting key
|
||||||
@ -84,34 +112,32 @@ type
|
|||||||
WalletName* = distinct string
|
WalletName* = distinct string
|
||||||
Mnemonic* = distinct string
|
Mnemonic* = distinct string
|
||||||
KeyPath* = distinct string
|
KeyPath* = distinct string
|
||||||
KeyStorePass* = distinct string
|
KeystorePass* = distinct string
|
||||||
KeySeed* = distinct seq[byte]
|
KeySeed* = distinct seq[byte]
|
||||||
|
|
||||||
KeyStoreContent* = distinct JsonString
|
|
||||||
WalletContent* = distinct JsonString
|
|
||||||
|
|
||||||
SensitiveData = Mnemonic|KeyStorePass|KeySeed
|
|
||||||
|
|
||||||
Credentials* = object
|
Credentials* = object
|
||||||
mnemonic*: Mnemonic
|
mnemonic*: Mnemonic
|
||||||
keyStore*: KeyStoreContent
|
keystore*: Keystore
|
||||||
signingKey*: ValidatorPrivKey
|
signingKey*: ValidatorPrivKey
|
||||||
withdrawalKey*: ValidatorPrivKey
|
withdrawalKey*: ValidatorPrivKey
|
||||||
|
|
||||||
const
|
SensitiveData = Mnemonic|KeystorePass|KeySeed
|
||||||
saltSize = 32
|
SimpleHexEncodedTypes = ScryptSalt|ChecksumBytes|CipherBytes
|
||||||
|
|
||||||
scryptParams = KdfScrypt(
|
const
|
||||||
dklen: saltSize,
|
keyLen = 32
|
||||||
|
|
||||||
|
scryptParams = ScryptParams(
|
||||||
|
dklen: keyLen,
|
||||||
n: 2^18,
|
n: 2^18,
|
||||||
r: 1,
|
r: 1,
|
||||||
p: 8
|
p: 8
|
||||||
)
|
)
|
||||||
|
|
||||||
pbkdf2Params = KdfPbkdf2(
|
pbkdf2Params = Pbkdf2Params(
|
||||||
dklen: saltSize,
|
dklen: keyLen,
|
||||||
c: 2^18,
|
c: 2^18,
|
||||||
prf: "hmac-sha256"
|
prf: HmacSha256
|
||||||
)
|
)
|
||||||
|
|
||||||
# https://eips.ethereum.org/EIPS/eip-2334
|
# https://eips.ethereum.org/EIPS/eip-2334
|
||||||
@ -122,8 +148,14 @@ const
|
|||||||
wordListLen = 2048
|
wordListLen = 2048
|
||||||
|
|
||||||
UUID.serializesAsBaseIn Json
|
UUID.serializesAsBaseIn Json
|
||||||
|
KeyPath.serializesAsBaseIn Json
|
||||||
WalletName.serializesAsBaseIn Json
|
WalletName.serializesAsBaseIn Json
|
||||||
|
|
||||||
|
ChecksumFunctionKind.serializesAsTextInJson
|
||||||
|
CipherFunctionKind.serializesAsTextInJson
|
||||||
|
PrfKind.serializesAsTextInJson
|
||||||
|
KdfKind.serializesAsTextInJson
|
||||||
|
|
||||||
template `$`*(m: Mnemonic): string =
|
template `$`*(m: Mnemonic): string =
|
||||||
string(m)
|
string(m)
|
||||||
|
|
||||||
@ -191,7 +223,7 @@ func makeKeyPath*(validatorIdx: Natural,
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
raiseAssert "All values above can be converted successfully to strings"
|
raiseAssert "All values above can be converted successfully to strings"
|
||||||
|
|
||||||
func getSeed*(mnemonic: Mnemonic, password: KeyStorePass): KeySeed =
|
func getSeed*(mnemonic: Mnemonic, password: KeystorePass): KeySeed =
|
||||||
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
|
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
|
||||||
let salt = "mnemonic-" & password.string
|
let salt = "mnemonic-" & password.string
|
||||||
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
|
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
|
||||||
@ -251,7 +283,7 @@ proc deriveMasterKey*(seed: KeySeed): ValidatorPrivKey =
|
|||||||
doAssert success
|
doAssert success
|
||||||
|
|
||||||
proc deriveMasterKey*(mnemonic: Mnemonic,
|
proc deriveMasterKey*(mnemonic: Mnemonic,
|
||||||
password: KeyStorePass): ValidatorPrivKey =
|
password: KeystorePass): ValidatorPrivKey =
|
||||||
deriveMasterKey(getSeed(mnemonic, password))
|
deriveMasterKey(getSeed(mnemonic, password))
|
||||||
|
|
||||||
proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
||||||
@ -261,106 +293,160 @@ proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
|||||||
result = deriveChildKey(result, idx)
|
result = deriveChildKey(result, idx)
|
||||||
|
|
||||||
proc keyFromPath*(mnemonic: Mnemonic,
|
proc keyFromPath*(mnemonic: Mnemonic,
|
||||||
password: KeyStorePass,
|
password: KeystorePass,
|
||||||
path: KeyPath): ValidatorPrivKey =
|
path: KeyPath): ValidatorPrivKey =
|
||||||
deriveChildKey(deriveMasterKey(mnemonic, password), path)
|
deriveChildKey(deriveMasterKey(mnemonic, password), path)
|
||||||
|
|
||||||
proc shaChecksum(key, cipher: openarray[byte]): array[32, byte] =
|
proc shaChecksum(key, cipher: openarray[byte]): Sha256Digest =
|
||||||
var ctx: sha256
|
var ctx: sha256
|
||||||
ctx.init()
|
ctx.init()
|
||||||
ctx.update(key)
|
ctx.update(key)
|
||||||
ctx.update(cipher)
|
ctx.update(cipher)
|
||||||
result = ctx.finish().data
|
result = ctx.finish()
|
||||||
ctx.clear()
|
ctx.clear()
|
||||||
|
|
||||||
template tryJsonToCrypto(json: JsonNode; crypto: typedesc): untyped =
|
|
||||||
try:
|
|
||||||
json.to(Crypto[crypto])
|
|
||||||
except Exception:
|
|
||||||
return err "ks: failed to parse crypto"
|
|
||||||
|
|
||||||
template hexToBytes(data, name: string): untyped =
|
template hexToBytes(data, name: string): untyped =
|
||||||
try:
|
try:
|
||||||
hexToSeqByte(data)
|
hexToSeqByte(data)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return err "ks: failed to parse " & name
|
return err "ks: failed to parse " & name
|
||||||
|
|
||||||
proc decryptoCryptoField*(json: JsonNode,
|
proc writeJsonHexString(s: OutputStream, data: openarray[byte])
|
||||||
password: KeyStorePass): KsResult[seq[byte]] =
|
{.raises: [IOError, Defect].} =
|
||||||
|
s.write '"'
|
||||||
|
s.writeHex data
|
||||||
|
s.write '"'
|
||||||
|
|
||||||
|
proc readValue*(r: var JsonReader, value: var Pbkdf2Salt)
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
var s = r.readValue(string)
|
||||||
|
|
||||||
|
if s.len == 0 or s.len mod 16 != 0:
|
||||||
|
r.raiseUnexpectedValue(
|
||||||
|
"The Pbkdf2Salt salf must have a non-zero length divisible by 16")
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = Pbkdf2Salt hexToSeqByte(s)
|
||||||
|
except ValueError:
|
||||||
|
r.raiseUnexpectedValue(
|
||||||
|
"The Pbkdf2Salt must be a valid hex string")
|
||||||
|
|
||||||
|
proc readValue*(r: var JsonReader, value: var Aes128CtrIv)
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
var s = r.readValue(string)
|
||||||
|
|
||||||
|
if s.len != 32:
|
||||||
|
r.raiseUnexpectedValue(
|
||||||
|
"The aes-128-ctr IV must be a string of length 32")
|
||||||
|
|
||||||
|
try:
|
||||||
|
value = Aes128CtrIv hexToSeqByte(s)
|
||||||
|
except ValueError:
|
||||||
|
r.raiseUnexpectedValue(
|
||||||
|
"The aes-128-ctr IV must be a valid hex string")
|
||||||
|
|
||||||
|
proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T)
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
|
try:
|
||||||
|
value = T hexToSeqByte(r.readValue(string))
|
||||||
|
except ValueError:
|
||||||
|
r.raiseUnexpectedValue("Valid hex string expected")
|
||||||
|
|
||||||
|
proc readValue*(r: var JsonReader, value: var Kdf)
|
||||||
|
{.raises: [SerializationError, IOError, Defect].} =
|
||||||
var
|
var
|
||||||
decKey: seq[byte]
|
functionSpecified = false
|
||||||
salt: seq[byte]
|
paramsSpecified = false
|
||||||
iv: seq[byte]
|
|
||||||
cipherMsg: seq[byte]
|
|
||||||
checksumMsg: seq[byte]
|
|
||||||
|
|
||||||
let kdf = json{"kdf", "function"}.getStr
|
for fieldName in readObjectFields(r):
|
||||||
|
case fieldName
|
||||||
|
of "function":
|
||||||
|
value.function = r.readValue(KdfKind)
|
||||||
|
functionSpecified = true
|
||||||
|
|
||||||
case kdf
|
of "params":
|
||||||
of "scrypt":
|
if functionSpecified:
|
||||||
let crypto = tryJsonToCrypto(json, KdfScrypt)
|
case value.function
|
||||||
return err "ks: scrypt not supported"
|
of kdfPbkdf2:
|
||||||
of "pbkdf2":
|
r.readValue(value.pbkdf2Params)
|
||||||
let
|
of kdfScrypt:
|
||||||
crypto = tryJsonToCrypto(json, KdfPbkdf2)
|
r.readValue(value.scryptParams)
|
||||||
kdfParams = crypto.kdf.params
|
else:
|
||||||
|
r.raiseUnexpectedValue(
|
||||||
|
"The 'params' field must be specified after the 'function' field")
|
||||||
|
paramsSpecified = true
|
||||||
|
|
||||||
salt = hexToBytes(kdfParams.salt, "salt")
|
of "message":
|
||||||
decKey = sha256.pbkdf2(password.string, salt, kdfParams.c, kdfParams.dklen)
|
r.readValue(value.message)
|
||||||
iv = hexToBytes(crypto.cipher.params.iv, "iv")
|
|
||||||
cipherMsg = hexToBytes(crypto.cipher.message, "cipher")
|
|
||||||
checksumMsg = hexToBytes(crypto.checksum.message, "checksum")
|
|
||||||
else:
|
|
||||||
return err "ks: unknown cipher"
|
|
||||||
|
|
||||||
if decKey.len < saltSize:
|
else:
|
||||||
return err "ks: decryption key must be at least 32 bytes"
|
r.raiseUnexpectedField(fieldName, "Kdf")
|
||||||
|
|
||||||
if iv.len < aes128.sizeBlock:
|
if not (functionSpecified and paramsSpecified):
|
||||||
return err "ks: invalid iv"
|
r.raiseUnexpectedValue(
|
||||||
|
"The Kdf value should have sub-fields named 'function' and 'params'")
|
||||||
|
|
||||||
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
template writeValue*(w: var JsonWriter,
|
||||||
if sum != checksumMsg:
|
value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) =
|
||||||
return err "ks: invalid checksum"
|
writeJsonHexString(w.stream, distinctBase value)
|
||||||
|
|
||||||
|
template bytes(value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv): seq[byte] =
|
||||||
|
distinctBase value
|
||||||
|
|
||||||
|
proc decryptCryptoField*(crypto: Crypto, password: KeystorePass): seq[byte] =
|
||||||
|
## Returns 0 bytes if the supplied password is incorrect
|
||||||
|
|
||||||
|
let decKey = case crypto.kdf.function
|
||||||
|
of kdfPbkdf2:
|
||||||
|
template params: auto = crypto.kdf.pbkdf2Params
|
||||||
|
sha256.pbkdf2(password.string, params.salt.bytes, params.c, params.dklen)
|
||||||
|
of kdfScrypt:
|
||||||
|
raiseAssert "Scrypt is not supported yet"
|
||||||
|
|
||||||
|
let derivedChecksum = shaChecksum(decKey.toOpenArray(16, 31),
|
||||||
|
crypto.cipher.message.bytes)
|
||||||
|
if derivedChecksum != crypto.checksum.message:
|
||||||
|
return
|
||||||
|
|
||||||
var
|
var
|
||||||
aesCipher: CTR[aes128]
|
aesCipher: CTR[aes128]
|
||||||
secret = newSeq[byte](cipherMsg.len)
|
secret = newSeq[byte](crypto.cipher.message.bytes.len)
|
||||||
|
|
||||||
aesCipher.init(decKey.toOpenArray(0, 15), iv)
|
aesCipher.init(decKey.toOpenArray(0, 15), crypto.cipher.params.iv.bytes)
|
||||||
aesCipher.decrypt(cipherMsg, secret)
|
aesCipher.decrypt(crypto.cipher.message.bytes, secret)
|
||||||
aesCipher.clear()
|
aesCipher.clear()
|
||||||
|
|
||||||
ok secret
|
return secret
|
||||||
|
|
||||||
proc decryptKeystore*(data: KeyStoreContent,
|
func cstringToStr(v: cstring): string = $v
|
||||||
password: KeyStorePass): KsResult[ValidatorPrivKey] =
|
|
||||||
# TODO: `parseJson` can raise a general `Exception`
|
|
||||||
let
|
|
||||||
ks = try: parseJson(data.string)
|
|
||||||
except Exception: return err "ks: failed to parse keystore"
|
|
||||||
secret = decryptoCryptoField(ks{"crypto"}, password)
|
|
||||||
|
|
||||||
ValidatorPrivKey.fromRaw(? secret)
|
proc decryptKeystore*(keystore: Keystore,
|
||||||
|
password: KeystorePass): KsResult[ValidatorPrivKey] =
|
||||||
|
let decryptedBytes = decryptCryptoField(keystore.crypto, password)
|
||||||
|
if decryptedBytes.len > 0:
|
||||||
|
return ValidatorPrivKey.fromRaw(decryptedBytes).mapErr(cstringToStr)
|
||||||
|
|
||||||
proc createCryptoField(T: type[KdfParams],
|
proc decryptKeystore*(keystore: JsonString,
|
||||||
|
password: KeystorePass): KsResult[ValidatorPrivKey] =
|
||||||
|
let keystore = try: Json.decode(keystore.string, Keystore)
|
||||||
|
except SerializationError as e:
|
||||||
|
return err e.formatMsg("<keystore>")
|
||||||
|
decryptKeystore(keystore, password)
|
||||||
|
|
||||||
|
proc createCryptoField(kdfKind: KdfKind,
|
||||||
rng: var BrHmacDrbgContext,
|
rng: var BrHmacDrbgContext,
|
||||||
secret: openarray[byte],
|
secret: openarray[byte],
|
||||||
password = KeyStorePass "",
|
password = KeystorePass "",
|
||||||
salt: openarray[byte] = @[],
|
salt: openarray[byte] = @[],
|
||||||
iv: openarray[byte] = @[]): Crypto[T] =
|
iv: openarray[byte] = @[]): Crypto =
|
||||||
type AES = aes128
|
type AES = aes128
|
||||||
|
|
||||||
var
|
let kdfSalt = Pbkdf2Salt:
|
||||||
decKey: seq[byte]
|
if salt.len > 0:
|
||||||
aesCipher: CTR[AES]
|
doAssert salt.len == keyLen
|
||||||
cipherMsg = newSeq[byte](secret.len)
|
@salt
|
||||||
|
else:
|
||||||
let kdfSalt = if salt.len > 0:
|
getRandomBytes(rng, keyLen)
|
||||||
doAssert salt.len == saltSize
|
|
||||||
@salt
|
|
||||||
else:
|
|
||||||
getRandomBytes(rng, saltSize)
|
|
||||||
|
|
||||||
let aesIv = if iv.len > 0:
|
let aesIv = if iv.len > 0:
|
||||||
doAssert iv.len == AES.sizeBlock
|
doAssert iv.len == AES.sizeBlock
|
||||||
@ -368,14 +454,21 @@ proc createCryptoField(T: type[KdfParams],
|
|||||||
else:
|
else:
|
||||||
getRandomBytes(rng, AES.sizeBlock)
|
getRandomBytes(rng, AES.sizeBlock)
|
||||||
|
|
||||||
when T is KdfPbkdf2:
|
let decKey = sha256.pbkdf2(password.string,
|
||||||
decKey = sha256.pbkdf2(password.string, kdfSalt, pbkdf2Params.c,
|
kdfSalt.bytes,
|
||||||
pbkdf2Params.dklen)
|
pbkdf2Params.c,
|
||||||
|
pbkdf2Params.dklen)
|
||||||
|
let kdf = case kdfKind
|
||||||
|
of kdfPbkdf2:
|
||||||
|
var params = pbkdf2Params
|
||||||
|
params.salt = kdfSalt
|
||||||
|
Kdf(function: kdfPbkdf2, pbkdf2Params: params, message: "")
|
||||||
|
of kdfScrypt:
|
||||||
|
raiseAssert "Scrypt is not implemented yet"
|
||||||
|
|
||||||
var kdf = Kdf[KdfPbkdf2](function: "pbkdf2", params: pbkdf2Params, message: "")
|
var
|
||||||
kdf.params.salt = byteutils.toHex(kdfSalt)
|
aesCipher: CTR[AES]
|
||||||
else:
|
cipherMsg = newSeq[byte](secret.len)
|
||||||
{.fatal: "Other KDFs are supported yet".}
|
|
||||||
|
|
||||||
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
|
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
|
||||||
aesCipher.encrypt(secret, cipherMsg)
|
aesCipher.encrypt(secret, cipherMsg)
|
||||||
@ -383,46 +476,43 @@ proc createCryptoField(T: type[KdfParams],
|
|||||||
|
|
||||||
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
||||||
|
|
||||||
Crypto[T](
|
Crypto(
|
||||||
kdf: kdf,
|
kdf: kdf,
|
||||||
checksum: Checksum(
|
checksum: Checksum(
|
||||||
function: "sha256",
|
function: sha256Checksum,
|
||||||
message: byteutils.toHex(sum)),
|
message: sum),
|
||||||
cipher: Cipher(
|
cipher: Cipher(
|
||||||
function: "aes-128-ctr",
|
function: aes128CtrCipher,
|
||||||
params: CipherParams(iv: byteutils.toHex(aesIv)),
|
params: Aes128CtrParams(iv: Aes128CtrIv aesIv),
|
||||||
message: byteutils.toHex(cipherMsg)))
|
message: CipherBytes cipherMsg))
|
||||||
|
|
||||||
proc encryptKeystore*(T: type[KdfParams],
|
proc createKeystore*(kdfKind: KdfKind,
|
||||||
rng: var BrHmacDrbgContext,
|
rng: var BrHmacDrbgContext,
|
||||||
privKey: ValidatorPrivkey,
|
privKey: ValidatorPrivkey,
|
||||||
password = KeyStorePass "",
|
password = KeystorePass "",
|
||||||
path = KeyPath "",
|
path = KeyPath "",
|
||||||
salt: openarray[byte] = @[],
|
salt: openarray[byte] = @[],
|
||||||
iv: openarray[byte] = @[],
|
iv: openarray[byte] = @[]): Keystore =
|
||||||
pretty = true): KeyStoreContent =
|
|
||||||
let
|
let
|
||||||
secret = privKey.toRaw[^32..^1]
|
secret = privKey.toRaw[^32..^1]
|
||||||
cryptoField = createCryptoField(T, rng, secret, password, salt, iv)
|
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv)
|
||||||
pubkey = privKey.toPubKey()
|
pubkey = privKey.toPubKey()
|
||||||
uuid = uuidGenerate().expect("Random bytes should be available")
|
uuid = uuidGenerate().expect("Random bytes should be available")
|
||||||
keystore = Keystore[T](
|
|
||||||
crypto: cryptoField,
|
|
||||||
pubkey: toHex(pubkey),
|
|
||||||
path: path.string,
|
|
||||||
uuid: $uuid,
|
|
||||||
version: 4)
|
|
||||||
|
|
||||||
KeyStoreContent if pretty: json.pretty(%keystore)
|
Keystore(
|
||||||
else: $(%keystore)
|
crypto: cryptoField,
|
||||||
|
pubkey: pubkey,
|
||||||
|
path: path,
|
||||||
|
uuid: $uuid,
|
||||||
|
version: 4)
|
||||||
|
|
||||||
proc createWallet*(T: type[KdfParams],
|
proc createWallet*(kdfKind: KdfKind,
|
||||||
rng: var BrHmacDrbgContext,
|
rng: var BrHmacDrbgContext,
|
||||||
mnemonic: Mnemonic,
|
mnemonic: Mnemonic,
|
||||||
name = WalletName "",
|
name = WalletName "",
|
||||||
salt: openarray[byte] = @[],
|
salt: openarray[byte] = @[],
|
||||||
iv: openarray[byte] = @[],
|
iv: openarray[byte] = @[],
|
||||||
password = KeyStorePass "",
|
password = KeystorePass "",
|
||||||
nextAccount = none(Natural),
|
nextAccount = none(Natural),
|
||||||
pretty = true): Wallet =
|
pretty = true): Wallet =
|
||||||
let
|
let
|
||||||
@ -430,8 +520,9 @@ proc createWallet*(T: type[KdfParams],
|
|||||||
# Please note that we are passing an empty password here because
|
# Please note that we are passing an empty password here because
|
||||||
# we want the wallet restoration procedure to depend only on the
|
# we want the wallet restoration procedure to depend only on the
|
||||||
# mnemonic (the user is asked to treat the mnemonic as a password).
|
# mnemonic (the user is asked to treat the mnemonic as a password).
|
||||||
seed = getSeed(mnemonic, KeyStorePass"")
|
seed = getSeed(mnemonic, KeystorePass"")
|
||||||
cryptoField = %createCryptoField(T,rng, distinctBase seed, password, salt, iv)
|
crypto = createCryptoField(kdfKind, rng, distinctBase seed,
|
||||||
|
password, salt, iv)
|
||||||
|
|
||||||
Wallet(
|
Wallet(
|
||||||
uuid: uuid,
|
uuid: uuid,
|
||||||
@ -439,23 +530,9 @@ proc createWallet*(T: type[KdfParams],
|
|||||||
else: WalletName(uuid),
|
else: WalletName(uuid),
|
||||||
version: 1,
|
version: 1,
|
||||||
walletType: "hierarchical deterministic",
|
walletType: "hierarchical deterministic",
|
||||||
crypto: JsonString(if pretty: json.pretty(cryptoField)
|
crypto: crypto,
|
||||||
else: $cryptoField),
|
|
||||||
nextAccount: nextAccount.get(0))
|
nextAccount: nextAccount.get(0))
|
||||||
|
|
||||||
proc createWalletContent*(T: type[KdfParams],
|
|
||||||
rng: var BrHmacDrbgContext,
|
|
||||||
mnemonic: Mnemonic,
|
|
||||||
name = WalletName "",
|
|
||||||
salt: openarray[byte] = @[],
|
|
||||||
iv: openarray[byte] = @[],
|
|
||||||
password = KeyStorePass "",
|
|
||||||
nextAccount = none(Natural),
|
|
||||||
pretty = true): (UUID, WalletContent) =
|
|
||||||
let wallet = createWallet(
|
|
||||||
T, rng, mnemonic, name, salt, iv, password, nextAccount, pretty)
|
|
||||||
(wallet.uuid, WalletContent Json.encode(wallet, pretty = pretty))
|
|
||||||
|
|
||||||
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/deposit-contract.md#withdrawal-credentials
|
# https://github.com/ethereum/eth2.0-specs/blob/v0.12.2/specs/phase0/deposit-contract.md#withdrawal-credentials
|
||||||
proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
|
proc makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
|
||||||
var bytes = eth2digest(k.toRaw())
|
var bytes = eth2digest(k.toRaw())
|
||||||
|
@ -8,9 +8,10 @@
|
|||||||
{.used.}
|
{.used.}
|
||||||
|
|
||||||
import
|
import
|
||||||
unittest, ./testutil, json,
|
json, unittest,
|
||||||
stew/byteutils, blscurve, eth/keys,
|
stew/byteutils, blscurve, eth/keys, json_serialization,
|
||||||
../beacon_chain/spec/[crypto, keystore]
|
../beacon_chain/spec/[crypto, keystore],
|
||||||
|
./testutil
|
||||||
|
|
||||||
from strutils import replace
|
from strutils import replace
|
||||||
|
|
||||||
@ -94,18 +95,20 @@ suiteReport "Keystore":
|
|||||||
let secret = ValidatorPrivKey.fromRaw(secretBytes).get
|
let secret = ValidatorPrivKey.fromRaw(secretBytes).get
|
||||||
|
|
||||||
timedTest "Pbkdf2 decryption":
|
timedTest "Pbkdf2 decryption":
|
||||||
let decrypt = decryptKeystore(KeyStoreContent pbkdf2Vector,
|
let
|
||||||
KeyStorePass password)
|
keystore = Json.decode(pbkdf2Vector, Keystore)
|
||||||
|
decrypt = decryptKeystore(keystore, KeystorePass password)
|
||||||
|
|
||||||
check decrypt.isOk
|
check decrypt.isOk
|
||||||
check secret == decrypt.get()
|
check secret == decrypt.get()
|
||||||
|
|
||||||
timedTest "Pbkdf2 encryption":
|
timedTest "Pbkdf2 encryption":
|
||||||
let encrypt = encryptKeystore(KdfPbkdf2, rng[], secret,
|
let keystore = createKeystore(kdfPbkdf2, rng[], secret,
|
||||||
KeyStorePass password,
|
KeystorePass password,
|
||||||
salt=salt, iv=iv,
|
salt=salt, iv=iv,
|
||||||
path = validateKeyPath "m/12381/60/0/0")
|
path = validateKeyPath "m/12381/60/0/0")
|
||||||
var
|
var
|
||||||
encryptJson = parseJson(encrypt.string)
|
encryptJson = parseJson Json.encode(keystore)
|
||||||
pbkdf2Json = parseJson(pbkdf2Vector)
|
pbkdf2Json = parseJson(pbkdf2Vector)
|
||||||
encryptJson{"uuid"} = %""
|
encryptJson{"uuid"} = %""
|
||||||
pbkdf2Json{"uuid"} = %""
|
pbkdf2Json{"uuid"} = %""
|
||||||
@ -114,26 +117,26 @@ suiteReport "Keystore":
|
|||||||
|
|
||||||
timedTest "Pbkdf2 errors":
|
timedTest "Pbkdf2 errors":
|
||||||
expect Defect:
|
expect Defect:
|
||||||
echo encryptKeystore(KdfPbkdf2, rng[], secret, salt = [byte 1]).string
|
echo createKeystore(kdfPbkdf2, rng[], secret, salt = [byte 1])
|
||||||
|
|
||||||
expect Defect:
|
expect Defect:
|
||||||
echo encryptKeystore(KdfPbkdf2, rng[], secret, iv = [byte 1]).string
|
echo createKeystore(kdfPbkdf2, rng[], secret, iv = [byte 1])
|
||||||
|
|
||||||
check decryptKeystore(KeyStoreContent pbkdf2Vector,
|
check decryptKeystore(JsonString pbkdf2Vector,
|
||||||
KeyStorePass "wrong pass").isErr
|
KeystorePass "wrong pass").isErr
|
||||||
|
|
||||||
check decryptKeystore(KeyStoreContent pbkdf2Vector,
|
check decryptKeystore(JsonString pbkdf2Vector,
|
||||||
KeyStorePass "").isErr
|
KeystorePass "").isErr
|
||||||
|
|
||||||
check decryptKeystore(KeyStoreContent "{\"a\": 0}",
|
check decryptKeystore(JsonString "{\"a\": 0}",
|
||||||
KeyStorePass "").isErr
|
KeystorePass "").isErr
|
||||||
|
|
||||||
check decryptKeystore(KeyStoreContent "",
|
check decryptKeystore(JsonString "",
|
||||||
KeyStorePass "").isErr
|
KeystorePass "").isErr
|
||||||
|
|
||||||
template checkVariant(remove): untyped =
|
template checkVariant(remove): untyped =
|
||||||
check decryptKeystore(KeyStoreContent pbkdf2Vector.replace(remove, ""),
|
check decryptKeystore(JsonString pbkdf2Vector.replace(remove, ""),
|
||||||
KeyStorePass password).isErr
|
KeystorePass password).isErr
|
||||||
|
|
||||||
checkVariant "d4e5" # salt
|
checkVariant "d4e5" # salt
|
||||||
checkVariant "18b1" # checksum
|
checkVariant "18b1" # checksum
|
||||||
@ -143,5 +146,5 @@ suiteReport "Keystore":
|
|||||||
var badKdf = parseJson(pbkdf2Vector)
|
var badKdf = parseJson(pbkdf2Vector)
|
||||||
badKdf{"crypto", "kdf", "function"} = %"invalid"
|
badKdf{"crypto", "kdf", "function"} = %"invalid"
|
||||||
|
|
||||||
check decryptKeystore(KeyStoreContent $badKdf,
|
check decryptKeystore(JsonString $badKdf,
|
||||||
KeyStorePass password).iserr
|
KeystorePass password).iserr
|
||||||
|
2
vendor/nim-faststreams
vendored
2
vendor/nim-faststreams
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 5df69fc6961e58205189cd92ae2477769fa8c4c0
|
Subproject commit 87309f3120d4e627082171a188324d3ee14d8986
|
2
vendor/nim-json-serialization
vendored
2
vendor/nim-json-serialization
vendored
@ -1 +1 @@
|
|||||||
Subproject commit 9ca88fdcd43f5a4cbc2d86caf057a7bd12575698
|
Subproject commit 1dccd4b2ef14c5e3ce30ad3f3a0962e0b98da6a3
|
2
vendor/nimcrypto
vendored
2
vendor/nimcrypto
vendored
@ -1 +1 @@
|
|||||||
Subproject commit f767595f4ddec2b5570b5194feb96954c00a6499
|
Subproject commit 029a1f0f1efea17c37b38992c836cf0ac2c803f2
|
Loading…
x
Reference in New Issue
Block a user