Skip to content

Commit

Permalink
fix: refactor, remove console.logs, new test
Browse files Browse the repository at this point in the history
  • Loading branch information
schmanu committed Oct 4, 2023
1 parent 6a1edf0 commit fad177c
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 147 deletions.
2 changes: 0 additions & 2 deletions src/components/common/ConnectWallet/MPCWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ export const MPCWallet = () => {
const { loginPending, triggerLogin, resetAccount, userInfo, walletState, recoverFactorWithPassword } =
useContext(MpcWalletContext)

console.log(walletState)

return (
<>
{userInfo.email ? (
Expand Down
47 changes: 25 additions & 22 deletions src/components/settings/SignerAccountMFA/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import { COREKIT_STATUS } from '@web3auth/mpc-core-kit'
import { getPubKeyPoint } from '@tkey-mpc/common-types'
import { BN } from 'bn.js'
import { useEffect, useMemo, useState } from 'react'
import { useSecurityQuestions } from '@/hooks/wallets/mpc/recovery/useSecurityQuestions'
import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery'
import useMFASettings from './useMFASettings'
import { useForm } from 'react-hook-form'
import { useDeviceShare } from '@/hooks/wallets/mpc/recovery/useDeviceShare'
import { DeviceShareRecovery } from '@/hooks/wallets/mpc/recovery/DeviceShareRecovery'

type SignerAccountFormData = {
oldPassword: string | undefined
Expand All @@ -19,8 +19,6 @@ type SignerAccountFormData = {
const SignerAccountMFA = () => {
const mpcCoreKit = useMPC()
const mfaSettings = useMFASettings(mpcCoreKit)
const securityQuestions = useSecurityQuestions(mpcCoreKit)
const deviceShareModule = useDeviceShare(mpcCoreKit)

const formMethods = useForm<SignerAccountFormData>({
mode: 'all',
Expand All @@ -30,19 +28,27 @@ const SignerAccountMFA = () => {

const [enablingMFA, setEnablingMFA] = useState(false)

const isPasswordSet = useMemo(() => securityQuestions.isEnabled(), [securityQuestions])

console.log(mpcCoreKit)
const isPasswordSet = useMemo(() => {
if (!mpcCoreKit) {
return false
}
const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit)
return securityQuestions.isEnabled()
}, [mpcCoreKit])

useEffect(() => {
deviceShareModule.isEnabled().then((value) => setValue('storeDeviceShare', value))
}, [deviceShareModule, setValue])
if (!mpcCoreKit) {
return
}
new DeviceShareRecovery(mpcCoreKit).isEnabled().then((value) => setValue('storeDeviceShare', value))
}, [mpcCoreKit, setValue])

const enableMFA = async () => {
if (!mpcCoreKit) {
return
}

const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit)
const deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit)
setEnablingMFA(true)
try {
const { newPassword, oldPassword, storeDeviceShare } = formMethods.getValues()
Expand All @@ -63,16 +69,16 @@ const SignerAccountMFA = () => {
await mpcCoreKit.deleteFactor(recoverPubKey, recoverKey)
}

const hasDeviceShare = await deviceShareModule.isEnabled()
const hasDeviceShare = await deviceShareRecovery.isEnabled()

if (!hasDeviceShare && storeDeviceShare) {
await deviceShareModule.createAndStoreDeviceFactor()
await deviceShareRecovery.createAndStoreDeviceFactor()
}

if (hasDeviceShare && !storeDeviceShare) {
// Switch to password recovery factor such that we can delete the device factor
await mpcCoreKit.inputFactorKey(new BN(securityQuestionFactor, 'hex'))
await deviceShareModule.removeDeviceFactor()
await deviceShareRecovery.removeDeviceFactor()
}
} catch (error) {
console.error(error)
Expand All @@ -90,20 +96,17 @@ const SignerAccountMFA = () => {
}

const onSubmit = async () => {
console.log('submitting')
await enableMFA()
}

return (
<form onSubmit={handleSubmit(onSubmit)}>
<Box display="flex" flexDirection="column" gap={3} alignItems="baseline">
{
/* TODO: Memoize this*/ securityQuestions.isEnabled() ? (
<Typography>You already have a recovery password setup.</Typography>
) : (
<Typography>You have no password setup. Secure your account now!</Typography>
)
}
{isPasswordSet ? (
<Typography>You already have a recovery password setup.</Typography>
) : (
<Typography>You have no password setup. Secure your account now!</Typography>
)}
{isPasswordSet && (
<TextField
placeholder="Old password"
Expand All @@ -112,7 +115,7 @@ const SignerAccountMFA = () => {
error={!!formState.errors['oldPassword']}
helperText={formState.errors['oldPassword']?.message}
{...register('oldPassword', {
required: securityQuestions.isEnabled(),
required: true,
})}
/>
)}
Expand Down
116 changes: 112 additions & 4 deletions src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { act, renderHook, waitFor } from '@/tests/test-utils'
import { MPCWalletState, useMPCWallet } from '../useMPCWallet'
import * as useOnboard from '@/hooks/wallets/useOnboard'
import { type OnboardAPI } from '@web3-onboard/core'
import { COREKIT_STATUS, type UserInfo, type OauthLoginParams, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit'
import {
COREKIT_STATUS,
type UserInfo,
type OauthLoginParams,
type Web3AuthMPCCoreKit,
type TssSecurityQuestion,
} from '@web3auth/mpc-core-kit'
import * as mpcCoreKit from '@web3auth/mpc-core-kit'
import { setMPCCoreKitInstance } from '../useMPC'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module'
Expand Down Expand Up @@ -54,6 +60,7 @@ describe('useMPCWallet', () => {
})
beforeEach(() => {
jest.resetAllMocks()
setMPCCoreKitInstance(undefined)
})
afterAll(() => {
jest.useRealTimers()
Expand Down Expand Up @@ -98,7 +105,9 @@ describe('useMPCWallet', () => {
expect(result.current.walletState === MPCWalletState.AUTHENTICATING)
expect(connectWalletSpy).not.toBeCalled()

jest.advanceTimersByTime(MOCK_LOGIN_TIME)
act(() => {
jest.advanceTimersByTime(MOCK_LOGIN_TIME)
})

await waitFor(() => {
expect(result.current.walletState === MPCWalletState.READY)
Expand Down Expand Up @@ -137,8 +146,9 @@ describe('useMPCWallet', () => {

expect(result.current.walletState === MPCWalletState.AUTHENTICATING)
expect(connectWalletSpy).not.toBeCalled()

jest.advanceTimersByTime(MOCK_LOGIN_TIME)
act(() => {
jest.advanceTimersByTime(MOCK_LOGIN_TIME)
})

await waitFor(() => {
expect(result.current.walletState === MPCWalletState.READY)
Expand All @@ -150,6 +160,42 @@ describe('useMPCWallet', () => {
})
})
})

it('should require manual share for MFA account without device share', async () => {
jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI)
const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve())
jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy)
setMPCCoreKitInstance(
new MockMPCCoreKit(COREKIT_STATUS.REQUIRED_SHARE, {
email: '[email protected]',
name: 'Test',
} as unknown as UserInfo) as unknown as Web3AuthMPCCoreKit,
)

// TODO: remove unnecessary cast if mpc core sdk gets updated
jest.spyOn(mpcCoreKit, 'getWebBrowserFactor').mockReturnValue(Promise.resolve(undefined as unknown as string))
jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({
getQuestion: () => 'SOME RANDOM QUESTION',
} as unknown as TssSecurityQuestion)

const { result } = renderHook(() => useMPCWallet())

act(() => {
result.current.triggerLogin()
})

expect(result.current.walletState === MPCWalletState.AUTHENTICATING)
expect(connectWalletSpy).not.toBeCalled()

act(() => {
jest.advanceTimersByTime(MOCK_LOGIN_TIME)
})

await waitFor(() => {
expect(result.current.walletState === MPCWalletState.MANUAL_RECOVERY)
expect(connectWalletSpy).not.toBeCalled()
})
})
})

describe('resetAccount', () => {
Expand Down Expand Up @@ -185,4 +231,66 @@ describe('useMPCWallet', () => {
})
})
})

describe('recoverFactorWithPassword', () => {
it('should throw if mpcCoreKit is not initialized', () => {
const { result } = renderHook(() => useMPCWallet())
expect(result.current.recoverFactorWithPassword('test', false)).rejects.toEqual(
new Error('MPC Core Kit is not initialized'),
)
})

it('should not recover if wrong password is entered', () => {
setMPCCoreKitInstance({
state: {
userInfo: undefined,
},
} as unknown as Web3AuthMPCCoreKit)
const { result } = renderHook(() => useMPCWallet())
jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({
getQuestion: () => 'SOME RANDOM QUESTION',
recoverFactor: () => {
throw new Error('Invalid answer')
},
} as unknown as TssSecurityQuestion)

expect(result.current.recoverFactorWithPassword('test', false)).rejects.toEqual(new Error('Invalid answer'))
})

it.only('should input recovered factor if correct password is entered', async () => {
const mockSecurityQuestionFactor = ethers.Wallet.createRandom().privateKey.slice(2)
const connectWalletSpy = jest.fn().mockImplementation(() => Promise.resolve())
jest.spyOn(useOnboard, 'default').mockReturnValue({} as unknown as OnboardAPI)
jest.spyOn(useOnboard, 'connectWallet').mockImplementation(connectWalletSpy)

setMPCCoreKitInstance(
new MockMPCCoreKit(
COREKIT_STATUS.REQUIRED_SHARE,
{
email: '[email protected]',
name: 'Test',
} as unknown as UserInfo,
new BN(mockSecurityQuestionFactor, 'hex'),
) as unknown as Web3AuthMPCCoreKit,
)

const { result } = renderHook(() => useMPCWallet())
jest.spyOn(mpcCoreKit, 'TssSecurityQuestion').mockReturnValue({
getQuestion: () => 'SOME RANDOM QUESTION',
recoverFactor: () => Promise.resolve(mockSecurityQuestionFactor),
} as unknown as TssSecurityQuestion)

act(() => result.current.recoverFactorWithPassword('test', false))

await waitFor(() => {
expect(result.current.walletState === MPCWalletState.READY)
expect(connectWalletSpy).toBeCalledWith(expect.anything(), {
autoSelect: {
label: ONBOARD_MPC_MODULE_LABEL,
disableModals: true,
},
})
})
})
})
})
45 changes: 45 additions & 0 deletions src/hooks/wallets/mpc/recovery/DeviceShareRecovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
BrowserStorage,
getWebBrowserFactor,
storeWebBrowserFactor,
TssShareType,
type Web3AuthMPCCoreKit,
} from '@web3auth/mpc-core-kit'
import BN from 'bn.js'
import { getPubKeyPoint } from '@tkey-mpc/common-types'

export class DeviceShareRecovery {
private mpcCoreKit: Web3AuthMPCCoreKit

constructor(mpcCoreKit: Web3AuthMPCCoreKit) {
this.mpcCoreKit = mpcCoreKit
}

async isEnabled() {
if (!this.mpcCoreKit.tKey.metadata) {
return false
}
return !!(await getWebBrowserFactor(this.mpcCoreKit))
}

async createAndStoreDeviceFactor() {
const userAgent = navigator.userAgent

const deviceFactorKey = new BN(
await this.mpcCoreKit.createFactor({ shareType: TssShareType.DEVICE, additionalMetadata: { userAgent } }),
'hex',
)
await storeWebBrowserFactor(deviceFactorKey, this.mpcCoreKit)
}

async removeDeviceFactor() {
const deviceFactor = await getWebBrowserFactor(this.mpcCoreKit)
const key = new BN(deviceFactor, 'hex')
const pubKey = getPubKeyPoint(key)
const pubKeyX = pubKey.x.toString('hex', 64)
await this.mpcCoreKit.deleteFactor(pubKey)
const currentStorage = BrowserStorage.getInstance('mpc_corekit_store')
debugger
currentStorage.set(pubKeyX, undefined)
}
}
48 changes: 48 additions & 0 deletions src/hooks/wallets/mpc/recovery/SecurityQuestionRecovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { TssSecurityQuestion, TssShareType, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit'

const DEFAULT_SECURITY_QUESTION = 'ENTER PASSWORD'

export class SecurityQuestionRecovery {
private mpcCoreKit: Web3AuthMPCCoreKit
private securityQuestions = new TssSecurityQuestion()

constructor(mpcCoreKit: Web3AuthMPCCoreKit) {
this.mpcCoreKit = mpcCoreKit
}

isEnabled(): boolean {
try {
const question = this.securityQuestions.getQuestion(this.mpcCoreKit)
return !!question
} catch (error) {
console.error(error)
// It errors out if recovery is not setup currently
return false
}
}

async upsertPassword(newPassword: string, oldPassword?: string) {
if (this.isEnabled()) {
if (!oldPassword) {
throw Error('To change the password you need to provide the old password')
}
await this.securityQuestions.changeSecurityQuestion({
answer: oldPassword,
mpcCoreKit: this.mpcCoreKit,
newAnswer: newPassword,
newQuestion: DEFAULT_SECURITY_QUESTION,
})
} else {
await this.securityQuestions.setSecurityQuestion({
question: DEFAULT_SECURITY_QUESTION,
answer: newPassword,
mpcCoreKit: this.mpcCoreKit,
shareType: TssShareType.DEVICE,
})
}
}

async recoverWithPassword(password: string) {
return this.securityQuestions.recoverFactor(this.mpcCoreKit, password)
}
}
Loading

0 comments on commit fad177c

Please sign in to comment.