feat: keystore implementation (#70)

* add deps

* complete keystore, move code

* remove esling

* rename command

* rename export

* fix bug

* rollback example

* rollback example[2]

* improve compatibility with nwaku

* add packages

* update example

* add keystore.spec.ts

* up node, add whitelist words

* add types

* try break test

* add test

* add more tests

* add helper functions, update example, add tests

* fix types

* fix test

* add logs

* fix build of the object

* use different approach to catch errors

* run one test

* anotehr one

* fix validation error

* apply last fixes

* update example

* up

* ignroe generated files

* up

* up

* fix test

* test

* fix

* up

* add prettier ignore

* up

* fix lint

* up

* last
This commit is contained in:
Sasha 2023-10-14 02:21:35 +02:00 committed by GitHub
parent 5d7f77f300
commit 891ee3474a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 2927 additions and 153 deletions

View File

@ -13,7 +13,14 @@
"vkey",
"Waku",
"zerokit",
"zkey"
"zkey",
"Keccak",
"keccak",
"chainsafe",
"kdfparams",
"ciphertext",
"cipherparams",
"codegen"
],
"flagWords": [],
"ignorePaths": [

View File

@ -7,7 +7,14 @@
"env": {
"es6": true
},
"ignorePatterns": ["node_modules", "build", "coverage", "proto"],
"ignorePatterns": [
"node_modules",
"build",
"coverage",
"proto",
"*/**/credential_validation_generated.ts",
"*/**/keystore_validation_generated.ts"
],
"plugins": ["import", "eslint-comments", "functional"],
"extends": [
"eslint:recommended",

1
.prettierignore Normal file
View File

@ -0,0 +1 @@
*/**/*_generated.ts

View File

@ -62,3 +62,99 @@ rln.create().then(async (rlnInstance) => {
const event = await contract.registerMember(rlnInstance, signature);
console.log(`Registered as member with ${event}`);
});
const run = async () => {
const data = {
"application": "waku-rln-relay",
"appIdentifier": "01234567890abcdef",
"credentials": {
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548": {
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "fd6b39eb71d44c59f6bf5ff3d8945c80"
},
"ciphertext": "9c72f47ce95de03ed34502d0288e7576b66b51b9e7d5ae882c27bd89f94e6a03c2c44c2ddf0c982e72003d67212105f1b64614f57cabb0ceadab7e07be165eee1121ad6b81951368a9f3be2dd99ea294515f6013d5f2bd4702a40e36cfde2ea298b23b31e5ce719d8040c3331f73d6bf44f88bca39bac0e917d8bf545500e4f40d321c235426a80f315ac70666acbd3bdf803fbc1e7e7103fed466525ed332b25d72b2dbedf6fa383b2305987c1fe276b029570519b3e79930edf08c1029868d05c2c08ab61d7c64f63c054b4f6a5a12d43cdc79751b6fe58d3ed26b69443eb7c9f7efce27912340129c91b6b813ac94efd5776a40b1dda896d61357de208c7c47a14af911cc231355c8093ee6626e89c07e1037f9e0b22c690e3e049014399ca0212c509cb04c71c7860d1b17a0c47711c490c27bad2825926148a1f15a507f36ba2cdaa04897fce2914e53caed0beaf1bebd2a83af76511cc15bff2165ff0860ad6eca1f30022d7739b2a6b6a72f2feeef0f5941183cda015b4631469e1f4cf27003cab9a90920301cb30d95e4554686922dc5a05c13dfb575cdf113c700d607896011970e6ee7d6edb61210ab28ac8f0c84c606c097e3e300f0a5f5341edfd15432bef6225a498726b62a98283829ad51023b2987f30686cfb4ea3951f3957654035ec291f9b0964a3a8665d81b16cec20fb40f944d5f9bf03ac1e444ad45bae3fa85e7465ce620c0966d8148d6e2856f676c4fbbe3ebe470453efb4bbda1866680037917e37765f680e3da96ef3991f3fe5cda80c523996c2234758bf5f7b6d052dc6942f5a92c8b8eec5d2d8940203bbb6b1cba7b7ebc1334334ca69cdb509a5ea58ec6b2ebaea52307589eaae9430eb15ad234c0c39c83accdf3b77e52a616e345209c5bc9b442f9f0fa96836d9342f983a7",
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 1000000,
"prf": "hmac-sha256",
"salt": "60f0aa92fbf63a8356dfdbed2ab18058"
},
"mac": "51a227ac6db7f2797c63925880b3db664e034231a4c68daa919ab42d8df38bc6"
}
},
"263335559F0578FD785F9CDFEDBB45CFF276799A27580B8F580CDFDCB990257C": {
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "69f95461f811ac35a21987b1fdaa605e"
},
"ciphertext": "edfe844f8e2aedd62f26753e7247554920352b6b167f54ea4f728cd715577e9d2b7192b782471914870794205e77c2708b6db2d0ada19fec6b3533098cb2b7350bbaf81526d6bde7f1d0e83c366e3a2ddcced942cfb09a3c7704db7041132c3b511fed2f6d8599e6cddf649250b240687c2c335bf0aa75c892bc97f81c537898aefed20d1488e816d54eec72572acf36f140dc98cba0430cdeb8a00b8e8c8edf9b1292ca0e9c9a606acec51ea3dbe46438cb74b95d708cec18f8f126aecabbff11dd068d9194b25803f959f0bb62d49785dbc694486754f46bfe084cfa780cae27eca48cdcc88f4083d166d1747b8e2e637619e5d3848b9b6cdf7c7161eda8e476edfc083d417691d47b84fb224bfd26bf7713958893b934388e50783e49c5c84999971538ccda14c54b48b0d4aa37503e2a40212e9a1407d5a1ea4e96760de3d87e1b2287465a4e51cf330b7f1d14e3f2fb6521d10d32c798856464927b1e0286086a78f07a8f6f436d8c0c7b530f585320515e276d82c7b1f244702fa9ca6e6ad164fd2b1d9badcbdc17e01e95abf58e6825d8eeba5bc22db3a66dd41c64887d4c862298e921b3bae17d9fb7be1f619c60c82bd60dee351b77514d36e25d4092d6cde8ab613c40a117f7b784c80d65310e5b9cf1a31ba555f848e6984cc0c2d48315167d60131f3ffaaca5c81e359134bbfc81fa217f29b533868604ced4a2c5da8c89bd1238147b9f348168864ebea40c36a6abbf3d59d43086f26777104ce0a9f60cbf350058a337bc66abd5e4976950e5908192f98a9a8c1913abbc0d918479aeaa99e89a0e5cd65fd84a347d73df1d9c829863728a6fcd90150e52ecdec48bd07802110384f6c0aff0ca05ad42feb521223b58719fd4fc4ae88df8225ea58e303e4c61e8288e80f854bf0b",
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 1000000,
"prf": "hmac-sha256",
"salt": "3cf796e4857f296bef3bdb9ca844b1bf"
},
"mac": "3d6cb0492afcf89c891365f097ae8989dc50038010c419b18228be6816c24c32"
}
},
"F62A7FCE85E5B796AFCB38F54A44210515CB688EA0224E9A436CCA0A542F2C9D": {
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "49816dacf881c85db9f11f7f068dcf71"
},
"ciphertext": "d7d805ee24dd34368e3d1829aa6d0856ca2ea54d9bcdc2655f8f197af0d293aff56c6e06de3137b0eddffbcad7cc0b8e3f6ba761ac7983d8e59ce04c8936868297b9f70238cb295e17567f2404b278b93c985496a6e1e46185965491449ccbc1e7155224acdba354ed18b1b9867ff6f1a833a77c9b21e2e9c4b6af27d5bd6303efd574465920928e5c467bd3c7888c3f31e8bece6af2e0c35fa03661399e9b420eeecd4376cb2b3266692f46c03161bb32cc2c79521f7b19cb0e6ec911213e105967f8887d94c73e793b18e4c14ee045dca13fcfb62ae267d3175f8a4fefd0e8bd636bd9431cc0cc7119e75f116a16dcbdcac1c15a3dcec57e1c49dbf5dccd1c75c0cfcb3473e81e8546048ce5231a4d4c8dd5d66311354e9ab70ad5745d5be27746954a08b0b29218562bfb632ae0a498cf09d7955a27377ed7a50fc1b4adaa0a3fb3e87a3b4d923136be0767a1428050944b9fd247332dea1b5016dfa1ec4da167e70e11e07cd58034b8470366dc16d77978b49a61e213ab5a7817fd69af26c2a8c3cd3a488d6e1491e0215071e1f3e9d49d0dfab3a7e324644c98a088e20259980495dcc379dcdce2e61752711bdf8abf057a2e696624078601245828193d838cc806065ee3f2bb138302ec72c70f34f14c0ab816211011f0ac55423732875e220175c717f6bc86f071bb4fab51c1963eb5c5d70d504c1e4d2307a8c8c4b8b5a84566a4606deb3fc6d7a420adc2b2b37c0ef3018f82a3ce0044e082407e8e7cb6214a3abc139b7f75b2c36c6902080e7696c730ab062e75e597274e0c945b6a7a366d20bd210dd02b097071142d033597e2fc4174be683a866510fa1c2fe150a2fb81dbd2b5da25da27f29367fb22dd4e9d4785856e4deea56219f9495fb3ab772f7867db11cb14026b",
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 1000000,
"prf": "hmac-sha256",
"salt": "2dcb5ba5c98fe5e46d961dad36e79a5b"
},
"mac": "2d6e9de6440f52c5db64b13f80399967c8770e82616294e14f40a2e213e7d925"
}
},
"8479C6B9125D43E7B7739F1BAB41779F2F5A4D27FF0E2B6F6CA353032010A22C": {
"crypto": {
"cipher": "aes-128-ctr",
"cipherparams": {
"iv": "b0eef2c385a04909c4ae9b318e179fa7"
},
"ciphertext": "90b982222072366566fa194be5c170506888e184cadbd52aa38f184ac4e9bc160cc719d809fb6a128e0cbd908e70a71efdc5d51c4dab8aab71e3e6a2ebd9ea4238cb47585137990e896dfa53961bb2b328abfbba82f49db6a9b6e3790cf9e29c145796c6dbf409dc875e7998db827c944a835a29ab4192a11ad1efde5ebdd1a775ecbfefd139c50fbdcbebd6c124d9d65ba6ddaaa83e57695293e7c85dfd6f418d58fa5ffb9ab9b2395c84b57da796d31b6351fde3f1dbab29da6c3f259859bd0719c34f5111a9a12075b53ee91b4598fb2f452dbea823ec094cb757f370b5386a8e5db25cf732681d0cd9bda651ae55cdd125138fd2c8f1ffe87a5eea14df7d355762b37e3e71c33c6fe46a10c2083538910fec12e294de84ff587cab2dd268203699cb180e481f4a3a093b86854cea64341dd9482305abd4a9d7bb304b078bc255bf7cde78689225f17006f24c2cd82d38a59f1e0899965c38fcfd1ec67069143ee05a34922963a527549a002e3221e1461463f573e5f66ba87dcb83a63cb8e3a721c13cd9d4d0c9a0334a558f32027424a5bc9fc12b91981a3f74ac4b62eea3aae8be6c44504696b96afadce5d9222bb67dddf5a7d98dd43d544d79f8720a946c37eba8eb5ae6d70f4bdbbe554cbd4b3abb35ed357c8cb8f55e016ab83bef12bf5c0cdf26c7624c86f16437f545d796addb1aa7370de329930c68b174c871706e7afdf78cc07e0f0c58e45495d0d3bcf3faf9fb6d20369b0adc89766b0c9132677e52112770d017da7658f2a0c0eaeac57416f203700f98bf7b30119407733d4f0bd4322c622120cdf81646c4a1adfb80e757954e41ba0e7816c403b2e4b9ceb2d36e4198921ea719a410ae6f6983e49e7b99c266deb0465af716799e36a5bab70923291da808edeba54267e31e8b64c37123fd45d86e0638",
"kdf": "pbkdf2",
"kdfparams": {
"dklen": 32,
"c": 1000000,
"prf": "hmac-sha256",
"salt": "142a0a65b7f6f480546cc4ef743d7ef9"
},
"mac": "7119b7b78598850de5f6af742e42748a3b005394b6b8b272490f24527ebd8b15"
}
}
},
"version": "0.2"
};
const keystore = await rln.Keystore.fromObject(data);
const identity = {
"idTrapdoor":[248,73,210,129,12,83,146,208,57,117,35,145,20,66,203,61,238,130,60,23,94,249,123,92,114,190,24,0,4,242,10,24],
"idNullifier":[80,233,27,57,133,192,162,150,251,120,215,194,103,198,249,177,225,15,68,137,221,204,174,12,107,102,123,129,111,191,121,22],
"idSecretHash":[120,204,2,248,110,149,95,135,222,176,27,137,217,255,86,205,74,130,21,250,220,111,203,11,199,229,25,76,146,3,249,13],
"idCommitment":[71,207,30,230,39,50,198,40,128,131,79,200,166,168,187,137,235,132,131,164,92,233,195,170,21,205,94,85,246,144,203,9]
};
const membership = {
"chainId":"0xAA36A7","address":"0x0A988fd9CA5BAebDf098b8A73621b2AaDa6492E8","treeIndex":66851
};
await keystore.addCredential({ identity, membership }, "sup3rsecure");
await keystore.readCredential("8479C6B9125D43E7B7739F1BAB41779F2F5A4D27FF0E2B6F6CA353032010A22C", "sup3rsecure").then(console.log);
};
run();

