소스 검색

migrated change-password page

Abhinav 1 년 전
부모
커밋
073d10f4d7

+ 10 - 137
apps/photos/src/pages/change-password/index.tsx

@@ -1,144 +1,17 @@
-import { useState, useEffect } from 'react';
-import { t } from 'i18next';
-
-import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
+import ChangePasswordPage from '@ente/accounts/pages/change-password';
 import { useRouter } from 'next/router';
-import {
-    saveKeyInSessionStore,
-    generateAndSaveIntermediateKeyAttributes,
-    generateLoginSubKey,
-    generateSRPClient,
-    generateSRPSetupAttributes,
-} from 'utils/crypto';
-import { getActualKey } from 'utils/common/key';
-import { startSRPSetup, updateSRPAndKeys } from 'services/userService';
-import SetPasswordForm, {
-    SetPasswordFormProps,
-} from 'components/SetPasswordForm';
-import { SESSION_KEYS } from 'utils/storage/sessionStorage';
-import { PAGES } from 'constants/pages';
-import { KEK, KeyAttributes, UpdatedKey, User } from 'types/user';
-import LinkButton from 'components/pages/gallery/LinkButton';
-import { VerticallyCentered } from 'components/Container';
-import FormPaper from 'components/Form/FormPaper';
-import FormPaperFooter from 'components/Form/FormPaper/Footer';
-import FormPaperTitle from 'components/Form/FormPaper/Title';
-import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
-import { APPS, getAppName } from 'constants/apps';
-import { convertBufferToBase64, convertBase64ToBuffer } from 'utils/user';
-import InMemoryStore, { MS_KEYS } from 'services/InMemoryStore';
+import { AppContext } from 'pages/_app';
+import { useContext } from 'react';
+import { APPS } from '@ente/shared/apps/constants';
 
 export default function ChangePassword() {
-    const [token, setToken] = useState<string>();
+    const appContext = useContext(AppContext);
     const router = useRouter();
-    const [user, setUser] = useState<User>();
-
-    useEffect(() => {
-        const user = getData(LS_KEYS.USER);
-        setUser(user);
-        if (!user?.token) {
-            InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.CHANGE_PASSWORD);
-            router.push(PAGES.ROOT);
-        } else {
-            setToken(user.token);
-        }
-    }, []);
-
-    const onSubmit: SetPasswordFormProps['callback'] = async (
-        passphrase,
-        setFieldError
-    ) => {
-        const cryptoWorker = await ComlinkCryptoWorker.getInstance();
-        const key = await getActualKey();
-        const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
-        const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
-        let kek: KEK;
-        try {
-            kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
-        } catch (e) {
-            setFieldError('confirm', t('PASSWORD_GENERATION_FAILED'));
-            return;
-        }
-        const encryptedKeyAttributes = await cryptoWorker.encryptToB64(
-            key,
-            kek.key
-        );
-        const updatedKey: UpdatedKey = {
-            kekSalt,
-            encryptedKey: encryptedKeyAttributes.encryptedData,
-            keyDecryptionNonce: encryptedKeyAttributes.nonce,
-            opsLimit: kek.opsLimit,
-            memLimit: kek.memLimit,
-        };
-
-        const loginSubKey = await generateLoginSubKey(kek.key);
-
-        const { srpUserID, srpSalt, srpVerifier } =
-            await generateSRPSetupAttributes(loginSubKey);
-
-        const srpClient = await generateSRPClient(
-            srpSalt,
-            srpUserID,
-            loginSubKey
-        );
-
-        const srpA = convertBufferToBase64(srpClient.computeA());
-
-        const { setupID, srpB } = await startSRPSetup(token, {
-            srpUserID,
-            srpSalt,
-            srpVerifier,
-            srpA,
-        });
-
-        srpClient.setB(convertBase64ToBuffer(srpB));
-
-        const srpM1 = convertBufferToBase64(srpClient.computeM1());
-
-        await updateSRPAndKeys(token, {
-            setupID,
-            srpM1,
-            updatedKeyAttr: updatedKey,
-        });
-
-        const updatedKeyAttributes = Object.assign(keyAttributes, updatedKey);
-        await generateAndSaveIntermediateKeyAttributes(
-            passphrase,
-            updatedKeyAttributes,
-            key
-        );
-
-        await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
-        redirectToAppHome();
-    };
-
-    const redirectToAppHome = () => {
-        setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
-        const appName = getAppName();
-        if (appName === APPS.AUTH) {
-            router.push(PAGES.AUTH);
-        } else {
-            router.push(PAGES.GALLERY);
-        }
-    };
-
     return (
-        <VerticallyCentered>
-            <FormPaper>
-                <FormPaperTitle>{t('CHANGE_PASSWORD')}</FormPaperTitle>
-                <SetPasswordForm
-                    userEmail={user?.email}
-                    callback={onSubmit}
-                    buttonText={t('CHANGE_PASSWORD')}
-                />
-                {(getData(LS_KEYS.SHOW_BACK_BUTTON)?.value ?? true) && (
-                    <FormPaperFooter>
-                        <LinkButton onClick={router.back}>
-                            {t('GO_BACK')}
-                        </LinkButton>
-                    </FormPaperFooter>
-                )}
-            </FormPaper>
-        </VerticallyCentered>
+        <ChangePasswordPage
+            appContext={appContext}
+            router={router}
+            appName={APPS.PHOTOS}
+        />
     );
 }

