diff --git a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx index 1652577c6..95862d18d 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/CredentialDetails/EmailBlock.tsx @@ -1,19 +1,45 @@ import React from 'react'; import { EmailPreview } from '@/entrypoints/popup/components/EmailPreview'; +import { useDb } from '@/entrypoints/popup/context/DbContext'; type EmailBlockProps = { email: string; - isSupported: boolean; } /** * Render the email block. */ -const EmailBlock: React.FC = ({ email, isSupported }) => ( - <> - {isSupported && } - -); +const EmailBlock: React.FC = ({ email }) => { + const dbContext = useDb(); + + /** + * Check if the email domain is supported. + */ + const isEmailDomainSupported = async (email: string): Promise => { + const domain = email.split('@')[1]?.toLowerCase(); + if (!domain) { + return false; + } + + const vaultMetadata = await dbContext.getVaultMetadata(); + const publicDomains = vaultMetadata?.publicEmailDomains ?? []; + const privateDomains = vaultMetadata?.privateEmailDomains ?? []; + + return [...publicDomains, ...privateDomains].some(supportedDomain => + domain === supportedDomain || domain.endsWith(`.${supportedDomain}`) + ); + }; + + if (!isEmailDomainSupported(email)) { + return null; + } + + return ( + <> + {} + + ); +}; export default EmailBlock; \ No newline at end of file diff --git a/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx index 00dce9236..2f904ad76 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/AuthContext.tsx @@ -11,6 +11,7 @@ type AuthContextType = { isLoggedIn: boolean; isInitialized: boolean; username: string | null; + initializeAuth: () => Promise<{ isLoggedIn: boolean }>; setAuthTokens: (username: string, accessToken: string, refreshToken: string) => Promise; login: () => Promise; logout: (errorMessage?: string) => Promise; @@ -34,25 +35,29 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children const dbContext = useDb(); /** - * Check for tokens in browser local storage on initial load. + * Initialize the authentication state. + * + * @returns object containing whether the user is logged in. + */ + const initializeAuth = useCallback(async () : Promise<{ isLoggedIn: boolean }> => { + const accessToken = await storage.getItem('local:accessToken') as string; + const refreshToken = await storage.getItem('local:refreshToken') as string; + const username = await storage.getItem('local:username') as string; + if (accessToken && refreshToken && username) { + setUsername(username); + setIsLoggedIn(true); + } + setIsInitialized(true); + + return { isLoggedIn }; + }, [setUsername, setIsLoggedIn, isLoggedIn]); + + /** + * Check for tokens in browser local storage on initial load when this context is mounted. */ useEffect(() => { - /** - * Initialize the authentication state. - */ - const initializeAuth = async () : Promise => { - const accessToken = await storage.getItem('local:accessToken') as string; - const refreshToken = await storage.getItem('local:refreshToken') as string; - const username = await storage.getItem('local:username') as string; - if (accessToken && refreshToken && username) { - setUsername(username); - setIsLoggedIn(true); - } - setIsInitialized(true); - }; - initializeAuth(); - }, []); + }, [initializeAuth]); /** * Set auth tokens in browser local storage as part of the login process. After db is initialized, the login method should be called as well. @@ -103,12 +108,13 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children isLoggedIn, isInitialized, username, + initializeAuth, setAuthTokens, login, logout, globalMessage, clearGlobalMessage, - }), [isLoggedIn, isInitialized, username, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]); + }), [isLoggedIn, isInitialized, username, initializeAuth, globalMessage, setAuthTokens, login, logout, clearGlobalMessage]); return ( diff --git a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx index 3754d2917..dda0e649d 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx @@ -2,9 +2,10 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use import { sendMessage } from 'webext-bridge/popup'; import EncryptionUtility from '@/utils/EncryptionUtility'; +import type { VaultMetadata } from '@/utils/shared/models/metadata'; import type { VaultResponse } from '@/utils/shared/models/webapi'; import SqliteClient from '@/utils/SqliteClient'; -import { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse'; +import type { VaultResponse as messageVaultResponse } from '@/utils/types/messaging/VaultResponse'; type DbContextType = { sqliteClient: SqliteClient | null; @@ -12,9 +13,7 @@ type DbContextType = { dbAvailable: boolean; initializeDatabase: (vaultResponse: VaultResponse, derivedKey: string) => Promise; clearDatabase: () => void; - vaultRevision: number; - publicEmailDomains: string[]; - privateEmailDomains: string[]; + getVaultMetadata: () => Promise; } const DbContext = createContext(undefined); @@ -38,20 +37,10 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } */ const [dbAvailable, setDbAvailable] = useState(false); - /** - * Public email domains. - */ - const [publicEmailDomains, setPublicEmailDomains] = useState([]); - /** * Vault revision. */ - const [vaultRevision, setVaultRevision] = useState(0); - - /** - * Private email domains. - */ - const [privateEmailDomains, setPrivateEmailDomains] = useState([]); + const [vaultMetadata, setVaultMetadata] = useState(null); const initializeDatabase = useCallback(async (vaultResponse: VaultResponse, derivedKey: string) => { // Attempt to decrypt the blob. @@ -67,9 +56,11 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } setSqliteClient(client); setDbInitialized(true); setDbAvailable(true); - setPublicEmailDomains(vaultResponse.vault.publicEmailDomainList); - setPrivateEmailDomains(vaultResponse.vault.privateEmailDomainList); - setVaultRevision(vaultResponse.vault.currentRevisionNumber); + setVaultMetadata({ + publicEmailDomains: vaultResponse.vault.publicEmailDomainList, + privateEmailDomains: vaultResponse.vault.privateEmailDomainList, + vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, + }); /* * Store encrypted vault in background worker. @@ -90,9 +81,11 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } setSqliteClient(client); setDbInitialized(true); setDbAvailable(true); - setPublicEmailDomains(response.publicEmailDomains ?? []); - setPrivateEmailDomains(response.privateEmailDomains ?? []); - setVaultRevision(response.vaultRevisionNumber ?? 0); + setVaultMetadata({ + publicEmailDomains: response.publicEmailDomains ?? [], + privateEmailDomains: response.privateEmailDomains ?? [], + vaultRevisionNumber: response.vaultRevisionNumber ?? 0, + }); } else { setDbInitialized(true); setDbAvailable(false); @@ -104,6 +97,13 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } } }, []); + /** + * Get the vault metadata. + */ + const getVaultMetadata = useCallback(async () : Promise => { + return vaultMetadata; + }, [vaultMetadata]); + /** * Check if database is initialized and try to retrieve vault from background */ @@ -128,10 +128,8 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } dbAvailable, initializeDatabase, clearDatabase, - vaultRevision, - publicEmailDomains, - privateEmailDomains - }), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, vaultRevision, publicEmailDomains, privateEmailDomains]); + getVaultMetadata, + }), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, clearDatabase, getVaultMetadata]); return ( diff --git a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts index ff16435b7..e3b1109fa 100644 --- a/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts +++ b/apps/browser-extension/src/entrypoints/popup/hooks/useVaultSync.ts @@ -1,10 +1,10 @@ import { useCallback } from 'react'; +import { sendMessage } from 'webext-bridge/popup'; import { useAuth } from '@/entrypoints/popup/context/AuthContext'; import { useDb } from '@/entrypoints/popup/context/DbContext'; import { useWebApi } from '@/entrypoints/popup/context/WebApiContext'; -import { AppInfo } from '@/utils/AppInfo'; import type { VaultResponse } from '@/utils/shared/models/webapi'; /** @@ -13,9 +13,10 @@ import type { VaultResponse } from '@/utils/shared/models/webapi'; const withMinimumDelay = async ( operation: () => Promise, minDelayMs: number, - initialSync: boolean + enableDelay: boolean = true ): Promise => { - if (!initialSync) { + if (!enableDelay) { + // If delay is disabled, return the result immediately. return operation(); } @@ -35,7 +36,7 @@ type VaultSyncOptions = { onSuccess?: (hasNewVault: boolean) => void; onError?: (error: string) => void; onStatus?: (message: string) => void; - onOffline?: () => void; + _onOffline?: () => void; } /** @@ -49,7 +50,10 @@ export const useVaultSync = () : { const webApi = useWebApi(); const syncVault = useCallback(async (options: VaultSyncOptions = {}) => { - const { initialSync = false, onSuccess, onError, onStatus, onOffline } = options; + const { initialSync = false, onSuccess, onError, onStatus, _onOffline } = options; + + // For the initial sync, we add an artifical delay to various steps which makes it feel more fluid. + const enableDelay = initialSync; try { const { isLoggedIn } = await authContext.initializeAuth(); @@ -61,26 +65,15 @@ export const useVaultSync = () : { // Check app status and vault revision onStatus?.('Checking vault updates'); - const statusResponse = await withMinimumDelay( - () => webApi.getStatus(), - 300, - initialSync - ); + const statusResponse = await withMinimumDelay(() => webApi.getStatus(), 300, enableDelay); + // Check if server is actually available, 0.0.0 indicates connection error which triggers offline mode. if (statusResponse.serverVersion === '0.0.0') { - // Server is not available, go into offline mode - onOffline?.(); - return false; + // Offline mode is not implemented for browser extension yet, let it fail below due to the validateStatusResponse check. } - if (!statusResponse.clientVersionSupported) { - const statusError = 'This version of the AliasVault mobile app is not supported by the server anymore. Please update your app to the latest version.'; - onError?.(statusError); - return false; - } - - if (!AppInfo.isServerVersionSupported(statusResponse.serverVersion)) { - const statusError = 'The AliasVault server needs to be updated to a newer version in order to use this mobile app. Please contact support if you need help.'; + const statusError = webApi.validateStatusResponse(statusResponse); + if (statusError) { onError?.(statusError); return false; } @@ -97,11 +90,7 @@ export const useVaultSync = () : { if (statusResponse.vaultRevision > vaultRevisionNumber) { onStatus?.('Syncing updated vault'); - const vaultResponseJson = await withMinimumDelay( - () => webApi.get('Vault'), - 1000, - initialSync - ); + const vaultResponseJson = await withMinimumDelay(() => webApi.get('Vault'), 1000, enableDelay); const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse); if (vaultError) { @@ -122,7 +111,9 @@ export const useVaultSync = () : { } try { - await dbContext.initializeDatabase(vaultResponseJson as VaultResponse); + // Get derived key from background worker + const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string; + await dbContext.initializeDatabase(vaultResponseJson as VaultResponse, passwordHashBase64); onSuccess?.(true); return true; } catch { @@ -131,11 +122,7 @@ export const useVaultSync = () : { } } - await withMinimumDelay( - () => Promise.resolve(onSuccess?.(false)), - 300, - initialSync - ); + await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay); return false; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync'; diff --git a/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx b/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx index 3b0fff0d7..25cf32b31 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/CredentialDetails.tsx @@ -50,23 +50,6 @@ const CredentialDetails: React.FC = () => { window.close(); }; - /** - * Check if the email domain is supported. - */ - const isEmailDomainSupported = (email: string): boolean => { - const domain = email.split('@')[1]?.toLowerCase(); - if (!domain) { - return false; - } - - const publicDomains = dbContext.publicEmailDomains ?? []; - const privateDomains = dbContext.privateEmailDomains ?? []; - - return [...publicDomains, ...privateDomains].some(supportedDomain => - domain === supportedDomain || domain.endsWith(`.${supportedDomain}`) - ); - }; - useEffect(() => { if (isPopup()) { window.history.replaceState({}, '', `popup.html#/credentials`); @@ -101,7 +84,6 @@ const CredentialDetails: React.FC = () => { {credential.Alias?.Email && ( )} diff --git a/apps/browser-extension/src/entrypoints/popup/pages/CredentialsList.tsx b/apps/browser-extension/src/entrypoints/popup/pages/CredentialsList.tsx index a9f0c58d5..4b21a7729 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/CredentialsList.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/CredentialsList.tsx @@ -1,5 +1,4 @@ import React, { useState, useEffect, useCallback } from 'react'; -import { sendMessage } from 'webext-bridge/popup'; import CredentialCard from '@/entrypoints/popup/components/CredentialCard'; import LoadingSpinner from '@/entrypoints/popup/components/LoadingSpinner'; @@ -7,9 +6,9 @@ import ReloadButton from '@/entrypoints/popup/components/ReloadButton'; import { useDb } from '@/entrypoints/popup/context/DbContext'; import { useLoading } from '@/entrypoints/popup/context/LoadingContext'; import { useWebApi } from '@/entrypoints/popup/context/WebApiContext'; +import { useVaultSync } from '@/entrypoints/popup/hooks/useVaultSync'; import type { Credential } from '@/utils/shared/models/vault'; -import type { VaultResponse } from '@/utils/shared/models/webapi'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; @@ -19,6 +18,7 @@ import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; const CredentialsList: React.FC = () => { const dbContext = useDb(); const webApi = useWebApi(); + const { syncVault } = useVaultSync(); const [credentials, setCredentials] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const { showLoading, hideLoading, setIsInitialLoading } = useLoading(); @@ -36,53 +36,36 @@ const CredentialsList: React.FC = () => { return; } - // Do status check first to ensure the extension is (still) supported. - const statusResponse = await webApi.getStatus(); - const statusError = webApi.validateStatusResponse(statusResponse); - if (statusError !== null) { - await webApi.logout(statusError); - return; - } - try { - // If the vault revision is the same or lower, (re)load existing credentials. - if (statusResponse.vaultRevision <= dbContext.vaultRevision) { - const results = dbContext.sqliteClient.getAllCredentials(); - setCredentials(results); - return; - } - - /** - * If the vault revision is higher, fetch the latest vault and initialize the SQLite context again. - * This will trigger a new credentials list refresh. - */ - const vaultResponseJson = await webApi.get('Vault'); - - const vaultError = webApi.validateVaultResponse(vaultResponseJson); - if (vaultError) { - await webApi.logout(vaultError); - hideLoading(); - return; - } - - // Get derived key from background worker - const passwordHashBase64 = await sendMessage('GET_DERIVED_KEY', {}, 'background') as string; - - // Initialize the SQLite context again with the newly retrieved decrypted blob) - try { - await dbContext.initializeDatabase(vaultResponseJson, passwordHashBase64); - } catch { + // Sync vault and load credentials + await syncVault({ /** - * If error occurs during database initialization, it most likely has to do with decryption that - * failed. This is most likely due to the user changing their password. - * So we logout the user here to force them to re-authenticate. + * On success. */ - await webApi.logout('Vault could not be decrypted, please re-authenticate.'); - } + onSuccess: async (_hasNewVault) => { + // Refresh credentials list, whether there is a new vault or not. + const results = dbContext.sqliteClient?.getAllCredentials() ?? []; + setCredentials(results); + }, + /** + * On offline. + */ + onOffline: () => { + // Not implemented for browser extension yet. + }, + /** + * On error. + */ + onError: async (error) => { + console.error('Error syncing vault:', error); + await webApi.logout('Error while syncing vault, please re-authenticate.'); + }, + }); } catch (err) { - console.error('Refresh error:', err); + console.error('Error refreshing credentials:', err); + await webApi.logout('Error while syncing vault, please re-authenticate.'); } - }, [dbContext, webApi, hideLoading]); + }, [dbContext, webApi, syncVault]); /** * Manually refresh the credentials list. @@ -103,7 +86,8 @@ const CredentialsList: React.FC = () => { const refreshCredentials = async () : Promise => { if (dbContext?.sqliteClient) { setIsLoading(true); - await onRefresh(); + const results = dbContext.sqliteClient?.getAllCredentials() ?? []; + setCredentials(results); setIsLoading(false); // Hide the global app initial loading state after the credentials list is loaded. @@ -112,7 +96,7 @@ const CredentialsList: React.FC = () => { }; refreshCredentials(); - }, [dbContext?.sqliteClient, onRefresh, setIsLoading, setIsInitialLoading]); + }, [dbContext?.sqliteClient, setIsLoading, setIsInitialLoading]); // Add this function to filter credentials const filteredCredentials = credentials.filter(cred => { diff --git a/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx b/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx index dcc88283a..8a38d02b0 100644 --- a/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx +++ b/apps/browser-extension/src/entrypoints/popup/pages/EmailDetails.tsx @@ -8,7 +8,7 @@ import { useWebApi } from '@/entrypoints/popup/context/WebApiContext'; import ConversionUtility from '@/entrypoints/popup/utils/ConversionUtility'; import EncryptionUtility from '@/utils/EncryptionUtility'; -import type { Attachment, Email } from '@/utils/shared/models/webapi'; +import type { EmailAttachment, Email } from '@/utils/shared/models/webapi'; import { useMinDurationLoading } from '@/hooks/useMinDurationLoading'; @@ -112,7 +112,7 @@ const EmailDetails: React.FC = () => { /** * Handle downloading an attachment. */ - const handleDownloadAttachment = async (attachment: Attachment): Promise => { + const handleDownloadAttachment = async (attachment: EmailAttachment): Promise => { try { // Get the encrypted attachment bytes from the API const base64EncryptedAttachment = await webApi.downloadBlobAndConvertToBase64(`Email/${id}/attachments/${attachment.id}`); diff --git a/apps/browser-extension/src/utils/shared/models/metadata/index.d.ts b/apps/browser-extension/src/utils/shared/models/metadata/index.d.ts new file mode 100644 index 000000000..e09bb64c7 --- /dev/null +++ b/apps/browser-extension/src/utils/shared/models/metadata/index.d.ts @@ -0,0 +1,18 @@ +type VaultMetadata = { + publicEmailDomains: string[]; + privateEmailDomains: string[]; + vaultRevisionNumber: number; +}; + +/** + * These parameters for deriving encryption key from plain text password. These are stored + * as metadata in the vault upon initial login, and are used to derive the encryption key + * from the plain text password in the unlock screen. + */ +type EncryptionKeyDerivationParams = { + encryptionType: string; + encryptionSettings: string; + salt: string; +}; + +export type { EncryptionKeyDerivationParams, VaultMetadata }; diff --git a/apps/browser-extension/src/utils/shared/models/metadata/index.js b/apps/browser-extension/src/utils/shared/models/metadata/index.js new file mode 100644 index 000000000..a41e52c7b --- /dev/null +++ b/apps/browser-extension/src/utils/shared/models/metadata/index.js @@ -0,0 +1,3 @@ + +//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/apps/browser-extension/src/utils/shared/models/webapi/index.d.ts b/apps/browser-extension/src/utils/shared/models/webapi/index.d.ts index 475810070..53dd0e958 100644 --- a/apps/browser-extension/src/utils/shared/models/webapi/index.d.ts +++ b/apps/browser-extension/src/utils/shared/models/webapi/index.d.ts @@ -144,7 +144,7 @@ type MailboxBulkResponse = { /** * Email attachment type. */ -type Attachment = { +type EmailAttachment = { /** The ID of the attachment */ id: number; /** The ID of the email the attachment belongs to */ @@ -190,7 +190,7 @@ type Email = { /** The public key of the user used to encrypt the symmetric key */ encryptionKey: string; /** The attachments of the email */ - attachments: Attachment[]; + attachments: EmailAttachment[]; }; /** @@ -353,4 +353,4 @@ declare enum AuthEventType { AccountDeletion = 99 } -export { type Attachment, AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse }; +export { AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type EmailAttachment, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse }; diff --git a/apps/mobile-app/app/login.tsx b/apps/mobile-app/app/login.tsx index f92c0df28..56b8982f2 100644 --- a/apps/mobile-app/app/login.tsx +++ b/apps/mobile-app/app/login.tsx @@ -11,10 +11,10 @@ import { StyleSheet, View, Text, SafeAreaView, TextInput, TouchableOpacity, Acti import { AppInfo } from '@/utils/AppInfo'; import ConversionUtility from '@/utils/ConversionUtility'; import EncryptionUtility from '@/utils/EncryptionUtility'; +import type { EncryptionKeyDerivationParams } from '@/utils/shared/models/metadata'; import type { LoginResponse, VaultResponse } from '@/utils/shared/models/webapi'; import { SrpUtility } from '@/utils/SrpUtility'; import { ApiAuthError } from '@/utils/types/errors/ApiAuthError'; -import { EncryptionKeyDerivationParams } from '@/utils/types/messaging/EncryptionKeyDerivationParams'; import { useColors } from '@/hooks/useColorScheme'; import { useVaultSync } from '@/hooks/useVaultSync'; diff --git a/apps/mobile-app/context/DbContext.tsx b/apps/mobile-app/context/DbContext.tsx index 509f13a1f..d3c2dfe70 100644 --- a/apps/mobile-app/context/DbContext.tsx +++ b/apps/mobile-app/context/DbContext.tsx @@ -1,9 +1,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback, useMemo } from 'react'; +import type { EncryptionKeyDerivationParams, VaultMetadata } from '@/utils/shared/models/metadata'; import type { VaultResponse } from '@/utils/shared/models/webapi'; import SqliteClient from '@/utils/SqliteClient'; -import { EncryptionKeyDerivationParams } from '@/utils/types/messaging/EncryptionKeyDerivationParams'; -import { VaultMetadata } from '@/utils/types/messaging/VaultMetadata'; import NativeVaultManager from '@/specs/NativeVaultManager'; diff --git a/apps/mobile-app/hooks/useVaultMutate.ts b/apps/mobile-app/hooks/useVaultMutate.ts index bc79b1dcc..ba2e048b8 100644 --- a/apps/mobile-app/hooks/useVaultMutate.ts +++ b/apps/mobile-app/hooks/useVaultMutate.ts @@ -3,8 +3,8 @@ import Toast from 'react-native-toast-message'; import srp from 'secure-remote-password/client'; import EncryptionUtility from '@/utils/EncryptionUtility'; +import type { EncryptionKeyDerivationParams } from '@/utils/shared/models/metadata'; import type { PasswordChangeInitiateResponse, Vault, VaultPasswordChangeRequest } from '@/utils/shared/models/webapi'; -import { EncryptionKeyDerivationParams } from '@/utils/types/messaging/EncryptionKeyDerivationParams'; import { useVaultSync } from '@/hooks/useVaultSync'; diff --git a/apps/mobile-app/hooks/useVaultSync.ts b/apps/mobile-app/hooks/useVaultSync.ts index 225cda13c..c4cd00108 100644 --- a/apps/mobile-app/hooks/useVaultSync.ts +++ b/apps/mobile-app/hooks/useVaultSync.ts @@ -13,9 +13,10 @@ import { useWebApi } from '@/context/WebApiContext'; const withMinimumDelay = async ( operation: () => Promise, minDelayMs: number, - initialSync: boolean + enableDelay: boolean = true ): Promise => { - if (!initialSync) { + if (!enableDelay) { + // If delay is disabled, return the result immediately. return operation(); } @@ -51,6 +52,9 @@ export const useVaultSync = () : { const syncVault = useCallback(async (options: VaultSyncOptions = {}) => { const { initialSync = false, onSuccess, onError, onStatus, onOffline } = options; + // For the initial sync, we add an artifical delay to various steps which makes it feel more fluid. + const enableDelay = initialSync; + try { const { isLoggedIn } = await authContext.initializeAuth(); @@ -61,11 +65,7 @@ export const useVaultSync = () : { // Check app status and vault revision onStatus?.('Checking vault updates'); - const statusResponse = await withMinimumDelay( - () => webApi.getStatus(), - 300, - initialSync - ); + const statusResponse = await withMinimumDelay(() => webApi.getStatus(), 300, enableDelay); if (statusResponse.serverVersion === '0.0.0') { // Server is not available, go into offline mode @@ -94,11 +94,7 @@ export const useVaultSync = () : { if (statusResponse.vaultRevision > vaultRevisionNumber) { onStatus?.('Syncing updated vault'); - const vaultResponseJson = await withMinimumDelay( - () => webApi.get('Vault'), - 1000, - initialSync - ); + const vaultResponseJson = await withMinimumDelay(() => webApi.get('Vault'), 1000, enableDelay); const vaultError = webApi.validateVaultResponse(vaultResponseJson as VaultResponse); if (vaultError) { @@ -124,11 +120,7 @@ export const useVaultSync = () : { } } - await withMinimumDelay( - () => Promise.resolve(onSuccess?.(false)), - 300, - initialSync - ); + await withMinimumDelay(() => Promise.resolve(onSuccess?.(false)), 300, enableDelay); return false; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error during vault sync'; diff --git a/apps/mobile-app/utils/SqliteClient.tsx b/apps/mobile-app/utils/SqliteClient.tsx index a752f70f5..09ceb01e1 100644 --- a/apps/mobile-app/utils/SqliteClient.tsx +++ b/apps/mobile-app/utils/SqliteClient.tsx @@ -1,6 +1,5 @@ +import type { EncryptionKeyDerivationParams, VaultMetadata } from '@/utils/shared/models/metadata'; import type { Credential, EncryptionKey, PasswordSettings, TotpCode } from '@/utils/shared/models/vault'; -import { EncryptionKeyDerivationParams } from '@/utils/types/messaging/EncryptionKeyDerivationParams'; -import { VaultMetadata } from '@/utils/types/messaging/VaultMetadata'; import NativeVaultManager from '@/specs/NativeVaultManager'; diff --git a/apps/mobile-app/utils/shared/models/metadata/index.d.ts b/apps/mobile-app/utils/shared/models/metadata/index.d.ts new file mode 100644 index 000000000..e09bb64c7 --- /dev/null +++ b/apps/mobile-app/utils/shared/models/metadata/index.d.ts @@ -0,0 +1,18 @@ +type VaultMetadata = { + publicEmailDomains: string[]; + privateEmailDomains: string[]; + vaultRevisionNumber: number; +}; + +/** + * These parameters for deriving encryption key from plain text password. These are stored + * as metadata in the vault upon initial login, and are used to derive the encryption key + * from the plain text password in the unlock screen. + */ +type EncryptionKeyDerivationParams = { + encryptionType: string; + encryptionSettings: string; + salt: string; +}; + +export type { EncryptionKeyDerivationParams, VaultMetadata }; diff --git a/apps/mobile-app/utils/shared/models/metadata/index.js b/apps/mobile-app/utils/shared/models/metadata/index.js new file mode 100644 index 000000000..a41e52c7b --- /dev/null +++ b/apps/mobile-app/utils/shared/models/metadata/index.js @@ -0,0 +1,3 @@ + +//# sourceMappingURL=index.js.map +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/apps/mobile-app/utils/shared/models/webapi/index.d.ts b/apps/mobile-app/utils/shared/models/webapi/index.d.ts index 475810070..53dd0e958 100644 --- a/apps/mobile-app/utils/shared/models/webapi/index.d.ts +++ b/apps/mobile-app/utils/shared/models/webapi/index.d.ts @@ -144,7 +144,7 @@ type MailboxBulkResponse = { /** * Email attachment type. */ -type Attachment = { +type EmailAttachment = { /** The ID of the attachment */ id: number; /** The ID of the email the attachment belongs to */ @@ -190,7 +190,7 @@ type Email = { /** The public key of the user used to encrypt the symmetric key */ encryptionKey: string; /** The attachments of the email */ - attachments: Attachment[]; + attachments: EmailAttachment[]; }; /** @@ -353,4 +353,4 @@ declare enum AuthEventType { AccountDeletion = 99 } -export { type Attachment, AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse }; +export { AuthEventType, type AuthLogModel, type BadRequestResponse, type DeleteAccountInitiateRequest, type DeleteAccountInitiateResponse, type DeleteAccountRequest, type Email, type EmailAttachment, type FaviconExtractModel, type LoginRequest, type LoginResponse, type MailboxBulkRequest, type MailboxBulkResponse, type MailboxEmail, type PasswordChangeInitiateResponse, type RefreshToken, type StatusResponse, type ValidateLoginRequest, type ValidateLoginRequest2Fa, type ValidateLoginResponse, type Vault, type VaultPasswordChangeRequest, type VaultPostResponse, type VaultResponse }; diff --git a/shared/models/build.sh b/shared/models/build.sh index ba278abc2..51449797c 100755 --- a/shared/models/build.sh +++ b/shared/models/build.sh @@ -17,7 +17,7 @@ echo "📦 Building $package_name..." npm install && npm run lint && npm run build dist_path="dist" -files_to_copy=("webapi" "vault") +files_to_copy=("webapi" "vault" "metadata") for target in "${TARGETS[@]}"; do echo "📂 Copying $package_name → $target" diff --git a/apps/mobile-app/utils/types/messaging/EncryptionKeyDerivationParams.ts b/shared/models/src/metadata/EncryptionKeyDerivationParams.ts similarity index 100% rename from apps/mobile-app/utils/types/messaging/EncryptionKeyDerivationParams.ts rename to shared/models/src/metadata/EncryptionKeyDerivationParams.ts diff --git a/apps/mobile-app/utils/types/messaging/VaultMetadata.ts b/shared/models/src/metadata/VaultMetadata.ts similarity index 100% rename from apps/mobile-app/utils/types/messaging/VaultMetadata.ts rename to shared/models/src/metadata/VaultMetadata.ts diff --git a/shared/models/src/metadata/index.ts b/shared/models/src/metadata/index.ts new file mode 100644 index 000000000..3e296e642 --- /dev/null +++ b/shared/models/src/metadata/index.ts @@ -0,0 +1,7 @@ +/** + * Export all metadata models that are associated with a vault and returned by the web API. + * These models are stored locally in the client to make local (offline) key derivation and + * optimal vault sync possible. + */ +export * from './VaultMetadata'; +export * from './EncryptionKeyDerivationParams'; diff --git a/shared/models/src/vault/index.ts b/shared/models/src/vault/index.ts index 2320f6873..a9cf8a934 100644 --- a/shared/models/src/vault/index.ts +++ b/shared/models/src/vault/index.ts @@ -1,3 +1,6 @@ +/** + * Export all vault entity models that match the vault SQLite datamodel. + */ export * from './EncryptionKey'; export * from './PasswordSettings'; export * from './TotpCode'; diff --git a/shared/models/src/webapi/Email.ts b/shared/models/src/webapi/Email.ts index f88518999..3eeec03df 100644 --- a/shared/models/src/webapi/Email.ts +++ b/shared/models/src/webapi/Email.ts @@ -1,4 +1,4 @@ -import { Attachment } from "./Attachment"; +import { EmailAttachment } from "./EmailAttachment"; export type Email = { /** The body of the email message */ @@ -47,5 +47,5 @@ export type Email = { encryptionKey: string; /** The attachments of the email */ - attachments: Attachment[]; + attachments: EmailAttachment[]; } diff --git a/shared/models/src/webapi/Attachment.ts b/shared/models/src/webapi/EmailAttachment.ts similarity index 91% rename from shared/models/src/webapi/Attachment.ts rename to shared/models/src/webapi/EmailAttachment.ts index cd74065fe..13a593546 100644 --- a/shared/models/src/webapi/Attachment.ts +++ b/shared/models/src/webapi/EmailAttachment.ts @@ -1,7 +1,7 @@ /** * Email attachment type. */ -export type Attachment = { +export type EmailAttachment = { /** The ID of the attachment */ id: number; diff --git a/shared/models/src/webapi/index.ts b/shared/models/src/webapi/index.ts index 83d2384cb..b604649e9 100644 --- a/shared/models/src/webapi/index.ts +++ b/shared/models/src/webapi/index.ts @@ -1,3 +1,6 @@ +/** + * Export all web API models that are returned by the web API. + */ export * from './VaultResponse'; export * from './Vault'; export * from './VaultPostResponse'; @@ -7,7 +10,7 @@ export * from './ValidateLogin'; export * from './MailboxBulk'; export * from './MailboxEmail'; export * from './Email'; -export * from './Attachment'; +export * from './EmailAttachment'; export * from './AuthLog'; export * from './RefreshToken'; export * from './FaviconExtractModel';