View File

@ -8,7 +8,8 @@
"name": "@waku/rln-example",
"version": "0.1.0",
"dependencies": {
"@waku/rln": "file:../"
"@waku/rln": "file:../",
"@waku/utils": "^0.0.11"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
@ -22,9 +23,13 @@
"version": "0.1.1",
"license": "MIT OR Apache-2.0",
"dependencies": {
"@waku/utils": "^0.0.6",
"@chainsafe/bls-keystore": "^3.0.0",
"@waku/utils": "^0.0.11",
"@waku/zerokit-rln-wasm": "^0.0.10",
"ethers": "^5.7.2"
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"rlnjs": "^3.2.3",
"uuid": "^9.0.1"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.2",
@ -36,6 +41,7 @@
"@types/chai": "^4.2.15",
"@types/chai-spies": "^1.0.3",
"@types/debug": "^4.1.7",
"@types/lodash": "^4.14.199",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.6",
"@types/tail": "^2.0.0",
@ -46,6 +52,8 @@
"@waku/interfaces": "^0.0.13",
"@waku/message-encryption": "^0.0.16",
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"app-root-path": "^3.0.0",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
@ -365,10 +373,52 @@
"@types/node": "*"
}
},
"node_modules/@waku/interfaces": {
"version": "0.0.18",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.18.tgz",
"integrity": "sha512-esgXs8fZTth+7DSndnB92/YPOnpn0rD0E4GPu/yfQZbwSS+pPyfpk58iklgWterD/CelB487X4qy0ooe2uWrBg==",
"engines": {
"node": ">=16"
}
},
"node_modules/@waku/rln": {
"resolved": "..",
"link": true
},
"node_modules/@waku/utils": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.11.tgz",
"integrity": "sha512-AG/S9vxqqxQUO9dlfi5Apv5o364dIzg3cLm32qstqQDSWw3QGhOSRLg+MBxdFrfmELCI3O1zdMoGdLRaZIwr8Q==",
"dependencies": {
"@waku/interfaces": "0.0.18",
"debug": "^4.3.4",
"uint8arrays": "^4.0.4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/@waku/utils/node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"dependencies": {
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/@waku/utils/node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@ -2173,6 +2223,15 @@
"multicast-dns": "cli.js"
}
},
"node_modules/multiformats": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-12.1.2.tgz",
"integrity": "sha512-6mRIsrZXyw5xNPO31IGBMmxgDXBSgCGDsBAtazkZ02ip4hMwZNrQvfxXZtytRoBSWuzSq5f9VmMnXj76fIz5FQ==",
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -3260,6 +3319,14 @@
"node": ">= 0.6"
}
},
"node_modules/uint8arrays": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz",
"integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==",
"dependencies": {
"multiformats": "^12.0.1"
}
},
"node_modules/unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
@ -3940,9 +4007,15 @@
"@types/node": "*"
}
},
"@waku/interfaces": {
"version": "0.0.18",
"resolved": "https://registry.npmjs.org/@waku/interfaces/-/interfaces-0.0.18.tgz",
"integrity": "sha512-esgXs8fZTth+7DSndnB92/YPOnpn0rD0E4GPu/yfQZbwSS+pPyfpk58iklgWterD/CelB487X4qy0ooe2uWrBg=="
},
"@waku/rln": {
"version": "file:..",
"requires": {
"@chainsafe/bls-keystore": "^3.0.0",
"@rollup/plugin-commonjs": "^22.0.2",
"@rollup/plugin-json": "^4.1.0",
"@rollup/plugin-node-resolve": "^13.3.0",
@ -3952,6 +4025,7 @@
"@types/chai": "^4.2.15",
"@types/chai-spies": "^1.0.3",
"@types/debug": "^4.1.7",
"@types/lodash": "^4.14.199",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.6",
"@types/tail": "^2.0.0",
@ -3961,9 +4035,11 @@
"@waku/core": "^0.0.18",
"@waku/interfaces": "^0.0.13",
"@waku/message-encryption": "^0.0.16",
"@waku/utils": "^0.0.6",
"@waku/utils": "^0.0.11",
"@waku/zerokit-rln-wasm": "^0.0.10",
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"app-root-path": "^3.0.0",
"chai": "^4.3.4",
"chai-spies": "^1.0.0",
@ -3987,6 +4063,7 @@
"karma-mocha": "^2.0.1",
"karma-webpack": "^5.0.0",
"lint-staged": "^13.0.3",
"lodash": "^4.17.21",
"mocha": "10.1.0",
"npm-run-all": "^4.1.5",
"p-timeout": "^4.1.0",
@ -3994,6 +4071,7 @@
"process": "^0.11.10",
"puppeteer": "^13.0.1",
"resolve-typescript-plugin": "^1.2.0",
"rlnjs": "^3.2.3",
"rollup": "^2.75.0",
"rollup-plugin-copy": "^3.4.0",
"size-limit": "^8.0.0",
@ -4001,7 +4079,33 @@
"ts-loader": "^9.3.1",
"ts-node": "^10.9.1",
"typedoc": "^0.23.10",
"typescript": "^4.5.5"
"typescript": "^4.5.5",
"uuid": "^9.0.1"
}
},
"@waku/utils": {
"version": "0.0.11",
"resolved": "https://registry.npmjs.org/@waku/utils/-/utils-0.0.11.tgz",
"integrity": "sha512-AG/S9vxqqxQUO9dlfi5Apv5o364dIzg3cLm32qstqQDSWw3QGhOSRLg+MBxdFrfmELCI3O1zdMoGdLRaZIwr8Q==",
"requires": {
"@waku/interfaces": "0.0.18",
"debug": "^4.3.4",
"uint8arrays": "^4.0.4"
},
"dependencies": {
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
"requires": {
"ms": "2.1.2"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
}
}
},
"@webassemblyjs/ast": {
@ -5399,6 +5503,11 @@
"thunky": "^1.0.2"
}
},
"multiformats": {
"version": "12.1.2",
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-12.1.2.tgz",
"integrity": "sha512-6mRIsrZXyw5xNPO31IGBMmxgDXBSgCGDsBAtazkZ02ip4hMwZNrQvfxXZtytRoBSWuzSq5f9VmMnXj76fIz5FQ=="
},
"negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@ -6187,6 +6296,14 @@
"mime-types": "~2.1.24"
}
},
"uint8arrays": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-4.0.6.tgz",
"integrity": "sha512-4ZesjQhqOU2Ip6GPReIwN60wRxIupavL8T0Iy36BBHr2qyMrNxsPJvr7vpS4eFt8F8kSguWUPad6ZM9izs/vyw==",
"requires": {
"multiformats": "^12.0.1"
}
},
"unpipe": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",

