diff --git a/.prettierrc b/.prettierrc index 548c7c2..cfab77c 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,4 +3,4 @@ "singleQuote": true, "tabWidth": 2, "endOfLine": "auto" -} \ No newline at end of file +} diff --git a/README.md b/README.md index a050b1a..a6c9b4c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ # Zero-Knowledge 2-Factor Authentication 🗝️ (zk-2FA) - The goal of this project is to provide 2FA for EVM compatible blockchains. -We follow a parallel approach for a twofold Authentication solution. The first implements the popular and broadly adopted TOTP 2FA with a trusted validator. The second solution implements a password-generator based zk proof, which is validated onChain providing a zero-trust security level. +We follow a parallel approach for a twofold Authentication solution. The first implements the popular and broadly adopted TOTP 2FA with a trusted validator. The second solution implements a password-generator based zk proof, which is validated onChain providing a zero-trust security level. -Further we provide a dapp to facilitate user-interaction with our smrt-contracts. All dapp interactions can likewise be performed manually per console. +Further we provide a dapp to facilitate user-interaction with our smrt-contracts. All dapp interactions can likewise be performed manually per console. ## TOTP 2FA @@ -15,7 +14,6 @@ A picturesque flow-chart of our TOTP 2FA solution: **Artworq in the making** - ## Contribute Feedback and contributions are always welcome 🤗 diff --git a/backend/package.json b/backend/package.json index 29f257b..b6ebfbd 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,7 +1,7 @@ { "name": "hardhat-project", "scripts": { - "prettier": "prettier --write . --config ../.prettierrc" + "prettier": "prettier --write ../ --config ../.prettierrc" }, "devDependencies": { "@nomicfoundation/hardhat-chai-matchers": "^1.0.3", diff --git a/dapp/components/CardChoice.tsx b/dapp/components/CardChoice.tsx new file mode 100644 index 0000000..7cfad36 --- /dev/null +++ b/dapp/components/CardChoice.tsx @@ -0,0 +1,69 @@ +import { useTheme } from 'next-themes' +import Image from 'next/image' + +interface CardChoiceProps { + authType: string + setAuthType: (arg: string) => void +} + +const CardChoice = (props: CardChoiceProps) => { + const { theme } = useTheme() + return ( + { + totp: ( + + ), + zk: ( + + ), + }[props.authType] || <> + ) +} + +export default CardChoice diff --git a/dapp/components/DropdownAccount.tsx b/dapp/components/DropdownAccount.tsx new file mode 100644 index 0000000..a2e8068 --- /dev/null +++ b/dapp/components/DropdownAccount.tsx @@ -0,0 +1,96 @@ +/* This example requires Tailwind CSS v2.0+ */ +import { Fragment } from 'react' +import { Menu, Transition } from '@headlessui/react' +import { + ArrowTopRightOnSquareIcon, + ArrowRightOnRectangleIcon, +} from '@heroicons/react/20/solid' +import { shortenAddress, useEthers, useLookupAddress } from '@usedapp/core' + +function classNames(...classes: any) { + return classes.filter(Boolean).join(' ') +} + +interface DropdownProps { + account: string +} + +const DropdownAccount = (props: DropdownProps) => { + const { deactivate } = useEthers() + const { ens } = useLookupAddress(props.account) + + return ( + +
+ + {ens ?? shortenAddress(props.account)} + +
+ + + +
+ + {({ active }) => ( + + + )} + + {/* + {({ active }) => ( + deactivate()} + > + + )} + */} +
+
+
+
+ ) +} + +export default DropdownAccount diff --git a/dapp/components/Layout.tsx b/dapp/components/Layout.tsx new file mode 100644 index 0000000..b65a4c5 --- /dev/null +++ b/dapp/components/Layout.tsx @@ -0,0 +1,16 @@ +import { Navbar } from './' +import Head from 'next/head' + +export default function Layout(props: any) { + return ( + <> + + zkAuth + + + + +
{props.children}
+ + ) +} diff --git a/dapp/components/modalVerify.tsx b/dapp/components/ModalVerifyTotp.tsx similarity index 98% rename from dapp/components/modalVerify.tsx rename to dapp/components/ModalVerifyTotp.tsx index 3349b9e..494d88b 100644 --- a/dapp/components/modalVerify.tsx +++ b/dapp/components/ModalVerifyTotp.tsx @@ -6,12 +6,12 @@ import { CheckIcon, } from '@heroicons/react/24/outline' -interface ModalVerifyProps { +interface ModalVerifyTotpProps { verified: boolean verifyCode: any } -const ModalVerify = (props: ModalVerifyProps) => { +const ModalVerifyTotp = (props: ModalVerifyTotpProps) => { const [open, setOpen] = useState(false) const onSubmit = (e: React.UIEvent) => { e.preventDefault() @@ -135,4 +135,4 @@ const ModalVerify = (props: ModalVerifyProps) => { ) } -export default ModalVerify +export default ModalVerifyTotp diff --git a/dapp/components/TotpSetup.tsx b/dapp/components/TotpSetup.tsx new file mode 100644 index 0000000..8d42882 --- /dev/null +++ b/dapp/components/TotpSetup.tsx @@ -0,0 +1,123 @@ +import { ArrowUturnLeftIcon } from '@heroicons/react/24/outline' +import { useEthers } from '@usedapp/core' +import { useState, useRef, useEffect } from 'react' +import { usePinInput, PinInputActions } from 'react-pin-input-hook' +import { ModalVerifyTotp, QrCodeAuth } from './' + +var jsotp = require('jsotp') +var base32 = require('thirty-two') + +interface TotpSetupProps { + setAuthType: (arg: string) => void +} + +const TotpSetup = (props: TotpSetupProps) => { + const { account, library: provider } = useEthers() + + // Secret State Management + const [secret, setSecret] = useState('') + const [blur, setBlur] = useState('opacity-50 blur-sm') + const loadSecret = async (e: React.FormEvent) => { + e.preventDefault() + if (provider != undefined) { + const signer = provider.getSigner() + const signature = await signer.signMessage('zkAuth') // TODO: Random Input? ZK? + const secretEncoded = base32 + .encode(signature) + .toString() + .replace(/=/g, '') + setSecret(secretEncoded) + setBlur('') + } + } + + // PIN state management + const [verified, setVerified] = useState(false) + const [pin, setPin] = useState(['', '', '', '', '', '']) + const [error, setError] = useState(false) + const actionRef = useRef(null) + const { fields } = usePinInput({ + values: pin, + onChange: setPin, + error, + actionRef, + placeholder: '•', + }) + + function verifyCode(e: React.FormEvent) { + e.preventDefault() + // Check if there is at least one empty field. If there is, the input is considered empty. + if (pin.includes('')) { + // Setting the error. + setError(true) + // We set the focus on the first empty field if `error: true` was passed as a parameter in `options`. + actionRef.current?.focus() + } + const verifier = jsotp.TOTP(secret) + if (verifier.verify(pin.join(''))) { + setVerified(true) + } else { + setVerified(false) + } + } + + if (!account) return null + return ( +
+
+ +
+
+
+ Set up your TOTP +
+
+
+ +
+ {secret == '' ? ( + + ) : ( + <> + )} +
+ +
+ + +
+ {fields.map((propsField, index) => ( + + ))} +
+
+ + + +
+ ) +} + +export default TotpSetup diff --git a/dapp/components/ZkPasswordSetup.tsx b/dapp/components/ZkPasswordSetup.tsx new file mode 100644 index 0000000..7eaa718 --- /dev/null +++ b/dapp/components/ZkPasswordSetup.tsx @@ -0,0 +1,112 @@ +import { + ArrowUturnLeftIcon, + CheckIcon, + XMarkIcon, +} from '@heroicons/react/24/outline' +import { useEthers } from '@usedapp/core' +import { useState } from 'react' + +import dynamic from 'next/dynamic' +const PasswordChecklist = dynamic(import('react-password-checklist'), { + ssr: false, +}) +interface ZkSetupProps { + setAuthType: (arg: string) => void +} +const ZkPasswordSetup = (props: ZkSetupProps) => { + const { account, library: provider } = useEthers() + + // PIN state management + const [password, setPassword] = useState('') + const [passwordAgain, setPasswordAgain] = useState('') + const [isValid, setIsValid] = useState(false) + + // Set password to blockchain + const onSubmit = (e: React.FormEvent) => { + e.preventDefault() + } + + if (!account) return null + return ( +
+
+ +
+
+
+ Set up zk-Password +
+
+ +
+ + setPassword(e.target.value)} + /> +
+
+ + setPasswordAgain(e.target.value)} + /> +
+
+ , + InvalidIcon: , + }} + className="flex flex-col text-sm align-middle" + onChange={(isValid) => { + setIsValid(isValid) + }} + /> +
+ +
+
+ ) +} + +export default ZkPasswordSetup diff --git a/dapp/components/connectWalletButton.tsx b/dapp/components/connectWalletButton.tsx index 810529d..11d1889 100644 --- a/dapp/components/connectWalletButton.tsx +++ b/dapp/components/connectWalletButton.tsx @@ -3,6 +3,7 @@ import { useEthers } from '@usedapp/core' import Web3Modal from 'web3modal' import WalletConnectProvider from '@walletconnect/web3-provider' import { useTheme } from 'next-themes' +import { ethers } from 'ethers' const ConnectWalletButton = () => { const { theme } = useTheme() @@ -28,6 +29,7 @@ const ConnectWalletButton = () => { const [loaded, setLoaded] = useState(false) useEffect(() => setLoaded(true), []) + // Set up Web3modal const [web3Modal, setWeb3Modal] = useState(undefined) useEffect(() => { const providerOptions = { @@ -47,22 +49,16 @@ const ConnectWalletButton = () => { }, } - const newWeb3Modal = new Web3Modal({ - providerOptions, - cacheProvider: false, - }) - setWeb3Modal(newWeb3Modal) - }, []) + if (loaded) { + const newWeb3Modal = new Web3Modal({ + providerOptions, + cacheProvider: false, + theme: theme, + }) - // const connect = async () => { - // try { - // const provider = await web3Modal?.connect() - // await activate(provider) - // setActivateError("") - // } catch (error: any) { - // setActivateError(error.message) - // } - // } + setWeb3Modal(newWeb3Modal) + } + }, [loaded, theme]) const connect = useCallback(async () => { try { @@ -74,12 +70,22 @@ const ConnectWalletButton = () => { } }, [web3Modal, activate]) - // useEffect(() => { - // if (web3Modal && web3Modal.cachedProvider) { - // console.log(web3Modal.cachedProvider) - // connect() - // } - // }, [connect, web3Modal]) + // Set up provider if already connected + useEffect(() => { + const { ethereum } = window + const checkMetaMaskConnected = async () => { + var provider = new ethers.providers.Web3Provider(ethereum) + const accounts = await provider.listAccounts() + const connected = accounts.length > 0 + console.log('CONNECTED', connected) + if (connected) { + activate(provider) + } + } + if (ethereum) { + checkMetaMaskConnected() + } + }) if (!loaded) return null diff --git a/dapp/components/index.ts b/dapp/components/index.ts new file mode 100644 index 0000000..c99178f --- /dev/null +++ b/dapp/components/index.ts @@ -0,0 +1,11 @@ +export { default as ConnectWalletButton } from './ConnectWalletButton' +export { default as DropdownAccount } from './DropdownAccount' +export { default as LogInBox } from './LogInBox' +export { default as ModalVerifyTotp } from './ModalVerifyTotp' +export { default as Navbar } from './Navbar' +export { default as QrCodeAuth } from './QrCodeAuth' +export { default as ToggleColorMode } from './ToggleColorMode' +export { default as TotpSetup } from './TotpSetup' +export { default as ZkPasswordSetup } from './ZkPasswordSetup' +export { default as CardChoice } from './CardChoice' +export { default as Layout } from './Layout' diff --git a/dapp/components/logInBox.tsx b/dapp/components/logInBox.tsx index ea8ca78..855f803 100644 --- a/dapp/components/logInBox.tsx +++ b/dapp/components/logInBox.tsx @@ -1,123 +1,72 @@ -import { useEthers, shortenAddress, useLookupAddress } from '@usedapp/core' -import { ethers } from 'ethers' -import { useState, useRef, useEffect } from 'react' -import { usePinInput, PinInputActions } from 'react-pin-input-hook' -import ConnectWalletButton from './connectWalletButton' -import ModalVerify from './modalVerify' -import QrCodeAuth from './qrCodeAuth' +import { useEthers } from '@usedapp/core' +import { useState } from 'react' +import { motion } from 'framer-motion' -var jsotp = require('jsotp') -var base32 = require('thirty-two') +import { ConnectWalletButton, TotpSetup, ZkPasswordSetup, CardChoice } from './' const LogInBox = () => { const { account, library: provider } = useEthers() - const { ens } = useLookupAddress(account) - - // Secret State Management - const [secret, setSecret] = useState('') - const [blur, setBlur] = useState('opacity-50 blur-sm') - const loadSecret = async (e: React.FormEvent) => { - e.preventDefault() - if (provider != undefined) { - const signer = provider.getSigner() - const signature = await signer.signMessage('zkAuth') // TODO: Random Input? ZK? - const secretEncoded = base32 - .encode(signature) - .toString() - .replace(/=/g, '') - setSecret(secretEncoded) - setBlur('') - } - } - - // PIN state management - const [verified, setVerified] = useState(false) - const [pin, setPin] = useState(['', '', '', '', '', '']) - const [error, setError] = useState(false) - const actionRef = useRef(null) - const { fields } = usePinInput({ - values: pin, - onChange: setPin, - error, - actionRef, - placeholder: '•', - }) - - function verifyCode(e: React.FormEvent) { - e.preventDefault() - // Check if there is at least one empty field. If there is, the input is considered empty. - if (pin.includes('')) { - // Setting the error. - setError(true) - // We set the focus on the first empty field if `error: true` was passed as a parameter in `options`. - actionRef.current?.focus() - } - const verifier = jsotp.TOTP(secret) - if (verifier.verify(pin.join(''))) { - setVerified(true) - } else { - setVerified(false) - } - } + const [authType, setAuthType] = useState('') return ( <> {!account ? ( -
+ -
+ ) : ( -
-
-
- Sign in to our platform -
- - {ens ?? shortenAddress(account)} -
-
- -
- {secret == '' ? ( - - ) : ( - <> - )} -
- -
- - -
- {fields.map((propsField, index) => ( - - ))} -
-
- - - -
+ + + ), + zk: ( + + + + ), + }[authType] || ( +
+ + + + + + +
+ ) )} ) diff --git a/dapp/components/navbar.tsx b/dapp/components/navbar.tsx index 8c1d1bc..d39f844 100644 --- a/dapp/components/navbar.tsx +++ b/dapp/components/navbar.tsx @@ -1,18 +1,20 @@ -import ToggleColorMode from './toggleColorMode' +import { useEthers } from '@usedapp/core' +import { DropdownAccount, ToggleColorMode } from './' const Navbar = () => { + const { account } = useEthers() + return ( -