From caa1a108d84ef87ba9f18ceaebb815e32c551406 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 12 Dec 2022 09:17:05 +0100 Subject: [PATCH 1/2] improving AuthContextProvider.tsx --- package.json | 1 + src/AuthContext/AuthContextProvider.tsx | 109 +++++------------------- src/AuthContext/use-master-password.ts | 36 +++++++- src/AuthContext/use-user.ts | 78 +++++++++++++++++ 4 files changed, 131 insertions(+), 93 deletions(-) create mode 100644 src/AuthContext/use-user.ts diff --git a/package.json b/package.json index 3f51934..9759a08 100755 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "crypto-js": "^4.1.1", "date-fns": "^2.29.3", "deep-equal": "^2.0.5", + "fast-hash-code": "^2.1.0", "formik": "^2.2.9", "group-array": "^1.0.0", "i18next": "^22.0.4", diff --git a/src/AuthContext/AuthContextProvider.tsx b/src/AuthContext/AuthContextProvider.tsx index c5c3b61..be891f3 100644 --- a/src/AuthContext/AuthContextProvider.tsx +++ b/src/AuthContext/AuthContextProvider.tsx @@ -1,18 +1,15 @@ -import {ReactElement, ReactNode, useCallback, useEffect} from "react" -import {AxiosError} from "axios" -import {decrypt, readMessage, readPrivateKey} from "openpgp" - +import {ReactElement, ReactNode, useCallback} from "react" import {useLocalStorage} from "react-use" -import AuthContext from "./AuthContext" -import useExtensionHandler from "~/AuthContext/use-extension-handler" -import useMasterPassword from "~/AuthContext/use-master-password" +import fastHashCode from "fast-hash-code"; -import {AuthenticationDetails, ServerUser, User} from "~/server-types" -import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis" -import {client} from "~/constants/axios-client" -import {useMutation, useQuery} from "@tanstack/react-query" -import PasswordShareConfirmationDialog from "~/AuthContext/PasswordShareConfirmationDialog" -import useContextValue from "~/AuthContext/use-context-value" +import {ServerUser, User} from "~/server-types" + +import AuthContext from "./AuthContext" +import PasswordShareConfirmationDialog from "./PasswordShareConfirmationDialog" +import useContextValue from "./use-context-value" +import useExtensionHandler from "./use-extension-handler" +import useMasterPassword from "./use-master-password" +import useUser from "./use-user"; export interface AuthContextProviderProps { children: ReactNode @@ -23,48 +20,23 @@ export default function AuthContextProvider({children}: AuthContextProviderProps "_global-context-auth-user", null, ) + const { encryptUsingMasterPassword, decryptUsingMasterPassword, + decryptUsingPrivateKey, setDecryptionPassword, _masterPassword, logout: logoutMasterPassword, - } = useMasterPassword(user?.encryptedPassword || null) + } = useMasterPassword(user || null) const {sharePassword, closeDialog, showDialog, dispatchPasswordStatus} = useExtensionHandler( _masterPassword!, user as User, ) - const logout = useCallback(() => { logoutMasterPassword() + setUser(null) }, [logoutMasterPassword]) - const {mutateAsync: refresh} = useMutation(refreshToken, { - onError: () => logout(), - }) - - const decryptUsingPrivateKey = useCallback( - async (message: string): Promise => { - if (!user) { - throw new Error("User not set.") - } - - if (!user.isDecrypted) { - throw new Error("User is not decrypted.") - } - - return ( - await decrypt({ - message: await readMessage({ - armoredMessage: message, - }), - decryptionKeys: await readPrivateKey({ - armoredKey: user.notes.privateKey, - }), - }) - ).data.toString() - }, - [user], - ) const contextValue = useContextValue({ decryptUsingPrivateKey, @@ -76,55 +48,14 @@ export default function AuthContextProvider({children}: AuthContextProviderProps user: user || null, }) - useQuery(["get_me"], getMe, { - refetchOnWindowFocus: "always", - refetchOnReconnect: "always", - retry: 2, - enabled: user !== null, + useUser({ + logout, + decryptUsingMasterPassword, + user: user || null, + updateUser: setUser, + masterPasswordHash: _masterPassword ? fastHashCode(_masterPassword).toString() : null, }) - // Decrypt user notes - useEffect(() => { - if (user && !user.isDecrypted && user.encryptedPassword) { - const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!)) - - setUser( - prevUser => - ({ - ...(prevUser || {}), - notes: note, - isDecrypted: true, - } as User), - ) - } - }, [user, decryptUsingMasterPassword]) - - // Refresh token and logout user if needed - useEffect(() => { - const interceptor = client.interceptors.response.use( - response => response, - async (error: AxiosError) => { - if (error.isAxiosError) { - if (error.response?.status === 401) { - // Check if error comes from refreshing the token. - // If yes, the user has been logged out completely. - const request: XMLHttpRequest = error.request - - if (request.responseURL === REFRESH_TOKEN_URL) { - await logout() - } else { - await refresh() - } - } - } - - throw error - }, - ) - - return () => client.interceptors.response.eject(interceptor) - }, [logout, refresh]) - return ( <> {children} diff --git a/src/AuthContext/use-master-password.ts b/src/AuthContext/use-master-password.ts index 96c15f9..19e02ea 100644 --- a/src/AuthContext/use-master-password.ts +++ b/src/AuthContext/use-master-password.ts @@ -1,11 +1,14 @@ import {useLocalStorage} from "react-use" import {useCallback, useMemo} from "react" +import {decrypt, readMessage, readPrivateKey} from "openpgp"; import {decryptString, encryptString} from "~/utils" +import {ServerUser, User} from "~/server-types"; export interface UseMasterPasswordResult { encryptUsingMasterPassword: (content: string) => string decryptUsingMasterPassword: (content: string) => string + decryptUsingPrivateKey: (message: string) => Promise setDecryptionPassword: (password: string) => void logout: () => void @@ -14,7 +17,7 @@ export interface UseMasterPasswordResult { } export default function useMasterPassword( - encryptedPassword: string | null, + user: User | ServerUser | null, ): UseMasterPasswordResult { const [decryptionPassword, setDecryptionPassword] = useLocalStorage( "_global-context-auth-decryption-password", @@ -22,12 +25,12 @@ export default function useMasterPassword( ) const masterPassword = useMemo(() => { - if (decryptionPassword === null || !encryptedPassword) { + if (decryptionPassword === null || !user?.encryptedPassword) { return null } - return decryptString(encryptedPassword, decryptionPassword!) - }, [decryptionPassword, encryptedPassword]) + return decryptString(user.encryptedPassword, decryptionPassword!) + }, [decryptionPassword, user?.encryptedPassword]) const encryptUsingMasterPassword = useCallback( (content: string) => { @@ -51,6 +54,30 @@ export default function useMasterPassword( [masterPassword], ) + const decryptUsingPrivateKey = useCallback( + async (message: string): Promise => { + if (!user) { + throw new Error("User not set.") + } + + if (!user.isDecrypted) { + throw new Error("User is not decrypted.") + } + + return ( + await decrypt({ + message: await readMessage({ + armoredMessage: message, + }), + decryptionKeys: await readPrivateKey({ + armoredKey: user.notes.privateKey, + }), + }) + ).data.toString() + }, + [user], + ) + const logout = useCallback(() => { setDecryptionPassword(null) }, []) @@ -58,6 +85,7 @@ export default function useMasterPassword( return { encryptUsingMasterPassword, decryptUsingMasterPassword, + decryptUsingPrivateKey, setDecryptionPassword, logout, _masterPassword: masterPassword!, diff --git a/src/AuthContext/use-user.ts b/src/AuthContext/use-user.ts new file mode 100644 index 0000000..671048d --- /dev/null +++ b/src/AuthContext/use-user.ts @@ -0,0 +1,78 @@ +import {Dispatch, SetStateAction, useEffect} from "react"; +import {AxiosError} from "axios"; + +import {useMutation, useQuery} from "@tanstack/react-query"; + +import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis" +import {AuthenticationDetails, ServerUser, User} from "~/server-types"; +import {client} from "~/constants/axios-client"; + +export interface UseAuthData { + logout: () => void + masterPasswordHash: string | null + decryptUsingMasterPassword: (content: string) => string + user: User | ServerUser | null + updateUser: Dispatch> +} + +export default function useUser({ + logout, + masterPasswordHash, + decryptUsingMasterPassword, + user, + updateUser, +}: UseAuthData) { + + const {mutateAsync: refresh} = useMutation(refreshToken, { + onError: () => logout(), + }) + + useQuery(["get_me"], getMe, { + refetchOnWindowFocus: "always", + refetchOnReconnect: "always", + retry: 2, + enabled: user !== null, + }) + + // Decrypt user notes + useEffect(() => { + if (user && !user.isDecrypted && user.encryptedPassword && masterPasswordHash) { + const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!)) + + updateUser( + prevUser => + ({ + ...(prevUser || {}), + notes: note, + isDecrypted: true, + } as User), + ) + } + }, [user, decryptUsingMasterPassword, updateUser, masterPasswordHash]) + + // Refresh token and logout user if needed + useEffect(() => { + const interceptor = client.interceptors.response.use( + response => response, + async (error: AxiosError) => { + if (error.isAxiosError) { + if (error.response?.status === 401) { + // Check if error comes from refreshing the token. + // If yes, the user has been logged out completely. + const request: XMLHttpRequest = error.request + + if (request.responseURL === REFRESH_TOKEN_URL) { + await logout() + } else { + await refresh() + } + } + } + + throw error + }, + ) + + return () => client.interceptors.response.eject(interceptor) + }, [logout, refresh]) +} From c89ad41602de6645c782f8bb1977385e20501566 Mon Sep 17 00:00:00 2001 From: Myzel394 <50424412+Myzel394@users.noreply.github.com> Date: Mon, 12 Dec 2022 15:56:35 +0100 Subject: [PATCH 2/2] improving AuthContextProvider --- src/AuthContext/use-context-value.ts | 2 +- src/AuthContext/use-master-password.ts | 20 ++++++++++++++++++-- src/AuthContext/use-user.ts | 1 - src/hooks/use-user.ts | 4 ++-- src/routes/EnterDecryptionPassword.tsx | 1 + 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/AuthContext/use-context-value.ts b/src/AuthContext/use-context-value.ts index 34bcd6c..02af920 100644 --- a/src/AuthContext/use-context-value.ts +++ b/src/AuthContext/use-context-value.ts @@ -8,7 +8,7 @@ export interface UseContextValueData { encryptUsingMasterPassword: (content: string) => string decryptUsingMasterPassword: (content: string) => string decryptUsingPrivateKey: (message: string) => Promise - setDecryptionPassword: (password: string) => void + setDecryptionPassword: (password: string) => boolean } export default function useContextValue({ diff --git a/src/AuthContext/use-master-password.ts b/src/AuthContext/use-master-password.ts index 19e02ea..4613850 100644 --- a/src/AuthContext/use-master-password.ts +++ b/src/AuthContext/use-master-password.ts @@ -10,7 +10,7 @@ export interface UseMasterPasswordResult { decryptUsingMasterPassword: (content: string) => string decryptUsingPrivateKey: (message: string) => Promise - setDecryptionPassword: (password: string) => void + setDecryptionPassword: (password: string) => boolean logout: () => void // Use this cautiously _masterPassword: string @@ -78,6 +78,22 @@ export default function useMasterPassword( [user], ) + const updateDecryptionPassword = useCallback((password: string) => { + if (!user || !user.encryptedPassword) { + throw new Error("User not set.") + } + + try { + const masterPassword = decryptString(user.encryptedPassword, password) + JSON.parse(decryptString((user as ServerUser).encryptedNotes, masterPassword)) + setDecryptionPassword(password) + } catch { + return false; + } + + return true; + }, [user, masterPassword]) + const logout = useCallback(() => { setDecryptionPassword(null) }, []) @@ -86,8 +102,8 @@ export default function useMasterPassword( encryptUsingMasterPassword, decryptUsingMasterPassword, decryptUsingPrivateKey, - setDecryptionPassword, logout, + setDecryptionPassword: updateDecryptionPassword, _masterPassword: masterPassword!, } } diff --git a/src/AuthContext/use-user.ts b/src/AuthContext/use-user.ts index 671048d..7aff447 100644 --- a/src/AuthContext/use-user.ts +++ b/src/AuthContext/use-user.ts @@ -22,7 +22,6 @@ export default function useUser({ user, updateUser, }: UseAuthData) { - const {mutateAsync: refresh} = useMutation(refreshToken, { onError: () => logout(), }) diff --git a/src/hooks/use-user.ts b/src/hooks/use-user.ts index ef62835..35fb976 100644 --- a/src/hooks/use-user.ts +++ b/src/hooks/use-user.ts @@ -1,5 +1,5 @@ import {useLocation, useNavigate} from "react-router-dom" -import {useContext, useLayoutEffect} from "react" +import {useContext, useEffect, useLayoutEffect} from "react" import {ServerUser, User} from "~/server-types" import AuthContext from "~/AuthContext/AuthContext" @@ -17,7 +17,7 @@ export default function useUser(): ServerUser | User { const navigate = useNavigate() const {user, isAuthenticated} = useContext(AuthContext) - useLayoutEffect(() => { + useEffect(() => { if ( !isAuthenticated && !AUTHENTICATION_PATHS.includes(location.pathname) diff --git a/src/routes/EnterDecryptionPassword.tsx b/src/routes/EnterDecryptionPassword.tsx index 15bebbf..29e63c6 100644 --- a/src/routes/EnterDecryptionPassword.tsx +++ b/src/routes/EnterDecryptionPassword.tsx @@ -36,6 +36,7 @@ export default function EnterDecryptionPassword(): ReactElement { onSubmit: async ({password}, {setErrors}) => { const decryptionPassword = buildEncryptionPassword(password, user.email.address) + console.log("decryptionPassword", decryptionPassword) if (!_setDecryptionPassword(decryptionPassword)) { setErrors({ password: t(