-
Notifications
You must be signed in to change notification settings - Fork 464
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
228 additions
and
222 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
src/components/settings/SignerAccountMFA/PasswordForm.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
import { DeviceShareRecovery } from '@/hooks/wallets/mpc/recovery/DeviceShareRecovery' | ||
import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' | ||
import { Typography, TextField, FormControlLabel, Checkbox, Button, Box } from '@mui/material' | ||
import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' | ||
import { useState, useMemo } from 'react' | ||
import { Controller, useForm } from 'react-hook-form' | ||
import { enableMFA } from './helper' | ||
|
||
enum PasswordFieldNames { | ||
oldPassword = 'oldPassword', | ||
newPassword = 'newPassword', | ||
confirmPassword = 'confirmPassword', | ||
storeDeviceShare = 'storeDeviceShare', | ||
} | ||
|
||
type PasswordFormData = { | ||
[PasswordFieldNames.oldPassword]: string | undefined | ||
[PasswordFieldNames.newPassword]: string | ||
[PasswordFieldNames.confirmPassword]: string | ||
[PasswordFieldNames.storeDeviceShare]: boolean | ||
} | ||
|
||
export const PasswordForm = ({ mpcCoreKit }: { mpcCoreKit: Web3AuthMPCCoreKit }) => { | ||
const formMethods = useForm<PasswordFormData>({ | ||
mode: 'all', | ||
defaultValues: async () => { | ||
const isDeviceShareStored = await new DeviceShareRecovery(mpcCoreKit).isEnabled() | ||
return { | ||
confirmPassword: '', | ||
oldPassword: undefined, | ||
newPassword: '', | ||
storeDeviceShare: isDeviceShareStored, | ||
} | ||
}, | ||
}) | ||
|
||
const { register, formState, getValues, control, handleSubmit } = formMethods | ||
|
||
const [enablingMFA, setEnablingMFA] = useState(false) | ||
|
||
const isPasswordSet = useMemo(() => { | ||
const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) | ||
return securityQuestions.isEnabled() | ||
}, [mpcCoreKit]) | ||
|
||
const onSubmit = async (data: PasswordFormData) => { | ||
setEnablingMFA(true) | ||
try { | ||
await enableMFA(mpcCoreKit, data) | ||
} finally { | ||
setEnablingMFA(false) | ||
} | ||
} | ||
|
||
return ( | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<Box display="flex" flexDirection="column" gap={3} alignItems="baseline"> | ||
{isPasswordSet ? ( | ||
<Typography>You already have a recovery password setup.</Typography> | ||
) : ( | ||
<Typography>You have no password setup. We suggest adding one to secure your Account.</Typography> | ||
)} | ||
{isPasswordSet && ( | ||
<TextField | ||
placeholder="Old password" | ||
label="Old password" | ||
type="password" | ||
error={!!formState.errors[PasswordFieldNames.oldPassword]} | ||
helperText={formState.errors[PasswordFieldNames.oldPassword]?.message} | ||
{...register(PasswordFieldNames.oldPassword, { | ||
required: true, | ||
})} | ||
/> | ||
)} | ||
<TextField | ||
placeholder="New password" | ||
label="New password" | ||
type="password" | ||
error={!!formState.errors[PasswordFieldNames.newPassword]} | ||
helperText={formState.errors[PasswordFieldNames.newPassword]?.message} | ||
{...register(PasswordFieldNames.newPassword, { | ||
required: true, | ||
minLength: 6, | ||
})} | ||
/> | ||
<TextField | ||
placeholder="Confirm new password" | ||
label="Confirm new password" | ||
type="password" | ||
error={!!formState.errors[PasswordFieldNames.confirmPassword]} | ||
helperText={formState.errors[PasswordFieldNames.confirmPassword]?.message} | ||
{...register(PasswordFieldNames.confirmPassword, { | ||
required: true, | ||
validate: (value: string) => { | ||
const currentNewPW = getValues(PasswordFieldNames.newPassword) | ||
if (value !== currentNewPW) { | ||
return 'Passwords do not match' | ||
} | ||
}, | ||
})} | ||
/> | ||
|
||
<Controller | ||
control={control} | ||
name={PasswordFieldNames.storeDeviceShare} | ||
render={({ field: { value, ...field } }) => ( | ||
<FormControlLabel | ||
control={<Checkbox checked={value ?? false} {...field} />} | ||
label="Do not ask for second factor on this device" | ||
/> | ||
)} | ||
/> | ||
|
||
<Button | ||
sx={{ justifySelf: 'flex-start' }} | ||
disabled={!formMethods.formState.isValid || enablingMFA} | ||
type="submit" | ||
> | ||
Change | ||
</Button> | ||
</Box> | ||
</form> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { DeviceShareRecovery } from '@/hooks/wallets/mpc/recovery/DeviceShareRecovery' | ||
import { SecurityQuestionRecovery } from '@/hooks/wallets/mpc/recovery/SecurityQuestionRecovery' | ||
import { logError } from '@/services/exceptions' | ||
import ErrorCodes from '@/services/exceptions/ErrorCodes' | ||
import { asError } from '@/services/exceptions/utils' | ||
import { getPubKeyPoint } from '@tkey-mpc/common-types' | ||
import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' | ||
import BN from 'bn.js' | ||
|
||
export const isMFAEnabled = (mpcCoreKit: Web3AuthMPCCoreKit) => { | ||
if (!mpcCoreKit) { | ||
return false | ||
} | ||
const { shareDescriptions } = mpcCoreKit?.getKeyDetails() | ||
return !Object.entries(shareDescriptions).some((value) => value[0]?.includes('hashedShare')) | ||
} | ||
|
||
export const enableMFA = async ( | ||
mpcCoreKit: Web3AuthMPCCoreKit, | ||
{ | ||
newPassword, | ||
oldPassword, | ||
storeDeviceShare, | ||
}: { | ||
newPassword: string | ||
oldPassword: string | undefined | ||
storeDeviceShare: boolean | ||
}, | ||
) => { | ||
if (!mpcCoreKit) { | ||
return | ||
} | ||
const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) | ||
const deviceShareRecovery = new DeviceShareRecovery(mpcCoreKit) | ||
try { | ||
// 1. setup device factor with password recovery | ||
await securityQuestions.upsertPassword(newPassword, oldPassword) | ||
const securityQuestionFactor = await securityQuestions.recoverWithPassword(newPassword) | ||
if (!securityQuestionFactor) { | ||
throw Error('Could not recover using the new password recovery') | ||
} | ||
|
||
if (!isMFAEnabled(mpcCoreKit)) { | ||
// 2. enable MFA in mpcCoreKit | ||
const recoveryFactor = await mpcCoreKit.enableMFA({}) | ||
|
||
// 3. remove the recovery factor the mpcCoreKit creates | ||
const recoverKey = new BN(recoveryFactor, 'hex') | ||
const recoverPubKey = getPubKeyPoint(recoverKey) | ||
await mpcCoreKit.deleteFactor(recoverPubKey, recoverKey) | ||
} | ||
|
||
const hasDeviceShare = await deviceShareRecovery.isEnabled() | ||
|
||
if (!hasDeviceShare && storeDeviceShare) { | ||
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 deviceShareRecovery.removeDeviceFactor() | ||
} | ||
} catch (e) { | ||
const error = asError(e) | ||
logError(ErrorCodes._304, error.message) | ||
} | ||
} |
Oops, something went wrong.