Web Passkey Recovery (#1047)
## Description * Adds passkey recovery feature parity from #1013 ## Tests
This commit is contained in:
commit
f85f220c1d
10 changed files with 160 additions and 28 deletions
19
web/apps/accounts/src/pages/passkeys/flow/Recover.tsx
Normal file
19
web/apps/accounts/src/pages/passkeys/flow/Recover.tsx
Normal file
|
@ -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 (
|
||||
<RecoverPage
|
||||
appContext={appContext}
|
||||
router={router}
|
||||
appName={APPS.PHOTOS}
|
||||
twoFactorType={TwoFactorType.PASSKEY}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -257,6 +257,18 @@ const PasskeysFlow = () => {
|
|||
>
|
||||
{t("TRY_AGAIN")}
|
||||
</EnteButton>
|
||||
<EnteButton
|
||||
href="/passkeys/flow/recover"
|
||||
fullWidth
|
||||
style={{
|
||||
marginTop: "1rem",
|
||||
}}
|
||||
color="primary"
|
||||
type="button"
|
||||
variant="text"
|
||||
>
|
||||
{t("RECOVER_TWO_FACTOR")}
|
||||
</EnteButton>
|
||||
</FormPaper>
|
||||
</Box>
|
||||
</Box>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
4
web/packages/accounts/constants/twofactor.ts
Normal file
4
web/packages/accounts/constants/twofactor.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
export enum TwoFactorType {
|
||||
PASSKEY = "passkey",
|
||||
TOTP = "totp",
|
||||
}
|
|
@ -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<B64EncryptionResult>(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),
|
||||
|
|
56
web/packages/accounts/services/passkey.ts
Normal file
56
web/packages/accounts/services/passkey.ts
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Add table
Reference in a new issue