View File

@ -9,12 +9,13 @@
"start": "webpack-dev-server"
},
"dependencies": {
"@waku/rln": "file:../"
"@waku/rln": "file:../",
"@waku/utils": "^0.0.11"
},
"devDependencies": {
"copy-webpack-plugin": "^11.0.0",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0",
"webpack-dev-server": "^4.11.1",
"copy-webpack-plugin": "^11.0.0"
"webpack-dev-server": "^4.11.1"
}
}

1911
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@
"scripts": {
"prepare": "husky install",
"build": "run-s build:**",
"build:codegen": "node ./scripts/schema_validator_codegen.js",
"build:tsc": "tsc",
"build:bundle": "rollup --config rollup.config.js",
"size": "npm run build && size-limit",
@ -45,7 +46,7 @@
"crypto": false
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"publishConfig": {
"access": "public",
@ -59,19 +60,31 @@
"@size-limit/preset-big-lib": "^8.0.0",
"@types/app-root-path": "^1.2.4",
"@types/chai": "^4.2.15",
"@types/chai-as-promised": "^7.1.6",
"@types/chai-spies": "^1.0.3",
"@types/chai-subset": "^1.3.3",
"@types/debug": "^4.1.7",
"@types/deep-equal-in-any-order": "^1.0.1",
"@types/lodash": "^4.14.199",
"@types/mocha": "^9.1.0",
"@types/node": "^17.0.6",
"@types/tail": "^2.0.0",
"@types/uuid": "^8.3.0",
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"@waku/core": "^0.0.18",
"@waku/interfaces": "^0.0.13",
"@waku/message-encryption": "^0.0.16",
"@web/rollup-plugin-import-meta-assets": "^1.0.7",
"ajv": "^8.12.0",
"ajv-formats": "^2.1.1",
"app-root-path": "^3.0.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"chai-spies": "^1.0.0",
"chai-subset": "^1.6.0",
"cspell": "^5.14.0",
"deep-equal-in-any-order": "^2.0.6",
"eslint": "^8.6.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-eslint-comments": "^3.2.0",
@ -83,9 +96,6 @@
"husky": "^7.0.4",
"ignore-loader": "^0.1.2",
"isomorphic-fetch": "^3.0.0",
"@waku/interfaces": "^0.0.13",
"@waku/message-encryption": "^0.0.16",
"@waku/core": "^0.0.18",
"jsdom": "^19.0.0",
"jsdom-global": "^3.0.2",
"karma": "^6.3.12",
@ -129,8 +139,13 @@
]
},
"dependencies": {
"@waku/utils": "^0.0.6",
"@chainsafe/bls-keystore": "^3.0.0",
"@waku/utils": "^0.0.11",
"@waku/zerokit-rln-wasm": "^0.0.10",
"ethers": "^5.7.2"
"ethereum-cryptography": "^2.1.2",
"ethers": "^5.7.2",
"lodash": "^4.17.21",
"rlnjs": "^3.2.3",
"uuid": "^9.0.1"
}
}

View File

@ -0,0 +1,23 @@
{
"$ref": "#/definitions/Keystore",
"definitions": {
"Keystore": {
"type": "object",
"properties": {
"credentials": {
"type": "object"
},
"appIdentifier": {
"type": "string"
},
"version": {
"type": "string"
},
"application": {
"type": "string"
}
},
"required": ["application", "appIdentifier", "credentials", "version"]
}
}
}

