mirror of
https://github.com/status-im/nimbus-eth2.git
synced 2025-01-11 14:54:12 +00:00
parent
e33c8d9067
commit
8496e20a78
203
beacon_chain/spec/keystore.nim
Normal file
203
beacon_chain/spec/keystore.nim
Normal file
@ -0,0 +1,203 @@
|
||||
# beacon_chain
|
||||
# Copyright (c) 2018-2020 Status Research & Development GmbH
|
||||
# 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.
|
||||
|
||||
import
|
||||
json, math,
|
||||
stew/results,
|
||||
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand, utils],
|
||||
./crypto
|
||||
|
||||
import strutils except fromHex
|
||||
|
||||
export results
|
||||
|
||||
type
|
||||
ChecksumParams = object
|
||||
|
||||
Checksum = object
|
||||
function: string
|
||||
params: ChecksumParams
|
||||
message: string
|
||||
|
||||
CipherParams = object
|
||||
iv: string
|
||||
|
||||
Cipher = object
|
||||
function: string
|
||||
params: CipherParams
|
||||
message: string
|
||||
|
||||
KdfScrypt* = object
|
||||
dklen: int
|
||||
n, p, r: int
|
||||
salt: string
|
||||
|
||||
KdfPbkdf2* = object
|
||||
dklen: int
|
||||
c: int
|
||||
prf: string
|
||||
salt: string
|
||||
|
||||
KdfParams = KdfPbkdf2 | KdfScrypt
|
||||
|
||||
Kdf[T: KdfParams] = object
|
||||
function: string
|
||||
params: T
|
||||
message: string
|
||||
|
||||
Crypto[T: KdfParams] = object
|
||||
kdf: Kdf[T]
|
||||
checksum: Checksum
|
||||
cipher: Cipher
|
||||
|
||||
Keystore[T: KdfParams] = object
|
||||
crypto: Crypto[T]
|
||||
pubkey: string
|
||||
path: string
|
||||
uuid: string
|
||||
version: int
|
||||
|
||||
KsResult*[T] = Result[T, cstring]
|
||||
|
||||
const
|
||||
scryptParams = KdfScrypt(
|
||||
dklen: 32,
|
||||
n: 2^18,
|
||||
r: 1,
|
||||
p: 8
|
||||
)
|
||||
|
||||
pbkdf2Params = KdfPbkdf2(
|
||||
dklen: 32,
|
||||
c: 2^18,
|
||||
prf: "hmac-sha256"
|
||||
)
|
||||
|
||||
template shaChecksum(key, cipher: openarray[byte]): untyped =
|
||||
var ctx: sha256
|
||||
ctx.init()
|
||||
ctx.update(key)
|
||||
ctx.update(cipher)
|
||||
ctx.finish().data
|
||||
|
||||
proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] =
|
||||
var ks: JsonNode
|
||||
try:
|
||||
ks = parseJson(data)
|
||||
except JsonParsingError:
|
||||
return err "ks: failed to parse keystore"
|
||||
|
||||
var
|
||||
decKey: seq[byte]
|
||||
salt: seq[byte]
|
||||
iv: seq[byte]
|
||||
cipherMsg: seq[byte]
|
||||
checksumMsg: seq[byte]
|
||||
|
||||
let kdf = ks{"crypto", "kdf", "function"}.getStr
|
||||
|
||||
case kdf
|
||||
of "scrypt":
|
||||
let crypto = ks{"crypto"}.to(Crypto[KdfScrypt])
|
||||
return err "ks: scrypt not supported"
|
||||
of "pbkdf2":
|
||||
let
|
||||
crypto = ks{"crypto"}.to(Crypto[KdfPbkdf2])
|
||||
kdfParams = crypto.kdf.params
|
||||
|
||||
salt = fromHex(kdfParams.salt)
|
||||
decKey = sha256.pbkdf2(passphrase, salt, kdfParams.c, kdfParams.dklen)
|
||||
iv = fromHex(crypto.cipher.params.iv)
|
||||
cipherMsg = fromHex(crypto.cipher.message)
|
||||
checksumMsg = fromHex(crypto.checksum.message)
|
||||
else:
|
||||
return err "ks: unknown cipher"
|
||||
|
||||
if decKey.len < 32:
|
||||
return err "ks: decryption key must be at least 32 bytes"
|
||||
|
||||
let sum = shaChecksum(decKey[16..<32], cipherMsg)
|
||||
if sum != checksumMsg:
|
||||
return err "ks: invalid checksum"
|
||||
|
||||
var
|
||||
aesCipher: CTR[aes128]
|
||||
secret = newSeq[byte](cipherMsg.len)
|
||||
|
||||
aesCipher.init(decKey[0..<16], iv)
|
||||
aesCipher.decrypt(cipherMsg, secret)
|
||||
aesCipher.clear()
|
||||
|
||||
result = ok secret
|
||||
|
||||
proc encryptKeystore*[T: KdfParams](secret: openarray[byte];
|
||||
passphrase: string;
|
||||
path="";
|
||||
salt: openarray[byte] = @[];
|
||||
iv: openarray[byte] = @[];
|
||||
ugly=true): KsResult[string] =
|
||||
var
|
||||
decKey: seq[byte]
|
||||
aesCipher: CTR[aes128]
|
||||
aesIv = newSeq[byte](16)
|
||||
kdfSalt = newSeq[byte](32)
|
||||
cipherMsg = newSeq[byte](secret.len)
|
||||
|
||||
if salt.len == 32:
|
||||
kdfSalt = @salt
|
||||
elif salt.len > 0:
|
||||
return err "ks: invalid salt"
|
||||
elif randomBytes(kdfSalt) != 32:
|
||||
return err "ks: no random bytes for salt"
|
||||
|
||||
if iv.len == 16:
|
||||
aesIv = @iv
|
||||
elif iv.len > 0:
|
||||
return err "ks: invalid iv"
|
||||
elif randomBytes(aesIv) != 16:
|
||||
return err "ks: no random bytes for iv"
|
||||
|
||||
when T is KdfPbkdf2:
|
||||
decKey = sha256.pbkdf2(passphrase, kdfSalt, pbkdf2Params.c,
|
||||
pbkdf2Params.dklen)
|
||||
|
||||
var kdf = Kdf[KdfPbkdf2](function: "pbkdf2", params: pbkdf2Params, message: "")
|
||||
kdf.params.salt = kdfSalt.toHex(lowercase=true)
|
||||
else:
|
||||
return
|
||||
|
||||
aesCipher.init(decKey[0..<16], aesIv)
|
||||
aesCipher.encrypt(secret, cipherMsg)
|
||||
aesCipher.clear()
|
||||
|
||||
let
|
||||
privkey = ValidatorPrivkey.fromRaw(secret)
|
||||
pubkey = privkey.tryGet().toPubKey()
|
||||
|
||||
sum = shaChecksum(decKey[16..<32], cipherMsg)
|
||||
|
||||
keystore = Keystore[T](
|
||||
crypto: Crypto[T](
|
||||
kdf: kdf,
|
||||
checksum: Checksum(
|
||||
function: "sha256",
|
||||
message: sum.toHex(lowercase=true)
|
||||
),
|
||||
cipher: Cipher(
|
||||
function: "aes-128-ctr",
|
||||
params: CipherParams(iv: aesIv.toHex(lowercase=true)),
|
||||
message: cipherMsg.toHex(lowercase=true)
|
||||
)
|
||||
),
|
||||
pubkey: pubkey.toHex(),
|
||||
path: path,
|
||||
uuid: "", # TODO: uuid library?
|
||||
version: 4
|
||||
)
|
||||
|
||||
result = ok(if ugly: $(%keystore)
|
||||
else: pretty(%keystore, indent=4))
|
@ -17,6 +17,7 @@ import # Unit test
|
||||
./test_beaconstate,
|
||||
./test_block_pool,
|
||||
./test_helpers,
|
||||
./test_keystore,
|
||||
./test_mocking,
|
||||
./test_mainchain_monitor,
|
||||
./test_ssz,
|
||||
|
105
tests/test_keystore.nim
Normal file
105
tests/test_keystore.nim
Normal file
@ -0,0 +1,105 @@
|
||||
# beacon_chain
|
||||
# Copyright (c) 2018 Status Research & Development GmbH
|
||||
# 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.
|
||||
|
||||
{.used.}
|
||||
|
||||
import
|
||||
unittest, ./testutil, json,
|
||||
nimcrypto/utils,
|
||||
../beacon_chain/spec/keystore
|
||||
|
||||
from strutils import replace
|
||||
|
||||
const
|
||||
scryptVector = """{
|
||||
"crypto": {
|
||||
"kdf": {
|
||||
"function": "scrypt",
|
||||
"params": {
|
||||
"dklen": 32,
|
||||
"n": 262144,
|
||||
"p": 1,
|
||||
"r": 8,
|
||||
"salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
|
||||
},
|
||||
"message": ""
|
||||
},
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"params": {},
|
||||
"message": "149aafa27b041f3523c53d7acba1905fa6b1c90f9fef137568101f44b531a3cb"
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"params": {
|
||||
"iv": "264daa3f303d7259501c93d997d84fe6"
|
||||
},
|
||||
"message": "54ecc8863c0550351eee5720f3be6a5d4a016025aa91cd6436cfec938d6a8d30"
|
||||
}
|
||||
},
|
||||
"pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
|
||||
"path": "m/12381/60/3141592653/589793238",
|
||||
"uuid": "1d85ae20-35c5-4611-98e8-aa14a633906f",
|
||||
"version": 4
|
||||
}""" #"
|
||||
|
||||
pbkdf2Vector = """{
|
||||
"crypto": {
|
||||
"kdf": {
|
||||
"function": "pbkdf2",
|
||||
"params": {
|
||||
"dklen": 32,
|
||||
"c": 262144,
|
||||
"prf": "hmac-sha256",
|
||||
"salt": "d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
|
||||
},
|
||||
"message": ""
|
||||
},
|
||||
"checksum": {
|
||||
"function": "sha256",
|
||||
"params": {},
|
||||
"message": "18b148af8e52920318084560fd766f9d09587b4915258dec0676cba5b0da09d8"
|
||||
},
|
||||
"cipher": {
|
||||
"function": "aes-128-ctr",
|
||||
"params": {
|
||||
"iv": "264daa3f303d7259501c93d997d84fe6"
|
||||
},
|
||||
"message": "a9249e0ca7315836356e4c7440361ff22b9fe71e2e2ed34fc1eb03976924ed48"
|
||||
}
|
||||
},
|
||||
"pubkey": "9612d7a727c9d0a22e185a1c768478dfe919cada9266988cb32359c11f2b7b27f4ae4040902382ae2910c15e2b420d07",
|
||||
"path": "m/12381/60/0/0",
|
||||
"uuid": "64625def-3331-4eea-ab6f-782f3ed16a83",
|
||||
"version": 4
|
||||
}""" #"
|
||||
|
||||
const
|
||||
password = "testpassword"
|
||||
secret = fromHex("000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f")
|
||||
salt = fromHex("d4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3")
|
||||
iv = fromHex("264daa3f303d7259501c93d997d84fe6")
|
||||
uuid = "64625def-3331-4eea-ab6f-782f3ed16a83"
|
||||
|
||||
suiteReport "Keystore":
|
||||
timedTest "Pbkdf2 decryption":
|
||||
let decrypt = decryptKeystore(pbkdf2Vector, password)
|
||||
check decrypt.isOk
|
||||
check secret == decrypt.get()
|
||||
|
||||
timedTest "Pbkdf2 encryption":
|
||||
let encrypt = encryptKeystore[KdfPbkdf2](secret, password, salt=salt, iv=iv,
|
||||
path="m/12381/60/0/0", ugly=false)
|
||||
|
||||
check encrypt.isOk
|
||||
check encrypt.get() == pbkdf2Vector.replace(uuid, "")
|
||||
|
||||
timedTest "Pbkdf2 error":
|
||||
check encryptKeystore[KdfPbkdf2](secret, "", salt = [byte 1]).isErr
|
||||
check encryptKeystore[KdfPbkdf2](secret, "", iv = [byte 1]).isErr
|
||||
|
||||
check decryptKeystore(pbkdf2Vector, "").isErr
|
Loading…
x
Reference in New Issue
Block a user