@waku/rln
Version:
RLN (Rate Limiting Nullifier) implementation for Waku
237 lines • 9.86 kB
JavaScript
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