Implement EIP-2386 wallets; Progress towards a CLI for interactive wallet creation

For more information:
4494da0966/EIPS/eip-2386.md (specification)
This commit is contained in:
Zahary Karadjov 2020-06-23 22:11:07 +03:00 committed by zah
parent 6836d41ebd
commit 384e512031
5 changed files with 366 additions and 134 deletions

View File

@ -7,7 +7,7 @@
import
# Standard library
algorithm, os, tables, random, strutils, times, math,
algorithm, os, tables, random, strutils, times, math, terminal,
# Nimble packages
stew/[objects, byteutils], stew/shims/macros,
@ -899,7 +899,7 @@ func formatGwei(amount: uint64): string =
when hasPrompt:
from unicode import Rune
import terminal, prompt
import prompt
proc providePromptCompletions*(line: seq[Rune], cursorPos: int): seq[string] =
# TODO
@ -1026,6 +1026,104 @@ when hasPrompt:
# var t: Thread[ptr Prompt]
# createThread(t, processPromptCommands, addr p)
proc createWalletInteractively(conf: BeaconNodeConf): OutFile {.raises: [Defect].} =
if conf.nonInteractive:
fatal "Wallets can be created only in interactive mode"
quit 1
var mnemonic = generateMnemonic()
defer: keystore_management.burnMem(mnemonic)
template readLine: string =
try: stdin.readLine()
except IOError:
fatal "Failed to read data from stdin"
quit 1
echo "The created wallet will be protected with a password " &
"that applies only to the current Nimbus installation. " &
"In case you lose your wallet and you need to restore " &
"it on a different machine, you must use the following " &
"seed recovery phrase: \n"
echo $mnemonic
echo "Please back up the seed phrase now to a safe location as " &
"if you are protecting a sensitive password. The seed phrase " &
"be used to withdrawl funds from your wallet.\n"
echo "Did you back up your seed recovery phrase? (please type 'yes' to continue or press enter to quit)"
while true:
let answer = readLine()
if answer == "":
quit 1
elif answer != "yes":
echo "To continue, please type 'yes' (without the quotes) or press enter to quit"
else:
break
echo "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."
while true:
var password, confirmedPassword: TaintedString
try:
let status = try:
readPasswordFromStdin("Please enter a password:", password) and
readPasswordFromStdin("Please repeat the password:", confirmedPassword)
except IOError:
fatal "Failed to read password interactively"
quit 1
if status:
if password != confirmedPassword:
echo "Passwords don't match, please try again"
else:
var name: WalletName
if conf.createdWalletName.isSome:
name = conf.createdWalletName.get
else:
echo "For your convenience, the wallet can be identified with a name " &
"of your choice. Please enter a wallet name below or press ENTER " &
"to continue with a machine-generated name."
while true:
var enteredName = readLine()
if enteredName.len > 0:
name = try: WalletName.parseCmdArg(enteredName)
except CatchableError as err:
echo err.msg & ". Please try again."
continue
break
let (uuid, walletContent) = KdfPbkdf2.createWalletContent(mnemonic, name)
try:
var outWalletFile: OutFile
if conf.createdWalletFile.isSome:
outWalletFile = conf.createdWalletFile.get
createDir splitFile(string outWalletFile).dir
else:
let walletsDir = conf.walletsDir
createDir walletsDir
outWalletFile = OutFile(walletsDir / addFileExt(string uuid, "json"))
writeFile(string outWalletFile, string walletContent)
return outWalletFile
except CatchableError as err:
fatal "Failed to write wallet file", err = err.msg
quit 1
if not status:
fatal "Failed to read a password from stdin"
quit 1
finally:
keystore_management.burnMem(password)
keystore_management.burnMem(confirmedPassword)
programMain:
let config = makeBannerAndConfig(clientId, BeaconNodeConf)
@ -1100,22 +1198,6 @@ programMain:
writeFile(bootstrapFile, bootstrapEnr.tryGet().toURI)
echo "Wrote ", bootstrapFile
of importValidator:
template reportFailureFor(keyExpr) =
error "Failed to import validator key", key = keyExpr
programResult = 1
if config.keyFiles.len == 0:
stderr.write "Please specify at least one keyfile to import."
quit 1
for keyFile in config.keyFiles:
try:
saveValidatorKey(keyFile.string.extractFilename,
readFile(keyFile.string), config)
except:
reportFailureFor keyFile.string
of noCommand:
debug "Launching beacon node",
version = fullVersionStr,
@ -1178,3 +1260,19 @@ programMain:
config.depositContractAddress,
config.depositPrivateKey,
delayGenerator)
of DepositsCmd.status:
# TODO
echo "The status command is not implemented yet"
quit 1
of wallets:
case config.walletsCmd:
of WalletsCmd.create:
let walletFile = createWalletInteractively(config)
of WalletsCmd.list:
# TODO
discard
of WalletsCmd.restore:
# TODO
discard

