diff --git a/package-lock.json b/package-lock.json index bd4952fce..af2d06d70 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4375,6 +4375,10 @@ "resolved": "packages/service-provider-base", "link": true }, + "node_modules/@tkey/service-provider-sfa": { + "resolved": "packages/service-provider-sfa", + "link": true + }, "node_modules/@tkey/service-provider-torus": { "resolved": "packages/service-provider-torus", "link": true @@ -25133,6 +25137,49 @@ "@babel/runtime": "7.x" } }, + "packages/service-provider-sfa": { + "name": "@tkey/service-provider-sfa", + "version": "13.0.0-alpha.4", + "license": "MIT", + "dependencies": { + "@tkey/service-provider-base": "^13.0.0-alpha.4", + "@toruslabs/fetch-node-details": "^13.4.0", + "@toruslabs/torus.js": "^12.3.6", + "bn.js": "^5.2.1" + }, + "devDependencies": { + "@types/bn.js": "^5.1.5" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + } + }, + "packages/service-provider-sfa/node_modules/@toruslabs/torus.js": { + "version": "12.3.6", + "resolved": "https://registry.npmjs.org/@toruslabs/torus.js/-/torus.js-12.3.6.tgz", + "integrity": "sha512-cXPC+Cyw4W05eZW784pPd+QyJRmvK54wTYbBocU9gVaqZhGXNryVrqhBYZSOmAr3pUtOubjRZfeXCZLn7BA77Q==", + "dependencies": { + "@toruslabs/constants": "^13.4.0", + "@toruslabs/eccrypto": "^4.0.0", + "@toruslabs/http-helpers": "^6.1.1", + "bn.js": "^5.2.1", + "elliptic": "^6.5.5", + "ethereum-cryptography": "^2.1.3", + "json-stable-stringify": "^1.1.1", + "loglevel": "^1.9.1" + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + } + }, "packages/service-provider-torus": { "name": "@tkey/service-provider-torus", "version": "13.0.0-alpha.5", diff --git a/packages/common-types/src/baseTypes/commonTypes.ts b/packages/common-types/src/baseTypes/commonTypes.ts index a7036d36c..e392ca7bd 100644 --- a/packages/common-types/src/baseTypes/commonTypes.ts +++ b/packages/common-types/src/baseTypes/commonTypes.ts @@ -50,6 +50,8 @@ export interface IServiceProvider extends ISerializable { serviceProviderName: string; + migratableKey?: BN | null; + encrypt(msg: Buffer): Promise; decrypt(msg: EncryptedMessage): Promise; retrievePubKey(type: PubKeyType): Buffer; diff --git a/packages/core/src/core.ts b/packages/core/src/core.ts index 1d1d7ffce..2f12f2481 100644 --- a/packages/core/src/core.ts +++ b/packages/core/src/core.ts @@ -267,13 +267,29 @@ class ThresholdKey implements ITKey { if (neverInitializeNewKey) { throw CoreError.default("key has not been generated yet"); } + // no metadata set, assumes new user - await this._initializeNewKey({ - initializeModules: true, - importedKey: importKey, - importEd25519Seed: params?.importEd25519Seed, - delete1OutOf1: p.delete1OutOf1, - }); + + // check for serviceprovider migratableKey for import key from service provider for new user + // provided no importKey is provided ( importKey take precedent ) + if (this.serviceProvider.migratableKey && !importKey) { + // importkey from server provider need to be atomic, hence manual sync is required. + const tempStateManualSync = this.manualSync; // temp store manual sync flag + this.manualSync = true; // Setting this as true since _initializeNewKey has a check where for importkey from server provider need to be atomic, hence manual sync is required. + await this._initializeNewKey({ initializeModules: true, importedKey: this.serviceProvider.migratableKey, delete1OutOf1: true }); + if (!tempStateManualSync) await this.syncLocalMetadataTransitions(); // Only sync if we were not in manual sync mode, if manual sync is set by developer, they should handle it themselves + // restore manual sync flag + this.manualSync = tempStateManualSync; + } else { + await this._initializeNewKey({ + initializeModules: true, + importedKey: importKey, + delete1OutOf1: p.delete1OutOf1, + importEd25519Seed: params?.importEd25519Seed, + }); + } + + // return after created new tkey account ( skip other steps) return this.getKeyDetails(); } // else we continue with catching up share and metadata diff --git a/packages/service-provider-base/src/ServiceProviderBase.ts b/packages/service-provider-base/src/ServiceProviderBase.ts index ff3c6e609..36dc93209 100644 --- a/packages/service-provider-base/src/ServiceProviderBase.ts +++ b/packages/service-provider-base/src/ServiceProviderBase.ts @@ -22,6 +22,8 @@ class ServiceProviderBase implements IServiceProvider { serviceProviderName: string; + migratableKey: BN | null = null; + constructor({ enableLogging = false, postboxKey }: ServiceProviderArgs) { this.enableLogging = enableLogging; this.postboxKey = new BN(postboxKey, "hex"); diff --git a/packages/service-provider-sfa/README.md b/packages/service-provider-sfa/README.md new file mode 100644 index 000000000..2e94d6c6a --- /dev/null +++ b/packages/service-provider-sfa/README.md @@ -0,0 +1,27 @@ +# tKey Single Factor Auth Service Provider + +[![npm version](https://img.shields.io/npm/v/@tkey/service-provider-sfa?label=%22%22)](https://www.npmjs.com/package/@tkey/service-provider-sfa/v/latest) [![minzip](https://img.shields.io/bundlephobia/minzip/@tkey/service-provider-sfa?label=%22%22)](https://bundlephobia.com/result?p=@tkey/service-provider-sfa@latest) + +Service Provider in `tKey` is used for generating a social login share of the private key share managed by a wallet service provider via +their own authentication flows. + +## Installation + +```shell +npm install --save @tkey/service-provider-sfa +``` + +### See the full [SDK Reference](https://web3auth.io/docs/sdk/core-kit/tkey/usage#log-in) on the Web3Auth Documentation + +## Example + +```js +import SFAServiceProvider from '@tkey/service-provider-sfa'; + +const web3AuthOptions: any = { + clientId, // Get your Client ID from Web3Auth Dashboard + web3AuthNetwork: 'testnet', // ["cyan", "testnet", "mainnet", "aqua", "sapphire_devnet", "sapphire_mainnet"] +}; + +const serviceProvider = new SFAServiceProvider({web3AuthOptions}); +``` diff --git a/packages/service-provider-sfa/package.json b/packages/service-provider-sfa/package.json new file mode 100644 index 000000000..d73874b4a --- /dev/null +++ b/packages/service-provider-sfa/package.json @@ -0,0 +1,63 @@ +{ + "name": "@tkey/service-provider-sfa", + "version": "13.0.0-alpha.4", + "description": "TKey Torus Service Provider Module", + "author": "Torus Labs", + "homepage": "https://github.com/tkey/tkey#readme", + "license": "MIT", + "main": "dist/serviceProviderSfa.cjs.js", + "module": "dist/serviceProviderSfa.esm.js", + "unpkg": "dist/serviceProviderSfa.umd.min.js", + "jsdelivr": "dist/serviceProviderSfa.umd.min.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist", + "src" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/tkey/tkey.git" + }, + "scripts": { + "test": "cross-env MOCKED=true mocha --config ../../.mocharc.json ", + "coverage": "nyc npm test", + "coverage-production": "nyc npm run test-production", + "test-development": "cross-env MOCKED=false METADATA=http://localhost:5051 mocha --config ../../.mocharc.json ", + "test-production": "cross-env MOCKED=false METADATA=https://metadata.tor.us mocha --config ../../.mocharc.json ", + "test-debugger": "mocha --config ../../.mocharc.json --inspect-brk", + "dev": "rimraf dist/ && cross-env NODE_ENV=development torus-scripts build", + "build": "rimraf dist/ && cross-env NODE_ENV=production torus-scripts build", + "lint": "eslint --fix 'src/**/*.ts'", + "prepack": "npm run build", + "pre-commit": "lint-staged" + }, + "peerDependencies": { + "@babel/runtime": "7.x" + }, + "dependencies": { + "@tkey/service-provider-base": "^13.0.0-alpha.4", + "@toruslabs/fetch-node-details": "^13.4.0", + "@toruslabs/torus.js": "^12.3.6", + "bn.js": "^5.2.1" + }, + "devDependencies": { + "@types/bn.js": "^5.1.5" + }, + "bugs": { + "url": "https://github.com/tkey/tkey/issues" + }, + "lint-staged": { + "!(*d).ts": [ + "npm run lint --", + "prettier --write 'src/**/*.ts'" + ] + }, + "engines": { + "node": ">=18.x", + "npm": ">=9.x" + }, + "gitHead": "9967ce9f795f495f28ef0da1fc50acde31dcc258" +} diff --git a/packages/service-provider-sfa/src/SfaServiceProvider.ts b/packages/service-provider-sfa/src/SfaServiceProvider.ts new file mode 100644 index 000000000..2e15db3df --- /dev/null +++ b/packages/service-provider-sfa/src/SfaServiceProvider.ts @@ -0,0 +1,100 @@ +import { type StringifiedType } from "@tkey/common-types"; +import { ServiceProviderBase } from "@tkey/service-provider-base"; +import { NodeDetailManager } from "@toruslabs/fetch-node-details"; +import Torus, { keccak256, TorusKey } from "@toruslabs/torus.js"; +import BN from "bn.js"; + +import { AggregateVerifierParams, LoginParams, SfaServiceProviderArgs, Web3AuthOptions } from "./interfaces"; + +class SfaServiceProvider extends ServiceProviderBase { + web3AuthOptions: Web3AuthOptions; + + authInstance: Torus; + + public torusKey: TorusKey; + + public migratableKey: BN | null = null; // Migration of key from SFA to tKey + + private nodeDetailManagerInstance: NodeDetailManager; + + constructor({ enableLogging = false, postboxKey, web3AuthOptions }: SfaServiceProviderArgs) { + super({ enableLogging, postboxKey }); + this.web3AuthOptions = web3AuthOptions; + this.authInstance = new Torus({ + clientId: web3AuthOptions.clientId, + enableOneKey: true, + network: web3AuthOptions.network, + }); + Torus.enableLogging(enableLogging); + this.serviceProviderName = "SfaServiceProvider"; + this.nodeDetailManagerInstance = new NodeDetailManager({ network: web3AuthOptions.network, enableLogging }); + } + + static fromJSON(value: StringifiedType): SfaServiceProvider { + const { enableLogging, postboxKey, web3AuthOptions, serviceProviderName, torusKey } = value; + if (serviceProviderName !== "SfaServiceProvider") return undefined; + + const sfaSP = new SfaServiceProvider({ + enableLogging, + postboxKey, + web3AuthOptions, + }); + + sfaSP.torusKey = torusKey; + + return sfaSP; + } + + async connect(params: LoginParams): Promise { + const { verifier, verifierId, idToken, subVerifierInfoArray } = params; + const verifierDetails = { verifier, verifierId }; + + // fetch node details. + const { torusNodeEndpoints, torusIndexes } = await this.nodeDetailManagerInstance.getNodeDetails(verifierDetails); + + if (params.serverTimeOffset) { + this.authInstance.serverTimeOffset = params.serverTimeOffset; + } + + let finalIdToken = idToken; + let finalVerifierParams = { verifier_id: verifierId }; + if (subVerifierInfoArray && subVerifierInfoArray?.length > 0) { + const aggregateVerifierParams: AggregateVerifierParams = { verify_params: [], sub_verifier_ids: [], verifier_id: "" }; + const aggregateIdTokenSeeds = []; + for (let index = 0; index < subVerifierInfoArray.length; index += 1) { + const userInfo = subVerifierInfoArray[index]; + aggregateVerifierParams.verify_params.push({ verifier_id: verifierId, idtoken: userInfo.idToken }); + aggregateVerifierParams.sub_verifier_ids.push(userInfo.verifier); + aggregateIdTokenSeeds.push(userInfo.idToken); + } + aggregateIdTokenSeeds.sort(); + + finalIdToken = keccak256(Buffer.from(aggregateIdTokenSeeds.join(String.fromCharCode(29)), "utf8")).slice(2); + + aggregateVerifierParams.verifier_id = verifierId; + finalVerifierParams = aggregateVerifierParams; + } + + const torusKey = await this.authInstance.retrieveShares(torusNodeEndpoints, torusIndexes, verifier, finalVerifierParams, finalIdToken); + this.torusKey = torusKey; + + if (!torusKey.metadata.upgraded) { + const { finalKeyData, oAuthKeyData } = torusKey; + const privKey = finalKeyData.privKey || oAuthKeyData.privKey; + this.migratableKey = new BN(privKey, "hex"); + } + const postboxKey = Torus.getPostboxKey(torusKey); + this.postboxKey = new BN(postboxKey, 16); + return this.postboxKey; + } + + toJSON(): StringifiedType { + return { + ...super.toJSON(), + serviceProviderName: this.serviceProviderName, + web3AuthOptions: this.web3AuthOptions, + }; + } +} + +export default SfaServiceProvider; diff --git a/packages/service-provider-sfa/src/index.ts b/packages/service-provider-sfa/src/index.ts new file mode 100644 index 000000000..87736e2ae --- /dev/null +++ b/packages/service-provider-sfa/src/index.ts @@ -0,0 +1 @@ +export { default, default as SfaServiceProvider } from "./SfaServiceProvider"; diff --git a/packages/service-provider-sfa/src/interfaces.ts b/packages/service-provider-sfa/src/interfaces.ts new file mode 100644 index 000000000..8f955966f --- /dev/null +++ b/packages/service-provider-sfa/src/interfaces.ts @@ -0,0 +1,30 @@ +import { type ServiceProviderArgs } from "@tkey/common-types"; +import { type TORUS_NETWORK_TYPE } from "@toruslabs/constants"; + +export interface Web3AuthOptions { + clientId: string; + network: TORUS_NETWORK_TYPE; +} +export interface SfaServiceProviderArgs extends ServiceProviderArgs { + web3AuthOptions: Web3AuthOptions; +} + +export interface TorusSubVerifierInfo { + verifier: string; + idToken: string; +} + +export type AggregateVerifierParams = { + verify_params: { verifier_id: string; idtoken: string }[]; + sub_verifier_ids: string[]; + verifier_id: string; +}; + +export type LoginParams = { + verifier: string; + verifierId: string; + idToken: string; + subVerifierInfoArray?: TorusSubVerifierInfo[]; + // offset in seconds + serverTimeOffset?: number; +}; diff --git a/packages/service-provider-sfa/test/.eslintrc.json b/packages/service-provider-sfa/test/.eslintrc.json new file mode 100644 index 000000000..955546bd7 --- /dev/null +++ b/packages/service-provider-sfa/test/.eslintrc.json @@ -0,0 +1,7 @@ +{ + "rules": { + "prefer-arrow-callback": "off", + "func-names": "off" + } + +} diff --git a/packages/service-provider-sfa/test/test.js b/packages/service-provider-sfa/test/test.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/service-provider-sfa/torus.config.js b/packages/service-provider-sfa/torus.config.js new file mode 100644 index 000000000..66be7083b --- /dev/null +++ b/packages/service-provider-sfa/torus.config.js @@ -0,0 +1 @@ +module.exports = require("../../torus.config"); diff --git a/packages/service-provider-sfa/tsconfig.json b/packages/service-provider-sfa/tsconfig.json new file mode 100644 index 000000000..ef502e89c --- /dev/null +++ b/packages/service-provider-sfa/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src", "test"] +} diff --git a/packages/service-provider-sfa/webpack.config.js b/packages/service-provider-sfa/webpack.config.js new file mode 100644 index 000000000..f0607506c --- /dev/null +++ b/packages/service-provider-sfa/webpack.config.js @@ -0,0 +1,6 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +const generateWebpackConfig = require("../../webpack.config"); + +const config = generateWebpackConfig({ }); + +exports.baseConfig = config.baseConfig; diff --git a/packages/service-provider-torus/src/TorusServiceProvider.ts b/packages/service-provider-torus/src/TorusServiceProvider.ts index ab0c3c4e2..7c47c827a 100644 --- a/packages/service-provider-torus/src/TorusServiceProvider.ts +++ b/packages/service-provider-torus/src/TorusServiceProvider.ts @@ -10,7 +10,7 @@ import CustomAuth, { TorusHybridAggregateLoginResponse, TorusLoginResponse, } from "@toruslabs/customauth"; -import Torus from "@toruslabs/torus.js"; +import Torus, { TorusKey } from "@toruslabs/torus.js"; import BN from "bn.js"; class TorusServiceProvider extends ServiceProviderBase { @@ -18,6 +18,10 @@ class TorusServiceProvider extends ServiceProviderBase { singleLoginKey: BN; + public torusKey: TorusKey; + + public migratableKey: BN | null = null; // Migration of key from SFA to tKey + customAuthArgs: CustomAuthArgs; constructor({ enableLogging = false, postboxKey, customAuthArgs }: TorusServiceProviderArgs) { @@ -51,6 +55,14 @@ class TorusServiceProvider extends ServiceProviderBase { // `obj` maybe `null` in redirect mode. if (obj) { const localPrivKey = Torus.getPostboxKey(obj); + this.torusKey = obj; + + if (!obj.metadata.upgraded) { + const { finalKeyData, oAuthKeyData } = obj; + const privKey = finalKeyData.privKey || oAuthKeyData.privKey; + this.migratableKey = new BN(privKey, "hex"); + } + this.postboxKey = new BN(localPrivKey, "hex"); } @@ -63,9 +75,16 @@ class TorusServiceProvider extends ServiceProviderBase { async triggerAggregateLogin(params: AggregateLoginParams): Promise { const obj = await this.customAuthInstance.triggerAggregateLogin(params); - // `obj` maybe `null` in redirect mode. if (obj) { const localPrivKey = Torus.getPostboxKey(obj); + this.torusKey = obj; + + if (!obj.metadata.upgraded) { + const { finalKeyData, oAuthKeyData } = obj; + const privKey = finalKeyData.privKey || oAuthKeyData.privKey; + this.migratableKey = new BN(privKey, "hex"); + } + this.postboxKey = new BN(localPrivKey, "hex"); } return obj; @@ -76,6 +95,7 @@ class TorusServiceProvider extends ServiceProviderBase { */ async triggerHybridAggregateLogin(params: HybridAggregateLoginParams): Promise { const obj = await this.customAuthInstance.triggerHybridAggregateLogin(params); + this.torusKey = null; // Since there are multiple keys, we don't set the torusKey here. // `obj` maybe `null` in redirect mode. if (obj) { diff --git a/test/setup.mjs b/test/setup.mjs index 2975e570b..97b565ccb 100644 --- a/test/setup.mjs +++ b/test/setup.mjs @@ -18,14 +18,13 @@ Register({ rootMode: "upward", }); - const storeFn = { getItem(key) { - return this[key] + return this[key]; }, setItem(key, value) { - this[key] = value + this[key] = value; }, -} -globalThis.localStorage = { ...storeFn } -globalThis.sessionStorage = { ...storeFn } \ No newline at end of file +}; +globalThis.localStorage = { ...storeFn }; +globalThis.sessionStorage = { ...storeFn };