+ 18 - 1
packages/accounts/api/srp.ts

@@ -10,7 +10,9 @@ import {
     SRPVerificationResponse,
     SetupSRPRequest,
     SetupSRPResponse,
-} from '../types/srp';
+    UpdateSRPAndKeysRequest,
+    UpdateSRPAndKeysResponse,
+} from '@ente/accounts/types/srp';
 import { getToken } from '@ente/shared/storage/localStorage/helpers';
 import { ApiError, CustomError } from '@ente/shared/error';
 import { HttpStatusCode } from 'axios';
@@ -116,3 +118,18 @@ export const verifySRPSession = async (
         }
     }
 };
+
+export const updateSRPAndKeys = async (
+    token: string,
+    updateSRPAndKeyRequest: UpdateSRPAndKeysRequest
+): Promise<UpdateSRPAndKeysResponse> => {
+    const resp = await HTTPService.post(
+        `${ENDPOINT}/users/srp/update`,
+        updateSRPAndKeyRequest,
+        null,
+        {
+            'X-Auth-Token': token,
+        }
+    );
+    return resp.data as UpdateSRPAndKeysResponse;
+};

+ 159 - 0
packages/accounts/components/SetPasswordForm.tsx

@@ -0,0 +1,159 @@
+import React, { useState } from 'react';
+import { Formik } from 'formik';
+import * as Yup from 'yup';
+import SubmitButton from '@ente/shared/components/SubmitButton';
+import { Box, Input, TextField, Typography } from '@mui/material';
+import { PasswordStrengthHint } from './PasswordStrength';
+import { isWeakPassword } from '@ente/accounts/utils';
+import { Trans } from 'react-i18next';
+import { t } from 'i18next';
+import ShowHidePassword from '@ente/shared/components/Form/ShowHidePassword';
+
+export interface SetPasswordFormProps {
+    userEmail: string;
+    callback: (
+        passphrase: string,
+        setFieldError: (
+            field: keyof SetPasswordFormValues,
+            message: string
+        ) => void
+    ) => Promise<void>;
+    buttonText: string;
+}
+export interface SetPasswordFormValues {
+    passphrase: string;
+    confirm: string;
+}
+function SetPasswordForm(props: SetPasswordFormProps) {
+    const [loading, setLoading] = useState(false);
+    const [showPassword, setShowPassword] = useState(false);
+
+    const handleClickShowPassword = () => {
+        setShowPassword(!showPassword);
+    };
+
+    const handleMouseDownPassword = (
+        event: React.MouseEvent<HTMLButtonElement>
+    ) => {
+        event.preventDefault();
+    };
+
+    const onSubmit = async (
+        values: SetPasswordFormValues,
+        {
+            setFieldError,
+        }: {
+            setFieldError: (
+                field: keyof SetPasswordFormValues,
+                message: string
+            ) => void;
+        }
+    ) => {
+        setLoading(true);
+        try {
+            const { passphrase, confirm } = values;
+            if (passphrase === confirm) {
+                await props.callback(passphrase, setFieldError);
+            } else {
+                setFieldError('confirm', t('PASSPHRASE_MATCH_ERROR'));
+            }
+        } catch (e) {
+            setFieldError('confirm', `${t('UNKNOWN_ERROR')} ${e.message}`);
+        } finally {
+            setLoading(false);
+        }
+    };
+
+    return (
+        <Formik<SetPasswordFormValues>
+            initialValues={{ passphrase: '', confirm: '' }}
+            validationSchema={Yup.object().shape({
+                passphrase: Yup.string().required(t('REQUIRED')),
+                confirm: Yup.string().required(t('REQUIRED')),
+            })}
+            validateOnChange={false}
+            validateOnBlur={false}
+            onSubmit={onSubmit}>
+            {({ values, errors, handleChange, handleSubmit }) => (
+                <form noValidate onSubmit={handleSubmit}>
+                    <Typography mb={2} color="text.muted" variant="small">
+                        {t('ENTER_ENC_PASSPHRASE')}
+                    </Typography>
+
+                    <Input
+                        hidden
+                        name="email"
+                        id="email"
+                        autoComplete="username"
+                        type="email"
+                        value={props.userEmail}
+                    />
+                    <TextField
+                        fullWidth
+                        name="password"
+                        id="password"
+                        autoComplete="new-password"
+                        type={showPassword ? 'text' : 'password'}
+                        label={t('PASSPHRASE_HINT')}
+                        value={values.passphrase}
+                        onChange={handleChange('passphrase')}
+                        error={Boolean(errors.passphrase)}
+                        helperText={errors.passphrase}
+                        autoFocus
+                        disabled={loading}
+                        InputProps={{
+                            endAdornment: (
+                                <ShowHidePassword
+                                    showPassword={showPassword}
+                                    handleClickShowPassword={
+                                        handleClickShowPassword
+                                    }
+                                    handleMouseDownPassword={
+                                        handleMouseDownPassword
+                                    }
+                                />
+                            ),
+                        }}
+                    />
+                    <TextField
+                        fullWidth
+                        name="confirm-password"
+                        id="confirm-password"
+                        autoComplete="new-password"
+                        type="password"
+                        label={t('CONFIRM_PASSPHRASE')}
+                        value={values.confirm}
+                        onChange={handleChange('confirm')}
+                        disabled={loading}
+                        error={Boolean(errors.confirm)}
+                        helperText={errors.confirm}
+                    />
+                    <PasswordStrengthHint password={values.passphrase} />
+
+                    <Typography my={2} variant="small">
+                        <Trans i18nKey={'PASSPHRASE_DISCLAIMER'} />
+                    </Typography>
+
+                    <Box my={4}>
+                        <SubmitButton
+                            sx={{ my: 0 }}
+                            loading={loading}
+                            buttonText={props.buttonText}
+                            disabled={isWeakPassword(values.passphrase)}
+                        />
+                        {loading && (
+                            <Typography
+                                textAlign="center"
+                                mt={1}
+                                color="text.muted"
+                                variant="small">
+                                {t('KEY_GENERATION_IN_PROGRESS_MESSAGE')}
+                            </Typography>
+                        )}
+                    </Box>
+                </form>
+            )}
+        </Formik>
+    );
+}
+export default SetPasswordForm;

