From a545530e292d9e08387809b2daddb7a5be491178 Mon Sep 17 00:00:00 2001 From: Sasha Date: Mon, 29 Jan 2024 13:56:40 +0100 Subject: [PATCH] provide ability to use Keystore as a seed for credentials --- src/keystore/index.ts | 4 +-- src/keystore/keystore.ts | 16 ++++++---- src/keystore/types.ts | 17 ++++++++++ src/rln.ts | 67 +++++++++++++++++++++++++++++++++++----- 4 files changed, 88 insertions(+), 16 deletions(-) diff --git a/src/keystore/index.ts b/src/keystore/index.ts index 5dc3b56..e5d4abc 100644 --- a/src/keystore/index.ts +++ b/src/keystore/index.ts @@ -1,5 +1,5 @@ import { Keystore } from "./keystore.js"; -import type { KeystoreEntity } from "./types.js"; +import type { DecryptedCredentials, EncryptedCredentials } from "./types.js"; export { Keystore }; -export type { KeystoreEntity }; +export type { EncryptedCredentials, DecryptedCredentials }; diff --git a/src/keystore/keystore.ts b/src/keystore/keystore.ts index accd33f..5be4712 100644 --- a/src/keystore/keystore.ts +++ b/src/keystore/keystore.ts @@ -76,7 +76,9 @@ export class Keystore { return new Keystore(options); } - public static fromString(str: string): Keystore | null { + // should be valid JSON string that contains Keystore file + // https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keyfile.nim#L376 + public static fromString(str: string): undefined | Keystore { try { const obj = JSON.parse(str); @@ -87,7 +89,7 @@ export class Keystore { return new Keystore(obj); } catch (err) { console.error("Cannot create Keystore from string:", err); - return null; + return; } } @@ -133,11 +135,11 @@ export class Keystore { public async readCredential( membershipHash: MembershipHash, password: Password - ): Promise { + ): Promise { const nwakuCredential = this.data.credentials[membershipHash]; if (!nwakuCredential) { - return null; + return; } const eipKeystore = Keystore.fromCredentialToEip(nwakuCredential); @@ -230,7 +232,9 @@ export class Keystore { }; } - private static fromBytesToIdentity(bytes: Uint8Array): null | KeystoreEntity { + private static fromBytesToIdentity( + bytes: Uint8Array + ): undefined | KeystoreEntity { try { const str = bytesToUtf8(bytes); const obj = JSON.parse(str); @@ -264,7 +268,7 @@ export class Keystore { }; } catch (err) { console.error("Cannot parse bytes to Nwaku Credentials:", err); - return null; + return; } } diff --git a/src/keystore/types.ts b/src/keystore/types.ts index b792920..417eb83 100644 --- a/src/keystore/types.ts +++ b/src/keystore/types.ts @@ -17,3 +17,20 @@ export type KeystoreEntity = { identity: IdentityCredential; membership: MembershipInfo; }; + +export type DecryptedCredentials = KeystoreEntity; + +export type EncryptedCredentials = { + /** + * Valid JSON string that contains Keystore + */ + keystore: string; + /** + * ID of credentials from provided Keystore to use + */ + id: string; + /** + * Password to decrypt credentials provided + */ + password: Password; +}; diff --git a/src/rln.ts b/src/rln.ts index a7c6d93..8bb4efa 100644 --- a/src/rln.ts +++ b/src/rln.ts @@ -14,7 +14,12 @@ import type { RLNDecoder, RLNEncoder } from "./codec.js"; import { createRLNDecoder, createRLNEncoder } from "./codec.js"; import { SEPOLIA_CONTRACT } from "./constants.js"; import { dateToEpoch, epochIntToBytes } from "./epoch.js"; -import type { KeystoreEntity } from "./keystore/index.js"; +import { Keystore } from "./keystore/index.js"; +import type { + DecryptedCredentials, + EncryptedCredentials, +} from "./keystore/index.js"; +import { Password } from "./keystore/types.js"; import { extractMetaMaskSigner } from "./metamask.js"; import verificationKey from "./resources/verification_key.js"; import { RLNContract } from "./rln_contract.js"; @@ -184,7 +189,7 @@ type StartRLNOptions = { * Credentials to use for generating proofs and connecting to the contract and network. * If provided used for validating the network chainId and connecting to registry contract. */ - credentials?: KeystoreEntity; + credentials?: EncryptedCredentials | DecryptedCredentials; }; type RegisterMembershipOptions = @@ -197,7 +202,9 @@ export class RLNInstance { private _contract: undefined | RLNContract; private _signer: undefined | ethers.Signer; - private _credentials: undefined | KeystoreEntity; + + private _keystore: undefined | Keystore; + private _credentials: undefined | DecryptedCredentials; constructor( private zkRLN: number, @@ -212,6 +219,10 @@ export class RLNInstance { return this._signer; } + public get keystore(): undefined | Keystore { + return this._keystore; + } + public async start(options: StartRLNOptions = {}): Promise { if (this.started || this.starting) { return; @@ -220,11 +231,11 @@ export class RLNInstance { this.starting = true; try { - const { signer, credentials, registryAddress } = - await this.determineStartOptions(options); + const { signer, registryAddress } = await this.determineStartOptions( + options + ); this._signer = signer!; - this._credentials = credentials; this._contract = await RLNContract.init(this, { registryAddress: registryAddress!, signer: signer!, @@ -238,9 +249,13 @@ export class RLNInstance { private async determineStartOptions( options: StartRLNOptions ): Promise { - let chainId = options.credentials?.membership.chainId; + const credentials = await this.decryptCredentialsIfNeeded( + options.credentials + ); + + let chainId = credentials?.membership.chainId; const registryAddress = - options.credentials?.membership.address || + credentials?.membership.address || options.registryAddress || SEPOLIA_CONTRACT.address; @@ -264,6 +279,33 @@ export class RLNInstance { }; } + private async decryptCredentialsIfNeeded( + credentials?: EncryptedCredentials | DecryptedCredentials + ): Promise { + if (!credentials) { + return; + } + + if ("identity" in credentials) { + this._credentials = credentials; + return credentials; + } + + const keystore = Keystore.fromString(credentials.keystore); + + if (!keystore) { + throw Error("Failed to start RLN: cannot read Keystore provided."); + } + + this._keystore = keystore; + this._credentials = await keystore.readCredential( + credentials.id, + credentials.password + ); + + return this._credentials; + } + public async registerMembership( options: RegisterMembershipOptions ): Promise { @@ -284,6 +326,15 @@ export class RLNInstance { return this.contract.registerWithIdentity(identity); } + /** + * Changes credentials in use by relying on provided Keystore earlier in rln.start + * @param id: string, hash of credentials to select from Keystore + * @param password: string or bytes to use to decrypt credentials from Keystore + */ + public async useCredentials(id: string, password: Password): Promise { + this._credentials = await this.keystore?.readCredential(id, password); + } + public createEncoder(options: WakuEncoderOptions): RLNEncoder { if (!this._credentials) { throw Error(