diff --git a/apps/chat2/chat2.nim b/apps/chat2/chat2.nim index dd01e81e82..2aaa594f99 100644 --- a/apps/chat2/chat2.nim +++ b/apps/chat2/chat2.nim @@ -508,11 +508,10 @@ proc processInput(rfd: AsyncFD, rng: ref HmacDrbgContext) {.async.} = let rlnConf = WakuRlnConfig( rlnRelayDynamic: conf.rlnRelayDynamic, rlnRelayCredIndex: conf.rlnRelayCredIndex, - rlnRelayMembershipGroupIndex: conf.rlnRelayMembershipGroupIndex, rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress, rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress, rlnRelayCredPath: conf.rlnRelayCredPath, - rlnRelayCredentialsPassword: conf.rlnRelayCredentialsPassword + rlnRelayCredPassword: conf.rlnRelayCredPassword ) waitFor node.mountRlnRelay(rlnConf, diff --git a/apps/chat2/config_chat2.nim b/apps/chat2/config_chat2.nim index 09b23a0411..d3c792515b 100644 --- a/apps/chat2/config_chat2.nim +++ b/apps/chat2/config_chat2.nim @@ -237,11 +237,6 @@ type defaultValue: 0 name: "rln-relay-cred-index" }: uint - rlnRelayMembershipGroupIndex* {. - desc: "the index of credentials to use, within a specific rln membership set", - defaultValue: 0 - name: "rln-relay-membership-group-index" }: uint - rlnRelayDynamic* {. desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false", defaultValue: false @@ -267,7 +262,7 @@ type defaultValue: "" name: "rln-relay-eth-contract-address" }: string - rlnRelayCredentialsPassword* {. + rlnRelayCredPassword* {. desc: "Password for encrypting RLN credentials", defaultValue: "" name: "rln-relay-cred-password" }: string diff --git a/apps/wakunode2/app.nim b/apps/wakunode2/app.nim index 478ebc592e..d7c1482502 100644 --- a/apps/wakunode2/app.nim +++ b/apps/wakunode2/app.nim @@ -398,11 +398,10 @@ proc setupProtocols(node: WakuNode, let rlnConf = WakuRlnConfig( rlnRelayDynamic: conf.rlnRelayDynamic, rlnRelayCredIndex: conf.rlnRelayCredIndex, - rlnRelayMembershipGroupIndex: conf.rlnRelayMembershipGroupIndex, rlnRelayEthContractAddress: conf.rlnRelayEthContractAddress, rlnRelayEthClientAddress: conf.rlnRelayEthClientAddress, rlnRelayCredPath: conf.rlnRelayCredPath, - rlnRelayCredentialsPassword: conf.rlnRelayCredentialsPassword, + rlnRelayCredPassword: conf.rlnRelayCredPassword, rlnRelayTreePath: conf.rlnRelayTreePath, rlnRelayBandwidthThreshold: conf.rlnRelayBandwidthThreshold ) diff --git a/apps/wakunode2/external_config.nim b/apps/wakunode2/external_config.nim index b62092d2ea..21dda0a668 100644 --- a/apps/wakunode2/external_config.nim +++ b/apps/wakunode2/external_config.nim @@ -150,11 +150,6 @@ type defaultValue: 0 name: "rln-relay-membership-index" }: uint - rlnRelayMembershipGroupIndex* {. - desc: "the index of credentials to use, within a specific rln membership set", - defaultValue: 0 - name: "rln-relay-membership-group-index" }: uint - rlnRelayDynamic* {. desc: "Enable waku-rln-relay with on-chain dynamic group management: true|false", defaultValue: false @@ -180,7 +175,7 @@ type defaultValue: "" name: "rln-relay-eth-contract-address" }: string - rlnRelayCredentialsPassword* {. + rlnRelayCredPassword* {. desc: "Password for encrypting RLN credentials", defaultValue: "" name: "rln-relay-cred-password" }: string diff --git a/tests/test_waku_keystore.nim b/tests/test_waku_keystore.nim index 270feadb7e..bf870c33d3 100644 --- a/tests/test_waku_keystore.nim +++ b/tests/test_waku_keystore.nim @@ -1,8 +1,9 @@ {.used.} import - std/[algorithm, json, options, os], - testutils/unittests, chronos, stint + std/[os, json], + chronos, + testutils/unittests import ../../waku/waku_keystore, ./testlib/common @@ -44,7 +45,7 @@ procSuite "Credentials test suite": keystore["appIdentifier"].getStr() == testAppInfo.appIdentifier keystore["version"].getStr() == testAppInfo.version # We assume the loaded keystore to not have credentials set (previous tests delete the keystore at filepath) - keystore["credentials"].getElems().len() == 0 + keystore["credentials"].len() == 0 test "Add credentials to keystore": @@ -61,30 +62,15 @@ procSuite "Credentials test suite": var idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment) var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") - var index1 = MembershipIndex(1) - var membershipGroup1 = MembershipGroup(membershipContract: contract, treeIndex: index1) - - let membershipCredentials1 = MembershipCredentials(identityCredential: idCredential, - membershipGroups: @[membershipGroup1]) - - # We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen) - idTrapdoor = randomSeqByte(rng[], 32) - idNullifier = randomSeqByte(rng[], 32) - idSecretHash = randomSeqByte(rng[], 32) - idCommitment = randomSeqByte(rng[], 32) - - idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment) - - var index2 = MembershipIndex(2) - var membershipGroup2 = MembershipGroup(membershipContract: contract, treeIndex: index2) - - let membershipCredentials2 = MembershipCredentials(identityCredential: idCredential, - membershipGroups: @[membershipGroup2]) + var index = MembershipIndex(1) + let membershipCredential = KeystoreMembership(membershipContract: contract, + treeIndex: index, + identityCredential: idCredential) let password = "%m0um0ucoW%" let keystoreRes = addMembershipCredentials(path = filepath, - credentials = @[membershipCredentials1, membershipCredentials2], + membership = membershipCredential, password = password, appInfo = testAppInfo) @@ -98,47 +84,25 @@ procSuite "Credentials test suite": # We generate two random identity credentials (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen) var - idTrapdoor1 = randomSeqByte(rng[], 32) - idNullifier1 = randomSeqByte(rng[], 32) - idSecretHash1 = randomSeqByte(rng[], 32) - idCommitment1 = randomSeqByte(rng[], 32) - idCredential1 = IdentityCredential(idTrapdoor: idTrapdoor1, idNullifier: idNullifier1, idSecretHash: idSecretHash1, idCommitment: idCommitment1) - - var - idTrapdoor2 = randomSeqByte(rng[], 32) - idNullifier2 = randomSeqByte(rng[], 32) - idSecretHash2 = randomSeqByte(rng[], 32) - idCommitment2 = randomSeqByte(rng[], 32) - idCredential2 = IdentityCredential(idTrapdoor: idTrapdoor2, idNullifier: idNullifier2, idSecretHash: idSecretHash2, idCommitment: idCommitment2) + idTrapdoor = randomSeqByte(rng[], 32) + idNullifier = randomSeqByte(rng[], 32) + idSecretHash = randomSeqByte(rng[], 32) + idCommitment = randomSeqByte(rng[], 32) + idCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment) # We generate two distinct membership groups - var contract1 = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") - var index1 = MembershipIndex(1) - var membershipGroup1 = MembershipGroup(membershipContract: contract1, treeIndex: index1) - - var contract2 = MembershipContract(chainId: "6", address: "0x0000000000000000000000000000000000000000") - var index2 = MembershipIndex(2) - var membershipGroup2 = MembershipGroup(membershipContract: contract2, treeIndex: index2) - - # We generate three membership credentials - let membershipCredentials1 = MembershipCredentials(identityCredential: idCredential1, - membershipGroups: @[membershipGroup1]) - - let membershipCredentials2 = MembershipCredentials(identityCredential: idCredential2, - membershipGroups: @[membershipGroup2]) - - let membershipCredentials3 = MembershipCredentials(identityCredential: idCredential1, - membershipGroups: @[membershipGroup2]) + var contract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") + var index = MembershipIndex(1) + var membershipCredential = KeystoreMembership(membershipContract: contract, + treeIndex: index, + identityCredential: idCredential) - # This is the same as rlnMembershipCredentials3, should not change the keystore entry of idCredential - let membershipCredentials4 = MembershipCredentials(identityCredential: idCredential1, - membershipGroups: @[membershipGroup2]) let password = "%m0um0ucoW%" # We add credentials to the keystore. Note that only 3 credentials should be effectively added, since rlnMembershipCredentials3 is equal to membershipCredentials2 let keystoreRes = addMembershipCredentials(path = filepath, - credentials = @[membershipCredentials1, membershipCredentials2, membershipCredentials3, membershipCredentials4], + membership = membershipCredential, password = password, appInfo = testAppInfo) @@ -146,45 +110,16 @@ procSuite "Credentials test suite": keystoreRes.isOk() # We test retrieval of credentials. - var expectedMembershipGroups1 = @[membershipGroup1, membershipGroup2] - expectedMembershipGroups1.sort(sortMembershipGroup) - let expectedCredential1 = MembershipCredentials(identityCredential: idCredential1, - membershipGroups: expectedMembershipGroups1) - - - var expectedMembershipGroups2 = @[membershipGroup2] - expectedMembershipGroups2.sort(sortMembershipGroup) - let expectedCredential2 = MembershipCredentials(identityCredential: idCredential2, - membershipGroups: expectedMembershipGroups2) + var expectedMembership = membershipCredential + let membershipQuery = KeystoreMembership(membershipContract: contract, + treeIndex: index) - - # We retrieve all credentials stored under password (no filter) var recoveredCredentialsRes = getMembershipCredentials(path = filepath, password = password, + query = membershipQuery, appInfo = testAppInfo) check: recoveredCredentialsRes.isOk() - recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2] - - - # We retrieve credentials by filtering on an IdentityCredential - recoveredCredentialsRes = getMembershipCredentials(path = filepath, - password = password, - filterIdentityCredentials = @[idCredential1], - appInfo = testAppInfo) - - check: - recoveredCredentialsRes.isOk() - recoveredCredentialsRes.get() == @[expectedCredential1] - - # We retrieve credentials by filtering on multiple IdentityCredentials - recoveredCredentialsRes = getMembershipCredentials(path = filepath, - password = password, - filterIdentityCredentials = @[idCredential1, idCredential2], - appInfo = testAppInfo) - - check: - recoveredCredentialsRes.isOk() - recoveredCredentialsRes.get() == @[expectedCredential1, expectedCredential2] + recoveredCredentialsRes.get() == expectedMembership diff --git a/tests/waku_rln_relay/test_waku_rln_relay.nim b/tests/waku_rln_relay/test_waku_rln_relay.nim index 4797ee92c5..7c3f82244e 100644 --- a/tests/waku_rln_relay/test_waku_rln_relay.nim +++ b/tests/waku_rln_relay/test_waku_rln_relay.nim @@ -815,10 +815,14 @@ suite "Waku rln relay": let index = MembershipIndex(1) - let rlnMembershipContract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789") - let rlnMembershipGroup = MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: index) - let rlnMembershipCredentials = MembershipCredentials(identityCredential: idCredential, membershipGroups: @[rlnMembershipGroup]) - + let keystoreMembership = KeystoreMembership( + membershipContract: MembershipContract( + chainId: "5", + address: "0x0123456789012345678901234567890123456789" + ), + treeIndex: index, + identityCredential: idCredential, + ) let password = "%m0um0ucoW%" let filepath = "./testRLNCredentials.txt" @@ -827,30 +831,29 @@ suite "Waku rln relay": # Write RLN credentials require: addMembershipCredentials(path = filepath, - credentials = @[rlnMembershipCredentials], - password = password, - appInfo = RLNAppInfo).isOk() + membership = keystoreMembership, + password = password, + appInfo = RLNAppInfo).isOk() - let readCredentialsResult = getMembershipCredentials(path = filepath, + let readKeystoreRes = getMembershipCredentials(path = filepath, password = password, - filterMembershipContracts = @[rlnMembershipContract], + # here the query would not include + # the identityCredential, + # since it is not part of the query + # but have used the same value + # to avoid re-declaration + query = keystoreMembership, appInfo = RLNAppInfo) - - require: - readCredentialsResult.isOk() - - # getMembershipCredentials returns all credentials in keystore as sequence matching the filter - let allMatchingCredentials = readCredentialsResult.get() - # if any is found, we return the first credential, otherwise credentials is none - var credentials = none(MembershipCredentials) - if allMatchingCredentials.len() > 0: - credentials = some(allMatchingCredentials[0]) - - require: - credentials.isSome() + assert readKeystoreRes.isOk(), $readKeystoreRes.error + + # getMembershipCredentials returns the credential in the keystore which matches + # the query, in this case the query is = + # chainId = "5" and + # address = "0x0123456789012345678901234567890123456789" and + # treeIndex = 1 + let readKeystoreMembership = readKeystoreRes.get() check: - credentials.get().identityCredential == idCredential - credentials.get().membershipGroups == @[rlnMembershipGroup] + readKeystoreMembership == keystoreMembership test "histogram static bucket generation": let buckets = generateBucketsForHistogram(10) diff --git a/tools/rln_keystore_generator/rln_keystore_generator.nim b/tools/rln_keystore_generator/rln_keystore_generator.nim index f6a726debf..2283b83c8e 100644 --- a/tools/rln_keystore_generator/rln_keystore_generator.nim +++ b/tools/rln_keystore_generator/rln_keystore_generator.nim @@ -79,19 +79,17 @@ when isMainModule: debug "Transaction hash", txHash = groupManager.registrationTxHash.get() # 6. write to keystore - let keystoreCred = MembershipCredentials( + let keystoreCred = KeystoreMembership( + membershipContract: MembershipContract( + chainId: $groupManager.chainId.get(), + address: conf.rlnRelayEthContractAddress, + ), + treeIndex: groupManager.membershipIndex.get(), identityCredential: credential, - membershipGroups: @[MembershipGroup( - membershipContract: MembershipContract( - chainId: $groupManager.chainId.get(), - address: conf.rlnRelayEthContractAddress, - ), - treeIndex: groupManager.membershipIndex.get(), - )] ) let persistRes = addMembershipCredentials(conf.rlnRelayCredPath, - @[keystoreCred], + keystoreCred, conf.rlnRelayCredPassword, RLNAppInfo) if persistRes.isErr(): diff --git a/waku/waku_keystore/conversion_utils.nim b/waku/waku_keystore/conversion_utils.nim index b87b91cfe7..a77e691ee7 100644 --- a/waku/waku_keystore/conversion_utils.nim +++ b/waku/waku_keystore/conversion_utils.nim @@ -8,22 +8,24 @@ import stew/[results, byteutils], ./protocol_types -# Encodes a Membership credential to a byte sequence -proc encode*(credential: MembershipCredentials): seq[byte] = +# Encodes a KeystoreMembership credential to a byte sequence +proc encode*(credential: KeystoreMembership): seq[byte] = # TODO: use custom encoding, avoid wordy json var stringCredential: string # NOTE: toUgly appends to the string, doesn't replace its contents stringCredential.toUgly(%credential) return toBytes(stringCredential) -# Decodes a byte sequence to a Membership credential -proc decode*(encodedCredential: seq[byte]): KeystoreResult[MembershipCredentials] = +# Decodes a byte sequence to a KeystoreMembership credential +proc decode*(encodedCredential: seq[byte]): KeystoreResult[KeystoreMembership] = # TODO: use custom decoding, avoid wordy json try: # we parse the json decrypted keystoreCredential let jsonObject = parseJson(string.fromBytes(encodedCredential)) - return ok(to(jsonObject, MembershipCredentials)) + return ok(to(jsonObject, KeystoreMembership)) except JsonParsingError: - return err(KeystoreJsonError) + return err(AppKeystoreError(kind: KeystoreJsonError, + msg: getCurrentExceptionMsg())) except Exception: #parseJson raises Exception - return err(KeystoreOsError) \ No newline at end of file + return err(AppKeystoreError(kind: KeystoreOsError, + msg: getCurrentExceptionMsg())) diff --git a/waku/waku_keystore/keystore.nim b/waku/waku_keystore/keystore.nim index 5bcaa95dda..c7d491c9c1 100644 --- a/waku/waku_keystore/keystore.nim +++ b/waku/waku_keystore/keystore.nim @@ -5,7 +5,7 @@ else: import options, json, strutils, - std/[algorithm, os, sequtils, sets] + std/[tables, os] import ./keyfile, @@ -20,15 +20,16 @@ proc createAppKeystore*(path: string, let keystore = AppKeystore(application: appInfo.application, appIdentifier: appInfo.appIdentifier, - credentials: @[], - version: appInfo.version) + version: appInfo.version, + credentials: initTable[string, KeystoreMembership]()) var jsonKeystore: string jsonKeystore.toUgly(%keystore) var f: File if not f.open(path, fmWrite): - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: "Cannot open file for writing")) try: # To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user @@ -38,7 +39,8 @@ proc createAppKeystore*(path: string, f.write(separator) ok() except CatchableError: - err(KeystoreOsError) + err(AppKeystoreError(kind: KeystoreOsError, + msg: getCurrentExceptionMsg())) finally: f.close() @@ -54,16 +56,17 @@ proc loadAppKeystore*(path: string, # If no keystore exists at path we create a new empty one with passed keystore parameters if fileExists(path) == false: - let newKeystore = createAppKeystore(path, appInfo, separator) - if newKeystore.isErr(): - return err(KeystoreCreateKeystoreError) + let newKeystoreRes = createAppKeystore(path, appInfo, separator) + if newKeystoreRes.isErr(): + return err(newKeystoreRes.error) try: # We read all the file contents var f: File if not f.open(path, fmRead): - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: "Cannot open file for reading")) let fileContents = readAll(f) # We iterate over each substring split by separator (which we expect to correspond to a single keystore json) @@ -92,23 +95,28 @@ proc loadAppKeystore*(path: string, break # TODO: we might continue rather than return for some of these errors except JsonParsingError: - return err(KeystoreJsonError) + return err(AppKeystoreError(kind: KeystoreJsonError, + msg: getCurrentExceptionMsg())) except ValueError: - return err(KeystoreJsonError) + return err(AppKeystoreError(kind: KeystoreJsonError, + msg: getCurrentExceptionMsg())) except OSError: - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: getCurrentExceptionMsg())) except Exception: #parseJson raises Exception - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: getCurrentExceptionMsg())) except IOError: - return err(KeystoreIoError) + return err(AppKeystoreError(kind: KeystoreIoError, + msg: getCurrentExceptionMsg())) return ok(matchingAppKeystore) -# Adds a sequence of membership credential to the keystore matching the application, appIdentifier and version filters. +# Adds a membership credential to the keystore matching the application, appIdentifier and version filters. proc addMembershipCredentials*(path: string, - credentials: seq[MembershipCredentials], + membership: KeystoreMembership, password: string, appInfo: AppInfo, separator: string = "\n"): KeystoreResult[void] = @@ -118,77 +126,38 @@ proc addMembershipCredentials*(path: string, let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator) if jsonKeystoreRes.isErr(): - return err(KeystoreLoadKeystoreError) + return err(jsonKeystoreRes.error) # We load the JSON node corresponding to the app keystore var jsonKeystore = jsonKeystoreRes.get() try: - if jsonKeystore.hasKey("credentials"): # We get all credentials in keystore - var keystoreCredentials = jsonKeystore["credentials"] - var found: bool - - for membershipCredential in credentials: - - # A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential - found = false - - for keystoreCredential in keystoreCredentials.mitems(): - # keystoreCredential is encrypted. We decrypt it - let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) - if decodedKeyfileRes.isOk(): - - # we parse the json decrypted keystoreCredential - let decodedCredentialRes = decode(decodedKeyfileRes.get()) - - if decodedCredentialRes.isOk(): - let keyfileMembershipCredential = decodedCredentialRes.get() - - # We check if the decrypted credential has its identityCredential field equal to the input credential - if keyfileMembershipCredential.identityCredential == membershipCredential.identityCredential: - # idCredential is present in keystore. We add the input credential membership group to the one contained in the decrypted keystore credential (we deduplicate groups using sets) - var allMemberships = toSeq(toHashSet(keyfileMembershipCredential.membershipGroups) + toHashSet(membershipCredential.membershipGroups)) - - # We sort membership groups, otherwise we will not have deterministic results in tests - allMemberships.sort(sortMembershipGroup) - - # we define the updated credential with the updated membership sets - let updatedCredential = MembershipCredentials(identityCredential: keyfileMembershipCredential.identityCredential, membershipGroups: allMemberships) - - # we re-encrypt creating a new keyfile - let encodedUpdatedCredential = updatedCredential.encode() - let updatedCredentialKeyfileRes = createKeyFileJson(encodedUpdatedCredential, password) - if updatedCredentialKeyfileRes.isErr(): - return err(KeystoreCreateKeyfileError) - - # we update the original credential field in keystoreCredentials - keystoreCredential = updatedCredentialKeyfileRes.get() + let keystoreCredentials = jsonKeystore["credentials"] + let key = membership.hash() + if keystoreCredentials.hasKey(key): + # noop + return ok() - found = true + let encodedMembershipCredential = membership.encode() + let keyfileRes = createKeyFileJson(encodedMembershipCredential, password) + if keyfileRes.isErr(): + return err(AppKeystoreError(kind: KeystoreCreateKeyfileError, + msg: $keyfileRes.error)) - # We stop decrypting other credentials in the keystore - break - - # If no credential in keystore with same input identityCredential value is found, we add it - if found == false: - - let encodedMembershipCredential = membershipCredential.encode() - let keyfileRes = createKeyFileJson(encodedMembershipCredential, password) - if keyfileRes.isErr(): - return err(KeystoreCreateKeyfileError) - - # We add it to the credentials field of the keystore - jsonKeystore["credentials"].add(keyfileRes.get()) + # We add it to the credentials field of the keystore + jsonKeystore["credentials"][key] = keyfileRes.get() except CatchableError: - return err(KeystoreJsonError) + return err(AppKeystoreError(kind: KeystoreJsonError, + msg: getCurrentExceptionMsg())) # We save to disk the (updated) keystore. - if save(jsonKeystore, path, separator).isErr(): - return err(KeystoreOsError) + let saveRes = save(jsonKeystore, path, separator) + if saveRes.isErr(): + return err(saveRes.error) return ok() @@ -196,18 +165,15 @@ proc addMembershipCredentials*(path: string, # identity credentials and membership contracts proc getMembershipCredentials*(path: string, password: string, - filterIdentityCredentials: seq[IdentityCredential] = @[], - filterMembershipContracts: seq[MembershipContract] = @[], - appInfo: AppInfo): KeystoreResult[seq[MembershipCredentials]] = - - var outputMembershipCredentials: seq[MembershipCredentials] = @[] + query: KeystoreMembership, + appInfo: AppInfo): KeystoreResult[KeystoreMembership] = # We load the keystore corresponding to the desired parameters # This call ensures that JSON has all required fields let jsonKeystoreRes = loadAppKeystore(path, appInfo) if jsonKeystoreRes.isErr(): - return err(KeystoreLoadKeystoreError) + return err(jsonKeystoreRes.error) # We load the JSON node corresponding to the app keystore var jsonKeystore = jsonKeystoreRes.get() @@ -215,27 +181,24 @@ proc getMembershipCredentials*(path: string, try: if jsonKeystore.hasKey("credentials"): - # We get all credentials in keystore var keystoreCredentials = jsonKeystore["credentials"] - - for keystoreCredential in keystoreCredentials.mitems(): - - # keystoreCredential is encrypted. We decrypt it - let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) - if decodedKeyfileRes.isOk(): - # we parse the json decrypted keystoreCredential - let decodedCredentialRes = decode(decodedKeyfileRes.get()) - - if decodedCredentialRes.isOk(): - let keyfileMembershipCredential = decodedCredentialRes.get() - - let filteredCredentialOpt = filterCredential(keyfileMembershipCredential, filterIdentityCredentials, filterMembershipContracts) - - if filteredCredentialOpt.isSome(): - outputMembershipCredentials.add(filteredCredentialOpt.get()) + let key = query.hash() + if not keystoreCredentials.hasKey(key): + # error + return err(AppKeystoreError(kind: KeystoreCredentialNotFoundError, + msg: "Credential not found in keystore")) + + let keystoreCredential = keystoreCredentials[key] + let decodedKeyfileRes = decodeKeyFileJson(keystoreCredential, password) + if decodedKeyfileRes.isErr(): + return err(AppKeystoreError(kind: KeystoreReadKeyfileError, + msg: $decodedKeyfileRes.error)) + # we parse the json decrypted keystoreCredential + let decodedCredentialRes = decode(decodedKeyfileRes.get()) + let keyfileMembershipCredential = decodedCredentialRes.get() + return ok(keyfileMembershipCredential) except CatchableError: - return err(KeystoreJsonError) - - return ok(outputMembershipCredentials) + return err(AppKeystoreError(kind: KeystoreJsonError, + msg: getCurrentExceptionMsg())) diff --git a/waku/waku_keystore/protocol_types.nim b/waku/waku_keystore/protocol_types.nim index 95b3254a83..c1a5babd76 100644 --- a/waku/waku_keystore/protocol_types.nim +++ b/waku/waku_keystore/protocol_types.nim @@ -4,8 +4,9 @@ else: {.push raises: [].} import - std/sequtils, + std/[sequtils, tables], stew/[results, endians2], + nimcrypto, stint # NOTE: 256-bytes long credentials are due to the use of BN254 in RLN. Other implementations/curves might have a different byte size @@ -88,13 +89,28 @@ type MembershipContract* = object chainId*: string address*: string -type MembershipGroup* = object +type KeystoreMembership* = ref object of RootObj membershipContract*: MembershipContract treeIndex*: MembershipIndex + identityCredential*: IdentityCredential -type MembershipCredentials* = object - identityCredential*: IdentityCredential - membershipGroups*: seq[MembershipGroup] +proc `$`*(m: KeystoreMembership): string = + return "KeystoreMembership(chainId: " & m.membershipContract.chainId & ", contractAddress: " & m.membershipContract.address & ", treeIndex: " & $m.treeIndex & ", identityCredential: " & $m.identityCredential & ")" + +proc `==`*(x, y: KeystoreMembership): bool = + return x.membershipContract.chainId == y.membershipContract.chainId and + x.membershipContract.address == y.membershipContract.address and + x.treeIndex == y.treeIndex and + x.identityCredential.idTrapdoor == y.identityCredential.idTrapdoor and + x.identityCredential.idNullifier == y.identityCredential.idNullifier and + x.identityCredential.idSecretHash == y.identityCredential.idSecretHash and + x.identityCredential.idCommitment == y.identityCredential.idCommitment + +proc hash*(m: KeystoreMembership): string = + # hash together the chainId, address and treeIndex + return $sha256.digest(m.membershipContract.chainId & m.membershipContract.address & $m.treeIndex) + +type MembershipTable* = Table[string, KeystoreMembership] type AppInfo* = object application*: string @@ -104,11 +120,11 @@ type AppInfo* = object type AppKeystore* = object application*: string appIdentifier*: string - credentials*: seq[MembershipCredentials] + credentials*: MembershipTable version*: string type - AppKeystoreError* = enum + AppKeystoreErrorKind* = enum KeystoreOsError = "keystore error: OS specific error" KeystoreIoError = "keystore error: IO specific error" KeystoreJsonKeyError = "keystore error: fields not present in JSON" @@ -119,5 +135,14 @@ type KeystoreCreateKeyfileError = "Error while creating keyfile for credentials" KeystoreSaveKeyfileError = "Error while saving keyfile for credentials" KeystoreReadKeyfileError = "Error while reading keyfile for credentials" + KeystoreCredentialAlreadyPresentError = "Error while adding credentials to keystore: credential already present" + KeystoreCredentialNotFoundError = "Error while searching credentials in keystore: credential not found" + + AppKeystoreError* = object + kind*: AppKeystoreErrorKind + msg*: string + +proc `$`*(e: AppKeystoreError) : string = + return $e.kind & ": " & e.msg type KeystoreResult*[T] = Result[T, AppKeystoreError] \ No newline at end of file diff --git a/waku/waku_keystore/utils.nim b/waku/waku_keystore/utils.nim index da85298fd3..736acbd8df 100644 --- a/waku/waku_keystore/utils.nim +++ b/waku/waku_keystore/utils.nim @@ -5,7 +5,9 @@ else: import json, - std/[options, os, sequtils], + std/[os, sequtils] + +import ./keyfile, ./protocol_types @@ -13,25 +15,22 @@ import proc hasKeys*(data: JsonNode, keys: openArray[string]): bool = return all(keys, proc (key: string): bool = return data.hasKey(key)) -# Defines how to sort membership groups -proc sortMembershipGroup*(a,b: MembershipGroup): int = - return cmp(a.membershipContract.address, b.membershipContract.address) - # Safely saves a Keystore's JsonNode to disk. # If exists, the destination file is renamed with extension .bkp; the file is written at its destination and the .bkp file is removed if write is successful, otherwise is restored proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void] = - # We first backup the current keystore if fileExists(path): try: moveFile(path, path & ".bkp") except: # TODO: Fix "BareExcept" warning - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: "could not backup keystore: " & getCurrentExceptionMsg())) # We save the updated json var f: File if not f.open(path, fmAppend): - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: getCurrentExceptionMsg())) try: # To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user setFilePermissions(path, {fpUserWrite, fpUserRead}) @@ -47,8 +46,10 @@ proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void moveFile(path & ".bkp", path) except: # TODO: Fix "BareExcept" warning # Unlucky, we just fail - return err(KeystoreOsError) - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: "could not restore keystore backup: " & getCurrentExceptionMsg())) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: "could not write keystore: " & getCurrentExceptionMsg())) finally: f.close() @@ -57,39 +58,7 @@ proc save*(json: JsonNode, path: string, separator: string): KeystoreResult[void try: removeFile(path & ".bkp") except CatchableError: - return err(KeystoreOsError) + return err(AppKeystoreError(kind: KeystoreOsError, + msg: "could not remove keystore backup: " & getCurrentExceptionMsg())) return ok() - -# Filters a membership credential based on either input identity credential's value, membership contracts or both -proc filterCredential*(credential: MembershipCredentials, - filterIdentityCredentials: seq[IdentityCredential], - filterMembershipContracts: seq[MembershipContract]): Option[MembershipCredentials] = - - # We filter by identity credentials - if filterIdentityCredentials.len() != 0: - if (credential.identityCredential in filterIdentityCredentials) == false: - return none(MembershipCredentials) - - # We filter by membership groups credentials - if filterMembershipContracts.len() != 0: - # Here we keep only groups that match a contract in the filter - var membershipGroupsIntersection: seq[MembershipGroup] = @[] - # We check if we have a group in the input credential matching any contract in the filter - for membershipGroup in credential.membershipGroups: - if membershipGroup.membershipContract in filterMembershipContracts: - membershipGroupsIntersection.add(membershipGroup) - - if membershipGroupsIntersection.len() != 0: - # If we have a match on some groups, we return the credential with filtered groups - return some(MembershipCredentials(identityCredential: credential.identityCredential, - membershipGroups: membershipGroupsIntersection)) - - else: - return none(MembershipCredentials) - - # We hit this return only if - # - filterIdentityCredentials.len() == 0 and filterMembershipContracts.len() == 0 (no filter) - # - filterIdentityCredentials.len() != 0 and filterMembershipContracts.len() == 0 (filter only on identity credential) - # Indeed, filterMembershipContracts.len() != 0 will have its exclusive return based on all values of membershipGroupsIntersection.len() - return some(credential) diff --git a/waku/waku_rln_relay/constants.nim b/waku/waku_rln_relay/constants.nim index 792aad0414..7bd06ed97e 100644 --- a/waku/waku_rln_relay/constants.nim +++ b/waku/waku_rln_relay/constants.nim @@ -53,4 +53,4 @@ const MaxEpochGap* = uint64(MaxClockGapSeconds/EpochUnitSeconds) # RLN Keystore defaults const - RLNAppInfo* = AppInfo(application: "waku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.1") + RLNAppInfo* = AppInfo(application: "waku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.2") diff --git a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim index 4ca571c2ae..5c7b60c482 100644 --- a/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim +++ b/waku/waku_rln_relay/group_manager/on_chain/group_manager.nim @@ -56,8 +56,6 @@ type registrationTxHash*: Option[TxHash] chainId*: Option[Quantity] keystorePath*: Option[string] - keystoreIndex*: uint - membershipGroupIndex*: uint keystorePassword*: Option[string] registrationHandler*: Option[RegistrationHandler] # this buffer exists to backfill appropriate roots for the merkle tree, @@ -440,19 +438,24 @@ method init*(g: OnchainGroupManager): Future[void] {.async.} = g.registryContract = some(registryContract) if g.keystorePath.isSome() and g.keystorePassword.isSome(): + if g.membershipIndex.isNone(): + raise newException(CatchableError, "membership index is not set when keystore is provided") + let keystoreQuery = KeystoreMembership( + membershipContract: MembershipContract( + chainId: $g.chainId.get(), + address: g.ethContractAddress + ), + treeIndex: MembershipIndex(g.membershipIndex.get()), + ) waku_rln_membership_credentials_import_duration_seconds.nanosecondTime: - let parsedCredsRes = getMembershipCredentials(path = g.keystorePath.get(), - password = g.keystorePassword.get(), - filterMembershipContracts = @[MembershipContract(chainId: $chainId, - address: g.ethContractAddress)], - appInfo = RLNAppInfo) - if parsedCredsRes.isErr(): - raise newException(ValueError, "could not parse the keystore: " & $parsedCredsRes.error()) - let parsedCreds = parsedCredsRes.get() - if parsedCreds.len == 0: - raise newException(ValueError, "keystore is empty") - g.idCredentials = some(parsedCreds[g.keystoreIndex].identityCredential) - g.membershipIndex = some(parsedCreds[g.keystoreIndex].membershipGroups[g.membershipGroupIndex].treeIndex) + let keystoreCredRes = getMembershipCredentials(path = g.keystorePath.get(), + password = g.keystorePassword.get(), + query = keystoreQuery, + appInfo = RLNAppInfo) + if keystoreCredRes.isErr(): + raise newException(ValueError, "could not parse the keystore: " & $keystoreCredRes.error) + let keystoreCred = keystoreCredRes.get() + g.idCredentials = some(keystoreCred.identityCredential) let metadataGetRes = g.rlnInstance.getMetadata() if metadataGetRes.isErr(): diff --git a/waku/waku_rln_relay/rln_relay.nim b/waku/waku_rln_relay/rln_relay.nim index 3d5a9a5002..2c9046302d 100644 --- a/waku/waku_rln_relay/rln_relay.nim +++ b/waku/waku_rln_relay/rln_relay.nim @@ -32,11 +32,10 @@ logScope: type WakuRlnConfig* = object rlnRelayDynamic*: bool rlnRelayCredIndex*: uint - rlnRelayMembershipGroupIndex*: uint rlnRelayEthContractAddress*: string rlnRelayEthClientAddress*: string rlnRelayCredPath*: string - rlnRelayCredentialsPassword*: string + rlnRelayCredPassword*: string rlnRelayTreePath*: string rlnRelayBandwidthThreshold*: int @@ -343,7 +342,6 @@ proc mount(conf: WakuRlnConfig, ): Future[WakuRlnRelay] {.async.} = var groupManager: GroupManager - credentials: MembershipCredentials # create an RLN instance let rlnInstanceRes = createRLNInstance(tree_path = conf.rlnRelayTreePath) if rlnInstanceRes.isErr(): @@ -365,15 +363,14 @@ proc mount(conf: WakuRlnConfig, if s == "": none(string) else: some(s) let rlnRelayCredPath = useValueOrNone(conf.rlnRelayCredPath) - rlnRelayCredentialsPassword = useValueOrNone(conf.rlnRelayCredentialsPassword) + rlnRelayCredPassword = useValueOrNone(conf.rlnRelayCredPassword) groupManager = OnchainGroupManager(ethClientUrl: conf.rlnRelayEthClientAddress, ethContractAddress: $conf.rlnRelayEthContractAddress, rlnInstance: rlnInstance, registrationHandler: registrationHandler, keystorePath: rlnRelayCredPath, - keystorePassword: rlnRelayCredentialsPassword, - keystoreIndex: conf.rlnRelayCredIndex, - membershipGroupIndex: conf.rlnRelayMembershipGroupIndex) + keystorePassword: rlnRelayCredPassword, + membershipIndex: some(conf.rlnRelayCredIndex)) # Initialize the groupManager await groupManager.init() # Start the group sync