diff --git a/package-lock.json b/package-lock.json index cb193ff6..ede80551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@toruslabs/eccrypto": "4.0.0", "@toruslabs/fetch-node-details": "^13.1.1", "@toruslabs/fnd-base": "^13.1.1", + "@toruslabs/http-helpers": "^5.0.0", "@toruslabs/metadata-helpers": "^5.x", "@toruslabs/openlogin-session-manager": "^3.0.0", "@toruslabs/openlogin-utils": "^8.0.0", @@ -31,7 +32,8 @@ "@web3auth/base-provider": "^7.3.1", "bn.js": "^5.2.1", "bowser": "^2.11.0", - "elliptic": "^6.5.4" + "elliptic": "^6.5.4", + "hi-base32": "^0.5.1" }, "devDependencies": { "@babel/register": "^7.23.7", @@ -4506,28 +4508,6 @@ "@babel/runtime": "7.x" } }, - "node_modules/@tkey-mpc/common-types/node_modules/@toruslabs/http-helpers": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@toruslabs/http-helpers/-/http-helpers-6.1.1.tgz", - "integrity": "sha512-bJYOaltRzklzObhRdutT1wau17vXyrCCBKJOeN46F1t99MUXi5udQNeErFOcr9qBsvrq2q67eVBkU5XOeBMX5A==", - "dependencies": { - "lodash.merge": "^4.6.2", - "loglevel": "^1.9.1" - }, - "engines": { - "node": ">=18.x", - "npm": ">=9.x" - }, - "peerDependencies": { - "@babel/runtime": "^7.x", - "@sentry/types": "^7.x" - }, - "peerDependenciesMeta": { - "@sentry/types": { - "optional": true - } - } - }, "node_modules/@tkey-mpc/common-types/node_modules/@toruslabs/torus.js": { "version": "11.0.6", "resolved": "https://registry.npmjs.org/@toruslabs/torus.js/-/torus.js-11.0.6.tgz", @@ -4550,6 +4530,28 @@ "@babel/runtime": "7.x" } }, + "node_modules/@tkey-mpc/common-types/node_modules/@toruslabs/torus.js/node_modules/@toruslabs/http-helpers": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@toruslabs/http-helpers/-/http-helpers-5.0.0.tgz", + "integrity": "sha512-GmezWz9JeF6YyhjLSm+9XDF4YaeICEckY0Jbo43i86SjhfJYgRWqEi63VSiNsaqc/z810Q0FQvEk1TnBRX2tgA==", + "dependencies": { + "lodash.merge": "^4.6.2", + "loglevel": "^1.8.1" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "^7.x", + "@sentry/types": "^7.x" + }, + "peerDependenciesMeta": { + "@sentry/types": { + "optional": true + } + } + }, "node_modules/@tkey-mpc/core": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/@tkey-mpc/core/-/core-9.1.1.tgz", @@ -4775,11 +4777,7 @@ "@toruslabs/http-helpers": "^6.1.0" }, "engines": { - "node": ">=18.x", - "npm": ">=9.x" - }, - "peerDependencies": { - "@babel/runtime": "7.x" + "node": ">=16.0.0" } }, "node_modules/@toruslabs/base-session-manager/node_modules/@toruslabs/http-helpers": { @@ -4791,8 +4789,8 @@ "loglevel": "^1.9.1" }, "engines": { - "node": ">=18.x", - "npm": ">=9.x" + "node": ">=14.17.0", + "npm": ">=6.x" }, "peerDependencies": { "@babel/runtime": "^7.x", @@ -11506,6 +11504,11 @@ "he": "bin/he" } }, + "node_modules/hi-base32": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/hi-base32/-/hi-base32-0.5.1.tgz", + "integrity": "sha512-EmBBpvdYh/4XxsnUybsPag6VikPYnN30td+vQk+GI3qpahVEG9+gTkG0aXVxTjBqQ5T6ijbWIu77O+C5WFWsnA==" + }, "node_modules/hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -15794,7 +15797,7 @@ "parse-json": "^5.2.0" }, "engines": { - "node": ">=14" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/sponsors/d-fischer" diff --git a/package.json b/package.json index 37ab4958..7f29bb10 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@toruslabs/constants": "^13.0.1", "@toruslabs/customauth": "^18.1.0", "@toruslabs/eccrypto": "4.0.0", + "@toruslabs/http-helpers": "^5.0.0", "@toruslabs/fetch-node-details": "^13.1.1", "@toruslabs/fnd-base": "^13.1.1", "@toruslabs/metadata-helpers": "^5.x", @@ -57,7 +58,8 @@ "@web3auth/base-provider": "^7.3.1", "bn.js": "^5.2.1", "bowser": "^2.11.0", - "elliptic": "^6.5.4" + "elliptic": "^6.5.4", + "hi-base32": "^0.5.1" }, "devDependencies": { "@babel/register": "^7.23.7", diff --git a/src/constants.ts b/src/constants.ts index 872851eb..f7c04c00 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -65,3 +65,9 @@ export const CURVE = new EllipticCurve("secp256k1"); export const MAX_FACTORS = 10; // Maximum number of factors that can be added to an account. export const SOCIAL_TKEY_INDEX = 1; + +export const OPS = { + DELETE_FACTOR: "delete_factor", + CREATE_FACTOR: "created_factor", + LOCAL_SIGN: "local_sign", +}; diff --git a/src/helper/authenticator/authenticatorService.ts b/src/helper/authenticator/authenticatorService.ts new file mode 100644 index 00000000..b95c5ae5 --- /dev/null +++ b/src/helper/authenticator/authenticatorService.ts @@ -0,0 +1,128 @@ +import { generatePrivate } from "@toruslabs/eccrypto"; +import { post } from "@toruslabs/http-helpers"; +import { keccak256 } from "@toruslabs/metadata-helpers"; +import { log } from "@web3auth/base"; +import BN from "bn.js"; +import type { ec } from "elliptic"; +import base32 from "hi-base32"; + +import { CURVE } from "../../constants"; +import { IRemoteClientState, Web3AuthMPCCoreKit } from "../../index"; + +export class AuthenticatorService { + private backendUrl: string; + + private coreKitInstance: Web3AuthMPCCoreKit; + + private authenticatorType: string = "authenticator"; + + private factorPub: string = ""; + + private tssIndex: number; + + constructor(params: { backendUrl: string; coreKitInstance: Web3AuthMPCCoreKit; authenticatorType?: string }) { + const { backendUrl } = params; + this.backendUrl = backendUrl; + this.authenticatorType = params.authenticatorType || "authenticator"; + this.coreKitInstance = params.coreKitInstance; + // this.remoteClient = remoteClient || false; + } + + getDescriptionsAndUpdate() { + const arrayOfDescriptions = Object.entries(this.coreKitInstance.getKeyDetails().shareDescriptions).map(([key, value]) => { + const parsedDescription = (value || [])[0] ? JSON.parse(value[0]) : {}; + return { + key, + description: parsedDescription, + }; + }); + + const shareDescriptionsMobile = arrayOfDescriptions.find(({ description }) => description.authenticator === this.authenticatorType); + log.info("shareDescriptionsMobile", shareDescriptionsMobile); + + if (shareDescriptionsMobile) { + this.factorPub = shareDescriptionsMobile.key; + this.tssIndex = shareDescriptionsMobile.description.tssShareIndex; + } + + return shareDescriptionsMobile; + } + + generateSecretKey(): string { + const key = generatePrivate().subarray(0, 20); + return base32.encode(key).toString().replace(/=/g, ""); + } + + async register(privKey: BN, secretKey: string): Promise<{ success: boolean; message?: string }> { + const privKeyPair: ec.KeyPair = CURVE.keyFromPrivate(privKey.toString(16, 64)); + const pubKey = privKeyPair.getPublic(); + const sig = CURVE.sign(keccak256(Buffer.from(secretKey, "utf8")), Buffer.from(privKey.toString(16, 64), "hex")); + + const data = { + pubKey: { + x: pubKey.getX().toString(16, 64), + y: pubKey.getY().toString(16, 64), + }, + sig: { + r: sig.r.toString(16, 64), + s: sig.s.toString(16, 64), + v: new BN(sig.recoveryParam as number).toString(16, 2), + }, + secretKey, + }; + + const resp = await post<{ + success: boolean; + message: string; + }>(`${this.backendUrl}/api/v3/register`, data); + + return resp; + } + + async addRecovery(address: string, code: string, factorKey: BN) { + if (!factorKey) throw new Error("factorKey is not defined"); + if (!address) throw new Error("address is not defined"); + if (!code) throw new Error("code is not defined"); + + const data = { + address, + code, + data: { + // If the verification is complete, we save the factorKey for the user address. + // This factorKey is used to verify the user in the future on a new device and recover tss share. + factorKey: factorKey.toString(16, 64), + }, + }; + + await post(`${this.backendUrl}/api/v3/verify`, data); + } + + async verifyRecovery(address: string, code: string): Promise { + const verificationData = { + address, + code, + }; + + const response = await post<{ data?: Record }>(`${this.backendUrl}/api/v3/verify`, verificationData); + const { data } = response; + return data ? new BN(data.factorKey, "hex") : undefined; + } + + async verifyRemoteSetup(address: string, code: string): Promise { + const verificationData = { + address, + code, + }; + + const response = await post<{ data?: Record }>(`${this.backendUrl}/api/v3/verify_remote`, verificationData); + const { data } = response; + + return { + tssShareIndex: this.tssIndex.toString(), + remoteClientUrl: this.backendUrl, + remoteFactorPub: this.factorPub, + metadataShare: data.metadataShare, + remoteClientToken: data.signature, + }; + } +} diff --git a/src/helper/authenticator/smsService.ts b/src/helper/authenticator/smsService.ts new file mode 100644 index 00000000..d5dc83d4 --- /dev/null +++ b/src/helper/authenticator/smsService.ts @@ -0,0 +1,140 @@ +import { post } from "@toruslabs/http-helpers"; +import { keccak256 } from "@toruslabs/metadata-helpers"; +import { log } from "@web3auth/base"; +import BN from "bn.js"; +import type { ec } from "elliptic"; + +import { CURVE } from "../../constants"; +import { IRemoteClientState } from "../../interfaces"; +import { Web3AuthMPCCoreKit } from "../../mpcCoreKit"; + +export class SmsService { + private backendUrl: string; + + private coreKitInstance: Web3AuthMPCCoreKit; + + private authenticatorType: string = "sms"; + + private factorPub: string = ""; + + private tssIndex: number; + + constructor(params: { backendUrl: string; coreKitInstance: Web3AuthMPCCoreKit; authenticatorType?: string }) { + const { backendUrl } = params; + this.backendUrl = backendUrl; + this.authenticatorType = params.authenticatorType || "sms"; + this.coreKitInstance = params.coreKitInstance; + this.getDescriptionsAndUpdate(); + } + + getDescriptionsAndUpdate() { + const arrayOfDescriptions = Object.entries(this.coreKitInstance.getKeyDetails().shareDescriptions).map(([key, value]) => { + const parsedDescription = (value || [])[0] ? JSON.parse(value[0]) : {}; + return { + key, + description: parsedDescription, + }; + }); + + const shareDescriptionsMobile = arrayOfDescriptions.find(({ description }) => description.authenticator === this.authenticatorType); + log.info("shareDescriptionsMobile", shareDescriptionsMobile); + + if (shareDescriptionsMobile) { + this.factorPub = shareDescriptionsMobile.key; + this.tssIndex = shareDescriptionsMobile.description.tssShareIndex; + } + + return shareDescriptionsMobile; + } + + async register(privKey: BN, number: string): Promise { + const privKeyPair: ec.KeyPair = CURVE.keyFromPrivate(privKey.toString(16, 64)); + const pubKey = privKeyPair.getPublic(); + const sig = CURVE.sign(keccak256(Buffer.from(number, "utf8")), Buffer.from(privKey.toString(16, 64), "hex")); + + const data = { + pubKey: { + x: pubKey.getX().toString(16, 64), + y: pubKey.getY().toString(16, 64), + }, + sig: { + r: sig.r.toString(16, 64), + s: sig.s.toString(16, 64), + v: new BN(sig.recoveryParam as number).toString(16, 2), + }, + number, + }; + + await post<{ + success: boolean; + id_token?: string; + message: string; + }>(`${this.backendUrl}/api/v3/register`, data); + + // this is to send sms to the user instantly after registration. + const startData = { + address: `${pubKey.getX().toString(16, 64)}${pubKey.getY().toString(16, 64)}`, + }; + + // Sends the user sms. + const resp2 = await post<{ success: boolean; code?: string }>(`${this.backendUrl}/api/v3/start`, startData); + // if (resp2.status !== 200) throw new Error("Error sending sms"); + return resp2.code; + } + + async addSmsRecovery(address: string, code: string, factorKey: BN) { + if (!factorKey) throw new Error("factorKey is not defined"); + if (!address) throw new Error("address is not defined"); + + const data = { + address, + code, + data: { + // If the verification is complete, we save the factorKey for the user address. + // This factorKey is used to verify the user in the future on a new device and recover tss share. + factorKey: factorKey.toString(16, 64), + }, + }; + + await post(`${this.backendUrl}/api/v3/verify`, data); + } + + async requestOTP(address: string): Promise { + const startData = { + address, + }; + const resp2 = await post<{ success?: boolean; code?: string }>(`${this.backendUrl}/api/v3/start`, startData); + // eslint-disable-next-line no-console + console.log(resp2); + return resp2.code; + } + + async verifyRecovery(address: string, code: string): Promise { + const verificationData = { + address, + code, + }; + + const response = await post<{ data?: Record }>(`${this.backendUrl}/api/v3/verify`, verificationData); + const { data } = response; + return data ? new BN(data.factorKey, "hex") : undefined; + } + + async verifyRemoteSetup(address: string, code: string): Promise { + const verificationData = { + address, + code, + }; + + const response = await post<{ data?: Record }>(`${this.backendUrl}/api/v3/verify_remote`, verificationData); + const { data } = response; + + return { + tssShareIndex: this.tssIndex.toString(), + remoteClientUrl: this.backendUrl, + remoteFactorPub: this.factorPub, + metadataShare: data.metadataShare, + remoteClientToken: data.signature, + }; + } +} diff --git a/src/index.ts b/src/index.ts index 31dec0b2..53082d66 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ export * from "./constants"; export * from "./helper"; +export * from "./helper/authenticator/authenticatorService"; +export * from "./helper/authenticator/smsService"; export * from "./interfaces"; export * from "./mpcCoreKit"; export * from "./point"; diff --git a/src/interfaces.ts b/src/interfaces.ts index 74c81816..feccb93b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -139,6 +139,13 @@ export interface IdTokenLoginParams { importTssKey?: string; } +export interface IRemoteClientState { + remoteFactorPub: string; + remoteClientUrl: string; + remoteClientToken: string; + metadataShare: string; +} + export interface Web3AuthState { oAuthKey?: string; signatures?: string[]; @@ -147,6 +154,7 @@ export interface Web3AuthState { tssPubKey?: Buffer; accountIndex: number; factorKey?: BN; + remoteClient?: IRemoteClientState; } export interface ICoreKit { @@ -408,6 +416,7 @@ export interface SessionData { tssPubKey: string; signatures: string[]; userInfo: UserInfo; + remoteClient?: IRemoteClientState; } export interface TkeyLocalStoreData { diff --git a/src/mpcCoreKit.ts b/src/mpcCoreKit.ts index aebfe995..7575bc0a 100644 --- a/src/mpcCoreKit.ts +++ b/src/mpcCoreKit.ts @@ -1,6 +1,15 @@ /* eslint-disable @typescript-eslint/member-ordering */ import { createSwappableProxy, SwappableProxy } from "@metamask/swappable-obj-proxy"; -import { BNString, encrypt, getPubKeyPoint, Point as TkeyPoint, SHARE_DELETED, ShareStore, StringifiedType } from "@tkey-mpc/common-types"; +import { + BNString, + encrypt, + EncryptedMessage, + getPubKeyPoint, + Point as TkeyPoint, + SHARE_DELETED, + ShareStore, + StringifiedType, +} from "@tkey-mpc/common-types"; import ThresholdKey, { CoreError, lagrangeInterpolation } from "@tkey-mpc/core"; import { TorusServiceProvider } from "@tkey-mpc/service-provider-torus"; import { ShareSerializationModule } from "@tkey-mpc/share-serialization"; @@ -10,6 +19,7 @@ import { AGGREGATE_VERIFIER, TORUS_METHOD, TorusAggregateLoginResponse, TorusLog import type { UX_MODE_TYPE } from "@toruslabs/customauth/dist/types/utils/enums"; import { generatePrivate } from "@toruslabs/eccrypto"; import { NodeDetailManager } from "@toruslabs/fetch-node-details"; +import { post } from "@toruslabs/http-helpers"; import { keccak256 } from "@toruslabs/metadata-helpers"; import { OpenloginSessionManager } from "@toruslabs/openlogin-session-manager"; import TorusUtils, { TorusKey } from "@toruslabs/torus.js"; @@ -20,6 +30,7 @@ import { EthereumSigningProvider } from "@web3auth-mpc/ethereum-provider"; import BN from "bn.js"; import bowser from "bowser"; +// import { name, version } from "../package.json"; import { CURVE, DEFAULT_CHAIN_CONFIG, @@ -28,6 +39,7 @@ import { FactorKeyTypeShareDescription, FIELD_ELEMENT_HEX_LEN, MAX_FACTORS, + OPS, SOCIAL_TKEY_INDEX, TssShareType, VALID_SHARE_INDICES, @@ -59,6 +71,7 @@ import { deleteFactorAndRefresh, generateFactorKey, generateTSSEndpoints, + getAttestationServerUrls, getHashedPrivateKey, parseToken, scalarBNToBufferSEC1, @@ -90,6 +103,11 @@ export class Web3AuthMPCCoreKit implements ICoreKit { private ready = false; constructor(options: Web3AuthOptions) { + // log.info("======================================================"); + // log.info(`WEB3AUTH SDK : ${name}:${version}`); + + // log.info("======================================================"); + if (!options.chainConfig) options.chainConfig = DEFAULT_CHAIN_CONFIG; if (options.chainConfig.chainNamespace !== CHAIN_NAMESPACES.EIP155) { throw new Error("You must specify a eip155 chain config."); @@ -186,7 +204,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { const { tkey } = this; if (!tkey) return COREKIT_STATUS.NOT_INITIALIZED; if (!tkey.metadata) return COREKIT_STATUS.INITIALIZED; - if (!tkey.privKey || !this.state.factorKey) return COREKIT_STATUS.REQUIRED_SHARE; + if (!tkey.privKey || (!this.state.factorKey && !this.state.remoteClient)) return COREKIT_STATUS.REQUIRED_SHARE; return COREKIT_STATUS.LOGGED_IN; } catch (e) {} return COREKIT_STATUS.NOT_INITIALIZED; @@ -227,7 +245,9 @@ export class Web3AuthMPCCoreKit implements ICoreKit { const tssIndexesBN: BN[] = []; for (let i = 0; i < factorKey.length; i++) { const factorKeyBNInput = new BN(factorKey[i], "hex"); - const { tssIndex, tssShare } = await this.tKey.getTSSShare(factorKeyBNInput); + const { tssIndex, tssShare } = await this.tKey.getTSSShare(factorKeyBNInput, { + accountIndex: this.state.accountIndex, + }); if (tssIndexes.includes(tssIndex)) { // reset instance before throw error await this.init(); @@ -489,6 +509,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { public async inputFactorKey(factorKey: BN): Promise { this.checkReady(); + if (this.state.remoteClient) throw new Error("remoteClient is present, inputFactorKey are not allowed"); try { // input tkey device share when required share > 0 ( or not reconstructed ) // assumption tkey shares will not changed @@ -515,7 +536,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { public getCurrentFactorKey(): IFactorKey { this.checkReady(); - if (!this.state.factorKey) throw new Error("factorKey not present"); + if (!this.state.factorKey && !this.state.remoteClient) throw new Error("factorKey not present"); if (!this.state.tssShareIndex) throw new Error("TSS Share Type (Index) not present"); try { return { @@ -591,7 +612,8 @@ export class Web3AuthMPCCoreKit implements ICoreKit { public getTssFactorPub = (): string[] => { this.checkReady(); - if (!this.state.factorKey) throw new Error("factorKey not present"); + + if (!this.state.factorKey && !this.state.remoteClient) throw new Error("factorKey not present"); const factorPubsList = this.tKey.metadata.factorPubs[this.tKey.tssTag]; return factorPubsList.map((factorPub) => Point.fromTkeyPoint(factorPub).toBufferSEC1(true).toString("hex")); }; @@ -716,7 +738,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { throw new Error(`sessionAuth does not exist ${currentSession}`); } - const signatures = await this.getSigningSignatures(); + const signatures = await this.getSigningSignatures(OPS.LOCAL_SIGN); if (!signatures) { throw new Error(`Signature does not exist ${signatures}`); } @@ -746,17 +768,33 @@ export class Web3AuthMPCCoreKit implements ICoreKit { }; async deleteFactor(factorPub: TkeyPoint, factorKey?: BNString): Promise { - if (!this.state.factorKey) throw new Error("Factor key not present"); + if (!this.state.factorKey && !this.state.remoteClient) throw new Error("Factor key not present"); if (!this.tKey.metadata.factorPubs) throw new Error("Factor pubs not present"); const remainingFactors = this.tKey.metadata.factorPubs[this.tKey.tssTag].length || 0; if (remainingFactors <= 1) throw new Error("Cannot delete last factor"); const fpp = Point.fromTkeyPoint(factorPub); - const stateFpp = Point.fromTkeyPoint(getPubKeyPoint(this.state.factorKey)); - if (fpp.equals(stateFpp)) { - throw new Error("Cannot delete current active factor"); + + const signatures = await this.getSigningSignatures(OPS.DELETE_FACTOR); + if (this.state.remoteClient) { + const remoteStateFpp = this.state.remoteClient.remoteFactorPub; + if (fpp.equals(Point.fromTkeyPoint(getPubKeyPoint(new BN(remoteStateFpp, "hex"))))) { + throw new Error("Cannot delete current active factor"); + } + await deleteFactorAndRefresh( + this.tKey, + factorPub, + new BN(0), // not used in remoteClient + signatures, + this.state.remoteClient + ); + } else { + const stateFpp = Point.fromTkeyPoint(getPubKeyPoint(this.state.factorKey)); + if (fpp.equals(stateFpp)) { + throw new Error("Cannot delete current active factor"); + } + await deleteFactorAndRefresh(this.tKey, factorPub, this.state.factorKey, signatures); } - await deleteFactorAndRefresh(this.tKey, factorPub, this.state.factorKey, this.signatures); const factorPubHex = fpp.toBufferSEC1(true).toString("hex"); const allDesc = this.tKey.metadata.getShareDescription(); const keyDesc = allDesc[factorPubHex]; @@ -818,7 +856,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { public async commitChanges(): Promise { this.checkReady(); - if (!this.state.factorKey) throw new Error("factorKey not present"); + if (!this.state.factorKey && !this.state.remoteClient) throw new Error("factorKey not present"); try { // in case for manualsync = true, _syncShareMetadata will not call syncLocalMetadataTransitions() @@ -840,6 +878,36 @@ export class Web3AuthMPCCoreKit implements ICoreKit { this.tKey.manualSync = manualSync; } + public async setupRemoteClient(params: { + remoteClientUrl: string; + remoteFactorPub: string; + metadataShare: string; + remoteClientToken: string; + tssShareIndex: string; + }): Promise> { + const { remoteClientUrl, remoteFactorPub, metadataShare, remoteClientToken, tssShareIndex } = params; + + const remoteClient = { + remoteClientUrl: remoteClientUrl.at(-1) === "/" ? remoteClientUrl.slice(0, -1) : remoteClientUrl, + remoteFactorPub, + metadataShare, + remoteClientToken, + }; + + const sharestore = ShareStore.fromJSON(JSON.parse(metadataShare)); + this.tkey.inputShareStoreSafe(sharestore); + await this.tKey.reconstructKey(); + + // setup Tkey + const tssPubKey = Point.fromTkeyPoint(this.tKey.getTSSPub()).toBufferSEC1(false); + this.updateState({ tssShareIndex: parseInt(tssShareIndex), tssPubKey, remoteClient }); + + // // Finalize setup. + // setup provider + await this.setupProvider({ chainConfig: this.options.chainConfig }); + await this.createSession(); + } + public async switchChain(chainConfig: CustomChainConfig): Promise { try { await this.setupProvider({ chainConfig }); @@ -859,6 +927,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } public async _UNSAFE_exportTssKey(): Promise { + if (this.state.remoteClient) throw new Error("export tss key not supported for remote client"); if (!this.state.factorKey) throw new Error("factorKey not present"); if (!this.state.signatures) throw new Error("signatures not present"); @@ -878,11 +947,14 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } private async setupTkey(importTssKey?: string): Promise { + if (this.state.remoteClient) { + log.warn("remote client is present, setupTkey are skipped"); + return; + } if (!this.state.oAuthKey) { throw new Error("user not logged in"); } const existingUser = await this.isMetadataPresent(this.state.oAuthKey); - if (!existingUser) { // Generate or use hash factor and initialize tkey with it. let factorKey: BN; @@ -963,17 +1035,25 @@ export class Web3AuthMPCCoreKit implements ICoreKit { try { this.checkReady(); - const factorKey = new BN(result.factorKey, "hex"); - if (!factorKey) { - throw new Error("Invalid factor key"); + if (!this.sessionManager.sessionId) return {}; + if (!result.factorKey && !result.remoteClient) throw new Error("factorKey not present"); + let metadataShare; + + if (result.factorKey) { + const factorKey = new BN(result.factorKey, "hex"); + if (!factorKey) { + throw new Error("Invalid factor key"); + } + metadataShare = await this.getFactorKeyMetadata(factorKey); + } else { + metadataShare = ShareStore.fromJSON(JSON.parse(result.remoteClient.metadataShare)); } this.torusSp.postboxKey = new BN(result.oAuthKey, "hex"); this.torusSp.verifierName = result.userInfo.aggregateVerifier || result.userInfo.verifier; this.torusSp.verifierId = result.userInfo.verifierId; this.torusSp.verifierType = result.userInfo.aggregateVerifier ? "aggregate" : "normal"; - const factorKeyMetadata = await this.getFactorKeyMetadata(factorKey); await this.tKey.initialize({ neverInitializeNewKey: true }); - await this.tKey.inputShareStoreSafe(factorKeyMetadata, true); + await this.tKey.inputShareStoreSafe(metadataShare, true); await this.tKey.reconstructKey(); this.updateState({ @@ -983,6 +1063,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { tssPubKey: Point.fromTkeyPoint(this.tkey.getTSSPub()).toBufferSEC1(false), signatures: result.signatures, userInfo: result.userInfo, + remoteClient: result.remoteClient, }); if (this.options.setupProviderOnInit) { @@ -994,16 +1075,24 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } private async createSession() { + if (this.options.sessionTime === 0) { + log.info("sessionTime is 0, not creating session"); + return; + } + try { const sessionId = OpenloginSessionManager.generateRandomSessionKey(); this.sessionManager.sessionId = sessionId; - const { oAuthKey, factorKey, userInfo, tssShareIndex, tssPubKey } = this.state; - if (!this.state.factorKey) throw new Error("factorKey not present"); - const { tssShare } = await this.tKey.getTSSShare(this.state.factorKey, { - accountIndex: this.state.accountIndex, - }); - if (!oAuthKey || !factorKey || !tssShare || !tssPubKey || !userInfo) { - throw new Error("User not logged in"); + const { oAuthKey, factorKey, userInfo, tssShareIndex, tssPubKey, remoteClient } = this.state; + if (!this.state.factorKey && !this.state.remoteClient) throw new Error("factorKey not present"); + + if (!this.state.remoteClient) { + const { tssShare } = await this.tKey.getTSSShare(this.state.factorKey, { + accountIndex: this.state.accountIndex, + }); + if (!oAuthKey || !factorKey || !tssShare || !tssPubKey || !userInfo) { + throw new Error("User not logged in"); + } } const payload: SessionData = { oAuthKey, @@ -1012,6 +1101,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { tssPubKey: Buffer.from(tssPubKey).toString("hex"), signatures: this.signatures, userInfo, + remoteClient, }; await this.sessionManager.createSession(payload); // to accommodate async storage @@ -1062,7 +1152,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (!this.tKey.metadata.factorEncs || typeof this.tKey.metadata.factorEncs[this.tKey.tssTag] !== "object") { throw new Error("factorEncs does not exist, failed in copy factor pub"); } - if (!this.state.factorKey) { + if (!this.state.factorKey && !this.state.remoteClient) { throw new Error("factorKey not present"); } if (VALID_SHARE_INDICES.indexOf(newFactorTSSIndex) === -1) { @@ -1072,29 +1162,51 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (this.tKey.metadata.factorPubs[this.tKey.tssTag].length >= MAX_FACTORS) { throw new Error("Maximum number of factors reached"); } + const signatures = await this.getSigningSignatures(OPS.CREATE_FACTOR); if (this.state.tssShareIndex !== newFactorTSSIndex) { - if (!this.state.factorKey) throw new Error("factorKey not present"); - // Generate new share. - await addFactorAndRefresh(this.tKey, newFactorPub, newFactorTSSIndex, this.state.factorKey, this.signatures); - - // Update local share. - const { tssIndex } = await this.tKey.getTSSShare(this.state.factorKey); - this.updateState({ - tssShareIndex: tssIndex, - }); + if (!this.state.remoteClient) { + await addFactorAndRefresh(this.tKey, newFactorPub, newFactorTSSIndex, this.state.factorKey, signatures); + } else { + await addFactorAndRefresh(this.tKey, newFactorPub, newFactorTSSIndex, this.state.factorKey, signatures, this.state.remoteClient); + } return; } + // TODO : fix this + let userEnc: EncryptedMessage; + if (this.state.remoteClient) { + const remoteFactorPub = TkeyPoint.fromCompressedPub(this.state.remoteClient.remoteFactorPub); + const factorEnc = this.tkey.getFactorEncs(remoteFactorPub); + const tssCommits = this.tkey.getTSSCommits(); + const dataRequired = { + factorEnc, + tssCommits, + factorPub: newFactorPub, + }; + + userEnc = ( + await post<{ data?: EncryptedMessage }>( + `${this.state.remoteClient.remoteClientUrl}/api/v3/mpc/copy_tss_share`, + { dataRequired }, + { + headers: { + Authorization: `Bearer ${this.state.remoteClient.remoteClientToken}`, + }, + } + ) + ).data; + } else { + const { tssShare } = await this.tKey.getTSSShare(this.state.factorKey); + userEnc = await encrypt(Point.fromTkeyPoint(newFactorPub).toBufferSEC1(false), scalarBNToBufferSEC1(tssShare)); + } - if (!this.state.factorKey) throw new Error("factorKey not present"); - const { tssShare } = await this.tKey.getTSSShare(this.state.factorKey); const updatedFactorPubs = this.tKey.metadata.factorPubs[this.tKey.tssTag].concat([newFactorPub]); const factorEncs = JSON.parse(JSON.stringify(this.tKey.metadata.factorEncs[this.tKey.tssTag])); const factorPubID = newFactorPub.x.toString(16, FIELD_ELEMENT_HEX_LEN); factorEncs[factorPubID] = { tssIndex: this.state.tssShareIndex, type: "direct", - userEnc: await encrypt(Point.fromTkeyPoint(newFactorPub).toBufferSEC1(false), scalarBNToBufferSEC1(tssShare)), + userEnc, serverEncs: [], }; this.tKey.metadata.addTSSData({ @@ -1103,7 +1215,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { factorEncs, }); - if (!this.tKey.manualSync) await this.tKey._syncShareMetadata(); + // if (!this.tKey.manualSync) await this.tKey._syncShareMetadata(); } private async getMetadataShare(): Promise { @@ -1195,8 +1307,97 @@ export class Web3AuthMPCCoreKit implements ICoreKit { return sessionData.map((session) => JSON.stringify({ data: session.token, sig: session.signature })); } - private async getSigningSignatures(): Promise { + public async remoteSign(msgHash: Buffer): Promise<{ v: number; r: Buffer; s: Buffer }> { + if (!this.state.remoteClient.remoteClientUrl) throw new Error("remoteClientUrl not present"); + + // PreSetup + const { torusNodeTSSEndpoints } = await this.nodeDetailManager.getNodeDetails({ + verifier: "test-verifier", + verifierId: "test@example.com", + }); + + const tssCommits = this.tKey.getTSSCommits(); + + const tssNonce = this.getTssNonce() || 0; + + const vid = `${this.verifier}${DELIMITERS.Delimiter1}${this.verifierId}`; + const sessionId = `${vid}${DELIMITERS.Delimiter2}default${DELIMITERS.Delimiter3}${tssNonce}${DELIMITERS.Delimiter4}`; + + const parties = 4; + const clientIndex = parties - 1; + + const { nodeIndexes } = await (this.tKey.serviceProvider as TorusServiceProvider).getTSSPubKey( + this.tKey.tssTag, + this.tKey.metadata.tssNonces[this.tKey.tssTag] + ); + + if (parties - 1 > nodeIndexes.length) { + throw new Error(`Not enough nodes to perform TSS - parties :${parties}, nodeIndexes:${nodeIndexes.length}`); + } + const { endpoints, tssWSEndpoints, partyIndexes, nodeIndexesReturned } = generateTSSEndpoints( + torusNodeTSSEndpoints, + parties, + clientIndex, + nodeIndexes + ); + + const factor = TkeyPoint.fromCompressedPub(this.state.remoteClient.remoteFactorPub); + const factorEnc = this.tKey.getFactorEncs(factor); + + const data = { + dataRequired: { + factorEnc, + sessionId, + tssNonce, + nodeIndexes: nodeIndexesReturned, + tssCommits: tssCommits.map((commit) => commit.toJSON()), + signatures: await this.getSigningSignatures(msgHash.toString("hex")), + serverEndpoints: { endpoints, tssWSEndpoints, partyIndexes }, + }, + msgHash: msgHash.toString("hex"), + }; + + const result = await post<{ data?: Record }>(`${this.state.remoteClient.remoteClientUrl}/api/v3/mpc/sign`, data, { + headers: { + Authorization: `Bearer ${this.state.remoteClient.remoteClientToken}`, + }, + }); + const { r, s, v } = result.data as { v: string; r: string; s: string }; + return { v: parseInt(v), r: Buffer.from(r, "hex"), s: Buffer.from(s, "hex") }; + } + + private async getSigningSignatures(data: string): Promise { if (!this.signatures) throw new Error("signatures not present"); + if (this.state.remoteClient) { + if (!this.nodeDetailManager._nodeDetails) { + if (!this.signatures) throw new Error("SDK is not initialized, please call `init` function"); + } + const attestationServerUrls = getAttestationServerUrls(this.nodeDetailManager._nodeDetails); + // fetch one time sigs for giving authorization to remote client to make sign request to w3a tss server + const sigPromises = attestationServerUrls.map(async (url) => { + const pr = post<{ sig?: string } | undefined>(url, { + signatures: this.signatures, + verifier: this.verifier, + verifierID: this.verifierId, + clientID: this.options.web3AuthClientId, + data, + }).catch((err: unknown) => { + log.error("Error while fetching attestation sig", err); + }); + + return pr; + }); + + const resolvedPromises = await Promise.all(sigPromises); + const sigs: string[] = []; + resolvedPromises.forEach((pr) => { + if (pr && pr.sig) { + sigs.push(pr.sig); + } + }); + return sigs; + } + return this.signatures; } diff --git a/src/utils.ts b/src/utils.ts index 3eca4aad..ab6d5d49 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,11 +1,14 @@ -import { getPubKeyPoint, Point, Point as TkeyPoint, randomSelection } from "@tkey-mpc/common-types"; +import { FactorEnc, getPubKeyPoint, Point as TkeyPoint, PointHex, randomSelection } from "@tkey-mpc/common-types"; import ThresholdKey from "@tkey-mpc/core"; +import { INodeDetails } from "@toruslabs/constants"; import { generatePrivate } from "@toruslabs/eccrypto"; +import { post } from "@toruslabs/http-helpers"; import { safeatob } from "@toruslabs/openlogin-utils"; import { keccak256 } from "@toruslabs/torus.js"; import BN from "bn.js"; import { SCALAR_LEN, VALID_SHARE_INDICES as VALID_TSS_INDICES } from "./constants"; +import { IRemoteClientState } from "./interfaces"; export const generateFactorKey = (): { private: BN; pub: TkeyPoint } => { const factorKey = new BN(generatePrivate()); @@ -73,9 +76,9 @@ export function parseToken(token: string) { * @param factorKeyForExistingTSSShare - Factor key for existing TSS share. * @param signatures - Signatures for authentication against RSS servers. */ -async function refreshTssShares( +export async function refreshTssShares( tKey: ThresholdKey, - factorPubs: Point[], + factorPubs: TkeyPoint[], tssIndices: number[], factorKeyForExistingTSSShare: BN, signatures: string[], @@ -100,12 +103,118 @@ async function refreshTssShares( }); } +export interface refreshRemoteTssType { + // from client + factorEnc: FactorEnc; + + factorPubs: TkeyPoint[]; + targetIndexes: number[]; + verifierNameVerifierId: string; + + tssTag: string; + tssCommits: TkeyPoint[]; + tssNonce: number; + newTSSServerPub: TkeyPoint; + // nodeIndexes : number[], + + serverOpts: { + serverEndpoints: string[]; + serverPubKeys: PointHex[]; + serverThreshold: number; + selectedServers: number[]; + authSignatures: string[]; + }; +} +export interface refreshRemoteTssReturnType { + tssTag: string; + tssNonce: number; + tssPolyCommits: TkeyPoint[]; + factorPubs: TkeyPoint[]; + factorEncs: { + [factorPubID: string]: FactorEnc; + }; +} +/** + * Refreshes TSS shares. Allows to change number of shares. New user shares are + * only produced for the target indices. + * @param tKey - Tkey instance to use. + * @param factorPubs - Factor pub keys after refresh. + * @param tssIndices - Target tss indices to generate new shares for. + * @param remoteFactorPub - Factor Pub for remote share. + * @param signatures - Signatures for authentication against RSS servers. + */ +export async function remoteRefreshTssShares( + tKey: ThresholdKey, + factorPubs: TkeyPoint[], + tssIndices: number[], + signatures: string[], + remoteClient: IRemoteClientState, + updateMetadata = false +) { + const rssNodeDetails = await tKey._getRssNodeDetails(); + const { serverEndpoints, serverPubKeys, serverThreshold } = rssNodeDetails; + + const verifierNameVerifierId = tKey.serviceProvider.getVerifierNameVerifierId(); + + const tssCommits = tKey.metadata.tssPolyCommits[tKey.tssTag]; + const tssNonce: number = tKey.metadata.tssNonces[tKey.tssTag] || 0; + const { pubKey: newTSSServerPub, nodeIndexes } = await tKey.serviceProvider.getTSSPubKey(tKey.tssTag, tssNonce + 1); + + // move to pre-refresh + const finalSelectedServers = nodeIndexes.slice(0, Math.min(serverEndpoints.length, nodeIndexes.length)); + + const factorEnc = tKey.getFactorEncs(TkeyPoint.fromCompressedPub(remoteClient.remoteFactorPub)); + + const dataRequired = { + factorEnc, + factorPubs: factorPubs.map((pub) => pub.toJSON()), + targetIndexes: tssIndices, + verifierNameVerifierId, + tssTag: tKey.tssTag, + tssCommits: tssCommits.map((commit) => commit.toJSON()), + tssNonce, + newTSSServerPub: newTSSServerPub.toJSON(), + serverOpts: { + selectedServers: finalSelectedServers, + serverEndpoints, + serverPubKeys, + serverThreshold, + authSignatures: signatures, + }, + }; + + const result = ( + await post<{ data: refreshRemoteTssReturnType }>( + `${remoteClient.remoteClientUrl}/api/v3/mpc/refresh_tss`, + { dataRequired }, + { + headers: { + Authorization: `Bearer ${remoteClient.remoteClientToken}`, + }, + } + ) + ).data; + + tKey.metadata.addTSSData({ + tssTag: result.tssTag, + tssNonce: result.tssNonce, + tssPolyCommits: result.tssPolyCommits.map((commit) => TkeyPoint.fromJSON(commit)), + factorPubs: result.factorPubs.map((pub) => TkeyPoint.fromJSON(pub)), + factorEncs: result.factorEncs, + }); + + if (updateMetadata) { + await tKey._syncShareMetadata(); + } +} + export async function addFactorAndRefresh( tKey: ThresholdKey, - newFactorPub: Point, + newFactorPub: TkeyPoint, newFactorTSSIndex: number, factorKeyForExistingTSSShare: BN, - signatures: string[] + signatures: string[], + remoteClient?: IRemoteClientState ) { if (!tKey) { throw new Error("tkey does not exist, cannot add factor pub"); @@ -123,10 +232,20 @@ export async function addFactorAndRefresh( const existingTSSIndexes = existingFactorPubs.map((fb) => tKey.getFactorEncs(fb).tssIndex); const updatedTSSIndexes = existingTSSIndexes.concat([newFactorTSSIndex]); - await refreshTssShares(tKey, updatedFactorPubs, updatedTSSIndexes, factorKeyForExistingTSSShare, signatures); + if (!remoteClient) { + await refreshTssShares(tKey, updatedFactorPubs, updatedTSSIndexes, factorKeyForExistingTSSShare, signatures); + } else { + await remoteRefreshTssShares(tKey, updatedFactorPubs, updatedTSSIndexes, signatures, remoteClient); + } } -export async function deleteFactorAndRefresh(tKey: ThresholdKey, factorPubToDelete: Point, factorKeyForExistingTSSShare: BN, signatures: string[]) { +export async function deleteFactorAndRefresh( + tKey: ThresholdKey, + factorPubToDelete: TkeyPoint, + factorKeyForExistingTSSShare: BN, + signatures: string[], + remoteClient?: IRemoteClientState +) { if (!tKey) { throw new Error("tkey does not exist, cannot add factor pub"); } @@ -144,7 +263,11 @@ export async function deleteFactorAndRefresh(tKey: ThresholdKey, factorPubToDele updatedFactorPubs.splice(factorIndex, 1); const updatedTSSIndexes = updatedFactorPubs.map((fb) => tKey.getFactorEncs(fb).tssIndex); - await refreshTssShares(tKey, updatedFactorPubs, updatedTSSIndexes, factorKeyForExistingTSSShare, signatures); + if (!remoteClient) { + await refreshTssShares(tKey, updatedFactorPubs, updatedTSSIndexes, factorKeyForExistingTSSShare, signatures); + } else { + await remoteRefreshTssShares(tKey, updatedFactorPubs, updatedTSSIndexes, signatures, remoteClient); + } } export const getHashedPrivateKey = (postboxKey: string, clientId: string): BN => { @@ -163,3 +286,27 @@ export const getHashedPrivateKey = (postboxKey: string, clientId: string): BN => export function scalarBNToBufferSEC1(s: BN): Buffer { return s.toArrayLike(Buffer, "be", SCALAR_LEN); } + +// export function Web3AuthStateFromJSON(result: StringifiedType): Web3AuthState { +// if (!result.factorKey) throw new Error("factorKey not found in JSON"); +// if (!result.tssShareIndex) throw new Error("tssShareIndex not found in JSON"); + +// const factorKey = new BN(result.factorKey as string, "hex"); +// const tssPubKey = Buffer.from(result.tssPubKey as Buffer); +// return { +// accountIndex: result. +// factorKey, +// oAuthKey: result.oAuthKey as string, +// tssShareIndex: parseInt(result.tssShareIndex as string), +// tssPubKey, +// signatures: result.signatures as string[], +// userInfo: result.userInfo as UserInfo, +// }; +// } + +// TODO: add these urls in fnd package +export const getAttestationServerUrls = (nodeDetails: INodeDetails) => { + return nodeDetails.torusNodeEndpoints.map((ep) => { + return `${ep}/authorization`; + }); +}; diff --git a/tests/importRecovery.spec.ts b/tests/importRecovery.spec.ts index f38f3a92..b5ff60b4 100644 --- a/tests/importRecovery.spec.ts +++ b/tests/importRecovery.spec.ts @@ -18,7 +18,7 @@ export const ImportTest = async (testVariable: ImportKeyTestVariable) => { t.before(async () => { const instance = await newCoreKitLogInInstance({ network: WEB3AUTH_NETWORK.DEVNET, - manualSync: false, + manualSync: testVariable.manualSync, email: testVariable.email, }); await criticalResetAccount(instance); @@ -64,22 +64,12 @@ export const ImportTest = async (testVariable: ImportKeyTestVariable) => { shareType: TssShareType.RECOVERY, }); - if (testVariable.manualSync) { - await coreKitInstance.commitChanges(); - } - - const exportedTssKey1 = await coreKitInstance._UNSAFE_exportTssKey(); // recover key // reinitalize corekit await coreKitInstance.logout(); - BrowserStorage.getInstance("memory").resetStore(); - const recoveredTssKey = await coreKitInstance._UNSAFE_recoverTssKey([factorKeyDevice, factorKeyRecovery]); - assert.strictEqual(recoveredTssKey, exportedTssKey1); await criticalResetAccount(coreKitInstance); - BrowserStorage.getInstance("memory").resetStore(); - // reinitialize corekit const newEmail = testVariable.importKeyEmail; const newLogin = await mockLogin(newEmail); @@ -104,34 +94,9 @@ export const ImportTest = async (testVariable: ImportKeyTestVariable) => { await coreKitInstance2.loginWithJWT(newIdTokenLoginParams); const exportedTssKey = await coreKitInstance2._UNSAFE_exportTssKey(); - BrowserStorage.getInstance("memory").resetStore(); + criticalResetAccount(coreKitInstance2); assert.strictEqual(exportedTssKey, recoveredTssKey); - - // reinitialize corekit - const newEmail3 = testVariable.importKeyEmail; - const newLogin3 = await mockLogin(newEmail); - - const newIdTokenLoginParams3 = { - verifier: "torus-test-health", - verifierId: newLogin3.parsedToken.email, - idToken: newLogin3.idToken, - } as IdTokenLoginParams; - - const coreKitInstance3 = new Web3AuthMPCCoreKit({ - web3AuthClientId: "torus-key-test", - web3AuthNetwork: WEB3AUTH_NETWORK.DEVNET, - baseUrl: "http://localhost:3000", - uxMode: "nodejs", - tssLib: TssLib, - storageKey: "memory", - }); - - await coreKitInstance3.init(); - await coreKitInstance3.loginWithJWT(newIdTokenLoginParams3); - - const exportedTssKey3 = await coreKitInstance3._UNSAFE_exportTssKey(); - console.log(exportedTssKey3); }); t.afterEach(function () {