recover.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. import { VerticallyCentered } from "@ente/shared/components/Container";
  2. import SingleInputForm, {
  3. SingleInputFormProps,
  4. } from "@ente/shared/components/SingleInputForm";
  5. import { logError } from "@ente/shared/sentry";
  6. import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
  7. import { useEffect, useState } from "react";
  8. import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
  9. import { PAGES } from "@ente/accounts/constants/pages";
  10. import { logoutUser } from "@ente/accounts/services/user";
  11. import { PageProps } from "@ente/shared/apps/types";
  12. import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
  13. import FormPaper from "@ente/shared/components/Form/FormPaper";
  14. import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
  15. import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
  16. import LinkButton from "@ente/shared/components/LinkButton";
  17. import { SUPPORT_EMAIL } from "@ente/shared/constants/urls";
  18. import ComlinkCryptoWorker from "@ente/shared/crypto";
  19. import { B64EncryptionResult } from "@ente/shared/crypto/types";
  20. import { ApiError } from "@ente/shared/error";
  21. import { Link } from "@mui/material";
  22. import { HttpStatusCode } from "axios";
  23. import { t } from "i18next";
  24. import { Trans } from "react-i18next";
  25. const bip39 = require("bip39");
  26. // mobile client library only supports english.
  27. bip39.setDefaultWordlist("english");
  28. export default function Recover({ router, appContext }: PageProps) {
  29. const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
  30. useState<B64EncryptionResult>(null);
  31. const [sessionID, setSessionID] = useState(null);
  32. const [doesHaveEncryptedRecoveryKey, setDoesHaveEncryptedRecoveryKey] =
  33. useState(false);
  34. useEffect(() => {
  35. const user = getData(LS_KEYS.USER);
  36. if (!user || !user.email || !user.twoFactorSessionID) {
  37. router.push(PAGES.ROOT);
  38. } else if (
  39. !user.isTwoFactorEnabled &&
  40. (user.encryptedToken || user.token)
  41. ) {
  42. router.push(PAGES.GENERATE);
  43. } else {
  44. setSessionID(user.twoFactorSessionID);
  45. }
  46. const main = async () => {
  47. try {
  48. const resp = await recoverTwoFactor(user.twoFactorSessionID);
  49. setDoesHaveEncryptedRecoveryKey(!!resp.encryptedSecret);
  50. if (!resp.encryptedSecret) {
  51. showContactSupportDialog({
  52. text: t("GO_BACK"),
  53. action: router.back,
  54. });
  55. } else {
  56. setEncryptedTwoFactorSecret({
  57. encryptedData: resp.encryptedSecret,
  58. nonce: resp.secretDecryptionNonce,
  59. key: null,
  60. });
  61. }
  62. } catch (e) {
  63. if (
  64. e instanceof ApiError &&
  65. e.httpStatusCode === HttpStatusCode.NotFound
  66. ) {
  67. logoutUser();
  68. } else {
  69. logError(e, "two factor recovery page setup failed");
  70. setDoesHaveEncryptedRecoveryKey(false);
  71. showContactSupportDialog({
  72. text: t("GO_BACK"),
  73. action: router.back,
  74. });
  75. }
  76. }
  77. };
  78. main();
  79. }, []);
  80. const recover: SingleInputFormProps["callback"] = async (
  81. recoveryKey: string,
  82. setFieldError,
  83. ) => {
  84. try {
  85. recoveryKey = recoveryKey
  86. .trim()
  87. .split(" ")
  88. .map((part) => part.trim())
  89. .filter((part) => !!part)
  90. .join(" ");
  91. // check if user is entering mnemonic recovery key
  92. if (recoveryKey.indexOf(" ") > 0) {
  93. if (recoveryKey.split(" ").length !== 24) {
  94. throw new Error("recovery code should have 24 words");
  95. }
  96. recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
  97. }
  98. const cryptoWorker = await ComlinkCryptoWorker.getInstance();
  99. const twoFactorSecret = await cryptoWorker.decryptB64(
  100. encryptedTwoFactorSecret.encryptedData,
  101. encryptedTwoFactorSecret.nonce,
  102. await cryptoWorker.fromHex(recoveryKey),
  103. );
  104. const resp = await removeTwoFactor(sessionID, twoFactorSecret);
  105. const { keyAttributes, encryptedToken, token, id } = resp;
  106. setData(LS_KEYS.USER, {
  107. ...getData(LS_KEYS.USER),
  108. token,
  109. encryptedToken,
  110. id,
  111. isTwoFactorEnabled: false,
  112. });
  113. setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes);
  114. router.push(PAGES.CREDENTIALS);
  115. } catch (e) {
  116. logError(e, "two factor recovery failed");
  117. setFieldError(t("INCORRECT_RECOVERY_KEY"));
  118. }
  119. };
  120. const showContactSupportDialog = (
  121. dialogClose?: DialogBoxAttributesV2["close"],
  122. ) => {
  123. appContext.setDialogBoxAttributesV2({
  124. title: t("CONTACT_SUPPORT"),
  125. close: dialogClose ?? {},
  126. content: (
  127. <Trans
  128. i18nKey={"NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE"}
  129. values={{ emailID: SUPPORT_EMAIL }}
  130. components={{
  131. a: <Link href={`mailto:${SUPPORT_EMAIL}`} />,
  132. }}
  133. />
  134. ),
  135. });
  136. };
  137. if (!doesHaveEncryptedRecoveryKey) {
  138. return <></>;
  139. }
  140. return (
  141. <VerticallyCentered>
  142. <FormPaper>
  143. <FormPaperTitle>{t("RECOVER_TWO_FACTOR")}</FormPaperTitle>
  144. <SingleInputForm
  145. callback={recover}
  146. fieldType="text"
  147. placeholder={t("RECOVERY_KEY_HINT")}
  148. buttonText={t("RECOVER")}
  149. disableAutoComplete
  150. />
  151. <FormPaperFooter style={{ justifyContent: "space-between" }}>
  152. <LinkButton onClick={() => showContactSupportDialog()}>
  153. {t("NO_RECOVERY_KEY")}
  154. </LinkButton>
  155. <LinkButton onClick={router.back}>
  156. {t("GO_BACK")}
  157. </LinkButton>
  158. </FormPaperFooter>
  159. </FormPaper>
  160. </VerticallyCentered>
  161. );
  162. }