diff --git a/src/interfaces.ts b/src/interfaces.ts index 4cfcc5d..e308695 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -1,5 +1,5 @@ import { KeyType, Point as TkeyPoint, ShareDescriptionMap } from "@tkey/common-types"; -import { TKeyTSS } from "@tkey/tss"; +import { TKeyTSS, TSSTorusServiceProvider } from "@tkey/tss"; import { WEB3AUTH_SIG_TYPE } from "@toruslabs/constants"; import type { AGGREGATE_VERIFIER_TYPE, @@ -7,9 +7,12 @@ import type { LoginWindowResponse, PasskeyExtraParams, SubVerifierDetails, + TorusAggregateLoginResponse, + TorusLoginResponse, TorusVerifierResponse, UX_MODE_TYPE, } from "@toruslabs/customauth"; +import { TorusKey } from "@toruslabs/torus.js"; import { Client } from "@toruslabs/tss-client"; // TODO: move the types to a base class for both dkls and frost in future import type { tssLib as TssDklsLib } from "@toruslabs/tss-dkls-lib"; @@ -18,6 +21,7 @@ import type { tssLib as TssFrostLibBip340 } from "@toruslabs/tss-frost-lib-bip34 import BN from "bn.js"; import { FactorKeyTypeShareDescription, TssShareType, USER_PATH, WEB3AUTH_NETWORK } from "./constants"; +import { ISessionSigGenerator } from "./plugins/ISessionSigGenerator"; export type CoreKitMode = UX_MODE_TYPE | "nodejs" | "react-native"; @@ -86,7 +90,18 @@ export type MPCKeyDetails = { tssPubKey?: TkeyPoint; }; -export type OAuthLoginParams = (SubVerifierDetailsParams | AggregateVerifierLoginParams) & { importTssKey?: string }; +export type OAuthLoginParams = (SubVerifierDetailsParams | AggregateVerifierLoginParams) & { + /** + * Key to import key into Tss during first time login. + */ + importTssKey?: string; + + /** + * For new users, use SFA key if user was registered with SFA before. + * Useful when you created the user with SFA before and now want to convert it to TSS. + */ + registerExistingSFAKey?: boolean; +}; export type UserInfo = TorusVerifierResponse & LoginWindowResponse; export interface EnableMFAParams { @@ -147,6 +162,12 @@ export interface JWTLoginParams { */ importTssKey?: string; + /** + * For new users, use SFA key if user was registered with SFA before. + * Useful when you created the user with SFA before and now want to convert it to TSS. + */ + registerExistingSFAKey?: boolean; + /** * Number of TSS public keys to prefetch. For the best performance, set it to * the number of factors you want to create. Set it to 0 for an existing user. @@ -166,6 +187,24 @@ export interface Web3AuthState { factorKey?: BN; } +export interface IContext { + status: COREKIT_STATUS; + state: Web3AuthState; + sessionId: string; + serviceProvider: TSSTorusServiceProvider | null; + updateState: (newState: Partial) => void; + getUserInfo: () => UserInfo; + logout: () => Promise; + setupTkey: (params?: { + providedImportKey?: string; + sfaLoginResponse?: TorusKey | TorusLoginResponse | TorusAggregateLoginResponse; + userInfo?: UserInfo; + importingSFAKey?: boolean; + persistSessionSigs?: boolean; + }) => Promise; + setCustomSessionSigGenerator: (sessionSigGenerator: ISessionSigGenerator) => void; +} + export interface ICoreKit { /** * The tKey instance, if initialized. @@ -173,11 +212,6 @@ export interface ICoreKit { **/ tKey: TKeyTSS | null; - /** - * Signatures generated from the OAuth Login. - **/ - signatures: string[] | null; - /** * Status of the current MPC Core Kit Instance **/ @@ -193,6 +227,7 @@ export interface ICoreKit { */ sessionId: string; + getContext(): IContext; /** * The function used to initailise the state of MPCCoreKit * Also is useful to resume an existing session. @@ -268,6 +303,10 @@ export interface ICoreKit { */ getKeyDetails(): MPCKeyDetails; + getSessionSignatures(): Promise; + + setCustomSessionSigGenerator(sessionSigGenerator: ISessionSigGenerator): void; + /** * Commit the changes made to the user's account when in manual sync mode. */ diff --git a/src/mpcCoreKit.ts b/src/mpcCoreKit.ts index 30ceff8..3185d9e 100644 --- a/src/mpcCoreKit.ts +++ b/src/mpcCoreKit.ts @@ -1,4 +1,4 @@ -import { BNString, KeyType, Point, secp256k1, SHARE_DELETED, ShareStore, StringifiedType } from "@tkey/common-types"; +import { BNString, KeyType, ONE_KEY_DELETE_NONCE, Point, secp256k1, SHARE_DELETED, ShareStore, StringifiedType } from "@tkey/common-types"; import { CoreError } from "@tkey/core"; import { ShareSerializationModule } from "@tkey/share-serialization"; import { TorusStorageLayer } from "@tkey/storage-layer-torus"; @@ -38,6 +38,7 @@ import { CoreKitMode, CreateFactorParams, EnableMFAParams, + IContext, ICoreKit, IFactorKey, InitParams, @@ -55,6 +56,8 @@ import { Web3AuthOptionsWithDefaults, Web3AuthState, } from "./interfaces"; +import { DefaultSessionSigGeneratorPlugin } from "./plugins/DefaultSessionSigGenerator"; +import { ISessionSigGenerator } from "./plugins/ISessionSigGenerator"; import { deriveShareCoefficients, ed25519, @@ -75,8 +78,6 @@ export class Web3AuthMPCCoreKit implements ICoreKit { public torusSp: TSSTorusServiceProvider | null = null; - public fetchSessionSignatures: () => Promise; - private options: Web3AuthOptionsWithDefaults; private storageLayer: TorusStorageLayer | null = null; @@ -103,6 +104,8 @@ export class Web3AuthMPCCoreKit implements ICoreKit { private atomicCallStackCounter: number = 0; + private sessionSigGenerator: ISessionSigGenerator; + constructor(options: Web3AuthOptions) { if (!options.web3AuthClientId) { throw CoreKitError.clientIdInvalid(); @@ -129,7 +132,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (!options.disableHashedFactorKey) options.disableHashedFactorKey = false; if (!options.hashedFactorNonce) options.hashedFactorNonce = options.web3AuthClientId; if (options.disableSessionManager === undefined) options.disableSessionManager = false; - this.fetchSessionSignatures = () => Promise.resolve(this.signatures); + this.sessionSigGenerator = new DefaultSessionSigGeneratorPlugin(this.getContext()); this.options = options as Web3AuthOptionsWithDefaults; this.currentStorage = new AsyncStorage(this._storageBaseKey, options.storage); @@ -158,10 +161,6 @@ export class Web3AuthMPCCoreKit implements ICoreKit { return this._sigType; } - get signatures(): string[] { - return this.state?.signatures ? this.state.signatures : []; - } - get config(): Web3AuthOptionsWithDefaults { return this.options; } @@ -210,8 +209,26 @@ export class Web3AuthMPCCoreKit implements ICoreKit { return this._sigType === "ed25519" && this.options.useClientGeneratedTSSKey === undefined ? true : !!this.options.useClientGeneratedTSSKey; } - public setSessionSigGenerator(sessionSigGenerator: () => Promise) { - this.fetchSessionSignatures = sessionSigGenerator; + public getContext(): IContext { + return { + status: this.status, + state: this.state, + sessionId: this.sessionId, + serviceProvider: this.torusSp, + updateState: this.updateState.bind(this), + getUserInfo: this.getUserInfo.bind(this), + logout: this.logout.bind(this), + setupTkey: this.setupTkey.bind(this), + setCustomSessionSigGenerator: this.setCustomSessionSigGenerator.bind(this), + }; + } + + public setCustomSessionSigGenerator(sessionSigGenerator: ISessionSigGenerator) { + this.sessionSigGenerator = sessionSigGenerator; + } + + async getSessionSignatures(): Promise { + return this.sessionSigGenerator.getSessionSigs(); } // RecoverTssKey only valid for user that enable MFA where user has 2 type shares : @@ -335,30 +352,57 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (this.isNodejsOrRN(this.options.uxMode)) { throw CoreKitError.oauthLoginUnsupported(`Oauth login is NOT supported in ${this.options.uxMode} mode.`); } - const { importTssKey } = params; + const { importTssKey, registerExistingSFAKey } = params; const tkeyServiceProvider = this.torusSp; + if (registerExistingSFAKey && importTssKey) { + throw CoreKitError.invalidConfig("Cannot import TSS key and register SFA key at the same time."); + } + + if (this.isRedirectMode && (importTssKey || registerExistingSFAKey)) { + throw CoreKitError.invalidConfig("key import is not supported in redirect mode"); + } try { // oAuth login. const verifierParams = params as SubVerifierDetailsParams; const aggregateParams = params as AggregateVerifierLoginParams; + let loginResponse: TorusLoginResponse | TorusAggregateLoginResponse; + let userInfo; if (verifierParams.subVerifierDetails) { // single verifier login. - const loginResponse = await tkeyServiceProvider.triggerLogin((params as SubVerifierDetailsParams).subVerifierDetails); - + loginResponse = await tkeyServiceProvider.triggerLogin((params as SubVerifierDetailsParams).subVerifierDetails); + userInfo = loginResponse.userInfo; if (this.isRedirectMode) return; - - await this._finalizeOauthLogin(loginResponse, loginResponse.userInfo, true, importTssKey); } else if (aggregateParams.subVerifierDetailsArray) { - const loginResponse = await tkeyServiceProvider.triggerAggregateLogin({ + loginResponse = await tkeyServiceProvider.triggerAggregateLogin({ aggregateVerifierType: aggregateParams.aggregateVerifierType || AGGREGATE_VERIFIER.SINGLE_VERIFIER_ID, verifierIdentifier: aggregateParams.aggregateVerifierIdentifier as string, subVerifierDetailsArray: aggregateParams.subVerifierDetailsArray, }); - + userInfo = loginResponse.userInfo[0]; if (this.isRedirectMode) return; + } - await this._finalizeOauthLogin(loginResponse, loginResponse.userInfo[0], true, importTssKey); + if (loginResponse && registerExistingSFAKey && loginResponse.finalKeyData.privKey) { + if (loginResponse.metadata.typeOfUser === "v1") { + throw CoreKitError.invalidConfig("Cannot register existing SFA key for v1 users, please contact web3auth support."); + } + const existingSFAKey = loginResponse.finalKeyData.privKey.padStart(64, "0"); + await this.setupTkey({ + providedImportKey: existingSFAKey, + sfaLoginResponse: loginResponse, + userInfo, + importingSFAKey: true, + persistSessionSigs: true, + }); + } else { + await this.setupTkey({ + providedImportKey: importTssKey, + sfaLoginResponse: loginResponse, + userInfo, + importingSFAKey: true, + persistSessionSigs: true, + }); } } catch (err: unknown) { log.error("login error", err); @@ -378,10 +422,14 @@ export class Web3AuthMPCCoreKit implements ICoreKit { throw CoreKitError.prefetchValueExceeded(`The prefetch value '${prefetchTssPublicKeys}' exceeds the maximum allowed limit of 3.`); } - const { verifier, verifierId, idToken, importTssKey } = params; + const { verifier, verifierId, idToken, importTssKey, registerExistingSFAKey } = params; this.torusSp.verifierName = verifier; this.torusSp.verifierId = verifierId; + if (registerExistingSFAKey && importTssKey) { + throw CoreKitError.invalidConfig("Cannot import TSS key and register SFA key at the same time."); + } + try { // prefetch tss pub keys. const prefetchTssPubs = []; @@ -406,8 +454,28 @@ export class Web3AuthMPCCoreKit implements ICoreKit { // wait for prefetch completed before setup tkey const [loginResponse] = await Promise.all([loginPromise, ...prefetchTssPubs]); - const userInfo = { ...parseToken(idToken), verifier, verifierId }; - await this._finalizeOauthLogin(loginResponse, userInfo, true, importTssKey); + + if (registerExistingSFAKey && loginResponse.finalKeyData.privKey) { + if (loginResponse.metadata.typeOfUser === "v1") { + throw CoreKitError.invalidConfig("Cannot register existing SFA key for v1 users, please contact web3auth support."); + } + const existingSFAKey = loginResponse.finalKeyData.privKey.padStart(64, "0"); + await this.setupTkey({ + providedImportKey: existingSFAKey, + importingSFAKey: true, + sfaLoginResponse: loginResponse, + userInfo: { ...parseToken(idToken), verifier, verifierId }, + persistSessionSigs: true, + }); + } else { + await this.setupTkey({ + providedImportKey: importTssKey, + importingSFAKey: false, + sfaLoginResponse: loginResponse, + userInfo: { ...parseToken(idToken), verifier, verifierId }, + persistSessionSigs: true, + }); + } } catch (err: unknown) { log.error("login error", err); if (err instanceof CoreError) { @@ -428,44 +496,34 @@ export class Web3AuthMPCCoreKit implements ICoreKit { try { const result = await this.torusSp.customAuthInstance.getRedirectResult(); - + let loginResponse: TorusLoginResponse | TorusAggregateLoginResponse; + let userInfo; if (result.method === TORUS_METHOD.TRIGGER_LOGIN) { - const data = result.result as TorusLoginResponse; - if (!data) { + loginResponse = result.result as TorusLoginResponse; + if (!loginResponse) { throw CoreKitError.invalidTorusLoginResponse(); } - this.updateState({ - postBoxKey: this._getPostBoxKey(data), - postboxKeyNodeIndexes: data.nodesData?.nodeIndexes || [], - userInfo: data.userInfo, - signatures: this._getSignatures(data.sessionData.sessionTokenData), - }); - const userInfo = this.getUserInfo(); + userInfo = loginResponse.userInfo; this.torusSp.verifierName = userInfo.verifier; } else if (result.method === TORUS_METHOD.TRIGGER_AGGREGATE_LOGIN) { - const data = result.result as TorusAggregateLoginResponse; - if (!data) { + loginResponse = result.result as TorusAggregateLoginResponse; + if (!loginResponse) { throw CoreKitError.invalidTorusAggregateLoginResponse(); } - this.updateState({ - postBoxKey: this._getPostBoxKey(data), - postboxKeyNodeIndexes: data.nodesData?.nodeIndexes || [], - userInfo: data.userInfo[0], - signatures: this._getSignatures(data.sessionData.sessionTokenData), - }); - const userInfo = this.getUserInfo(); + userInfo = loginResponse.userInfo[0]; this.torusSp.verifierName = userInfo.aggregateVerifier; } else { throw CoreKitError.unsupportedRedirectMethod(); } - const userInfo = this.getUserInfo(); - if (!this.state.postBoxKey) { - throw CoreKitError.postBoxKeyMissing("postBoxKey not present in state after processing redirect result."); - } this.torusSp.postboxKey = new BN(this.state.postBoxKey, "hex"); this.torusSp.verifierId = userInfo.verifierId; - await this.setupTkey(); + await this.setupTkey({ + importingSFAKey: false, + userInfo, + sfaLoginResponse: loginResponse, + persistSessionSigs: true, + }); } catch (error: unknown) { this.resetState(); log.error("error while handling redirect result", error); @@ -473,29 +531,6 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } } - public async _finalizeOauthLogin(loginResponse: TorusKey, userInfo: UserInfo, persistSessionSigs = true, importTssKey?: string): Promise { - const postBoxKey = this._getPostBoxKey(loginResponse); - this.torusSp.postboxKey = new BN(postBoxKey, "hex"); - - this.updateState({ - postBoxKey, - postboxKeyNodeIndexes: loginResponse.nodesData?.nodeIndexes, - userInfo, - signatures: persistSessionSigs ? this._getSignatures(loginResponse.sessionData.sessionTokenData) : [], - }); - const sp = this.tkey.serviceProvider; - if (!sp) { - throw new Error("Oauth Service provider is missing in tkey"); - } - if (!sp?.verifierId) { - sp.verifierId = userInfo.verifierId; - } - if (!sp?.verifierName) { - sp.verifierName = userInfo.aggregateVerifier || userInfo.verifier; - } - await this.setupTkey(importTssKey); - } - public async inputFactorKey(factorKey: BN): Promise { this.checkReady(); try { @@ -744,7 +779,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { throw CoreKitError.activeSessionNotFound(); } - const signatures = await this.fetchSessionSignatures(); + const signatures = await this.getSessionSignatures(); if (!signatures) { throw CoreKitError.signaturesNotPresent(); } @@ -812,7 +847,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { throw CoreKitError.factorInUseCannotBeDeleted("Cannot delete current active factor"); } - const authSignatures = await this.fetchSessionSignatures(); + const authSignatures = await this.getSessionSignatures(); await this.tKey.deleteFactorPub({ factorKey: this.state.factorKey, deleteFactorPub: factorPub, authSignatures }); const factorPubHex = fpp.toSEC1(factorKeyCurve, true).toString("hex"); const allDesc = this.tKey.metadata.getShareDescription(); @@ -1020,27 +1055,56 @@ export class Web3AuthMPCCoreKit implements ICoreKit { return tssNonce; } - private async setupTkey(providedImportTssKey?: string): Promise { - if (!this.state.postBoxKey) { - throw CoreKitError.userNotLoggedIn(); + private async setupTkey(params?: { + providedImportKey?: string; + sfaLoginResponse?: TorusKey | TorusLoginResponse | TorusAggregateLoginResponse; + userInfo?: UserInfo; + importingSFAKey?: boolean; + persistSessionSigs?: boolean; + }): Promise { + const { providedImportKey, sfaLoginResponse, userInfo, importingSFAKey, persistSessionSigs } = params; + if (importingSFAKey && !sfaLoginResponse) { + throw CoreKitError.default("SFA key registration requires SFA login response"); + } + const postBoxKey = this._getPostBoxKey(sfaLoginResponse); + this.torusSp.postboxKey = new BN(postBoxKey, "hex"); + this.updateState({ + postBoxKey, + postboxKeyNodeIndexes: sfaLoginResponse.nodesData?.nodeIndexes, + userInfo, + signatures: persistSessionSigs ? this.extractSessionSignatures(sfaLoginResponse.sessionData.sessionTokenData) : [], + }); + const sp = this.tkey.serviceProvider; + if (!sp) { + throw new Error("Oauth Service provider is missing in tkey"); + } + if (!sp?.verifierId) { + sp.verifierId = userInfo.verifierId; } + if (!sp?.verifierName) { + sp.verifierName = userInfo.aggregateVerifier || userInfo.verifier; + } + const existingUser = await this.isMetadataPresent(this.state.postBoxKey); - let importTssKey = providedImportTssKey; + let importKey = providedImportKey; if (!existingUser) { - if (!importTssKey && this.useClientGeneratedTSSKey) { - if (this._sigType === "ed25519") { + if (!importKey && this.useClientGeneratedTSSKey) { + if (this.keyType === KeyType.ed25519) { const k = generateEd25519Seed(); - importTssKey = k.toString("hex"); + importKey = k.toString("hex"); } else if (this.keyType === KeyType.secp256k1) { const k = secp256k1.genKeyPair().getPrivate(); - importTssKey = scalarBNToBufferSEC1(k).toString("hex"); + importKey = scalarBNToBufferSEC1(k).toString("hex"); } else { throw CoreKitError.default(`Unsupported key type and sig type combination: ${this.keyType}, ${this._sigType}`); } } - await this.handleNewUser(importTssKey); + if (importingSFAKey && sfaLoginResponse && sfaLoginResponse.metadata.upgraded) { + throw CoreKitError.default("SFA key registration is not allowed for already upgraded users"); + } + await this.handleNewUser(importKey, importingSFAKey); } else { - if (importTssKey) { + if (importKey) { throw CoreKitError.tssKeyImportNotAllowed(); } await this.handleExistingUser(); @@ -1048,7 +1112,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } // mutation function - private async handleNewUser(importTssKey?: string) { + private async handleNewUser(importTssKey?: string, isSfaKey?: boolean) { await this.atomicSync(async () => { // Generate or use hash factor and initialize tkey with it. let factorKey: BN; @@ -1091,6 +1155,12 @@ export class Web3AuthMPCCoreKit implements ICoreKit { updateMetadata: false, }); } + if (importTssKey && isSfaKey) { + await this.tkey.addLocalMetadataTransitions({ + input: [{ message: ONE_KEY_DELETE_NONCE }], + privKey: [new BN(this.state.postBoxKey, "hex")], + }); + } }); } @@ -1179,6 +1249,9 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (!this.options.disableSessionManager && !this.sessionManager) { throw new Error("sessionManager is not available"); } + if (this.options.disableSessionManager) { + return; + } try { const sessionId = SessionManager.generateRandomSessionKey(); @@ -1193,13 +1266,14 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (!postBoxKey || !factorKey || !tssShare || !tssPubKey || !userInfo) { throw CoreKitError.userNotLoggedIn(); } + const sessionSigs = await this.getSessionSignatures(); const payload: SessionData = { postBoxKey, postboxKeyNodeIndexes: postboxKeyNodeIndexes || [], factorKey: factorKey?.toString("hex"), tssShareIndex: tssShareIndex as number, tssPubKey: Buffer.from(tssPubKey).toString("hex"), - signatures: this.signatures, + signatures: sessionSigs, userInfo, }; await this.sessionManager.createSession(payload); @@ -1260,7 +1334,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { if (this.tKey.metadata.factorPubs[this.tKey.tssTag].length >= MAX_FACTORS) { throw CoreKitError.maximumFactorsReached(`The maximum number of allowable factors (${MAX_FACTORS}) has been reached.`); } - const authSignatures = await this.fetchSessionSignatures(); + const authSignatures = await this.getSessionSignatures(); // Generate new share. await this.tkey.addFactorPub({ existingFactorKey: this.state.factorKey, @@ -1346,7 +1420,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { return TorusUtils.getPostboxKey(result); } - private _getSignatures(sessionData: TorusKey["sessionData"]["sessionTokenData"]): string[] { + private extractSessionSignatures(sessionData: TorusKey["sessionData"]["sessionTokenData"]): string[] { // There is a check in torus.js which pushes undefined to session data in case // that particular node call fails. // and before returning we are not filtering out undefined vals in torus.js @@ -1472,7 +1546,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { const serverURLs = endpoints.map((x) => x.url); const pubKeyHex = ec.pointToBuffer(tssPubKeyPoint, Buffer).toString("hex"); const serverCoefficientsHex = serverCoefficients.map((c) => ec.scalarToBuffer(c, Buffer).toString("hex")); - const authSignatures = await this.fetchSessionSignatures(); + const authSignatures = await this.getSessionSignatures(); const signature = await signFrost( this.wasmLib as FrostWasmLibEd25519 | FrostWasmLibBip340, session, diff --git a/src/plugins/DefaultSessionSigGenerator.ts b/src/plugins/DefaultSessionSigGenerator.ts new file mode 100644 index 0000000..72d53c7 --- /dev/null +++ b/src/plugins/DefaultSessionSigGenerator.ts @@ -0,0 +1,14 @@ +import { IContext } from "../interfaces"; +import { ISessionSigGenerator } from "./ISessionSigGenerator"; + +export class DefaultSessionSigGeneratorPlugin implements ISessionSigGenerator { + private context: IContext; + + constructor(readonly mpcCorekitContext: IContext) { + this.context = mpcCorekitContext; + } + + async getSessionSigs() { + return this.context.state.signatures || []; + } +} diff --git a/src/plugins/ISessionSigGenerator.ts b/src/plugins/ISessionSigGenerator.ts new file mode 100644 index 0000000..efa40c4 --- /dev/null +++ b/src/plugins/ISessionSigGenerator.ts @@ -0,0 +1,3 @@ +export interface ISessionSigGenerator { + getSessionSigs: () => Promise; +} diff --git a/tests/setup.ts b/tests/setup.ts index 596ca17..ab677bf 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -91,6 +91,7 @@ export const newCoreKitLogInInstance = async ({ email, storageInstance, importTssKey, + registerExistingSFAKey, login, }: { network: WEB3AUTH_NETWORK_TYPE; @@ -99,6 +100,7 @@ export const newCoreKitLogInInstance = async ({ storageInstance: IStorage | IAsyncStorage; tssLib?: TssLibType; importTssKey?: string; + registerExistingSFAKey?: boolean; login?: LoginFunc; }) => { const instance = new Web3AuthMPCCoreKit({ @@ -118,11 +120,55 @@ export const newCoreKitLogInInstance = async ({ verifierId: parsedToken.email, idToken, importTssKey, + registerExistingSFAKey }); return instance; }; +export const loginWithSFA = async ({ + network, + manualSync, + email, + storageInstance, + login, +}: { + network: WEB3AUTH_NETWORK_TYPE; + manualSync: boolean; + email: string; + storageInstance: IStorage | IAsyncStorage; + tssLib?: TssLibType; + login?: LoginFunc; +}) => { + const instance = new Web3AuthMPCCoreKit({ + web3AuthClientId: "torus-key-test", + web3AuthNetwork: network, + baseUrl: "http://localhost:3000", + uxMode: "nodejs", + tssLib: tssLib || tssLibDKLS, + storage: storageInstance, + manualSync, + }); + + const { idToken, parsedToken } = login ? await login(email) : await mockLogin(email); + await instance.init(); + const nodeDetails = await instance.torusSp.customAuthInstance.nodeDetailManager.getNodeDetails({ + verifier: "torus-test-health", + verifierId: parsedToken.email, + }) + return await instance.torusSp.customAuthInstance.torus.retrieveShares({ + idToken, + nodePubkeys: nodeDetails.torusNodePub, + verifier: "torus-test-health", + verifierParams: { + verifier_id: parsedToken.email, + }, + endpoints: nodeDetails.torusNodeEndpoints, + indexes: nodeDetails.torusIndexes, + }) + +} + export class AsyncMemoryStorage implements IAsyncStorage { private _store: Record = {}; @@ -138,3 +184,12 @@ export class AsyncMemoryStorage implements IAsyncStorage { export function bufferToElliptic(p: Buffer, ec = secp256k1): EllipticPoint { return ec.keyFromPublic(p).getPublic(); } + + +export function generateRandomEmail(): string { + const username = stringGen(10); + const domain = stringGen(5); + const tld = stringGen(3); + return `${username}@${domain}.${tld}`; +} + diff --git a/tests/sfaImport.spec.ts b/tests/sfaImport.spec.ts new file mode 100644 index 0000000..bfd2ec9 --- /dev/null +++ b/tests/sfaImport.spec.ts @@ -0,0 +1,106 @@ +import assert from "node:assert"; +import test from "node:test"; + +import { tssLib as tssLibDKLS } from "@toruslabs/tss-dkls-lib"; +import { tssLib as tssLibFROST } from "@toruslabs/tss-frost-lib"; + +import { AsyncStorage, MemoryStorage, TssLibType, TssShareType, WEB3AUTH_NETWORK } from "../src"; +import { criticalResetAccount, generateRandomEmail, loginWithSFA, newCoreKitLogInInstance } from "./setup"; + +type ImportKeyTestVariable = { + manualSync?: boolean; + email: string; + tssLib: TssLibType; +}; + +const storageInstance = new MemoryStorage(); +export const ImportSFATest = async (testVariable: ImportKeyTestVariable) => { + async function newCoreKitInstance(email: string) { + return newCoreKitLogInInstance({ + network: WEB3AUTH_NETWORK.DEVNET, + manualSync: testVariable.manualSync, + email: email, + storageInstance, + tssLib: testVariable.tssLib, + registerExistingSFAKey: true, + }); + } + + async function resetAccount(email: string) { + const kit = await newCoreKitInstance(email); + await criticalResetAccount(kit); + await kit.logout(); + await new AsyncStorage(kit._storageKey, storageInstance).resetStore(); + } + + test(`import sfa key and recover tss key : ${testVariable.manualSync}`, async function (t) { + const afterTest = async () => { + await resetAccount(testVariable.email); + }; + await t.test("#recover Tss key using 2 factors key, import tss key to new oauth login", async function () { + const sfaResult = await loginWithSFA({ + network: WEB3AUTH_NETWORK.DEVNET, + manualSync: testVariable.manualSync, + email: testVariable.email, + storageInstance, + }); + const coreKitInstance = await newCoreKitInstance(testVariable.email); + + // Create 2 factors which will be used to recover tss key. + const factorKeyDevice = await coreKitInstance.createFactor({ + shareType: TssShareType.DEVICE, + }); + + const factorKeyRecovery = await coreKitInstance.createFactor({ + shareType: TssShareType.RECOVERY, + }); + + if (testVariable.manualSync) { + await coreKitInstance.commitChanges(); + } + + // Export key and logout. + const exportedTssKey1 = await coreKitInstance._UNSAFE_exportTssKey(); + await coreKitInstance.logout(); + + // Recover key from any two factors. + const recoveredTssKey = await coreKitInstance._UNSAFE_recoverTssKey([factorKeyDevice, factorKeyRecovery]); + assert.strictEqual(recoveredTssKey, exportedTssKey1); + assert.strictEqual(sfaResult.finalKeyData.privKey,recoveredTssKey); + // sfa key should be empty after import to mpc + const sfaResult2 = await loginWithSFA({ + network: WEB3AUTH_NETWORK.DEVNET, + manualSync: testVariable.manualSync, + email: testVariable.email, + storageInstance, + }); + assert.strictEqual(sfaResult2.finalKeyData.privKey, ""); + + const coreKitInstance2 = await newCoreKitInstance(testVariable.email); + const tssKey2 = await coreKitInstance2._UNSAFE_exportTssKey(); + // core kit should have same sfa key which was imported before + assert.strictEqual(tssKey2, exportedTssKey1); + assert.strictEqual(sfaResult.finalKeyData.privKey, tssKey2); + + + }); + + await afterTest(); + t.afterEach(function () { + return console.info("finished running recovery test"); + }); + t.after(function () { + return console.info("finished running recovery tests"); + }); + }); +}; + +const variable: ImportKeyTestVariable[] = [ + { manualSync: false, email: generateRandomEmail(), tssLib: tssLibDKLS }, + { manualSync: true, email: generateRandomEmail(), tssLib: tssLibDKLS }, + { manualSync: false, email: generateRandomEmail(), tssLib: tssLibFROST }, +]; + +variable.forEach(async (testVariable) => { + await ImportSFATest(testVariable); +});