+ 150 - 0
packages/accounts/pages/change-password.tsx

@@ -0,0 +1,150 @@
+import { useState, useEffect } from 'react';
+import { t } from 'i18next';
+
+import { getData, LS_KEYS, setData } from '@ente/shared/storage/localStorage';
+import {
+    saveKeyInSessionStore,
+    generateAndSaveIntermediateKeyAttributes,
+    generateLoginSubKey,
+} from '@ente/shared/crypto/helpers';
+import {
+    generateSRPClient,
+    generateSRPSetupAttributes,
+} from '@ente/accounts/services/srp';
+
+import { getActualKey } from '@ente/shared/user';
+import { startSRPSetup, updateSRPAndKeys } from '@ente/accounts/api/srp';
+import SetPasswordForm, {
+    SetPasswordFormProps,
+} from '@ente/accounts/components/SetPasswordForm';
+import { SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
+import { PAGES } from '@ente/accounts/constants/pages';
+import { KEK, KeyAttributes, User } from '@ente/shared/user/types';
+import { UpdatedKey } from '@ente/accounts/types/user';
+
+import LinkButton from '@ente/shared/components/LinkButton';
+import { VerticallyCentered } from '@ente/shared/components/Container';
+import FormPaper from '@ente/shared/components/Form/FormPaper';
+import FormPaperFooter from '@ente/shared/components/Form/FormPaper/Footer';
+import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
+import ComlinkCryptoWorker from '@ente/shared/crypto';
+import { APPS } from '@ente/shared/apps/constants';
+import {
+    convertBufferToBase64,
+    convertBase64ToBuffer,
+} from '@ente/accounts/utils';
+import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
+import { PageProps } from '@ente/shared/apps/types';
+
+export default function ChangePassword({ appName, router }: PageProps) {
+    const [token, setToken] = useState<string>();
+    const [user, setUser] = useState<User>();
+
+    useEffect(() => {
+        const user = getData(LS_KEYS.USER);
+        setUser(user);
+        if (!user?.token) {
+            InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.CHANGE_PASSWORD);
+            router.push(PAGES.ROOT);
+        } else {
+            setToken(user.token);
+        }
+    }, []);
+
+    const onSubmit: SetPasswordFormProps['callback'] = async (
+        passphrase,
+        setFieldError
+    ) => {
+        const cryptoWorker = await ComlinkCryptoWorker.getInstance();
+        const key = await getActualKey();
+        const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
+        const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
+        let kek: KEK;
+        try {
+            kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
+        } catch (e) {
+            setFieldError('confirm', t('PASSWORD_GENERATION_FAILED'));
+            return;
+        }
+        const encryptedKeyAttributes = await cryptoWorker.encryptToB64(
+            key,
+            kek.key
+        );
+        const updatedKey: UpdatedKey = {
+            kekSalt,
+            encryptedKey: encryptedKeyAttributes.encryptedData,
+            keyDecryptionNonce: encryptedKeyAttributes.nonce,
+            opsLimit: kek.opsLimit,
+            memLimit: kek.memLimit,
+        };
+
+        const loginSubKey = await generateLoginSubKey(kek.key);
+
+        const { srpUserID, srpSalt, srpVerifier } =
+            await generateSRPSetupAttributes(loginSubKey);
+
+        const srpClient = await generateSRPClient(
+            srpSalt,
+            srpUserID,
+            loginSubKey
+        );
+
+        const srpA = convertBufferToBase64(srpClient.computeA());
+
+        const { setupID, srpB } = await startSRPSetup({
+            srpUserID,
+            srpSalt,
+            srpVerifier,
+            srpA,
+        });
+
+        srpClient.setB(convertBase64ToBuffer(srpB));
+
+        const srpM1 = convertBufferToBase64(srpClient.computeM1());
+
+        await updateSRPAndKeys(token, {
+            setupID,
+            srpM1,
+            updatedKeyAttr: updatedKey,
+        });
+
+        const updatedKeyAttributes = Object.assign(keyAttributes, updatedKey);
+        await generateAndSaveIntermediateKeyAttributes(
+            passphrase,
+            updatedKeyAttributes,
+            key
+        );
+
+        await saveKeyInSessionStore(SESSION_KEYS.ENCRYPTION_KEY, key);
+        redirectToAppHome();
+    };
+
+    const redirectToAppHome = () => {
+        setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
+        if (appName === APPS.AUTH) {
+            router.push(PAGES.AUTH);
+        } else {
+            router.push(PAGES.GALLERY);
+        }
+    };
+
+    return (
+        <VerticallyCentered>
+            <FormPaper>
+                <FormPaperTitle>{t('CHANGE_PASSWORD')}</FormPaperTitle>
+                <SetPasswordForm
+                    userEmail={user?.email}
+                    callback={onSubmit}
+                    buttonText={t('CHANGE_PASSWORD')}
+                />
+                {(getData(LS_KEYS.SHOW_BACK_BUTTON)?.value ?? true) && (
+                    <FormPaperFooter>
+                        <LinkButton onClick={router.back}>
+                            {t('GO_BACK')}
+                        </LinkButton>
+                    </FormPaperFooter>
+                )}
+            </FormPaper>
+        </VerticallyCentered>
+    );
+}

