From 5d2bfee727f5458fa6ff89300caeb3db40bd8200 Mon Sep 17 00:00:00 2001 From: Guru Date: Wed, 11 Dec 2024 02:56:20 +0530 Subject: [PATCH] feat: mfa card --- .../authenticator/AuthenticatorScan.tsx | 260 ++++++++++++++++++ .../authenticator/AuthenticatorVerify.tsx | 72 +++++ .../mfa-cards/mnemonic/CreateMnemonic.tsx | 75 +++++ .../mfa-cards/mnemonic/VerifyMnemonic.tsx | 64 +++++ .../mfa-cards/password/ConfirmPassword.tsx | 71 +++++ .../mfa-cards/password/CreatePassword.tsx | 73 +++++ .../mfa-cards/recovery/RecoveryOptionCard.tsx | 33 +++ 7 files changed, 648 insertions(+) create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorVerify.tsx create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/mnemonic/VerifyMnemonic.tsx create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/password/ConfirmPassword.tsx create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/password/CreatePassword.tsx create mode 100644 demo/redirect-flow-example/src/components/mfa-cards/recovery/RecoveryOptionCard.tsx diff --git a/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx b/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx new file mode 100644 index 0000000..4218006 --- /dev/null +++ b/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorScan.tsx @@ -0,0 +1,260 @@ +import * as React from "react"; +import { Button } from "../../Button"; +import { Card } from "../../Card"; +import crypto from "crypto"; +import base32 from "hi-base32"; +import { useCoreKit } from "../../../composibles/useCoreKit"; +import url from "url"; +import QRCode from "react-qr-code"; +import { COREKIT_STATUS, FactorKeyTypeShareDescription, generateFactorKey, TssShareType } from "@web3auth/mpc-core-kit"; +import axios from "axios"; +import BN from "bn.js"; +import { Point } from "@tkey/common-types"; +import { TextField } from "../../TextField"; +import { getEcCrypto } from "../../../App"; +import { keccak256 } from "ethereum-cryptography/keccak"; + +const AuthenticatorQRCodeCard: React.FC = () => { + const { coreKitInstance, setDrawerHeading, setDrawerInfo } = useCoreKit(); + const [imageUrl, setImageUrl] = React.useState(""); + const [currentStep, setCurrentStep] = React.useState("register"); + const [secretKey, setSecretKey] = React.useState(""); + const authenticatorVerifierUrl = "https://authenticator.web3auth.com"; + const [factorKey, setFactorKey] = React.useState<{ private: BN; pub: Point } | null>(null); + const [code, setCode] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(false); + + const otpauthURL = (options: any) => { + // unpack options + let secret = options.secret; + const label = options.label; + const issuer = options.issuer; + const type = (options.type || "totp").toLowerCase(); + const counter = options.counter; + const algorithm = (options.algorithm || "sha1").toLowerCase(); + const digits = options.digits || 6; + let period = options.period || 30; + // not so sure about this + const encoding = options.encoding || "ascii"; + + // validate type + switch (type) { + case "totp": + case "hotp": + break; + default: + throw new Error("otpauthURL - Invalid type `" + type + "`; must be `hotp` or `totp`"); + } + + // validate required options + if (!secret) throw new Error("otpauthURL - Missing secret"); + if (!label) throw new Error("otpauthURL - Missing label"); + + // require counter for HOTP + if (type === "hotp" && (counter === null || typeof counter === "undefined")) { + throw new Error("otpauthURL - Missing counter value for HOTP"); + } + + // convert secret to base32 + if (encoding !== "base32") secret = new Buffer(secret, encoding); + if (Buffer.isBuffer(secret)) secret = base32.encode(secret); + + // build query while validating + const query: any = { secret: secret }; + if (issuer) query.issuer = issuer; + if (type === "hotp") { + query.counter = counter; + } + + // validate algorithm + if (algorithm !== null) { + switch (algorithm.toUpperCase()) { + case "SHA1": + case "SHA256": + case "SHA512": + break; + default: + console.warn("otpauthURL - Warning - Algorithm generally should be SHA1, SHA256, or SHA512"); + } + query.algorithm = algorithm.toUpperCase(); + } + + // validate digits + if (digits !== null) { + if (isNaN(digits)) { + throw new Error("otpauthURL - Invalid digits `" + digits + "`"); + } else { + switch (parseInt(digits, 10)) { + case 6: + case 8: + break; + default: + console.warn("otpauthURL - Warning - Digits generally should be either 6 or 8"); + } + } + query.digits = digits; + } + + // validate period + if (period !== null) { + period = parseInt(period, 10); + if (~~period !== period) { + throw new Error("otpauthURL - Invalid period `" + period + "`"); + } + query.period = period; + } + + // return url + return url.format({ + protocol: "otpauth", + slashes: true, + hostname: type, + pathname: encodeURIComponent(label), + query: query, + }); + }; + + const generateSecretASCII = function generateSecretASCII() { + const bytes = crypto.randomBytes(20); + return base32.encode(bytes).toString().replace(/=/g, ""); + }; + + const generateSecretKey = async () => { + const key = generateSecretASCII(); + const userInfo = coreKitInstance.getUserInfo(); + const totpURL = otpauthURL({ + secret: key, + label: userInfo?.verifierId, + issuer: "trust wallet", + encoding: "base32", + }); + return { url, key, totpURL }; + }; + + React.useEffect(() => { + const init = async () => { + const { totpURL, key } = await generateSecretKey(); + const factorKey = generateFactorKey(); + setFactorKey(factorKey); + setImageUrl(totpURL); + setSecretKey(key); + }; + init(); + }, []); + + const verifyNewAuthenticator = async () => { + setIsLoading(true); + try { + if (!code) { + return; + } + const authenticatorFactorMetadataToSet = { + factorKey: factorKey?.private.toString("hex"), + }; + const pubKey = getEcCrypto().keyFromPublic(coreKitInstance.getPubKey()).getPublic(); + + const { data } = await axios.post<{ data: { id: string } }>(`${authenticatorVerifierUrl}/api/v1/verify`, { + address: `${pubKey?.getX().toString("hex") ?? ""}${pubKey?.getY()?.toString("hex") ?? ""}`, + code, + data: authenticatorFactorMetadataToSet, + }); + console.log(data); + await creatAuthFactor(); + setDrawerHeading("Authenticator"); + setDrawerInfo("Authenticator has been set successfully"); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + }; + + const registerNewAuthenticator = async () => { + setIsLoading(true); + try { + const precomputedTssClient = await coreKitInstance.precompute_secp256k1(); + const sig = await coreKitInstance.sign(Buffer.from(keccak256(Buffer.from(secretKey, "utf-8"))), true, precomputedTssClient); + const pubKey = getEcCrypto().keyFromPublic(coreKitInstance.getPubKey()).getPublic(); + // Extract r, s, and v from the signature buffer + const sigUint8Array = new Uint8Array(sig); + + // Extract r, s, and v from the signature buffer + const r = Buffer.from(sigUint8Array.slice(0, 32)).toString("hex"); + const s = Buffer.from(sigUint8Array.slice(32, 64)).toString("hex"); + const v = sigUint8Array[64]; + + const { data } = await axios.post<{ data: { id: string } }>(`${authenticatorVerifierUrl}/api/v1/register`, { + pubKey: { + x: pubKey.getX().toString("hex"), + y: pubKey.getY().toString("hex"), + }, + secretKey, + sig: { + r: r, + s: s, + v: v.toString(16), // Convert v to hex string + }, + }); + setCurrentStep("verify"); + return data; + } catch (error) { + console.error(error); + throw error; + } finally { + setIsLoading(false); + } + }; + + const creatAuthFactor = async (): Promise => { + if (!coreKitInstance || !secretKey) { + throw new Error("required fields are not set"); + } + + // await coreKitInstance.createFactor({ + // shareType: TssShareType.DEVICE, + // factorKey: factorKey?.private, + // }); + await coreKitInstance.enableMFA({ + factorKey: factorKey?.private, + additionalMetadata: { shareType: TssShareType.RECOVERY.toString() }, + shareDescription: FactorKeyTypeShareDescription.Other, + }); + + if (coreKitInstance.status === COREKIT_STATUS.LOGGED_IN) { + await coreKitInstance.commitChanges(); + } + // await inputBackupFactorKey(mnemonic); + }; + + return ( + <> + {currentStep === "register" ? ( + +
+

Authenticator QR Code

+ {imageUrl ? ( + + ) : ( + QR Code + )} + +
+
+ ) : ( + +
+

Verify Authenticator Code

+ setCode(e.target.value)} label="6 Digit Code" placeholder="Enter code" className="mb-4" /> + +
+
+ )} + + ); +}; + +export { AuthenticatorQRCodeCard }; diff --git a/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorVerify.tsx b/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorVerify.tsx new file mode 100644 index 0000000..ba275dc --- /dev/null +++ b/demo/redirect-flow-example/src/components/mfa-cards/authenticator/AuthenticatorVerify.tsx @@ -0,0 +1,72 @@ +import * as React from "react"; +import { Button } from "../../Button"; +import { Card } from "../../Card"; +import { TextField } from "../../TextField"; +import { getEcCrypto } from "../../../App"; +import { useCoreKit } from "../../../composibles/useCoreKit"; +import axios from "axios"; +import { useNavigate } from "react-router-dom"; +import Loader from "../../Loader"; + +const VerifyAuthenticatorCodeCard: React.FC = () => { + const authenticatorVerifierUrl = "https://authenticator.web3auth.com"; + const [code, setCode] = React.useState(""); + const { coreKitInstance, inputBackupFactorKey } = useCoreKit(); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = React.useState(false); + + const verifyExistingAuthenticator = async () => { + setIsLoading(true); + try { + if (!coreKitInstance || !code) { + throw new Error("coreKitInstance is not set"); + } + + // const pubKey = getEcCrypto().keyFromPublic(coreKitInstance.getPubKey()).getPublic(); + const pubKey = coreKitInstance.tKey.getTSSPub().toSEC1(coreKitInstance.tKey.tssCurve, false); + const pubKey2 = getEcCrypto().keyFromPublic(pubKey).getPublic(); + + const { + data: { data: dataFactor }, + } = await axios.post(`${authenticatorVerifierUrl}/api/v1/verify`, { + address: `${pubKey2.getX()?.toString("hex") ?? ""}${pubKey2.getY()?.toString("hex") ?? ""}`, + // address: `${coreKitInstance.tKey.metadata.pubKey.x?.toString("hex") ?? ""}${coreKitInstance.tKey.metadata.pubKey.y?.toString("hex") ?? ""}`, + code, + }); + console.log(dataFactor.factorKey); + await inputBackupFactorKey(dataFactor.factorKey); + navigate("/"); + } catch (error: any) { + console.error(error); + if (error.response && error.response.data && error.response.data.code) throw new Error("Generic error"); + throw error; + } finally { + setIsLoading(false); + } + }; + return ( +
+ {isLoading ? ( + <> +
+ +
+ + ) : ( +
+ +
+

Verify Authenticator Code

+ setCode(e.target.value)} label="6 Digit Code" placeholder="Enter code" className="mb-4" /> + +
+
+
+ )} +
+ ); +}; + +export { VerifyAuthenticatorCodeCard }; diff --git a/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx b/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx new file mode 100644 index 0000000..c3b5c0a --- /dev/null +++ b/demo/redirect-flow-example/src/components/mfa-cards/mnemonic/CreateMnemonic.tsx @@ -0,0 +1,75 @@ +import * as React from "react"; +import { Button } from "../../Button"; +import { Card } from "../../Card"; +import { useCoreKit } from "../../../composibles/useCoreKit"; +import { COREKIT_STATUS, FactorKeyTypeShareDescription, generateFactorKey, keyToMnemonic, TssShareType } from "@web3auth/mpc-core-kit"; +import BN from "bn.js"; + +const CreateMnemonicPhraseCard: React.FC = () => { + const { coreKitInstance, setDrawerHeading, setDrawerInfo } = useCoreKit(); + const [mnemonic, setMnemonic] = React.useState(""); + const [factorKey, setFactorKey] = React.useState(null); + const [isLoading, setIsLoading] = React.useState(false); + + React.useEffect(() => { + const factorKey = generateFactorKey(); + const factorKeyMnemonic = keyToMnemonic(factorKey.private.toString("hex")); + setMnemonic(factorKeyMnemonic); + setFactorKey(factorKey.private); + }, [coreKitInstance]); + + const createMnemonicFactor = async (): Promise => { + setIsLoading(true); + try { + if (!coreKitInstance || !factorKey) { + throw new Error("required fields are not set"); + } + + // await coreKitInstance.createFactor({ + // shareType: TssShareType.RECOVERY, + // factorKey: factorKey, + // }); + await coreKitInstance.enableMFA({ + factorKey: factorKey, + additionalMetadata: { shareType: TssShareType.RECOVERY.toString() }, + shareDescription: FactorKeyTypeShareDescription.SeedPhrase, + }); + // await coreKitInstance.enableMFA({}, false) + + console.log("created mnemonice factor share"); + if (coreKitInstance.status === COREKIT_STATUS.LOGGED_IN) { + await coreKitInstance.commitChanges(); + } + setDrawerHeading("Seed Phrase"); + setDrawerInfo("Seed phrase has been set successfully"); + } catch (error) { + console.error(error); + } finally { + setIsLoading(false); + } + // await inputBackupFactorKey(mnemonic); + }; + + return ( + +
+
+

Enter Mnemonic Phrase

+