mirror of https://github.com/waku-org/nwaku.git
feat(credentials): store and processing generic app credentials (#1466)
* feat(credentials): store and processing generic app credentials * feat(credentials): separate module; minimal tests * more work * feat(credentials): check presence of idCredential in keystore and add only new membership groups * feat(credential): refactor, new data structure, dynamic add credential, filter * feat(credential): add filter, get credentials * feat(credential): encode/decode utility * feat(credential): sort groups, test credential retrieval/group merging * fix(credential): remove unnecessary order in sort * fix(credentials): fix vendor commits * fix(credential/rln): embed credential module in rln relay * feat(credentials/rln): use credentials API in rln-relay to store/read credentials * refactor(credentials): implement hasKeys for JsonNode * fix(credentials): restore connectToNodes call * refactor(credentials): remove unnecessary imports * refactor(credentials): add Res suffix to results * refactor(credential): moved save json to separate proc; added comments * feat(credentials): use appInfo * refactor(keystore): refactor code in a more structured module; address reviewers * fix(keystore): fix indentation
This commit is contained in:
parent
1a9f633311
commit
cdc09aeeb4
|
@ -27,7 +27,6 @@ when defined(waku_exp_store_resume):
|
|||
# TODO: Review store resume test cases (#1282)
|
||||
import ./v2/waku_store/test_resume
|
||||
|
||||
|
||||
import
|
||||
# Waku v2 tests
|
||||
./v2/test_wakunode,
|
||||
|
@ -61,11 +60,12 @@ import
|
|||
./v2/test_waku_noise,
|
||||
./v2/test_waku_noise_sessions,
|
||||
./v2/test_waku_switch,
|
||||
# Waku Keystore
|
||||
./v2/test_waku_keystore_keyfile,
|
||||
./v2/test_waku_keystore,
|
||||
# Utils
|
||||
./v2/test_utils_compat,
|
||||
./v2/test_utils_keyfile
|
||||
|
||||
|
||||
./v2/test_utils_compat
|
||||
|
||||
## Experimental
|
||||
|
||||
when defined(rln):
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
{.used.}
|
||||
|
||||
import
|
||||
std/[algorithm, json, options, os],
|
||||
testutils/unittests, chronos, stint,
|
||||
../../waku/v2/protocol/waku_keystore,
|
||||
../test_helpers
|
||||
|
||||
from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte
|
||||
|
||||
procSuite "Credentials test suite":
|
||||
|
||||
# We initialize the RNG in test_helpers
|
||||
let rng = rng()
|
||||
let testAppInfo = AppInfo(application: "test", appIdentifier: "1234", version: "0.1")
|
||||
|
||||
asyncTest "Create keystore":
|
||||
|
||||
let filepath = "./testAppKeystore.txt"
|
||||
defer: removeFile(filepath)
|
||||
|
||||
let keystoreRes = createAppKeystore(path = filepath,
|
||||
appInfo = testAppInfo)
|
||||
|
||||
check:
|
||||
keystoreRes.isOk()
|
||||
|
||||
asyncTest "Load keystore":
|
||||
|
||||
let filepath = "./testAppKeystore.txt"
|
||||
defer: removeFile(filepath)
|
||||
|
||||
# If no keystore exists at filepath, a new one is created for appInfo and empty credentials
|
||||
let keystoreRes = loadAppKeystore(path = filepath,
|
||||
appInfo = testAppInfo)
|
||||
|
||||
check:
|
||||
keystoreRes.isOk()
|
||||
|
||||
let keystore = keystoreRes.get()
|
||||
|
||||
check:
|
||||
keystore.hasKeys(["application", "appIdentifier", "version", "credentials"])
|
||||
keystore["application"].getStr() == testAppInfo.application
|
||||
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
|
||||
|
||||
asyncTest "Add credentials to keystore":
|
||||
|
||||
let filepath = "./testAppKeystore.txt"
|
||||
defer: removeFile(filepath)
|
||||
|
||||
# We generate a random identity credential (inter-value constrains are not enforced, otherwise we need to load e.g. zerokit RLN keygen)
|
||||
var
|
||||
idTrapdoor = randomSeqByte(rng[], 32)
|
||||
idNullifier = randomSeqByte(rng[], 32)
|
||||
idSecretHash = randomSeqByte(rng[], 32)
|
||||
idCommitment = randomSeqByte(rng[], 32)
|
||||
|
||||
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])
|
||||
|
||||
let password = "%m0um0ucoW%"
|
||||
|
||||
let keystoreRes = addMembershipCredentials(path = filepath,
|
||||
credentials = @[membershipCredentials1, membershipCredentials2],
|
||||
password = password,
|
||||
appInfo = testAppInfo)
|
||||
|
||||
check:
|
||||
keystoreRes.isOk()
|
||||
|
||||
asyncTest "Add/retrieve credentials in keystore":
|
||||
|
||||
let filepath = "./testAppKeystore.txt"
|
||||
defer: removeFile(filepath)
|
||||
|
||||
# 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)
|
||||
|
||||
# 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])
|
||||
|
||||
# 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],
|
||||
password = password,
|
||||
appInfo = testAppInfo)
|
||||
|
||||
check:
|
||||
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)
|
||||
|
||||
|
||||
# We retrieve all credentials stored under password (no filter)
|
||||
var recoveredCredentialsRes = getMembershipCredentials(path = filepath,
|
||||
password = password,
|
||||
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]
|
||||
|
|
@ -6,7 +6,7 @@ import
|
|||
testutils/unittests, chronos,
|
||||
eth/keys
|
||||
import
|
||||
../../waku/v2/utils/keyfile
|
||||
../../waku/v2/protocol/waku_keystore
|
||||
|
||||
from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte
|
||||
|
||||
|
@ -312,7 +312,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)":
|
|||
|
||||
check:
|
||||
secret.isErr()
|
||||
secret.error == KeyFileError.IncorrectMac
|
||||
secret.error == KeyFileError.KeyfileIncorrectMac
|
||||
|
||||
test "Wrong mac in keyfile":
|
||||
|
||||
|
@ -350,7 +350,7 @@ suite "KeyFile test suite (adapted from nim-eth keyfile tests)":
|
|||
keyfileWrongMac.getOrDefault("password").getStr())
|
||||
check:
|
||||
secret.isErr()
|
||||
secret.error == KeyFileError.IncorrectMac
|
||||
secret.error == KeyFileError.KeyFileIncorrectMac
|
||||
|
||||
test "Scrypt keyfiles":
|
||||
let
|
|
@ -14,12 +14,14 @@ import
|
|||
../../waku/v2/node/waku_node,
|
||||
../../waku/v2/protocol/waku_message,
|
||||
../../waku/v2/protocol/waku_rln_relay,
|
||||
../../waku/v2/protocol/waku_keystore,
|
||||
../test_helpers
|
||||
|
||||
const RlnRelayPubsubTopic = "waku/2/rlnrelay/proto"
|
||||
const RlnRelayContentTopic = "waku/2/rlnrelay/proto"
|
||||
|
||||
procSuite "Waku rln relay":
|
||||
|
||||
asyncTest "mount waku-rln-relay in the off-chain mode":
|
||||
let
|
||||
nodeKey = crypto.PrivateKey.random(Secp256k1, rng[])[]
|
||||
|
@ -1041,10 +1043,11 @@ suite "Waku rln relay":
|
|||
|
||||
debug "the generated identity credential: ", idCredential
|
||||
|
||||
let index = MembershipIndex(1)
|
||||
let index = MembershipIndex(1)
|
||||
|
||||
let rlnMembershipCredentials = RlnMembershipCredentials(identityCredential: idCredential,
|
||||
rlnIndex: index)
|
||||
let rlnMembershipContract = MembershipContract(chainId: "5", address: "0x0123456789012345678901234567890123456789")
|
||||
let rlnMembershipGroup = MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: index)
|
||||
let rlnMembershipCredentials = MembershipCredentials(identityCredential: idCredential, membershipGroups: @[rlnMembershipGroup])
|
||||
|
||||
let password = "%m0um0ucoW%"
|
||||
|
||||
|
@ -1053,19 +1056,31 @@ suite "Waku rln relay":
|
|||
|
||||
# Write RLN credentials
|
||||
require:
|
||||
writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk()
|
||||
addMembershipCredentials(path = filepath,
|
||||
credentials = @[rlnMembershipCredentials],
|
||||
password = password,
|
||||
appInfo = RLNAppInfo).isOk()
|
||||
|
||||
let readCredentialsResult = getMembershipCredentials(path = filepath,
|
||||
password = password,
|
||||
filterMembershipContracts = @[rlnMembershipContract],
|
||||
appInfo = RLNAppInfo)
|
||||
|
||||
let readCredentialsResult = readRlnCredentials(filepath, password)
|
||||
require:
|
||||
readCredentialsResult.isOk()
|
||||
|
||||
let credentials = readCredentialsResult.get()
|
||||
# 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()
|
||||
check:
|
||||
credentials.get().identityCredential == idCredential
|
||||
credentials.get().rlnIndex == index
|
||||
credentials.get().membershipGroups == @[rlnMembershipGroup]
|
||||
|
||||
test "histogram static bucket generation":
|
||||
let buckets = generateBucketsForHistogram(10)
|
||||
|
|
|
@ -8,6 +8,7 @@ import
|
|||
stew/byteutils, stew/shims/net as stewNet,
|
||||
libp2p/crypto/crypto,
|
||||
eth/keys,
|
||||
../../waku/v2/protocol/waku_keystore,
|
||||
../../waku/v2/protocol/waku_rln_relay,
|
||||
../../waku/v2/node/waku_node,
|
||||
../test_helpers,
|
||||
|
|
|
@ -17,6 +17,7 @@ import
|
|||
../../waku/v2/node/waku_node,
|
||||
../../waku/v2/protocol/waku_message,
|
||||
../../waku/v2/protocol/waku_rln_relay,
|
||||
../../waku/v2/protocol/waku_keystore,
|
||||
../../waku/v2/utils/peers
|
||||
|
||||
from std/times import epochTime
|
||||
|
|
|
@ -390,7 +390,7 @@ proc info*(node: WakuNode): WakuInfo =
|
|||
proc connectToNodes*(node: WakuNode, nodes: seq[RemotePeerInfo] | seq[string], source = "api") {.async.} =
|
||||
## `source` indicates source of node addrs (static config, api call, discovery, etc)
|
||||
# NOTE This is dialing on WakuRelay protocol specifically
|
||||
await connectToNodes(node.peerManager, nodes, WakuRelayCodec, source=source)
|
||||
await peer_manager.connectToNodes(node.peerManager, nodes, WakuRelayCodec, source=source)
|
||||
|
||||
|
||||
## Waku relay
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
# The keyfile submodule (implementation adapted from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile)
|
||||
import
|
||||
./waku_keystore/keyfile
|
||||
|
||||
export
|
||||
keyfile
|
||||
|
||||
# The Waku Keystore implementation
|
||||
import
|
||||
./waku_keystore/keystore,
|
||||
./waku_keystore/conversion_utils,
|
||||
./waku_keystore/protocol_types,
|
||||
./waku_keystore/utils
|
||||
|
||||
export
|
||||
keystore,
|
||||
conversion_utils,
|
||||
protocol_types,
|
||||
utils
|
|
@ -0,0 +1,29 @@
|
|||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
json,
|
||||
stew/[results, byteutils],
|
||||
./protocol_types
|
||||
|
||||
# Encodes a Membership credential to a byte sequence
|
||||
proc encode*(credential: MembershipCredentials): 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] =
|
||||
# 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))
|
||||
except JsonParsingError:
|
||||
return err(KeystoreJsonError)
|
||||
except Exception: #parseJson raises Exception
|
||||
return err(KeystoreOsError)
|
|
@ -29,26 +29,26 @@ const
|
|||
|
||||
type
|
||||
KeyFileError* = enum
|
||||
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"
|
||||
IoError = "keyfile error: IO specific error"
|
||||
JsonError = "keyfile error: JSON encoder/decoder error"
|
||||
KeyfileRandomError = "keyfile error: Random generator error"
|
||||
KeyfileUuidError = "keyfile error: UUID generator error"
|
||||
KeyfileBufferOverrun = "keyfile error: Supplied buffer is too small"
|
||||
KeyfileIncorrectDKLen = "keyfile error: `dklen` parameter is 0 or more then MaxDKLen"
|
||||
KeyfileMalformedError = "keyfile error: JSON has incorrect structure"
|
||||
KeyfileNotImplemented = "keyfile error: Feature is not implemented"
|
||||
KeyfileNotSupported = "keyfile error: Feature is not supported"
|
||||
KeyfileEmptyMac = "keyfile error: `mac` parameter is zero length or not in hexadecimal form"
|
||||
KeyfileEmptyCiphertext = "keyfile error: `ciphertext` parameter is zero length or not in hexadecimal format"
|
||||
KeyfileEmptySalt = "keyfile error: `salt` parameter is zero length or not in hexadecimal format"
|
||||
KeyfileEmptyIV = "keyfile error: `cipherparams.iv` parameter is zero length or not in hexadecimal format"
|
||||
KeyfileIncorrectIV = "keyfile error: Size of IV vector is not equal to cipher block size"
|
||||
KeyfilePrfNotSupported = "keyfile error: PRF algorithm for PBKDF2 is not supported"
|
||||
KeyfileKdfNotSupported = "keyfile error: KDF algorithm is not supported"
|
||||
KeyfileCipherNotSupported = "keyfile error: `cipher` parameter is not supported"
|
||||
KeyfileIncorrectMac = "keyfile error: `mac` verification failed"
|
||||
KeyfileScryptBadParam = "keyfile error: bad scrypt's parameters"
|
||||
KeyfileOsError = "keyfile error: OS specific error"
|
||||
KeyfileIoError = "keyfile error: IO specific error"
|
||||
KeyfileJsonError = "keyfile error: JSON encoder/decoder error"
|
||||
KeyfileDoesNotExist = "keyfile error: file does not exist"
|
||||
|
||||
KdfKind* = enum
|
||||
|
@ -215,9 +215,9 @@ proc deriveKey(password: string,
|
|||
ctx.clear()
|
||||
ok(output)
|
||||
else:
|
||||
err(PrfNotSupported)
|
||||
err(KeyfilePrfNotSupported)
|
||||
else:
|
||||
err(NotImplemented)
|
||||
err(KeyfileNotImplemented)
|
||||
|
||||
# Scrypt wrapper
|
||||
func scrypt[T, M](password: openArray[T], salt: openArray[M],
|
||||
|
@ -234,7 +234,7 @@ proc deriveKey(password: string, salt: string,
|
|||
let wf = if workFactor == 0: ScryptWorkFactor else: workFactor
|
||||
var output: DKey
|
||||
if scrypt(password, salt, wf, r, p, output) == 0:
|
||||
return err(ScryptBadParam)
|
||||
return err(KeyfileScryptBadParam)
|
||||
|
||||
return ok(output)
|
||||
|
||||
|
@ -251,7 +251,7 @@ proc encryptData(plaintext: openArray[byte],
|
|||
ctx.clear()
|
||||
ok(ciphertext)
|
||||
else:
|
||||
err(NotImplemented)
|
||||
err(KeyfileNotImplemented)
|
||||
|
||||
# Decryption routine
|
||||
proc decryptData(ciphertext: openArray[byte],
|
||||
|
@ -260,7 +260,7 @@ proc decryptData(ciphertext: openArray[byte],
|
|||
iv: openArray[byte]): KfResult[seq[byte]] =
|
||||
if cryptkind == AES128CTR:
|
||||
if len(iv) != aes128.sizeBlock:
|
||||
return err(IncorrectIV)
|
||||
return err(KeyfileIncorrectIV)
|
||||
var plaintext = newSeqWith(ciphertext.len, 0.byte)
|
||||
var ctx: CTR[aes128]
|
||||
ctx.init(toOpenArray(key, 0, 15), iv)
|
||||
|
@ -268,7 +268,7 @@ proc decryptData(ciphertext: openArray[byte],
|
|||
ctx.clear()
|
||||
ok(plaintext)
|
||||
else:
|
||||
err(NotImplemented)
|
||||
err(KeyfileNotImplemented)
|
||||
|
||||
# Encodes KDF parameters in JSON
|
||||
proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNode] =
|
||||
|
@ -294,7 +294,7 @@ proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNo
|
|||
}
|
||||
)
|
||||
else:
|
||||
err(NotImplemented)
|
||||
err(KeyfileNotImplemented)
|
||||
|
||||
# Decodes hex strings to byte sequences
|
||||
proc decodeHex*(m: string): seq[byte] =
|
||||
|
@ -350,12 +350,12 @@ proc createKeyFileJson*(secret: openArray[byte],
|
|||
var salt: array[SaltSize, byte]
|
||||
var saltstr = newString(SaltSize)
|
||||
if randomBytes(iv) != aes128.sizeBlock:
|
||||
return err(RandomError)
|
||||
return err(KeyfileRandomError)
|
||||
if randomBytes(salt) != SaltSize:
|
||||
return err(RandomError)
|
||||
return err(KeyfileRandomError)
|
||||
copyMem(addr saltstr[0], addr salt[0], SaltSize)
|
||||
|
||||
let u = ? uuidGenerate().mapErrTo(UuidError)
|
||||
let u = ? uuidGenerate().mapErrTo(KeyfileUuidError)
|
||||
|
||||
let
|
||||
dkey = case kdfkind
|
||||
|
@ -398,21 +398,21 @@ proc createKeyFileJson*(secret: openArray[byte],
|
|||
proc decodeCrypto(n: JsonNode): KfResult[Crypto] =
|
||||
var crypto = n.getOrDefault("crypto")
|
||||
if isNil(crypto):
|
||||
return err(MalformedError)
|
||||
return err(KeyfileMalformedError)
|
||||
|
||||
var kdf = crypto.getOrDefault("kdf")
|
||||
if isNil(kdf):
|
||||
return err(MalformedError)
|
||||
return err(KeyfileMalformedError)
|
||||
|
||||
var c: Crypto
|
||||
case kdf.getStr()
|
||||
of "pbkdf2": c.kind = PBKDF2
|
||||
of "scrypt": c.kind = SCRYPT
|
||||
else: return err(KdfNotSupported)
|
||||
else: return err(KeyfileKdfNotSupported)
|
||||
|
||||
var cipherparams = crypto.getOrDefault("cipherparams")
|
||||
if isNil(cipherparams):
|
||||
return err(MalformedError)
|
||||
return err(KeyfileMalformedError)
|
||||
|
||||
c.cipher.kind = getCipher(crypto.getOrDefault("cipher").getStr())
|
||||
c.cipher.params.iv = decodeHex(cipherparams.getOrDefault("iv").getStr())
|
||||
|
@ -421,13 +421,13 @@ proc decodeCrypto(n: JsonNode): KfResult[Crypto] =
|
|||
c.kdfParams = crypto.getOrDefault("kdfparams")
|
||||
|
||||
if c.cipher.kind == CipherNoSupport:
|
||||
return err(CipherNotSupported)
|
||||
return err(KeyfileCipherNotSupported)
|
||||
if len(c.cipher.text) == 0:
|
||||
return err(EmptyCiphertext)
|
||||
return err(KeyfileEmptyCiphertext)
|
||||
if len(c.mac) == 0:
|
||||
return err(EmptyMac)
|
||||
return err(KeyfileEmptyMac)
|
||||
if isNil(c.kdfParams):
|
||||
return err(MalformedError)
|
||||
return err(KeyfileMalformedError)
|
||||
|
||||
return ok(c)
|
||||
|
||||
|
@ -436,16 +436,16 @@ proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] =
|
|||
var p: Pbkdf2Params
|
||||
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
|
||||
if len(p.salt) == 0:
|
||||
return err(EmptySalt)
|
||||
return err(KeyfileEmptySalt)
|
||||
|
||||
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)
|
||||
return err(KeyfilePrfNotSupported)
|
||||
if p.dklen == 0 or p.dklen > MaxDKLen:
|
||||
return err(IncorrectDKLen)
|
||||
return err(KeyfileIncorrectDKLen)
|
||||
|
||||
return ok(p)
|
||||
|
||||
|
@ -454,7 +454,7 @@ proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] =
|
|||
var p: ScryptParams
|
||||
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
|
||||
if len(p.salt) == 0:
|
||||
return err(EmptySalt)
|
||||
return err(KeyfileEmptySalt)
|
||||
|
||||
p.dklen = params.getOrDefault("dklen").getInt()
|
||||
p.n = params.getOrDefault("n").getInt()
|
||||
|
@ -462,7 +462,7 @@ proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] =
|
|||
p.r = params.getOrDefault("r").getInt()
|
||||
|
||||
if p.dklen == 0 or p.dklen > MaxDKLen:
|
||||
return err(IncorrectDKLen)
|
||||
return err(KeyfileIncorrectDKLen)
|
||||
|
||||
return ok(p)
|
||||
|
||||
|
@ -475,7 +475,7 @@ func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] =
|
|||
var mac = ctx.finish()
|
||||
ctx.clear()
|
||||
if not compareMac(mac.data, crypto.mac):
|
||||
return err(IncorrectMac)
|
||||
return err(KeyfileIncorrectMac)
|
||||
|
||||
let plaintext = ? decryptData(crypto.cipher.text, crypto.cipher.kind, dkey, crypto.cipher.params.iv)
|
||||
|
||||
|
@ -537,20 +537,20 @@ proc loadKeyFiles*(pathname: string,
|
|||
try:
|
||||
data = json.parseJson(keyfile)
|
||||
except JsonParsingError:
|
||||
return err(JsonError)
|
||||
return err(KeyfileJsonError)
|
||||
except ValueError:
|
||||
return err(JsonError)
|
||||
return err(KeyfileJsonError)
|
||||
except OSError:
|
||||
return err(OsError)
|
||||
return err(KeyfileOsError)
|
||||
except Exception: #parseJson raises Exception
|
||||
return err(OsError)
|
||||
return err(KeyfileOsError)
|
||||
|
||||
decodedKeyfile = decodeKeyFileJson(data, password)
|
||||
if decodedKeyfile.isOk():
|
||||
successfullyDecodedKeyfiles.add decodedKeyfile
|
||||
|
||||
except IOError:
|
||||
return err(IoError)
|
||||
return err(KeyfileIoError)
|
||||
|
||||
return ok(successfullyDecodedKeyfiles)
|
||||
|
||||
|
@ -561,7 +561,7 @@ proc saveKeyFile*(pathname: string,
|
|||
var
|
||||
f: File
|
||||
if not f.open(pathname, fmAppend):
|
||||
return err(OsError)
|
||||
return err(KeyfileOsError)
|
||||
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})
|
||||
|
@ -570,7 +570,7 @@ proc saveKeyFile*(pathname: string,
|
|||
f.write("\n")
|
||||
ok()
|
||||
except CatchableError:
|
||||
err(OsError)
|
||||
err(KeyfileOsError)
|
||||
finally:
|
||||
f.close()
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
options, json, strutils,
|
||||
std/[algorithm, os, sequtils, sets]
|
||||
|
||||
import
|
||||
./keyfile,
|
||||
./conversion_utils,
|
||||
./protocol_types,
|
||||
./utils
|
||||
|
||||
# This proc creates an empty keystore (i.e. with no credentials)
|
||||
proc createAppKeystore*(path: string,
|
||||
appInfo: AppInfo,
|
||||
separator: string = "\n"): KeystoreResult[void] =
|
||||
|
||||
let keystore = AppKeystore(application: appInfo.application,
|
||||
appIdentifier: appInfo.appIdentifier,
|
||||
credentials: @[],
|
||||
version: appInfo.version)
|
||||
|
||||
var jsonKeystore: string
|
||||
jsonKeystore.toUgly(%keystore)
|
||||
|
||||
var f: File
|
||||
if not f.open(path, fmWrite):
|
||||
return err(KeystoreOsError)
|
||||
|
||||
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})
|
||||
f.write(jsonKeystore)
|
||||
# We separate keystores with separator
|
||||
f.write(separator)
|
||||
ok()
|
||||
except CatchableError:
|
||||
err(KeystoreOsError)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
# This proc load a keystore based on the application, appIdentifier and version filters.
|
||||
# If none is found, it automatically creates an empty keystore for the passed parameters
|
||||
proc loadAppKeystore*(path: string,
|
||||
appInfo: AppInfo,
|
||||
separator: string = "\n"): KeystoreResult[JsonNode] =
|
||||
|
||||
## Load and decode JSON keystore from pathname
|
||||
var data: JsonNode
|
||||
var matchingAppKeystore: JsonNode
|
||||
|
||||
# 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)
|
||||
|
||||
try:
|
||||
|
||||
# We read all the file contents
|
||||
var f: File
|
||||
if not f.open(path, fmRead):
|
||||
return err(KeystoreOsError)
|
||||
let fileContents = readAll(f)
|
||||
|
||||
# We iterate over each substring split by separator (which we expect to correspond to a single keystore json)
|
||||
for keystore in fileContents.split(separator):
|
||||
|
||||
# We skip if read line is empty
|
||||
if keystore.len == 0:
|
||||
continue
|
||||
# We skip all lines that don't seem to define a json
|
||||
if not keystore.startsWith("{") or not keystore.endsWith("}"):
|
||||
continue
|
||||
|
||||
try:
|
||||
# We parse the json
|
||||
data = json.parseJson(keystore)
|
||||
|
||||
# We check if parsed json contains the relevant keystore credentials fields and if these are set to the passed parameters
|
||||
# (note that "if" is lazy, so if one of the .contains() fails, the json fields contents will not be checked and no ResultDefect will be raised due to accessing unavailable fields)
|
||||
if data.hasKeys(["application", "appIdentifier", "credentials", "version"]) and
|
||||
data["application"].getStr() == appInfo.application and
|
||||
data["appIdentifier"].getStr() == appInfo.appIdentifier and
|
||||
data["version"].getStr() == appInfo.version:
|
||||
# We return the first json keystore that matches the passed app parameters
|
||||
# We assume a unique kesytore with such parameters is present in the file
|
||||
matchingAppKeystore = data
|
||||
break
|
||||
# TODO: we might continue rather than return for some of these errors
|
||||
except JsonParsingError:
|
||||
return err(KeystoreJsonError)
|
||||
except ValueError:
|
||||
return err(KeystoreJsonError)
|
||||
except OSError:
|
||||
return err(KeystoreOsError)
|
||||
except Exception: #parseJson raises Exception
|
||||
return err(KeystoreOsError)
|
||||
|
||||
except IOError:
|
||||
return err(KeystoreIoError)
|
||||
|
||||
return ok(matchingAppKeystore)
|
||||
|
||||
|
||||
# Adds a sequence of membership credential to the keystore matching the application, appIdentifier and version filters.
|
||||
proc addMembershipCredentials*(path: string,
|
||||
credentials: seq[MembershipCredentials],
|
||||
password: string,
|
||||
appInfo: AppInfo,
|
||||
separator: string = "\n"): KeystoreResult[void] =
|
||||
|
||||
# We load the keystore corresponding to the desired parameters
|
||||
# This call ensures that JSON has all required fields
|
||||
let jsonKeystoreRes = loadAppKeystore(path, appInfo, separator)
|
||||
|
||||
if jsonKeystoreRes.isErr():
|
||||
return err(KeystoreLoadKeystoreError)
|
||||
|
||||
# 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()
|
||||
|
||||
found = true
|
||||
|
||||
# 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())
|
||||
|
||||
except:
|
||||
return err(KeystoreJsonError)
|
||||
|
||||
# We save to disk the (updated) keystore.
|
||||
if save(jsonKeystore, path, separator).isErr():
|
||||
return err(KeystoreOsError)
|
||||
|
||||
return ok()
|
||||
|
||||
# Returns the membership credentials in the keystore matching the application, appIdentifier and version filters, further filtered by the input
|
||||
# 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] = @[]
|
||||
|
||||
# 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)
|
||||
|
||||
# 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"]
|
||||
|
||||
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())
|
||||
|
||||
except:
|
||||
return err(KeystoreJsonError)
|
||||
|
||||
return ok(outputMembershipCredentials)
|
|
@ -0,0 +1,66 @@
|
|||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
stew/results
|
||||
|
||||
type
|
||||
IdentityTrapdoor* = seq[byte] #array[32, byte]
|
||||
IdentityNullifier* = seq[byte] #array[32, byte]
|
||||
# identity key as defined in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
IdentitySecretHash* = seq[byte] #array[32, byte]
|
||||
# hash of identity key as defined ed in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
IDCommitment* = seq[byte] #array[32, byte]
|
||||
|
||||
type IdentityCredential* = object
|
||||
idTrapdoor*: IdentityTrapdoor
|
||||
idNullifier*: IdentityNullifier
|
||||
## user's identity key (a secret key) which is selected randomly
|
||||
## see details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
idSecretHash*: IdentitySecretHash
|
||||
# hash of user's identity key generated by
|
||||
# Poseidon hash function implemented in rln lib
|
||||
# more details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
idCommitment*: IDCommitment
|
||||
|
||||
type MembershipIndex* = uint
|
||||
|
||||
type MembershipContract* = object
|
||||
chainId*: string
|
||||
address*: string
|
||||
|
||||
type MembershipGroup* = object
|
||||
membershipContract*: MembershipContract
|
||||
treeIndex*: MembershipIndex
|
||||
|
||||
type MembershipCredentials* = object
|
||||
identityCredential*: IdentityCredential
|
||||
membershipGroups*: seq[MembershipGroup]
|
||||
|
||||
type AppInfo* = object
|
||||
application*: string
|
||||
appIdentifier*: string
|
||||
version*: string
|
||||
|
||||
type AppKeystore* = object
|
||||
application*: string
|
||||
appIdentifier*: string
|
||||
credentials*: seq[MembershipCredentials]
|
||||
version*: string
|
||||
|
||||
type
|
||||
AppKeystoreError* = enum
|
||||
KeystoreOsError = "keystore error: OS specific error"
|
||||
KeystoreIoError = "keystore error: IO specific error"
|
||||
KeystoreJsonKeyError = "keystore error: fields not present in JSON"
|
||||
KeystoreJsonError = "keystore error: JSON encoder/decoder error"
|
||||
KeystoreKeystoreDoesNotExist = "keystore error: file does not exist"
|
||||
KeystoreCreateKeystoreError = "Error while creating application keystore"
|
||||
KeystoreLoadKeystoreError = "Error while loading application keystore"
|
||||
KeystoreCreateKeyfileError = "Error while creating keyfile for credentials"
|
||||
KeystoreSaveKeyfileError = "Error while saving keyfile for credentials"
|
||||
KeystoreReadKeyfileError = "Error while reading keyfile for credentials"
|
||||
|
||||
type KeystoreResult*[T] = Result[T, AppKeystoreError]
|
|
@ -0,0 +1,95 @@
|
|||
when (NimMajor, NimMinor) < (1, 4):
|
||||
{.push raises: [Defect].}
|
||||
else:
|
||||
{.push raises: [].}
|
||||
|
||||
import
|
||||
json,
|
||||
std/[options, os, sequtils],
|
||||
./keyfile,
|
||||
./protocol_types
|
||||
|
||||
# Checks if a JsonNode has all keys contained in "keys"
|
||||
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:
|
||||
return err(KeystoreOsError)
|
||||
|
||||
# We save the updated json
|
||||
var f: File
|
||||
if not f.open(path, fmAppend):
|
||||
return err(KeystoreOsError)
|
||||
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})
|
||||
f.write($json)
|
||||
# We store a keyfile per line
|
||||
f.write(separator)
|
||||
except CatchableError:
|
||||
# We got some KeystoreOsError writing to disk. We attempt to restore the previous keystore backup
|
||||
if fileExists(path & ".bkp"):
|
||||
try:
|
||||
f.close()
|
||||
removeFile(path)
|
||||
moveFile(path & ".bkp", path)
|
||||
except:
|
||||
# Unlucky, we just fail
|
||||
return err(KeystoreOsError)
|
||||
return err(KeystoreOsError)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
# The write went fine, so we can remove the backup keystore
|
||||
if fileExists(path & ".bkp"):
|
||||
try:
|
||||
removeFile(path & ".bkp")
|
||||
except:
|
||||
return err(KeystoreOsError)
|
||||
|
||||
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)
|
|
@ -1,6 +1,9 @@
|
|||
import
|
||||
stint
|
||||
|
||||
import
|
||||
../waku_keystore
|
||||
|
||||
# Acceptable roots for merkle root validation of incoming messages
|
||||
const AcceptableRootWindowSize* = 5
|
||||
|
||||
|
@ -48,3 +51,9 @@ const MaxClockGapSeconds* = 20.0 # the maximum clock difference between peers in
|
|||
|
||||
# maximum allowed gap between the epochs of messages' RateLimitProofs
|
||||
const MaxEpochGap* = uint64(MaxClockGapSeconds/EpochUnitSeconds)
|
||||
|
||||
# RLN Keystore defaults
|
||||
const
|
||||
RLNAppInfo* = AppInfo(application: "nwaku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.1")
|
||||
# NOTE: 256-bytes long credentials are due to the use of BN254 in RLN. Other implementations/curves might have a different byte size
|
||||
CredentialByteSize* = 256
|
|
@ -10,9 +10,10 @@ import
|
|||
stew/[arrayops, results, endians2],
|
||||
stint
|
||||
import
|
||||
./constants,
|
||||
./protocol_types
|
||||
import
|
||||
../../utils/keyfile
|
||||
../waku_keystore
|
||||
|
||||
export
|
||||
web3,
|
||||
|
@ -27,16 +28,9 @@ proc toUInt256*(idCommitment: IDCommitment): UInt256 =
|
|||
return pk
|
||||
|
||||
proc toIDCommitment*(idCommitmentUint: UInt256): IDCommitment =
|
||||
let pk = IDCommitment(idCommitmentUint.toBytesLE())
|
||||
let pk = IDCommitment(@(idCommitmentUint.toBytesLE()))
|
||||
return pk
|
||||
|
||||
proc inHex*(value: array[32, byte]): string =
|
||||
var valueHex = (UInt256.fromBytesLE(value)).toHex()
|
||||
# We pad leading zeroes
|
||||
while valueHex.len < value.len * 2:
|
||||
valueHex = "0" & valueHex
|
||||
return valueHex
|
||||
|
||||
proc toMembershipIndex*(v: UInt256): MembershipIndex =
|
||||
let membershipIndex: MembershipIndex = cast[MembershipIndex](v)
|
||||
return membershipIndex
|
||||
|
@ -115,10 +109,10 @@ proc toIdentityCredentials*(groupKeys: seq[(string, string, string, string)]): R
|
|||
for i in 0..groupKeys.len-1:
|
||||
try:
|
||||
let
|
||||
idTrapdoor = hexToUint[IdentityTrapdoor.len*8](groupKeys[i][0]).toBytesLE()
|
||||
idNullifier = hexToUint[IdentityNullifier.len*8](groupKeys[i][1]).toBytesLE()
|
||||
idSecretHash = hexToUint[IdentitySecretHash.len*8](groupKeys[i][2]).toBytesLE()
|
||||
idCommitment = hexToUint[IDCommitment.len*8](groupKeys[i][3]).toBytesLE()
|
||||
idTrapdoor = IdentityTrapdoor(@(hexToUint[CredentialByteSize](groupKeys[i][0]).toBytesLE()))
|
||||
idNullifier = IdentityNullifier(@(hexToUint[CredentialByteSize](groupKeys[i][1]).toBytesLE()))
|
||||
idSecretHash = IdentitySecretHash(@(hexToUint[CredentialByteSize](groupKeys[i][2]).toBytesLE()))
|
||||
idCommitment = IDCommitment(@(hexToUint[CredentialByteSize](groupKeys[i][3]).toBytesLE()))
|
||||
groupIdCredentials.add(IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash,
|
||||
idCommitment: idCommitment))
|
||||
except ValueError as err:
|
||||
|
@ -138,8 +132,8 @@ proc toIdentityCredentials*(groupKeys: seq[(string, string)]): RlnRelayResult[se
|
|||
for i in 0..groupKeys.len-1:
|
||||
try:
|
||||
let
|
||||
idSecretHash = hexToUint[IdentitySecretHash.len*8](groupKeys[i][0]).toBytesLE()
|
||||
idCommitment = hexToUint[IDCommitment.len*8](groupKeys[i][1]).toBytesLE()
|
||||
idSecretHash = IdentitySecretHash(@(hexToUint[CredentialByteSize](groupKeys[i][0]).toBytesLE()))
|
||||
idCommitment = IDCommitment(@(hexToUint[CredentialByteSize](groupKeys[i][1]).toBytesLE()))
|
||||
groupIdCredentials.add(IdentityCredential(idSecretHash: idSecretHash,
|
||||
idCommitment: idCommitment))
|
||||
except ValueError as err:
|
||||
|
|
|
@ -10,6 +10,7 @@ import
|
|||
web3,
|
||||
eth/keys
|
||||
import
|
||||
../waku_keystore,
|
||||
../../../common/protobuf
|
||||
|
||||
type RlnRelayResult*[T] = Result[T, string]
|
||||
|
@ -19,12 +20,6 @@ type RLN* {.incompleteStruct.} = object
|
|||
type RLNResult* = RlnRelayResult[ptr RLN]
|
||||
|
||||
type
|
||||
IdentityTrapdoor* = array[32, byte]
|
||||
IdentityNullifier* = array[32, byte]
|
||||
# identity key as defined in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
IdentitySecretHash* = array[32, byte]
|
||||
# hash of identity key as defined ed in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
IDCommitment* = array[32, byte]
|
||||
MerkleNode* = array[32, byte] # Each node of the Merkle tee is a Poseidon hash which is a 32 byte value
|
||||
Nullifier* = array[32, byte]
|
||||
Epoch* = array[32, byte]
|
||||
|
@ -32,17 +27,6 @@ type
|
|||
ZKSNARK* = array[128, byte]
|
||||
|
||||
# Custom data types defined for waku rln relay -------------------------
|
||||
type IdentityCredential* = object
|
||||
idTrapdoor*: IdentityTrapdoor
|
||||
idNullifier*: IdentityNullifier
|
||||
## user's identity key (a secret key) which is selected randomly
|
||||
## see details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
idSecretHash*: IdentitySecretHash
|
||||
# hash of user's identity key generated by
|
||||
# Poseidon hash function implemented in rln lib
|
||||
# more details in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Membership
|
||||
idCommitment*: IDCommitment
|
||||
|
||||
type RateLimitProof* = object
|
||||
## RateLimitProof holds the public inputs to rln circuit as
|
||||
## defined in https://hackmd.io/tMTLMYmTR5eynw2lwK9n1w?view#Public-Inputs
|
||||
|
@ -63,12 +47,6 @@ type RateLimitProof* = object
|
|||
## Application specific RLN Identifier
|
||||
rlnIdentifier*: RlnIdentifier
|
||||
|
||||
type MembershipIndex* = uint
|
||||
|
||||
type RlnMembershipCredentials* = object
|
||||
identityCredential*: IdentityCredential
|
||||
rlnIndex*: MembershipIndex
|
||||
|
||||
type ProofMetadata* = object
|
||||
nullifier*: Nullifier
|
||||
shareX*: MerkleNode
|
||||
|
|
|
@ -11,6 +11,7 @@ import
|
|||
../protocol_metrics,
|
||||
../constants
|
||||
import
|
||||
../../waku_keystore,
|
||||
../../../utils/time
|
||||
|
||||
logScope:
|
||||
|
@ -47,7 +48,7 @@ proc membershipKeyGen*(ctxPtr: ptr RLN): RlnRelayResult[IdentityCredential] =
|
|||
for (i, x) in idCommitment.mpairs: x = generatedKeys[i+3*32]
|
||||
|
||||
var
|
||||
identityCredential = IdentityCredential(idTrapdoor: idTrapdoor, idNullifier: idNullifier, idSecretHash: idSecretHash, idCommitment: idCommitment)
|
||||
identityCredential = IdentityCredential(idTrapdoor: @idTrapdoor, idNullifier: @idNullifier, idSecretHash: @idSecretHash, idCommitment: @idCommitment)
|
||||
|
||||
return ok(identityCredential)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ else:
|
|||
{.push raises: [].}
|
||||
|
||||
import
|
||||
std/[sequtils, tables, times, os, deques],
|
||||
std/[algorithm, sequtils, strutils, tables, times, os, deques],
|
||||
chronicles, options, chronos, stint,
|
||||
confutils,
|
||||
strutils,
|
||||
|
@ -23,7 +23,7 @@ import
|
|||
./protocol_metrics
|
||||
import
|
||||
../../utils/time,
|
||||
../../utils/keyfile,
|
||||
../waku_keystore,
|
||||
../waku_message,
|
||||
../waku_relay
|
||||
|
||||
|
@ -50,7 +50,7 @@ type
|
|||
MembershipTuple* = tuple[index: MembershipIndex, idComm: IDCommitment]
|
||||
|
||||
# membership contract interface
|
||||
contract(MembershipContract):
|
||||
contract(MembershipContractInterface):
|
||||
proc register(pubkey: Uint256) # external payable
|
||||
proc MemberRegistered(pubkey: Uint256, index: Uint256) {.event.}
|
||||
# TODO the followings are to be supported
|
||||
|
@ -58,22 +58,21 @@ contract(MembershipContract):
|
|||
# proc withdraw(secret: Uint256, pubkeyIndex: Uint256, receiver: Address)
|
||||
# proc withdrawBatch( secrets: seq[Uint256], pubkeyIndex: seq[Uint256], receiver: seq[Address])
|
||||
|
||||
proc inHex*(value:
|
||||
IdentityTrapdoor or
|
||||
proc inHex*(value: IdentityTrapdoor or
|
||||
IdentityNullifier or
|
||||
IdentitySecretHash or
|
||||
IDCommitment or
|
||||
MerkleNode or
|
||||
Nullifier or
|
||||
Epoch or
|
||||
RlnIdentifier
|
||||
): string =
|
||||
var valueHex = UInt256.fromBytesLE(value)
|
||||
valueHex = valueHex.toHex()
|
||||
RlnIdentifier): string =
|
||||
var valueHex = "" #UInt256.fromBytesLE(value)
|
||||
for b in value.reversed():
|
||||
valueHex = valueHex & b.toHex()
|
||||
# We pad leading zeroes
|
||||
while valueHex.len < value.len * 2:
|
||||
valueHex = "0" & valueHex
|
||||
return valueHex
|
||||
return toLowerAscii(valueHex)
|
||||
|
||||
proc register*(idComm: IDCommitment, ethAccountAddress: Option[Address], ethAccountPrivKey: keys.PrivateKey, ethClientAddress: string, membershipContractAddress: Address, registrationHandler: Option[RegistrationHandler] = none(RegistrationHandler)): Future[Result[MembershipIndex, string]] {.async.} =
|
||||
# TODO may need to also get eth Account Private Key as PrivateKey
|
||||
|
@ -95,7 +94,7 @@ proc register*(idComm: IDCommitment, ethAccountAddress: Option[Address], ethAcco
|
|||
# when the private key is set in a web3 instance, the send proc (sender.register(pk).send(MembershipFee))
|
||||
# does the signing using the provided key
|
||||
# web3.privateKey = some(ethAccountPrivateKey)
|
||||
var sender = web3.contractSender(MembershipContract, membershipContractAddress) # creates a Sender object with a web3 field and contract address of type Address
|
||||
var sender = web3.contractSender(MembershipContractInterface, membershipContractAddress) # creates a Sender object with a web3 field and contract address of type Address
|
||||
|
||||
debug "registering an id commitment", idComm=idComm.inHex()
|
||||
let pk = idComm.toUInt256()
|
||||
|
@ -563,7 +562,7 @@ proc getHistoricalEvents*(ethClientUri: string,
|
|||
## returns a table that maps block numbers to the list of members registered in that block
|
||||
## returns an error if it cannot retrieve the historical events
|
||||
let web3 = await newWeb3(ethClientUri)
|
||||
let contract = web3.contractSender(MembershipContract, contractAddress)
|
||||
let contract = web3.contractSender(MembershipContractInterface, contractAddress)
|
||||
# Get the historical events, and insert memberships into the tree
|
||||
let historicalEvents = await contract.getJsonLogs(MemberRegistered,
|
||||
fromBlock=some(fromBlock.blockId()),
|
||||
|
@ -592,11 +591,11 @@ proc subscribeToGroupEvents*(ethClientUri: string,
|
|||
blockNumber: string = "0x0",
|
||||
handler: GroupUpdateHandler) {.async, gcsafe.} =
|
||||
## connects to the eth client whose URI is supplied as `ethClientUri`
|
||||
## subscribes to the `MemberRegistered` event emitted from the `MembershipContract` which is available on the supplied `contractAddress`
|
||||
## subscribes to the `MemberRegistered` event emitted from the `MembershipContractInterface` which is available on the supplied `contractAddress`
|
||||
## it collects all the events starting from the given `blockNumber`
|
||||
## for every received block, it calls the `handler`
|
||||
let web3 = await newWeb3(ethClientUri)
|
||||
let contract = web3.contractSender(MembershipContract, contractAddress)
|
||||
let contract = web3.contractSender(MembershipContractInterface, contractAddress)
|
||||
|
||||
let blockTableRes = await getHistoricalEvents(ethClientUri,
|
||||
contractAddress,
|
||||
|
@ -730,7 +729,7 @@ proc mountRlnRelayStatic*(wakuRelay: WakuRelay,
|
|||
|
||||
debug "mounting rln-relay in off-chain/static mode"
|
||||
# check the peer's index and the inclusion of user's identity commitment in the group
|
||||
if not memIdCredential.idCommitment == group[int(memIndex)]:
|
||||
if memIdCredential.idCommitment != group[int(memIndex)]:
|
||||
return err("The peer's index is not consistent with the group")
|
||||
|
||||
# create an RLN instance
|
||||
|
@ -832,51 +831,6 @@ proc mountRlnRelayDynamic*(wakuRelay: WakuRelay,
|
|||
|
||||
return ok(rlnPeer)
|
||||
|
||||
proc writeRlnCredentials*(path: string,
|
||||
credentials: RlnMembershipCredentials,
|
||||
password: string): RlnRelayResult[void] =
|
||||
# Returns RlnRelayResult[void], which indicates the success of the call
|
||||
info "Storing RLN credentials"
|
||||
var jsonString: string
|
||||
jsonString.toUgly(%credentials)
|
||||
let keyfile = createKeyFileJson(toBytes(jsonString), password)
|
||||
if keyfile.isErr():
|
||||
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()
|
||||
|
||||
# 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]] =
|
||||
# Returns RlnRelayResult[Option[RlnMembershipCredentials]], which indicates the success of the call
|
||||
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:
|
||||
|
||||
try:
|
||||
var decodedKeyfiles = loadKeyFiles(path, password)
|
||||
|
||||
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=decodedKeyfiles.error
|
||||
return ok(none(RlnMembershipCredentials))
|
||||
except:
|
||||
return err("Error while loading keyfile for RLN credentials at " & path)
|
||||
|
||||
proc mount(wakuRelay: WakuRelay,
|
||||
conf: WakuRlnConfig,
|
||||
spamHandler: Option[SpamHandler] = none(SpamHandler),
|
||||
|
@ -943,10 +897,14 @@ proc mount(wakuRelay: WakuRelay,
|
|||
return err("invalid eth contract address: " & err.msg)
|
||||
var ethAccountPrivKeyOpt = none(keys.PrivateKey)
|
||||
var ethAccountAddressOpt = none(Address)
|
||||
var credentials = none(RlnMembershipCredentials)
|
||||
var credentials = none(MembershipCredentials)
|
||||
var rlnRelayRes: RlnRelayResult[WakuRlnRelay]
|
||||
var rlnRelayCredPath: string
|
||||
var persistCredentials: bool = false
|
||||
# The RLN membership contract
|
||||
let rlnMembershipContract = MembershipContract(chainId: "5", # This is Goerli ChainID. TODO: pass chainId to web3 as config option
|
||||
address: conf.rlnRelayEthContractAddress)
|
||||
|
||||
|
||||
if conf.rlnRelayEthAccountPrivateKey != "":
|
||||
ethAccountPrivKeyOpt = some(keys.PrivateKey(SkSecretKey.fromHex(conf.rlnRelayEthAccountPrivateKey).value))
|
||||
|
@ -973,12 +931,23 @@ proc mount(wakuRelay: WakuRelay,
|
|||
info "A RLN credential file exists in provided path", path=rlnRelayCredPath
|
||||
|
||||
# retrieve rln-relay credential
|
||||
let readCredentialsRes = readRlnCredentials(rlnRelayCredPath, conf.rlnRelayCredentialsPassword)
|
||||
waku_rln_membership_credentials_import_duration_seconds.nanosecondTime:
|
||||
let readCredentialsRes = getMembershipCredentials(path = rlnRelayCredPath,
|
||||
password = conf.rlnRelayCredentialsPassword,
|
||||
filterMembershipContracts = @[rlnMembershipContract],
|
||||
# TODO: the following can be embedded in conf
|
||||
appInfo = RLNAppInfo)
|
||||
|
||||
if readCredentialsRes.isErr():
|
||||
return err("RLN credentials cannot be read: " & readCredentialsRes.error())
|
||||
if readCredentialsRes.isErr():
|
||||
return err("RLN credentials cannot be read")
|
||||
|
||||
credentials = readCredentialsRes.get()
|
||||
# getMembershipCredentials returns all credentials in keystore as sequence matching the filter
|
||||
let allMatchingCredentials = readCredentialsRes.get()
|
||||
# if any is found, we return the first credential, otherwise credentials is none
|
||||
if allMatchingCredentials.len() > 0:
|
||||
credentials = some(allMatchingCredentials[0])
|
||||
else:
|
||||
credentials = none(MembershipCredentials)
|
||||
|
||||
else: # there is no credential file available in the supplied path
|
||||
# mount the rln-relay protocol leaving rln-relay credentials arguments unassigned
|
||||
|
@ -997,7 +966,7 @@ proc mount(wakuRelay: WakuRelay,
|
|||
spamHandler = spamHandler,
|
||||
registrationHandler = registrationHandler,
|
||||
memIdCredential = some(credentials.get().identityCredential),
|
||||
memIndex = some(credentials.get().rlnIndex))
|
||||
memIndex = some(credentials.get().membershipGroups[0].treeIndex)) # TODO: use a proper proc to get a certain membership index
|
||||
else:
|
||||
# mount rln-relay in on-chain mode, with the provided private key
|
||||
rlnRelayRes = await mountRlnRelayDynamic(wakuRelay,
|
||||
|
@ -1030,10 +999,16 @@ proc mount(wakuRelay: WakuRelay,
|
|||
return err("dynamic rln-relay could not be mounted: " & rlnRelayRes.error())
|
||||
let wakuRlnRelay = rlnRelayRes.get()
|
||||
if persistCredentials:
|
||||
# persist rln credential
|
||||
credentials = some(RlnMembershipCredentials(rlnIndex: wakuRlnRelay.membershipIndex,
|
||||
identityCredential: wakuRlnRelay.identityCredential))
|
||||
if writeRlnCredentials(rlnRelayCredPath, credentials.get(), conf.rlnRelayCredentialsPassword).isErr():
|
||||
|
||||
credentials = some(MembershipCredentials(identityCredential: wakuRlnRelay.identityCredential,
|
||||
membershipGroups: @[MembershipGroup(membershipContract: rlnMembershipContract, treeIndex: wakuRlnRelay.membershipIndex)]
|
||||
))
|
||||
|
||||
if addMembershipCredentials(path = rlnRelayCredPath,
|
||||
credentials = @[credentials.get()],
|
||||
password = conf.rlnRelayCredentialsPassword,
|
||||
# TODO: the following can be embedded in conf
|
||||
appInfo = RLNAppInfo).isErr():
|
||||
return err("error in storing rln credentials")
|
||||
return ok(wakuRlnRelay)
|
||||
|
||||
|
|
Loading…
Reference in New Issue