Implement EIP 2335 compliant keystore

Closes #1024
This commit is contained in:
Zed 2020-05-19 19:30:28 +02:00 committed by zah
parent e33c8d9067
commit 8496e20a78
3 changed files with 309 additions and 0 deletions

View 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))

View File

@ -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
View 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