fixed password setup not working; general improvements & bugfixes

This commit is contained in:
Myzel394 2022-12-17 23:14:01 +01:00
parent 4ab7303071
commit 934a071f81
8 changed files with 186 additions and 146 deletions

View File

@ -11,13 +11,13 @@ export enum EncryptionStatus {
interface AuthContextTypeBase { interface AuthContextTypeBase {
user: ServerUser | User | null user: ServerUser | User | null
isAuthenticated: boolean isAuthenticated: boolean
login: (user: ServerUser | User) => void login: (user: ServerUser | User, callback?: () => void) => void
logout: () => void logout: () => void
encryptionStatus: EncryptionStatus encryptionStatus: EncryptionStatus
_decryptUsingMasterPassword: (content: string) => string _decryptUsingMasterPassword: (content: string) => string
_encryptUsingMasterPassword: (content: string) => string _encryptUsingMasterPassword: (content: string) => string
_decryptUsingPrivateKey: (message: string) => Promise<string> _decryptUsingPrivateKey: (message: string) => Promise<string>
_setDecryptionPassword: (decryptionPassword: string) => boolean _setDecryptionPassword: (decryptionPassword: string, callback?: () => void) => boolean
_updateUser: (user: ServerUser | User) => void _updateUser: (user: ServerUser | User) => void
} }

View File

@ -1,6 +1,6 @@
import {ReactElement, ReactNode, useCallback} from "react" import {ReactElement, ReactNode, useCallback} from "react"
import {useLocalStorage} from "react-use" import {useLocalStorage} from "react-use"
import fastHashCode from "fast-hash-code"; import fastHashCode from "fast-hash-code"
import {ServerUser, User} from "~/server-types" import {ServerUser, User} from "~/server-types"
@ -9,7 +9,7 @@ import PasswordShareConfirmationDialog from "./PasswordShareConfirmationDialog"
import useContextValue from "./use-context-value" import useContextValue from "./use-context-value"
import useExtensionHandler from "./use-extension-handler" import useExtensionHandler from "./use-extension-handler"
import useMasterPassword from "./use-master-password" import useMasterPassword from "./use-master-password"
import useUser from "./use-user"; import useUser from "./use-user"
export interface AuthContextProviderProps { export interface AuthContextProviderProps {
children: ReactNode children: ReactNode
@ -26,9 +26,11 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
decryptUsingMasterPassword, decryptUsingMasterPassword,
decryptUsingPrivateKey, decryptUsingPrivateKey,
setDecryptionPassword, setDecryptionPassword,
decryptionPasswordHash,
_masterPassword, _masterPassword,
logout: logoutMasterPassword, logout: logoutMasterPassword,
} = useMasterPassword(user || null) } = useMasterPassword(user || null)
const passwordHash = _masterPassword ? fastHashCode(_masterPassword).toString() : null
const {sharePassword, closeDialog, showDialog, dispatchPasswordStatus} = useExtensionHandler( const {sharePassword, closeDialog, showDialog, dispatchPasswordStatus} = useExtensionHandler(
_masterPassword!, _masterPassword!,
user as User, user as User,
@ -39,10 +41,11 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
}, [logoutMasterPassword]) }, [logoutMasterPassword])
const contextValue = useContextValue({ const contextValue = useContextValue({
decryptUsingPrivateKey, _decryptUsingPrivateKey: decryptUsingPrivateKey,
encryptUsingMasterPassword, _encryptUsingMasterPassword: encryptUsingMasterPassword,
decryptUsingMasterPassword, _decryptUsingMasterPassword: decryptUsingMasterPassword,
setDecryptionPassword, _setDecryptionPassword: setDecryptionPassword,
decryptionPasswordHash,
logout, logout,
login: setUser, login: setUser,
user: user || null, user: user || null,
@ -53,7 +56,7 @@ export default function AuthContextProvider({children}: AuthContextProviderProps
decryptUsingMasterPassword, decryptUsingMasterPassword,
user: user || null, user: user || null,
updateUser: setUser, updateUser: setUser,
masterPasswordHash: _masterPassword ? fastHashCode(_masterPassword).toString() : null, masterPasswordHash: passwordHash,
}) })
return ( return (

View File

@ -1,49 +1,86 @@
import {useMemo, useRef} from "react"
import {useUpdateEffect} from "react-use"
import {AuthContextType, EncryptionStatus} from "~/AuthContext/AuthContext" import {AuthContextType, EncryptionStatus} from "~/AuthContext/AuthContext"
import {ServerUser, User} from "~/server-types" import {ServerUser, User} from "~/server-types"
export interface UseContextValueData { export type UseContextValueData = Pick<
user: User | ServerUser | null AuthContextType,
| "user"
| "logout"
| "_encryptUsingMasterPassword"
| "_decryptUsingMasterPassword"
| "_decryptUsingPrivateKey"
> & {
decryptionPasswordHash: string
_setDecryptionPassword: (password: string) => boolean
login: (user: User | ServerUser) => void login: (user: User | ServerUser) => void
logout: () => void
encryptUsingMasterPassword: (content: string) => string
decryptUsingMasterPassword: (content: string) => string
decryptUsingPrivateKey: (message: string) => Promise<string>
setDecryptionPassword: (password: string) => boolean
} }
export default function useContextValue({ export default function useContextValue({
user, user,
login, login,
logout, logout,
encryptUsingMasterPassword, _encryptUsingMasterPassword,
decryptUsingMasterPassword, _decryptUsingMasterPassword,
setDecryptionPassword, _setDecryptionPassword,
decryptUsingPrivateKey, _decryptUsingPrivateKey,
decryptionPasswordHash,
}: UseContextValueData): AuthContextType { }: UseContextValueData): AuthContextType {
return { const $decryptionPasswordChangeCallback = useRef<(() => void) | null>(null)
user, const $userChangeCallback = useRef<(() => void) | null>(null)
login,
logout,
isAuthenticated: Boolean(user),
encryptionStatus: (() => {
if (!user) {
return EncryptionStatus.Unavailable
}
if (!user.encryptedPassword) { useUpdateEffect(() => {
return EncryptionStatus.Unavailable $decryptionPasswordChangeCallback.current?.()
} }, [decryptionPasswordHash, user])
if (user.isDecrypted) { return useMemo(
return EncryptionStatus.Available () => ({
} user,
login: (user, callback) => {
if (callback) {
$userChangeCallback.current = callback
}
return EncryptionStatus.PasswordRequired return login(user)
})(), },
_updateUser: login, logout,
_setDecryptionPassword: setDecryptionPassword, isAuthenticated: Boolean(user),
_encryptUsingMasterPassword: encryptUsingMasterPassword, encryptionStatus: (() => {
_decryptUsingMasterPassword: decryptUsingMasterPassword, if (!user) {
_decryptUsingPrivateKey: decryptUsingPrivateKey, return EncryptionStatus.Unavailable
} }
if (!user.encryptedPassword) {
return EncryptionStatus.Unavailable
}
if (user.isDecrypted) {
return EncryptionStatus.Available
}
return EncryptionStatus.PasswordRequired
})(),
_updateUser: login,
_setDecryptionPassword: (password, callback) => {
if (callback) {
$decryptionPasswordChangeCallback.current = callback
}
return _setDecryptionPassword(password)
},
_encryptUsingMasterPassword,
_decryptUsingMasterPassword,
_decryptUsingPrivateKey,
}),
[
user,
login,
logout,
_setDecryptionPassword,
_encryptUsingMasterPassword,
_decryptUsingMasterPassword,
_decryptUsingPrivateKey,
],
)
} }

View File

@ -1,9 +1,10 @@
import {useLocalStorage} from "react-use" import {useLocalStorage} from "react-use"
import {useCallback, useMemo} from "react" import {useCallback, useMemo} from "react"
import {decrypt, readMessage, readPrivateKey} from "openpgp"; import {decrypt, readMessage, readPrivateKey} from "openpgp"
import fastHashCode from "fast-hash-code"
import {decryptString, encryptString} from "~/utils" import {decryptString, encryptString} from "~/utils"
import {ServerUser, User} from "~/server-types"; import {ServerUser, User} from "~/server-types"
export interface UseMasterPasswordResult { export interface UseMasterPasswordResult {
encryptUsingMasterPassword: (content: string) => string encryptUsingMasterPassword: (content: string) => string
@ -12,13 +13,12 @@ export interface UseMasterPasswordResult {
setDecryptionPassword: (password: string) => boolean setDecryptionPassword: (password: string) => boolean
logout: () => void logout: () => void
decryptionPasswordHash: string
// Use this cautiously // Use this cautiously
_masterPassword: string _masterPassword: string
} }
export default function useMasterPassword( export default function useMasterPassword(user: User | ServerUser | null): UseMasterPasswordResult {
user: User | ServerUser | null,
): UseMasterPasswordResult {
const [decryptionPassword, setDecryptionPassword] = useLocalStorage<string | null>( const [decryptionPassword, setDecryptionPassword] = useLocalStorage<string | null>(
"_global-context-auth-decryption-password", "_global-context-auth-decryption-password",
null, null,
@ -78,21 +78,23 @@ export default function useMasterPassword(
[user], [user],
) )
const updateDecryptionPassword = useCallback((password: string) => { const updateDecryptionPassword = useCallback(
if (!user || !user.encryptedPassword) { (password: string) => {
throw new Error("User not set.") if (user?.encryptedPassword) {
} try {
const masterPassword = decryptString(user.encryptedPassword, password)
JSON.parse(decryptString((user as ServerUser).encryptedNotes, masterPassword))
} catch (e) {
return false
}
}
try {
const masterPassword = decryptString(user.encryptedPassword, password)
JSON.parse(decryptString((user as ServerUser).encryptedNotes, masterPassword))
setDecryptionPassword(password) setDecryptionPassword(password)
} catch {
return false;
}
return true; return true
}, [user, masterPassword]) },
[user, masterPassword],
)
const logout = useCallback(() => { const logout = useCallback(() => {
setDecryptionPassword(null) setDecryptionPassword(null)
@ -105,5 +107,6 @@ export default function useMasterPassword(
logout, logout,
setDecryptionPassword: updateDecryptionPassword, setDecryptionPassword: updateDecryptionPassword,
_masterPassword: masterPassword!, _masterPassword: masterPassword!,
decryptionPasswordHash: fastHashCode(decryptionPassword || "").toString(),
} }
} }

View File

@ -1,77 +1,74 @@
import {Dispatch, SetStateAction, useEffect} from "react"; import {Dispatch, SetStateAction, useEffect} from "react"
import {AxiosError} from "axios"; import {AxiosError} from "axios"
import {useMutation, useQuery} from "@tanstack/react-query"; import {useMutation, useQuery} from "@tanstack/react-query"
import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis" import {REFRESH_TOKEN_URL, RefreshTokenResult, getMe, refreshToken} from "~/apis"
import {AuthenticationDetails, ServerUser, User} from "~/server-types"; import {AuthenticationDetails, ServerUser, User} from "~/server-types"
import {client} from "~/constants/axios-client"; import {client} from "~/constants/axios-client"
export interface UseAuthData { export interface UseAuthData {
logout: () => void logout: () => void
masterPasswordHash: string | null masterPasswordHash: string | null
decryptUsingMasterPassword: (content: string) => string decryptUsingMasterPassword: (content: string) => string
user: User | ServerUser | null user: User | ServerUser | null
updateUser: Dispatch<SetStateAction<User | ServerUser | null | undefined>> updateUser: Dispatch<SetStateAction<User | ServerUser | null | undefined>>
} }
export default function useUser({ export default function useUser({
logout, logout,
masterPasswordHash, masterPasswordHash,
decryptUsingMasterPassword, decryptUsingMasterPassword,
user, user,
updateUser, updateUser,
}: UseAuthData) { }: UseAuthData) {
const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, { const {mutateAsync: refresh} = useMutation<RefreshTokenResult, AxiosError, void>(refreshToken, {
onError: () => logout(), onError: () => logout(),
}) })
useQuery<AuthenticationDetails, AxiosError>(["get_me"], getMe, { useQuery<AuthenticationDetails, AxiosError>(["get_me"], getMe, {
refetchOnWindowFocus: "always", refetchOnWindowFocus: "always",
refetchOnReconnect: "always", refetchOnReconnect: "always",
retry: 2, retry: 2,
enabled: user !== null, enabled: user !== null,
}) })
// Decrypt user notes // Decrypt user notes
useEffect(() => { useEffect(() => {
if (user && !user.isDecrypted && user.encryptedPassword && masterPasswordHash) { if (user && !user.isDecrypted && user.encryptedPassword && masterPasswordHash) {
const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!)) const note = JSON.parse(decryptUsingMasterPassword(user.encryptedNotes!))
updateUser( updateUser({
prevUser => ...user,
({ notes: note,
...(prevUser || {}), isDecrypted: true,
notes: note, } as User)
isDecrypted: true, }
} as User), }, [user, decryptUsingMasterPassword, updateUser, masterPasswordHash])
)
}
}, [user, decryptUsingMasterPassword, updateUser, masterPasswordHash])
// Refresh token and logout user if needed // Refresh token and logout user if needed
useEffect(() => { useEffect(() => {
const interceptor = client.interceptors.response.use( const interceptor = client.interceptors.response.use(
response => response, response => response,
async (error: AxiosError) => { async (error: AxiosError) => {
if (error.isAxiosError) { if (error.isAxiosError) {
if (error.response?.status === 401) { if (error.response?.status === 401) {
// Check if error comes from refreshing the token. // Check if error comes from refreshing the token.
// If yes, the user has been logged out completely. // If yes, the user has been logged out completely.
const request: XMLHttpRequest = error.request const request: XMLHttpRequest = error.request
if (request.responseURL === REFRESH_TOKEN_URL) { if (request.responseURL === REFRESH_TOKEN_URL) {
await logout() await logout()
} else { } else {
await refresh() await refresh()
} }
} }
} }
throw error throw error
}, },
) )
return () => client.interceptors.response.eject(interceptor) return () => client.interceptors.response.eject(interceptor)
}, [logout, refresh]) }, [logout, refresh])
} }

View File

@ -12,7 +12,7 @@ import passwordGenerator from "secure-random-password"
import {PasswordField, SimpleForm} from "~/components" import {PasswordField, SimpleForm} from "~/components"
import {buildEncryptionPassword, encryptString} from "~/utils" import {buildEncryptionPassword, encryptString} from "~/utils"
import {isDev} from "~/constants/development" import {isDev} from "~/constants/development"
import {useExtensionHandler, useSystemPreferredTheme, useUser} from "~/hooks" import {useExtensionHandler, useNavigateToNext, useSystemPreferredTheme, useUser} from "~/hooks"
import {MASTER_PASSWORD_LENGTH} from "~/constants/values" import {MASTER_PASSWORD_LENGTH} from "~/constants/values"
import {AuthenticationDetails, UserNote} from "~/server-types" import {AuthenticationDetails, UserNote} from "~/server-types"
import {UpdateAccountData, updateAccount} from "~/apis" import {UpdateAccountData, updateAccount} from "~/apis"
@ -34,6 +34,7 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
const user = useUser() const user = useUser()
const theme = useSystemPreferredTheme() const theme = useSystemPreferredTheme()
const navigateToNext = useNavigateToNext()
const $password = useRef<HTMLInputElement | null>(null) const $password = useRef<HTMLInputElement | null>(null)
const $passwordConfirmation = useRef<HTMLInputElement | null>(null) const $passwordConfirmation = useRef<HTMLInputElement | null>(null)
const schema = yup.object().shape({ const schema = yup.object().shape({
@ -65,12 +66,6 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
) )
const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, UpdateAccountData>( const {mutateAsync} = useMutation<AuthenticationDetails, AxiosError, UpdateAccountData>(
updateAccount, updateAccount,
{
onSuccess: ({user}) => {
login(user)
setTimeout(onDone, 0)
},
},
) )
const formik = useFormik<Form>({ const formik = useFormik<Form>({
validationSchema: schema, validationSchema: schema,
@ -97,19 +92,25 @@ export default function PasswordForm({onDone}: PasswordFormProps): ReactElement
} }
const encryptedNotes = encryptUserNote(note, masterPassword) const encryptedNotes = encryptUserNote(note, masterPassword)
_setDecryptionPassword(encryptionPassword) await mutateAsync(
{
await mutateAsync({ encryptedPassword: encryptedMasterPassword,
encryptedPassword: encryptedMasterPassword, publicKey: (
publicKey: ( await readKey({
await readKey({ armoredKey: keyPair.publicKey,
armoredKey: keyPair.publicKey, })
}) )
) .toPublic()
.toPublic() .armor(),
.armor(), encryptedNotes,
encryptedNotes, },
}) {
onSuccess: ({user: newUser}) => {
login(newUser)
_setDecryptionPassword(encryptionPassword, navigateToNext)
},
},
)
} catch (error) { } catch (error) {
setErrors({detail: t("general.defaultError")}) setErrors({detail: t("general.defaultError")})
} }

View File

@ -57,7 +57,7 @@ export default function ConfirmCodeForm({
sameRequestToken, sameRequestToken,
}: ConfirmCodeFormProps): ReactElement { }: ConfirmCodeFormProps): ReactElement {
const settings = useLoaderData() as ServerSettings const settings = useLoaderData() as ServerSettings
const expirationTime = isDev ? 9 : settings.emailLoginExpirationInSeconds const expirationTime = isDev ? 70 : settings.emailLoginExpirationInSeconds
const {t} = useTranslation() const {t} = useTranslation()
const requestDate = useMemo(() => new Date(), []) const requestDate = useMemo(() => new Date(), [])
const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false) const [isExpiringSoon, setIsExpiringSoon] = useState<boolean>(false)

View File

@ -36,15 +36,14 @@ export default function EnterDecryptionPassword(): ReactElement {
onSubmit: async ({password}, {setErrors}) => { onSubmit: async ({password}, {setErrors}) => {
const decryptionPassword = buildEncryptionPassword(password, user.email.address) const decryptionPassword = buildEncryptionPassword(password, user.email.address)
console.log("decryptionPassword", decryptionPassword) const isPasswordCorrect = _setDecryptionPassword(decryptionPassword, navigateToNext)
if (!_setDecryptionPassword(decryptionPassword)) {
if (!isPasswordCorrect) {
setErrors({ setErrors({
password: t( password: t(
"components.EnterDecryptionPassword.form.password.errors.invalidPassword", "components.EnterDecryptionPassword.form.password.errors.invalidPassword",
), ),
}) })
} else {
setTimeout(navigateToNext, 0)
} }
}, },
}) })