2020-05-19 17:30:28 +00:00
|
|
|
# beacon_chain
|
2022-03-24 21:44:34 +00:00
|
|
|
# Copyright (c) 2018-2022 Status Research & Development GmbH
|
2020-05-19 17:30:28 +00:00
|
|
|
# Licensed and distributed under either of
|
|
|
|
# * MIT license (license terms in the root directory or at https://opensource.org/licenses/MIT).
|
|
|
|
# * Apache v2 license (license terms in the root directory or at https://www.apache.org/licenses/LICENSE-2.0).
|
|
|
|
# at your option. This file may not be copied, modified, or distributed except according to those terms.
|
|
|
|
|
2022-07-29 10:53:42 +00:00
|
|
|
when (NimMajor, NimMinor) < (1, 4):
|
|
|
|
{.push raises: [Defect].}
|
|
|
|
else:
|
|
|
|
{.push raises: [].}
|
2020-11-27 22:16:13 +00:00
|
|
|
|
2020-05-19 17:30:28 +00:00
|
|
|
import
|
2020-09-24 05:27:56 +00:00
|
|
|
# Standard library
|
2021-11-30 01:20:21 +00:00
|
|
|
std/[algorithm, math, parseutils, strformat, strutils, typetraits, unicode,
|
|
|
|
uri],
|
2020-10-02 15:46:05 +00:00
|
|
|
# Third-party libraries
|
|
|
|
normalize,
|
2020-09-24 05:27:56 +00:00
|
|
|
# Status libraries
|
2022-08-07 21:53:20 +00:00
|
|
|
stew/[results, bitops2, base10, io2], stew/shims/macros,
|
2022-08-19 10:30:07 +00:00
|
|
|
eth/keyfile/uuid, blscurve,
|
|
|
|
json_serialization, json_serialization/std/options,
|
2020-08-20 13:01:08 +00:00
|
|
|
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, scrypt],
|
2020-10-02 15:46:05 +00:00
|
|
|
# Local modules
|
2020-08-20 13:01:08 +00:00
|
|
|
libp2p/crypto/crypto as lcrypto,
|
2021-08-12 13:08:20 +00:00
|
|
|
./datatypes/base, ./signatures
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
export base, uri, io2, options
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
# We use `ncrutils` for constant-time hexadecimal encoding/decoding procedures.
|
|
|
|
import nimcrypto/utils as ncrutils
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
export
|
2020-08-02 17:26:57 +00:00
|
|
|
results, burnMem, writeValue, readValue
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2022-06-09 14:30:13 +00:00
|
|
|
{.localPassC: "-fno-lto".} # no LTO for crypto
|
2020-05-20 07:01:20 +00:00
|
|
|
|
2020-05-19 17:30:28 +00:00
|
|
|
type
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreMode* = enum
|
|
|
|
Secure, Fast
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
ChecksumFunctionKind* = enum
|
|
|
|
sha256Checksum = "sha256"
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Sha256Params* = object
|
|
|
|
Sha256Digest* = MDigest[256]
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
ChecksumBytes* = distinct seq[byte]
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Checksum* = object
|
|
|
|
case function*: ChecksumFunctionKind
|
|
|
|
of sha256Checksum:
|
|
|
|
params*: Sha256Params
|
|
|
|
message*: Sha256Digest
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Aes128CtrIv* = distinct seq[byte]
|
|
|
|
|
|
|
|
Aes128CtrParams* = object
|
|
|
|
iv*: Aes128CtrIv
|
|
|
|
|
|
|
|
CipherFunctionKind* = enum
|
|
|
|
aes128CtrCipher = "aes-128-ctr"
|
|
|
|
|
|
|
|
CipherBytes* = distinct seq[byte]
|
|
|
|
|
|
|
|
Cipher* = object
|
|
|
|
case function*: CipherFunctionKind
|
2022-04-08 16:22:49 +00:00
|
|
|
of aes128CtrCipher:
|
2020-08-02 17:26:57 +00:00
|
|
|
params*: Aes128CtrParams
|
|
|
|
message*: CipherBytes
|
|
|
|
|
|
|
|
KdfKind* = enum
|
|
|
|
kdfPbkdf2 = "pbkdf2"
|
|
|
|
kdfScrypt = "scrypt"
|
|
|
|
|
|
|
|
ScryptSalt* = distinct seq[byte]
|
|
|
|
|
|
|
|
ScryptParams* = object
|
2022-02-07 20:36:09 +00:00
|
|
|
dklen*: uint64
|
|
|
|
n*, p*, r*: int
|
|
|
|
salt*: ScryptSalt
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Pbkdf2Salt* = distinct seq[byte]
|
|
|
|
|
|
|
|
PrfKind* = enum # Pseudo-random-function Kind
|
|
|
|
HmacSha256 = "hmac-sha256"
|
|
|
|
|
|
|
|
Pbkdf2Params* = object
|
2020-10-09 16:41:53 +00:00
|
|
|
dklen*: uint64
|
|
|
|
c*: uint64
|
2020-08-02 17:26:57 +00:00
|
|
|
prf*: PrfKind
|
|
|
|
salt*: Pbkdf2Salt
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
DecryptionStatus* = enum
|
|
|
|
Success = "Success"
|
|
|
|
InvalidPassword = "Invalid password"
|
|
|
|
InvalidKeystore = "Invalid keystore"
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
# 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]`
|
2020-08-02 17:26:57 +00:00
|
|
|
crypto*: Crypto
|
2020-06-23 19:11:07 +00:00
|
|
|
nextAccount* {.serializedFieldName: "nextaccount".}: Natural
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Kdf* = object
|
|
|
|
case function*: KdfKind
|
|
|
|
of kdfPbkdf2:
|
|
|
|
pbkdf2Params* {.serializedFieldName: "params".}: Pbkdf2Params
|
|
|
|
of kdfScrypt:
|
|
|
|
scryptParams* {.serializedFieldName: "params".}: ScryptParams
|
|
|
|
message*: string
|
|
|
|
|
|
|
|
Crypto* = object
|
|
|
|
kdf*: Kdf
|
|
|
|
checksum*: Checksum
|
|
|
|
cipher*: Cipher
|
|
|
|
|
|
|
|
Keystore* = object
|
|
|
|
crypto*: Crypto
|
2022-08-19 10:30:07 +00:00
|
|
|
description*: Option[string]
|
2020-08-02 17:26:57 +00:00
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
path*: KeyPath
|
|
|
|
uuid*: string
|
|
|
|
version*: int
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreKind* = enum
|
|
|
|
Local, Remote
|
|
|
|
|
|
|
|
RemoteKeystoreFlag* {.pure.} = enum
|
|
|
|
IgnoreSSLVerification
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
HttpHostUri* = distinct Uri
|
|
|
|
|
2022-05-10 00:32:12 +00:00
|
|
|
RemoteSignerInfo* = object
|
|
|
|
url*: HttpHostUri
|
|
|
|
id*: uint32
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
|
2022-08-07 21:53:20 +00:00
|
|
|
FileLockHandle* = ref object
|
|
|
|
ioHandle*: IoLockHandle
|
|
|
|
opened*: bool
|
|
|
|
|
2021-12-22 12:37:31 +00:00
|
|
|
KeystoreData* = object
|
|
|
|
version*: uint64
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
description*: Option[string]
|
2022-08-07 21:53:20 +00:00
|
|
|
handle*: FileLockHandle
|
2021-12-22 12:37:31 +00:00
|
|
|
case kind*: KeystoreKind
|
|
|
|
of KeystoreKind.Local:
|
|
|
|
privateKey*: ValidatorPrivKey
|
|
|
|
path*: KeyPath
|
|
|
|
uuid*: string
|
|
|
|
of KeystoreKind.Remote:
|
|
|
|
flags*: set[RemoteKeystoreFlag]
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes*: seq[RemoteSignerInfo]
|
|
|
|
threshold*: uint32
|
2021-12-22 12:37:31 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
NetKeystore* = object
|
|
|
|
crypto*: Crypto
|
2022-08-19 10:30:07 +00:00
|
|
|
description*: Option[string]
|
2020-08-20 13:01:08 +00:00
|
|
|
pubkey*: lcrypto.PublicKey
|
|
|
|
uuid*: string
|
|
|
|
version*: int
|
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
RemoteSignerType* {.pure.} = enum
|
|
|
|
Web3Signer
|
|
|
|
|
|
|
|
RemoteKeystore* = object
|
2021-12-22 12:37:31 +00:00
|
|
|
version*: uint64
|
2021-11-30 01:20:21 +00:00
|
|
|
description*: Option[string]
|
|
|
|
remoteType*: RemoteSignerType
|
|
|
|
pubkey*: ValidatorPubKey
|
|
|
|
flags*: set[RemoteKeystoreFlag]
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes*: seq[RemoteSignerInfo]
|
|
|
|
threshold*: uint32
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
KsResult*[T] = Result[T, string]
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
Eth2KeyKind* = enum
|
|
|
|
signingKeyKind # Also known as voting key
|
|
|
|
withdrawalKeyKind
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
UUID* = distinct string
|
|
|
|
WalletName* = distinct string
|
2020-06-01 19:48:20 +00:00
|
|
|
Mnemonic* = distinct string
|
|
|
|
KeyPath* = distinct string
|
|
|
|
KeySeed* = distinct seq[byte]
|
2020-10-02 15:46:05 +00:00
|
|
|
KeystorePass* = object
|
|
|
|
str*: string
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
Credentials* = object
|
|
|
|
mnemonic*: Mnemonic
|
2020-08-02 17:26:57 +00:00
|
|
|
keystore*: Keystore
|
2020-06-01 19:48:20 +00:00
|
|
|
signingKey*: ValidatorPrivKey
|
|
|
|
withdrawalKey*: ValidatorPrivKey
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
SimpleHexEncodedTypes* = ScryptSalt|ChecksumBytes|CipherBytes
|
2020-08-02 17:26:57 +00:00
|
|
|
|
2020-05-19 17:30:28 +00:00
|
|
|
const
|
2020-08-02 17:26:57 +00:00
|
|
|
keyLen = 32
|
2020-05-20 07:22:21 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
scryptParams = ScryptParams(
|
2020-10-09 16:41:53 +00:00
|
|
|
dklen: uint64 keyLen,
|
2020-05-19 17:30:28 +00:00
|
|
|
n: 2^18,
|
2020-08-02 18:46:12 +00:00
|
|
|
p: 1,
|
|
|
|
r: 8
|
2020-05-19 17:30:28 +00:00
|
|
|
)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
pbkdf2Params = Pbkdf2Params(
|
2020-10-09 16:41:53 +00:00
|
|
|
dklen: uint64 keyLen,
|
|
|
|
c: uint64(2^18),
|
2020-08-02 17:26:57 +00:00
|
|
|
prf: HmacSha256
|
2020-05-19 17:30:28 +00:00
|
|
|
)
|
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
# https://eips.ethereum.org/EIPS/eip-2334
|
|
|
|
eth2KeyPurpose = 12381
|
|
|
|
eth2CoinType* = 3600
|
2020-10-19 19:02:48 +00:00
|
|
|
baseKeyPath* = [Natural eth2KeyPurpose, eth2CoinType]
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0039/bip-0039-wordlists.md
|
|
|
|
wordListLen = 2048
|
2020-08-21 19:36:42 +00:00
|
|
|
maxWordLen = 16
|
2020-06-02 19:59:51 +00:00
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
UUID.serializesAsBaseIn Json
|
2020-08-02 17:26:57 +00:00
|
|
|
KeyPath.serializesAsBaseIn Json
|
2020-06-23 19:11:07 +00:00
|
|
|
WalletName.serializesAsBaseIn Json
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
ChecksumFunctionKind.serializesAsTextInJson
|
|
|
|
CipherFunctionKind.serializesAsTextInJson
|
|
|
|
PrfKind.serializesAsTextInJson
|
|
|
|
KdfKind.serializesAsTextInJson
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
template `$`*(u: HttpHostUri): string =
|
|
|
|
`$`(Uri(u))
|
|
|
|
|
|
|
|
template `==`*(lhs, rhs: HttpHostUri): bool =
|
|
|
|
Uri(lhs) == Uri(rhs)
|
|
|
|
|
|
|
|
template `<`*(lhs, rhs: HttpHostUri): bool =
|
|
|
|
$Uri(lhs) < $Uri(rhs)
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
template `$`*(m: Mnemonic): string =
|
|
|
|
string(m)
|
|
|
|
|
2020-07-17 20:59:50 +00:00
|
|
|
template `==`*(lhs, rhs: WalletName): bool =
|
|
|
|
string(lhs) == string(rhs)
|
|
|
|
|
|
|
|
template `$`*(x: WalletName): string =
|
|
|
|
string(x)
|
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
# TODO: `burnMem` in nimcrypto could use distinctBase
|
|
|
|
# to make its usage less error-prone.
|
2022-03-24 21:44:34 +00:00
|
|
|
template burnMem*(m: var (Mnemonic|string)) =
|
2020-08-20 14:59:59 +00:00
|
|
|
ncrutils.burnMem(string m)
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-10-19 19:02:48 +00:00
|
|
|
template burnMem*(m: var KeySeed) =
|
|
|
|
ncrutils.burnMem(distinctBase m)
|
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
template burnMem*(m: var KeystorePass) =
|
|
|
|
ncrutils.burnMem(m.str)
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
func longName*(wallet: Wallet): string =
|
|
|
|
if wallet.name.string == wallet.uuid.string:
|
|
|
|
wallet.name.string
|
|
|
|
else:
|
|
|
|
wallet.name.string & " (" & wallet.uuid.string & ")"
|
|
|
|
|
2020-07-14 19:00:35 +00:00
|
|
|
macro wordListArray*(filename: static string,
|
|
|
|
maxWords: static int = 0,
|
2020-08-21 19:36:42 +00:00
|
|
|
minWordLen: static int = 0,
|
|
|
|
maxWordLen: static int = high(int)): untyped =
|
2020-06-02 19:59:51 +00:00
|
|
|
result = newTree(nnkBracket)
|
2021-02-02 22:31:01 +00:00
|
|
|
var words = slurp(filename.replace('\\', '/')).splitLines()
|
2020-06-02 19:59:51 +00:00
|
|
|
for word in words:
|
2020-08-21 19:36:42 +00:00
|
|
|
if word.len >= minWordLen and word.len <= maxWordLen:
|
2020-07-14 19:00:35 +00:00
|
|
|
result.add newCall("cstring", newLit(word))
|
|
|
|
if maxWords > 0 and result.len >= maxWords:
|
|
|
|
return
|
2020-06-02 19:59:51 +00:00
|
|
|
|
|
|
|
const
|
2020-07-14 19:00:35 +00:00
|
|
|
englishWords = wordListArray("english_word_list.txt",
|
2020-08-21 19:36:42 +00:00
|
|
|
maxWords = wordListLen,
|
|
|
|
maxWordLen = maxWordLen)
|
2020-09-29 16:49:09 +00:00
|
|
|
englishWordsDigest =
|
|
|
|
"AD90BF3BEB7B0EB7E5ACD74727DC0DA96E0A280A258354E7293FB7E211AC03DB".toDigest
|
|
|
|
|
|
|
|
proc checkEnglishWords(): bool =
|
|
|
|
if len(englishWords) != wordListLen:
|
|
|
|
false
|
|
|
|
else:
|
|
|
|
var ctx: sha256
|
|
|
|
ctx.init()
|
|
|
|
for item in englishWords:
|
|
|
|
ctx.update($item)
|
|
|
|
ctx.finish() == englishWordsDigest
|
2020-08-21 19:36:42 +00:00
|
|
|
|
|
|
|
static:
|
2020-09-29 16:49:09 +00:00
|
|
|
doAssert(checkEnglishWords(), "English words array is corrupted!")
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
func validateKeyPath*(path: string): Result[KeyPath, cstring] =
|
2020-09-24 05:27:56 +00:00
|
|
|
var digitCount: int
|
2022-04-08 16:22:49 +00:00
|
|
|
var number: BiggestUInt
|
2020-09-24 05:27:56 +00:00
|
|
|
try:
|
|
|
|
for elem in path.string.split("/"):
|
2020-09-29 16:49:09 +00:00
|
|
|
# TODO: doesn't "m" have to be the first character and is it the only
|
|
|
|
# place where it is valid?
|
2020-09-24 05:27:56 +00:00
|
|
|
if elem == "m":
|
|
|
|
continue
|
|
|
|
# parseBiggestUInt can raise if overflow
|
|
|
|
digitCount = elem.parseBiggestUInt(number)
|
|
|
|
if digitCount == 0:
|
|
|
|
return err("Invalid derivation path")
|
|
|
|
except ValueError:
|
|
|
|
return err("KeyPath contains invalid number(s)")
|
|
|
|
|
|
|
|
return ok(KeyPath path)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
iterator pathNodes(path: KeyPath): Natural =
|
2020-09-24 05:27:56 +00:00
|
|
|
# TODO: we have exceptions there
|
|
|
|
# and this iterator is used to derive secret keys
|
|
|
|
# if we fail we want to scrub secrets from memory
|
2020-06-01 19:48:20 +00:00
|
|
|
try:
|
2020-09-24 05:27:56 +00:00
|
|
|
for elem in path.string.split("/"):
|
|
|
|
if elem == "m": continue
|
|
|
|
yield parseBiggestUInt(elem)
|
2020-06-01 19:48:20 +00:00
|
|
|
except ValueError:
|
|
|
|
doAssert false, "Make sure you've validated the key path with `validateKeyPath`"
|
|
|
|
|
|
|
|
func makeKeyPath*(validatorIdx: Natural,
|
|
|
|
keyType: Eth2KeyKind): KeyPath =
|
|
|
|
# https://eips.ethereum.org/EIPS/eip-2334
|
|
|
|
let use = case keyType
|
|
|
|
of withdrawalKeyKind: "0"
|
|
|
|
of signingKeyKind: "0/0"
|
|
|
|
|
|
|
|
try:
|
|
|
|
KeyPath &"m/{eth2KeyPurpose}/{eth2CoinType}/{validatorIdx}/{use}"
|
|
|
|
except ValueError:
|
|
|
|
raiseAssert "All values above can be converted successfully to strings"
|
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
func isControlRune(r: Rune): bool =
|
|
|
|
let r = int r
|
|
|
|
(r >= 0 and r < 0x20) or (r >= 0x7F and r < 0xA0)
|
|
|
|
|
|
|
|
proc init*(T: type KeystorePass, input: string): T =
|
|
|
|
for rune in toNFKD(input):
|
|
|
|
if not isControlRune(rune):
|
|
|
|
result.str.add rune
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
func getSeed*(mnemonic: Mnemonic, password: KeystorePass): KeySeed =
|
2020-06-01 19:48:20 +00:00
|
|
|
# https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed
|
2020-10-02 16:08:57 +00:00
|
|
|
let salt = toNFKD("mnemonic" & password.str)
|
2020-06-01 19:48:20 +00:00
|
|
|
KeySeed sha512.pbkdf2(mnemonic.string, salt, 2048, 64)
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
template add(m: var Mnemonic, s: cstring) =
|
|
|
|
m.string.add s
|
|
|
|
|
2020-07-07 15:51:02 +00:00
|
|
|
proc generateMnemonic*(
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-10-28 18:35:31 +00:00
|
|
|
words: openArray[cstring] = englishWords,
|
|
|
|
entropyParam: openArray[byte] = @[]): Mnemonic =
|
2020-08-21 19:36:42 +00:00
|
|
|
## Generates a valid BIP-0039 mnenomic:
|
|
|
|
## https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#generating-the-mnemonic
|
2022-06-21 08:29:16 +00:00
|
|
|
var entropy =
|
|
|
|
if entropyParam.len == 0:
|
|
|
|
rng.generateBytes(32)
|
|
|
|
else:
|
|
|
|
doAssert entropyParam.len >= 128 and
|
|
|
|
entropyParam.len <= 256 and
|
|
|
|
entropyParam.len mod 32 == 0
|
|
|
|
@entropyParam
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
let
|
|
|
|
checksumBits = entropy.len div 4 # ranges from 4 to 8
|
|
|
|
mnemonicWordCount = 12 + (checksumBits - 4) * 3
|
|
|
|
checksum = sha256.digest(entropy)
|
|
|
|
|
|
|
|
entropy.add byte(checksum.data.getBitsBE(0 ..< checksumBits))
|
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
# Make sure the string won't be reallocated as this may
|
|
|
|
# leave partial copies of the mnemonic in memory:
|
|
|
|
result = Mnemonic newStringOfCap(mnemonicWordCount * maxWordLen)
|
|
|
|
result.add words[entropy.getBitsBE(0..10)]
|
2020-06-02 19:59:51 +00:00
|
|
|
|
2020-06-01 19:48:20 +00:00
|
|
|
for i in 1 ..< mnemonicWordCount:
|
|
|
|
let
|
|
|
|
firstBit = i*11
|
|
|
|
lastBit = firstBit + 10
|
2020-08-21 19:36:42 +00:00
|
|
|
result.add " "
|
|
|
|
result.add words[entropy.getBitsBE(firstBit..lastBit)]
|
|
|
|
|
|
|
|
proc cmpIgnoreCase(lhs: cstring, rhs: string): int =
|
|
|
|
# TODO: This is a bit silly.
|
|
|
|
# Nim should have a `cmp` function for C strings.
|
|
|
|
cmpIgnoreCase($lhs, rhs)
|
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
proc validateMnemonic*(inputWords: string,
|
2020-08-21 19:36:42 +00:00
|
|
|
outputMnemonic: var Mnemonic): bool =
|
|
|
|
## Accept a case-insensitive input string and returns `true`
|
|
|
|
## if it represents a valid mnenomic. The `outputMnemonic`
|
|
|
|
## value will be populated with a normalized lower-case
|
|
|
|
## version of the mnemonic using a single space separator.
|
|
|
|
##
|
|
|
|
## The `outputMnemonic` value may be populated partially
|
|
|
|
## with sensitive data even in case of validator failure.
|
|
|
|
## Make sure to burn the received data after usage.
|
|
|
|
|
2022-03-24 21:44:34 +00:00
|
|
|
# TODO consider using a SecretString type for inputWords
|
|
|
|
|
2020-10-02 15:46:05 +00:00
|
|
|
let words = strutils.strip(inputWords.string.toNFKD).split(Whitespace)
|
2020-08-21 19:36:42 +00:00
|
|
|
if words.len < 12 or words.len > 24 or words.len mod 3 != 0:
|
|
|
|
return false
|
|
|
|
|
|
|
|
# Make sure the string won't be re-allocated as this may
|
|
|
|
# leave partial copies of the mnemonic in memory:
|
|
|
|
outputMnemonic = Mnemonic newStringOfCap(words.len * maxWordLen)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-21 19:36:42 +00:00
|
|
|
for word in words:
|
|
|
|
let foundIdx = binarySearch(englishWords, word, cmpIgnoreCase)
|
|
|
|
if foundIdx == -1:
|
|
|
|
return false
|
|
|
|
if outputMnemonic.string.len > 0:
|
|
|
|
outputMnemonic.add " "
|
|
|
|
outputMnemonic.add englishWords[foundIdx]
|
|
|
|
|
|
|
|
return true
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
proc deriveChildKey*(parentKey: ValidatorPrivKey,
|
|
|
|
index: Natural): ValidatorPrivKey =
|
2020-06-08 14:56:56 +00:00
|
|
|
let success = derive_child_secretKey(SecretKey result,
|
|
|
|
SecretKey parentKey,
|
|
|
|
uint32 index)
|
|
|
|
# TODO `derive_child_secretKey` is reporting pre-condition
|
|
|
|
# failures with return values. We should turn the checks
|
|
|
|
# into asserts inside the function.
|
|
|
|
doAssert success
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
proc deriveMasterKey*(seed: KeySeed): ValidatorPrivKey =
|
2020-06-08 14:56:56 +00:00
|
|
|
let success = derive_master_secretKey(SecretKey result,
|
|
|
|
seq[byte] seed)
|
|
|
|
# TODO `derive_master_secretKey` is reporting pre-condition
|
|
|
|
# failures with return values. We should turn the checks
|
|
|
|
# into asserts inside the function.
|
|
|
|
doAssert success
|
2020-06-01 19:48:20 +00:00
|
|
|
|
|
|
|
proc deriveMasterKey*(mnemonic: Mnemonic,
|
2020-08-02 17:26:57 +00:00
|
|
|
password: KeystorePass): ValidatorPrivKey =
|
2020-06-01 19:48:20 +00:00
|
|
|
deriveMasterKey(getSeed(mnemonic, password))
|
|
|
|
|
|
|
|
proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
|
|
|
path: KeyPath): ValidatorPrivKey =
|
|
|
|
result = masterKey
|
|
|
|
for idx in pathNodes(path):
|
2020-10-19 19:02:48 +00:00
|
|
|
result = deriveChildKey(result, idx)
|
|
|
|
|
|
|
|
proc deriveChildKey*(masterKey: ValidatorPrivKey,
|
|
|
|
path: openArray[Natural]): ValidatorPrivKey =
|
|
|
|
result = masterKey
|
|
|
|
for idx in path:
|
2020-09-24 05:27:56 +00:00
|
|
|
# TODO: we have exceptions in pathNodes unless `validateKeyPath`
|
|
|
|
# was called,
|
|
|
|
# and this iterator is used to derive secret keys
|
|
|
|
# if we fail we want to scrub secrets from memory
|
2020-06-01 19:48:20 +00:00
|
|
|
result = deriveChildKey(result, idx)
|
|
|
|
|
|
|
|
proc keyFromPath*(mnemonic: Mnemonic,
|
2020-08-02 17:26:57 +00:00
|
|
|
password: KeystorePass,
|
2020-06-01 19:48:20 +00:00
|
|
|
path: KeyPath): ValidatorPrivKey =
|
|
|
|
deriveChildKey(deriveMasterKey(mnemonic, password), path)
|
|
|
|
|
2020-10-28 18:35:31 +00:00
|
|
|
proc shaChecksum(key, cipher: openArray[byte]): Sha256Digest =
|
2020-05-19 17:30:28 +00:00
|
|
|
var ctx: sha256
|
|
|
|
ctx.init()
|
|
|
|
ctx.update(key)
|
|
|
|
ctx.update(cipher)
|
2020-08-02 17:26:57 +00:00
|
|
|
result = ctx.finish()
|
2020-05-20 07:22:21 +00:00
|
|
|
ctx.clear()
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-10-28 18:35:31 +00:00
|
|
|
proc writeJsonHexString(s: OutputStream, data: openArray[byte])
|
2020-08-02 17:26:57 +00:00
|
|
|
{.raises: [IOError, Defect].} =
|
|
|
|
s.write '"'
|
2020-08-20 13:01:08 +00:00
|
|
|
s.write ncrutils.toHex(data, {HexFlags.LowerCase})
|
2020-08-02 17:26:57 +00:00
|
|
|
s.write '"'
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc readValue*(r: var JsonReader, value: var Pbkdf2Salt)
|
|
|
|
{.raises: [SerializationError, IOError, Defect].} =
|
|
|
|
var s = r.readValue(string)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
if s.len == 0 or s.len mod 16 != 0:
|
|
|
|
r.raiseUnexpectedValue(
|
2020-08-20 13:01:08 +00:00
|
|
|
"The Pbkdf2Salt salt must have a non-zero length divisible by 16")
|
2020-05-27 14:05:32 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
value = Pbkdf2Salt ncrutils.fromHex(s)
|
|
|
|
let length = len(seq[byte](value))
|
|
|
|
if length == 0 or (length mod 8) != 0:
|
2020-08-02 17:26:57 +00:00
|
|
|
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")
|
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
value = Aes128CtrIv ncrutils.fromHex(s)
|
|
|
|
if len(seq[byte](value)) != 16:
|
2020-08-02 17:26:57 +00:00
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The aes-128-ctr IV must be a valid hex string")
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc readValue*[T: SimpleHexEncodedTypes](r: var JsonReader, value: var T) {.
|
|
|
|
raises: [SerializationError, IOError, Defect].} =
|
2020-08-20 13:01:08 +00:00
|
|
|
value = T ncrutils.fromHex(r.readValue(string))
|
|
|
|
if len(seq[byte](value)) == 0:
|
2020-08-02 17:26:57 +00:00
|
|
|
r.raiseUnexpectedValue("Valid hex string expected")
|
|
|
|
|
|
|
|
proc readValue*(r: var JsonReader, value: var Kdf)
|
|
|
|
{.raises: [SerializationError, IOError, Defect].} =
|
|
|
|
var
|
|
|
|
functionSpecified = false
|
|
|
|
paramsSpecified = false
|
|
|
|
|
|
|
|
for fieldName in readObjectFields(r):
|
|
|
|
case fieldName
|
|
|
|
of "function":
|
|
|
|
value.function = r.readValue(KdfKind)
|
|
|
|
functionSpecified = true
|
|
|
|
|
|
|
|
of "params":
|
|
|
|
if functionSpecified:
|
|
|
|
case value.function
|
|
|
|
of kdfPbkdf2:
|
|
|
|
r.readValue(value.pbkdf2Params)
|
|
|
|
of kdfScrypt:
|
|
|
|
r.readValue(value.scryptParams)
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The 'params' field must be specified after the 'function' field")
|
|
|
|
paramsSpecified = true
|
|
|
|
|
|
|
|
of "message":
|
|
|
|
r.readValue(value.message)
|
|
|
|
|
|
|
|
else:
|
|
|
|
r.raiseUnexpectedField(fieldName, "Kdf")
|
|
|
|
|
|
|
|
if not (functionSpecified and paramsSpecified):
|
|
|
|
r.raiseUnexpectedValue(
|
|
|
|
"The Kdf value should have sub-fields named 'function' and 'params'")
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
# HttpHostUri
|
|
|
|
proc readValue*(reader: var JsonReader, value: var HttpHostUri) {.
|
|
|
|
raises: [IOError, SerializationError, Defect].} =
|
|
|
|
let svalue = reader.readValue(string)
|
|
|
|
let res = parseUri(svalue)
|
|
|
|
if res.scheme != "http" and res.scheme != "https":
|
|
|
|
reader.raiseUnexpectedValue("Incorrect URL scheme")
|
|
|
|
if len(res.hostname) == 0:
|
|
|
|
reader.raiseUnexpectedValue("Missing URL hostname")
|
|
|
|
value = HttpHostUri(res)
|
|
|
|
|
|
|
|
proc writeValue*(writer: var JsonWriter, value: HttpHostUri) {.
|
|
|
|
raises: [IOError, Defect].} =
|
|
|
|
writer.writeValue($distinctBase(value))
|
|
|
|
|
|
|
|
# RemoteKeystore
|
2022-05-10 00:32:12 +00:00
|
|
|
proc writeValue*(writer: var JsonWriter, value: RemoteKeystore)
|
|
|
|
{.raises: [IOError, Defect].} =
|
2022-02-07 20:36:09 +00:00
|
|
|
writer.beginRecord()
|
|
|
|
writer.writeField("version", value.version)
|
|
|
|
writer.writeField("pubkey", "0x" & value.pubkey.toHex())
|
2022-05-10 00:32:12 +00:00
|
|
|
writer.writeField("remotes", value.remotes)
|
|
|
|
writer.writeField("threshold", value.threshold)
|
2022-02-07 20:36:09 +00:00
|
|
|
case value.remoteType
|
|
|
|
of RemoteSignerType.Web3Signer:
|
|
|
|
writer.writeField("type", "web3signer")
|
|
|
|
if value.description.isSome():
|
|
|
|
writer.writeField("description", value.description.get())
|
|
|
|
if RemoteKeystoreFlag.IgnoreSSLVerification in value.flags:
|
|
|
|
writer.writeField("ignore_ssl_verification", true)
|
|
|
|
writer.endRecord()
|
|
|
|
|
|
|
|
template writeValue*(w: var JsonWriter,
|
|
|
|
value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) =
|
|
|
|
writeJsonHexString(w.stream, distinctBase value)
|
|
|
|
|
|
|
|
proc readValue*(reader: var JsonReader, value: var RemoteKeystore)
|
2021-11-30 01:20:21 +00:00
|
|
|
{.raises: [SerializationError, IOError, Defect].} =
|
|
|
|
var
|
2022-02-07 20:36:09 +00:00
|
|
|
version: Option[uint64]
|
2021-11-30 01:20:21 +00:00
|
|
|
description: Option[string]
|
2022-02-07 20:36:09 +00:00
|
|
|
remote: Option[HttpHostUri]
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes: Option[seq[RemoteSignerInfo]]
|
2021-11-30 01:20:21 +00:00
|
|
|
remoteType: Option[string]
|
|
|
|
ignoreSslVerification: Option[bool]
|
|
|
|
pubkey: Option[ValidatorPubKey]
|
2022-05-10 00:32:12 +00:00
|
|
|
threshold: Option[uint32]
|
|
|
|
implicitVersion1 = false
|
2022-02-07 20:36:09 +00:00
|
|
|
|
2022-05-10 00:32:12 +00:00
|
|
|
# TODO: implementing deserializers for versioned objects
|
|
|
|
# manually is extremely error-prone. This should use
|
|
|
|
# the auto-generated deserializer from nim-json-serialization
|
2022-02-07 20:36:09 +00:00
|
|
|
for fieldName in readObjectFields(reader):
|
2021-11-30 01:20:21 +00:00
|
|
|
case fieldName:
|
|
|
|
of "pubkey":
|
|
|
|
if pubkey.isSome():
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `pubkey` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
pubkey = some(reader.readValue(ValidatorPubKey))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "remote":
|
2022-05-10 00:32:12 +00:00
|
|
|
if version.isSome and version.get > 1:
|
|
|
|
reader.raiseUnexpectedField(
|
|
|
|
"The `remote` field is valid only in version 1 of the remote keystore format",
|
|
|
|
"RemoteKeystore")
|
|
|
|
|
2021-11-30 01:20:21 +00:00
|
|
|
if remote.isSome():
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `remote` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
remote = some(reader.readValue(HttpHostUri))
|
2022-05-10 00:32:12 +00:00
|
|
|
implicitVersion1 = true
|
|
|
|
of "remotes":
|
|
|
|
if remotes.isSome():
|
|
|
|
reader.raiseUnexpectedField("Multiple `remote` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
remotes = some(reader.readValue(seq[RemoteSignerInfo]))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "version":
|
2022-02-07 20:36:09 +00:00
|
|
|
if version.isSome():
|
|
|
|
reader.raiseUnexpectedField("Multiple `version` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
version = some(reader.readValue(uint64))
|
2022-05-10 00:32:12 +00:00
|
|
|
if implicitVersion1 and version.get > 1'u64:
|
|
|
|
reader.raiseUnexpectedValue(
|
|
|
|
"Remote keystore format doesn't match the specified version number")
|
|
|
|
if version.get > 2'u64:
|
|
|
|
reader.raiseUnexpectedValue(
|
|
|
|
"Remote keystore version " & $version.get &
|
|
|
|
" requires a more recent version of Nimbus")
|
2021-11-30 01:20:21 +00:00
|
|
|
of "description":
|
2022-02-07 20:36:09 +00:00
|
|
|
let res = reader.readValue(string)
|
|
|
|
if description.isSome():
|
|
|
|
description = some(description.get() & "\n" & res)
|
2021-11-30 01:20:21 +00:00
|
|
|
else:
|
2022-02-07 20:36:09 +00:00
|
|
|
description = some(res)
|
2021-11-30 01:20:21 +00:00
|
|
|
of "ignore_ssl_verification":
|
|
|
|
if ignoreSslVerification.isSome():
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple conflicting options found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
ignoreSslVerification = some(reader.readValue(bool))
|
2021-11-30 01:20:21 +00:00
|
|
|
of "type":
|
|
|
|
if remoteType.isSome():
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedField("Multiple `type` fields found",
|
2022-05-10 00:32:12 +00:00
|
|
|
"RemoteKeystore")
|
2022-02-07 20:36:09 +00:00
|
|
|
remoteType = some(reader.readValue(string))
|
2022-05-10 00:32:12 +00:00
|
|
|
of "threshold":
|
|
|
|
if threshold.isSome():
|
|
|
|
reader.raiseUnexpectedField("Multiple `threshold` fields found",
|
|
|
|
"RemoteKeystore")
|
|
|
|
threshold = some(reader.readValue(uint32))
|
2021-11-30 01:20:21 +00:00
|
|
|
else:
|
|
|
|
# Ignore unknown field names.
|
|
|
|
discard
|
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
if version.isNone():
|
|
|
|
reader.raiseUnexpectedValue("Field `version` is missing")
|
2022-05-10 00:32:12 +00:00
|
|
|
if remotes.isNone():
|
|
|
|
if remote.isSome and pubkey.isSome:
|
|
|
|
remotes = some @[RemoteSignerInfo(
|
|
|
|
pubkey: pubkey.get,
|
|
|
|
id: 0,
|
|
|
|
url: remote.get
|
|
|
|
)]
|
|
|
|
else:
|
|
|
|
reader.raiseUnexpectedValue("Field `remotes` is missing")
|
2021-11-30 01:20:21 +00:00
|
|
|
if pubkey.isNone():
|
2022-02-07 20:36:09 +00:00
|
|
|
reader.raiseUnexpectedValue("Field `pubkey` is missing")
|
|
|
|
|
|
|
|
let keystoreType =
|
|
|
|
if remoteType.isSome():
|
|
|
|
let res = remoteType.get()
|
|
|
|
case res.toLowerAscii()
|
|
|
|
of "web3signer":
|
|
|
|
RemoteSignerType.Web3Signer
|
|
|
|
else:
|
|
|
|
reader.raiseUnexpectedValue("Unsupported remote signer `type` value")
|
|
|
|
else:
|
|
|
|
RemoteSignerType.Web3Signer
|
|
|
|
|
|
|
|
let keystoreFlags =
|
|
|
|
block:
|
|
|
|
var res: set[RemoteKeystoreFlag]
|
|
|
|
if ignoreSslVerification.isSome():
|
|
|
|
res.incl(RemoteKeystoreFlag.IgnoreSSLVerification)
|
|
|
|
res
|
|
|
|
|
|
|
|
value = RemoteKeystore(
|
2022-05-10 00:32:12 +00:00
|
|
|
version: 2'u64,
|
|
|
|
pubkey: pubkey.get,
|
2022-02-07 20:36:09 +00:00
|
|
|
description: description,
|
|
|
|
remoteType: keystoreType,
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes: remotes.get,
|
|
|
|
threshold: threshold.get(1),
|
2022-02-07 20:36:09 +00:00
|
|
|
)
|
2021-11-30 01:20:21 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
template writeValue*(w: var JsonWriter,
|
|
|
|
value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv) =
|
|
|
|
writeJsonHexString(w.stream, distinctBase value)
|
|
|
|
|
|
|
|
template bytes(value: Pbkdf2Salt|SimpleHexEncodedTypes|Aes128CtrIv): seq[byte] =
|
|
|
|
distinctBase value
|
|
|
|
|
2020-08-02 18:46:12 +00:00
|
|
|
func scrypt(password: openArray[char], salt: openArray[byte],
|
2021-12-22 12:37:31 +00:00
|
|
|
N, r, p: int; keyLen: static[int]): array[keyLen, byte] =
|
2020-08-02 18:46:12 +00:00
|
|
|
let (xyvLen, bLen) = scryptCalc(N, r, p)
|
|
|
|
var xyv = newSeq[uint32](xyvLen)
|
|
|
|
var b = newSeq[byte](bLen)
|
|
|
|
discard scrypt(password, salt, N, r, p, xyv, b, result)
|
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
func areValid(params: Pbkdf2Params): bool =
|
2022-04-08 16:22:49 +00:00
|
|
|
if params.c == 0 or params.dklen < 32 or params.salt.bytes.len == 0:
|
2020-10-09 16:41:53 +00:00
|
|
|
return false
|
|
|
|
|
2021-12-15 18:55:11 +00:00
|
|
|
# https://www.ietf.org/rfc/rfc2898.txt
|
2020-10-09 16:41:53 +00:00
|
|
|
let hLen = case params.prf
|
|
|
|
of HmacSha256: 256 / 8
|
|
|
|
params.dklen <= high(uint32).uint64 * hLen.uint64
|
|
|
|
|
|
|
|
func areValid(params: ScryptParams): bool =
|
2021-12-15 18:55:11 +00:00
|
|
|
static: doAssert scryptParams.dklen >= 32
|
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
params.dklen == scryptParams.dklen and
|
|
|
|
params.n == scryptParams.n and
|
|
|
|
params.r == scryptParams.r and
|
|
|
|
params.p == scryptParams.p and
|
|
|
|
params.salt.bytes.len > 0
|
|
|
|
|
|
|
|
proc decryptCryptoField*(crypto: Crypto,
|
|
|
|
password: KeystorePass,
|
|
|
|
outSecret: var seq[byte]): DecryptionStatus =
|
2021-12-15 18:55:11 +00:00
|
|
|
# https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition
|
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
if crypto.cipher.message.bytes.len == 0:
|
|
|
|
return InvalidKeystore
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
let decKey = case crypto.kdf.function
|
|
|
|
of kdfPbkdf2:
|
|
|
|
template params: auto = crypto.kdf.pbkdf2Params
|
2020-10-09 16:41:53 +00:00
|
|
|
if not params.areValid or params.c > high(int).uint64:
|
|
|
|
return InvalidKeystore
|
2022-11-09 22:05:50 +00:00
|
|
|
Eth2DigestCtx.pbkdf2(
|
|
|
|
password.str,
|
|
|
|
params.salt.bytes,
|
|
|
|
int params.c,
|
|
|
|
int params.dklen)
|
2020-08-02 17:26:57 +00:00
|
|
|
of kdfScrypt:
|
2020-08-02 18:46:12 +00:00
|
|
|
template params: auto = crypto.kdf.scryptParams
|
2020-10-09 16:41:53 +00:00
|
|
|
if not params.areValid:
|
|
|
|
return InvalidKeystore
|
2020-10-02 15:46:05 +00:00
|
|
|
@(scrypt(password.str,
|
2020-08-02 18:46:12 +00:00
|
|
|
params.salt.bytes,
|
|
|
|
scryptParams.n,
|
|
|
|
scryptParams.r,
|
|
|
|
scryptParams.p,
|
2020-10-09 16:41:53 +00:00
|
|
|
int scryptParams.dklen))
|
2020-08-02 17:26:57 +00:00
|
|
|
|
|
|
|
let derivedChecksum = shaChecksum(decKey.toOpenArray(16, 31),
|
|
|
|
crypto.cipher.message.bytes)
|
|
|
|
if derivedChecksum != crypto.checksum.message:
|
2020-10-09 16:41:53 +00:00
|
|
|
return InvalidPassword
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
var aesCipher: CTR[aes128]
|
|
|
|
outSecret.setLen(crypto.cipher.message.bytes.len)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
aesCipher.init(decKey.toOpenArray(0, 15), crypto.cipher.params.iv.bytes)
|
2020-10-09 16:41:53 +00:00
|
|
|
aesCipher.decrypt(crypto.cipher.message.bytes, outSecret)
|
2020-05-19 17:30:28 +00:00
|
|
|
aesCipher.clear()
|
|
|
|
|
2020-10-09 16:41:53 +00:00
|
|
|
return Success
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
func cstringToStr(v: cstring): string = $v
|
|
|
|
|
2022-08-19 10:30:07 +00:00
|
|
|
template parseKeystore*(jsonContent: string): Keystore =
|
|
|
|
Json.decode(jsonContent, Keystore,
|
|
|
|
requireAllFields = true,
|
|
|
|
allowUnknownFields = true)
|
|
|
|
|
|
|
|
template parseNetKeystore*(jsonContent: string): NetKeystore =
|
|
|
|
Json.decode(jsonContent, NetKeystore,
|
|
|
|
requireAllFields = true,
|
|
|
|
allowUnknownFields = true)
|
|
|
|
|
|
|
|
template parseRemoteKeystore*(jsonContent: string): RemoteKeystore =
|
|
|
|
Json.decode(jsonContent, RemoteKeystore,
|
|
|
|
requireAllFields = false,
|
|
|
|
allowUnknownFields = true)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc decryptKeystore*(keystore: Keystore,
|
|
|
|
password: KeystorePass): KsResult[ValidatorPrivKey] =
|
2020-10-09 16:41:53 +00:00
|
|
|
var secret: seq[byte]
|
|
|
|
defer: burnMem(secret)
|
|
|
|
let status = decryptCryptoField(keystore.crypto, password, secret)
|
|
|
|
case status
|
|
|
|
of Success:
|
|
|
|
ValidatorPrivKey.fromRaw(secret).mapErr(cstringToStr)
|
|
|
|
else:
|
|
|
|
err $status
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc decryptKeystore*(keystore: JsonString,
|
|
|
|
password: KeystorePass): KsResult[ValidatorPrivKey] =
|
2022-08-19 10:30:07 +00:00
|
|
|
let keystore = try: parseKeystore(string keystore)
|
2020-08-02 17:26:57 +00:00
|
|
|
except SerializationError as e:
|
|
|
|
return err e.formatMsg("<keystore>")
|
|
|
|
decryptKeystore(keystore, password)
|
2020-06-23 19:11:07 +00:00
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
proc writeValue*(writer: var JsonWriter, value: lcrypto.PublicKey) {.
|
|
|
|
inline, raises: [IOError, Defect].} =
|
|
|
|
writer.writeValue(ncrutils.toHex(value.getBytes().get(),
|
|
|
|
{HexFlags.LowerCase}))
|
|
|
|
|
|
|
|
proc readValue*(reader: var JsonReader, value: var lcrypto.PublicKey) {.
|
|
|
|
raises: [SerializationError, IOError, Defect].} =
|
|
|
|
let res = init(lcrypto.PublicKey, reader.readValue(string))
|
|
|
|
if res.isOk():
|
|
|
|
value = res.get()
|
|
|
|
else:
|
|
|
|
# TODO: Can we provide better diagnostic?
|
|
|
|
raiseUnexpectedValue(reader, "Valid hex-encoded public key expected")
|
|
|
|
|
|
|
|
proc decryptNetKeystore*(nkeystore: NetKeystore,
|
|
|
|
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
|
2020-10-09 16:41:53 +00:00
|
|
|
var secret: seq[byte]
|
|
|
|
defer: burnMem(secret)
|
|
|
|
let status = decryptCryptoField(nkeystore.crypto, password, secret)
|
|
|
|
case status
|
|
|
|
of Success:
|
|
|
|
let res = lcrypto.PrivateKey.init(secret)
|
|
|
|
if res.isOk:
|
|
|
|
ok res.get
|
2020-08-20 13:01:08 +00:00
|
|
|
else:
|
2020-10-09 16:41:53 +00:00
|
|
|
err "Invalid key"
|
2020-08-20 13:01:08 +00:00
|
|
|
else:
|
2020-10-09 16:41:53 +00:00
|
|
|
err $status
|
2020-08-20 13:01:08 +00:00
|
|
|
|
|
|
|
proc decryptNetKeystore*(nkeystore: JsonString,
|
|
|
|
password: KeystorePass): KsResult[lcrypto.PrivateKey] =
|
|
|
|
try:
|
2022-08-19 10:30:07 +00:00
|
|
|
let keystore = parseNetKeystore(string nkeystore)
|
2020-08-20 13:01:08 +00:00
|
|
|
return decryptNetKeystore(keystore, password)
|
|
|
|
except SerializationError as exc:
|
|
|
|
return err(exc.formatMsg("<keystore>"))
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc createCryptoField(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-10-28 18:35:31 +00:00
|
|
|
secret: openArray[byte],
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
2021-12-22 12:37:31 +00:00
|
|
|
iv: openArray[byte] = @[],
|
|
|
|
mode = Secure): Crypto =
|
2020-06-23 19:11:07 +00:00
|
|
|
type AES = aes128
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 18:46:12 +00:00
|
|
|
let kdfSalt =
|
2020-08-02 17:26:57 +00:00
|
|
|
if salt.len > 0:
|
|
|
|
doAssert salt.len == keyLen
|
|
|
|
@salt
|
|
|
|
else:
|
2022-06-21 08:29:16 +00:00
|
|
|
rng.generateBytes(keyLen)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
let aesIv = if iv.len > 0:
|
|
|
|
doAssert iv.len == AES.sizeBlock
|
|
|
|
@iv
|
2020-06-01 19:48:20 +00:00
|
|
|
else:
|
2022-06-21 08:29:16 +00:00
|
|
|
rng.generateBytes(AES.sizeBlock)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 18:46:12 +00:00
|
|
|
var decKey: seq[byte]
|
2020-08-02 17:26:57 +00:00
|
|
|
let kdf = case kdfKind
|
|
|
|
of kdfPbkdf2:
|
|
|
|
var params = pbkdf2Params
|
2020-08-02 18:46:12 +00:00
|
|
|
params.salt = Pbkdf2Salt kdfSalt
|
2021-12-22 12:37:31 +00:00
|
|
|
if mode == Fast: params.c = 1
|
|
|
|
decKey = sha256.pbkdf2(password.str,
|
|
|
|
kdfSalt,
|
|
|
|
int params.c,
|
|
|
|
int params.dklen)
|
2020-08-02 17:26:57 +00:00
|
|
|
Kdf(function: kdfPbkdf2, pbkdf2Params: params, message: "")
|
|
|
|
of kdfScrypt:
|
2020-08-02 18:46:12 +00:00
|
|
|
var params = scryptParams
|
|
|
|
params.salt = ScryptSalt kdfSalt
|
2021-12-22 12:37:31 +00:00
|
|
|
if mode == Fast: params.n = 1
|
|
|
|
decKey = @(scrypt(password.str, kdfSalt,
|
|
|
|
params.n, params.r, params.p, keyLen))
|
2020-08-02 18:46:12 +00:00
|
|
|
Kdf(function: kdfScrypt, scryptParams: params, message: "")
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
var
|
|
|
|
aesCipher: CTR[AES]
|
|
|
|
cipherMsg = newSeq[byte](secret.len)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-05-20 07:22:21 +00:00
|
|
|
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
|
2020-05-19 17:30:28 +00:00
|
|
|
aesCipher.encrypt(secret, cipherMsg)
|
|
|
|
aesCipher.clear()
|
|
|
|
|
2020-06-23 19:11:07 +00:00
|
|
|
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
|
2020-05-19 17:30:28 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Crypto(
|
2020-06-23 19:11:07 +00:00
|
|
|
kdf: kdf,
|
|
|
|
checksum: Checksum(
|
2020-08-02 17:26:57 +00:00
|
|
|
function: sha256Checksum,
|
|
|
|
message: sum),
|
2020-06-23 19:11:07 +00:00
|
|
|
cipher: Cipher(
|
2020-08-02 17:26:57 +00:00
|
|
|
function: aes128CtrCipher,
|
|
|
|
params: Aes128CtrParams(iv: Aes128CtrIv aesIv),
|
|
|
|
message: CipherBytes cipherMsg))
|
|
|
|
|
2020-08-20 13:01:08 +00:00
|
|
|
proc createNetKeystore*(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-08-20 13:01:08 +00:00
|
|
|
privKey: lcrypto.PrivateKey,
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-08-20 13:01:08 +00:00
|
|
|
description = "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
|
|
|
iv: openArray[byte] = @[]): NetKeystore =
|
2020-08-20 13:01:08 +00:00
|
|
|
let
|
|
|
|
secret = privKey.getBytes().get()
|
|
|
|
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv)
|
2021-12-22 12:37:31 +00:00
|
|
|
pubkey = privKey.getPublicKey().get()
|
2020-08-20 13:01:08 +00:00
|
|
|
uuid = uuidGenerate().expect("Random bytes should be available")
|
|
|
|
|
|
|
|
NetKeystore(
|
|
|
|
crypto: cryptoField,
|
2021-12-22 12:37:31 +00:00
|
|
|
pubkey: pubkey,
|
2022-08-19 10:30:07 +00:00
|
|
|
description: if len(description) > 0: some(description)
|
|
|
|
else: none[string](),
|
2020-08-20 13:01:08 +00:00
|
|
|
uuid: $uuid,
|
2020-09-30 11:04:54 +00:00
|
|
|
version: 1
|
2020-08-20 13:01:08 +00:00
|
|
|
)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc createKeystore*(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2022-04-08 16:22:49 +00:00
|
|
|
privKey: ValidatorPrivKey,
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-08-02 17:26:57 +00:00
|
|
|
path = KeyPath "",
|
2020-08-02 17:54:48 +00:00
|
|
|
description = "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
2021-12-22 12:37:31 +00:00
|
|
|
iv: openArray[byte] = @[],
|
|
|
|
mode = Secure): Keystore =
|
2020-06-23 19:11:07 +00:00
|
|
|
let
|
|
|
|
secret = privKey.toRaw[^32..^1]
|
2021-12-22 12:37:31 +00:00
|
|
|
cryptoField = createCryptoField(kdfKind, rng, secret, password, salt, iv, mode)
|
2020-06-23 19:11:07 +00:00
|
|
|
pubkey = privKey.toPubKey()
|
|
|
|
uuid = uuidGenerate().expect("Random bytes should be available")
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
Keystore(
|
|
|
|
crypto: cryptoField,
|
2021-06-01 11:13:40 +00:00
|
|
|
pubkey: pubkey.toPubKey(),
|
2020-08-02 17:26:57 +00:00
|
|
|
path: path,
|
2022-08-19 10:30:07 +00:00
|
|
|
description: if len(description) > 0: some(description)
|
|
|
|
else: none[string](),
|
2020-08-02 17:26:57 +00:00
|
|
|
uuid: $uuid,
|
|
|
|
version: 4)
|
2020-06-01 19:48:20 +00:00
|
|
|
|
2022-02-07 20:36:09 +00:00
|
|
|
proc createRemoteKeystore*(pubKey: ValidatorPubKey, remoteUri: HttpHostUri,
|
|
|
|
version = 1'u64, description = "",
|
|
|
|
remoteType = RemoteSignerType.Web3Signer,
|
|
|
|
flags: set[RemoteKeystoreFlag] = {}): RemoteKeystore =
|
2022-05-10 00:32:12 +00:00
|
|
|
let signerInfo = RemoteSignerInfo(
|
|
|
|
url: remoteUri,
|
|
|
|
pubkey: pubKey,
|
|
|
|
id: 0
|
|
|
|
)
|
2022-02-07 20:36:09 +00:00
|
|
|
RemoteKeystore(
|
|
|
|
version: version,
|
|
|
|
description: if len(description) > 0: some(description)
|
|
|
|
else: none[string](),
|
|
|
|
remoteType: remoteType,
|
|
|
|
pubkey: pubKey,
|
2022-05-10 00:32:12 +00:00
|
|
|
remotes: @[signerInfo],
|
2022-02-07 20:36:09 +00:00
|
|
|
flags: flags
|
|
|
|
)
|
|
|
|
|
2020-08-02 17:26:57 +00:00
|
|
|
proc createWallet*(kdfKind: KdfKind,
|
2022-06-21 08:29:16 +00:00
|
|
|
rng: var HmacDrbgContext,
|
2020-10-19 19:02:48 +00:00
|
|
|
seed: KeySeed,
|
2020-06-23 19:11:07 +00:00
|
|
|
name = WalletName "",
|
2020-10-28 18:35:31 +00:00
|
|
|
salt: openArray[byte] = @[],
|
|
|
|
iv: openArray[byte] = @[],
|
2020-10-02 15:46:05 +00:00
|
|
|
password = KeystorePass.init "",
|
2020-06-23 19:11:07 +00:00
|
|
|
nextAccount = none(Natural),
|
|
|
|
pretty = true): Wallet =
|
|
|
|
let
|
|
|
|
uuid = UUID $(uuidGenerate().expect("Random bytes should be available"))
|
2020-08-02 17:26:57 +00:00
|
|
|
crypto = createCryptoField(kdfKind, rng, distinctBase seed,
|
|
|
|
password, salt, iv)
|
2020-06-23 19:11:07 +00:00
|
|
|
Wallet(
|
|
|
|
uuid: uuid,
|
|
|
|
name: if name.string.len > 0: name
|
|
|
|
else: WalletName(uuid),
|
|
|
|
version: 1,
|
|
|
|
walletType: "hierarchical deterministic",
|
2020-08-02 17:26:57 +00:00
|
|
|
crypto: crypto,
|
2020-06-23 19:11:07 +00:00
|
|
|
nextAccount: nextAccount.get(0))
|
|
|
|
|
2022-11-24 19:07:02 +00:00
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v1.3.0-alpha.1/specs/phase0/validator.md#bls_withdrawal_prefix
|
2021-06-01 11:13:40 +00:00
|
|
|
func makeWithdrawalCredentials*(k: ValidatorPubKey): Eth2Digest =
|
2020-06-16 12:16:43 +00:00
|
|
|
var bytes = eth2digest(k.toRaw())
|
2020-06-01 19:48:20 +00:00
|
|
|
bytes.data[0] = BLS_WITHDRAWAL_PREFIX.uint8
|
|
|
|
bytes
|
|
|
|
|
2021-08-20 23:37:45 +00:00
|
|
|
# https://github.com/ethereum/consensus-specs/blob/v0.12.2/specs/phase0/deposit-contract.md#withdrawal-credentials
|
2021-06-01 11:13:40 +00:00
|
|
|
proc makeWithdrawalCredentials*(k: CookedPubKey): Eth2Digest =
|
|
|
|
makeWithdrawalCredentials(k.toPubKey())
|
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
proc prepareDeposit*(cfg: RuntimeConfig,
|
2021-06-01 11:13:40 +00:00
|
|
|
withdrawalPubKey: CookedPubKey,
|
|
|
|
signingKey: ValidatorPrivKey, signingPubKey: CookedPubKey,
|
2020-07-17 20:59:50 +00:00
|
|
|
amount = MAX_EFFECTIVE_BALANCE.Gwei): DepositData =
|
|
|
|
var res = DepositData(
|
|
|
|
amount: amount,
|
2021-06-01 11:13:40 +00:00
|
|
|
pubkey: signingPubKey.toPubKey(),
|
2020-07-17 20:59:50 +00:00
|
|
|
withdrawal_credentials: makeWithdrawalCredentials(withdrawalPubKey))
|
|
|
|
|
Implement split preset/config support (#2710)
* Implement split preset/config support
This is the initial bulk refactor to introduce runtime config values in
a number of places, somewhat replacing the existing mechanism of loading
network metadata.
It still needs more work, this is the initial refactor that introduces
runtime configuration in some of the places that need it.
The PR changes the way presets and constants work, to match the spec. In
particular, a "preset" now refers to the compile-time configuration
while a "cfg" or "RuntimeConfig" is the dynamic part.
A single binary can support either mainnet or minimal, but not both.
Support for other presets has been removed completely (can be readded,
in case there's need).
There's a number of outstanding tasks:
* `SECONDS_PER_SLOT` still needs fixing
* loading custom runtime configs needs redoing
* checking constants against YAML file
* yeerongpilly support
`build/nimbus_beacon_node --network=yeerongpilly --discv5:no --log-level=DEBUG`
* load fork epoch from config
* fix fork digest sent in status
* nicer error string for request failures
* fix tools
* one more
* fixup
* fixup
* fixup
* use "standard" network definition folder in local testnet
Files are loaded from their standard locations, including genesis etc,
to conform to the format used in the `eth2-networks` repo.
* fix launch scripts, allow unknown config values
* fix base config of rest test
* cleanups
* bundle mainnet config using common loader
* fix spec links and names
* only include supported preset in binary
* drop yeerongpilly, add altair-devnet-0, support boot_enr.yaml
2021-07-12 13:01:38 +00:00
|
|
|
res.signature = get_deposit_signature(cfg, res, signingKey).toValidatorSig()
|
2020-07-17 20:59:50 +00:00
|
|
|
return res
|