deploy: ea8d72188e9ee74430fc1f311a7905c52cce4d02

This commit is contained in:
s1fr0 2022-10-28 10:22:12 +00:00
parent e90639330d
commit 97ddd4d70e
8 changed files with 1032 additions and 24 deletions

View File

@ -290,6 +290,11 @@ type
desc: "Address of membership contract on an Ethereum testnet", desc: "Address of membership contract on an Ethereum testnet",
defaultValue: "" defaultValue: ""
name: "rln-relay-eth-contract-address" }: string name: "rln-relay-eth-contract-address" }: string
rlnRelayCredentialsPassword* {.
desc: "Password for encrypting RLN credentials",
defaultValue: ""
name: "rln-relay-cred-password" }: string
# NOTE: Keys are different in nim-libp2p # NOTE: Keys are different in nim-libp2p
proc parseCmdArg*(T: type crypto.PrivateKey, p: TaintedString): T = proc parseCmdArg*(T: type crypto.PrivateKey, p: TaintedString): T =

View File

@ -161,7 +161,12 @@ type
desc: "Address of membership contract on an Ethereum testnet", desc: "Address of membership contract on an Ethereum testnet",
defaultValue: "" defaultValue: ""
name: "rln-relay-eth-contract-address" }: string name: "rln-relay-eth-contract-address" }: string
rlnRelayCredentialsPassword* {.
desc: "Password for encrypting RLN credentials",
defaultValue: ""
name: "rln-relay-cred-password" }: string
staticnodes* {. staticnodes* {.
desc: "Peer multiaddr to directly connect with. Argument may be repeated." desc: "Peer multiaddr to directly connect with. Argument may be repeated."
name: "staticnode" }: seq[string] name: "staticnode" }: seq[string]

View File

@ -45,7 +45,9 @@ import
./v2/test_enr_utils, ./v2/test_enr_utils,
./v2/test_peer_exchange, ./v2/test_peer_exchange,
./v2/test_waku_noise, ./v2/test_waku_noise,
./v2/test_waku_noise_sessions ./v2/test_waku_noise_sessions,
# Utils
./v2/test_utils_keyfile
when defined(rln) or defined(rlnzerokit): when defined(rln) or defined(rlnzerokit):
import import

View File

@ -0,0 +1,377 @@
{.used.}
import
std/[json, os],
stew/byteutils,
testutils/unittests, chronos,
eth/keys
import
../../waku/v2/utils/keyfile
from ../../waku/v2/protocol/waku_noise/noise_utils import randomSeqByte
suite "KeyFile test suite":
let rng = newRng()
test "Create/Save/Load single keyfile":
# The password we use to encrypt our secret
let password = "randompassword"
# The filepath were the keyfile will be stored
let filepath = "./test.keyfile"
defer: removeFile(filepath)
# The secret
var secret = randomSeqByte(rng[], 300)
# We create a keyfile encrypting the secret with password
let keyfile = createKeyFileJson(secret, password)
check:
keyfile.isOk()
# We save to disk the keyfile
saveKeyFile(filepath, keyfile.get()).isOk()
# We load from the file all the decrypted keyfiles encrypted under password
var decodedKeyfiles = loadKeyFiles(filepath, password)
check:
decodedKeyfiles.isOk()
# Since only one secret was stored in file, we expect only one keyfile being decrypted
decodedKeyfiles.get().len == 1
# We check if the decrypted secret is the same as the original secret
let decodedSecret = decodedKeyfiles.get()[0]
check:
secret == decodedSecret.get()
test "Create/Save/Load multiple keyfiles in same file":
# We set different passwords for different keyfiles that will be stored in same file
let password1 = string.fromBytes(randomSeqByte(rng[], 20))
let password2 = ""
let password3 = string.fromBytes(randomSeqByte(rng[], 20))
var keyfile: KfResult[JsonNode]
let filepath = "./test.keyfile"
defer: removeFile(filepath)
# We generate 6 different secrets and we encrypt them using 3 different passwords, and we store the obtained keystore
let secret1 = randomSeqByte(rng[], 300)
keyfile = createKeyFileJson(secret1, password1)
check:
keyfile.isOk()
saveKeyFile(filepath, keyfile.get()).isOk()
let secret2 = randomSeqByte(rng[], 300)
keyfile = createKeyFileJson(secret2, password2)
check:
keyfile.isOk()
saveKeyFile(filepath, keyfile.get()).isOk()
let secret3 = randomSeqByte(rng[], 300)
keyfile = createKeyFileJson(secret3, password3)
check:
keyfile.isOk()
saveKeyFile(filepath, keyfile.get()).isOk()
# We encrypt secret4 with password3
let secret4 = randomSeqByte(rng[], 300)
keyfile = createKeyFileJson(secret4, password3)
check:
keyfile.isOk()
saveKeyFile(filepath, keyfile.get()).isOk()
# We encrypt secret5 with password1
let secret5 = randomSeqByte(rng[], 300)
keyfile = createKeyFileJson(secret5, password1)
check:
keyfile.isOk()
saveKeyFile(filepath, keyfile.get()).isOk()
# We encrypt secret6 with password1
let secret6 = randomSeqByte(rng[], 300)
keyfile = createKeyFileJson(secret6, password1)
check:
keyfile.isOk()
saveKeyFile(filepath, keyfile.get()).isOk()
# Now there are 6 keyfiles stored in filepath encrypted with 3 different passwords
# We decrypt the keyfiles using the respective passwords and we check that the number of
# successful decryptions corresponds to the number of secrets encrypted under that password
var decodedKeyfilesPassword1 = loadKeyFiles(filepath, password1)
check:
decodedKeyfilesPassword1.isOk()
decodedKeyfilesPassword1.get().len == 3
var decodedSecretsPassword1 = decodedKeyfilesPassword1.get()
var decodedKeyfilesPassword2 = loadKeyFiles(filepath, password2)
check:
decodedKeyfilesPassword2.isOk()
decodedKeyfilesPassword2.get().len == 1
var decodedSecretsPassword2 = decodedKeyfilesPassword2.get()
var decodedKeyfilesPassword3 = loadKeyFiles(filepath, password3)
check:
decodedKeyfilesPassword3.isOk()
decodedKeyfilesPassword3.get().len == 2
var decodedSecretsPassword3 = decodedKeyfilesPassword3.get()
# We check if the corresponding secrets are correct
check:
# Secrets encrypted with password 1
secret1 == decodedSecretsPassword1[0].get()
secret5 == decodedSecretsPassword1[1].get()
secret6 == decodedSecretsPassword1[2].get()
# Secrets encrypted with password 2
secret2 == decodedSecretsPassword2[0].get()
# Secrets encrypted with password 3
secret3 == decodedSecretsPassword3[0].get()
secret4 == decodedSecretsPassword3[1].get()
# The following tests are originally from the nim-eth keyfile tests module https://github.com/status-im/nim-eth/blob/master/tests/keyfile/test_keyfile.nim
# and are slightly adapted to test backwards compatibility with nim-eth implementation of our customized version of the utils/keyfile module
# Note: the original nim-eth "Create/Save/Load test" is redefined and expanded above in "KeyFile test suite"
suite "KeyFile test suite (adapted from nim-eth keyfile tests)":
let rng = newRng()
# Testvectors originally from https://github.com/status-im/nim-eth/blob/fef47331c37ee8abb8608037222658737ff498a6/tests/keyfile/test_keyfile.nim#L22-L168
let TestVectors = [
%*{
"keyfile": {
"crypto" : {
"cipher" : "aes-128-ctr",
"cipherparams" : {"iv" : "6087dab2f9fdbbfaddc31a909735c1e6"},
"ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46",
"kdf" : "pbkdf2",
"kdfparams" : {
"c" : 262144,
"dklen" : 32,
"prf" : "hmac-sha256",
"salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"
},
"mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2"
},
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6",
"version" : 3
},
"name": "test1",
"password": "testpassword",
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"
},
%*{
"keyfile": {
"version": 3,
"crypto": {
"ciphertext": "ee75456c006b1e468133c5d2a916bacd3cf515ced4d9b021b5c59978007d1e87",
"version": 1,
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 262144,
"prf": "hmac-sha256",
"salt": "504490577620f64f43d73f29479c2cf0"
},
"mac": "196815708465de9af7504144a1360d08874fc3c30bb0e648ce88fbc36830d35d",
"cipherparams": {"iv": "514ccc8c4fb3e60e5538e0cf1e27c233"},
"cipher": "aes-128-ctr"
},
"id": "98d193c7-5174-4c7c-5345-c1daf95477b5"
},
"name": "python_generated_test_with_odd_iv",
"password": "foo",
"priv": "0101010101010101010101010101010101010101010101010101010101010101"
},
%*{
"keyfile": {
"version": 3,
"crypto": {
"ciphertext": "d69313b6470ac1942f75d72ebf8818a0d484ac78478a132ee081cd954d6bd7a9",
"cipherparams": {"iv": "ffffffffffffffffffffffffffffffff"},
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 262144,
"prf": "hmac-sha256",
"salt": "c82ef14476014cbf438081a42709e2ed"
},
"mac": "cf6bfbcc77142a22c4a908784b4a16f1023a1d0e2aff404c20158fa4f1587177",
"cipher": "aes-128-ctr",
"version": 1
},
"id": "abb67040-8dbe-0dad-fc39-2b082ef0ee5f"
},
"name": "evilnonce",
"password": "bar",
"priv": "0202020202020202020202020202020202020202020202020202020202020202"
},
%*{
"keyfile": {
"version" : 3,
"crypto" : {
"cipher" : "aes-128-ctr",
"cipherparams" : {
"iv" : "83dbcc02d8ccb40e466191a123791e0e"
},
"ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c",
"kdf" : "scrypt",
"kdfparams" : {
"dklen" : 32,
"n" : 262144,
"r" : 1,
"p" : 8,
"salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19"
},
"mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097"
},
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6"
},
"name" : "test2",
"password": "testpassword",
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"
},
%*{
"keyfile": {
"version": 3,
"address": "460121576cc7df020759730751f92bd62fd78dd6",
"crypto": {
"ciphertext": "54ae683c6287fa3d58321f09d56e26d94e58a00d4f90bdd95782ae0e4aab618b",
"cipherparams": {
"iv": "681679cdb125bba9495d068b002816a4"
},
"cipher": "aes-128-ctr",
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"salt": "c3407f363fce02a66e3c4bf4a8f6b7da1c1f54266cef66381f0625c251c32785",
"n": 8192,
"r": 8,
"p": 1
},
"mac": "dea6bdf22a2f522166ed82808c22a6311e84c355f4bbe100d4260483ff675a46"
},
"id": "0eb785e0-340a-4290-9c42-90a11973ee47"
},
"name": "mycrypto",
"password": "foobartest121",
"priv": "05a4d3eb46c742cb8850440145ce70cbc80b59f891cf5f50fd3e9c280b50c4e4"
},
%*{
"keyfile": {
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "7e7b02d2b4ef45d6c98cb885e75f48d5",
},
"ciphertext": "a7a5743a6c7eb3fa52396bd3fd94043b79075aac3ccbae8e62d3af94db00397c",
"kdf": "scrypt",
"kdfparams": {
"dklen": 32,
"n": 8192,
"p": 1,
"r": 8,
"salt": "247797c7a357b707a3bdbfaa55f4c553756bca09fec20ddc938e7636d21e4a20",
},
"mac": "5a3ba5bebfda2c384586eda5fcda9c8397d37c9b0cc347fea86525cf2ea3a468",
},
"address": "0b6f2de3dee015a95d3330dcb7baf8e08aa0112d",
"id": "3c8efdd6-d538-47ec-b241-36783d3418b9",
"version": 3
},
"password": "moomoocow",
"priv": "21eac69b9a52f466bfe9047f0f21c9caf3a5cdaadf84e2750a9b3265d450d481",
"name": "eth-keyfile-conftest"
}
]
test "Testing nim-eth test vectors":
var secret: KfResult[seq[byte]]
var expectedSecret: seq[byte]
for i in 0..<TestVectors.len:
# Decryption with correct password
expectedSecret = decodeHex(TestVectors[i].getOrDefault("priv").getStr())
secret =
decodeKeyFileJson(TestVectors[i].getOrDefault("keyfile"),
TestVectors[i].getOrDefault("password").getStr())
check:
secret.isOk()
secret.get() == expectedSecret
# Decryption with wrong password
secret = decodeKeyFileJson(TestVectors[i].getOrDefault("keyfile"), "wrongpassword")
check:
secret.isErr()
secret.error == KeyFileError.IncorrectMac
test "Wrong mac in keyfile":
# This keyfile is the same as the first one in TestVectors,
# but the last byte of mac is changed to 00.
# While ciphertext is the correct encryption of priv under password,
# mac verfication should fail and nothing will be decrypted
let keyfileWrongMac = %*{
"keyfile": {
"crypto" : {
"cipher" : "aes-128-ctr",
"cipherparams" : {"iv" : "6087dab2f9fdbbfaddc31a909735c1e6"},
"ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46",
"kdf" : "pbkdf2",
"kdfparams" : {
"c" : 262144,
"dklen" : 32,
"prf" : "hmac-sha256",
"salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd"
},
"mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e900"
},
"id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6",
"version" : 3
},
"name": "test1",
"password": "testpassword",
"priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d"
}
# Decryption with correct password
let expectedSecret = decodeHex(keyfileWrongMac.getOrDefault("priv").getStr())
let secret =
decodeKeyFileJson(keyfileWrongMac.getOrDefault("keyfile"),
keyfileWrongMac.getOrDefault("password").getStr())
check:
secret.isErr()
secret.error == KeyFileError.IncorrectMac
test "Scrypt keyfiles":
let
expectedSecret = randomSeqByte(rng[], 300)
password = "miawmiawcat"
# By default, keyfiles' encryption key is derived from password using PBKDF2.
# Here we test keyfiles encypted with a key derived from password using scrypt
jsonKeyfile = createKeyFileJson(expectedSecret, password, 3, AES128CTR, SCRYPT)
check:
jsonKeyfile.isOk()
let secret = decodeKeyFileJson(jsonKeyfile.get(), password)
check:
secret.isOk()
secret.get() == expectedSecret
test "Load non-existent keyfile test":
check:
loadKeyFiles("nonexistant.keyfile", "password").error ==
KeyFileError.KeyfileDoesNotExist

View File

@ -2,7 +2,7 @@
{.used.} {.used.}
import import
std/options, sequtils, times, deques, std/[options, os, sequtils, times, deques],
testutils/unittests, chronos, chronicles, stint, testutils/unittests, chronos, chronicles, stint,
stew/byteutils, stew/shims/net as stewNet, stew/byteutils, stew/shims/net as stewNet,
libp2p/crypto/crypto, libp2p/crypto/crypto,
@ -980,7 +980,7 @@ suite "Waku rln relay":
check: check:
keypair.get().idCommitment == idCommitment keypair.get().idCommitment == idCommitment
test "Read Persistent RLN credentials": test "Read/Write RLN credentials":
# create an RLN instance # create an RLN instance
var rlnInstance = createRLNInstance() var rlnInstance = createRLNInstance()
check: check:
@ -1003,17 +1003,26 @@ suite "Waku rln relay":
var rlnMembershipCredentials = RlnMembershipCredentials(membershipKeyPair: k, rlnIndex: index) var rlnMembershipCredentials = RlnMembershipCredentials(membershipKeyPair: k, rlnIndex: index)
let path = "testPath.txt" let password = "%m0um0ucoW%"
let filepath = "./testRLNCredentials.txt"
defer: removeFile(filepath)
# Write RLN credentials # Write RLN credentials
writeFile(path, pretty(%rlnMembershipCredentials)) check:
writeRlnCredentials(filepath, rlnMembershipCredentials, password).isOk()
var credentials = readPersistentRlnCredentials(path) let readCredentialsResult = readRlnCredentials(filepath, password)
check:
readCredentialsResult.isOk()
let credentials = readCredentialsResult.get()
check: check:
credentials.membershipKeyPair == k credentials.isSome()
credentials.rlnIndex == index credentials.get().membershipKeyPair == k
credentials.get().rlnIndex == index
test "histogram static bucket generation": test "histogram static bucket generation":
let buckets = generateBucketsForHistogram(10) let buckets = generateBucketsForHistogram(10)

View File

@ -2,7 +2,7 @@
# libtool - Provide generalized library-building support services. # libtool - Provide generalized library-building support services.
# Generated automatically by config.status (libbacktrace) version-unused # Generated automatically by config.status (libbacktrace) version-unused
# Libtool was configured on host fv-az245-751: # Libtool was configured on host fv-az222-281:
# NOTE: Changes made to this file will be lost: look at ltmain.sh. # NOTE: Changes made to this file will be lost: look at ltmain.sh.
# #
# Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2003, 2004, 2005, # Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2003, 2004, 2005,

View File

@ -16,6 +16,7 @@ import
waku_rln_relay_types, waku_rln_relay_types,
waku_rln_relay_metrics, waku_rln_relay_metrics,
../../utils/time, ../../utils/time,
../../utils/keyfile,
../../node/waku_node, ../../node/waku_node,
../../../../../apps/wakunode2/config, ## TODO: Decouple the protocol code from the app configuration ../../../../../apps/wakunode2/config, ## TODO: Decouple the protocol code from the app configuration
../../../../../apps/chat2/config_chat2, ## TODO: Decouple the protocol code from the app configuration ../../../../../apps/chat2/config_chat2, ## TODO: Decouple the protocol code from the app configuration
@ -1145,19 +1146,45 @@ proc mountRlnRelayDynamic*(node: WakuNode,
node.wakuRlnRelay = rlnPeer node.wakuRlnRelay = rlnPeer
return ok(true) return ok(true)
proc readPersistentRlnCredentials*(path: string) : RlnMembershipCredentials {.raises: [Defect, OSError, IOError, Exception].} = proc writeRlnCredentials*(path: string, credentials: RlnMembershipCredentials, password: string): RlnRelayResult[void] =
info "Rln credentials exist in file" 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]] =
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. # 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. # 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. # 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: waku_rln_membership_credentials_import_duration_seconds.nanosecondTime:
let entireRlnCredentialsFile = readFile(path)
let jsonObject = parseJson(entireRlnCredentialsFile) try:
let deserializedRlnCredentials = to(jsonObject, RlnMembershipCredentials) var decodedKeyfiles = loadKeyFiles(path, password)
debug "Deserialized Rln credentials", rlnCredentials=deserializedRlnCredentials if decodedKeyfiles.isOk():
return deserializedRlnCredentials 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(node: WakuNode, proc mount(node: WakuNode,
conf: WakuNodeConf|Chat2Conf, conf: WakuNodeConf|Chat2Conf,
@ -1217,13 +1244,23 @@ proc mount(node: WakuNode,
# if the path does not contain any credential file, then a new set is generated and pesisted in the same path # if the path does not contain any credential file, then a new set is generated and pesisted in the same path
# if there is a credential file, then no new credentials are generated, instead the content of the file is read and used to mount rln-relay # if there is a credential file, then no new credentials are generated, instead the content of the file is read and used to mount rln-relay
if conf.rlnRelayCredPath != "": if conf.rlnRelayCredPath != "":
let rlnRelayCredPath = joinPath(conf.rlnRelayCredPath, RlnCredentialsFilename) let rlnRelayCredPath = joinPath(conf.rlnRelayCredPath, RlnCredentialsFilename)
debug "rln-relay credential path", rlnRelayCredPath debug "rln-relay credential path", rlnRelayCredPath
# check if there is an rln-relay credential file in the supplied path # check if there is an rln-relay credential file in the supplied path
if fileExists(rlnRelayCredPath): if fileExists(rlnRelayCredPath):
info "A RLN credential file exists in provided path", path=rlnRelayCredPath
# retrieve rln-relay credential # retrieve rln-relay credential
credentials = some(readPersistentRlnCredentials(rlnRelayCredPath)) let readCredentialsRes = readRlnCredentials(rlnRelayCredPath, conf.rlnRelayCredentialsPassword)
if readCredentialsRes.isErr():
return err("RLN credentials cannot be read: " & readCredentialsRes.error())
credentials = readCredentialsRes.get()
else: # there is no credential file available in the supplied path else: # there is no credential file available in the supplied path
# mount the rln-relay protocol leaving rln-relay credentials arguments unassigned # mount the rln-relay protocol leaving rln-relay credentials arguments unassigned
# this infroms mountRlnRelayDynamic proc that new credentials should be generated and registered to the membership contract # this infroms mountRlnRelayDynamic proc that new credentials should be generated and registered to the membership contract
@ -1256,8 +1293,8 @@ proc mount(node: WakuNode,
# persist rln credential # persist rln credential
credentials = some(RlnMembershipCredentials(rlnIndex: node.wakuRlnRelay.membershipIndex, credentials = some(RlnMembershipCredentials(rlnIndex: node.wakuRlnRelay.membershipIndex,
membershipKeyPair: node.wakuRlnRelay.membershipKeyPair)) membershipKeyPair: node.wakuRlnRelay.membershipKeyPair))
writeFile(rlnRelayCredPath, pretty(%credentials.get())) if writeRlnCredentials(rlnRelayCredPath, credentials.get(), conf.rlnRelayCredentialsPassword).isErr():
return err("error in storing rln credentials")
else: else:
# do not persist or use a persisted rln-relay credential # do not persist or use a persisted rln-relay credential

573
waku/v2/utils/keyfile.nim Normal file
View File

@ -0,0 +1,573 @@
# This implementation is originally taken from nim-eth keyfile module https://github.com/status-im/nim-eth/blob/master/eth/keyfile and adapted to
# - create keyfiles for arbitrary-long input byte data (rather than fixed-size private keys)
# - allow storage of multiple keyfiles (encrypted with different passwords) in same file and iteration among successful decryptions
# - enable/disable at compilation time the keyfile id and version fields
{.push raises: [Defect].}
import
std/[os, strutils, json, sequtils],
nimcrypto/[bcmode, hmac, rijndael, pbkdf2, sha2, sysrand, utils, keccak, scrypt],
stew/results,
eth/keys,
eth/keyfile/uuid
export results
const
# Version 3 constants
SaltSize = 16
DKLen = 32
MaxDKLen = 128
ScryptR = 1
ScryptP = 8
Pbkdf2WorkFactor = 1_000_000
ScryptWorkFactor = 262_144
type
KeyFileError* = enum
RandomError = "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"
KeyfileDoesNotExist = "keyfile error: file does not exist"
KdfKind* = enum
PBKDF2, ## PBKDF2
SCRYPT ## SCRYPT
HashKind* = enum
HashNoSupport, HashSHA2_224, HashSHA2_256, HashSHA2_384, HashSHA2_512,
HashKECCAK224, HashKECCAK256, HashKECCAK384, HashKECCAK512,
HashSHA3_224, HashSHA3_256, HashSHA3_384, HashSHA3_512
CryptKind* = enum
CipherNoSupport, ## Cipher not supported
AES128CTR ## AES-128-CTR
CipherParams = object
iv: seq[byte]
Cipher = object
kind: CryptKind
params: CipherParams
text: seq[byte]
Crypto = object
kind: KdfKind
cipher: Cipher
kdfParams: JsonNode
mac: seq[byte]
ScryptParams* = object
dklen: int
n, p, r: int
salt: string
Pbkdf2Params* = object
dklen: int
c: int
prf: HashKind
salt: string
DKey = array[DKLen, byte]
KfResult*[T] = Result[T, KeyFileError]
const
SupportedHashes = [
"sha224", "sha256", "sha384", "sha512",
"keccak224", "keccak256", "keccak384", "keccak512",
"sha3_224", "sha3_256", "sha3_384", "sha3_512"
]
SupportedHashesKinds = [
HashSHA2_224, HashSHA2_256, HashSHA2_384, HashSHA2_512,
HashKECCAK224, HashKECCAK256, HashKECCAK384, HashKECCAK512,
HashSHA3_224, HashSHA3_256, HashSHA3_384, HashSHA3_512
]
# When true, the keyfile json will contain "version" and "id" fields, respectively. Default to false.
VersionInKeyfile: bool = false
IdInKeyfile: bool = false
proc mapErrTo[T, E](r: Result[T, E], v: static KeyFileError): KfResult[T] =
r.mapErr(proc (e: E): KeyFileError = v)
proc `$`(k: KdfKind): string =
case k
of SCRYPT:
return "scrypt"
else:
return "pbkdf2"
proc `$`(k: CryptKind): string =
case k
of AES128CTR:
return "aes-128-ctr"
else:
return "aes-128-ctr"
# Parses the prf name to HashKind
proc getPrfHash(prf: string): HashKind =
let p = prf.toLowerAscii()
if p.startsWith("hmac-"):
var hash = p[5..^1]
var res = SupportedHashes.find(hash)
if res >= 0:
return SupportedHashesKinds[res]
return HashNoSupport
# Parses the cipher name to CryptoKind
proc getCipher(c: string): CryptKind =
var cl = c.toLowerAscii()
if cl == "aes-128-ctr":
return AES128CTR
else:
return CipherNoSupport
# Key derivation routine for PBKDF2
proc deriveKey(password: string,
salt: string,
kdfkind: KdfKind,
hashkind: HashKind,
workfactor: int): KfResult[DKey] =
if kdfkind == PBKDF2:
var output: DKey
var c = if workfactor == 0: Pbkdf2WorkFactor else: workfactor
case hashkind
of HashSHA2_224:
var ctx: HMAC[sha224]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA2_256:
var ctx: HMAC[sha256]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA2_384:
var ctx: HMAC[sha384]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA2_512:
var ctx: HMAC[sha512]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashKECCAK224:
var ctx: HMAC[keccak224]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashKECCAK256:
var ctx: HMAC[keccak256]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashKECCAK384:
var ctx: HMAC[keccak384]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashKECCAK512:
var ctx: HMAC[keccak512]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA3_224:
var ctx: HMAC[sha3_224]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA3_256:
var ctx: HMAC[sha3_256]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA3_384:
var ctx: HMAC[sha3_384]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
of HashSHA3_512:
var ctx: HMAC[sha3_512]
discard ctx.pbkdf2(password, salt, c, output)
ctx.clear()
ok(output)
else:
err(PrfNotSupported)
else:
err(NotImplemented)
# Scrypt wrapper
func scrypt[T, M](password: openArray[T], salt: openArray[M],
N, r, p: int, output: var openArray[byte]): int =
let (xyvLen, bLen) = scryptCalc(N, r, p)
var xyv = newSeq[uint32](xyvLen)
var b = newSeq[byte](bLen)
scrypt(password, salt, N, r, p, xyv, b, output)
# Key derivation routine for Scrypt
proc deriveKey(password: string, salt: string,
workFactor, r, p: int): KfResult[DKey] =
let wf = if workFactor == 0: ScryptWorkFactor else: workFactor
var output: DKey
if scrypt(password, salt, wf, r, p, output) == 0:
return err(ScryptBadParam)
return ok(output)
# Encryption routine
proc encryptData(plaintext: openArray[byte],
cryptkind: CryptKind,
key: openArray[byte],
iv: openArray[byte]): KfResult[seq[byte]] =
if cryptkind == AES128CTR:
var ciphertext = newSeqWith(plaintext.len, 0.byte)
var ctx: CTR[aes128]
ctx.init(toOpenArray(key, 0, 15), iv)
ctx.encrypt(plaintext, ciphertext)
ctx.clear()
ok(ciphertext)
else:
err(NotImplemented)
# Decryption routine
proc decryptData(ciphertext: openArray[byte],
cryptkind: CryptKind,
key: openArray[byte],
iv: openArray[byte]): KfResult[seq[byte]] =
if cryptkind == AES128CTR:
if len(iv) != aes128.sizeBlock:
return err(IncorrectIV)
var plaintext = newSeqWith(ciphertext.len, 0.byte)
var ctx: CTR[aes128]
ctx.init(toOpenArray(key, 0, 15), iv)
ctx.decrypt(ciphertext, plaintext)
ctx.clear()
ok(plaintext)
else:
err(NotImplemented)
# Encodes KDF parameters in JSON
proc kdfParams(kdfkind: KdfKind, salt: string, workfactor: int): KfResult[JsonNode] =
if kdfkind == SCRYPT:
let wf = if workfactor == 0: ScryptWorkFactor else: workfactor
ok(%*
{
"dklen": DKLen,
"n": wf,
"r": ScryptR,
"p": ScryptP,
"salt": salt
}
)
elif kdfkind == PBKDF2:
let wf = if workfactor == 0: Pbkdf2WorkFactor else: workfactor
ok(%*
{
"dklen": DKLen,
"c": wf,
"prf": "hmac-sha256",
"salt": salt
}
)
else:
err(NotImplemented)
# Decodes hex strings to byte sequences
proc decodeHex*(m: string): seq[byte] =
if len(m) > 0:
try:
return utils.fromHex(m)
except CatchableError:
return newSeq[byte]()
else:
return newSeq[byte]()
# Parses the salt from hex string to byte string
proc decodeSalt(m: string): string =
var sarr: seq[byte]
if len(m) > 0:
try:
sarr = utils.fromHex(m)
var output = newString(len(sarr))
copyMem(addr output[0], addr sarr[0], len(sarr))
return output
except CatchableError:
return ""
else:
return ""
# Compares the message authentication code
proc compareMac(m1: openArray[byte], m2: openArray[byte]): bool =
if len(m1) == len(m2) and len(m1) > 0:
return equalMem(unsafeAddr m1[0], unsafeAddr m2[0], len(m1))
else:
return false
# Creates a keyfile for secret encrypted with password according to the other parameters
# Returns keyfile in JSON according to Web3 Secure storage format (here, differently than standard, version and id are optional)
proc createKeyFileJson*(secret: openArray[byte],
password: string,
version: int = 3,
cryptkind: CryptKind = AES128CTR,
kdfkind: KdfKind = PBKDF2,
workfactor: int = 0): KfResult[JsonNode] =
## Create JSON object with keyfile structure.
##
## ``secret`` - secret data, which will be stored
## ``password`` - encryption password
## ``outjson`` - result JSON object
## ``version`` - version of keyfile format (default is 3)
## ``cryptkind`` - algorithm for encryption
## (default is AES128-CTR)
## ``kdfkind`` - algorithm for key deriviation function (default is PBKDF2)
## ``workfactor`` - Key deriviation function work factor, 0 is to use
## default workfactor.
var iv: array[aes128.sizeBlock, byte]
var salt: array[SaltSize, byte]
var saltstr = newString(SaltSize)
if randomBytes(iv) != aes128.sizeBlock:
return err(RandomError)
if randomBytes(salt) != SaltSize:
return err(RandomError)
copyMem(addr saltstr[0], addr salt[0], SaltSize)
let u = ? uuidGenerate().mapErrTo(UuidError)
let
dkey = case kdfkind
of PBKDF2: ? deriveKey(password, saltstr, kdfkind, HashSHA2_256, workfactor)
of SCRYPT: ? deriveKey(password, saltstr, workfactor, ScryptR, ScryptP)
ciphertext = ? encryptData(secret, cryptkind, dkey, iv)
var ctx: keccak256
ctx.init()
ctx.update(toOpenArray(dkey, 16, 31))
ctx.update(ciphertext)
var mac = ctx.finish()
ctx.clear()
let params = ? kdfParams(kdfkind, toHex(salt, true), workfactor)
let json = %*
{
"crypto": {
"cipher": $cryptkind,
"cipherparams": {
"iv": toHex(iv, true)
},
"ciphertext": toHex(ciphertext, true),
"kdf": $kdfkind,
"kdfparams": params,
"mac": toHex(mac.data, true),
},
}
if IdInKeyfile:
json.add("id", %($u))
if VersionInKeyfile:
json.add("version", %version)
ok(json)
# Parses Cipher JSON information
proc decodeCrypto(n: JsonNode): KfResult[Crypto] =
var crypto = n.getOrDefault("crypto")
if isNil(crypto):
return err(MalformedError)
var kdf = crypto.getOrDefault("kdf")
if isNil(kdf):
return err(MalformedError)
var c: Crypto
case kdf.getStr()
of "pbkdf2": c.kind = PBKDF2
of "scrypt": c.kind = SCRYPT
else: return err(KdfNotSupported)
var cipherparams = crypto.getOrDefault("cipherparams")
if isNil(cipherparams):
return err(MalformedError)
c.cipher.kind = getCipher(crypto.getOrDefault("cipher").getStr())
c.cipher.params.iv = decodeHex(cipherparams.getOrDefault("iv").getStr())
c.cipher.text = decodeHex(crypto.getOrDefault("ciphertext").getStr())
c.mac = decodeHex(crypto.getOrDefault("mac").getStr())
c.kdfParams = crypto.getOrDefault("kdfparams")
if c.cipher.kind == CipherNoSupport:
return err(CipherNotSupported)
if len(c.cipher.text) == 0:
return err(EmptyCiphertext)
if len(c.mac) == 0:
return err(EmptyMac)
if isNil(c.kdfParams):
return err(MalformedError)
return ok(c)
# Parses PNKDF2 JSON parameters
proc decodePbkdf2Params(params: JsonNode): KfResult[Pbkdf2Params] =
var p: Pbkdf2Params
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
if len(p.salt) == 0:
return err(EmptySalt)
p.dklen = params.getOrDefault("dklen").getInt()
p.c = params.getOrDefault("c").getInt()
p.prf = getPrfHash(params.getOrDefault("prf").getStr())
if p.prf == HashNoSupport:
return err(PrfNotSupported)
if p.dklen == 0 or p.dklen > MaxDKLen:
return err(IncorrectDKLen)
return ok(p)
# Parses JSON Scrypt parameters
proc decodeScryptParams(params: JsonNode): KfResult[ScryptParams] =
var p: ScryptParams
p.salt = decodeSalt(params.getOrDefault("salt").getStr())
if len(p.salt) == 0:
return err(EmptySalt)
p.dklen = params.getOrDefault("dklen").getInt()
p.n = params.getOrDefault("n").getInt()
p.p = params.getOrDefault("p").getInt()
p.r = params.getOrDefault("r").getInt()
if p.dklen == 0 or p.dklen > MaxDKLen:
return err(IncorrectDKLen)
return ok(p)
# Decrypts data
func decryptSecret(crypto: Crypto, dkey: DKey): KfResult[seq[byte]] =
var ctx: keccak256
ctx.init()
ctx.update(toOpenArray(dkey, 16, 31))
ctx.update(crypto.cipher.text)
var mac = ctx.finish()
ctx.clear()
if not compareMac(mac.data, crypto.mac):
return err(IncorrectMac)
let plaintext = ? decryptData(crypto.cipher.text, crypto.cipher.kind, dkey, crypto.cipher.params.iv)
ok(plaintext)
# Parse JSON keyfile and decrypts its content using password
proc decodeKeyFileJson*(j: JsonNode,
password: string): KfResult[seq[byte]] =
## Decode secret from keyfile json object ``j`` using
## password string ``password``.
let res = decodeCrypto(j)
if res.isErr:
return err(res.error)
let crypto = res.get()
case crypto.kind
of PBKDF2:
let res = decodePbkdf2Params(crypto.kdfParams)
if res.isErr:
return err(res.error)
let params = res.get()
let dkey = ? deriveKey(password, params.salt, PBKDF2, params.prf, params.c)
return decryptSecret(crypto, dkey)
of SCRYPT:
let res = decodeScryptParams(crypto.kdfParams)
if res.isErr:
return err(res.error)
let params = res.get()
let dkey = ? deriveKey(password, params.salt, params.n, params.r, params.p)
return decryptSecret(crypto, dkey)
# Loads the file at pathname, decrypts and returns all keyfiles encrypted under password
proc loadKeyFiles*(pathname: string,
password: string): KfResult[seq[KfResult[seq[byte]]]] =
## Load and decode data from file with pathname
## ``pathname``, using password string ``password``.
## The index successful decryptions is returned
var data: JsonNode
var decodedKeyfile: KfResult[seq[byte]]
var successfullyDecodedKeyfiles: seq[KfResult[seq[byte]]]
if fileExists(pathname) == false:
return err(KeyfileDoesNotExist)
# Note that lines strips the ending newline, if present
try:
for keyfile in lines(pathname):
# We skip empty lines
if keyfile.len == 0:
continue
# We skip all lines that doesn't seem to define a json
if keyfile[0] != '{' or keyfile[^1] != '}':
continue
try:
data = json.parseJson(keyfile)
except JsonParsingError:
return err(JsonError)
except ValueError:
return err(JsonError)
except OSError:
return err(OsError)
except Exception: #parseJson raises Exception
return err(OsError)
decodedKeyfile = decodeKeyFileJson(data, password)
if decodedKeyfile.isOk():
successfullyDecodedKeyfiles.add decodedKeyfile
except IOError:
return err(IoError)
return ok(successfullyDecodedKeyfiles)
# Note that the keyfile is open in Append mode so that multiple credentials can be stored in same file
proc saveKeyFile*(pathname: string,
jobject: JsonNode): KfResult[void] =
## Save JSON object ``jobject`` to file with pathname ``pathname``.
var
f: File
if not f.open(pathname, fmAppend):
return err(OsError)
try:
# To avoid other users/attackers to be able to read keyfiles, we make the file readable/writable only by the running user
setFilePermissions(pathname, {fpUserWrite, fpUserRead})
f.write($jobject)
# We store a keyfile per line
f.write("\n")
ok()
except CatchableError:
err(OsError)
finally:
f.close()