From 7afa22cf41444e2a45ed058e5c0af3f9c8b25f80 Mon Sep 17 00:00:00 2001 From: jangko Date: Mon, 27 Jul 2020 19:34:36 +0700 Subject: [PATCH] add scrypt kdf to keyfile and implement test for it --- eth/keyfile/keyfile.nim | 206 ++++++++++++++++++++++++--------- tests/keyfile/test_keyfile.nim | 119 +++++++++++++++++++ 2 files changed, 272 insertions(+), 53 deletions(-) diff --git a/eth/keyfile/keyfile.nim b/eth/keyfile/keyfile.nim index 991c048..213ac96 100644 --- a/eth/keyfile/keyfile.nim +++ b/eth/keyfile/keyfile.nim @@ -9,7 +9,7 @@ {.push raises: [Defect].} -import nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak], +import nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak, scrypt], eth/keys, json, uuid, strutils, stew/results export results @@ -43,6 +43,8 @@ type CipherNotSupported = "kf: `cipher` parameter is not supported" IncorrectMac = "kf: `mac` verification failed" IncorrectPrivateKey = "kf: incorrect private key" + IncorrectN = "kf: incorrect N value for scrypt" + ScryptBadParam = "kf: bad scrypt's parameters" OsError = "kf: OS specific error" JsonError = "kf: JSON encoder/decoder error" @@ -59,6 +61,32 @@ type CipherNoSupport, ## Cipher not supported AES128CTR ## AES-128-CTR + CipherParams = object + iv: seq[byte] + + Cipher = object + kind: CryptKind + params: CipherParams + text: seq[byte] + + Crypto = object + kind: KdfKind + cipher: Cipher + kdfParams: JsonNode + mac: seq[byte] + + ScryptParams* = object + dklen: int + n, p, r: int + salt: string + + Pbkdf2Params* = object + dklen: int + c: int + prf: HashKind + salt: string + + DKey = array[DKLen, byte] KfResult*[T] = Result[T, KeyFileError] proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] = @@ -113,9 +141,9 @@ proc deriveKey(password: string, salt: string, kdfkind: KdfKind, hashkind: HashKind, - workfactor: int): KfResult[array[DKLen, byte]] = + workfactor: int): KfResult[DKey] = if kdfkind == PBKDF2: - var output: array[DKLen, byte] + var output: DKey var c = if workfactor == 0: Pbkdf2WorkFactor else: workfactor case hashkind of HashSHA2_224: @@ -171,6 +199,16 @@ proc deriveKey(password: string, else: err(NotImplemented) +proc deriveKey(password: string, salt: string, + workFactor, r, p: int): KfResult[DKey] = + + let wf = if workFactor == 0: ScryptWorkFactor else: workFactor + var output: DKey + if scrypt(password, salt, wf, r, p, output) == 0: + return err(ScryptBadParam) + + result = ok(output) + proc encryptKey(seckey: PrivateKey, cryptkind: CryptKind, key: openarray[byte], @@ -279,11 +317,11 @@ proc createKeyFileJson*(seckey: PrivateKey, let u = ? uuidGenerate().mapErrTo(UuidError) - if kdfkind != PBKDF2: - return err(NotImplemented) - let - dkey = ? deriveKey(password, saltstr, kdfkind, HashSHA2_256, workfactor) + dkey = case kdfkind + of PBKDF2: ? deriveKey(password, saltstr, kdfkind, HashSHA2_256, workfactor) + of SCRYPT: ? deriveKey(password, saltstr, workfactor, ScryptR, ScryptP) + ciphertext = ? encryptKey(seckey, cryptkind, dkey, iv) var ctx: keccak256 @@ -313,11 +351,8 @@ proc createKeyFileJson*(seckey: PrivateKey, } ) -proc decodeKeyFileJson*(j: JsonNode, - password: string): KfResult[PrivateKey] = - ## Decode private key into ``seckey`` from keyfile json object ``j`` using - ## password string ``password``. - var crypto = j.getOrDefault("crypto") +proc decodeCrypto(n: JsonNode): KfResult[Crypto] = + var crypto = n.getOrDefault("crypto") if isNil(crypto): return err(MalformedError) @@ -325,57 +360,122 @@ proc decodeKeyFileJson*(j: JsonNode, if isNil(kdf): return err(MalformedError) + var c: Crypto + case kdf.getStr() + of "pbkdf2": c.kind = PBKDF2 + of "scrypt": c.kind = SCRYPT + else: return err(KdfNotSupported) + var cipherparams = crypto.getOrDefault("cipherparams") if isNil(cipherparams): return err(MalformedError) - if kdf.getStr() == "pbkdf2": - var params = crypto.getOrDefault("kdfparams") + c.cipher.kind = getCipher(crypto.getOrDefault("cipher").getStr()) + c.cipher.params.iv = decodeHex(cipherparams.getOrDefault("iv").getStr()) + c.cipher.text = decodeHex(crypto.getOrDefault("ciphertext").getStr()) + c.mac = decodeHex(crypto.getOrDefault("mac").getStr()) + c.kdfParams = crypto.getOrDefault("kdfparams") - if isNil(params): - return err(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 err(EmptySalt) - if len(ciphertext) == 0: - return err(EmptyCiphertext) - if len(mactext) == 0: - return err(EmptyMac) - if cryptkind == CipherNoSupport: - return err(CipherNotSupported) - - var dklen = params.getOrDefault("dklen").getInt() - var c = params.getOrDefault("c").getInt() - var hash = getPrfHash(params.getOrDefault("prf").getStr()) - - if hash == HashNoSupport: - return err(PrfNotSupported) - if dklen == 0 or dklen > MaxDKLen: - return err(IncorrectDKLen) - if len(ciphertext) != KeyLength: + if c.cipher.kind == CipherNoSupport: + return err(CipherNotSupported) + if len(c.cipher.text) == 0: + return err(EmptyCiphertext) + if len(c.cipher.text) != KeyLength: return err(IncorrectPrivateKey) + if len(c.mac) == 0: + return err(EmptyMac) + if isNil(c.kdfParams): + return err(MalformedError) - let dkey = ? deriveKey(password, salt, PBKDF2, hash, c) + result = ok(c) - 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 err(IncorrectMac) +proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] = + var p: Pbkdf2Params + p.salt = decodeSalt(params.getOrDefault("salt").getStr()) + if len(p.salt) == 0: + return err(EmptySalt) - let plaintext = ? decryptKey(ciphertext, cryptkind, dkey, iv) + p.dklen = params.getOrDefault("dklen").getInt() + p.c = params.getOrDefault("c").getInt() + p.prf = getPrfHash(params.getOrDefault("prf").getStr()) - PrivateKey.fromRaw(plaintext).mapErrTo(IncorrectPrivateKey) - else: - err(KdfNotSupported) + if p.prf == HashNoSupport: + return err(PrfNotSupported) + if p.dklen == 0 or p.dklen > MaxDKLen: + return err(IncorrectDKLen) + result = ok(p) + +proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] = + var p: ScryptParams + p.salt = decodeSalt(params.getOrDefault("salt").getStr()) + if len(p.salt) == 0: + return err(EmptySalt) + + p.dklen = params.getOrDefault("dklen").getInt() + p.n = params.getOrDefault("n").getInt() + p.p = params.getOrDefault("p").getInt() + p.r = params.getOrDefault("r").getInt() + + if p.n <= 1 or (p.n and (p.n-1)) != 0: + return err(IncorrectN) + if p.dklen == 0 or p.dklen > MaxDKLen: + return err(IncorrectDKLen) + + const + maxInt = high(int64) + maxIntd128 = maxInt div 128 + maxIntd256 = maxInt div 256 + + let + badParam1 = uint64(p.r)*uint64(p.p) >= 1 shl 30 + badParam2 = p.r > maxIntd128 div p.p + badParam3 = p.r > maxIntd256 + badParam4 = p.n > maxIntd128 div p.r + + if badParam1 or badParam2 or badParam3 or badParam4: + return err(ScryptBadParam) + + result = ok(p) + +func decryptPrivateKey(crypto: Crypto, dkey: DKey): KfResult[PrivateKey] = + var ctx: keccak256 + ctx.init() + ctx.update(toOpenArray(dkey, 16, 31)) + ctx.update(crypto.cipher.text) + var mac = ctx.finish() + if not compareMac(mac.data, crypto.mac): + return err(IncorrectMac) + + let plaintext = ? decryptKey(crypto.cipher.text, crypto.cipher.kind, dkey, crypto.cipher.params.iv) + PrivateKey.fromRaw(plaintext).mapErrTo(IncorrectPrivateKey) + +proc decodeKeyFileJson*(j: JsonNode, + password: string): KfResult[PrivateKey] = + ## Decode private key into ``seckey`` from keyfile json object ``j`` using + ## password string ``password``. + let res = decodeCrypto(j) + if res.isErr: + return err(res.error) + let crypto = res.get() + + case crypto.kind + of PBKDF2: + let res = decodePbkdf2Params(crypto.kdfParams) + if res.isErr: + return err(res.error) + + let params = res.get() + let dkey = ? deriveKey(password, params.salt, PBKDF2, params.prf, params.c) + result = decryptPrivateKey(crypto, dkey) + + of SCRYPT: + let res = decodeScryptParams(crypto.kdfParams) + if res.isErr: + return err(res.error) + + let params = res.get() + let dkey = ? deriveKey(password, params.salt, params.n, params.r, params.p) + result = decryptPrivateKey(crypto, dkey) proc loadKeyFile*(pathname: string, password: string): KfResult[PrivateKey] = diff --git a/tests/keyfile/test_keyfile.nim b/tests/keyfile/test_keyfile.nim index 9d0fd4c..0145968 100644 --- a/tests/keyfile/test_keyfile.nim +++ b/tests/keyfile/test_keyfile.nim @@ -84,6 +84,83 @@ var TestVectors = [ "name": "evilnonce", "password": "bar", "priv": "0202020202020202020202020202020202020202020202020202020202020202" + }, + %*{ + "keyfile": { + "version" : 3, + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6" + }, + "name" : "test2", + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" + }, + %*{ + "keyfile": { + "version": 3, + "address": "460121576cc7df020759730751f92bd62fd78dd6", + "crypto": { + "ciphertext": "54ae683c6287fa3d58321f09d56e26d94e58a00d4f90bdd95782ae0e4aab618b", + "cipherparams": { + "iv": "681679cdb125bba9495d068b002816a4" + }, + "cipher": "aes-128-ctr", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "salt": "c3407f363fce02a66e3c4bf4a8f6b7da1c1f54266cef66381f0625c251c32785", + "n": 8192, + "r": 8, + "p": 1 + }, + "mac": "dea6bdf22a2f522166ed82808c22a6311e84c355f4bbe100d4260483ff675a46" + }, + "id": "0eb785e0-340a-4290-9c42-90a11973ee47" + }, + "name": "mycrypto", + "password": "foobartest121", + "priv": "05a4d3eb46c742cb8850440145ce70cbc80b59f891cf5f50fd3e9c280b50c4e4" + }, + %*{ + "keyfile": { + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { + "iv": "7e7b02d2b4ef45d6c98cb885e75f48d5", + }, + "ciphertext": "a7a5743a6c7eb3fa52396bd3fd94043b79075aac3ccbae8e62d3af94db00397c", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 8192, + "p": 1, + "r": 8, + "salt": "247797c7a357b707a3bdbfaa55f4c553756bca09fec20ddc938e7636d21e4a20", + }, + "mac": "5a3ba5bebfda2c384586eda5fcda9c8397d37c9b0cc347fea86525cf2ea3a468", + }, + "address": "0b6f2de3dee015a95d3330dcb7baf8e08aa0112d", + "id": "3c8efdd6-d538-47ec-b241-36783d3418b9", + "version": 3 + }, + "password": "moomoocow", + "priv": "21eac69b9a52f466bfe9047f0f21c9caf3a5cdaadf84e2750a9b3265d450d481", + "name": "eth-keyfile-conftest" } ] @@ -115,6 +192,39 @@ suite "KeyFile test suite": "wrongpassword") check: seckey.error == KeyFileError.IncorrectMac + + + test "KeyStoreTests/basic_tests.json test2": + var expectkey = PrivateKey.fromHex(TestVectors[3].getOrDefault("priv").getStr())[] + let seckey = + decodeKeyFileJson(TestVectors[3].getOrDefault("keyfile"), + TestVectors[3].getOrDefault("password").getStr())[] + check: + seckey.toRaw == expectkey.toRaw + + test "KeyStoreTests/basic_tests.json mycrypto": + var expectkey = PrivateKey.fromHex(TestVectors[4].getOrDefault("priv").getStr())[] + let seckey = + decodeKeyFileJson(TestVectors[4].getOrDefault("keyfile"), + TestVectors[4].getOrDefault("password").getStr())[] + check: + seckey.toRaw == expectkey.toRaw + + test "eth-key/conftest.py": + var expectkey = PrivateKey.fromHex(TestVectors[5].getOrDefault("priv").getStr())[] + let seckey = + decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"), + TestVectors[5].getOrDefault("password").getStr())[] + check: + seckey.toRaw == expectkey.toRaw + + test "eth-key/conftest.py with wrong password": + let seckey = + decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"), + "wrongpassword") + check: + seckey.error == KeyFileError.IncorrectMac + test "Create/Save/Load test": var seckey0 = PrivateKey.random(rng[]) let jobject = createKeyFileJson(seckey0, "randompassword")[] @@ -125,6 +235,15 @@ suite "KeyFile test suite": check: seckey0.toRaw == seckey1.toRaw removeFile("test.keyfile") + + test "Scrypt roundtrip": + let + seckey1 = PrivateKey.random(rng[]) + jobject = createKeyFileJson(seckey1, "miawmiawcat", 3, AES128CTR, SCRYPT)[] + privKey = decodeKeyFileJson(jobject, "miawmiawcat")[] + + check privKey.toRaw == secKey1.toRaw + test "Load non-existent pathname test": check: loadKeyFile("nonexistant.keyfile", "password").error ==