+ 2 - 5
packages/accounts/pages/verify.tsx

@@ -10,11 +10,7 @@ import { clearFiles } from '@ente/shared/storage/localForage/helpers';
 import { setIsFirstLogin } from '@ente/shared/storage/localStorage/helpers';
 import { clearKeys } from '@ente/shared/storage/sessionStorage';
 import { PAGES } from '../constants/pages';
-import {
-    KeyAttributes,
-    UserVerificationResponse,
-    User,
-} from '@ente/shared/user/types';
+import { KeyAttributes, User } from '@ente/shared/user/types';
 import { SRPSetupAttributes } from '../types/srp';
 import { Box, Typography } from '@mui/material';
 import FormPaperTitle from '@ente/shared/components/Form/FormPaper/Title';
@@ -30,6 +26,7 @@ import InMemoryStore, { MS_KEYS } from '@ente/shared/storage/InMemoryStore';
 import { ApiError } from '@ente/shared/error';
 import { HttpStatusCode } from 'axios';
 import { PageProps } from '@ente/shared/apps/types';
+import { UserVerificationResponse } from '@ente/accounts/types/user';
 
 export default function VerifyPage({ appContext, router, appName }: PageProps) {
     const [email, setEmail] = useState('');

+ 2 - 2
packages/accounts/services/srp.ts

@@ -13,7 +13,7 @@ import {
 import { v4 as uuidv4 } from 'uuid';
 import ComlinkCryptoWorker from '@ente/shared/crypto';
 import { generateLoginSubKey } from '@ente/shared/crypto/helpers';
-import { UserVerificationResponse } from '@ente/shared/user/types';
+import { UserVerificationResponse } from '@ente/accounts/types/user';
 
 const SRP_PARAMS = SRP.params['4096'];
 
@@ -143,7 +143,7 @@ export const loginViaSRP = async (
 // HELPERS
 // ====================
 
-const generateSRPClient = async (
+export const generateSRPClient = async (
     srpSalt: string,
     srpUserID: string,
     loginSubKey: string

+ 15 - 1
packages/accounts/types/srp.ts

@@ -1,4 +1,7 @@
-import { UserVerificationResponse } from '@ente/shared/user/types';
+import {
+    UpdatedKey,
+    UserVerificationResponse,
+} from '@ente/accounts/types/user';
 
 export interface SRPAttributes {
     srpUserID: string;
@@ -57,3 +60,14 @@ export interface SRPSetupAttributes {
     srpUserID: string;
     loginSubKey: string;
 }
+
+export interface UpdateSRPAndKeysRequest {
+    srpM1: string;
+    setupID: string;
+    updatedKeyAttr: UpdatedKey;
+}
+
+export interface UpdateSRPAndKeysResponse {
+    srpM2: string;
+    setupID: string;
+}

+ 8 - 0
packages/accounts/types/user.ts

@@ -25,3 +25,11 @@ export interface TwoFactorRecoveryResponse {
     encryptedSecret: string;
     secretDecryptionNonce: string;
 }
+
+export interface UpdatedKey {
+    kekSalt: string;
+    encryptedKey: string;
+    keyDecryptionNonce: string;
+    memLimit: number;
+    opsLimit: number;
+}

+ 22 - 0
packages/shared/user/index.ts

@@ -0,0 +1,22 @@
+import { B64EncryptionResult } from '@ente/shared/crypto/types';
+import { CustomError } from '@ente/shared/error';
+import { getKey, SESSION_KEYS } from '@ente/shared/storage/sessionStorage';
+import ComlinkCryptoWorker from '@ente/shared/crypto';
+
+export const getActualKey = async () => {
+    try {
+        const encryptionKeyAttributes: B64EncryptionResult = getKey(
+            SESSION_KEYS.ENCRYPTION_KEY
+        );
+
+        const cryptoWorker = await ComlinkCryptoWorker.getInstance();
+        const key = await cryptoWorker.decryptB64(
+            encryptionKeyAttributes.encryptedData,
+            encryptionKeyAttributes.nonce,
+            encryptionKeyAttributes.key
+        );
+        return key;
+    } catch (e) {
+        throw new Error(CustomError.KEY_MISSING);
+    }
+};

+ 6 - 0
packages/shared/user/types.ts

@@ -21,3 +21,9 @@ export interface User {
     isTwoFactorEnabled: boolean;
     twoFactorSessionID: string;
 }
+
+export interface KEK {
+    key: string;
+    opsLimit: number;
+    memLimit: number;
+}