import crypto from "crypto"; import { Controller, useForm } from "react-hook-form"; import { faCheck, faXmark } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { zodResolver } from "@hookform/resolvers/zod"; import { useSearch } from "@tanstack/react-router"; import jsrp from "jsrp"; import { z } from "zod"; import passwordCheck from "@app/components/utilities/checks/password/PasswordCheck"; import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; import { Button, FormControl, Input } from "@app/components/v2"; import { ROUTE_PATHS } from "@app/const/routes"; import { useResetPassword, useResetPasswordV2 } from "@app/hooks/api"; import { UserEncryptionVersion } from "@app/hooks/api/auth/types"; const formData = z.object({ password: z.string(), passwordErrorTooShort: z.boolean().optional(), passwordErrorTooLong: z.boolean().optional(), passwordErrorNoLetterChar: z.boolean().optional(), passwordErrorNoNumOrSpecialChar: z.boolean().optional(), passwordErrorRepeatedChar: z.boolean().optional(), passwordErrorEscapeChar: z.boolean().optional(), passwordErrorLowEntropy: z.boolean().optional(), passwordErrorBreached: z.boolean() }); type TFormData = z.infer; type Props = { verificationToken: string; privateKey: string; encryptionVersion: UserEncryptionVersion; onComplete: () => void; }; export const EnterPasswordStep = ({ verificationToken, encryptionVersion, privateKey, onComplete }: Props) => { const search = useSearch({ from: ROUTE_PATHS.Auth.PasswordResetPage.id }); const { to: email } = search; const { control, watch, handleSubmit, setValue, formState: { isSubmitting } } = useForm({ resolver: zodResolver(formData) }); const { mutateAsync: resetPassword, isPending: isLoading } = useResetPassword(); const { mutateAsync: resetPasswordV2, isPending: isLoadingV2 } = useResetPasswordV2(); const passwordErrorTooShort = watch("passwordErrorTooShort"); const passwordErrorTooLong = watch("passwordErrorTooLong"); const passwordErrorNoLetterChar = watch("passwordErrorNoLetterChar"); const passwordErrorNoNumOrSpecialChar = watch("passwordErrorNoNumOrSpecialChar"); const passwordErrorRepeatedChar = watch("passwordErrorRepeatedChar"); const passwordErrorEscapeChar = watch("passwordErrorEscapeChar"); const passwordErrorLowEntropy = watch("passwordErrorLowEntropy"); const passwordErrorBreached = watch("passwordErrorBreached"); const isPasswordError = passwordErrorTooShort || passwordErrorTooLong || passwordErrorNoLetterChar || passwordErrorNoNumOrSpecialChar || passwordErrorRepeatedChar || passwordErrorEscapeChar || passwordErrorLowEntropy || passwordErrorBreached; const handlePasswordCheck = async (checkPassword: string) => { const errorCheck = await passwordCheck({ password: checkPassword, setPasswordErrorTooShort: (v) => setValue("passwordErrorTooShort", v), setPasswordErrorTooLong: (v) => setValue("passwordErrorTooLong", v), setPasswordErrorNoLetterChar: (v) => setValue("passwordErrorNoLetterChar", v), setPasswordErrorNoNumOrSpecialChar: (v) => setValue("passwordErrorNoNumOrSpecialChar", v), setPasswordErrorRepeatedChar: (v) => setValue("passwordErrorRepeatedChar", v), setPasswordErrorEscapeChar: (v) => setValue("passwordErrorEscapeChar", v), setPasswordErrorLowEntropy: (v) => setValue("passwordErrorLowEntropy", v), setPasswordErrorBreached: (v) => setValue("passwordErrorBreached", v) }); return errorCheck; }; const resetPasswordHandler = async (data: TFormData) => { const errorCheck = await handlePasswordCheck(data.password); if (errorCheck) return; if (encryptionVersion === UserEncryptionVersion.V2) { await resetPasswordV2({ newPassword: data.password, verificationToken }); } else { // eslint-disable-next-line new-cap const client = new jsrp.client(); client.init( { username: email, password: data.password }, async () => { client.createVerifier(async (_err: any, result: { salt: string; verifier: string }) => { const derivedKey = await deriveArgonKey({ password: data.password, salt: result.salt, mem: 65536, time: 3, parallelism: 1, hashLen: 32 }); if (!derivedKey) throw new Error("Failed to derive key from password"); const key = crypto.randomBytes(32); // create encrypted private key by encrypting the private // key with the symmetric key [key] const { ciphertext: encryptedPrivateKey, iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag } = Aes256Gcm.encrypt({ text: privateKey, secret: key }); // create the protected key by encrypting the symmetric key // [key] with the derived key const { ciphertext: protectedKey, iv: protectedKeyIV, tag: protectedKeyTag } = Aes256Gcm.encrypt({ text: key.toString("hex"), secret: Buffer.from(derivedKey.hash) }); await resetPassword({ protectedKey, protectedKeyIV, protectedKeyTag, encryptedPrivateKey, encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt: result.salt, verifier: result.verifier, verificationToken, password: data.password }); }); } ); } onComplete(); }; return (

Enter new password

Make sure you save it somewhere safe.

( { field.onChange(e); handlePasswordCheck(e.target.value); }} type="password" /> )} />
{passwordErrorTooShort || passwordErrorTooLong || passwordErrorNoLetterChar || passwordErrorNoNumOrSpecialChar || passwordErrorRepeatedChar || passwordErrorEscapeChar || passwordErrorLowEntropy || passwordErrorBreached ? (
Password should contain:
{passwordErrorTooShort ? ( ) : ( )}
at least 14 characters
{passwordErrorTooLong ? ( ) : ( )}
at most 100 characters
{passwordErrorNoLetterChar ? ( ) : ( )}
at least 1 letter character
{passwordErrorNoNumOrSpecialChar ? ( ) : ( )}
at least 1 number or special character
{passwordErrorRepeatedChar ? ( ) : ( )}
at most 3 repeated, consecutive characters
{passwordErrorEscapeChar ? ( ) : ( )}
No escape characters allowed.
{passwordErrorLowEntropy ? ( ) : ( )}
Password contains personal info.
{passwordErrorBreached ? ( ) : ( )}
Password was found in a data breach.
) : null}
); };