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] 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]) +}