@oraichain/multifactors.js
Version:
Handle communication with multifactors nodes
336 lines (294 loc) • 11.4 kB
text/typescript
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;