mirror of https://github.com/status-im/nim-eth.git
439 lines
13 KiB
Nim
439 lines
13 KiB
Nim
|
#
|
||
|
# Ethereum KeyFile
|
||
|
# (c) Copyright 2018
|
||
|
# Status Research & Development GmbH
|
||
|
#
|
||
|
# Licensed under either of
|
||
|
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
||
|
# MIT license (LICENSE-MIT)
|
||
|
|
||
|
import nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak],
|
||
|
eth/keys, json, uuid, os, strutils, streams
|
||
|
|
||
|
const
|
||
|
# Version 3 constants
|
||
|
SaltSize = 16
|
||
|
DKLen = 32
|
||
|
MaxDKLen = 128
|
||
|
ScryptR = 1
|
||
|
ScryptP = 8
|
||
|
Pbkdf2WorkFactor = 1_000_000
|
||
|
ScryptWorkFactor = 262_144
|
||
|
|
||
|
type
|
||
|
KeyFileStatus* = enum
|
||
|
Success, ## No Error
|
||
|
RandomError, ## Random generator error
|
||
|
UuidError, ## UUID generator error
|
||
|
BufferOverrun, ## Supplied buffer is too small
|
||
|
IncorrectDKLen, ## `dklen` parameter is 0 or more then MaxDKLen
|
||
|
MalformedError, ## JSON has incorrect structure
|
||
|
NotImplemented, ## Feature is not implemented
|
||
|
NotSupported, ## Feature is not supported
|
||
|
EmptyMac, ## `mac` parameter is zero length or not in
|
||
|
## hexadecimal form
|
||
|
EmptyCiphertext, ## `ciphertext` parameter is zero length or not in
|
||
|
## hexadecimal format
|
||
|
EmptySalt, ## `salt` parameter is zero length or not in
|
||
|
## hexadecimal format
|
||
|
EmptyIV, ## `cipherparams.iv` parameter is zero length or not in
|
||
|
## hexadecimal format
|
||
|
IncorrectIV, ## Size of IV vector is not equal to cipher block size
|
||
|
PrfNotSupported, ## PRF algorithm for PBKDF2 is not supported
|
||
|
KdfNotSupported, ## KDF algorithm is not supported
|
||
|
CipherNotSupported, ## `cipher` parameter is not supported
|
||
|
IncorrectMac, ## `mac` verification failed
|
||
|
IncorrectPrivateKey, ## incorrect private key
|
||
|
OsError, ## OS specific error
|
||
|
JsonError ## JSON encoder/decoder error
|
||
|
|
||
|
KdfKind* = enum
|
||
|
PBKDF2, ## PBKDF2
|
||
|
SCRYPT ## SCRYPT
|
||
|
|
||
|
HashKind* = enum
|
||
|
HashNoSupport, HashSHA2_224, HashSHA2_256, HashSHA2_384, HashSHA2_512,
|
||
|
HashKECCAK224, HashKECCAK256, HashKECCAK384, HashKECCAK512,
|
||
|
HashSHA3_224, HashSHA3_256, HashSHA3_384, HashSHA3_512
|
||
|
|
||
|
CryptKind* = enum
|
||
|
CipherNoSupport, ## Cipher not supported
|
||
|
AES128CTR ## AES-128-CTR
|
||
|
|
||
|
const
|
||
|
SupportedHashes = [
|
||
|
"sha224", "sha256", "sha384", "sha512",
|
||
|
"keccak224", "keccak256", "keccak384", "keccak512",
|
||
|
"sha3_224", "sha3_256", "sha3_384", "sha3_512"
|
||
|
]
|
||
|
|
||
|
SupportedHashesKinds = [
|
||
|
HashSHA2_224, HashSHA2_256, HashSHA2_384, HashSHA2_512,
|
||
|
HashKECCAK224, HashKECCAK256, HashKECCAK384, HashKECCAK512,
|
||
|
HashSHA3_224, HashSHA3_256, HashSHA3_384, HashSHA3_512
|
||
|
]
|
||
|
|
||
|
proc `$`(k: KdfKind): string =
|
||
|
case k
|
||
|
of SCRYPT:
|
||
|
result = "scrypt"
|
||
|
else:
|
||
|
result = "pbkdf2"
|
||
|
|
||
|
proc `$`(k: CryptKind): string =
|
||
|
case k
|
||
|
of AES128CTR:
|
||
|
result = "aes-128-ctr"
|
||
|
else:
|
||
|
result = "aes-128-ctr"
|
||
|
|
||
|
proc getPrfHash(prf: string): HashKind =
|
||
|
result = HashNoSupport
|
||
|
let p = prf.toLowerAscii()
|
||
|
if p.startsWith("hmac-"):
|
||
|
var hash = p[5..^1]
|
||
|
var res = SupportedHashes.find(hash)
|
||
|
if res >= 0:
|
||
|
result = SupportedHashesKinds[res]
|
||
|
else:
|
||
|
result = HashNoSupport
|
||
|
|
||
|
proc getCipher(c: string): CryptKind =
|
||
|
var cl = c.toLowerAscii()
|
||
|
if cl == "aes-128-ctr":
|
||
|
result = AES128CTR
|
||
|
else:
|
||
|
result = CipherNoSupport
|
||
|
|
||
|
proc deriveKey(password: string,
|
||
|
salt: string,
|
||
|
kdfkind: KdfKind,
|
||
|
hashkind: HashKind,
|
||
|
workfactor: int,
|
||
|
output: var openarray[byte]): KeyFileStatus =
|
||
|
if kdfkind == SCRYPT:
|
||
|
return NotImplemented
|
||
|
elif kdfkind == PBKDF2:
|
||
|
var c = if workfactor == 0: Pbkdf2WorkFactor else: workfactor
|
||
|
case hashkind
|
||
|
of HashSHA2_224:
|
||
|
var ctx: HMAC[sha224]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA2_256:
|
||
|
var ctx: HMAC[sha256]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA2_384:
|
||
|
var ctx: HMAC[sha384]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA2_512:
|
||
|
var ctx: HMAC[sha512]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashKECCAK224:
|
||
|
var ctx: HMAC[keccak224]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashKECCAK256:
|
||
|
var ctx: HMAC[keccak256]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashKECCAK384:
|
||
|
var ctx: HMAC[keccak384]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashKECCAK512:
|
||
|
var ctx: HMAC[keccak512]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA3_224:
|
||
|
var ctx: HMAC[sha3_224]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA3_256:
|
||
|
var ctx: HMAC[sha3_256]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA3_384:
|
||
|
var ctx: HMAC[sha3_384]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
of HashSHA3_512:
|
||
|
var ctx: HMAC[sha3_512]
|
||
|
discard ctx.pbkdf2(password, salt, c, output)
|
||
|
result = Success
|
||
|
else:
|
||
|
result = PrfNotSupported
|
||
|
|
||
|
proc encryptKey(seckey: PrivateKey,
|
||
|
cryptkind: CryptKind,
|
||
|
key: openarray[byte],
|
||
|
iv: openarray[byte],
|
||
|
crypttext: var openarray[byte]): KeyFileStatus =
|
||
|
if len(crypttext) != KeyLength:
|
||
|
return BufferOverrun
|
||
|
if cryptkind == AES128CTR:
|
||
|
var ctx: CTR[aes128]
|
||
|
ctx.init(toOpenArray(key, 0, 15), iv)
|
||
|
ctx.encrypt(seckey.data, crypttext)
|
||
|
ctx.clear()
|
||
|
result = Success
|
||
|
else:
|
||
|
result = NotImplemented
|
||
|
|
||
|
proc decryptKey(ciphertext: openarray[byte],
|
||
|
cryptkind: CryptKind,
|
||
|
key: openarray[byte],
|
||
|
iv: openarray[byte],
|
||
|
plaintext: var openarray[byte]): KeyFileStatus =
|
||
|
if len(ciphertext) != len(plaintext):
|
||
|
return BufferOverrun
|
||
|
if cryptkind == AES128CTR:
|
||
|
if len(iv) != aes128.sizeBlock:
|
||
|
return IncorrectIV
|
||
|
var ctx: CTR[aes128]
|
||
|
ctx.init(toOpenArray(key, 0, 15), iv)
|
||
|
ctx.decrypt(ciphertext, plaintext)
|
||
|
ctx.clear()
|
||
|
result = Success
|
||
|
else:
|
||
|
result = NotImplemented
|
||
|
|
||
|
proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int,
|
||
|
outjson: var JsonNode): KeyFileStatus =
|
||
|
if kdfkind == SCRYPT:
|
||
|
var wf = if workfactor == 0: ScryptWorkFactor else: workfactor
|
||
|
outjson = %*
|
||
|
{
|
||
|
"dklen": DKLen,
|
||
|
"n": wf,
|
||
|
"r": ScryptR,
|
||
|
"p": ScryptP,
|
||
|
"salt": salt
|
||
|
}
|
||
|
result = Success
|
||
|
elif kdfkind == PBKDF2:
|
||
|
var wf = if workfactor == 0: Pbkdf2WorkFactor else: workfactor
|
||
|
outjson = %*
|
||
|
{
|
||
|
"dklen": DKLen,
|
||
|
"c": wf,
|
||
|
"prf": "hmac-sha256",
|
||
|
"salt": salt
|
||
|
}
|
||
|
result = Success
|
||
|
else:
|
||
|
result = NotImplemented
|
||
|
|
||
|
proc decodeHex(m: string): seq[byte] =
|
||
|
if len(m) > 0:
|
||
|
try:
|
||
|
result = fromHex(m)
|
||
|
except:
|
||
|
result = newSeq[byte]()
|
||
|
else:
|
||
|
result = newSeq[byte]()
|
||
|
|
||
|
proc decodeSalt(m: string): string =
|
||
|
var sarr: seq[byte]
|
||
|
if len(m) > 0:
|
||
|
try:
|
||
|
sarr = fromHex(m)
|
||
|
result = newString(len(sarr))
|
||
|
copyMem(addr result[0], addr sarr[0], len(sarr))
|
||
|
except:
|
||
|
result = ""
|
||
|
else:
|
||
|
result = ""
|
||
|
|
||
|
proc compareMac(m1: openarray[byte], m2: openarray[byte]): bool =
|
||
|
if len(m1) == len(m2) and len(m1) > 0:
|
||
|
result = equalMem(unsafeAddr m1[0], unsafeAddr m2[0], len(m1))
|
||
|
|
||
|
proc createKeyFileJson*(seckey: PrivateKey,
|
||
|
password: string,
|
||
|
outjson: var JsonNode,
|
||
|
version: int = 3,
|
||
|
cryptkind: CryptKind = AES128CTR,
|
||
|
kdfkind: KdfKind = PBKDF2,
|
||
|
workfactor: int = 0): KeyFileStatus =
|
||
|
## Create JSON object with keyfile structure.
|
||
|
##
|
||
|
## ``seckey`` - private key, which will be stored
|
||
|
## ``password`` - encryption password
|
||
|
## ``outjson`` - result JSON object
|
||
|
## ``version`` - version of keyfile format (default is 3)
|
||
|
## ``cryptkind`` - algorithm for private key encryption
|
||
|
## (default is AES128-CTR)
|
||
|
## ``kdfkind`` - algorithm for key deriviation function (default is PBKDF2)
|
||
|
## ``workfactor`` - Key deriviation function work factor, 0 is to use
|
||
|
## default workfactor.
|
||
|
var res: KeyFileStatus
|
||
|
var iv: array[aes128.sizeBlock, byte]
|
||
|
var ciphertext: array[KeyLength, byte]
|
||
|
var salt: array[SaltSize, byte]
|
||
|
var saltstr = newString(SaltSize)
|
||
|
var u: UUID
|
||
|
if randomBytes(iv) != aes128.sizeBlock:
|
||
|
return RandomError
|
||
|
if randomBytes(salt) != SaltSize:
|
||
|
return RandomError
|
||
|
copyMem(addr saltstr[0], addr salt[0], SaltSize)
|
||
|
if uuidGenerate(u) != 1:
|
||
|
return UuidError
|
||
|
if kdfkind != PBKDF2:
|
||
|
return NotImplemented
|
||
|
|
||
|
var dkey = newSeq[byte](DKLen)
|
||
|
res = deriveKey(password, saltstr, kdfkind, HashSHA2_256,
|
||
|
workfactor, dkey)
|
||
|
if res != Success:
|
||
|
return res
|
||
|
res = encryptKey(seckey, cryptkind, dkey, iv, ciphertext)
|
||
|
if res != Success:
|
||
|
return res
|
||
|
var ctx: keccak256
|
||
|
ctx.init()
|
||
|
ctx.update(toOpenArray(dkey, 16, 31))
|
||
|
ctx.update(ciphertext)
|
||
|
var mac = ctx.finish()
|
||
|
ctx.clear()
|
||
|
|
||
|
var params: JsonNode
|
||
|
res = kdfParams(kdfkind, toHex(salt, true), workfactor, params)
|
||
|
if res != Success:
|
||
|
return res
|
||
|
|
||
|
outjson = %*
|
||
|
{
|
||
|
"address": seckey.getPublicKey().toAddress(false),
|
||
|
"crypto": {
|
||
|
"cipher": $cryptkind,
|
||
|
"cipherparams": {
|
||
|
"iv": toHex(iv, true)
|
||
|
},
|
||
|
"ciphertext": toHex(ciphertext, true),
|
||
|
"kdf": $kdfkind,
|
||
|
"kdfparams": params,
|
||
|
"mac": toHex(mac.data, true),
|
||
|
},
|
||
|
"id": $u,
|
||
|
"version": version
|
||
|
}
|
||
|
result = Success
|
||
|
|
||
|
proc decodeKeyFileJson*(j: JsonNode,
|
||
|
password: string,
|
||
|
seckey: var PrivateKey): KeyFileStatus =
|
||
|
## Decode private key into ``seckey`` from keyfile json object ``j`` using
|
||
|
## password string ``password``.
|
||
|
var
|
||
|
res: KeyFileStatus
|
||
|
plaintext: array[KeyLength, byte]
|
||
|
|
||
|
var crypto = j.getOrDefault("crypto")
|
||
|
if isNil(crypto):
|
||
|
return MalformedError
|
||
|
|
||
|
var kdf = crypto.getOrDefault("kdf")
|
||
|
if isNil(kdf):
|
||
|
return MalformedError
|
||
|
|
||
|
var cipherparams = crypto.getOrDefault("cipherparams")
|
||
|
if isNil(cipherparams):
|
||
|
return MalformedError
|
||
|
|
||
|
if kdf.getStr() == "pbkdf2":
|
||
|
var params = crypto.getOrDefault("kdfparams")
|
||
|
|
||
|
if isNil(params):
|
||
|
return MalformedError
|
||
|
|
||
|
var salt = decodeSalt(params.getOrDefault("salt").getStr())
|
||
|
var ciphertext = decodeHex(crypto.getOrDefault("ciphertext").getStr())
|
||
|
var mactext = decodeHex(crypto.getOrDefault("mac").getStr())
|
||
|
var cryptkind = getCipher(crypto.getOrDefault("cipher").getStr())
|
||
|
var iv = decodeHex(cipherparams.getOrDefault("iv").getStr())
|
||
|
|
||
|
if len(salt) == 0:
|
||
|
return EmptySalt
|
||
|
if len(ciphertext) == 0:
|
||
|
return EmptyCiphertext
|
||
|
if len(mactext) == 0:
|
||
|
return EmptyMac
|
||
|
if cryptkind == CipherNoSupport:
|
||
|
return CipherNotSupported
|
||
|
|
||
|
var dklen = params.getOrDefault("dklen").getInt()
|
||
|
var c = params.getOrDefault("c").getInt()
|
||
|
var hash = getPrfHash(params.getOrDefault("prf").getStr())
|
||
|
|
||
|
if hash == HashNoSupport:
|
||
|
return PrfNotSupported
|
||
|
if dklen == 0 or dklen > MaxDKLen:
|
||
|
return IncorrectDKLen
|
||
|
if len(ciphertext) != KeyLength:
|
||
|
return IncorrectPrivateKey
|
||
|
|
||
|
var dkey = newSeq[byte](dklen)
|
||
|
res = deriveKey(password, salt, PBKDF2, hash, c, dkey)
|
||
|
if res != Success:
|
||
|
return res
|
||
|
|
||
|
var ctx: keccak256
|
||
|
ctx.init()
|
||
|
ctx.update(toOpenArray(dkey, 16, 31))
|
||
|
ctx.update(ciphertext)
|
||
|
var mac = ctx.finish()
|
||
|
if not compareMac(mac.data, mactext):
|
||
|
return IncorrectMac
|
||
|
|
||
|
res = decryptKey(ciphertext, cryptkind, dkey, iv, plaintext)
|
||
|
if res != Success:
|
||
|
return res
|
||
|
try:
|
||
|
seckey = initPrivateKey(plaintext)
|
||
|
except:
|
||
|
return IncorrectPrivateKey
|
||
|
result = Success
|
||
|
else:
|
||
|
return KdfNotSupported
|
||
|
|
||
|
proc loadKeyFile*(pathname: string,
|
||
|
password: string,
|
||
|
seckey: var PrivateKey): KeyFileStatus =
|
||
|
## Load and decode private key ``seckey`` from file with pathname
|
||
|
## ``pathname``, using password string ``password``.
|
||
|
var data: JsonNode
|
||
|
var stream = newFileStream(pathname)
|
||
|
if isNil(stream):
|
||
|
return OsError
|
||
|
|
||
|
try:
|
||
|
data = parseFile(pathname)
|
||
|
result = Success
|
||
|
except:
|
||
|
result = JsonError
|
||
|
finally:
|
||
|
stream.close()
|
||
|
|
||
|
if result == Success:
|
||
|
result = decodeKeyFileJson(data, password, seckey)
|
||
|
|
||
|
proc saveKeyFile*(pathname: string,
|
||
|
jobject: JsonNode): KeyFileStatus =
|
||
|
## Save JSON object ``jobject`` to file with pathname ``pathname``.
|
||
|
var
|
||
|
f: File
|
||
|
if not f.open(pathname, fmWrite):
|
||
|
return OsError
|
||
|
try:
|
||
|
f.write($jobject)
|
||
|
result = Success
|
||
|
except:
|
||
|
result = OsError
|
||
|
finally:
|
||
|
f.close()
|