diff --git a/src/interfaces.ts b/src/interfaces.ts index 4795c22..ef5f64d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -85,7 +85,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 { @@ -146,6 +157,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. diff --git a/src/mpcCoreKit.ts b/src/mpcCoreKit.ts index b31302a..7c0eabf 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"; @@ -318,16 +318,19 @@ 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 (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; if (verifierParams.subVerifierDetails) { // single verifier login. - const loginResponse = await tkeyServiceProvider.triggerLogin((params as SubVerifierDetailsParams).subVerifierDetails); + loginResponse = await tkeyServiceProvider.triggerLogin((params as SubVerifierDetailsParams).subVerifierDetails); if (this.isRedirectMode) return; @@ -338,7 +341,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { signatures: this._getSignatures(loginResponse.sessionData.sessionTokenData), }); } 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, @@ -354,7 +357,15 @@ export class Web3AuthMPCCoreKit implements ICoreKit { }); } - await this.setupTkey(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(existingSFAKey, true); + } else { + await this.setupTkey(importTssKey, false); + } } catch (err: unknown) { log.error("login error", err); if (err instanceof CoreError) { @@ -373,10 +384,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 = []; @@ -412,7 +427,15 @@ export class Web3AuthMPCCoreKit implements ICoreKit { userInfo: { ...parseToken(idToken), verifier, verifierId }, signatures: this._getSignatures(loginResponse.sessionData.sessionTokenData), }); - await this.setupTkey(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(existingSFAKey, true); + } else { + await this.setupTkey(importTssKey, false); + } } catch (err: unknown) { log.error("login error", err); if (err instanceof CoreError) { @@ -433,30 +456,30 @@ export class Web3AuthMPCCoreKit implements ICoreKit { try { const result = await this.torusSp.customAuthInstance.getRedirectResult(); - + let loginResponse: TorusLoginResponse | TorusAggregateLoginResponse; 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), + postBoxKey: this._getPostBoxKey(loginResponse), + postboxKeyNodeIndexes: loginResponse.nodesData?.nodeIndexes || [], + userInfo: loginResponse.userInfo, + signatures: this._getSignatures(loginResponse.sessionData.sessionTokenData), }); const userInfo = this.getUserInfo(); 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), + postBoxKey: this._getPostBoxKey(loginResponse), + postboxKeyNodeIndexes: loginResponse.nodesData?.nodeIndexes || [], + userInfo: loginResponse.userInfo[0], + signatures: this._getSignatures(loginResponse.sessionData.sessionTokenData), }); const userInfo = this.getUserInfo(); this.torusSp.verifierName = userInfo.aggregateVerifier; @@ -984,7 +1007,7 @@ export class Web3AuthMPCCoreKit implements ICoreKit { return tssNonce; } - private async setupTkey(providedImportTssKey?: string): Promise { + private async setupTkey(providedImportTssKey?: string, isSfaKey?: boolean): Promise { if (!this.state.postBoxKey) { throw CoreKitError.userNotLoggedIn(); } @@ -1003,10 +1026,14 @@ export class Web3AuthMPCCoreKit implements ICoreKit { } } await this.handleNewUser(importTssKey); - } else { if (importTssKey) { throw CoreKitError.tssKeyImportNotAllowed(); } + } else { + if (importTssKey && isSfaKey) { + await this.tkey.addLocalMetadataTransitions({ input: [{ message: ONE_KEY_DELETE_NONCE }], privKey: [new BN(this.state.postBoxKey, "hex")] }); + if (!this.tkey?.manualSync) await this.tkey?.syncLocalMetadataTransitions(); + } await this.handleExistingUser(); } } diff --git a/tests/importRecovery.spec.ts b/tests/importRecovery.spec.ts index d7bb18b..8d3bbc8 100644 --- a/tests/importRecovery.spec.ts +++ b/tests/importRecovery.spec.ts @@ -4,14 +4,14 @@ 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, TssLib, TssShareType, WEB3AUTH_NETWORK } from "../src"; +import { AsyncStorage, MemoryStorage, TssLibType, TssShareType, WEB3AUTH_NETWORK } from "../src"; import { bufferToElliptic, criticalResetAccount, newCoreKitLogInInstance } from "./setup"; type ImportKeyTestVariable = { manualSync?: boolean; email: string; importKeyEmail: string; - tssLib: TssLib; + tssLib: TssLibType; }; const storageInstance = new MemoryStorage(); diff --git a/tests/setup.ts b/tests/setup.ts index ed76817..5bbf74a 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -4,7 +4,7 @@ import BN from "bn.js"; import jwt, { Algorithm } from "jsonwebtoken"; import { tssLib as tssLibDKLS } from "@toruslabs/tss-dkls-lib"; -import { IAsyncStorage, IStorage, parseToken, TssLib, WEB3AUTH_NETWORK_TYPE, Web3AuthMPCCoreKit } from "../src"; +import { IAsyncStorage, IStorage, parseToken, TssLibType, WEB3AUTH_NETWORK_TYPE, Web3AuthMPCCoreKit } from "../src"; export const mockLogin2 = async (email: string) => { const req = new Request("https://li6lnimoyrwgn2iuqtgdwlrwvq0upwtr.lambda-url.eu-west-1.on.aws/", { @@ -91,14 +91,16 @@ export const newCoreKitLogInInstance = async ({ email, storageInstance, importTssKey, + registerExistingSFAKey, login, }: { network: WEB3AUTH_NETWORK_TYPE; manualSync: boolean; email: string; storageInstance: IStorage | IAsyncStorage; - tssLib?: TssLib; + 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-key-test", + verifierId: parsedToken.email, + }) + return await instance.torusSp.customAuthInstance.torus.retrieveShares({ + idToken, + nodePubkeys: nodeDetails.torusNodePub, + verifier: "torus-key-test", + 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..d1a0a7b --- /dev/null +++ b/tests/sfaImport.spec.ts @@ -0,0 +1,101 @@ +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 beforeTest = async () => { + await resetAccount(testVariable.email); + }; + + await beforeTest(); + + 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,""); + + }); + + 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); +});