View File

@ -0,0 +1,42 @@
{
"$ref": "#/definitions/Credential",
"definitions": {
"Credential": {
"type": "object",
"properties": {
"crypto": {
"type": "object",
"properties": {
"cipher": {
"type": "string"
},
"cipherparams": {
"type": "object"
},
"ciphertext": {
"type": "string"
},
"kdf": {
"type": "string"
},
"kdfparams": {
"type": "object"
},
"mac": {
"type": "string"
}
},
"required": [
"cipher",
"cipherparams",
"ciphertext",
"kdf",
"kdfparams",
"mac"
]
}
},
"required": ["crypto"]
}
}
}

View File

@ -0,0 +1,77 @@
import { writeFileSync } from "fs";
import { join } from "path";
import { fileURLToPath } from "url";
import { dirname } from "path";
import Ajv from "ajv";
import addFormats from "ajv-formats";
import standaloneCode from "ajv/dist/standalone/index.js";
import keystoreSchema from "./nwaku_keystore.json" assert { type: "json" };
import credentialSchema from "./nwaku_keystore_credential.json" assert { type: "json" };
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Generate a function that validates the Keystore schema.
// Write the function to a file in the source.
const OUTPUT_DIR = join(__dirname, "../src/keystore");
const OUTPUT_KEYSTORE_FILE = join(
OUTPUT_DIR,
"keystore_validation_generated.ts"
);
const OUTPUT_CREDENTIALS_FILE = join(
OUTPUT_DIR,
"credential_validation_generated.ts"
);
// Initialize ajv instance
let ajv = new Ajv({
schemas: [keystoreSchema],
code: {
source: true,
esm: true,
},
});
addFormats(ajv);
// The output code is minified vanilla javascript and uses the cute pattern of attaching errors to the exported function as a property.
// The easiest way to treat this is to just ts-ignore the whole output and wrap the function with a handcrafted type.
const keystoreModuleCode =
`
/* eslint eslint-comments/no-unlimited-disable: "off" */
// This file was generated by /scripts/schema-validation-codegen.ts
// Do not modify this file by hand.
/* eslint-disable */
// @ts-ignore
` +
standaloneCode(ajv, {
Keystore: "#/definitions/Keystore",
});
ajv = new Ajv({
schemas: [credentialSchema],
code: {
source: true,
esm: true,
},
});
addFormats(ajv);
const credentialModuleCode =
`
/* eslint eslint-comments/no-unlimited-disable: "off" */
// This file was generated by /scripts/schema-validation-codegen.ts
// Do not modify this file by hand.
/* eslint-disable */
// @ts-ignore
` +
standaloneCode(ajv, {
Credential: "#/definitions/Credential",
});
writeFileSync(OUTPUT_KEYSTORE_FILE, keystoreModuleCode);
writeFileSync(OUTPUT_CREDENTIALS_FILE, credentialModuleCode);

View File

@ -37,3 +37,13 @@ export function writeUIntLE(
return buf;
}
/**
* Transforms Uint8Array into BigInt
* @param array: Uint8Array
* @returns BigInt
*/
export function buildBigIntFromUint8Array(array: Uint8Array): bigint {
const dataView = new DataView(array.buffer);
return dataView.getBigUint64(0, true);
}

View File

