UNPKG

@oraichain/multifactors.js

Version:

Handle communication with multifactors nodes

450 lines (444 loc) 16 kB
import _defineProperty from '@babel/runtime/helpers/defineProperty'; import { generatePrivate, getPublic, decrypt } from '@toruslabs/eccrypto'; import { post, generateJsonRPCObject, setEmbedHost } from '@toruslabs/http-helpers'; import BN from 'bn.js'; import { ec } from 'elliptic'; import JsonStringify from 'json-stable-stringify'; import loglevel from 'loglevel'; import createKeccakHash from 'keccak'; function capitalizeFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } class SomeError extends Error { constructor(_ref) { let { errors, responses, predicate } = _ref; super("Unable to resolve enough promises."); _defineProperty(this, "errors", void 0); _defineProperty(this, "responses", void 0); _defineProperty(this, "predicate", void 0); this.errors = errors; this.responses = responses; this.predicate = predicate; } } const Some = (promises, predicate) => new Promise((resolve, reject) => { let finishedCount = 0; const sharedState = { resolved: false }; const errorArr = new Array(promises.length).fill(undefined); const resultArr = new Array(promises.length).fill(undefined); let predicateError; return promises.forEach((x, index) => { return x.then(resp => { resultArr[index] = resp; return undefined; }).catch(error => { errorArr[index] = error; }).finally(() => { if (sharedState.resolved) return; return predicate(resultArr.slice(0), sharedState).then(data => { sharedState.resolved = true; resolve(data); return undefined; }).catch(error => { // log only the last predicate error predicateError = error; }).finally(() => { finishedCount += 1; if (finishedCount === promises.length) { const errors = Object.values(resultArr.reduce((acc, z) => { if (z) { const { id, error } = z; if (error?.data?.length > 0) { if (error.data.startsWith("Error occurred while verifying params")) acc[id] = capitalizeFirstLetter(error.data);else acc[id] = error.data; } } return acc; }, {})); if (errors.length > 0) { // Format-able errors const msg = errors.length > 1 ? `\n${errors.map(it => `• ${it}`).join("\n")}` : errors[0]; reject(new Error(msg)); } else { reject(new SomeError({ errors: errorArr, responses: resultArr, predicate: predicateError?.message || predicateError })); } } }); }); }); }); const log = loglevel.getLogger("orai.js"); log.disableAll(); class GetOrSetNonceError extends Error {} const kCombinations = (s, k) => { let set = s; if (typeof set === "number") { set = Array.from({ length: set }, (_, i) => i); } if (k > set.length || k <= 0) { return []; } if (k === set.length) { return [set]; } if (k === 1) { return set.reduce((acc, cur) => [...acc, [cur]], []); } const combs = []; let tailCombs = []; for (let i = 0; i <= set.length - k + 1; i += 1) { tailCombs = kCombinations(set.slice(i + 1), k - 1); for (let j = 0; j < tailCombs.length; j += 1) { combs.push([set[i], ...tailCombs[j]]); } } return combs; }; const thresholdSame = (arr, t) => { const hashMap = {}; for (let i = 0; i < arr.length; i += 1) { const str = JsonStringify(arr[i]); hashMap[str] = hashMap[str] ? hashMap[str] + 1 : 1; if (hashMap[str] === t) { return arr[i]; } } return undefined; }; const keyLookup = async (endpoints, verifier, verifierId) => { const lookupPromises = endpoints.map(x => post(x, generateJsonRPCObject("VerifierLookupRequest", { verifier, verifier_id: verifierId.toString() })).catch(err => log.error("lookup request failed", err))); return Some(lookupPromises, lookupResults => { const lookupShares = lookupResults.filter(x1 => x1); const errorResult = thresholdSame(lookupShares.map(x2 => x2 && x2.error), ~~(endpoints.length / 2) + 1); const keyResult = thresholdSame(lookupShares.map(x3 => x3 && x3.result), ~~(endpoints.length / 2) + 1); if (keyResult || errorResult) { return Promise.resolve({ keyResult, errorResult }); } return Promise.reject(new Error(`invalid results ${JSON.stringify(lookupResults)}`)); }); }; const waitKeyLookup = (endpoints, verifierId, timeout) => new Promise((resolve, reject) => { setTimeout(() => { keyLookup(endpoints, "test", verifierId).then(resolve).catch(reject); // TODO: remove test }, timeout); }); function keccak256(a) { const hash = createKeccakHash("keccak256").update(a).digest().toString("hex"); return `0x${hash}`; } const allSettled = Promise.allSettled.bind(Promise) || (promises => Promise.all(promises.map(p => p.then(value => ({ status: "fulfilled", value })).catch(reason => ({ status: "rejected", reason }))))); function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } // Implement threshold logic wrappers around public APIs // of Multifactors nodes to handle malicious node responses class Multifactors { constructor() { let { enableOneKey = false, metadataHost = "https://metadata.social-login.orai.io", serverTimeOffset = 0, network = "mainnet", blsdkg } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _defineProperty(this, "metadataHost", void 0); _defineProperty(this, "serverTimeOffset", void 0); _defineProperty(this, "enableOneKey", void 0); _defineProperty(this, "network", void 0); _defineProperty(this, "ec", void 0); _defineProperty(this, "blsdkg", void 0); this.ec = new ec("secp256k1"); this.metadataHost = metadataHost; this.enableOneKey = enableOneKey; this.serverTimeOffset = serverTimeOffset || 0; // ms this.network = network; this.blsdkg = blsdkg; } static enableLogging() { let v = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; if (v) log.enableAll();else log.disableAll(); } static setEmbedHost(embedHost) { setEmbedHost(embedHost); } static isGetOrSetNonceError(err) { return err instanceof GetOrSetNonceError; } /* istanbul ignore next @preserve */ async setCustomKey(_ref) { let { privKeyHex, metadataNonce, oraiKeyHex, customKeyHex } = _ref; let oraiKey; if (oraiKeyHex) { oraiKey = new BN(oraiKeyHex, 16); } else { const privKey = new BN(privKeyHex, 16); oraiKey = privKey.sub(metadataNonce).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) { if (!this.blsdkg) throw new Error("Undefined blsdkg"); const { sharesIndexes, shares, thresholdPublicKey } = await this._retrieveShares(args); const privateKey = 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) { return this._retrieveShares(args); } async _retrieveShares(_ref2) { let { typeOfLogin, endpoints, verifierParams, idToken, extraParams } = _ref2; // 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 => { 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, _objectSpread(_objectSpread({}, 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); return { shares: shareResolved, sharesIndexes, thresholdPublicKey }; } async getSharesCommitment(_ref3) { let { endpoints, tokencommitment, temppubx, temppuby } = _ref3; const commitmentRequestPromises = endpoints.map(endpoint => post(endpoint, generateJsonRPCObject("CommitmentRequest", { tokencommitment, temppubx, temppuby }))); const commitmentResponses = await allSettled(commitmentRequestPromises); const fulfillRespone = commitmentResponses.filter(x => x.status === "fulfilled"); return fulfillRespone.map(x => x.value.result).filter(x => x); } async getShareRequestResponseWithIndex(_ref4) { let { endpoints, typeOfLogin, verifierParams, nodesignatures, idtoken, extraParams } = _ref4; const shareRequestPromises = endpoints.map(endpoint => post(endpoint, generateJsonRPCObject("ShareRequest", _objectSpread(_objectSpread({}, verifierParams), {}, { typeOfLogin, idtoken, nodesignatures }, extraParams)))); const shareResponses = await allSettled(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) { 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) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; try { const metadataResponse = await post(`${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, privateKey) { 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(JsonStringify(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) { let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; try { const metadataResponse = await post(`${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, nodeIndex) { 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, Y, privKey) { let getOnly = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; let 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(`${this.metadataHost}/get_or_set_nonce`, data, undefined, { useAPIKey: true }); } /* istanbul ignore next @preserve */ async getNonce(X, Y, privKey) { return this.getOrSetNonce(X, Y, privKey, true); } /* istanbul ignore next @preserve */ getPostboxKeyFrom1OutOf1(privKey, nonce) { const privKeyBN = new BN(privKey, 16); const nonceBN = new BN(nonce, 16); return privKeyBN.sub(nonceBN).umod(this.ec.curve.n).toString("hex"); } } export { GetOrSetNonceError, Some, SomeError, allSettled, Multifactors as default, kCombinations, keccak256, keyLookup, thresholdSame, waitKeyLookup }; //# sourceMappingURL=multifactorsUtils.esm.js.map