UNPKG

@oraichain/multifactors.js

Version:

Handle communication with multifactors nodes

336 lines (294 loc) 11.4 kB
import { decrypt, generatePrivate, getPublic } from "@toruslabs/eccrypto"; import { Data, generateJsonRPCObject, post, setEmbedHost } from "@toruslabs/http-helpers"; import BN from "bn.js"; import { ec as EC } from "elliptic"; import stringify from "json-stable-stringify"; import { CommitmentRequestResult, GetOrSetNonceResult, JRPCResponse, MetadataParams, PrivateKeyResponse, RetrieveSharesParams, RetrieveSharesResponse, SetCustomKeyOptions, ShareCommitmentParams, ShareRequestParams, ShareRequestResponse, MultifactorsCtorOptions, } from "./interfaces"; import log from "./loglevel"; import { allSettled, GetOrSetNonceError, keccak256, thresholdSame } from "./utils"; // Implement threshold logic wrappers around public APIs // of Multifactors nodes to handle malicious node responses class Multifactors { public metadataHost: string; public serverTimeOffset: number; public enableOneKey: boolean; public network: string; protected ec: EC; public blsdkg: { init: () => Promise<void>; interpolate: (indexes: Uint8Array[], shares: Uint8Array[]) => Uint8Array; get_pk: (privKey: Uint8Array) => Uint8Array; }; constructor({ enableOneKey = false, metadataHost = "https://metadata.social-login.orai.io", serverTimeOffset = 0, network = "mainnet", blsdkg, }: MultifactorsCtorOptions = {}) { this.ec = new EC("secp256k1"); this.metadataHost = metadataHost; this.enableOneKey = enableOneKey; this.serverTimeOffset = serverTimeOffset || 0; // ms this.network = network; this.blsdkg = blsdkg; } static enableLogging(v = true): void { if (v) log.enableAll(); else log.disableAll(); } static setEmbedHost(embedHost: string): void { setEmbedHost(embedHost); } static isGetOrSetNonceError(err: unknown): boolean { return err instanceof GetOrSetNonceError; } /* istanbul ignore next @preserve */ async setCustomKey({ privKeyHex, metadataNonce, oraiKeyHex, customKeyHex }: SetCustomKeyOptions): Promise<void> { let oraiKey: BN; if (oraiKeyHex) { oraiKey = new BN(oraiKeyHex, 16); } else { const privKey = new BN(privKeyHex as string, 16); oraiKey = privKey.sub(metadataNonce as BN).umod(this.ec.curve.n); } const customKey = new BN(customKeyHex, 16); const newMetadataNonce = customKey.sub(oraiKey).umod(this.ec.curve.n); const data = this.generateMetadataParams(newMetadataNonce.toString(16), oraiKey); await this.setMetadata(data); } async retrieveShares(args: RetrieveSharesParams): Promise<PrivateKeyResponse> { if (!this.blsdkg) throw new Error("Undefined blsdkg"); const { sharesIndexes, shares, thresholdPublicKey } = await this._retrieveShares(args); const privateKey: Uint8Array = await new Promise((resolve, reject) => { this.blsdkg .init() .then(() => { const blsPrivKey = this.blsdkg.interpolate(sharesIndexes.slice(0, 3), shares.slice(0, 3)); const blsPubKey = Buffer.from(this.blsdkg.get_pk(blsPrivKey)); log.info(blsPubKey.toString("hex")); log.info("ThresholdPubkey", thresholdPublicKey); if (blsPubKey.toString("hex") !== thresholdPublicKey) { reject("Public key not same"); } resolve(blsPrivKey); }) .catch(() => reject(new Error("Blsdkg process has been failed"))); }); return { privKey: Buffer.from(privateKey).toString("hex"), }; } async retrieveSharesMobile(args: RetrieveSharesParams): Promise<RetrieveSharesResponse> { return this._retrieveShares(args); } async _retrieveShares({ typeOfLogin, endpoints, verifierParams, idToken, extraParams }: RetrieveSharesParams): Promise<RetrieveSharesResponse> { // generate temporary private and public key that is used to secure receive shares const tmpKey = generatePrivate(); const pubKey = getPublic(tmpKey).toString("hex"); const pubKeyX = pubKey.slice(2, 66); const pubKeyY = pubKey.slice(66); const tokenCommitment = keccak256(idToken); const commitmentResponses = await this.getSharesCommitment({ endpoints, tokencommitment: tokenCommitment.slice(2), temppubx: pubKeyX, temppuby: pubKeyY, }); if (commitmentResponses.length < ~~(endpoints.length / 2) + 1) { throw new Error("Insufficient commitment responses"); } const shareResponsesAndIndex = await this.getShareRequestResponseWithIndex({ endpoints, typeOfLogin, verifierParams, idtoken: idToken, nodesignatures: commitmentResponses, extraParams, }); if (shareResponsesAndIndex.length < ~~(endpoints.length / 2) + 1) { throw new Error("Insufficient share responses"); } const thresholdPublicKey = thresholdSame( shareResponsesAndIndex.map((x) => x && x.share.PublicKey), ~~(endpoints.length / 2) + 1 ); if (!thresholdPublicKey) throw new Error("Threshold pubkey is undefined"); const sharePromises = shareResponsesAndIndex.map(async (x): Promise<void | Buffer> => { const key = x.share; if (key.Metadata) { const metadata = { ephemPublicKey: Buffer.from(key.Metadata.ephemPublicKey, "hex"), iv: Buffer.from(key.Metadata.iv, "hex"), mac: Buffer.from(key.Metadata.mac, "hex"), }; return decrypt(tmpKey, { ...metadata, ciphertext: Buffer.from(key.Share, "base64"), }).catch((err) => log.debug("share decryption", err)); } return Promise.resolve(Buffer.from(key.Share, "hex")); }); const sharesIndexes = shareResponsesAndIndex.map((x) => Buffer.from([x.index + 1])); const shareResolved = (await Promise.all(sharePromises)).filter((x) => x) as Buffer[]; return { shares: shareResolved, sharesIndexes, thresholdPublicKey, }; } async getSharesCommitment({ endpoints, tokencommitment, temppubx, temppuby }: Readonly<ShareCommitmentParams>): Promise<CommitmentRequestResult[]> { const commitmentRequestPromises = endpoints.map((endpoint) => post<JRPCResponse<CommitmentRequestResult>>( endpoint, generateJsonRPCObject("CommitmentRequest", { tokencommitment, temppubx, temppuby, }) ) ); const commitmentResponses = await allSettled<JRPCResponse<CommitmentRequestResult>>(commitmentRequestPromises); const fulfillRespone = commitmentResponses.filter((x) => x.status === "fulfilled"); return fulfillRespone.map((x) => (x as PromiseFulfilledResult<JRPCResponse<CommitmentRequestResult>>).value.result).filter((x) => x); } async getShareRequestResponseWithIndex({ endpoints, typeOfLogin, verifierParams, nodesignatures, idtoken, extraParams, }: ShareRequestParams): Promise<{ share: ShareRequestResponse; index: number }[]> { const shareRequestPromises = endpoints.map((endpoint) => post<JRPCResponse<ShareRequestResponse>>( endpoint, generateJsonRPCObject("ShareRequest", { ...verifierParams, typeOfLogin, idtoken, nodesignatures, ...extraParams, }) ) ); const shareResponses = await allSettled<JRPCResponse<ShareRequestResponse>>(shareRequestPromises); const fulfillRespones = shareResponses.map((x, index) => { if (x.status === "rejected") return undefined; return { share: x.value.result, index, }; }); return fulfillRespones.filter((x) => x); } /* istanbul ignore next @preserve */ async getPrivKey(key: BN): Promise<PrivateKeyResponse> { const privateKey = key; if (!privateKey) throw new Error("Invalid private key returned"); return { privKey: privateKey.toString("hex", 64), }; } /* istanbul ignore next @preserve */ async getMetadata(data: Omit<MetadataParams, "set_data" | "signature">, options: RequestInit = {}): Promise<BN> { try { const metadataResponse = await post<{ message?: string }>(`${this.metadataHost}/get`, data, options, { useAPIKey: true }); if (!metadataResponse || !metadataResponse.message) { return new BN(0); } return new BN(metadataResponse.message, 16); // nonce } catch (error) { log.error("get metadata error", error); return new BN(0); } } /* istanbul ignore next @preserve */ generateMetadataParams(message: string, privateKey: BN): MetadataParams { const key = this.ec.keyFromPrivate(privateKey.toString("hex", 64)); const setData = { data: message, timestamp: new BN(~~(this.serverTimeOffset + Date.now() / 1000)).toString(16), }; const sig = key.sign(keccak256(stringify(setData)).slice(2)); return { pub_key_X: key.getPublic().getX().toString("hex"), pub_key_Y: key.getPublic().getY().toString("hex"), set_data: setData, signature: Buffer.from(sig.r.toString(16, 64) + sig.s.toString(16, 64) + new BN("").toString(16, 2), "hex").toString("base64"), }; } /* istanbul ignore next @preserve */ async setMetadata(data: MetadataParams, options: RequestInit = {}): Promise<string> { try { const metadataResponse = await post<{ message: string }>(`${this.metadataHost}/set`, data, options, { useAPIKey: true }); return metadataResponse.message; // IPFS hash } catch (error) { log.error("set metadata error", error); return ""; } } /* istanbul ignore next @preserve */ lagrangeInterpolation(shares: BN[], nodeIndex: BN[]): BN | null { if (shares.length !== nodeIndex.length) { return null; } let secret = new BN(0); for (let i = 0; i < shares.length; i += 1) { let upper = new BN(1); let lower = new BN(1); for (let j = 0; j < shares.length; j += 1) { if (i !== j) { upper = upper.mul(nodeIndex[j].neg()); upper = upper.umod(this.ec.curve.n); let temp = nodeIndex[i].sub(nodeIndex[j]); temp = temp.umod(this.ec.curve.n); lower = lower.mul(temp).umod(this.ec.curve.n); } } let delta = upper.mul(lower.invm(this.ec.curve.n)).umod(this.ec.curve.n); delta = delta.mul(shares[i]).umod(this.ec.curve.n); secret = secret.add(delta); } return secret.umod(this.ec.curve.n); } /* istanbul ignore next @preserve */ async getOrSetNonce(X: string, Y: string, privKey?: BN, getOnly = false): Promise<GetOrSetNonceResult> { let data: Data; const msg = getOnly ? "getNonce" : "getOrSetNonce"; if (privKey) { data = this.generateMetadataParams(msg, privKey); } else { data = { pub_key_X: X, pub_key_Y: Y, set_data: { data: msg }, }; } return post<GetOrSetNonceResult>(`${this.metadataHost}/get_or_set_nonce`, data, undefined, { useAPIKey: true }); } /* istanbul ignore next @preserve */ async getNonce(X: string, Y: string, privKey?: BN): Promise<GetOrSetNonceResult> { return this.getOrSetNonce(X, Y, privKey, true); } /* istanbul ignore next @preserve */ getPostboxKeyFrom1OutOf1(privKey: string, nonce: string): string { const privKeyBN = new BN(privKey, 16); const nonceBN = new BN(nonce, 16); return privKeyBN.sub(nonceBN).umod(this.ec.curve.n).toString("hex"); } } export default Multifactors;