From 7abe2fd59cfc789cc54c80776eecb42663893868 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 20 Oct 2022 20:05:35 +0200 Subject: [PATCH 01/20] feat(utils): add keyfile + test --- tests/all_tests_v2.nim | 3 +- tests/v2/test_keyfile.nim | 25 ++ waku/v2/utils/keyfile.nim | 494 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 tests/v2/test_keyfile.nim create mode 100644 waku/v2/utils/keyfile.nim diff --git a/tests/all_tests_v2.nim b/tests/all_tests_v2.nim index 9227fe6d3a..be2c6c1308 100644 --- a/tests/all_tests_v2.nim +++ b/tests/all_tests_v2.nim @@ -41,7 +41,8 @@ import ./v2/test_enr_utils, ./v2/test_peer_exchange, ./v2/test_waku_noise, - ./v2/test_waku_noise_sessions + ./v2/test_waku_noise_sessions, + ./v2/test_keyfile when defined(rln) or defined(rlnzerokit): import diff --git a/tests/v2/test_keyfile.nim b/tests/v2/test_keyfile.nim new file mode 100644 index 0000000000..c0715d9b13 --- /dev/null +++ b/tests/v2/test_keyfile.nim @@ -0,0 +1,25 @@ +{.used.} + +import + std/[json, os], + testutils/unittests, chronos, chronicles, + eth/keys, + ../../waku/v2/utils/keyfile, + ../test_helpers + +from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte + +suite "KeyFile test suite": + + let rng = newRng() + + test "Create/Save/Load test": + var secret = randomSeqByte(rng[], 100) + let jobject = createKeyFileJson(secret, "randompassword")[] + + check: + saveKeyFile("test.keyfile", jobject).isOk() + var decodedSecret = loadKeyFile("test.keyfile", "randompassword")[] + check: + secret == decodedSecret + removeFile("test.keyfile") \ No newline at end of file diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim new file mode 100644 index 0000000000..bdb21cbcc9 --- /dev/null +++ b/waku/v2/utils/keyfile.nim @@ -0,0 +1,494 @@ +# This implementation is taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile +# and adapted to create keyfiles for arbitrary-long byte data (rather than fixed-size private keys) + +{.push raises: [Defect].} + +import + std/[strutils, json, sequtils], + nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak, scrypt], + stew/results, + eth/keys, + eth/keyfile/uuid + +export results + +const + # Version 3 constants + SaltSize = 16 + DKLen = 32 + MaxDKLen = 128 + ScryptR = 1 + ScryptP = 8 + Pbkdf2WorkFactor = 1_000_000 + ScryptWorkFactor = 262_144 + +type + KeyFileError* = enum + RandomError = "kf: Random generator error" + UuidError = "kf: UUID generator error" + BufferOverrun = "kf: Supplied buffer is too small" + IncorrectDKLen = "kf: `dklen` parameter is 0 or more then MaxDKLen" + MalformedError = "kf: JSON has incorrect structure" + NotImplemented = "kf: Feature is not implemented" + NotSupported = "kf: Feature is not supported" + EmptyMac = "kf: `mac` parameter is zero length or not in hexadecimal form" + EmptyCiphertext = "kf: `ciphertext` parameter is zero length or not in hexadecimal format" + EmptySalt = "kf: `salt` parameter is zero length or not in hexadecimal format" + EmptyIV = "kf: `cipherparams.iv` parameter is zero length or not in hexadecimal format" + IncorrectIV = "kf: Size of IV vector is not equal to cipher block size" + PrfNotSupported = "kf: PRF algorithm for PBKDF2 is not supported" + KdfNotSupported = "kf: KDF algorithm is not supported" + CipherNotSupported = "kf: `cipher` parameter is not supported" + IncorrectMac = "kf: `mac` verification failed" + IncorrectPrivateKey = "kf: incorrect private key" + ScryptBadParam = "kf: bad scrypt's parameters" + OsError = "kf: OS specific error" + JsonError = "kf: 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 + + 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] = + r.mapErr(proc (e: E): KeyFileError = v) + +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): KfResult[DKey] = + if kdfkind == PBKDF2: + var output: DKey + var c = if workfactor == 0: Pbkdf2WorkFactor else: workfactor + case hashkind + of HashSHA2_224: + var ctx: HMAC[sha224] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA2_256: + var ctx: HMAC[sha256] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA2_384: + var ctx: HMAC[sha384] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA2_512: + var ctx: HMAC[sha512] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashKECCAK224: + var ctx: HMAC[keccak224] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashKECCAK256: + var ctx: HMAC[keccak256] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashKECCAK384: + var ctx: HMAC[keccak384] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashKECCAK512: + var ctx: HMAC[keccak512] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA3_224: + var ctx: HMAC[sha3_224] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA3_256: + var ctx: HMAC[sha3_256] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA3_384: + var ctx: HMAC[sha3_384] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + of HashSHA3_512: + var ctx: HMAC[sha3_512] + discard ctx.pbkdf2(password, salt, c, output) + ok(output) + else: + err(PrfNotSupported) + else: + err(NotImplemented) + +func scrypt[T, M](password: openArray[T], salt: openArray[M], + N, r, p: int, output: var openArray[byte]): int = + let (xyvLen, bLen) = scryptCalc(N, r, p) + var xyv = newSeq[uint32](xyvLen) + var b = newSeq[byte](bLen) + scrypt(password, salt, N, r, p, xyv, b, output) + +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 encryptData(secret: openArray[byte], + cryptkind: CryptKind, + key: openArray[byte], + iv: openArray[byte]): KfResult[seq[byte]] = + if cryptkind == AES128CTR: + var crypttext = newSeqWith(secret.len, 0.byte) + var ctx: CTR[aes128] + ctx.init(toOpenArray(key, 0, 15), iv) + ctx.encrypt(secret, crypttext) + ctx.clear() + ok(crypttext) + else: + err(NotImplemented) + +proc decryptData(ciphertext: openArray[byte], + cryptkind: CryptKind, + key: openArray[byte], + iv: openArray[byte]): KfResult[seq[byte]] = + if cryptkind == AES128CTR: + if len(iv) != aes128.sizeBlock: + return err(IncorrectIV) + var plaintext = newSeqWith(ciphertext.len, 0.byte) + var ctx: CTR[aes128] + ctx.init(toOpenArray(key, 0, 15), iv) + ctx.decrypt(ciphertext, plaintext) + ctx.clear() + ok(plaintext) + else: + err(NotImplemented) + +proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNode] = + if kdfkind == SCRYPT: + let wf = if workfactor == 0: ScryptWorkFactor else: workfactor + ok(%* + { + "dklen": DKLen, + "n": wf, + "r": ScryptR, + "p": ScryptP, + "salt": salt + } + ) + elif kdfkind == PBKDF2: + let wf = if workfactor == 0: Pbkdf2WorkFactor else: workfactor + ok(%* + { + "dklen": DKLen, + "c": wf, + "prf": "hmac-sha256", + "salt": salt + } + ) + else: + err(NotImplemented) + +proc decodeHex(m: string): seq[byte] = + if len(m) > 0: + try: + result = utils.fromHex(m) + except CatchableError: + result = newSeq[byte]() + else: + result = newSeq[byte]() + +proc decodeSalt(m: string): string = + var sarr: seq[byte] + if len(m) > 0: + try: + sarr = utils.fromHex(m) + result = newString(len(sarr)) + copyMem(addr result[0], addr sarr[0], len(sarr)) + except CatchableError: + 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*(secret: openArray[byte], + password: string, + version: int = 3, + cryptkind: CryptKind = AES128CTR, + kdfkind: KdfKind = PBKDF2, + workfactor: int = 0): KfResult[JsonNode] = + ## 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 iv: array[aes128.sizeBlock, byte] + var salt: array[SaltSize, byte] + var saltstr = newString(SaltSize) + if randomBytes(iv) != aes128.sizeBlock: + return err(RandomError) + if randomBytes(salt) != SaltSize: + return err(RandomError) + copyMem(addr saltstr[0], addr salt[0], SaltSize) + + let u = ? uuidGenerate().mapErrTo(UuidError) + + let + dkey = case kdfkind + of PBKDF2: ? deriveKey(password, saltstr, kdfkind, HashSHA2_256, workfactor) + of SCRYPT: ? deriveKey(password, saltstr, workfactor, ScryptR, ScryptP) + + ciphertext = ? encryptData(secret, cryptkind, dkey, iv) + + var ctx: keccak256 + ctx.init() + ctx.update(toOpenArray(dkey, 16, 31)) + ctx.update(ciphertext) + var mac = ctx.finish() + ctx.clear() + + let params = ? kdfParams(kdfkind, toHex(salt, true), workfactor) + + ok(%* + { + "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 + } + ) + +proc decodeCrypto(n: JsonNode): KfResult[Crypto] = + var crypto = n.getOrDefault("crypto") + if isNil(crypto): + return err(MalformedError) + + var kdf = crypto.getOrDefault("kdf") + 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) + + 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 c.cipher.kind == CipherNoSupport: + return err(CipherNotSupported) + if len(c.cipher.text) == 0: + return err(EmptyCiphertext) + if len(c.mac) == 0: + return err(EmptyMac) + if isNil(c.kdfParams): + return err(MalformedError) + + result = ok(c) + +proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] = + var p: Pbkdf2Params + p.salt = decodeSalt(params.getOrDefault("salt").getStr()) + if len(p.salt) == 0: + return err(EmptySalt) + + p.dklen = params.getOrDefault("dklen").getInt() + p.c = params.getOrDefault("c").getInt() + p.prf = getPrfHash(params.getOrDefault("prf").getStr()) + + 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.dklen == 0 or p.dklen > MaxDKLen: + return err(IncorrectDKLen) + + result = ok(p) + +func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = + 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 = ? decryptData(crypto.cipher.text, crypto.cipher.kind, dkey, crypto.cipher.params.iv) + + ok(plaintext) + +proc decodeKeyFileJson*(j: JsonNode, + password: string): KfResult[seq[byte]] = + ## 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 = decryptSecret(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 = decryptSecret(crypto, dkey) + +proc loadKeyFile*(pathname: string, + password: string): KfResult[seq[byte]] = + ## Load and decode private key ``seckey`` from file with pathname + ## ``pathname``, using password string ``password``. + var data: JsonNode + try: + data = json.parseFile(pathname) + except JsonParsingError: + return err(JsonError) + except Exception: # json raises Exception + return err(OsError) + + decodeKeyFileJson(data, password) + +proc saveKeyFile*(pathname: string, + jobject: JsonNode): KfResult[void] = + ## Save JSON object ``jobject`` to file with pathname ``pathname``. + var + f: File + if not f.open(pathname, fmWrite): + return err(OsError) + try: + f.write($jobject) + ok() + except CatchableError: + err(OsError) + finally: + f.close() + From a6bf2be69e3f09e436dc054c191bf1f72d27ccdf Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Fri, 21 Oct 2022 17:18:22 +0200 Subject: [PATCH 02/20] feat(keyfile): make id and version field optional --- waku/v2/utils/keyfile.nim | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index bdb21cbcc9..5d738addec 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -40,7 +40,6 @@ type KdfNotSupported = "kf: KDF algorithm is not supported" CipherNotSupported = "kf: `cipher` parameter is not supported" IncorrectMac = "kf: `mac` verification failed" - IncorrectPrivateKey = "kf: incorrect private key" ScryptBadParam = "kf: bad scrypt's parameters" OsError = "kf: OS specific error" JsonError = "kf: JSON encoder/decoder error" @@ -86,9 +85,6 @@ type DKey = array[DKLen, byte] KfResult*[T] = Result[T, KeyFileError] -proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] = - r.mapErr(proc (e: E): KeyFileError = v) - const SupportedHashes = [ "sha224", "sha256", "sha384", "sha512", @@ -102,6 +98,13 @@ const HashSHA3_224, HashSHA3_256, HashSHA3_384, HashSHA3_512 ] + # When true, the keyfile json will contain "version" and "id" fields, respectively. Default to false. + VersionInKeyfile: bool = false + IdInKeyfile: bool = false + +proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] = + r.mapErr(proc (e: E): KeyFileError = v) + proc `$`(k: KdfKind): string = case k of SCRYPT: @@ -337,7 +340,7 @@ proc createKeyFileJson*(secret: openArray[byte], let params = ? kdfParams(kdfkind, toHex(salt, true), workfactor) - ok(%* + let json = %* { "crypto": { "cipher": $cryptkind, @@ -349,10 +352,16 @@ proc createKeyFileJson*(secret: openArray[byte], "kdfparams": params, "mac": toHex(mac.data, true), }, - "id": $u, - "version": version } - ) + + if IdInKeyfile: + json.add("id", %($u)) + if VersionInKeyfile: + json.add("version", %version) + + echo json + + ok(json) proc decodeCrypto(n: JsonNode): KfResult[Crypto] = var crypto = n.getOrDefault("crypto") From 31a97873ec8890b7caa35639dcc5a86c109b4d53 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:14:04 +0200 Subject: [PATCH 03/20] feat(rln): enable input password for keyfile encryption --- apps/chat2/config_chat2.nim | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index 6a4d8648bc..566ca238fa 100644 --- a/apps/chat2/config_chat2.nim +++ b/apps/chat2/config_chat2.nim @@ -290,6 +290,11 @@ type desc: "Address of membership contract on an Ethereum testnet", defaultValue: "" name: "rln-relay-eth-contract-address" }: string + + rlnRelayCredentialsPassword* {. + desc: "Password for encrypting RLN credentials", + defaultValue: "" + name: "rln-relay-cred-password" }: string # NOTE: Keys are different in nim-libp2p proc parseCmdArg*(T: type crypto.PrivateKey, p: TaintedString): T = From 1d79fbfc7213a5861d2bcf4cca39bbe33739a469 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:15:20 +0200 Subject: [PATCH 04/20] refactor(keyfile): store multiple keyfiles in single file --- tests/v2/test_keyfile.nim | 23 +++++++---- waku/v2/utils/keyfile.nim | 81 ++++++++++++++++++++++----------------- 2 files changed, 61 insertions(+), 43 deletions(-) diff --git a/tests/v2/test_keyfile.nim b/tests/v2/test_keyfile.nim index c0715d9b13..feba0279d7 100644 --- a/tests/v2/test_keyfile.nim +++ b/tests/v2/test_keyfile.nim @@ -13,13 +13,22 @@ suite "KeyFile test suite": let rng = newRng() - test "Create/Save/Load test": - var secret = randomSeqByte(rng[], 100) - let jobject = createKeyFileJson(secret, "randompassword")[] + test "Create/Save/Load keyfile test": + + let password = "randompassword" + let filepath = "./test.keyfile" + + var secret = randomSeqByte(rng[], 300) + let keyfile = createKeyFileJson(secret, password) check: - saveKeyFile("test.keyfile", jobject).isOk() - var decodedSecret = loadKeyFile("test.keyfile", "randompassword")[] + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + var decodedSecret = loadKeyFile("test.keyfile", password) + check: - secret == decodedSecret - removeFile("test.keyfile") \ No newline at end of file + decodedSecret.isOk() + secret == decodedSecret.get() + + removeFile(filepath) \ No newline at end of file diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index 5d738addec..61f6cacb40 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -1,5 +1,7 @@ -# This implementation is taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile -# and adapted to create keyfiles for arbitrary-long byte data (rather than fixed-size private keys) +# This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to +# - create keyfiles for arbitrary-long input byte data (rather than fixed-size private keys) +# - allow storage of multiple keyfiles (encrypted with different passwords) in same file (the secret of the first keyfile successfully decrypted is returned or none) +# - enable/disable at compilation time the keyfile id and version fields {.push raises: [Defect].} @@ -24,25 +26,25 @@ const type KeyFileError* = enum - RandomError = "kf: Random generator error" - UuidError = "kf: UUID generator error" - BufferOverrun = "kf: Supplied buffer is too small" - IncorrectDKLen = "kf: `dklen` parameter is 0 or more then MaxDKLen" - MalformedError = "kf: JSON has incorrect structure" - NotImplemented = "kf: Feature is not implemented" - NotSupported = "kf: Feature is not supported" - EmptyMac = "kf: `mac` parameter is zero length or not in hexadecimal form" - EmptyCiphertext = "kf: `ciphertext` parameter is zero length or not in hexadecimal format" - EmptySalt = "kf: `salt` parameter is zero length or not in hexadecimal format" - EmptyIV = "kf: `cipherparams.iv` parameter is zero length or not in hexadecimal format" - IncorrectIV = "kf: Size of IV vector is not equal to cipher block size" - PrfNotSupported = "kf: PRF algorithm for PBKDF2 is not supported" - KdfNotSupported = "kf: KDF algorithm is not supported" - CipherNotSupported = "kf: `cipher` parameter is not supported" - IncorrectMac = "kf: `mac` verification failed" - ScryptBadParam = "kf: bad scrypt's parameters" - OsError = "kf: OS specific error" - JsonError = "kf: JSON encoder/decoder error" + RandomError = "keyfile error: Random generator error" + UuidError = "keyfile error: UUID generator error" + BufferOverrun = "keyfile error: Supplied buffer is too small" + IncorrectDKLen = "keyfile error: `dklen` parameter is 0 or more then MaxDKLen" + MalformedError = "keyfile error: JSON has incorrect structure" + NotImplemented = "keyfile error: Feature is not implemented" + NotSupported = "keyfile error: Feature is not supported" + EmptyMac = "keyfile error: `mac` parameter is zero length or not in hexadecimal form" + EmptyCiphertext = "keyfile error: `ciphertext` parameter is zero length or not in hexadecimal format" + EmptySalt = "keyfile error: `salt` parameter is zero length or not in hexadecimal format" + EmptyIV = "keyfile error: `cipherparams.iv` parameter is zero length or not in hexadecimal format" + IncorrectIV = "keyfile error: Size of IV vector is not equal to cipher block size" + PrfNotSupported = "keyfile error: PRF algorithm for PBKDF2 is not supported" + KdfNotSupported = "keyfile error: KDF algorithm is not supported" + CipherNotSupported = "keyfile error: `cipher` parameter is not supported" + IncorrectMac = "keyfile error: `mac` verification failed" + ScryptBadParam = "keyfile error: bad scrypt's parameters" + OsError = "keyfile error: OS specific error" + JsonError = "keyfile error: JSON encoder/decoder error" KdfKind* = enum PBKDF2, ## PBKDF2 @@ -304,11 +306,11 @@ proc createKeyFileJson*(secret: openArray[byte], workfactor: int = 0): KfResult[JsonNode] = ## Create JSON object with keyfile structure. ## - ## ``seckey`` - private key, which will be stored + ## ``secret`` - secret data, 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 + ## ``cryptkind`` - algorithm for encryption ## (default is AES128-CTR) ## ``kdfkind`` - algorithm for key deriviation function (default is PBKDF2) ## ``workfactor`` - Key deriviation function work factor, 0 is to use @@ -359,8 +361,6 @@ proc createKeyFileJson*(secret: openArray[byte], if VersionInKeyfile: json.add("version", %version) - echo json - ok(json) proc decodeCrypto(n: JsonNode): KfResult[Crypto] = @@ -446,7 +446,7 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = proc decodeKeyFileJson*(j: JsonNode, password: string): KfResult[seq[byte]] = - ## Decode private key into ``seckey`` from keyfile json object ``j`` using + ## Decode secret from keyfile json object ``j`` using ## password string ``password``. let res = decodeCrypto(j) if res.isErr: @@ -473,28 +473,37 @@ proc decodeKeyFileJson*(j: JsonNode, result = decryptSecret(crypto, dkey) proc loadKeyFile*(pathname: string, - password: string): KfResult[seq[byte]] = - ## Load and decode private key ``seckey`` from file with pathname + password: string): KfResult[seq[byte]] {.raises: [Defect, IOError].} = + ## Load and decode data from file with pathname ## ``pathname``, using password string ``password``. var data: JsonNode - try: - data = json.parseFile(pathname) - except JsonParsingError: - return err(JsonError) - except Exception: # json raises Exception - return err(OsError) + var decodedKeyfile: KfResult[seq[byte]] + + for keyfile in lines(pathname): + try: + data = json.parseJson(keyfile) + except JsonParsingError: + return err(JsonError) + except Exception: # json raises Exception + return err(OsError) + + decodedKeyfile = decodeKeyFileJson(data, password) + if decodedKeyfile.isOk(): + break - decodeKeyFileJson(data, password) + return decodedKeyfile +# Note that the keyfile is open in Append mode so that multiple credentials can be stored in same file proc saveKeyFile*(pathname: string, jobject: JsonNode): KfResult[void] = ## Save JSON object ``jobject`` to file with pathname ``pathname``. var f: File - if not f.open(pathname, fmWrite): + if not f.open(pathname, fmAppend): return err(OsError) try: f.write($jobject) + f.write("\n") ok() except CatchableError: err(OsError) From 0ded30f73011c0fae5f95b1595e8cfc3fa053cbc Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:16:21 +0200 Subject: [PATCH 05/20] refactor(rln): write and read rln credentials proc using keyfiles + test update --- tests/all_tests_v2.nim | 1 + tests/v2/test_waku_rln_relay.nim | 19 ++++--- .../waku_rln_relay/waku_rln_relay_utils.nim | 53 ++++++++++++++----- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/tests/all_tests_v2.nim b/tests/all_tests_v2.nim index be2c6c1308..c7a755bfcf 100644 --- a/tests/all_tests_v2.nim +++ b/tests/all_tests_v2.nim @@ -42,6 +42,7 @@ import ./v2/test_peer_exchange, ./v2/test_waku_noise, ./v2/test_waku_noise_sessions, + # Utils ./v2/test_keyfile when defined(rln) or defined(rlnzerokit): diff --git a/tests/v2/test_waku_rln_relay.nim b/tests/v2/test_waku_rln_relay.nim index 80be995524..7ba6bd6aaf 100644 --- a/tests/v2/test_waku_rln_relay.nim +++ b/tests/v2/test_waku_rln_relay.nim @@ -2,7 +2,7 @@ {.used.} import - std/options, sequtils, times, deques, + std/[options,os], sequtils, times, deques, testutils/unittests, chronos, chronicles, stint, stew/byteutils, stew/shims/net as stewNet, libp2p/crypto/crypto, @@ -979,7 +979,7 @@ suite "Waku rln relay": check: keypair.get().idCommitment == idCommitment - test "Read Persistent RLN credentials": + test "Read/Write RLN credentials": # create an RLN instance var rlnInstance = createRLNInstance() check: @@ -1002,16 +1002,21 @@ suite "Waku rln relay": var rlnMembershipCredentials = RlnMembershipCredentials(membershipKeyPair: k, rlnIndex: index) - let path = "testPath.txt" + let filepath = "./testRLNCredentials.txt" + let password = "%m0um0ucoW%" # Write RLN credentials - writeFile(path, pretty(%rlnMembershipCredentials)) + check: + writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk() - var credentials = readPersistentRlnCredentials(path) + var credentials = readRlnCredentials(filepath, password) check: - credentials.membershipKeyPair == k - credentials.rlnIndex == index + credentials.isSome() + credentials.get().membershipKeyPair == k + credentials.get().rlnIndex == index + + removeFile(filepath) test "histogram static bucket generation": let buckets = generateBucketsForHistogram(10) diff --git a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim index 3b4f983c51..fd2e00020d 100644 --- a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim +++ b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim @@ -15,7 +15,7 @@ import waku_rln_relay_constants, waku_rln_relay_types, waku_rln_relay_metrics, - ../../utils/time, + ../../utils/[time, keyfile], ../../node/waku_node, ../../../../../apps/wakunode2/config, ## TODO: Decouple the protocol code from the app configuration ../../../../../apps/chat2/config_chat2, ## TODO: Decouple the protocol code from the app configuration @@ -38,6 +38,18 @@ contract(MembershipContract): # proc withdraw(secret: Uint256, pubkeyIndex: Uint256, receiver: Address) # proc withdrawBatch( secrets: seq[Uint256], pubkeyIndex: seq[Uint256], receiver: seq[Address]) +# Note: works only for non empty input +proc toString(bytes: openArray[byte]): string = + var output = newString(bytes.len) + copyMem(output[0].addr, bytes[0].unsafeAddr, bytes.len) + return output + +# Note: works only for non empty input +proc toSeqByte(str: string): seq[byte] = + var output = newSeq[byte](str.len) + copyMem(output[0].addr, str[0].unsafeAddr, str.len) + return output + proc toBuffer*(x: openArray[byte]): Buffer = ## converts the input to a Buffer object ## the Buffer object is used to communicate data with the rln lib @@ -1145,19 +1157,31 @@ proc mountRlnRelayDynamic*(node: WakuNode, node.wakuRlnRelay = rlnPeer return ok(true) -proc readPersistentRlnCredentials*(path: string) : RlnMembershipCredentials {.raises: [Defect, OSError, IOError, Exception].} = - info "Rln credentials exist in file" +proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, password: string) : KfResult[void] {.raises: [Defect, OSError, IOError, Exception].} = + info "Storing RLN credentials" + var jsonString: string + jsonString.toUgly(%credentials) + let keyfile = createKeyFileJson(toSeqByte(jsonString), password) + if keyfile.isErr(): + return err(keyfile.error) + return saveKeyFile(path, keyfile.get()) + +proc readRlnCredentials*(path: string, password: string) : Option[RlnMembershipCredentials] {.raises: [Defect, OSError, IOError, Exception].} = + info "Reading RLN credentials" # With regards to printing the keys, it is purely for debugging purposes so that the user becomes explicitly aware of the current keys in use when nwaku is started. # Note that this is only until the RLN contract being used is the one deployed on Goerli testnet. # These prints need to omitted once RLN contract is deployed on Ethereum mainnet and using valuable funds for staking. waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: - let entireRlnCredentialsFile = readFile(path) - - let jsonObject = parseJson(entireRlnCredentialsFile) - let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials) - - debug "Deserialized Rln credentials", rlnCredentials=deserializedRlnCredentials - return deserializedRlnCredentials + let entireRlnCredentialsFile = loadKeyFile(path, password) + if entireRlnCredentialsFile.isOk(): + let jsonObject = parseJson(toString(entireRlnCredentialsFile.get())) + let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials) + debug "Deserialized RLN credentials", rlnCredentials=deserializedRlnCredentials + return some(deserializedRlnCredentials) + else: + debug "Unable to decrypt RLN credentials with provided password. ", error=entireRlnCredentialsFile.error + echo "Unable to decrypt RLN credentials with provided password. ", entireRlnCredentialsFile.error + return none(RlnMembershipCredentials) proc mount(node: WakuNode, conf: WakuNodeConf|Chat2Conf, @@ -1220,9 +1244,10 @@ proc mount(node: WakuNode, let rlnRelayCredPath = joinPath(conf.rlnRelayCredPath, RlnCredentialsFilename) debug "rln-relay credential path", rlnRelayCredPath # check if there is an rln-relay credential file in the supplied path - if fileExists(rlnRelayCredPath): + if fileExists(rlnRelayCredPath): + info "A RLN credential file exists in provided path" # retrieve rln-relay credential - credentials = some(readPersistentRlnCredentials(rlnRelayCredPath)) + credentials = readRlnCredentials(rlnRelayCredPath, conf.rlnRelayCredentialsPassword) else: # there is no credential file available in the supplied path # mount the rln-relay protocol leaving rln-relay credentials arguments unassigned @@ -1256,8 +1281,8 @@ proc mount(node: WakuNode, # persist rln credential credentials = some(RlnMembershipCredentials(rlnIndex: node.wakuRlnRelay.membershipIndex, membershipKeyPair: node.wakuRlnRelay.membershipKeyPair)) - writeFile(rlnRelayCredPath, pretty(%credentials.get())) - + if writeRlnCredentials(rlnRelayCredPath, credentials.get(), conf.rlnRelayCredentialsPassword).isErr(): + return err("error in storing rln credentials") else: # do not persist or use a persisted rln-relay credential From 2a6464be1440f081f807237c8711d6612e4cb722 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Sat, 22 Oct 2022 02:59:31 +0200 Subject: [PATCH 06/20] feat(keyfile): allow skip successful decryption to iterate among multiple keyfiles + test --- tests/v2/test_keyfile.nim | 87 ++++++++++++++++++++++++++++++++++++++- waku/v2/utils/keyfile.nim | 12 ++++-- 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/tests/v2/test_keyfile.nim b/tests/v2/test_keyfile.nim index feba0279d7..c8e498e486 100644 --- a/tests/v2/test_keyfile.nim +++ b/tests/v2/test_keyfile.nim @@ -13,7 +13,7 @@ suite "KeyFile test suite": let rng = newRng() - test "Create/Save/Load keyfile test": + test "Create/Save/Load single keyfile": let password = "randompassword" let filepath = "./test.keyfile" @@ -31,4 +31,89 @@ suite "KeyFile test suite": decodedSecret.isOk() secret == decodedSecret.get() + removeFile(filepath) + + test "Create/Save/Load multiple keyfiles in same file": + + let password1 = "password1" + let password2 = "password2" + let password3 = "password3" + let filepath = "./test.keyfile" + var keyfile: KfResult[JsonNode] + + let secret1 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret1, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + let secret2 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret2, password2) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + let secret3 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret3, password3) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret4 with password3 + let secret4 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret4, password3) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret5 with password1 + let secret5 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret5, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret5 with password1 + let secret6 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret6, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # Now there are 5 keyfiles stored in filepath encrypted with 3 different passwords + # We decode with the respective passwords + + var decodedSecret1 = loadKeyFile("test.keyfile", password1) + check: + decodedSecret1.isOk() + secret1 == decodedSecret1.get() + + var decodedSecret2 = loadKeyFile("test.keyfile", password2) + check: + decodedSecret2.isOk() + secret2 == decodedSecret2.get() + + var decodedSecret3 = loadKeyFile("test.keyfile", password3) + check: + decodedSecret3.isOk() + secret3 == decodedSecret3.get() + + # Since we have 2 keyfiles encrypted with same password3, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret3 + var decodedSecret4 = loadKeyFile("test.keyfile", password3, skip = 1) + check: + decodedSecret4.isOk() + secret4 == decodedSecret4.get() + + # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret1 + var decodedSecret5 = loadKeyFile("test.keyfile", password1, skip = 1) + check: + decodedSecret5.isOk() + secret5 == decodedSecret5.get() + + # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first 2 successful decryptions, i.e. the keyfile for secret1 and secret5 + var decodedSecret6 = loadKeyFile("test.keyfile", password1, skip = 2) + check: + decodedSecret6.isOk() + secret6 == decodedSecret6.get() + removeFile(filepath) \ No newline at end of file diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index 61f6cacb40..f525f4843e 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -1,6 +1,6 @@ # This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to # - create keyfiles for arbitrary-long input byte data (rather than fixed-size private keys) -# - allow storage of multiple keyfiles (encrypted with different passwords) in same file (the secret of the first keyfile successfully decrypted is returned or none) +# - allow storage of multiple keyfiles (encrypted with different passwords) in same file and iteration among successful decryptions # - enable/disable at compilation time the keyfile id and version fields {.push raises: [Defect].} @@ -473,11 +473,14 @@ proc decodeKeyFileJson*(j: JsonNode, result = decryptSecret(crypto, dkey) proc loadKeyFile*(pathname: string, - password: string): KfResult[seq[byte]] {.raises: [Defect, IOError].} = + password: string, skip: int32 = 0): KfResult[seq[byte]] {.raises: [Defect, IOError].} = ## Load and decode data from file with pathname ## ``pathname``, using password string ``password``. + ## If skip is non-zero, the first skip successful decryptions are skipped var data: JsonNode var decodedKeyfile: KfResult[seq[byte]] + var skipDecryptSuccesses = skip + echo "skip ", skipDecryptSuccesses for keyfile in lines(pathname): try: @@ -489,7 +492,10 @@ proc loadKeyFile*(pathname: string, decodedKeyfile = decodeKeyFileJson(data, password) if decodedKeyfile.isOk(): - break + if skipDecryptSuccesses <= 0: + break + else: + skipDecryptSuccesses -= 1 return decodedKeyfile From 7dc2469c855042493f4c020264ade8c47c44bd01 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Sat, 22 Oct 2022 03:03:27 +0200 Subject: [PATCH 07/20] fix(rln): add rln-relay-cred-password in wakunode2 config --- apps/wakunode2/config.nim | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/wakunode2/config.nim b/apps/wakunode2/config.nim index 9f9fa87029..ebc1fa7130 100644 --- a/apps/wakunode2/config.nim +++ b/apps/wakunode2/config.nim @@ -174,7 +174,12 @@ type desc: "Address of membership contract on an Ethereum testnet", defaultValue: "" name: "rln-relay-eth-contract-address" }: string - + + rlnRelayCredentialsPassword* {. + desc: "Password for encrypting RLN credentials", + defaultValue: "" + name: "rln-relay-cred-password" }: string + staticnodes* {. desc: "Peer multiaddr to directly connect with. Argument may be repeated." name: "staticnode" }: seq[string] From 7c2c1f9ebe20234b175c850de65bf4cd315de219 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Sat, 22 Oct 2022 03:06:32 +0200 Subject: [PATCH 08/20] chore(keyfile): remove unnencessary echo --- waku/v2/utils/keyfile.nim | 1 - 1 file changed, 1 deletion(-) diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index f525f4843e..62b8c37fe5 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -480,7 +480,6 @@ proc loadKeyFile*(pathname: string, var data: JsonNode var decodedKeyfile: KfResult[seq[byte]] var skipDecryptSuccesses = skip - echo "skip ", skipDecryptSuccesses for keyfile in lines(pathname): try: From 517c3b14aaf7d1646f5cc6187350204d76d67aab Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Tue, 25 Oct 2022 01:03:33 +0200 Subject: [PATCH 09/20] refactor(rln/kesyore): address reviewers' comments --- tests/all_tests_v2.nim | 2 +- tests/v2/test_keyfile.nim | 119 ----------------- tests/v2/test_utils_keyfile.nim | 125 ++++++++++++++++++ tests/v2/test_waku_rln_relay.nim | 28 ++-- .../waku_rln_relay/waku_rln_relay_utils.nim | 68 +++++----- waku/v2/utils/keyfile.nim | 63 +++++---- 6 files changed, 219 insertions(+), 186 deletions(-) delete mode 100644 tests/v2/test_keyfile.nim create mode 100644 tests/v2/test_utils_keyfile.nim diff --git a/tests/all_tests_v2.nim b/tests/all_tests_v2.nim index 908713f3ae..fcefa198e8 100644 --- a/tests/all_tests_v2.nim +++ b/tests/all_tests_v2.nim @@ -45,7 +45,7 @@ import ./v2/test_waku_noise, ./v2/test_waku_noise_sessions, # Utils - ./v2/test_keyfile + ./v2/test_utils_keyfile when defined(rln) or defined(rlnzerokit): import diff --git a/tests/v2/test_keyfile.nim b/tests/v2/test_keyfile.nim deleted file mode 100644 index c8e498e486..0000000000 --- a/tests/v2/test_keyfile.nim +++ /dev/null @@ -1,119 +0,0 @@ -{.used.} - -import - std/[json, os], - testutils/unittests, chronos, chronicles, - eth/keys, - ../../waku/v2/utils/keyfile, - ../test_helpers - -from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte - -suite "KeyFile test suite": - - let rng = newRng() - - test "Create/Save/Load single keyfile": - - let password = "randompassword" - let filepath = "./test.keyfile" - - var secret = randomSeqByte(rng[], 300) - let keyfile = createKeyFileJson(secret, password) - - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - var decodedSecret = loadKeyFile("test.keyfile", password) - - check: - decodedSecret.isOk() - secret == decodedSecret.get() - - removeFile(filepath) - - test "Create/Save/Load multiple keyfiles in same file": - - let password1 = "password1" - let password2 = "password2" - let password3 = "password3" - let filepath = "./test.keyfile" - var keyfile: KfResult[JsonNode] - - let secret1 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret1, password1) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - let secret2 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret2, password2) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - let secret3 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret3, password3) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # We encrypt secret4 with password3 - let secret4 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret4, password3) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # We encrypt secret5 with password1 - let secret5 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret5, password1) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # We encrypt secret5 with password1 - let secret6 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret6, password1) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # Now there are 5 keyfiles stored in filepath encrypted with 3 different passwords - # We decode with the respective passwords - - var decodedSecret1 = loadKeyFile("test.keyfile", password1) - check: - decodedSecret1.isOk() - secret1 == decodedSecret1.get() - - var decodedSecret2 = loadKeyFile("test.keyfile", password2) - check: - decodedSecret2.isOk() - secret2 == decodedSecret2.get() - - var decodedSecret3 = loadKeyFile("test.keyfile", password3) - check: - decodedSecret3.isOk() - secret3 == decodedSecret3.get() - - # Since we have 2 keyfiles encrypted with same password3, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret3 - var decodedSecret4 = loadKeyFile("test.keyfile", password3, skip = 1) - check: - decodedSecret4.isOk() - secret4 == decodedSecret4.get() - - # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret1 - var decodedSecret5 = loadKeyFile("test.keyfile", password1, skip = 1) - check: - decodedSecret5.isOk() - secret5 == decodedSecret5.get() - - # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first 2 successful decryptions, i.e. the keyfile for secret1 and secret5 - var decodedSecret6 = loadKeyFile("test.keyfile", password1, skip = 2) - check: - decodedSecret6.isOk() - secret6 == decodedSecret6.get() - - removeFile(filepath) \ No newline at end of file diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim new file mode 100644 index 0000000000..46a644e775 --- /dev/null +++ b/tests/v2/test_utils_keyfile.nim @@ -0,0 +1,125 @@ +{.used.} + +import + std/[json, os], + testutils/unittests, chronos, chronicles, + eth/keys +import + ../../waku/v2/utils/keyfile, + ../test_helpers + +from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte + +suite "KeyFile test suite": + + let rng = newRng() + + test "Create/Save/Load single keyfile": + + let password = "randompassword" + let filepath = "./test.keyfile" + + try: + + var secret = randomSeqByte(rng[], 300) + let keyfile = createKeyFileJson(secret, password) + + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + var decodedSecret = loadKeyFile("test.keyfile", password) + + check: + decodedSecret.isOk() + secret == decodedSecret.get() + + finally: + removeFile(filepath) + + test "Create/Save/Load multiple keyfiles in same file": + + let password1 = "password1" + let password2 = "password2" + let password3 = "password3" + let filepath = "./test.keyfile" + var keyfile: KfResult[JsonNode] + + try: + let secret1 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret1, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + let secret2 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret2, password2) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + let secret3 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret3, password3) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret4 with password3 + let secret4 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret4, password3) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret5 with password1 + let secret5 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret5, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret5 with password1 + let secret6 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret6, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # Now there are 5 keyfiles stored in filepath encrypted with 3 different passwords + # We decode with the respective passwords + + var decodedSecret1 = loadKeyFile("test.keyfile", password1) + check: + decodedSecret1.isOk() + secret1 == decodedSecret1.get() + + var decodedSecret2 = loadKeyFile("test.keyfile", password2) + check: + decodedSecret2.isOk() + secret2 == decodedSecret2.get() + + var decodedSecret3 = loadKeyFile("test.keyfile", password3) + check: + decodedSecret3.isOk() + secret3 == decodedSecret3.get() + + # Since we have 2 keyfiles encrypted with same password3, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret3 + var decodedSecret4 = loadKeyFile("test.keyfile", password3, skip = 1) + check: + decodedSecret4.isOk() + secret4 == decodedSecret4.get() + + # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret1 + var decodedSecret5 = loadKeyFile("test.keyfile", password1, skip = 1) + check: + decodedSecret5.isOk() + secret5 == decodedSecret5.get() + + # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first 2 successful decryptions, i.e. the keyfile for secret1 and secret5 + var decodedSecret6 = loadKeyFile("test.keyfile", password1, skip = 2) + check: + decodedSecret6.isOk() + secret6 == decodedSecret6.get() + + finally: + removeFile(filepath) \ No newline at end of file diff --git a/tests/v2/test_waku_rln_relay.nim b/tests/v2/test_waku_rln_relay.nim index c0766a3a5a..98702a1133 100644 --- a/tests/v2/test_waku_rln_relay.nim +++ b/tests/v2/test_waku_rln_relay.nim @@ -2,7 +2,7 @@ {.used.} import - std/[options,os], sequtils, times, deques, + std/[options, os, sequtils, times, deques], testutils/unittests, chronos, chronicles, stint, stew/byteutils, stew/shims/net as stewNet, libp2p/crypto/crypto, @@ -1006,19 +1006,25 @@ suite "Waku rln relay": let filepath = "./testRLNCredentials.txt" let password = "%m0um0ucoW%" - # Write RLN credentials - check: - writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk() + try: + # Write RLN credentials + check: + writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk() - var credentials = readRlnCredentials(filepath, password) + let readCredentialsResult = readRlnCredentials(filepath, password) + check: + readCredentialsResult.isOk() - check: - credentials.isSome() - credentials.get().membershipKeyPair == k - credentials.get().rlnIndex == index + let credentials = readCredentialsResult.get() + + check: + credentials.isSome() + credentials.get().membershipKeyPair == k + credentials.get().rlnIndex == index + + finally: + removeFile(filepath) - removeFile(filepath) - test "histogram static bucket generation": let buckets = generateBucketsForHistogram(10) diff --git a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim index fd2e00020d..e36055101b 100644 --- a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim +++ b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim @@ -15,7 +15,8 @@ import waku_rln_relay_constants, waku_rln_relay_types, waku_rln_relay_metrics, - ../../utils/[time, keyfile], + ../../utils/time, + ../../utils/keyfile, ../../node/waku_node, ../../../../../apps/wakunode2/config, ## TODO: Decouple the protocol code from the app configuration ../../../../../apps/chat2/config_chat2, ## TODO: Decouple the protocol code from the app configuration @@ -38,18 +39,6 @@ contract(MembershipContract): # proc withdraw(secret: Uint256, pubkeyIndex: Uint256, receiver: Address) # proc withdrawBatch( secrets: seq[Uint256], pubkeyIndex: seq[Uint256], receiver: seq[Address]) -# Note: works only for non empty input -proc toString(bytes: openArray[byte]): string = - var output = newString(bytes.len) - copyMem(output[0].addr, bytes[0].unsafeAddr, bytes.len) - return output - -# Note: works only for non empty input -proc toSeqByte(str: string): seq[byte] = - var output = newSeq[byte](str.len) - copyMem(output[0].addr, str[0].unsafeAddr, str.len) - return output - proc toBuffer*(x: openArray[byte]): Buffer = ## converts the input to a Buffer object ## the Buffer object is used to communicate data with the rln lib @@ -1157,31 +1146,39 @@ proc mountRlnRelayDynamic*(node: WakuNode, node.wakuRlnRelay = rlnPeer return ok(true) -proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, password: string) : KfResult[void] {.raises: [Defect, OSError, IOError, Exception].} = +proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, password: string) : RlnRelayResult[void] = info "Storing RLN credentials" var jsonString: string jsonString.toUgly(%credentials) - let keyfile = createKeyFileJson(toSeqByte(jsonString), password) + let keyfile = createKeyFileJson(toBytes(jsonString), password) if keyfile.isErr(): - return err(keyfile.error) - return saveKeyFile(path, keyfile.get()) + return err("Error while creating keyfile for RLN credentials") + if saveKeyFile(path, keyfile.get()).isErr(): + return err("Error while saving keyfile for RLN credentials") + return ok() -proc readRlnCredentials*(path: string, password: string) : Option[RlnMembershipCredentials] {.raises: [Defect, OSError, IOError, Exception].} = +proc readRlnCredentials*(path: string, password: string) : RlnRelayResult[Option[RlnMembershipCredentials]] {.raises: [Defect, IOError, Exception].} = info "Reading RLN credentials" # With regards to printing the keys, it is purely for debugging purposes so that the user becomes explicitly aware of the current keys in use when nwaku is started. # Note that this is only until the RLN contract being used is the one deployed on Goerli testnet. # These prints need to omitted once RLN contract is deployed on Ethereum mainnet and using valuable funds for staking. waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: - let entireRlnCredentialsFile = loadKeyFile(path, password) - if entireRlnCredentialsFile.isOk(): - let jsonObject = parseJson(toString(entireRlnCredentialsFile.get())) - let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials) - debug "Deserialized RLN credentials", rlnCredentials=deserializedRlnCredentials - return some(deserializedRlnCredentials) - else: - debug "Unable to decrypt RLN credentials with provided password. ", error=entireRlnCredentialsFile.error - echo "Unable to decrypt RLN credentials with provided password. ", entireRlnCredentialsFile.error - return none(RlnMembershipCredentials) + + try: + var loadedRlnCredentialsFile = loadKeyFile(path, password) + + if loadedRlnCredentialsFile.isOk(): + let jsonObject = parseJson(string.fromBytes(loadedRlnCredentialsFile.get())) + let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials) + debug "Deserialized RLN credentials", rlnCredentials=deserializedRlnCredentials + return ok(some(deserializedRlnCredentials)) + else: + debug "Unable to decrypt RLN credentials with provided password. ", error=loadedRlnCredentialsFile.error + return ok(none(RlnMembershipCredentials)) + + except IOError: + return err("IOError while loading keyfile for RLN credentials at " & path) + proc mount(node: WakuNode, conf: WakuNodeConf|Chat2Conf, @@ -1241,14 +1238,23 @@ proc mount(node: WakuNode, # if the path does not contain any credential file, then a new set is generated and pesisted in the same path # if there is a credential file, then no new credentials are generated, instead the content of the file is read and used to mount rln-relay if conf.rlnRelayCredPath != "": + let rlnRelayCredPath = joinPath(conf.rlnRelayCredPath, RlnCredentialsFilename) debug "rln-relay credential path", rlnRelayCredPath + # check if there is an rln-relay credential file in the supplied path if fileExists(rlnRelayCredPath): - info "A RLN credential file exists in provided path" + + info "A RLN credential file exists in provided path", path=rlnRelayCredPath + # retrieve rln-relay credential - credentials = readRlnCredentials(rlnRelayCredPath, conf.rlnRelayCredentialsPassword) - + let readCredentialsRes = readRlnCredentials(rlnRelayCredPath, conf.rlnRelayCredentialsPassword) + + if readCredentialsRes.isErr(): + return err("RLN credentials cannot be read: " & readCredentialsRes.error()) + + credentials = readCredentialsRes.get() + else: # there is no credential file available in the supplied path # mount the rln-relay protocol leaving rln-relay credentials arguments unassigned # this infroms mountRlnRelayDynamic proc that new credentials should be generated and registered to the membership contract diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index 62b8c37fe5..7f33e4ea4e 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -110,34 +110,32 @@ proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] = proc `$`(k: KdfKind): string = case k of SCRYPT: - result = "scrypt" + return "scrypt" else: - result = "pbkdf2" + return "pbkdf2" proc `$`(k: CryptKind): string = case k of AES128CTR: - result = "aes-128-ctr" + return "aes-128-ctr" else: - result = "aes-128-ctr" + return "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 + return SupportedHashesKinds[res] + return HashNoSupport proc getCipher(c: string): CryptKind = var cl = c.toLowerAscii() if cl == "aes-128-ctr": - result = AES128CTR + return AES128CTR else: - result = CipherNoSupport + return CipherNoSupport proc deriveKey(password: string, salt: string, @@ -151,50 +149,62 @@ proc deriveKey(password: string, of HashSHA2_224: var ctx: HMAC[sha224] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA2_256: var ctx: HMAC[sha256] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA2_384: var ctx: HMAC[sha384] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA2_512: var ctx: HMAC[sha512] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashKECCAK224: var ctx: HMAC[keccak224] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashKECCAK256: var ctx: HMAC[keccak256] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashKECCAK384: var ctx: HMAC[keccak384] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashKECCAK512: var ctx: HMAC[keccak512] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA3_224: var ctx: HMAC[sha3_224] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA3_256: var ctx: HMAC[sha3_256] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA3_384: var ctx: HMAC[sha3_384] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) of HashSHA3_512: var ctx: HMAC[sha3_512] discard ctx.pbkdf2(password, salt, c, output) + ctx.clear() ok(output) else: err(PrfNotSupported) @@ -216,7 +226,7 @@ proc deriveKey(password: string, salt: string, if scrypt(password, salt, wf, r, p, output) == 0: return err(ScryptBadParam) - result = ok(output) + return ok(output) proc encryptData(secret: openArray[byte], cryptkind: CryptKind, @@ -276,27 +286,30 @@ proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNo proc decodeHex(m: string): seq[byte] = if len(m) > 0: try: - result = utils.fromHex(m) + return utils.fromHex(m) except CatchableError: - result = newSeq[byte]() + return newSeq[byte]() else: - result = newSeq[byte]() + return newSeq[byte]() proc decodeSalt(m: string): string = var sarr: seq[byte] if len(m) > 0: try: sarr = utils.fromHex(m) - result = newString(len(sarr)) - copyMem(addr result[0], addr sarr[0], len(sarr)) + var output = newString(len(sarr)) + copyMem(addr output[0], addr sarr[0], len(sarr)) + return output except CatchableError: - result = "" + return "" else: - result = "" + return "" 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)) + return equalMem(unsafeAddr m1[0], unsafeAddr m2[0], len(m1)) + else: + return false proc createKeyFileJson*(secret: openArray[byte], password: string, @@ -397,7 +410,7 @@ proc decodeCrypto(n: JsonNode): KfResult[Crypto] = if isNil(c.kdfParams): return err(MalformedError) - result = ok(c) + return ok(c) proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] = var p: Pbkdf2Params @@ -413,7 +426,8 @@ proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] = return err(PrfNotSupported) if p.dklen == 0 or p.dklen > MaxDKLen: return err(IncorrectDKLen) - result = ok(p) + + return ok(p) proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] = var p: ScryptParams @@ -429,7 +443,7 @@ proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] = if p.dklen == 0 or p.dklen > MaxDKLen: return err(IncorrectDKLen) - result = ok(p) + return ok(p) func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = var ctx: keccak256 @@ -437,6 +451,7 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = ctx.update(toOpenArray(dkey, 16, 31)) ctx.update(crypto.cipher.text) var mac = ctx.finish() + ctx.clear() if not compareMac(mac.data, crypto.mac): return err(IncorrectMac) @@ -461,7 +476,7 @@ proc decodeKeyFileJson*(j: JsonNode, let params = res.get() let dkey = ? deriveKey(password, params.salt, PBKDF2, params.prf, params.c) - result = decryptSecret(crypto, dkey) + return decryptSecret(crypto, dkey) of SCRYPT: let res = decodeScryptParams(crypto.kdfParams) @@ -470,7 +485,7 @@ proc decodeKeyFileJson*(j: JsonNode, let params = res.get() let dkey = ? deriveKey(password, params.salt, params.n, params.r, params.p) - result = decryptSecret(crypto, dkey) + return decryptSecret(crypto, dkey) proc loadKeyFile*(pathname: string, password: string, skip: int32 = 0): KfResult[seq[byte]] {.raises: [Defect, IOError].} = From 5a47ec321433a650fffc54e36a2bf3499fde2ad9 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Tue, 25 Oct 2022 19:00:45 +0200 Subject: [PATCH 10/20] refactor(rln/keyfile): use defer instead of try/finally --- tests/v2/test_utils_keyfile.nim | 181 +++++++++++++++---------------- tests/v2/test_waku_rln_relay.nim | 28 +++-- 2 files changed, 101 insertions(+), 108 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 46a644e775..23dc50b5b2 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -17,109 +17,104 @@ suite "KeyFile test suite": test "Create/Save/Load single keyfile": let password = "randompassword" - let filepath = "./test.keyfile" - try: - - var secret = randomSeqByte(rng[], 300) - let keyfile = createKeyFileJson(secret, password) + let filepath = "./test.keyfile" + defer: removeFile(filepath) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() + var secret = randomSeqByte(rng[], 300) + let keyfile = createKeyFileJson(secret, password) - var decodedSecret = loadKeyFile("test.keyfile", password) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() - check: - decodedSecret.isOk() - secret == decodedSecret.get() + var decodedSecret = loadKeyFile("test.keyfile", password) - finally: - removeFile(filepath) + check: + decodedSecret.isOk() + secret == decodedSecret.get() test "Create/Save/Load multiple keyfiles in same file": let password1 = "password1" let password2 = "password2" let password3 = "password3" - let filepath = "./test.keyfile" var keyfile: KfResult[JsonNode] - try: - let secret1 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret1, password1) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - let secret2 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret2, password2) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - let secret3 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret3, password3) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # We encrypt secret4 with password3 - let secret4 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret4, password3) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # We encrypt secret5 with password1 - let secret5 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret5, password1) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # We encrypt secret5 with password1 - let secret6 = randomSeqByte(rng[], 300) - keyfile = createKeyFileJson(secret6, password1) - check: - keyfile.isOk() - saveKeyFile(filepath, keyfile.get()).isOk() - - # Now there are 5 keyfiles stored in filepath encrypted with 3 different passwords - # We decode with the respective passwords - - var decodedSecret1 = loadKeyFile("test.keyfile", password1) - check: - decodedSecret1.isOk() - secret1 == decodedSecret1.get() - - var decodedSecret2 = loadKeyFile("test.keyfile", password2) - check: - decodedSecret2.isOk() - secret2 == decodedSecret2.get() - - var decodedSecret3 = loadKeyFile("test.keyfile", password3) - check: - decodedSecret3.isOk() - secret3 == decodedSecret3.get() - - # Since we have 2 keyfiles encrypted with same password3, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret3 - var decodedSecret4 = loadKeyFile("test.keyfile", password3, skip = 1) - check: - decodedSecret4.isOk() - secret4 == decodedSecret4.get() - - # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret1 - var decodedSecret5 = loadKeyFile("test.keyfile", password1, skip = 1) - check: - decodedSecret5.isOk() - secret5 == decodedSecret5.get() - - # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first 2 successful decryptions, i.e. the keyfile for secret1 and secret5 - var decodedSecret6 = loadKeyFile("test.keyfile", password1, skip = 2) - check: - decodedSecret6.isOk() - secret6 == decodedSecret6.get() - - finally: - removeFile(filepath) \ No newline at end of file + let filepath = "./test.keyfile" + defer: removeFile(filepath) + + let secret1 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret1, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + let secret2 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret2, password2) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + let secret3 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret3, password3) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret4 with password3 + let secret4 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret4, password3) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret5 with password1 + let secret5 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret5, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # We encrypt secret5 with password1 + let secret6 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret6, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # Now there are 5 keyfiles stored in filepath encrypted with 3 different passwords + # We decode with the respective passwords + + var decodedSecret1 = loadKeyFile("test.keyfile", password1) + check: + decodedSecret1.isOk() + secret1 == decodedSecret1.get() + + var decodedSecret2 = loadKeyFile("test.keyfile", password2) + check: + decodedSecret2.isOk() + secret2 == decodedSecret2.get() + + var decodedSecret3 = loadKeyFile("test.keyfile", password3) + check: + decodedSecret3.isOk() + secret3 == decodedSecret3.get() + + # Since we have 2 keyfiles encrypted with same password3, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret3 + var decodedSecret4 = loadKeyFile("test.keyfile", password3, skip = 1) + check: + decodedSecret4.isOk() + secret4 == decodedSecret4.get() + + # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret1 + var decodedSecret5 = loadKeyFile("test.keyfile", password1, skip = 1) + check: + decodedSecret5.isOk() + secret5 == decodedSecret5.get() + + # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first 2 successful decryptions, i.e. the keyfile for secret1 and secret5 + var decodedSecret6 = loadKeyFile("test.keyfile", password1, skip = 2) + check: + decodedSecret6.isOk() + secret6 == decodedSecret6.get() \ No newline at end of file diff --git a/tests/v2/test_waku_rln_relay.nim b/tests/v2/test_waku_rln_relay.nim index 98702a1133..3cda5878b3 100644 --- a/tests/v2/test_waku_rln_relay.nim +++ b/tests/v2/test_waku_rln_relay.nim @@ -1003,27 +1003,25 @@ suite "Waku rln relay": var rlnMembershipCredentials = RlnMembershipCredentials(membershipKeyPair: k, rlnIndex: index) - let filepath = "./testRLNCredentials.txt" let password = "%m0um0ucoW%" - try: - # Write RLN credentials - check: - writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk() + let filepath = "./testRLNCredentials.txt" + defer: removeFile(filepath) - let readCredentialsResult = readRlnCredentials(filepath, password) - check: - readCredentialsResult.isOk() + # Write RLN credentials + check: + writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk() - let credentials = readCredentialsResult.get() + let readCredentialsResult = readRlnCredentials(filepath, password) + check: + readCredentialsResult.isOk() - check: - credentials.isSome() - credentials.get().membershipKeyPair == k - credentials.get().rlnIndex == index + let credentials = readCredentialsResult.get() - finally: - removeFile(filepath) + check: + credentials.isSome() + credentials.get().membershipKeyPair == k + credentials.get().rlnIndex == index test "histogram static bucket generation": let buckets = generateBucketsForHistogram(10) From 168612e6b6854167f3bd0854c5c476cf297b49a0 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Wed, 26 Oct 2022 22:50:56 +0200 Subject: [PATCH 11/20] feat(rln): address reviewers comments; return all successful decryptions --- tests/v2/test_utils_keyfile.nim | 77 +++++++++++-------- .../waku_rln_relay/waku_rln_relay_utils.nim | 19 +++-- waku/v2/utils/keyfile.nim | 13 ++-- 3 files changed, 64 insertions(+), 45 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 23dc50b5b2..c3b531833c 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -16,34 +16,51 @@ suite "KeyFile test suite": test "Create/Save/Load single keyfile": + # The password we use to encrypt our secret let password = "randompassword" + # The filepath were the keyfile will be stored let filepath = "./test.keyfile" defer: removeFile(filepath) + # The secret var secret = randomSeqByte(rng[], 300) + + # We create a keyfile encrypting the secret with password let keyfile = createKeyFileJson(secret, password) check: keyfile.isOk() + # We save to disk the keyfile saveKeyFile(filepath, keyfile.get()).isOk() - var decodedSecret = loadKeyFile("test.keyfile", password) + # We load from the file all the decrypted keyfiles encrypted under password + var decodedKeyfiles = loadKeyFile(filepath, password) + + check: + decodedKeyfiles.isOk() + # Since only one secret was stored in file, we expect only one keyfile being decrypted + decodedKeyfiles.get().len == 1 + + # We check if the decrypted secret is the same as the original secret + let decodedSecret = decodedKeyfiles.get()[0] check: - decodedSecret.isOk() secret == decodedSecret.get() test "Create/Save/Load multiple keyfiles in same file": + # We set different passwords for different keyfiles that will be stored in same file let password1 = "password1" - let password2 = "password2" + let password2 = "" let password3 = "password3" var keyfile: KfResult[JsonNode] let filepath = "./test.keyfile" defer: removeFile(filepath) + # We generate 6 different secrets and we encrypt them using 3 different passwords, and we store the obtained keystore + let secret1 = randomSeqByte(rng[], 300) keyfile = createKeyFileJson(secret1, password1) check: @@ -76,45 +93,43 @@ suite "KeyFile test suite": keyfile.isOk() saveKeyFile(filepath, keyfile.get()).isOk() - # We encrypt secret5 with password1 + # We encrypt secret6 with password1 let secret6 = randomSeqByte(rng[], 300) keyfile = createKeyFileJson(secret6, password1) check: keyfile.isOk() saveKeyFile(filepath, keyfile.get()).isOk() - # Now there are 5 keyfiles stored in filepath encrypted with 3 different passwords - # We decode with the respective passwords + # Now there are 6 keyfiles stored in filepath encrypted with 3 different passwords + # We decrypt the keyfiles using the respective passwords and we check that the number of + # successful decryptions corresponds to the number of secrets encrypted under that password - var decodedSecret1 = loadKeyFile("test.keyfile", password1) + var decodedKeyfilesPassword1 = loadKeyFile(filepath, password1) check: - decodedSecret1.isOk() - secret1 == decodedSecret1.get() + decodedKeyfilesPassword1.isOk() + decodedKeyfilesPassword1.get().len == 3 + var decodedSecretsPassword1 = decodedKeyfilesPassword1.get() - var decodedSecret2 = loadKeyFile("test.keyfile", password2) + var decodedKeyfilesPassword2 = loadKeyFile(filepath, password2) check: - decodedSecret2.isOk() - secret2 == decodedSecret2.get() + decodedKeyfilesPassword2.isOk() + decodedKeyfilesPassword2.get().len == 1 + var decodedSecretsPassword2 = decodedKeyfilesPassword2.get() - var decodedSecret3 = loadKeyFile("test.keyfile", password3) + var decodedKeyfilesPassword3 = loadKeyFile(filepath, password3) check: - decodedSecret3.isOk() - secret3 == decodedSecret3.get() + decodedKeyfilesPassword3.isOk() + decodedKeyfilesPassword3.get().len == 2 + var decodedSecretsPassword3 = decodedKeyfilesPassword3.get() - # Since we have 2 keyfiles encrypted with same password3, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret3 - var decodedSecret4 = loadKeyFile("test.keyfile", password3, skip = 1) - check: - decodedSecret4.isOk() - secret4 == decodedSecret4.get() - - # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first successful decryption, i.e. the keyfile for secret1 - var decodedSecret5 = loadKeyFile("test.keyfile", password1, skip = 1) - check: - decodedSecret5.isOk() - secret5 == decodedSecret5.get() - - # Since we have 3 keyfiles encrypted with same password1, to obtain the secret we skip the first 2 successful decryptions, i.e. the keyfile for secret1 and secret5 - var decodedSecret6 = loadKeyFile("test.keyfile", password1, skip = 2) + # We check if the corresponding secrets are correct check: - decodedSecret6.isOk() - secret6 == decodedSecret6.get() \ No newline at end of file + # Secrets encrypted with password 1 + secret1 == decodedSecretsPassword1[0].get() + secret5 == decodedSecretsPassword1[1].get() + secret6 == decodedSecretsPassword1[2].get() + # Secrets encrypted with password 2 + secret2 == decodedSecretsPassword2[0].get() + # Secrets encrypted with password 3 + secret3 == decodedSecretsPassword3[0].get() + secret4 == decodedSecretsPassword3[1].get() \ No newline at end of file diff --git a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim index e36055101b..7bc72b6c38 100644 --- a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim +++ b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim @@ -1146,7 +1146,7 @@ proc mountRlnRelayDynamic*(node: WakuNode, node.wakuRlnRelay = rlnPeer return ok(true) -proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, password: string) : RlnRelayResult[void] = +proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, password: string): RlnRelayResult[void] = info "Storing RLN credentials" var jsonString: string jsonString.toUgly(%credentials) @@ -1157,7 +1157,9 @@ proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, p return err("Error while saving keyfile for RLN credentials") return ok() -proc readRlnCredentials*(path: string, password: string) : RlnRelayResult[Option[RlnMembershipCredentials]] {.raises: [Defect, IOError, Exception].} = +# Attempts decryptions of all keyfiles with the provided password. +# If one or more credentials are successfully decrypted, the max(min(index,number_decrypted),0)-th is returned. +proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRelayResult[Option[RlnMembershipCredentials]] {.raises: [Defect, IOError, Exception].} = info "Reading RLN credentials" # With regards to printing the keys, it is purely for debugging purposes so that the user becomes explicitly aware of the current keys in use when nwaku is started. # Note that this is only until the RLN contract being used is the one deployed on Goerli testnet. @@ -1165,15 +1167,20 @@ proc readRlnCredentials*(path: string, password: string) : RlnRelayResult[Option waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: try: - var loadedRlnCredentialsFile = loadKeyFile(path, password) + var decodedKeyfiles = loadKeyFile(path, password) - if loadedRlnCredentialsFile.isOk(): - let jsonObject = parseJson(string.fromBytes(loadedRlnCredentialsFile.get())) + if decodedKeyfiles.isOk(): + var decodedRlnCredentials = decodedKeyfiles.get() + debug "Successfully decrypted keyfiles for the provided password", numberKeyfilesDecrypted=decodedRlnCredentials.len + # We should return the index-th decrypted credential, but we ensure to not overflow + let credentialIndex = max(min(index, decodedRlnCredentials.len - 1), 0) + debug "Picking credential with (adjusted) index", inputIndex=index, adjustedIndex=credentialIndex + let jsonObject = parseJson(string.fromBytes(decodedRlnCredentials[credentialIndex].get())) let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials) debug "Deserialized RLN credentials", rlnCredentials=deserializedRlnCredentials return ok(some(deserializedRlnCredentials)) else: - debug "Unable to decrypt RLN credentials with provided password. ", error=loadedRlnCredentialsFile.error + debug "Unable to decrypt RLN credentials with provided password. ", error=decodedKeyfiles.error return ok(none(RlnMembershipCredentials)) except IOError: diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index 7f33e4ea4e..d9451ac42a 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -488,13 +488,13 @@ proc decodeKeyFileJson*(j: JsonNode, return decryptSecret(crypto, dkey) proc loadKeyFile*(pathname: string, - password: string, skip: int32 = 0): KfResult[seq[byte]] {.raises: [Defect, IOError].} = + password: string, index: int32 = 0): KfResult[seq[KfResult[seq[byte]]]] {.raises: [Defect, IOError].} = ## Load and decode data from file with pathname ## ``pathname``, using password string ``password``. - ## If skip is non-zero, the first skip successful decryptions are skipped + ## The index successful decryptions is returned var data: JsonNode var decodedKeyfile: KfResult[seq[byte]] - var skipDecryptSuccesses = skip + var successfullyDecodedKeyfiles: seq[KfResult[seq[byte]]] for keyfile in lines(pathname): try: @@ -506,12 +506,9 @@ proc loadKeyFile*(pathname: string, decodedKeyfile = decodeKeyFileJson(data, password) if decodedKeyfile.isOk(): - if skipDecryptSuccesses <= 0: - break - else: - skipDecryptSuccesses -= 1 + successfullyDecodedKeyfiles.add decodedKeyfile - return decodedKeyfile + return ok(successfullyDecodedKeyfiles) # Note that the keyfile is open in Append mode so that multiple credentials can be stored in same file proc saveKeyFile*(pathname: string, From 08be67c151ecf72959377da97732d00f18572f7e Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Wed, 26 Oct 2022 23:18:17 +0200 Subject: [PATCH 12/20] chore(keyfile): add comments; changed name to loadKeyfiles --- tests/v2/test_utils_keyfile.nim | 8 +++---- .../waku_rln_relay/waku_rln_relay_utils.nim | 2 +- waku/v2/utils/keyfile.nim | 23 +++++++++++++++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index c3b531833c..5babc69c46 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -35,7 +35,7 @@ suite "KeyFile test suite": saveKeyFile(filepath, keyfile.get()).isOk() # We load from the file all the decrypted keyfiles encrypted under password - var decodedKeyfiles = loadKeyFile(filepath, password) + var decodedKeyfiles = loadKeyFiles(filepath, password) check: decodedKeyfiles.isOk() @@ -104,19 +104,19 @@ suite "KeyFile test suite": # We decrypt the keyfiles using the respective passwords and we check that the number of # successful decryptions corresponds to the number of secrets encrypted under that password - var decodedKeyfilesPassword1 = loadKeyFile(filepath, password1) + var decodedKeyfilesPassword1 = loadKeyFiles(filepath, password1) check: decodedKeyfilesPassword1.isOk() decodedKeyfilesPassword1.get().len == 3 var decodedSecretsPassword1 = decodedKeyfilesPassword1.get() - var decodedKeyfilesPassword2 = loadKeyFile(filepath, password2) + var decodedKeyfilesPassword2 = loadKeyFiles(filepath, password2) check: decodedKeyfilesPassword2.isOk() decodedKeyfilesPassword2.get().len == 1 var decodedSecretsPassword2 = decodedKeyfilesPassword2.get() - var decodedKeyfilesPassword3 = loadKeyFile(filepath, password3) + var decodedKeyfilesPassword3 = loadKeyFiles(filepath, password3) check: decodedKeyfilesPassword3.isOk() decodedKeyfilesPassword3.get().len == 2 diff --git a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim index 7bc72b6c38..b6e2498c67 100644 --- a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim +++ b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim @@ -1167,7 +1167,7 @@ proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRel waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: try: - var decodedKeyfiles = loadKeyFile(path, password) + var decodedKeyfiles = loadKeyFiles(path, password) if decodedKeyfiles.isOk(): var decodedRlnCredentials = decodedKeyfiles.get() diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index d9451ac42a..e2c10ffac8 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -121,6 +121,7 @@ proc `$`(k: CryptKind): string = else: return "aes-128-ctr" +# Parses the prf name to HashKind proc getPrfHash(prf: string): HashKind = let p = prf.toLowerAscii() if p.startsWith("hmac-"): @@ -130,6 +131,7 @@ proc getPrfHash(prf: string): HashKind = return SupportedHashesKinds[res] return HashNoSupport +# Parses the cipher name to CryptoKind proc getCipher(c: string): CryptKind = var cl = c.toLowerAscii() if cl == "aes-128-ctr": @@ -137,6 +139,7 @@ proc getCipher(c: string): CryptKind = else: return CipherNoSupport +# Key derivation routine for PBKDF2 proc deriveKey(password: string, salt: string, kdfkind: KdfKind, @@ -211,6 +214,7 @@ proc deriveKey(password: string, else: err(NotImplemented) +# Scrypt wrapper func scrypt[T, M](password: openArray[T], salt: openArray[M], N, r, p: int, output: var openArray[byte]): int = let (xyvLen, bLen) = scryptCalc(N, r, p) @@ -218,6 +222,7 @@ func scrypt[T, M](password: openArray[T], salt: openArray[M], var b = newSeq[byte](bLen) scrypt(password, salt, N, r, p, xyv, b, output) +# Key derivation routine for Scrypt proc deriveKey(password: string, salt: string, workFactor, r, p: int): KfResult[DKey] = @@ -228,6 +233,7 @@ proc deriveKey(password: string, salt: string, return ok(output) +# Encryption routine proc encryptData(secret: openArray[byte], cryptkind: CryptKind, key: openArray[byte], @@ -242,6 +248,7 @@ proc encryptData(secret: openArray[byte], else: err(NotImplemented) +# Decryption routine proc decryptData(ciphertext: openArray[byte], cryptkind: CryptKind, key: openArray[byte], @@ -258,6 +265,7 @@ proc decryptData(ciphertext: openArray[byte], else: err(NotImplemented) +# Encodes KDF parameters in JSON proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNode] = if kdfkind == SCRYPT: let wf = if workfactor == 0: ScryptWorkFactor else: workfactor @@ -283,6 +291,7 @@ proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNo else: err(NotImplemented) +# Decodes hex strings to byte sequences proc decodeHex(m: string): seq[byte] = if len(m) > 0: try: @@ -292,6 +301,7 @@ proc decodeHex(m: string): seq[byte] = else: return newSeq[byte]() +# Parses the salt from hex string to byte string proc decodeSalt(m: string): string = var sarr: seq[byte] if len(m) > 0: @@ -305,12 +315,15 @@ proc decodeSalt(m: string): string = else: return "" +# Compares the message authentication code proc compareMac(m1: openArray[byte], m2: openArray[byte]): bool = if len(m1) == len(m2) and len(m1) > 0: return equalMem(unsafeAddr m1[0], unsafeAddr m2[0], len(m1)) else: return false +# Creates a keyfile for secret encrypted with password according to the other parameters +# Returns keyfile in JSON according to Web3 Secure storage format (here, differently than standard, version and id are optional) proc createKeyFileJson*(secret: openArray[byte], password: string, version: int = 3, @@ -376,6 +389,7 @@ proc createKeyFileJson*(secret: openArray[byte], ok(json) +# Parses Cipher JSON information proc decodeCrypto(n: JsonNode): KfResult[Crypto] = var crypto = n.getOrDefault("crypto") if isNil(crypto): @@ -412,6 +426,7 @@ proc decodeCrypto(n: JsonNode): KfResult[Crypto] = return ok(c) +# Parses PNKDF2 JSON parameters proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] = var p: Pbkdf2Params p.salt = decodeSalt(params.getOrDefault("salt").getStr()) @@ -429,6 +444,7 @@ proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] = return ok(p) +# Parses JSON Scrypt parameters proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] = var p: ScryptParams p.salt = decodeSalt(params.getOrDefault("salt").getStr()) @@ -445,6 +461,7 @@ proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] = return ok(p) +# Decrypts data func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = var ctx: keccak256 ctx.init() @@ -459,6 +476,7 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] = ok(plaintext) +# Parse JSON keyfile and decrypts its content using password proc decodeKeyFileJson*(j: JsonNode, password: string): KfResult[seq[byte]] = ## Decode secret from keyfile json object ``j`` using @@ -487,8 +505,9 @@ proc decodeKeyFileJson*(j: JsonNode, let dkey = ? deriveKey(password, params.salt, params.n, params.r, params.p) return decryptSecret(crypto, dkey) -proc loadKeyFile*(pathname: string, - password: string, index: int32 = 0): KfResult[seq[KfResult[seq[byte]]]] {.raises: [Defect, IOError].} = +# Loads the file at pathname, decrypts and returns all keyfiles encrypted under password +proc loadKeyFiles*(pathname: string, + password: string): KfResult[seq[KfResult[seq[byte]]]] {.raises: [Defect, IOError].} = ## Load and decode data from file with pathname ## ``pathname``, using password string ``password``. ## The index successful decryptions is returned From d814f178efd2ca9161ab5f5d2de95c70759a2d1c Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Wed, 26 Oct 2022 23:48:29 +0200 Subject: [PATCH 13/20] feat(keyfile): chmod 600 for keyfiles --- waku/v2/utils/keyfile.nim | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index e2c10ffac8..c2985dec93 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -6,7 +6,7 @@ {.push raises: [Defect].} import - std/[strutils, json, sequtils], + std/[os, strutils, json, sequtils], nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak, scrypt], stew/results, eth/keys, @@ -538,6 +538,8 @@ proc saveKeyFile*(pathname: string, if not f.open(pathname, fmAppend): return err(OsError) try: + # To avoid other users/attackers to be able to read keyfiles, we make the make the file readable/writable only by the running user + setFilePermissions(pathname, {fpUserWrite, fpUserRead}) f.write($jobject) f.write("\n") ok() From 01ebc549f5000d3e0021fc63b2c815e1a8165af2 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 27 Oct 2022 00:10:13 +0200 Subject: [PATCH 14/20] fix(rln): fix errors raised by readRlnCredentials --- waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim index b6e2498c67..7d7a70335b 100644 --- a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim +++ b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim @@ -1159,7 +1159,7 @@ proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, p # Attempts decryptions of all keyfiles with the provided password. # If one or more credentials are successfully decrypted, the max(min(index,number_decrypted),0)-th is returned. -proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRelayResult[Option[RlnMembershipCredentials]] {.raises: [Defect, IOError, Exception].} = +proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRelayResult[Option[RlnMembershipCredentials]] {.raises: [Defect, IOError, OSError].} = info "Reading RLN credentials" # With regards to printing the keys, it is purely for debugging purposes so that the user becomes explicitly aware of the current keys in use when nwaku is started. # Note that this is only until the RLN contract being used is the one deployed on Goerli testnet. @@ -1182,9 +1182,8 @@ proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRel else: debug "Unable to decrypt RLN credentials with provided password. ", error=decodedKeyfiles.error return ok(none(RlnMembershipCredentials)) - - except IOError: - return err("IOError while loading keyfile for RLN credentials at " & path) + except: + return err("Error while loading keyfile for RLN credentials at " & path) proc mount(node: WakuNode, From 9b6850ec77338fbd97c9af3cb6a1c4417484733b Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 27 Oct 2022 14:58:33 +0200 Subject: [PATCH 15/20] feat(keyfile): add nim-eth tests; address reviewers comments --- tests/v2/test_utils_keyfile.nim | 252 +++++++++++++++++++++++++++++++- waku/v2/utils/keyfile.nim | 26 +++- 2 files changed, 268 insertions(+), 10 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 5babc69c46..3d661e07f4 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -2,11 +2,10 @@ import std/[json, os], - testutils/unittests, chronos, chronicles, + testutils/unittests, chronos, eth/keys import - ../../waku/v2/utils/keyfile, - ../test_helpers + ../../waku/v2/utils/keyfile from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte @@ -132,4 +131,249 @@ suite "KeyFile test suite": secret2 == decodedSecretsPassword2[0].get() # Secrets encrypted with password 3 secret3 == decodedSecretsPassword3[0].get() - secret4 == decodedSecretsPassword3[1].get() \ No newline at end of file + secret4 == decodedSecretsPassword3[1].get() + + +# The following tests are originally from the nim-eth keyfile tests module https://github.com/status-im/nim-eth/blob/master/tests/keyfile/test_keyfile.nim +# and are slightly adapted to test our customized version of the utils/keyfile module +# Note: the original nim-eth "Create/Save/Load test" is redefined and expanded above in "KeyFile test suite" +suite "KeyFile test suite (adapted from nim-eth keyfile tests)": + + let rng = newRng() + + let 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" + }, + %*{ + "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" + } + ] + + test "KeyStoreTests/basic_tests.json test1": + var expectkey = decodeHex(TestVectors[0].getOrDefault("priv").getStr()) + let seckey = + decodeKeyFileJson(TestVectors[0].getOrDefault("keyfile"), + TestVectors[0].getOrDefault("password").getStr()) + check: + seckey.isOk() + seckey.get() == expectkey + + test "KeyStoreTests/basic_tests.json python_generated_test_with_odd_iv": + var expectkey = decodeHex(TestVectors[1].getOrDefault("priv").getStr()) + let seckey = + decodeKeyFileJson(TestVectors[1].getOrDefault("keyfile"), + TestVectors[1].getOrDefault("password").getStr()) + check: + seckey.isOk() + seckey.get() == expectkey + + test "KeyStoreTests/basic_tests.json evilnonce": + var expectkey = decodeHex(TestVectors[2].getOrDefault("priv").getStr()) + let seckey = decodeKeyFileJson(TestVectors[2].getOrDefault("keyfile"), + TestVectors[2].getOrDefault("password").getStr()) + check: + seckey.isOk() + seckey.get() == expectkey + + test "KeyStoreTests/basic_tests.json evilnonce with wrong password": + let seckey = + decodeKeyFileJson(TestVectors[2].getOrDefault("keyfile"), + "wrongpassword") + check: + seckey.isErr() + seckey.error == KeyFileError.IncorrectMac + + test "KeyStoreTests/basic_tests.json test2": + var expectkey = decodeHex(TestVectors[3].getOrDefault("priv").getStr()) + let seckey = + decodeKeyFileJson(TestVectors[3].getOrDefault("keyfile"), + TestVectors[3].getOrDefault("password").getStr()) + check: + seckey.isOk() + seckey.get() == expectkey + + test "KeyStoreTests/basic_tests.json mycrypto": + var expectkey = decodeHex(TestVectors[4].getOrDefault("priv").getStr()) + let seckey = + decodeKeyFileJson(TestVectors[4].getOrDefault("keyfile"), + TestVectors[4].getOrDefault("password").getStr()) + check: + seckey.isOk() + seckey.get() == expectkey + + test "eth-key/conftest.py": + var expectkey = decodeHex(TestVectors[5].getOrDefault("priv").getStr()) + let seckey = + decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"), + TestVectors[5].getOrDefault("password").getStr()) + check: + seckey.isOk() + seckey.get() == expectkey + + test "eth-key/conftest.py with wrong password": + let seckey = + decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"), + "wrongpassword") + check: + seckey.isErr() + seckey.error == KeyFileError.IncorrectMac + + test "Scrypt roundtrip": + let + expectkey = randomSeqByte(rng[], 300) + jobject = createKeyFileJson(expectkey, "miawmiawcat", 3, AES128CTR, SCRYPT) + + check: + jobject.isOk() + + let seckey = decodeKeyFileJson(jobject.get(), "miawmiawcat") + + check: + seckey.isOk() + seckey.get() == expectkey + + test "Load non-existent pathname test": + + check: + loadKeyFiles("nonexistant.keyfile", "password").error == + KeyFileError.KeyfileDoesNotExist \ No newline at end of file diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index c2985dec93..a0529dcea6 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -45,6 +45,7 @@ type ScryptBadParam = "keyfile error: bad scrypt's parameters" OsError = "keyfile error: OS specific error" JsonError = "keyfile error: JSON encoder/decoder error" + KeyfileDoesNotExist = "keyfile error: file does not exist" KdfKind* = enum PBKDF2, ## PBKDF2 @@ -234,17 +235,17 @@ proc deriveKey(password: string, salt: string, return ok(output) # Encryption routine -proc encryptData(secret: openArray[byte], +proc encryptData(plaintext: openArray[byte], cryptkind: CryptKind, key: openArray[byte], iv: openArray[byte]): KfResult[seq[byte]] = if cryptkind == AES128CTR: - var crypttext = newSeqWith(secret.len, 0.byte) + var ciphertext = newSeqWith(plaintext.len, 0.byte) var ctx: CTR[aes128] ctx.init(toOpenArray(key, 0, 15), iv) - ctx.encrypt(secret, crypttext) + ctx.encrypt(plaintext, ciphertext) ctx.clear() - ok(crypttext) + ok(ciphertext) else: err(NotImplemented) @@ -292,7 +293,7 @@ proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNo err(NotImplemented) # Decodes hex strings to byte sequences -proc decodeHex(m: string): seq[byte] = +proc decodeHex*(m: string): seq[byte] = if len(m) > 0: try: return utils.fromHex(m) @@ -515,7 +516,19 @@ proc loadKeyFiles*(pathname: string, var decodedKeyfile: KfResult[seq[byte]] var successfullyDecodedKeyfiles: seq[KfResult[seq[byte]]] + if fileExists(pathname) == false: + return err(KeyfileDoesNotExist) + + # Note that lines strips the ending newline, if present for keyfile in lines(pathname): + + # We skip empty lines + if keyfile.len == 0: + continue + # We skip all lines that doesn't seem to define a json + if keyfile[0] != '{' or keyfile[^1] != '}': + continue + try: data = json.parseJson(keyfile) except JsonParsingError: @@ -538,9 +551,10 @@ proc saveKeyFile*(pathname: string, if not f.open(pathname, fmAppend): return err(OsError) try: - # To avoid other users/attackers to be able to read keyfiles, we make the make the file readable/writable only by the running user + # To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user setFilePermissions(pathname, {fpUserWrite, fpUserRead}) f.write($jobject) + # We store a keyfile per line f.write("\n") ok() except CatchableError: From 6aa78052f8987ee258a0e4b8aab7faa919ba38bb Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 27 Oct 2022 16:13:02 +0200 Subject: [PATCH 16/20] fix(keyfile/rln): better error handling --- .../waku_rln_relay/waku_rln_relay_utils.nim | 2 +- waku/v2/utils/keyfile.nim | 49 +++++++++++-------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim index 7d7a70335b..d07940d60c 100644 --- a/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim +++ b/waku/v2/protocol/waku_rln_relay/waku_rln_relay_utils.nim @@ -1159,7 +1159,7 @@ proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, p # Attempts decryptions of all keyfiles with the provided password. # If one or more credentials are successfully decrypted, the max(min(index,number_decrypted),0)-th is returned. -proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRelayResult[Option[RlnMembershipCredentials]] {.raises: [Defect, IOError, OSError].} = +proc readRlnCredentials*(path: string, password: string, index: int = 0): RlnRelayResult[Option[RlnMembershipCredentials]] = info "Reading RLN credentials" # With regards to printing the keys, it is purely for debugging purposes so that the user becomes explicitly aware of the current keys in use when nwaku is started. # Note that this is only until the RLN contract being used is the one deployed on Goerli testnet. diff --git a/waku/v2/utils/keyfile.nim b/waku/v2/utils/keyfile.nim index a0529dcea6..2c0ca7c37a 100644 --- a/waku/v2/utils/keyfile.nim +++ b/waku/v2/utils/keyfile.nim @@ -44,6 +44,7 @@ type IncorrectMac = "keyfile error: `mac` verification failed" ScryptBadParam = "keyfile error: bad scrypt's parameters" OsError = "keyfile error: OS specific error" + IoError = "keyfile error: IO specific error" JsonError = "keyfile error: JSON encoder/decoder error" KeyfileDoesNotExist = "keyfile error: file does not exist" @@ -508,7 +509,7 @@ proc decodeKeyFileJson*(j: JsonNode, # Loads the file at pathname, decrypts and returns all keyfiles encrypted under password proc loadKeyFiles*(pathname: string, - password: string): KfResult[seq[KfResult[seq[byte]]]] {.raises: [Defect, IOError].} = + password: string): KfResult[seq[KfResult[seq[byte]]]] = ## Load and decode data from file with pathname ## ``pathname``, using password string ``password``. ## The index successful decryptions is returned @@ -520,25 +521,33 @@ proc loadKeyFiles*(pathname: string, return err(KeyfileDoesNotExist) # Note that lines strips the ending newline, if present - for keyfile in lines(pathname): - - # We skip empty lines - if keyfile.len == 0: - continue - # We skip all lines that doesn't seem to define a json - if keyfile[0] != '{' or keyfile[^1] != '}': - continue - - try: - data = json.parseJson(keyfile) - except JsonParsingError: - return err(JsonError) - except Exception: # json raises Exception - return err(OsError) - - decodedKeyfile = decodeKeyFileJson(data, password) - if decodedKeyfile.isOk(): - successfullyDecodedKeyfiles.add decodedKeyfile + try: + for keyfile in lines(pathname): + + # We skip empty lines + if keyfile.len == 0: + continue + # We skip all lines that doesn't seem to define a json + if keyfile[0] != '{' or keyfile[^1] != '}': + continue + + try: + data = json.parseJson(keyfile) + except JsonParsingError: + return err(JsonError) + except ValueError: + return err(JsonError) + except OSError: + return err(OsError) + except Exception: #parseJson raises Exception + return err(OsError) + + decodedKeyfile = decodeKeyFileJson(data, password) + if decodedKeyfile.isOk(): + successfullyDecodedKeyfiles.add decodedKeyfile + + except IOError: + return err(IoError) return ok(successfullyDecodedKeyfiles) From edab924c693ed216e2ad401b8a482558234d7523 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 27 Oct 2022 16:46:58 +0200 Subject: [PATCH 17/20] refactor(keyfile): refactoring nim-eth tests --- tests/v2/test_utils_keyfile.nim | 108 ++++++++++---------------------- 1 file changed, 33 insertions(+), 75 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 3d661e07f4..4e0e8b400a 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -135,12 +135,13 @@ suite "KeyFile test suite": # The following tests are originally from the nim-eth keyfile tests module https://github.com/status-im/nim-eth/blob/master/tests/keyfile/test_keyfile.nim -# and are slightly adapted to test our customized version of the utils/keyfile module +# and are slightly adapted to test backwards compatibility with nim-eth implementation of our customized version of the utils/keyfile module # Note: the original nim-eth "Create/Save/Load test" is redefined and expanded above in "KeyFile test suite" suite "KeyFile test suite (adapted from nim-eth keyfile tests)": let rng = newRng() + # Testvectors originally from https://github.com/status-im/nim-eth/blob/fef47331c37ee8abb8608037222658737ff498a6/tests/keyfile/test_keyfile.nim#L22-L168 let TestVectors = [ %*{ "keyfile": { @@ -289,90 +290,47 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)": } ] - test "KeyStoreTests/basic_tests.json test1": - var expectkey = decodeHex(TestVectors[0].getOrDefault("priv").getStr()) - let seckey = - decodeKeyFileJson(TestVectors[0].getOrDefault("keyfile"), - TestVectors[0].getOrDefault("password").getStr()) - check: - seckey.isOk() - seckey.get() == expectkey - - test "KeyStoreTests/basic_tests.json python_generated_test_with_odd_iv": - var expectkey = decodeHex(TestVectors[1].getOrDefault("priv").getStr()) - let seckey = - decodeKeyFileJson(TestVectors[1].getOrDefault("keyfile"), - TestVectors[1].getOrDefault("password").getStr()) - check: - seckey.isOk() - seckey.get() == expectkey + test "Testing nim-eth test vectors": - test "KeyStoreTests/basic_tests.json evilnonce": - var expectkey = decodeHex(TestVectors[2].getOrDefault("priv").getStr()) - let seckey = decodeKeyFileJson(TestVectors[2].getOrDefault("keyfile"), - TestVectors[2].getOrDefault("password").getStr()) - check: - seckey.isOk() - seckey.get() == expectkey - - test "KeyStoreTests/basic_tests.json evilnonce with wrong password": - let seckey = - decodeKeyFileJson(TestVectors[2].getOrDefault("keyfile"), - "wrongpassword") - check: - seckey.isErr() - seckey.error == KeyFileError.IncorrectMac - - test "KeyStoreTests/basic_tests.json test2": - var expectkey = decodeHex(TestVectors[3].getOrDefault("priv").getStr()) - let seckey = - decodeKeyFileJson(TestVectors[3].getOrDefault("keyfile"), - TestVectors[3].getOrDefault("password").getStr()) - check: - seckey.isOk() - seckey.get() == expectkey - - test "KeyStoreTests/basic_tests.json mycrypto": - var expectkey = decodeHex(TestVectors[4].getOrDefault("priv").getStr()) - let seckey = - decodeKeyFileJson(TestVectors[4].getOrDefault("keyfile"), - TestVectors[4].getOrDefault("password").getStr()) - check: - seckey.isOk() - seckey.get() == expectkey - - test "eth-key/conftest.py": - var expectkey = decodeHex(TestVectors[5].getOrDefault("priv").getStr()) - let seckey = - decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"), - TestVectors[5].getOrDefault("password").getStr()) - check: - seckey.isOk() - seckey.get() == expectkey + var secret: KfResult[seq[byte]] + var expectedSecret: seq[byte] - test "eth-key/conftest.py with wrong password": - let seckey = - decodeKeyFileJson(TestVectors[5].getOrDefault("keyfile"), - "wrongpassword") - check: - seckey.isErr() - seckey.error == KeyFileError.IncorrectMac + for i in 0..TestVectors.len: + + # Decryption with correct password + expectedSecret = decodeHex(TestVectors[i].getOrDefault("priv").getStr()) + secret = + decodeKeyFileJson(TestVectors[i].getOrDefault("keyfile"), + TestVectors[i].getOrDefault("password").getStr()) + check: + secret.isOk() + secret.get() == expectedSecret - test "Scrypt roundtrip": + # Decryption with wrong password + secret = decodeKeyFileJson(TestVectors[i].getOrDefault("keyfile"), "wrongpassword") + + check: + secret.isErr() + secret.error == KeyFileError.IncorrectMac + + test "Scrypt keyfiles": let - expectkey = randomSeqByte(rng[], 300) - jobject = createKeyFileJson(expectkey, "miawmiawcat", 3, AES128CTR, SCRYPT) + expectedSecret = randomSeqByte(rng[], 300) + password = "miawmiawcat" + + # By default keyfiles are created using PBKDF2. Here we test keyfiles created using scrypt + jsonKeyfile = createKeyFileJson(expectedSecret, password, 3, AES128CTR, SCRYPT) check: - jobject.isOk() + jsonKeyfile.isOk() - let seckey = decodeKeyFileJson(jobject.get(), "miawmiawcat") + let secret = decodeKeyFileJson(jsonKeyfile.get(), password) check: - seckey.isOk() - seckey.get() == expectkey + secret.isOk() + secret.get() == expectedSecret - test "Load non-existent pathname test": + test "Load non-existent keyfile test": check: loadKeyFiles("nonexistant.keyfile", "password").error == From 75c6ff649af392e444e160ad2edb5db995e5c53c Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 27 Oct 2022 17:18:18 +0200 Subject: [PATCH 18/20] fix(keyfile): fix wrong index --- tests/v2/test_utils_keyfile.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 4e0e8b400a..6ad20bb2a7 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -50,9 +50,9 @@ suite "KeyFile test suite": test "Create/Save/Load multiple keyfiles in same file": # We set different passwords for different keyfiles that will be stored in same file - let password1 = "password1" + let password1 = decodeHex(string.fromBytes(randomSeqByte(rng[], 20))) let password2 = "" - let password3 = "password3" + let password3 = decodeHex(string.fromBytes(randomSeqByte(rng[], 20))) var keyfile: KfResult[JsonNode] let filepath = "./test.keyfile" @@ -295,7 +295,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)": var secret: KfResult[seq[byte]] var expectedSecret: seq[byte] - for i in 0..TestVectors.len: + for i in 0.. Date: Thu, 27 Oct 2022 19:11:13 +0200 Subject: [PATCH 19/20] fix(keyfile): fix wrong password generation in test --- tests/v2/test_utils_keyfile.nim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 6ad20bb2a7..141f26c938 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -2,6 +2,7 @@ import std/[json, os], + stew/byteutils, testutils/unittests, chronos, eth/keys import @@ -50,9 +51,9 @@ suite "KeyFile test suite": test "Create/Save/Load multiple keyfiles in same file": # We set different passwords for different keyfiles that will be stored in same file - let password1 = decodeHex(string.fromBytes(randomSeqByte(rng[], 20))) + let password1 = string.fromBytes(randomSeqByte(rng[], 20)) let password2 = "" - let password3 = decodeHex(string.fromBytes(randomSeqByte(rng[], 20))) + let password3 = string.fromBytes(randomSeqByte(rng[], 20)) var keyfile: KfResult[JsonNode] let filepath = "./test.keyfile" From 239113c11c7e905733bf46638cf4af296a4fba68 Mon Sep 17 00:00:00 2001 From: s1fr0 <28568419+s1fr0@users.noreply.github.com> Date: Thu, 27 Oct 2022 20:31:05 +0200 Subject: [PATCH 20/20] feat(keyfile): add wrong mac test --- tests/v2/test_utils_keyfile.nim | 41 ++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim index 141f26c938..ffaa871642 100644 --- a/tests/v2/test_utils_keyfile.nim +++ b/tests/v2/test_utils_keyfile.nim @@ -314,12 +314,51 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)": secret.isErr() secret.error == KeyFileError.IncorrectMac + test "Wrong mac in keyfile": + + # This keyfile is the same as the first one in TestVectors, + # but the last byte of mac is changed to 00. + # While ciphertext is the correct encryption of priv under password, + # mac verfication should fail and nothing will be decrypted + let keyfileWrongMac = %*{ + "keyfile": { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : {"iv" : "6087dab2f9fdbbfaddc31a909735c1e6"}, + "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", + "kdf" : "pbkdf2", + "kdfparams" : { + "c" : 262144, + "dklen" : 32, + "prf" : "hmac-sha256", + "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" + }, + "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e900" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + }, + "name": "test1", + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" + } + + # Decryption with correct password + let expectedSecret = decodeHex(keyfileWrongMac.getOrDefault("priv").getStr()) + let secret = + decodeKeyFileJson(keyfileWrongMac.getOrDefault("keyfile"), + keyfileWrongMac.getOrDefault("password").getStr()) + check: + secret.isErr() + secret.error == KeyFileError.IncorrectMac + test "Scrypt keyfiles": let expectedSecret = randomSeqByte(rng[], 300) password = "miawmiawcat" - # By default keyfiles are created using PBKDF2. Here we test keyfiles created using scrypt + # By default, keyfiles' encryption key is derived from password using PBKDF2. + # Here we test keyfiles encypted with a key derived from password using scrypt jsonKeyfile = createKeyFileJson(expectedSecret, password, 3, AES128CTR, SCRYPT) check: