mirror of
https://github.com/waku-org/nwaku.git
synced 2025-02-12 15:06:38 +00:00
deploy: ea8d72188e9ee74430fc1f311a7905c52cce4d02
This commit is contained in:
parent
e90639330d
commit
97ddd4d70e
@ -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 =
|
||||||
|
@ -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]
|
||||||
|
@ -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
|
||||||
|
377
tests/v2/test_utils_keyfile.nim
Normal file
377
tests/v2/test_utils_keyfile.nim
Normal 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
|
@ -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)
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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
573
waku/v2/utils/keyfile.nim
Normal 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()
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user