@oraichain/multifactors.js
Version:
Handle communication with multifactors nodes
450 lines (444 loc) • 16 kB
JavaScript
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