View File

@ -15,16 +15,19 @@ type
BNStartUpCmd* = enum
noCommand
importValidator
createTestnet
deposits
wallets
DepositsCmd* = enum
create = "Create validator keystores and deposits"
send = "Send prepared deposits to the validator deposit contract"
WalletsCmd* {.pure.} = enum
create = "Creates a new EIP-2386 wallet"
restore = "Restores a wallet from cold storage"
list = "Lists details about all wallets"
# TODO
# status = "Display status information about all deposits"
DepositsCmd* {.pure.} = enum
create = "Creates validator keystores and deposits"
send = "Sends prepared deposits to the validator deposit contract"
status = "Displays status information about all deposits"
VCStartUpCmd* = enum
VCNoCommand
@ -38,36 +41,36 @@ type
BeaconNodeConf* = object
logLevel* {.
defaultValue: "DEBUG"
desc: "Sets the log level."
desc: "Sets the log level"
name: "log-level" }: string
eth1Network* {.
defaultValue: goerli
desc: "The Eth1 network tracked by the beacon node."
desc: "The Eth1 network tracked by the beacon node"
name: "eth1-network" }: Eth1Network
dataDir* {.
defaultValue: config.defaultDataDir()
desc: "The directory where nimbus will store all blockchain data."
desc: "The directory where nimbus will store all blockchain data"
abbr: "d"
name: "data-dir" }: OutDir
web3Url* {.
defaultValue: ""
desc: "URL of the Web3 server to observe Eth1."
desc: "URL of the Web3 server to observe Eth1"
name: "web3-url" }: string
depositContractAddress* {.
defaultValue: ""
desc: "Address of the deposit contract."
desc: "Address of the deposit contract"
name: "deposit-contract" }: string
depositContractDeployedAt* {.
desc: "The Eth1 block hash where the deposit contract has been deployed."
desc: "The Eth1 block hash where the deposit contract has been deployed"
name: "deposit-contract-block" }: Option[Eth2Digest]
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration."
desc: "Do not display interative prompts. Quit on missing configuration"
name: "non-interactive" }: bool
case cmd* {.
@ -76,28 +79,28 @@ type
of noCommand:
bootstrapNodes* {.
desc: "Specifies one or more bootstrap nodes to use when connecting to the network."
desc: "Specifies one or more bootstrap nodes to use when connecting to the network"
abbr: "b"
name: "bootstrap-node" }: seq[string]
bootstrapNodesFile* {.
defaultValue: ""
desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses."
desc: "Specifies a line-delimited file of bootstrap Ethereum network addresses"
name: "bootstrap-file" }: InputFile
libp2pAddress* {.
defaultValue: defaultListenAddress(config)
desc: "Listening address for the Ethereum LibP2P traffic."
desc: "Listening address for the Ethereum LibP2P traffic"
name: "listen-address" }: ValidIpAddress
tcpPort* {.
defaultValue: defaultEth2TcpPort
desc: "Listening TCP port for Ethereum LibP2P traffic."
desc: "Listening TCP port for Ethereum LibP2P traffic"
name: "tcp-port" }: Port
udpPort* {.
defaultValue: defaultEth2TcpPort
desc: "Listening UDP port for node discovery."
desc: "Listening UDP port for node discovery"
name: "udp-port" }: Port
maxPeers* {.
@ -107,7 +110,7 @@ type
nat* {.
desc: "Specify method to use for determining public address. " &
"Must be one of: any, none, upnp, pmp, extip:<IP>."
"Must be one of: any, none, upnp, pmp, extip:<IP>"
defaultValue: "any" }: string
validators* {.
@ -117,52 +120,56 @@ type
name: "validator" }: seq[ValidatorKeyPath]
validatorsDirFlag* {.
desc: "A directory containing validator keystores."
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDirFlag* {.
desc: "A directory containing validator keystore passwords."
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
walletsDirFlag* {.
desc: "A directory containing wallet files"
name: "wallets-dir" }: Option[InputDir]
stateSnapshot* {.
desc: "Json file specifying a recent state snapshot."
desc: "Json file specifying a recent state snapshot"
abbr: "s"
name: "state-snapshot" }: Option[InputFile]
nodeName* {.
defaultValue: ""
desc: "A name for this node that will appear in the logs. " &
"If you set this to 'auto', a persistent automatically generated ID will be selected for each --dataDir folder."
"If you set this to 'auto', a persistent automatically generated ID will be selected for each --data-dir folder"
name: "node-name" }: string
verifyFinalization* {.
defaultValue: false
desc: "Specify whether to verify finalization occurs on schedule, for testing."
desc: "Specify whether to verify finalization occurs on schedule, for testing"
name: "verify-finalization" }: bool
stopAtEpoch* {.
defaultValue: 0
desc: "A positive epoch selects the epoch at which to stop."
desc: "A positive epoch selects the epoch at which to stop"
name: "stop-at-epoch" }: uint64
metricsEnabled* {.
defaultValue: false
desc: "Enable the metrics server."
desc: "Enable the metrics server"
name: "metrics" }: bool
metricsAddress* {.
defaultValue: defaultAdminListenAddress(config)
desc: "Listening address of the metrics server."
desc: "Listening address of the metrics server"
name: "metrics-address" }: ValidIpAddress
metricsPort* {.
defaultValue: 8008
desc: "Listening HTTP port of the metrics server."
desc: "Listening HTTP port of the metrics server"
name: "metrics-port" }: Port
statusBarEnabled* {.
defaultValue: true
desc: "Display a status bar at the bottom of the terminal screen."
desc: "Display a status bar at the bottom of the terminal screen"
name: "status-bar" }: bool
statusBarContents* {.
@ -171,7 +178,7 @@ type
"head: $head_root:$head_epoch:$head_epoch_slot;" &
"time: $epoch:$epoch_slot ($slot)|" &
"ETH: $attached_validators_balance"
desc: "Textual template for the contents of the status bar."
desc: "Textual template for the contents of the status bar"
name: "status-bar-contents" }: string
rpcEnabled* {.
@ -181,7 +188,7 @@ type
rpcPort* {.
defaultValue: defaultEth2RpcPort
desc: "HTTP port for the JSON-RPC service."
desc: "HTTP port for the JSON-RPC service"
name: "rpc-port" }: Port
rpcAddress* {.
@ -196,77 +203,109 @@ type
of createTestnet:
testnetDepositsDir* {.
desc: "Directory containing validator keystores."
desc: "Directory containing validator keystores"
name: "validators-dir" }: InputDir
totalValidators* {.
desc: "The number of validator deposits in the newly created chain."
desc: "The number of validator deposits in the newly created chain"
name: "total-validators" }: uint64
firstValidator* {.
defaultValue: 0
desc: "Index of first validator to add to validator list."
desc: "Index of first validator to add to validator list"
name: "first-validator" }: uint64
lastUserValidator* {.
defaultValue: config.totalValidators - 1,
desc: "The last validator index that will free for taking from a testnet participant."
desc: "The last validator index that will free for taking from a testnet participant"
name: "last-user-validator" }: uint64
bootstrapAddress* {.
defaultValue: ValidIpAddress.init("127.0.0.1")
desc: "The public IP address that will be advertised as a bootstrap node for the testnet."
desc: "The public IP address that will be advertised as a bootstrap node for the testnet"
name: "bootstrap-address" }: ValidIpAddress
bootstrapPort* {.
defaultValue: defaultEth2TcpPort
desc: "The TCP/UDP port that will be used by the bootstrap node."
desc: "The TCP/UDP port that will be used by the bootstrap node"
name: "bootstrap-port" }: Port
genesisOffset* {.
defaultValue: 5
desc: "Seconds from now to add to genesis time."
desc: "Seconds from now to add to genesis time"
name: "genesis-offset" }: int
outputGenesis* {.
desc: "Output file where to write the initial state snapshot."
desc: "Output file where to write the initial state snapshot"
name: "output-genesis" }: OutFile
withGenesisRoot* {.
defaultValue: false
desc: "Include a genesis root in 'network.json'."
desc: "Include a genesis root in 'network.json'"
name: "with-genesis-root" }: bool
outputBootstrapFile* {.
desc: "Output file with list of bootstrap nodes for the network."
desc: "Output file with list of bootstrap nodes for the network"
name: "output-bootstrap-file" }: OutFile
of importValidator:
keyFiles* {.
desc: "File with validator key to be imported (in hex form)."
name: "keyfile" }: seq[ValidatorKeyPath]
of wallets:
case walletsCmd* {.command.}: WalletsCmd
of WalletsCmd.create:
createdWalletName* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "name"}: Option[WalletName]
nextAccount* {.
desc: "Initial value for the 'nextaccount' property of the wallet"
name: "next-account" }: Option[Natural]
createdWalletFile* {.
desc: "Output wallet file"
name: "out" }: Option[OutFile]
of WalletsCmd.restore:
restoredWalletName* {.
desc: "An easy-to-remember name for the wallet of your choice"
name: "name"}: Option[WalletName]
restoredDepositsCount* {.
desc: "Expected number of deposits to recover. If not specified, " &
"Nimbus will try to guess the number by inspecting the latest " &
"beacon state"
name: "deposits".}: Option[Natural]
restoredWalletFile* {.
desc: "Output wallet file"
name: "out" }: Option[OutFile]
of WalletsCmd.list:
discard
of deposits:
case depositsCmd* {.command.}: DepositsCmd
of create:
of DepositsCmd.create:
totalDeposits* {.
defaultValue: 1
desc: "Number of deposits to generate."
desc: "Number of deposits to generate"
name: "count" }: int
existingWalletId* {.
desc: "An existing wallet ID. If not specified, a new wallet will be created"
name: "wallet" }: Option[WalletName]
outValidatorsDir* {.
defaultValue: "validators"
desc: "Output folder for validator keystores and deposits."
desc: "Output folder for validator keystores and deposits"
name: "out-deposits-dir" }: string
outSecretsDir* {.
defaultValue: "secrets"
desc: "Output folder for randomly generated keystore passphrases."
desc: "Output folder for randomly generated keystore passphrases"
name: "out-secrets-dir" }: string
depositPrivateKey* {.
defaultValue: ""
desc: "Private key of the controlling (sending) account.",
desc: "Private key of the controlling (sending) account",
name: "deposit-private-key" }: string
dontSend* {.
@ -274,25 +313,28 @@ type
desc: "By default, all created deposits are also immediately sent " &
"to the validator deposit contract. You can use this option to " &
"prevent this behavior. Use the `deposits send` command to send " &
"the deposit transactions at your convenience later."
"the deposit transactions at your convenience later"
name: "dont-send" .}: bool
of send:
of DepositsCmd.send:
depositsDir* {.
defaultValue: "validators"
desc: "A folder with validator metadata created by the `deposits create` command."
desc: "A folder with validator metadata created by the `deposits create` command"
name: "deposits-dir" }: string
minDelay* {.
defaultValue: 0.0
desc: "Minimum possible delay between making two deposits (in seconds)."
desc: "Minimum possible delay between making two deposits (in seconds)"
name: "min-delay" }: float
maxDelay* {.
defaultValue: 0.0
desc: "Maximum possible delay between making two deposits (in seconds)."
desc: "Maximum possible delay between making two deposits (in seconds)"
name: "max-delay" }: float
of DepositsCmd.status:
discard
ValidatorClientConf* = object
logLevel* {.
defaultValue: "DEBUG"
@ -301,12 +343,12 @@ type
dataDir* {.
defaultValue: config.defaultDataDir()
desc: "The directory where nimbus will store all blockchain data."
desc: "The directory where nimbus will store all blockchain data"
abbr: "d"
name: "data-dir" }: OutDir
nonInteractive* {.
desc: "Do not display interative prompts. Quit on missing configuration."
desc: "Do not display interative prompts. Quit on missing configuration"
name: "non-interactive" }: bool
case cmd* {.
@ -316,26 +358,26 @@ type
of VCNoCommand:
rpcPort* {.
defaultValue: defaultEth2RpcPort
desc: "HTTP port of the server to connect to for RPC."
desc: "HTTP port of the server to connect to for RPC"
name: "rpc-port" }: Port
rpcAddress* {.
defaultValue: defaultAdminListenAddress(config)
desc: "Address of the server to connect to for RPC."
desc: "Address of the server to connect to for RPC"
name: "rpc-address" }: ValidIpAddress
validators* {.
required
desc: "Attach a validator by supplying a keystore path."
desc: "Attach a validator by supplying a keystore path"
abbr: "v"
name: "validator" }: seq[ValidatorKeyPath]
validatorsDirFlag* {.
desc: "A directory containing validator keystores."
desc: "A directory containing validator keystores"
name: "validators-dir" }: Option[InputDir]
secretsDirFlag* {.
desc: "A directory containing validator keystore passwords."
desc: "A directory containing validator keystore passwords"
name: "secrets-dir" }: Option[InputDir]
proc defaultDataDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
@ -377,12 +419,26 @@ func parseCmdArg*(T: type Eth2Digest, input: TaintedString): T
func completeCmdArg*(T: type Eth2Digest, input: TaintedString): seq[string] =
return @[]
func parseCmdArg*(T: type WalletName, input: TaintedString): T
{.raises: [ValueError, Defect].} =
if input.len == 0:
raise newException(ValueError, "The wallet name should not be empty")
if input[0] == '_':
raise newException(ValueError, "The wallet name should not start with an underscore")
return T(input)
func completeCmdArg*(T: type WalletName, input: TaintedString): seq[string] =
return @[]
func validatorsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
string conf.validatorsDirFlag.get(InputDir(conf.dataDir / "validators"))
func secretsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
string conf.secretsDirFlag.get(InputDir(conf.dataDir / "secrets"))
func walletsDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
string conf.walletsDirFlag.get(InputDir(conf.dataDir / "wallets"))
func databaseDir*(conf: BeaconNodeConf|ValidatorClientConf): string =
conf.dataDir / "db"

View File

@ -4,6 +4,9 @@ import
web3, stint, eth/keys, confutils,
spec/[datatypes, digest, crypto, keystore], conf, ssz/merkleization, merkle_minimal
export
keystore
contract(DepositContract):
proc deposit(pubkey: Bytes48, withdrawalCredentials: Bytes32, signature: Bytes96, deposit_data_root: FixedBytes[32])

View File

@ -6,10 +6,10 @@
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
json, math, strutils, strformat,
json, math, strutils, strformat, typetraits,
stew/[results, byteutils, bitseqs, bitops2], stew/shims/macros,
eth/keyfile/uuid, blscurve,
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand],
eth/keyfile/uuid, blscurve, json_serialization,
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand, utils],
./datatypes, ./crypto, ./digest, ./signatures
export
@ -44,6 +44,17 @@ type
prf: string
salt: string
# https://github.com/ethereum/EIPs/blob/4494da0966afa7318ec0157948821b19c4248805/EIPS/eip-2386.md#specification
Wallet* = object
uuid*: UUID
name*: WalletName
version*: uint
walletType* {.serializedFieldName: "type"}: string
# TODO: The use of `JsonString` can be removed once we
# solve the serialization problem for `Crypto[T]`
crypto*: JsonString
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
KdfParams = KdfPbkdf2 | KdfScrypt
Kdf[T: KdfParams] = object
@ -69,12 +80,18 @@ type
signingKeyKind # Also known as voting key
withdrawalKeyKind
UUID* = distinct string
WalletName* = distinct string
Mnemonic* = distinct string
KeyPath* = distinct string
KeyStorePass* = distinct string
KeyStoreContent* = distinct JsonString
KeySeed* = distinct seq[byte]
KeyStoreContent* = distinct JsonString
WalletContent* = distinct JsonString
SensitiveData = Mnemonic|KeyStorePass|KeySeed
Credentials* = object
mnemonic*: Mnemonic
keyStore*: KeyStoreContent
@ -104,6 +121,17 @@ const
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
wordListLen = 2048
UUID.serializesAsBaseIn Json
WalletName.serializesAsBaseIn Json
template `$`*(m: Mnemonic): string =
string(m)
template burnMem*(m: var (SensitiveData|TaintedString)) =
# TODO: `burnMem` in nimcrypto could use distinctBase
# to make its usage less error-prone.
utils.burnMem(string m)
macro wordListArray(filename: static string): array[wordListLen, cstring] =
result = newTree(nnkBracket)
var words = slurp(filename).split()
@ -152,7 +180,7 @@ func getSeed*(mnemonic: Mnemonic, password: KeyStorePass): KeySeed =
let salt = "mnemonic-" & password.string
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
proc generateMnemonic*(words: openarray[cstring],
proc generateMnemonic*(words: openarray[cstring] = englishWords,
entropyParam: openarray[byte] = @[]): Mnemonic =
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
doAssert words.len == wordListLen
@ -226,9 +254,9 @@ proc shaChecksum(key, cipher: openarray[byte]): array[32, byte] =
result = ctx.finish().data
ctx.clear()
template tryJsonToCrypto(ks: JsonNode; crypto: typedesc): untyped =
template tryJsonToCrypto(json: JsonNode; crypto: typedesc): untyped =
try:
ks{"crypto"}.to(Crypto[crypto])
json.to(Crypto[crypto])
except Exception:
return err "ks: failed to parse crypto"
@ -238,12 +266,8 @@ template hexToBytes(data, name: string): untyped =
except ValueError:
return err "ks: failed to parse " & name
proc decryptKeystore*(data: KeyStoreContent,
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"
proc decryptoCryptoField*(json: JsonNode,
password: KeyStorePass): KsResult[seq[byte]] =
var
decKey: seq[byte]
salt: seq[byte]
@ -251,15 +275,15 @@ proc decryptKeystore*(data: KeyStoreContent,
cipherMsg: seq[byte]
checksumMsg: seq[byte]
let kdf = ks{"crypto", "kdf", "function"}.getStr
let kdf = json{"kdf", "function"}.getStr
case kdf
of "scrypt":
let crypto = tryJsonToCrypto(ks, KdfScrypt)
let crypto = tryJsonToCrypto(json, KdfScrypt)
return err "ks: scrypt not supported"
of "pbkdf2":
let
crypto = tryJsonToCrypto(ks, KdfPbkdf2)
crypto = tryJsonToCrypto(json, KdfPbkdf2)
kdfParams = crypto.kdf.params
salt = hexToBytes(kdfParams.salt, "salt")
@ -288,34 +312,41 @@ proc decryptKeystore*(data: KeyStoreContent,
aesCipher.decrypt(cipherMsg, secret)
aesCipher.clear()
ValidatorPrivKey.fromRaw(secret)
ok secret
proc decryptKeystore*(data: KeyStoreContent,
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 createCryptoField(T: type[KdfParams],
secret: openarray[byte],
password = KeyStorePass "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[]): Crypto[T] =
type AES = aes128
proc encryptKeystore*(T: type[KdfParams],
privKey: ValidatorPrivkey,
password = KeyStorePass "",
path = KeyPath "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[],
pretty = true): KeyStoreContent =
var
secret = privKey.toRaw[^32..^1]
decKey: seq[byte]
aesCipher: CTR[aes128]
aesIv = newSeq[byte](aes128.sizeBlock)
kdfSalt = newSeq[byte](saltSize)
aesCipher: CTR[AES]
cipherMsg = newSeq[byte](secret.len)
if salt.len > 0:
let kdfSalt = if salt.len > 0:
doAssert salt.len == saltSize
kdfSalt = @salt
@salt
else:
getRandomBytesOrPanic(kdfSalt)
getRandomBytesOrPanic(saltSize)
if iv.len > 0:
doAssert iv.len == aes128.sizeBlock
aesIv = @iv
let aesIv = if iv.len > 0:
doAssert iv.len == AES.sizeBlock
@iv
else:
getRandomBytesOrPanic(aesIv)
getRandomBytesOrPanic(AES.sizeBlock)
when T is KdfPbkdf2:
decKey = sha256.pbkdf2(password.string, kdfSalt, pbkdf2Params.c,
@ -324,39 +355,83 @@ proc encryptKeystore*(T: type[KdfParams],
var kdf = Kdf[KdfPbkdf2](function: "pbkdf2", params: pbkdf2Params, message: "")
kdf.params.salt = byteutils.toHex(kdfSalt)
else:
return
{.fatal: "Other KDFs are supported yet".}
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
aesCipher.encrypt(secret, cipherMsg)
aesCipher.clear()
let pubkey = privKey.toPubKey()
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
Crypto[T](
kdf: kdf,
checksum: Checksum(
function: "sha256",
message: byteutils.toHex(sum)),
cipher: Cipher(
function: "aes-128-ctr",
params: CipherParams(iv: byteutils.toHex(aesIv)),
message: byteutils.toHex(cipherMsg)))
proc encryptKeystore*(T: type[KdfParams],
privKey: ValidatorPrivkey,
password = KeyStorePass "",
path = KeyPath "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[],
pretty = true): KeyStoreContent =
let
sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
uuid = uuidGenerate().get
secret = privKey.toRaw[^32..^1]
cryptoField = createCryptoField(T, secret, password, salt, iv)
pubkey = privKey.toPubKey()
uuid = uuidGenerate().expect("Random bytes should be available")
keystore = Keystore[T](
crypto: Crypto[T](
kdf: kdf,
checksum: Checksum(
function: "sha256",
message: byteutils.toHex(sum)
),
cipher: Cipher(
function: "aes-128-ctr",
params: CipherParams(iv: byteutils.toHex(aesIv)),
message: byteutils.toHex(cipherMsg)
)
),
crypto: cryptoField,
pubkey: toHex(pubkey),
path: path.string,
uuid: $uuid,
version: 4)
KeyStoreContent if pretty: json.pretty(%keystore, indent=4)
KeyStoreContent if pretty: json.pretty(%keystore)
else: $(%keystore)
proc createWallet*(T: type[KdfParams],
mnemonic: Mnemonic,
name = WalletName "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[],
password = KeyStorePass "",
nextAccount = none(Natural),
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"")
cryptoField = %createCryptoField(T, distinctBase seed, password, salt, iv)
Wallet(
uuid: uuid,
name: if name.string.len > 0: name
else: WalletName(uuid),
version: 1,
walletType: "hierarchical deterministic",
crypto: JsonString(if pretty: json.pretty(cryptoField)
else: $cryptoField),
nextAccount: nextAccount.get(0))
proc createWalletContent*(T: type[KdfParams],
mnemonic: Mnemonic,
name = WalletName "",
salt: openarray[byte] = @[],
iv: openarray[byte] = @[],
password = KeyStorePass "",
nextAccount = none(Natural),
pretty = true): (UUID, WalletContent) =
let wallet = createWallet(T, mnemonic, name, salt, iv, password, nextAccount, pretty)
(wallet.uuid, WalletContent Json.encode(wallet, pretty = pretty))
proc restoreCredentials*(mnemonic: Mnemonic,
password = KeyStorePass ""): Credentials =
let

@ -1 +1 @@
Subproject commit aac25d1610a8fc15ae90fdd2a0b38ed60ea412fb
Subproject commit a76faa5eec2454309753bbe99a68fb3cc25782f1