@ -1,5 +1,6 @@
import { RLNDecoder, RLNEncoder } from "./codec.js";
import { RLN_ABI, SEPOLIA_CONTRACT } from "./constants.js";
import { Keystore } from "./keystore/index.js";
import {
IdentityCredential,
Proof,
@ -19,6 +20,7 @@ export async function create(): Promise<RLNInstance> {
}
export {
Keystore,
RLNInstance,
IdentityCredential,
Proof,

54
src/keystore/cipher.ts Normal file
View File

@ -0,0 +1,54 @@
import type { IKeystore as IEipKeystore } from "@chainsafe/bls-keystore";
import { cipherDecrypt } from "@chainsafe/bls-keystore/lib/cipher.js";
import { kdf } from "@chainsafe/bls-keystore/lib/kdf.js";
import { normalizePassword } from "@chainsafe/bls-keystore/lib/password.js";
import { keccak256 } from "ethereum-cryptography/keccak";
import {
bytesToHex,
concatBytes,
hexToBytes,
} from "ethereum-cryptography/utils";
import type { Keccak256Hash, Password } from "./types.js";
// eipKeystore supports only sha256 checksum so we just make an assumption it is keccak256
const validateChecksum = async (
password: Password,
eipKeystore: IEipKeystore
): Promise<boolean> => {
const computedChecksum = await keccak256Checksum(password, eipKeystore);
return computedChecksum === eipKeystore.crypto.checksum.message;
};
// decrypt from @chainsafe/bls-keystore supports only sha256
// but nwaku uses keccak256
// https://github.com/waku-org/nwaku/blob/25d6e52e3804d15f9b61bc4cc6dd448540c072a1/waku/waku_keystore/keyfile.nim#L367
export const decryptEipKeystore = async (
password: Password,
eipKeystore: IEipKeystore
): Promise<Uint8Array> => {
const decryptionKey = await kdf(
eipKeystore.crypto.kdf,
normalizePassword(password)
);
const isChecksumValid = await validateChecksum(password, eipKeystore);
if (!isChecksumValid) {
throw Error("Password is invalid.");
}
return cipherDecrypt(eipKeystore.crypto.cipher, decryptionKey.slice(0, 16));
};
export const keccak256Checksum = async (
password: Password,
eipKeystore: IEipKeystore
): Promise<Keccak256Hash> => {
const key = await kdf(eipKeystore.crypto.kdf, normalizePassword(password));
const payload = concatBytes(
key.slice(16),
hexToBytes(eipKeystore.crypto.cipher.message)
);
const ciphertext = keccak256(payload);
return bytesToHex(ciphertext);
};

View File

@ -0,0 +1,8 @@
/* eslint eslint-comments/no-unlimited-disable: "off" */
// This file was generated by /scripts/schema-validation-codegen.ts
// Do not modify this file by hand.
/* eslint-disable */
// @ts-ignore
"use strict";export const Credential = validate11;const schema12 = {"type":"object","properties":{"crypto":{"type":"object","properties":{"cipher":{"type":"string"},"cipherparams":{"type":"object"},"ciphertext":{"type":"string"},"kdf":{"type":"string"},"kdfparams":{"type":"object"},"mac":{"type":"string"}},"required":["cipher","cipherparams","ciphertext","kdf","kdfparams","mac"]}},"required":["crypto"]};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if((data.crypto === undefined) && (missing0 = "crypto")){validate11.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.crypto !== undefined){let data0 = data.crypto;const _errs1 = errors;if(errors === _errs1){if(data0 && typeof data0 == "object" && !Array.isArray(data0)){let missing1;if(((((((data0.cipher === undefined) && (missing1 = "cipher")) || ((data0.cipherparams === undefined) && (missing1 = "cipherparams"))) || ((data0.ciphertext === undefined) && (missing1 = "ciphertext"))) || ((data0.kdf === undefined) && (missing1 = "kdf"))) || ((data0.kdfparams === undefined) && (missing1 = "kdfparams"))) || ((data0.mac === undefined) && (missing1 = "mac"))){validate11.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/required",keyword:"required",params:{missingProperty: missing1},message:"must have required property '"+missing1+"'"}];return false;}else {if(data0.cipher !== undefined){const _errs3 = errors;if(typeof data0.cipher !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/cipher",schemaPath:"#/properties/crypto/properties/cipher/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs3 === errors;}else {var valid1 = true;}if(valid1){if(data0.cipherparams !== undefined){let data2 = data0.cipherparams;const _errs5 = errors;if(!(data2 && typeof data2 == "object" && !Array.isArray(data2))){validate11.errors = [{instancePath:instancePath+"/crypto/cipherparams",schemaPath:"#/properties/crypto/properties/cipherparams/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid1 = _errs5 === errors;}else {var valid1 = true;}if(valid1){if(data0.ciphertext !== undefined){const _errs7 = errors;if(typeof data0.ciphertext !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/ciphertext",schemaPath:"#/properties/crypto/properties/ciphertext/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs7 === errors;}else {var valid1 = true;}if(valid1){if(data0.kdf !== undefined){const _errs9 = errors;if(typeof data0.kdf !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/kdf",schemaPath:"#/properties/crypto/properties/kdf/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs9 === errors;}else {var valid1 = true;}if(valid1){if(data0.kdfparams !== undefined){let data5 = data0.kdfparams;const _errs11 = errors;if(!(data5 && typeof data5 == "object" && !Array.isArray(data5))){validate11.errors = [{instancePath:instancePath+"/crypto/kdfparams",schemaPath:"#/properties/crypto/properties/kdfparams/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid1 = _errs11 === errors;}else {var valid1 = true;}if(valid1){if(data0.mac !== undefined){const _errs13 = errors;if(typeof data0.mac !== "string"){validate11.errors = [{instancePath:instancePath+"/crypto/mac",schemaPath:"#/properties/crypto/properties/mac/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid1 = _errs13 === errors;}else {var valid1 = true;}}}}}}}}else {validate11.errors = [{instancePath:instancePath+"/crypto",schemaPath:"#/properties/crypto/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;}

3
src/keystore/index.ts Normal file
View File

@ -0,0 +1,3 @@
import { Keystore } from "./keystore.js";
export { Keystore };

View File

@ -0,0 +1,313 @@
import chai, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import chaiSubset from "chai-subset";
import deepEqualInAnyOrder from "deep-equal-in-any-order";
chai.use(chaiSubset);
chai.use(deepEqualInAnyOrder);
chai.use(chaiAsPromised);
import { buildBigIntFromUint8Array } from "../byte_utils.js";
import { IdentityCredential } from "../rln.js";
import { Keystore } from "./keystore.js";
import type { MembershipInfo } from "./types.js";
const DEFAULT_PASSWORD = "sup3rsecure";
const NWAKU_KEYSTORE = {
application: "waku-rln-relay",
appIdentifier: "01234567890abcdef",
version: "0.2",
credentials: {
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548": {
crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "fd6b39eb71d44c59f6bf5ff3d8945c80",
},
ciphertext:
"9c72f47ce95de03ed34502d0288e7576b66b51b9e7d5ae882c27bd89f94e6a03c2c44c2ddf0c982e72003d67212105f1b64614f57cabb0ceadab7e07be165eee1121ad6b81951368a9f3be2dd99ea294515f6013d5f2bd4702a40e36cfde2ea298b23b31e5ce719d8040c3331f73d6bf44f88bca39bac0e917d8bf545500e4f40d321c235426a80f315ac70666acbd3bdf803fbc1e7e7103fed466525ed332b25d72b2dbedf6fa383b2305987c1fe276b029570519b3e79930edf08c1029868d05c2c08ab61d7c64f63c054b4f6a5a12d43cdc79751b6fe58d3ed26b69443eb7c9f7efce27912340129c91b6b813ac94efd5776a40b1dda896d61357de208c7c47a14af911cc231355c8093ee6626e89c07e1037f9e0b22c690e3e049014399ca0212c509cb04c71c7860d1b17a0c47711c490c27bad2825926148a1f15a507f36ba2cdaa04897fce2914e53caed0beaf1bebd2a83af76511cc15bff2165ff0860ad6eca1f30022d7739b2a6b6a72f2feeef0f5941183cda015b4631469e1f4cf27003cab9a90920301cb30d95e4554686922dc5a05c13dfb575cdf113c700d607896011970e6ee7d6edb61210ab28ac8f0c84c606c097e3e300f0a5f5341edfd15432bef6225a498726b62a98283829ad51023b2987f30686cfb4ea3951f3957654035ec291f9b0964a3a8665d81b16cec20fb40f944d5f9bf03ac1e444ad45bae3fa85e7465ce620c0966d8148d6e2856f676c4fbbe3ebe470453efb4bbda1866680037917e37765f680e3da96ef3991f3fe5cda80c523996c2234758bf5f7b6d052dc6942f5a92c8b8eec5d2d8940203bbb6b1cba7b7ebc1334334ca69cdb509a5ea58ec6b2ebaea52307589eaae9430eb15ad234c0c39c83accdf3b77e52a616e345209c5bc9b442f9f0fa96836d9342f983a7",
kdf: "pbkdf2",
kdfparams: {
dklen: 32,
c: 1000000,
prf: "hmac-sha256",
salt: "60f0aa92fbf63a8356dfdbed2ab18058",
},
mac: "51a227ac6db7f2797c63925880b3db664e034231a4c68daa919ab42d8df38bc6",
},
},
"263335559F0578FD785F9CDFEDBB45CFF276799A27580B8F580CDFDCB990257C": {
crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "69f95461f811ac35a21987b1fdaa605e",
},
ciphertext:
"edfe844f8e2aedd62f26753e7247554920352b6b167f54ea4f728cd715577e9d2b7192b782471914870794205e77c2708b6db2d0ada19fec6b3533098cb2b7350bbaf81526d6bde7f1d0e83c366e3a2ddcced942cfb09a3c7704db7041132c3b511fed2f6d8599e6cddf649250b240687c2c335bf0aa75c892bc97f81c537898aefed20d1488e816d54eec72572acf36f140dc98cba0430cdeb8a00b8e8c8edf9b1292ca0e9c9a606acec51ea3dbe46438cb74b95d708cec18f8f126aecabbff11dd068d9194b25803f959f0bb62d49785dbc694486754f46bfe084cfa780cae27eca48cdcc88f4083d166d1747b8e2e637619e5d3848b9b6cdf7c7161eda8e476edfc083d417691d47b84fb224bfd26bf7713958893b934388e50783e49c5c84999971538ccda14c54b48b0d4aa37503e2a40212e9a1407d5a1ea4e96760de3d87e1b2287465a4e51cf330b7f1d14e3f2fb6521d10d32c798856464927b1e0286086a78f07a8f6f436d8c0c7b530f585320515e276d82c7b1f244702fa9ca6e6ad164fd2b1d9badcbdc17e01e95abf58e6825d8eeba5bc22db3a66dd41c64887d4c862298e921b3bae17d9fb7be1f619c60c82bd60dee351b77514d36e25d4092d6cde8ab613c40a117f7b784c80d65310e5b9cf1a31ba555f848e6984cc0c2d48315167d60131f3ffaaca5c81e359134bbfc81fa217f29b533868604ced4a2c5da8c89bd1238147b9f348168864ebea40c36a6abbf3d59d43086f26777104ce0a9f60cbf350058a337bc66abd5e4976950e5908192f98a9a8c1913abbc0d918479aeaa99e89a0e5cd65fd84a347d73df1d9c829863728a6fcd90150e52ecdec48bd07802110384f6c0aff0ca05ad42feb521223b58719fd4fc4ae88df8225ea58e303e4c61e8288e80f854bf0b",
kdf: "pbkdf2",
kdfparams: {
dklen: 32,
c: 1000000,
prf: "hmac-sha256",
salt: "3cf796e4857f296bef3bdb9ca844b1bf",
},
mac: "3d6cb0492afcf89c891365f097ae8989dc50038010c419b18228be6816c24c32",
},
},
F62A7FCE85E5B796AFCB38F54A44210515CB688EA0224E9A436CCA0A542F2C9D: {
crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "49816dacf881c85db9f11f7f068dcf71",
},
ciphertext:
"d7d805ee24dd34368e3d1829aa6d0856ca2ea54d9bcdc2655f8f197af0d293aff56c6e06de3137b0eddffbcad7cc0b8e3f6ba761ac7983d8e59ce04c8936868297b9f70238cb295e17567f2404b278b93c985496a6e1e46185965491449ccbc1e7155224acdba354ed18b1b9867ff6f1a833a77c9b21e2e9c4b6af27d5bd6303efd574465920928e5c467bd3c7888c3f31e8bece6af2e0c35fa03661399e9b420eeecd4376cb2b3266692f46c03161bb32cc2c79521f7b19cb0e6ec911213e105967f8887d94c73e793b18e4c14ee045dca13fcfb62ae267d3175f8a4fefd0e8bd636bd9431cc0cc7119e75f116a16dcbdcac1c15a3dcec57e1c49dbf5dccd1c75c0cfcb3473e81e8546048ce5231a4d4c8dd5d66311354e9ab70ad5745d5be27746954a08b0b29218562bfb632ae0a498cf09d7955a27377ed7a50fc1b4adaa0a3fb3e87a3b4d923136be0767a1428050944b9fd247332dea1b5016dfa1ec4da167e70e11e07cd58034b8470366dc16d77978b49a61e213ab5a7817fd69af26c2a8c3cd3a488d6e1491e0215071e1f3e9d49d0dfab3a7e324644c98a088e20259980495dcc379dcdce2e61752711bdf8abf057a2e696624078601245828193d838cc806065ee3f2bb138302ec72c70f34f14c0ab816211011f0ac55423732875e220175c717f6bc86f071bb4fab51c1963eb5c5d70d504c1e4d2307a8c8c4b8b5a84566a4606deb3fc6d7a420adc2b2b37c0ef3018f82a3ce0044e082407e8e7cb6214a3abc139b7f75b2c36c6902080e7696c730ab062e75e597274e0c945b6a7a366d20bd210dd02b097071142d033597e2fc4174be683a866510fa1c2fe150a2fb81dbd2b5da25da27f29367fb22dd4e9d4785856e4deea56219f9495fb3ab772f7867db11cb14026b",
kdf: "pbkdf2",
kdfparams: {
dklen: 32,
c: 1000000,
prf: "hmac-sha256",
salt: "2dcb5ba5c98fe5e46d961dad36e79a5b",
},
mac: "2d6e9de6440f52c5db64b13f80399967c8770e82616294e14f40a2e213e7d925",
},
},
"8479C6B9125D43E7B7739F1BAB41779F2F5A4D27FF0E2B6F6CA353032010A22C": {
crypto: {
cipher: "aes-128-ctr",
cipherparams: {
iv: "b0eef2c385a04909c4ae9b318e179fa7",
},
ciphertext:
"90b982222072366566fa194be5c170506888e184cadbd52aa38f184ac4e9bc160cc719d809fb6a128e0cbd908e70a71efdc5d51c4dab8aab71e3e6a2ebd9ea4238cb47585137990e896dfa53961bb2b328abfbba82f49db6a9b6e3790cf9e29c145796c6dbf409dc875e7998db827c944a835a29ab4192a11ad1efde5ebdd1a775ecbfefd139c50fbdcbebd6c124d9d65ba6ddaaa83e57695293e7c85dfd6f418d58fa5ffb9ab9b2395c84b57da796d31b6351fde3f1dbab29da6c3f259859bd0719c34f5111a9a12075b53ee91b4598fb2f452dbea823ec094cb757f370b5386a8e5db25cf732681d0cd9bda651ae55cdd125138fd2c8f1ffe87a5eea14df7d355762b37e3e71c33c6fe46a10c2083538910fec12e294de84ff587cab2dd268203699cb180e481f4a3a093b86854cea64341dd9482305abd4a9d7bb304b078bc255bf7cde78689225f17006f24c2cd82d38a59f1e0899965c38fcfd1ec67069143ee05a34922963a527549a002e3221e1461463f573e5f66ba87dcb83a63cb8e3a721c13cd9d4d0c9a0334a558f32027424a5bc9fc12b91981a3f74ac4b62eea3aae8be6c44504696b96afadce5d9222bb67dddf5a7d98dd43d544d79f8720a946c37eba8eb5ae6d70f4bdbbe554cbd4b3abb35ed357c8cb8f55e016ab83bef12bf5c0cdf26c7624c86f16437f545d796addb1aa7370de329930c68b174c871706e7afdf78cc07e0f0c58e45495d0d3bcf3faf9fb6d20369b0adc89766b0c9132677e52112770d017da7658f2a0c0eaeac57416f203700f98bf7b30119407733d4f0bd4322c622120cdf81646c4a1adfb80e757954e41ba0e7816c403b2e4b9ceb2d36e4198921ea719a410ae6f6983e49e7b99c266deb0465af716799e36a5bab70923291da808edeba54267e31e8b64c37123fd45d86e0638",
kdf: "pbkdf2",
kdfparams: {
dklen: 32,
c: 1000000,
prf: "hmac-sha256",
salt: "142a0a65b7f6f480546cc4ef743d7ef9",
},
mac: "7119b7b78598850de5f6af742e42748a3b005394b6b8b272490f24527ebd8b15",
},
},
},
};
describe("Keystore", () => {
it("shoud create empty store with predefined values", () => {
const store = Keystore.create();
expect(store.toObject()).to.deep.eq({
application: "waku-rln-relay",
appIdentifier: "01234567890abcdef",
version: "0.2",
credentials: {},
});
});
// TODO: add more thorow credentials testing
[
{
some: "3123",
dadw: "1212",
},
{
application: 123,
version: "01234567890abcdef",
appIdentifier: "0.2",
credentials: {},
},
{
application: "waku-rln-relay",
version: 213,
appIdentifier: "0.2",
credentials: {},
},
{
application: "waku-rln-relay",
version: "01234567890abcdef",
appIdentifier: 12,
credentials: {},
},
{
application: "waku-rln-relay",
version: "01234567890abcdef",
appIdentifier: "12",
credentials: [],
},
{
application: "waku-rln-relay",
version: "01234567890abcdef",
appIdentifier: "12",
credentials: 123,
},
{
application: "waku-rln-relay",
version: "01234567890abcdef",
appIdentifier: "12",
credentials: "123",
},
{
application: "waku-rln-relay",
version: "01234567890abcdef",
appIdentifier: "12",
credentials: {
hash: {
invalid: "here",
},
},
},
].map((options) => {
it("should fail to create store from invalid object", () => {
expect(() => Keystore.fromObject(options as any)).to.throw(
"Invalid object, does not match Nwaku Keystore format."
);
});
});
it("shoud create store from valid object", () => {
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
expect(store.toObject()).to.deep.eq(NWAKU_KEYSTORE);
});
it("should fail to create store from invalid string", () => {
expect(Keystore.fromString("/asdq}")).to.eq(null);
expect(Keystore.fromString('{ "name": "it" }')).to.eq(null);
});
it("shoud create store from valid string", async () => {
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
expect(store.toObject()).to.deep.eq(NWAKU_KEYSTORE);
});
it("should convert keystore to string", async () => {
let store = Keystore.create();
expect(store.toString()).to.eq(
JSON.stringify({
application: "waku-rln-relay",
appIdentifier: "01234567890abcdef",
version: "0.2",
credentials: {},
})
);
store = Keystore.fromObject(NWAKU_KEYSTORE as any);
expect(store.toString()).to.eq(JSON.stringify(NWAKU_KEYSTORE));
});
it("shoud add / read new credentials", async () => {
const expectedHash =
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548";
const identity = {
IDTrapdoor: [
211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244,
216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176,
126, 9,
],
IDNullifier: [
238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9,
178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81,
42,
],
IDSecretHash: [
150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146,
101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124,
187, 4,
],
IDCommitment: [
112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229,
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15,
],
IDCommitmentBigInt: buildBigIntFromUint8Array(
new Uint8Array([
112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229,
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171,
15,
])
),
} as unknown as IdentityCredential;
const membership = {
chainId: "0xAA36A7",
treeIndex: 8,
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71",
} as unknown as MembershipInfo;
const store = Keystore.create();
const hash = await store.addCredential(
{ identity, membership },
DEFAULT_PASSWORD
);
expect(hash).to.eq(expectedHash);
const actualCredentials = await store.readCredential(
expectedHash,
DEFAULT_PASSWORD
);
expect(actualCredentials).to.deep.equalInAnyOrder({
identity,
membership,
});
});
it("shoud fail to add credentials if already exist", async () => {
const identity = {
IDTrapdoor: [
211, 23, 66, 42, 179, 130, 131, 111, 201, 205, 244, 34, 27, 238, 244,
216, 131, 240, 188, 45, 193, 172, 4, 168, 225, 225, 43, 197, 114, 176,
126, 9,
],
IDNullifier: [
238, 168, 239, 65, 73, 63, 105, 19, 132, 62, 213, 205, 191, 255, 209, 9,
178, 155, 239, 201, 131, 125, 233, 136, 246, 217, 9, 237, 55, 89, 81,
42,
],
IDSecretHash: [
150, 54, 194, 28, 18, 216, 138, 253, 95, 139, 120, 109, 98, 129, 146,
101, 41, 194, 36, 36, 96, 152, 152, 89, 151, 160, 118, 15, 222, 124,
187, 4,
],
IDCommitment: [
112, 216, 27, 89, 188, 135, 203, 19, 168, 211, 117, 13, 231, 135, 229,
58, 94, 20, 246, 8, 33, 65, 238, 37, 112, 97, 65, 241, 255, 93, 171, 15,
],
} as unknown as IdentityCredential;
const membership = {
chainId: "0xAA36A7",
treeIndex: 8,
address: "0x8e1F3742B987d8BA376c0CBbD7357fE1F003ED71",
} as unknown as MembershipInfo;
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
try {
await store.addCredential({ identity, membership }, DEFAULT_PASSWORD);
} catch (e) {
expect((e as Error).message).to.eq(
"Credential already exists in the store."
);
}
});
it("shoud fail to read credentials with wrong password", async () => {
const expectedHash =
"9DB2B4718A97485B9F70F68D1CC19F4E10F0B4CE943418838E94956CB8E57548";
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
try {
await store.readCredential(expectedHash, "wrong-password");
} catch (e) {
expect((e as Error).message).to.eq("Password is invalid.");
}
});
it("shoud fail to read missing credentials", async () => {
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
const result = await store.readCredential("wrong-hash", "wrong-password");
expect(result).to.eq(null);
});
});

