diff --git a/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx b/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx
new file mode 100644
index 000000000..aea836f6e
--- /dev/null
+++ b/web/apps/accounts/src/pages/passkeys/flow/Recover.tsx
@@ -0,0 +1,19 @@
+import { TwoFactorType } from "@ente/accounts/constants/twofactor";
+import RecoverPage from "@ente/accounts/pages/recover";
+import { APPS } from "@ente/shared/apps/constants";
+import { useRouter } from "next/router";
+import { AppContext } from "pages/_app";
+import { useContext } from "react";
+
+export default function Recover() {
+ const appContext = useContext(AppContext);
+ const router = useRouter();
+ return (
+
+ );
+}
diff --git a/web/apps/accounts/src/pages/passkeys/flow/index.tsx b/web/apps/accounts/src/pages/passkeys/flow/index.tsx
index 517777b9c..1f082bf6b 100644
--- a/web/apps/accounts/src/pages/passkeys/flow/index.tsx
+++ b/web/apps/accounts/src/pages/passkeys/flow/index.tsx
@@ -257,6 +257,18 @@ const PasskeysFlow = () => {
>
{t("TRY_AGAIN")}
+
+ {t("RECOVER_TWO_FACTOR")}
+
diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json
index 1a088c99d..e0098cd36 100644
--- a/web/apps/photos/package.json
+++ b/web/apps/photos/package.json
@@ -56,7 +56,7 @@
"sanitize-filename": "^1.6.3",
"similarity-transformation": "^0.0.1",
"transformation-matrix": "^2.15.0",
- "uuid": "^9.0.0",
+ "uuid": "^9.0.1",
"vscode-uri": "^3.0.7",
"xml-js": "^1.6.11",
"zxcvbn": "^4.4.2"
diff --git a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx
index bf9782e3b..b297f36ee 100644
--- a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx
+++ b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx
@@ -11,8 +11,17 @@ import TwoFactorModal from "components/TwoFactor/Modal";
import { useRouter } from "next/router";
import { AppContext } from "pages/_app";
// import mlIDbStorage from 'utils/storage/mlIDbStorage';
+import {
+ configurePasskeyRecovery,
+ isPasskeyRecoveryEnabled,
+} from "@ente/accounts/services/passkey";
import { APPS, CLIENT_PACKAGE_NAMES } from "@ente/shared/apps/constants";
import ThemeSwitcher from "@ente/shared/components/ThemeSwitcher";
+import { getRecoveryKey } from "@ente/shared/crypto/helpers";
+import {
+ encryptToB64,
+ generateEncryptionKey,
+} from "@ente/shared/crypto/internal/libsodium";
import { getAccountsURL } from "@ente/shared/network/api";
import { logError } from "@ente/shared/sentry";
import { THEME_COLOR } from "@ente/shared/themes/constants";
@@ -72,13 +81,35 @@ export default function UtilitySection({ closeSidebar }) {
closeSidebar();
try {
+ // check if the user has passkey recovery enabled
+ const recoveryEnabled = await isPasskeyRecoveryEnabled();
+ if (!recoveryEnabled) {
+ // let's create the necessary recovery information
+ const recoveryKey = await getRecoveryKey();
+
+ const resetSecret = await generateEncryptionKey();
+
+ const encryptionResult = await encryptToB64(
+ resetSecret,
+ recoveryKey,
+ );
+
+ await configurePasskeyRecovery(
+ resetSecret,
+ encryptionResult.encryptedData,
+ encryptionResult.nonce,
+ );
+ }
+
const accountsToken = await getAccountsToken();
- window.location.href = `${getAccountsURL()}${
- ACCOUNTS_PAGES.ACCOUNT_HANDOFF
- }?package=${CLIENT_PACKAGE_NAMES.get(
- APPS.PHOTOS,
- )}&token=${accountsToken}`;
+ window.open(
+ `${getAccountsURL()}${
+ ACCOUNTS_PAGES.ACCOUNT_HANDOFF
+ }?package=${CLIENT_PACKAGE_NAMES.get(
+ APPS.PHOTOS,
+ )}&token=${accountsToken}`,
+ );
} catch (e) {
logError(e, "failed to redirect to accounts page");
}
diff --git a/web/packages/accounts/api/user.ts b/web/packages/accounts/api/user.ts
index def8fa8c4..0aa534306 100644
--- a/web/packages/accounts/api/user.ts
+++ b/web/packages/accounts/api/user.ts
@@ -15,6 +15,7 @@ import { logError } from "@ente/shared/sentry";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { KeyAttributes } from "@ente/shared/user/types";
import { HttpStatusCode } from "axios";
+import { TwoFactorType } from "../constants/twofactor";
const ENDPOINT = getEndpoint();
@@ -79,17 +80,26 @@ export const verifyTwoFactor = async (code: string, sessionID: string) => {
return resp.data as UserVerificationResponse;
};
-export const recoverTwoFactor = async (sessionID: string) => {
+export const recoverTwoFactor = async (
+ sessionID: string,
+ twoFactorType: TwoFactorType = TwoFactorType.TOTP,
+) => {
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/recover`, {
sessionID,
+ twoFactorType,
});
return resp.data as TwoFactorRecoveryResponse;
};
-export const removeTwoFactor = async (sessionID: string, secret: string) => {
+export const removeTwoFactor = async (
+ sessionID: string,
+ secret: string,
+ twoFactorType: TwoFactorType = TwoFactorType.TOTP,
+) => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
sessionID,
secret,
+ twoFactorType,
});
return resp.data as TwoFactorVerificationResponse;
};
diff --git a/web/packages/accounts/constants/twofactor.ts b/web/packages/accounts/constants/twofactor.ts
new file mode 100644
index 000000000..92b10730d
--- /dev/null
+++ b/web/packages/accounts/constants/twofactor.ts
@@ -0,0 +1,4 @@
+export enum TwoFactorType {
+ PASSKEY = "passkey",
+ TOTP = "totp",
+}
diff --git a/web/packages/accounts/pages/two-factor/recover.tsx b/web/packages/accounts/pages/two-factor/recover.tsx
index 67bd709e1..fcaf05eab 100644
--- a/web/packages/accounts/pages/two-factor/recover.tsx
+++ b/web/packages/accounts/pages/two-factor/recover.tsx
@@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { recoverTwoFactor, removeTwoFactor } from "@ente/accounts/api/user";
import { PAGES } from "@ente/accounts/constants/pages";
+import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import { logoutUser } from "@ente/accounts/services/user";
import { PageProps } from "@ente/shared/apps/types";
import { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
@@ -28,7 +29,11 @@ const bip39 = require("bip39");
// mobile client library only supports english.
bip39.setDefaultWordlist("english");
-export default function Recover({ router, appContext }: PageProps) {
+export default function Recover({
+ router,
+ appContext,
+ twoFactorType = TwoFactorType.TOTP,
+}: PageProps) {
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
useState(null);
const [sessionID, setSessionID] = useState(null);
@@ -49,7 +54,10 @@ export default function Recover({ router, appContext }: PageProps) {
}
const main = async () => {
try {
- const resp = await recoverTwoFactor(user.twoFactorSessionID);
+ const resp = await recoverTwoFactor(
+ user.twoFactorSessionID,
+ twoFactorType,
+ );
setDoesHaveEncryptedRecoveryKey(!!resp.encryptedSecret);
if (!resp.encryptedSecret) {
showContactSupportDialog({
@@ -106,7 +114,11 @@ export default function Recover({ router, appContext }: PageProps) {
encryptedTwoFactorSecret.nonce,
await cryptoWorker.fromHex(recoveryKey),
);
- const resp = await removeTwoFactor(sessionID, twoFactorSecret);
+ const resp = await removeTwoFactor(
+ sessionID,
+ twoFactorSecret,
+ twoFactorType,
+ );
const { keyAttributes, encryptedToken, token, id } = resp;
setData(LS_KEYS.USER, {
...getData(LS_KEYS.USER),
diff --git a/web/packages/accounts/services/passkey.ts b/web/packages/accounts/services/passkey.ts
new file mode 100644
index 000000000..89a9ae61a
--- /dev/null
+++ b/web/packages/accounts/services/passkey.ts
@@ -0,0 +1,56 @@
+import { CustomError } from "@ente/shared/error";
+import HTTPService from "@ente/shared/network/HTTPService";
+import { logError } from "@ente/shared/sentry";
+import { getToken } from "@ente/shared/storage/localStorage/helpers";
+
+export const isPasskeyRecoveryEnabled = async () => {
+ try {
+ const token = getToken();
+
+ const resp = await HTTPService.get(
+ "/users/two-factor/recovery-status",
+ {},
+ {
+ "X-Auth-Token": token,
+ },
+ );
+
+ if (typeof resp.data === "undefined") {
+ throw Error(CustomError.REQUEST_FAILED);
+ }
+
+ return resp.data["isPasskeyRecoveryEnabled"] as boolean;
+ } catch (e) {
+ logError(e, "failed to get passkey recovery status");
+ throw e;
+ }
+};
+
+export const configurePasskeyRecovery = async (
+ secret: string,
+ userEncryptedSecret: string,
+ userSecretNonce: string,
+) => {
+ try {
+ const token = getToken();
+
+ const resp = await HTTPService.post(
+ "/users/two-factor/passkeys/configure-recovery",
+ {
+ secret,
+ userEncryptedSecret,
+ userSecretNonce,
+ },
+ {
+ "X-Auth-Token": token,
+ },
+ );
+
+ if (typeof resp.data === "undefined") {
+ throw Error(CustomError.REQUEST_FAILED);
+ }
+ } catch (e) {
+ logError(e, "failed to configure passkey recovery");
+ throw e;
+ }
+};
diff --git a/web/packages/shared/apps/types.ts b/web/packages/shared/apps/types.ts
index da5451311..364342516 100644
--- a/web/packages/shared/apps/types.ts
+++ b/web/packages/shared/apps/types.ts
@@ -1,4 +1,5 @@
import { EmotionCache } from "@emotion/react";
+import { TwoFactorType } from "@ente/accounts/constants/twofactor";
import { SetDialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types";
import { AppProps } from "next/app";
import { NextRouter } from "next/router";
@@ -16,4 +17,5 @@ export interface PageProps {
};
router: NextRouter;
appName: APPS;
+ twoFactorType?: TwoFactorType;
}
diff --git a/web/yarn.lock b/web/yarn.lock
index 2f5cbb105..4e31afe7e 100644
--- a/web/yarn.lock
+++ b/web/yarn.lock
@@ -1979,11 +1979,7 @@ fastq@^1.6.0:
reusify "^1.0.4"
"ffmpeg-wasm@file:./apps/photos/thirdparty/ffmpeg-wasm":
- version "0.10.1"
- dependencies:
- is-url "^1.2.4"
- node-fetch "^2.6.1"
- regenerator-runtime "^0.13.7"
+ version "0.0.0"
file-entry-cache@^6.0.1:
version "6.0.1"
@@ -2618,11 +2614,6 @@ is-typed-array@^1.1.13:
dependencies:
which-typed-array "^1.1.14"
-is-url@^1.2.4:
- version "1.2.4"
- resolved "https://registry.yarnpkg.com/is-url/-/is-url-1.2.4.tgz#04a4df46d28c4cff3d73d01ff06abeb318a1aa52"
- integrity sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==
-
is-weakmap@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2"
@@ -3253,7 +3244,7 @@ peek-readable@^4.1.0:
integrity sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==
"photoswipe@file:./apps/photos/thirdparty/photoswipe":
- version "4.1.6"
+ version "0.0.0"
picocolors@^1.0.0:
version "1.0.0"
@@ -3585,11 +3576,6 @@ reflect.getprototypeof@^1.0.4:
globalthis "^1.0.3"
which-builtin-type "^1.1.3"
-regenerator-runtime@^0.13.7:
- version "0.13.11"
- resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9"
- integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
-
regenerator-runtime@^0.14.0:
version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
@@ -4201,7 +4187,7 @@ util-deprecate@^1.0.1, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
-uuid@^9.0.0:
+uuid@^9.0.1:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==