UNPKG

@waku/rln

Version:

RLN (Rate Limiting Nullifier) implementation for Waku

237 lines 9.86 kB
import { create as createEipKeystore } from "@chainsafe/bls-keystore"; import { Logger } from "@waku/utils"; import { sha256 } from "ethereum-cryptography/sha256"; import { bytesToHex, bytesToUtf8, utf8ToBytes } from "ethereum-cryptography/utils"; import _ from "lodash"; import { v4 as uuidV4 } from "uuid"; import { BytesUtils } from "../utils/bytes.js"; import { decryptEipKeystore, keccak256Checksum } from "./cipher.js"; import { isCredentialValid, isKeystoreValid } from "./schema_validator.js"; const log = new Logger("waku:rln:keystore"); export class Keystore { data; constructor(options) { this.data = Object.assign({ application: "waku-rln-relay", appIdentifier: "01234567890abcdef", version: "0.2", credentials: {} }, options); } static create(options = {}) { return new Keystore(options); } // should be valid JSON string that contains Keystore file // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keyfile.nim#L376 static fromString(str) { 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) { log.error("Cannot create Keystore from string:", err); return; } } static fromObject(obj) { if (!Keystore.isValidNwakuStore(obj)) { throw Error("Invalid object, does not match Nwaku Keystore format."); } return new Keystore(obj); } async addCredential(options, password) { const 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; } async readCredential(membershipHash, password) { const nwakuCredential = this.data.credentials[membershipHash]; if (!nwakuCredential) { return; } const eipKeystore = Keystore.fromCredentialToEip(nwakuCredential); const bytes = await decryptEipKeystore(password, eipKeystore); return Keystore.fromBytesToIdentity(bytes); } removeCredential(hash) { if (!this.data.credentials[hash]) { return; } delete this.data.credentials[hash]; } toString() { return JSON.stringify(this.data); } toObject() { return this.data; } /** * Read array of hashes of current credentials * @returns array of keys of credentials in current Keystore */ keys() { return Object.keys(this.toObject().credentials || {}); } static isValidNwakuStore(obj) { if (!isKeystoreValid(obj)) { return false; } const areCredentialsValid = Object.values(_.get(obj, "credentials", {})) .map((c) => isCredentialValid(c)) .every((v) => v); return areCredentialsValid; } static fromCredentialToEip(credential) { const nwakuCrypto = credential.crypto; const eipCrypto = { 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 }; } static fromEipToCredential(eipKeystore, checksum) { const eipCrypto = eipKeystore.crypto; const eipKdf = eipCrypto.kdf; 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 } }; } static fromBytesToIdentity(bytes) { try { const str = bytesToUtf8(bytes); const obj = JSON.parse(str); // Little Endian const idCommitmentLE = Keystore.fromArraylikeToBytes(_.get(obj, "identityCredential.idCommitment", [])); const idTrapdoorLE = Keystore.fromArraylikeToBytes(_.get(obj, "identityCredential.idTrapdoor", [])); const idNullifierLE = Keystore.fromArraylikeToBytes(_.get(obj, "identityCredential.idNullifier", [])); const idSecretHashLE = Keystore.fromArraylikeToBytes(_.get(obj, "identityCredential.idSecretHash", [])); // Big Endian const idCommitmentBE = BytesUtils.switchEndianness(idCommitmentLE); const idTrapdoorBE = BytesUtils.switchEndianness(idTrapdoorLE); const idNullifierBE = BytesUtils.switchEndianness(idNullifierLE); const idSecretHashBE = BytesUtils.switchEndianness(idSecretHashLE); const idCommitmentBigInt = BytesUtils.buildBigIntFromUint8ArrayBE(idCommitmentBE); return { identity: { IDCommitment: idCommitmentBE, IDTrapdoor: idTrapdoorBE, IDNullifier: idNullifierBE, IDSecretHash: idSecretHashBE, IDCommitmentBigInt: idCommitmentBigInt }, membership: { treeIndex: _.get(obj, "treeIndex"), chainId: _.get(obj, "membershipContract.chainId"), address: _.get(obj, "membershipContract.address"), rateLimit: _.get(obj, "userMessageLimit") } }; } catch (err) { log.error("Cannot parse bytes to Nwaku Credentials:", err); return; } } static fromArraylikeToBytes(obj) { if (Array.isArray(obj)) { return new Uint8Array(obj); } const bytes = []; let index = 0; let lastElement = obj[index]; while (lastElement !== undefined) { bytes.push(lastElement); index += 1; lastElement = obj[index]; } return new Uint8Array(bytes); } // follows nwaku implementation // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L111 static computeMembershipHash(info) { 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 // IdentityCredential is stored in Big Endian format => switch to Little Endian static fromIdentityToBytes(options) { const { IDCommitment, IDNullifier, IDSecretHash, IDTrapdoor } = options.identity; const idCommitmentLE = BytesUtils.switchEndianness(IDCommitment); const idNullifierLE = BytesUtils.switchEndianness(IDNullifier); const idSecretHashLE = BytesUtils.switchEndianness(IDSecretHash); const idTrapdoorLE = BytesUtils.switchEndianness(IDTrapdoor); // eslint-disable-next-line no-console console.log({ idCommitmentBE: IDCommitment, idCommitmentLE, idNullifierBE: IDNullifier, idNullifierLE, idSecretHashBE: IDSecretHash, idSecretHashLE, idTrapdoorBE: IDTrapdoor, idTrapdoorLE }); return utf8ToBytes(JSON.stringify({ treeIndex: options.membership.treeIndex, identityCredential: { idCommitment: Array.from(idCommitmentLE), idNullifier: Array.from(idNullifierLE), idSecretHash: Array.from(idSecretHashLE), idTrapdoor: Array.from(idTrapdoorLE) }, membershipContract: { chainId: options.membership.chainId, address: options.membership.address }, userMessageLimit: options.membership.rateLimit })); } } //# sourceMappingURL=keystore.js.map