295
src/keystore/keystore.ts Normal file
View File

@ -0,0 +1,295 @@
import type {
ICipherModule,
IKeystore as IEipKeystore,
IPbkdf2KdfModule,
} from "@chainsafe/bls-keystore";
import { create as createEipKeystore } from "@chainsafe/bls-keystore";
import { sha256 } from "ethereum-cryptography/sha256";
import {
bytesToHex,
bytesToUtf8,
utf8ToBytes,
} from "ethereum-cryptography/utils";
import _ from "lodash";
import { v4 as uuidV4 } from "uuid";
import { buildBigIntFromUint8Array } from "../byte_utils.js";
import type { IdentityCredential } from "../rln.js";
import { decryptEipKeystore, keccak256Checksum } from "./cipher.js";
import { isCredentialValid, isKeystoreValid } from "./schema_validator.js";
import type {
Keccak256Hash,
MembershipHash,
MembershipInfo,
Password,
Sha256Hash,
} from "./types.js";
type NwakuCredential = {
crypto: {
cipher: ICipherModule["function"];
cipherparams: ICipherModule["params"];
ciphertext: ICipherModule["message"];
kdf: IPbkdf2KdfModule["function"];
kdfparams: IPbkdf2KdfModule["params"];
mac: Sha256Hash;
};
};
// examples from nwaku
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/tests/test_waku_keystore.nim#L43
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keystore.nim#L154C35-L154C38
// important: each credential has it's own password
// important: not compatible with https://eips.ethereum.org/EIPS/eip-2335
interface NwakuKeystore {
application: string;
version: string;
appIdentifier: string;
credentials: {
[key: MembershipHash]: NwakuCredential;
};
}
type KeystoreCreateOptions = {
application?: string;
version?: string;
appIdentifier?: string;
};
type IdentityOptions = {
identity: IdentityCredential;
membership: MembershipInfo;
};
export class Keystore {
private data: NwakuKeystore;
private constructor(options: KeystoreCreateOptions | NwakuKeystore) {
this.data = Object.assign(
{
application: "waku-rln-relay",
appIdentifier: "01234567890abcdef",
version: "0.2",
credentials: {},
},
options
);
}
public static create(options: KeystoreCreateOptions = {}): Keystore {
return new Keystore(options);
}
public static fromString(str: string): Keystore | null {
try {
const obj = JSON.parse(str);
if (!Keystore.isValidNwakuStore(obj)) {
throw Error("Invalid string, does not match Nwaku Keystore format.");
}
return new Keystore(obj);
} catch (err) {
console.error("Cannot create Keystore from string:", err);
return null;
}
}
public static fromObject(obj: NwakuKeystore): Keystore {
if (!Keystore.isValidNwakuStore(obj)) {
throw Error("Invalid object, does not match Nwaku Keystore format.");
}
return new Keystore(obj);
}
public async addCredential(
options: IdentityOptions,
password: Password
): Promise<MembershipHash> {
const membershipHash: MembershipHash = Keystore.computeMembershipHash(
options.membership
);
if (this.data.credentials[membershipHash]) {
throw Error("Credential already exists in the store.");
}
// these are not important
const stubPath = "/stub/path";
const stubPubkey = new Uint8Array([0]);
const secret = Keystore.fromIdentityToBytes(options);
const eipKeystore = await createEipKeystore(
password,
secret,
stubPubkey,
stubPath
);
// need to re-compute checksum since nwaku uses keccak256 instead of sha256
const checksum = await keccak256Checksum(password, eipKeystore);
const nwakuCredential = Keystore.fromEipToCredential(eipKeystore, checksum);
this.data.credentials[membershipHash] = nwakuCredential;
return membershipHash;
}
public async readCredential(
membershipHash: MembershipHash,
password: Password
): Promise<null | IdentityOptions> {
const nwakuCredential = this.data.credentials[membershipHash];
if (!nwakuCredential) {
return null;
}
const eipKeystore = Keystore.fromCredentialToEip(nwakuCredential);
const bytes = await decryptEipKeystore(password, eipKeystore);
return Keystore.fromBytesToIdentity(bytes);
}
public removeCredential(hash: MembershipHash): void {
if (!this.data.credentials[hash]) {
return;
}
delete this.data.credentials[hash];
}
public toString(): string {
return JSON.stringify(this.data);
}
public toObject(): NwakuKeystore {
return this.data;
}
private static isValidNwakuStore(obj: unknown): boolean {
if (!isKeystoreValid(obj)) {
return false;
}
const areCredentialsValid = Object.values(_.get(obj, "credentials", {}))
.map((c) => isCredentialValid(c))
.every((v) => v);
return areCredentialsValid;
}
private static fromCredentialToEip(
credential: NwakuCredential
): IEipKeystore {
const nwakuCrypto = credential.crypto;
const eipCrypto: IEipKeystore["crypto"] = {
kdf: {
function: nwakuCrypto.kdf,
params: nwakuCrypto.kdfparams,
message: "",
},
cipher: {
function: nwakuCrypto.cipher,
params: nwakuCrypto.cipherparams,
message: nwakuCrypto.ciphertext,
},
checksum: {
// @chainsafe/bls-keystore supports only sha256
// but nwaku uses keccak256
// https://github.com/waku-org/nwaku/blob/25d6e52e3804d15f9b61bc4cc6dd448540c072a1/waku/waku_keystore/keyfile.nim#L367
function: "sha256",
params: {},
message: nwakuCrypto.mac,
},
};
return {
version: 4,
uuid: uuidV4(),
description: undefined,
path: "safe to ignore, not important for decrypt",
pubkey: "safe to ignore, not important for decrypt",
crypto: eipCrypto,
};
}
private static fromEipToCredential(
eipKeystore: IEipKeystore,
checksum: Keccak256Hash
): NwakuCredential {
const eipCrypto = eipKeystore.crypto;
const eipKdf = eipCrypto.kdf as IPbkdf2KdfModule;
return {
crypto: {
cipher: eipCrypto.cipher.function,
cipherparams: eipCrypto.cipher.params,
ciphertext: eipCrypto.cipher.message,
kdf: eipKdf.function,
kdfparams: eipKdf.params,
// @chainsafe/bls-keystore generates only sha256
// but nwaku uses keccak256
// https://github.com/waku-org/nwaku/blob/25d6e52e3804d15f9b61bc4cc6dd448540c072a1/waku/waku_keystore/keyfile.nim#L367
mac: checksum,
},
};
}
private static fromBytesToIdentity(
bytes: Uint8Array
): null | IdentityOptions {
try {
const str = bytesToUtf8(bytes);
const obj = JSON.parse(str);
// TODO: add runtime validation of nwaku credentials
return {
identity: {
IDCommitment: _.get(obj, "identityCredential.idCommitment"),
IDTrapdoor: _.get(obj, "identityCredential.idTrapdoor"),
IDNullifier: _.get(obj, "identityCredential.idNullifier"),
IDCommitmentBigInt: buildBigIntFromUint8Array(
new Uint8Array(_.get(obj, "identityCredential.idCommitment", []))
),
IDSecretHash: _.get(obj, "identityCredential.idSecretHash"),
},
membership: {
treeIndex: _.get(obj, "treeIndex"),
chainId: _.get(obj, "membershipContract.chainId"),
address: _.get(obj, "membershipContract.address"),
},
};
} catch (err) {
console.error("Cannot parse bytes to Nwaku Credentials:", err);
return null;
}
}
// follows nwaku implementation
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111
private static computeMembershipHash(info: MembershipInfo): MembershipHash {
return bytesToHex(
sha256(utf8ToBytes(`${info.chainId}${info.address}${info.treeIndex}`))
).toUpperCase();
}
// follows nwaku implementation
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
private static fromIdentityToBytes(options: IdentityOptions): Uint8Array {
return utf8ToBytes(
JSON.stringify({
treeIndex: options.membership.treeIndex,
identityCredential: {
idCommitment: options.identity.IDCommitment,
idNullifier: options.identity.IDNullifier,
idSecretHash: options.identity.IDSecretHash,
idTrapdoor: options.identity.IDTrapdoor,
},
membershipContract: {
chainId: options.membership.chainId,
address: options.membership.address,
},
})
);
}
}

