nimbus-eth2/beacon_chain/spec/keystore.nim

222 lines
5.4 KiB
Nim

# 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, strutils,
eth/keyfile/uuid,
stew/[results, byteutils],
nimcrypto/[sha2, rijndael, pbkdf2, bcmode, hash, sysrand],
./crypto
export results
{.push raises: [Defect].}
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
saltSize = 32
scryptParams = KdfScrypt(
dklen: saltSize,
n: 2^18,
r: 1,
p: 8
)
pbkdf2Params = KdfPbkdf2(
dklen: saltSize,
c: 2^18,
prf: "hmac-sha256"
)
proc shaChecksum(key, cipher: openarray[byte]): array[32, byte] =
var ctx: sha256
ctx.init()
ctx.update(key)
ctx.update(cipher)
result = ctx.finish().data
ctx.clear()
template tryJsonToCrypto(ks: JsonNode; crypto: typedesc): untyped =
try:
ks{"crypto"}.to(Crypto[crypto])
except Exception:
return err "ks: failed to parse crypto"
template hexToBytes(data, name: string): untyped =
try:
hexToSeqByte(data)
except ValueError:
return err "ks: failed to parse " & name
proc decryptKeystore*(data, passphrase: string): KsResult[seq[byte]] =
let ks =
try:
parseJson(data)
except Exception:
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 = tryJsonToCrypto(ks, KdfScrypt)
return err "ks: scrypt not supported"
of "pbkdf2":
let
crypto = tryJsonToCrypto(ks, KdfPbkdf2)
kdfParams = crypto.kdf.params
salt = hexToBytes(kdfParams.salt, "salt")
decKey = sha256.pbkdf2(passphrase, salt, kdfParams.c, kdfParams.dklen)
iv = hexToBytes(crypto.cipher.params.iv, "iv")
cipherMsg = hexToBytes(crypto.cipher.message, "cipher")
checksumMsg = hexToBytes(crypto.checksum.message, "checksum")
else:
return err "ks: unknown cipher"
if decKey.len < saltSize:
return err "ks: decryption key must be at least 32 bytes"
if iv.len < aes128.sizeBlock:
return err "ks: invalid iv"
let sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
if sum != checksumMsg:
return err "ks: invalid checksum"
var
aesCipher: CTR[aes128]
secret = newSeq[byte](cipherMsg.len)
aesCipher.init(decKey.toOpenArray(0, 15), 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](aes128.sizeBlock)
kdfSalt = newSeq[byte](saltSize)
cipherMsg = newSeq[byte](secret.len)
if salt.len == saltSize:
kdfSalt = @salt
elif salt.len > 0:
return err "ks: invalid salt"
elif randomBytes(kdfSalt) != saltSize:
return err "ks: no random bytes for salt"
if iv.len == aes128.sizeBlock:
aesIv = @iv
elif iv.len > 0:
return err "ks: invalid iv"
elif randomBytes(aesIv) != aes128.sizeBlock:
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()
else:
return
aesCipher.init(decKey.toOpenArray(0, 15), aesIv)
aesCipher.encrypt(secret, cipherMsg)
aesCipher.clear()
let pubkey = (? ValidatorPrivkey.fromRaw(secret)).toPubKey()
let
sum = shaChecksum(decKey.toOpenArray(16, 31), cipherMsg)
keystore = Keystore[T](
crypto: Crypto[T](
kdf: kdf,
checksum: Checksum(
function: "sha256",
message: sum.toHex()
),
cipher: Cipher(
function: "aes-128-ctr",
params: CipherParams(iv: aesIv.toHex()),
message: cipherMsg.toHex()
)
),
pubkey: pubkey.toHex(),
path: path,
uuid: $(? uuidGenerate()),
version: 4
)
result = ok(if ugly: $(%keystore)
else: pretty(%keystore, indent=4))