From 0d18ffac317dfee372160774c4d77bfcc1c0cf66 Mon Sep 17 00:00:00 2001 From: Yuriy Glukhov Date: Tue, 5 Feb 2019 12:45:09 +0200 Subject: [PATCH] Moved eth-keyfile to eth --- eth/keyfile.nim | 11 + eth/keyfile/keyfile.nim | 438 +++++++++++++++++++++++++++++++++ eth/keyfile/uuid.nim | 154 ++++++++++++ tests/keyfile/config.nims | 3 + tests/keyfile/test_keyfile.nim | 134 ++++++++++ tests/keyfile/test_uuid.nim | 23 ++ 6 files changed, 763 insertions(+) create mode 100644 eth/keyfile.nim create mode 100644 eth/keyfile/keyfile.nim create mode 100644 eth/keyfile/uuid.nim create mode 100644 tests/keyfile/config.nims create mode 100644 tests/keyfile/test_keyfile.nim create mode 100644 tests/keyfile/test_uuid.nim diff --git a/eth/keyfile.nim b/eth/keyfile.nim new file mode 100644 index 0000000..e4c28b7 --- /dev/null +++ b/eth/keyfile.nim @@ -0,0 +1,11 @@ +# +# 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 keyfile/uuid, keyfile/keyfile +export uuid, keyfile diff --git a/eth/keyfile/keyfile.nim b/eth/keyfile/keyfile.nim new file mode 100644 index 0000000..a9d9030 --- /dev/null +++ b/eth/keyfile/keyfile.nim @@ -0,0 +1,438 @@ +# +# 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() diff --git a/eth/keyfile/uuid.nim b/eth/keyfile/uuid.nim new file mode 100644 index 0000000..943e3ef --- /dev/null +++ b/eth/keyfile/uuid.nim @@ -0,0 +1,154 @@ +# +# Ethereum KeyFile +# (c) Copyright 2018 +# Status Research & Development GmbH +# +# Licensed under either of +# Apache License, version 2.0, (LICENSE-APACHEv2) +# MIT license (LICENSE-MIT) + +## This module implements interface to cross-platform UUID +## generator. +## +## - ``Windows`` - using rpcrt4.dll's `UuidCreate()`. +## - ``Linux`` and ``Android`` - using `/proc/sys/kernel/random/uuid`. +## - ``MacOS`` and ``iOS`` - using `uuid_generate_random()`. +## - ``FreeBSD``, ``OpenBSD``, ``NetBSD``, +## ``DragonflyBSD`` - using `uuid_create()`. + +{.deadCodeElim:on.} + +import nimcrypto/utils, endians + +type + UUIDException = object of Exception + + UUID* = object + ## Represents UUID object + data*: array[16, byte] + +proc raiseInvalidUuid() = + raise newException(UUIDException, "Invalid UUID!") + +proc uuidFromString*(s: string): UUID = + ## Convert string representation of UUID into UUID object. + if len(s) != 36: + raiseInvalidUuid() + for i in 0.. 0: + result += res + elif res == 0: + break + else: + if osLastError().int32 != EINTR: + result = -1 + break + discard posix.close(fd) + + proc uuidGenerate*(output: var UUID): int = + result = 0 + var buffer = newString(37) + if uuidRead(buffer, 36) == 36: + buffer.setLen(36) + output = uuidFromString(buffer) + result = 1 + else: + if randomBytes(output.data) == sizeof(output.data): + result = 1 + else: + import nimcrypto/sysrand + + proc uuidGenerate*(output: var UUID): int = + if randomBytes(output.data) == sizeof(output.data): + result = 1 + else: + result = 0 + +elif defined(windows): + proc UuidCreate(p: pointer): int32 + {.stdcall, dynlib: "rpcrt4", importc: "UuidCreate".} + + proc uuidGenerate*(output: var UUID): int = + if UuidCreate(cast[pointer](addr output)) == 0: + return 1 + else: + return 0 +elif not defined(nimdoc): + import nimcrypto/sysrand + + proc uuidGenerate*(output: var UUID): int = + if randomBytes(output.data) == sizeof(output.data): + result = 1 + else: + result = 0 diff --git a/tests/keyfile/config.nims b/tests/keyfile/config.nims new file mode 100644 index 0000000..3ad3ec5 --- /dev/null +++ b/tests/keyfile/config.nims @@ -0,0 +1,3 @@ +switch("path", "$projectDir/..") +switch("threads", "on") + diff --git a/tests/keyfile/test_keyfile.nim b/tests/keyfile/test_keyfile.nim new file mode 100644 index 0000000..b0c58ec --- /dev/null +++ b/tests/keyfile/test_keyfile.nim @@ -0,0 +1,134 @@ +# +# 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 eth_keys, eth_keyfile/[uuid, keyfile], json, strutils, os, unittest + +# Test vectors copied from +# https://github.com/ethereum/tests/blob/develop/KeyStoreTests/basic_tests.json + +var TestVectors = [ + %*{ + "keyfile": { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : {"iv" : "6087dab2f9fdbbfaddc31a909735c1e6"}, + "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", + "kdf" : "pbkdf2", + "kdfparams" : { + "c" : 262144, + "dklen" : 32, + "prf" : "hmac-sha256", + "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" + }, + "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + }, + "name": "test1", + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" + }, + %*{ + "keyfile": { + "version": 3, + "crypto": { + "ciphertext": "ee75456c006b1e468133c5d2a916bacd3cf515ced4d9b021b5c59978007d1e87", + "version": 1, + "kdf": "pbkdf2", + "kdfparams": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "504490577620f64f43d73f29479c2cf0" + }, + "mac": "196815708465de9af7504144a1360d08874fc3c30bb0e648ce88fbc36830d35d", + "cipherparams": {"iv": "514ccc8c4fb3e60e5538e0cf1e27c233"}, + "cipher": "aes-128-ctr" + }, + "id": "98d193c7-5174-4c7c-5345-c1daf95477b5" + }, + "name": "python_generated_test_with_odd_iv", + "password": "foo", + "priv": "0101010101010101010101010101010101010101010101010101010101010101" + }, + %*{ + "keyfile": { + "version": 3, + "crypto": { + "ciphertext": "d69313b6470ac1942f75d72ebf8818a0d484ac78478a132ee081cd954d6bd7a9", + "cipherparams": {"iv": "ffffffffffffffffffffffffffffffff"}, + "kdf": "pbkdf2", + "kdfparams": { + "dklen": 32, + "c": 262144, + "prf": "hmac-sha256", + "salt": "c82ef14476014cbf438081a42709e2ed" + }, + "mac": "cf6bfbcc77142a22c4a908784b4a16f1023a1d0e2aff404c20158fa4f1587177", + "cipher": "aes-128-ctr", + "version": 1 + }, + "id": "abb67040-8dbe-0dad-fc39-2b082ef0ee5f" + }, + "name": "evilnonce", + "password": "bar", + "priv": "0202020202020202020202020202020202020202020202020202020202020202" + } +] + +var jobject: JsonNode + +suite "KeyFile test suite": + test "KeyStoreTests/basic_tests.json test1": + var seckey: PrivateKey + var expectkey = initPrivateKey(TestVectors[0].getOrDefault("priv").getStr()) + check: + decodeKeyFileJson(TestVectors[0].getOrDefault("keyfile"), + TestVectors[0].getOrDefault("password").getStr(), + seckey) == KeyFileStatus.Success + seckey.data == expectkey.data + test "KeyStoreTests/basic_tests.json python_generated_test_with_odd_iv": + var seckey: PrivateKey + var expectkey = initPrivateKey(TestVectors[1].getOrDefault("priv").getStr()) + check: + decodeKeyFileJson(TestVectors[1].getOrDefault("keyfile"), + TestVectors[1].getOrDefault("password").getStr(), + seckey) == KeyFileStatus.Success + seckey.data == expectkey.data + test "KeyStoreTests/basic_tests.json evilnonce": + var seckey: PrivateKey + var expectkey = initPrivateKey(TestVectors[2].getOrDefault("priv").getStr()) + check: + decodeKeyFileJson(TestVectors[2].getOrDefault("keyfile"), + TestVectors[2].getOrDefault("password").getStr(), + seckey) == KeyFileStatus.Success + seckey.data == expectkey.data + test "KeyStoreTests/basic_tests.json evilnonce with wrong password": + var seckey: PrivateKey + check: + decodeKeyFileJson(TestVectors[2].getOrDefault("keyfile"), + "wrongpassword", + seckey) == KeyFileStatus.IncorrectMac + test "Create/Save/Load test": + var seckey0 = newPrivateKey() + var seckey1: PrivateKey + check: + createKeyFileJson(seckey0, "randompassword", + jobject) == KeyFileStatus.Success + saveKeyFile("test.keyfile", jobject) == KeyFileStatus.Success + loadKeyFile("test.keyfile", "randompassword", + seckey1) == KeyFileStatus.Success + seckey0.data == seckey1.data + removeFile("test.keyfile") + test "Load non-existent pathname test": + var seckey: PrivateKey + check: + loadKeyFile("nonexistant.keyfile", "password", + seckey) == KeyFileStatus.OsError diff --git a/tests/keyfile/test_uuid.nim b/tests/keyfile/test_uuid.nim new file mode 100644 index 0000000..581de47 --- /dev/null +++ b/tests/keyfile/test_uuid.nim @@ -0,0 +1,23 @@ +# +# 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 eth_keyfile/uuid, strutils, unittest + +suite "Cross-platform UUID test suite": + test "Platform UUID check": + var u: UUID + check uuidGenerate(u) == 1 + test "Conversion test": + var u: UUID + check: + uuidGenerate(u) == 1 + len($u) == 36 + $uuidFromString($u) == $u + uuidToString(u, true) == $u + uuidToString(u, false) == toUpperAscii($u)