diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index a830bd5a3..8b51e0551 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 = diff --git a/apps/wakunode2/config.nim b/apps/wakunode2/config.nim index a4635ffaa..dfd8ca175 100644 --- a/apps/wakunode2/config.nim +++ b/apps/wakunode2/config.nim @@ -161,7 +161,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] diff --git a/tests/all_tests_v2.nim b/tests/all_tests_v2.nim index 308c195c7..fb9bfb8a2 100644 --- a/tests/all_tests_v2.nim +++ b/tests/all_tests_v2.nim @@ -45,7 +45,9 @@ import ./v2/test_enr_utils, ./v2/test_peer_exchange, ./v2/test_waku_noise, - ./v2/test_waku_noise_sessions + ./v2/test_waku_noise_sessions, + # Utils + ./v2/test_utils_keyfile when defined(rln) or defined(rlnzerokit): import diff --git a/tests/v2/test_utils_keyfile.nim b/tests/v2/test_utils_keyfile.nim new file mode 100644 index 000000000..ffaa87164 --- /dev/null +++ b/tests/v2/test_utils_keyfile.nim @@ -0,0 +1,377 @@ +{.used.} + +import + std/[json, os], + stew/byteutils, + testutils/unittests, chronos, + eth/keys +import + ../../waku/v2/utils/keyfile + +from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte + +suite "KeyFile test suite": + + let rng = newRng() + + 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() + + # We load from the file all the decrypted keyfiles encrypted under password + var decodedKeyfiles = loadKeyFiles(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: + 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 = string.fromBytes(randomSeqByte(rng[], 20)) + let password2 = "" + let password3 = string.fromBytes(randomSeqByte(rng[], 20)) + 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: + 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 secret6 with password1 + let secret6 = randomSeqByte(rng[], 300) + keyfile = createKeyFileJson(secret6, password1) + check: + keyfile.isOk() + saveKeyFile(filepath, keyfile.get()).isOk() + + # 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 decodedKeyfilesPassword1 = loadKeyFiles(filepath, password1) + check: + decodedKeyfilesPassword1.isOk() + decodedKeyfilesPassword1.get().len == 3 + var decodedSecretsPassword1 = decodedKeyfilesPassword1.get() + + var decodedKeyfilesPassword2 = loadKeyFiles(filepath, password2) + check: + decodedKeyfilesPassword2.isOk() + decodedKeyfilesPassword2.get().len == 1 + var decodedSecretsPassword2 = decodedKeyfilesPassword2.get() + + var decodedKeyfilesPassword3 = loadKeyFiles(filepath, password3) + check: + decodedKeyfilesPassword3.isOk() + decodedKeyfilesPassword3.get().len == 2 + var decodedSecretsPassword3 = decodedKeyfilesPassword3.get() + + # We check if the corresponding secrets are correct + check: + # 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() + + +# 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 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": { + "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 "Testing nim-eth test vectors": + + var secret: KfResult[seq[byte]] + var expectedSecret: seq[byte] + + for i in 0..= 0: + 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": + return AES128CTR + else: + return CipherNoSupport + +# Key derivation routine for PBKDF2 +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) + 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) + 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) + var xyv = newSeq[uint32](xyvLen) + 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] = + + let wf = if workFactor == 0: ScryptWorkFactor else: workFactor + var output: DKey + if scrypt(password, salt, wf, r, p, output) == 0: + return err(ScryptBadParam) + + return ok(output) + +# Encryption routine +proc encryptData(plaintext: openArray[byte], + cryptkind: CryptKind, + key: openArray[byte], + iv: openArray[byte]): KfResult[seq[byte]] = + if cryptkind == AES128CTR: + var ciphertext = newSeqWith(plaintext.len, 0.byte) + var ctx: CTR[aes128] + ctx.init(toOpenArray(key, 0, 15), iv) + ctx.encrypt(plaintext, ciphertext) + ctx.clear() + ok(ciphertext) + else: + err(NotImplemented) + +# Decryption routine +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) + +# 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 + 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) + +# Decodes hex strings to byte sequences +proc decodeHex*(m: string): seq[byte] = + if len(m) > 0: + try: + return utils.fromHex(m) + except CatchableError: + return newSeq[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: + try: + sarr = utils.fromHex(m) + var output = newString(len(sarr)) + copyMem(addr output[0], addr sarr[0], len(sarr)) + return output + except CatchableError: + return "" + 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, + cryptkind: CryptKind = AES128CTR, + kdfkind: KdfKind = PBKDF2, + workfactor: int = 0): KfResult[JsonNode] = + ## Create JSON object with keyfile structure. + ## + ## ``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 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) + + let json = %* + { + "crypto": { + "cipher": $cryptkind, + "cipherparams": { + "iv": toHex(iv, true) + }, + "ciphertext": toHex(ciphertext, true), + "kdf": $kdfkind, + "kdfparams": params, + "mac": toHex(mac.data, true), + }, + } + + if IdInKeyfile: + json.add("id", %($u)) + if VersionInKeyfile: + json.add("version", %version) + + ok(json) + +# Parses Cipher JSON information +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) + + return ok(c) + +# Parses PNKDF2 JSON parameters +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) + + return ok(p) + +# Parses JSON Scrypt parameters +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) + + return ok(p) + +# Decrypts data +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() + ctx.clear() + 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) + +# 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 + ## 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) + return 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) + return decryptSecret(crypto, dkey) + +# Loads the file at pathname, decrypts and returns all keyfiles encrypted under password +proc loadKeyFiles*(pathname: string, + 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 + var data: JsonNode + 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 + 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) + +# 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, fmAppend): + return err(OsError) + try: + # 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: + err(OsError) + finally: + f.close() +