View File

@ -0,0 +1,8 @@
/* eslint eslint-comments/no-unlimited-disable: "off" */
// This file was generated by /scripts/schema-validation-codegen.ts
// Do not modify this file by hand.
/* eslint-disable */
// @ts-ignore
"use strict";export const Keystore = validate11;const schema12 = {"type":"object","properties":{"credentials":{"type":"object"},"appIdentifier":{"type":"string"},"version":{"type":"string"},"application":{"type":"string"}},"required":["application","appIdentifier","credentials","version"]};function validate11(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if(errors === 0){if(data && typeof data == "object" && !Array.isArray(data)){let missing0;if(((((data.application === undefined) && (missing0 = "application")) || ((data.appIdentifier === undefined) && (missing0 = "appIdentifier"))) || ((data.credentials === undefined) && (missing0 = "credentials"))) || ((data.version === undefined) && (missing0 = "version"))){validate11.errors = [{instancePath,schemaPath:"#/required",keyword:"required",params:{missingProperty: missing0},message:"must have required property '"+missing0+"'"}];return false;}else {if(data.credentials !== undefined){let data0 = data.credentials;const _errs1 = errors;if(!(data0 && typeof data0 == "object" && !Array.isArray(data0))){validate11.errors = [{instancePath:instancePath+"/credentials",schemaPath:"#/properties/credentials/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}var valid0 = _errs1 === errors;}else {var valid0 = true;}if(valid0){if(data.appIdentifier !== undefined){const _errs3 = errors;if(typeof data.appIdentifier !== "string"){validate11.errors = [{instancePath:instancePath+"/appIdentifier",schemaPath:"#/properties/appIdentifier/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs3 === errors;}else {var valid0 = true;}if(valid0){if(data.version !== undefined){const _errs5 = errors;if(typeof data.version !== "string"){validate11.errors = [{instancePath:instancePath+"/version",schemaPath:"#/properties/version/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs5 === errors;}else {var valid0 = true;}if(valid0){if(data.application !== undefined){const _errs7 = errors;if(typeof data.application !== "string"){validate11.errors = [{instancePath:instancePath+"/application",schemaPath:"#/properties/application/type",keyword:"type",params:{type: "string"},message:"must be string"}];return false;}var valid0 = _errs7 === errors;}else {var valid0 = true;}}}}}}else {validate11.errors = [{instancePath,schemaPath:"#/type",keyword:"type",params:{type: "object"},message:"must be object"}];return false;}}validate11.errors = vErrors;return errors === 0;}

View File

@ -0,0 +1,34 @@
import { Credential as _validateCredentialGenerated } from "./credential_validation_generated.js";
import { Keystore as _validateKeystoreGenerated } from "./keystore_validation_generated.js";
type ErrorObject = {
instancePath: string;
schemaPath: string;
keyword: string;
params: object;
message: string;
};
type ValidatorFn = ((data: unknown) => boolean) & { errors: ErrorObject[] };
const _validateKeystore = _validateKeystoreGenerated as ValidatorFn;
const _validateCredential = _validateCredentialGenerated as ValidatorFn;
function schemaValidationErrors(
validator: ValidatorFn,
data: unknown
): ErrorObject[] | null {
const validated = validator(data);
if (validated) {
return null;
}
return validator.errors;
}
export function isKeystoreValid(keystore: unknown): boolean {
return !schemaValidationErrors(_validateKeystore, keystore);
}
export function isCredentialValid(credential: unknown): boolean {
return !schemaValidationErrors(_validateCredential, credential);
}

12
src/keystore/types.ts Normal file
View File

@ -0,0 +1,12 @@
export type MembershipHash = string;
export type Sha256Hash = string;
export type Keccak256Hash = string;
export type Password = string | Uint8Array;
// see reference
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111
export type MembershipInfo = {
chainId: number;
address: string;
treeIndex: number;
};

View File

@ -2,7 +2,7 @@ import type { IRateLimitProof } from "@waku/interfaces";
import { default as init } from "@waku/zerokit-rln-wasm";
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { writeUIntLE } from "./byte_utils.js";
import { buildBigIntFromUint8Array, writeUIntLE } from "./byte_utils.js";
import { dateToEpoch, epochIntToBytes } from "./epoch.js";
import verificationKey from "./resources/verification_key.js";
import * as wc from "./witness_calculator.js";
@ -27,16 +27,6 @@ function concatenate(...input: Uint8Array[]): Uint8Array {
return result;
}
/**
* Transforms Uint8Array into BigInt
* @param array: Uint8Array
* @returns BigInt
*/
function buildBigIntFromUint8Array(array: Uint8Array): bigint {
const dataView = new DataView(array.buffer);
return dataView.getBigUint64(0, true);
}
const stringEncoder = new TextEncoder();
const DEPTH = 20;