diff --git a/.env.example b/.env.example index 1f475fa73..96d0f4f45 100644 --- a/.env.example +++ b/.env.example @@ -37,11 +37,19 @@ FORCE_HTTPS_REDIRECT=true # your DNS. Please refer to the full documentation for more instructions on DNS: # https://docs.aliasvault.net/installation/install.html#3-email-server-setup # -# Set the private email domains below that are allowed to be used (comma separated values). +# Set the private email domains below that the server should accept incoming mail for (comma separated values). # Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org # To disable the private email domains feature, keep this empty. PRIVATE_EMAIL_DOMAINS= +# Set private email domains that should be hidden from UI components (comma separated values). +# These domains will still function as private email domains for receiving email and claims, +# but will not appear in domain selection dropdowns or settings. This is useful for deprecating +# legacy domains while maintaining backwards compatibility. +# Example: HIDDEN_PRIVATE_EMAIL_DOMAINS=old-domain.com,deprecated.org +# Note: Domains listed here should ALSO be included in PRIVATE_EMAIL_DOMAINS above. +HIDDEN_PRIVATE_EMAIL_DOMAINS= + # Enable TLS for SMTP. # ⚠️ Requires valid TLS certificates on your mail server (not provided by the AliasVault installer). # If set to true without proper certificates, the SMTP service will fail to start. diff --git a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts index d4fda43f0..6b3c1189e 100644 --- a/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts +++ b/apps/browser-extension/src/entrypoints/background/VaultMessageHandler.ts @@ -105,6 +105,10 @@ export async function handleStoreVault( await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList); } + if (vaultRequest.hiddenPrivateEmailDomainList) { + await storage.setItem('session:hiddenPrivateEmailDomains', vaultRequest.hiddenPrivateEmailDomainList); + } + if (vaultRequest.vaultRevisionNumber) { await storage.setItem('session:vaultRevisionNumber', vaultRequest.vaultRevisionNumber); } @@ -168,6 +172,7 @@ export async function handleSyncVault( { key: 'session:encryptedVault', value: vaultResponse.vault.blob }, { key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList }, { key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList }, + { key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList }, { key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber } ]); } @@ -186,6 +191,7 @@ export async function handleGetVault( const encryptedVault = await storage.getItem('session:encryptedVault') as string; const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[]; const privateEmailDomains = await storage.getItem('session:privateEmailDomains') as string[]; + const hiddenPrivateEmailDomains = await storage.getItem('session:hiddenPrivateEmailDomains') as string[] ?? []; const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number; if (!encryptedVault) { @@ -208,6 +214,7 @@ export async function handleGetVault( vault: decryptedVault, publicEmailDomains: publicEmailDomains ?? [], privateEmailDomains: privateEmailDomains ?? [], + hiddenPrivateEmailDomains: hiddenPrivateEmailDomains ?? [], vaultRevisionNumber: vaultRevisionNumber ?? 0 }; } catch (error) { @@ -229,6 +236,7 @@ export function handleClearVault( 'session:encryptionKeyDerivationParams', 'session:publicEmailDomains', 'session:privateEmailDomains', + 'session:hiddenPrivateEmailDomains', 'session:vaultRevisionNumber' ]); @@ -497,13 +505,11 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise {}); diff --git a/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx b/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx index 032cc409c..2096b2348 100644 --- a/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx +++ b/apps/browser-extension/src/entrypoints/popup/components/Forms/EmailDomainField.tsx @@ -45,18 +45,18 @@ const EmailDomainField: React.FC = ({ const [selectedDomain, setSelectedDomain] = useState(''); const [isPopupVisible, setIsPopupVisible] = useState(false); const [privateEmailDomains, setPrivateEmailDomains] = useState([]); + const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState([]); const popupRef = useRef(null); // Get private email domains from vault metadata useEffect(() => { /** - * Load private email domains from vault metadata. + * Load private email domains from vault metadata, excluding hidden ones. */ const loadDomains = async (): Promise => { const metadata = await dbContext.getVaultMetadata(); - if (metadata?.privateEmailDomains) { - setPrivateEmailDomains(metadata.privateEmailDomains); - } + setPrivateEmailDomains(metadata?.privateEmailDomains ?? []); + setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []); }; loadDomains(); }, [dbContext]); @@ -84,9 +84,10 @@ const EmailDomainField: React.FC = ({ setLocalPart(local); setSelectedDomain(domain); - // Check if it's a custom domain + // Check if it's a custom domain (including hidden private domains as known domains) const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) || - privateEmailDomains.includes(domain); + privateEmailDomains.includes(domain) || + hiddenPrivateEmailDomains.includes(domain); setIsCustomDomain(!isKnownDomain); } else { setLocalPart(value); @@ -102,7 +103,7 @@ const EmailDomainField: React.FC = ({ } } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, privateEmailDomains, showPrivateDomains]); + }, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]); // Handle local part changes const handleLocalPartChange = useCallback((e: React.ChangeEvent) => { @@ -246,20 +247,22 @@ const EmailDomainField: React.FC = ({ {t('credentials.privateEmailDescription')}

- {privateEmailDomains.map((domain) => ( - - ))} + {privateEmailDomains + .filter((domain) => !hiddenPrivateEmailDomains.includes(domain)) + .map((domain) => ( + + ))}
)} diff --git a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx index 2e1c34a46..a22cc2b7e 100644 --- a/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx +++ b/apps/browser-extension/src/entrypoints/popup/context/DbContext.tsx @@ -62,8 +62,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } setDbInitialized(true); setDbAvailable(true); setVaultMetadata({ - publicEmailDomains: vaultResponse.vault.publicEmailDomainList, - privateEmailDomains: vaultResponse.vault.privateEmailDomainList, + publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [], + privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [], + hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [], vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, }); @@ -74,6 +75,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } vaultBlob: vaultResponse.vault.blob, publicEmailDomainList: vaultResponse.vault.publicEmailDomainList, privateEmailDomainList: vaultResponse.vault.privateEmailDomainList, + hiddenPrivateEmailDomainList: vaultResponse.vault.hiddenPrivateEmailDomainList, vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, }; @@ -96,6 +98,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } setVaultMetadata({ publicEmailDomains: response.publicEmailDomains ?? [], privateEmailDomains: response.privateEmailDomains ?? [], + hiddenPrivateEmailDomains: response.hiddenPrivateEmailDomains ?? [], vaultRevisionNumber: response.vaultRevisionNumber ?? 0, }); } else { @@ -123,6 +126,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } setVaultMetadata({ publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [], privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [], + hiddenPrivateEmailDomains: vaultMetadata?.hiddenPrivateEmailDomains ?? [], vaultRevisionNumber: revisionNumber, }); }, [vaultMetadata]); diff --git a/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts b/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts index e09bb64c7..f15dd4f7c 100644 --- a/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts +++ b/apps/browser-extension/src/utils/dist/shared/models/metadata/index.d.ts @@ -1,6 +1,7 @@ type VaultMetadata = { publicEmailDomains: string[]; privateEmailDomains: string[]; + hiddenPrivateEmailDomains: string[]; vaultRevisionNumber: number; }; diff --git a/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts b/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts index af8a6cbfd..46c8a7754 100644 --- a/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts +++ b/apps/browser-extension/src/utils/dist/shared/models/webapi/index.d.ts @@ -28,18 +28,18 @@ type ApiErrorResponse = { * Vault type. */ type Vault = { - blob: string; - createdAt: string; - credentialsCount: number; - currentRevisionNumber: number; - emailAddressList: string[]; - privateEmailDomainList: string[]; - publicEmailDomainList: string[]; - encryptionPublicKey: string; - updatedAt: string; username: string; + blob: string; version: string; - client: string; + currentRevisionNumber: number; + credentialsCount: number; + createdAt: string; + updatedAt: string; + encryptionPublicKey?: string; + emailAddressList?: string[]; + privateEmailDomainList?: string[]; + hiddenPrivateEmailDomainList?: string[]; + publicEmailDomainList?: string[]; }; /** diff --git a/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts b/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts index 4a1a4421f..830d121fe 100644 --- a/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts +++ b/apps/browser-extension/src/utils/types/messaging/StoreVaultRequest.ts @@ -2,5 +2,6 @@ export type StoreVaultRequest = { vaultBlob: string; publicEmailDomainList?: string[]; privateEmailDomainList?: string[]; + hiddenPrivateEmailDomainList?: string[]; vaultRevisionNumber?: number; } diff --git a/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts b/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts index e57e54265..202c0dbd5 100644 --- a/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts +++ b/apps/browser-extension/src/utils/types/messaging/VaultResponse.ts @@ -3,5 +3,6 @@ export type VaultResponse = { vault?: string, publicEmailDomains?: string[], privateEmailDomains?: string[], + hiddenPrivateEmailDomains?: string[], vaultRevisionNumber?: number }; diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt index 2f71b4ea8..baad47edf 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMetadataManager.kt @@ -47,6 +47,7 @@ class VaultMetadataManager( JSONObject().apply { put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains)) put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains)) + put("hiddenPrivateEmailDomains", JSONArray(updatedMetadata.hiddenPrivateEmailDomains)) put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber) }.toString(), ) @@ -158,6 +159,9 @@ class VaultMetadataManager( privateEmailDomains = json.optJSONArray("privateEmailDomains")?.let { array -> List(array.length()) { i -> array.getString(i) } } ?: emptyList(), + hiddenPrivateEmailDomains = json.optJSONArray("hiddenPrivateEmailDomains")?.let { array -> + List(array.length()) { i -> array.getString(i) } + } ?: emptyList(), vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0), ) } catch (e: Exception) { diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt index daa253321..2c0bdd133 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultMutate.kt @@ -33,13 +33,10 @@ class VaultMutate( json.put("credentialsCount", vault.credentialsCount) json.put("currentRevisionNumber", vault.currentRevisionNumber) json.put("emailAddressList", JSONArray(vault.emailAddressList)) - json.put("privateEmailDomainList", JSONArray(vault.privateEmailDomainList)) - json.put("publicEmailDomainList", JSONArray(vault.publicEmailDomainList)) json.put("encryptionPublicKey", vault.encryptionPublicKey) json.put("updatedAt", vault.updatedAt) json.put("username", vault.username) json.put("version", vault.version) - json.put("client", vault.client) val response = webApiService.executeRequest( method = "POST", @@ -124,8 +121,6 @@ class VaultMutate( } catch (e: Exception) { "0.0.0" } - val baseVersion = version.split("-").firstOrNull() ?: "0.0.0" - val client = "android-$baseVersion" val dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", java.util.Locale.US) dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC") @@ -137,13 +132,11 @@ class VaultMutate( credentialsCount = credentials.size, currentRevisionNumber = currentRevision, emailAddressList = privateEmailAddresses, - privateEmailDomainList = emptyList(), - publicEmailDomainList = emptyList(), + // TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this. encryptionPublicKey = "", updatedAt = now, username = username, version = dbVersion, - client = client, ) } @@ -157,13 +150,10 @@ class VaultMutate( val credentialsCount: Int, val currentRevisionNumber: Int, val emailAddressList: List, - val privateEmailDomainList: List, - val publicEmailDomainList: List, val encryptionPublicKey: String, val updatedAt: String, val username: String, val version: String, - val client: String, ) private data class VaultPostResponse( diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt index f8fac296f..5facc7726 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/VaultSync.kt @@ -173,12 +173,34 @@ class VaultSync( database.storeEncryptedDatabase(vault.vault.blob) metadata.setVaultRevisionNumber(newRevision) + // Store vault metadata (public/private email domains) + val vaultMetadata = net.aliasvault.app.vaultstore.models.VaultMetadata( + publicEmailDomains = vault.vault.publicEmailDomainList, + privateEmailDomains = vault.vault.privateEmailDomainList, + hiddenPrivateEmailDomains = vault.vault.hiddenPrivateEmailDomainList, + vaultRevisionNumber = newRevision, + ) + storeVaultMetadata(vaultMetadata) + if (database.isVaultUnlocked()) { // Re-unlock with new data // Note: This requires auth methods to be passed, handled by VaultStore } } + /** + * Store vault metadata as JSON string. + */ + private fun storeVaultMetadata(vaultMetadata: net.aliasvault.app.vaultstore.models.VaultMetadata) { + val json = JSONObject().apply { + put("publicEmailDomains", org.json.JSONArray(vaultMetadata.publicEmailDomains)) + put("privateEmailDomains", org.json.JSONArray(vaultMetadata.privateEmailDomains)) + put("hiddenPrivateEmailDomains", org.json.JSONArray(vaultMetadata.hiddenPrivateEmailDomains)) + put("vaultRevisionNumber", vaultMetadata.vaultRevisionNumber) + } + metadata.storeMetadata(json.toString()) + } + private fun parseVaultResponse(body: String): VaultResponse { return try { val json = JSONObject(body) @@ -196,6 +218,12 @@ class VaultSync( privateList.add(privateArray.getString(i)) } + val hiddenPrivateList = mutableListOf() + val hiddenPrivateArray = vaultJson.getJSONArray("hiddenPrivateEmailDomainList") + for (i in 0 until hiddenPrivateArray.length()) { + hiddenPrivateList.add(hiddenPrivateArray.getString(i)) + } + val publicList = mutableListOf() val publicArray = vaultJson.getJSONArray("publicEmailDomainList") for (i in 0 until publicArray.length()) { @@ -213,6 +241,7 @@ class VaultSync( credentialsCount = vaultJson.getInt("credentialsCount"), emailAddressList = emailList, privateEmailDomainList = privateList, + hiddenPrivateEmailDomainList = hiddenPrivateList, publicEmailDomainList = publicList, createdAt = vaultJson.getString("createdAt"), updatedAt = vaultJson.getString("updatedAt"), @@ -253,6 +282,7 @@ class VaultSync( val credentialsCount: Int, val emailAddressList: List, val privateEmailDomainList: List, + val hiddenPrivateEmailDomainList: List, val publicEmailDomainList: List, val createdAt: String, val updatedAt: String, diff --git a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt index dc5a89759..974572f4e 100644 --- a/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt +++ b/apps/mobile-app/android/app/src/main/java/net/aliasvault/app/vaultstore/models/VaultMetadata.kt @@ -14,6 +14,12 @@ data class VaultMetadata( */ val privateEmailDomains: List = emptyList(), + /** + * The hidden private email domains of the vault. + * These domains still function as private email domains but are hidden from UI components. + */ + val hiddenPrivateEmailDomains: List = emptyList(), + /** * The revision number of the vault. */ diff --git a/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt b/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt index 830162677..bd8bb4202 100644 --- a/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt +++ b/apps/mobile-app/android/app/src/test/java/net/aliasvault/app/nativevaultmanager/VaultStoreTest.kt @@ -35,7 +35,8 @@ class VaultStoreTest { val metadata = """ { "publicEmailDomains": ["spamok.com", "spamok.nl"], - "privateEmailDomains": ["aliasvault.net", "main.aliasvault.net"], + "privateEmailDomains": ["aliasvault.net", "main.aliasvault.net", "hidden.aliasvault.net"], + "hiddenPrivateEmailDomains": ["hidden.aliasvault.net"], "vaultRevisionNumber": 1 } """ diff --git a/apps/mobile-app/app/initialize.tsx b/apps/mobile-app/app/initialize.tsx index e215cdd9d..f6da98cd8 100644 --- a/apps/mobile-app/app/initialize.tsx +++ b/apps/mobile-app/app/initialize.tsx @@ -261,7 +261,6 @@ export default function Initialize() : React.ReactNode { // Now perform vault sync (network operations - these are skippable) await syncVault({ - initialSync: true, abortSignal: abortControllerRef.current.signal, /** * Handle the status update. diff --git a/apps/mobile-app/app/login.tsx b/apps/mobile-app/app/login.tsx index 3a261c735..6eba26797 100644 --- a/apps/mobile-app/app/login.tsx +++ b/apps/mobile-app/app/login.tsx @@ -10,7 +10,7 @@ import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Ani import { useApiUrl } from '@/utils/ApiUrlUtility'; import ConversionUtility from '@/utils/ConversionUtility'; import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata'; -import type { LoginResponse, VaultResponse } from '@/utils/dist/shared/models/webapi'; +import type { LoginResponse } from '@/utils/dist/shared/models/webapi'; import EncryptionUtility from '@/utils/EncryptionUtility'; import { SrpUtility } from '@/utils/SrpUtility'; import { ApiAuthError } from '@/utils/types/errors/ApiAuthError'; @@ -77,14 +77,12 @@ export default function LoginScreen() : React.ReactNode { * Process the vault response by storing the vault and logging in the user. * @param token - The token to use for the vault * @param refreshToken - The refresh token to use for the vault - * @param vaultResponseJson - The vault response * @param passwordHashBase64 - The password hash base64 * @param initiateLoginResponse - The initiate login response */ const processVaultResponse = async ( token: string, refreshToken: string, - vaultResponseJson: VaultResponse, passwordHashBase64: string, initiateLoginResponse: LoginResponse ) : Promise => { @@ -109,7 +107,6 @@ export default function LoginScreen() : React.ReactNode { await continueProcessVaultResponse( token, refreshToken, - vaultResponseJson, passwordHashBase64, initiateLoginResponse ); @@ -126,7 +123,6 @@ export default function LoginScreen() : React.ReactNode { await continueProcessVaultResponse( token, refreshToken, - vaultResponseJson, passwordHashBase64, initiateLoginResponse ); @@ -140,7 +136,6 @@ export default function LoginScreen() : React.ReactNode { await continueProcessVaultResponse( token, refreshToken, - vaultResponseJson, passwordHashBase64, initiateLoginResponse ); @@ -151,7 +146,6 @@ export default function LoginScreen() : React.ReactNode { * Continue processing the vault response after biometric choice * @param token - The token to use for the vault * @param refreshToken - The refresh token to use for the vault - * @param vaultResponseJson - The vault response * @param passwordHashBase64 - The password hash base64 * @param initiateLoginResponse - The initiate login response * @param encryptionKeyDerivationParams - The encryption key derivation parameters @@ -159,7 +153,6 @@ export default function LoginScreen() : React.ReactNode { const continueProcessVaultResponse = async ( token: string, refreshToken: string, - vaultResponseJson: VaultResponse, passwordHashBase64: string, initiateLoginResponse: LoginResponse ) : Promise => { @@ -169,20 +162,29 @@ export default function LoginScreen() : React.ReactNode { salt: initiateLoginResponse.salt, }; - // Set auth tokens, store encryption key and key derivation params, and initialize database + /* + * Set auth tokens, store encryption key and key derivation params. + * Note: We don't call initializeDatabase here anymore - instead, syncVault will download + * the vault and store it (including metadata) through native code. + */ await authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), token, refreshToken); await dbContext.storeEncryptionKey(passwordHashBase64); await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams); - await dbContext.initializeDatabase(vaultResponseJson); let checkSuccess = true; /** * After setting auth tokens, execute a server status check immediately * which takes care of certain sanity checks such as ensuring client/server - * compatibility. + * compatibility. This also downloads the vault and stores it (including metadata) + * through native code. */ await syncVault({ - initialSync: true, + /** + * Update login status during sync. + */ + onStatus: (status) => { + setLoginStatus(status); + }, /** * Handle the status update. */ @@ -218,6 +220,12 @@ export default function LoginScreen() : React.ReactNode { return; } + /* + * After syncVault completes, the vault has been downloaded and stored by native code. + * Immediately mark the database as available without file system checks for faster bootstrap. + */ + dbContext.setDatabaseAvailable(); + await authContext.login(); authContext.setOfflineMode(false); @@ -286,23 +294,10 @@ export default function LoginScreen() : React.ReactNode { setLoginStatus(t('auth.syncingVault')); await new Promise(resolve => requestAnimationFrame(resolve)); - const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: { - 'Authorization': `Bearer ${validationResponse.token.token}` - } }); - - const vaultError = webApi.validateVaultResponse(vaultResponseJson); - if (vaultError) { - console.error('vaultError', vaultError); - setError(vaultError); - setIsLoading(false); - setLoginStatus(null); - return; - } await processVaultResponse( validationResponse.token.token, validationResponse.token.refreshToken, - vaultResponseJson, passwordHashBase64, initiateLoginResponse ); @@ -357,21 +352,10 @@ export default function LoginScreen() : React.ReactNode { setLoginStatus(t('auth.syncingVault')); await new Promise(resolve => requestAnimationFrame(resolve)); - const vaultResponseJson = await webApi.authFetch('Vault', { method: 'GET', headers: { - 'Authorization': `Bearer ${validationResponse.token.token}` - } }); - - const vaultError = webApi.validateVaultResponse(vaultResponseJson); - if (vaultError) { - setError(vaultError); - setIsLoading(false); - return; - } await processVaultResponse( validationResponse.token.token, validationResponse.token.refreshToken, - vaultResponseJson, passwordHashBase64, initiateLoginResponse ); diff --git a/apps/mobile-app/app/reinitialize.tsx b/apps/mobile-app/app/reinitialize.tsx index 0b2b46b30..c35771c43 100644 --- a/apps/mobile-app/app/reinitialize.tsx +++ b/apps/mobile-app/app/reinitialize.tsx @@ -245,7 +245,6 @@ export default function ReinitializeScreen() : React.ReactNode { // Now perform vault sync (network operations - these are skippable) await syncVault({ - initialSync: true, /** * Handle the status update. */ diff --git a/apps/mobile-app/components/credentials/details/EmailPreview.tsx b/apps/mobile-app/components/credentials/details/EmailPreview.tsx index e24d62a0e..0441c52fb 100644 --- a/apps/mobile-app/components/credentials/details/EmailPreview.tsx +++ b/apps/mobile-app/components/credentials/details/EmailPreview.tsx @@ -75,7 +75,7 @@ export const EmailPreview: React.FC = ({ email }) : React.Rea }, [dbContext]); /** - * Check if the email is a private domain. + * Check if the email is a private domain (including hidden domains). */ const isPrivateDomain = useCallback(async (emailAddress: string): Promise => { // Get private domains from stored metadata @@ -84,7 +84,9 @@ export const EmailPreview: React.FC = ({ email }) : React.Rea return false; } - return metadata.privateEmailDomains.includes(emailAddress.split('@')[1]); + const domain = emailAddress.split('@')[1]; + return metadata.privateEmailDomains.includes(domain) || + (metadata.hiddenPrivateEmailDomains || []).includes(domain); }, [dbContext]); // Handle app state changes diff --git a/apps/mobile-app/components/form/EmailDomainField.tsx b/apps/mobile-app/components/form/EmailDomainField.tsx index e3df6cdca..8d083d3c0 100644 --- a/apps/mobile-app/components/form/EmailDomainField.tsx +++ b/apps/mobile-app/components/form/EmailDomainField.tsx @@ -49,18 +49,21 @@ export const EmailDomainField: React.FC = ({ const [selectedDomain, setSelectedDomain] = useState(''); const [isModalVisible, setIsModalVisible] = useState(false); const [privateEmailDomains, setPrivateEmailDomains] = useState([]); + const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState([]); // Get private email domains from vault metadata useEffect(() => { /** - * Load private email domains from vault metadata. + * Load private email domains from vault metadata, excluding hidden ones from UI. */ const loadDomains = async (): Promise => { try { const metadata = await dbContext.getVaultMetadata(); - if (metadata?.privateEmailDomains) { - setPrivateEmailDomains(metadata.privateEmailDomains); - } + setPrivateEmailDomains(metadata?.privateEmailDomains ?? []); + setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []); + + console.log('privateEmailDomains', metadata?.privateEmailDomains); + console.log('hiddenPrivateEmailDomains', metadata?.hiddenPrivateEmailDomains); } catch (err) { console.error('Error loading email domains:', err); } @@ -91,9 +94,10 @@ export const EmailDomainField: React.FC = ({ setLocalPart(local); setSelectedDomain(domain); - // Check if it's a custom domain + // Check if it's a custom domain (including hidden private domains as known domains) const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) || - privateEmailDomains.includes(domain); + privateEmailDomains.includes(domain) || + hiddenPrivateEmailDomains.includes(domain); setIsCustomDomain(!isKnownDomain); } else { setLocalPart(value); @@ -108,7 +112,7 @@ export const EmailDomainField: React.FC = ({ } } } - }, [value, privateEmailDomains, showPrivateDomains]); + }, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]); // Handle local part changes const handleLocalPartChange = useCallback((newText: string) => { @@ -410,7 +414,7 @@ export const EmailDomainField: React.FC = ({ {t('credentials.privateEmailDescription')} - {privateEmailDomains.map((domain) => ( + {privateEmailDomains.filter(domain => !hiddenPrivateEmailDomains.includes(domain)).map((domain) => ( Promise; testDatabaseConnection: (derivedKey: string) => Promise; unlockVault: () => Promise; + checkStoredVault: () => Promise; + setDatabaseAvailable: () => void; } const DbContext = createContext(undefined); @@ -80,13 +82,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } /** * Initialize the database in the native module. + * This is called during initial login/registration to set up the vault. + * Note: During sync operations, metadata is stored automatically by native VaultSync. * * @param vaultResponse The vault response from the API */ const initializeDatabase = useCallback(async (vaultResponse: VaultResponse) => { const metadata: VaultMetadata = { - publicEmailDomains: vaultResponse.vault.publicEmailDomainList, - privateEmailDomains: vaultResponse.vault.privateEmailDomainList, + publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [], + privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [], + hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [], vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, }; @@ -94,7 +99,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } await sqliteClient.storeEncryptedDatabase(vaultResponse.vault.blob); await sqliteClient.storeMetadata(JSON.stringify(metadata)); - // Initialize the database in the native module + // Unlock the vault to make it available for queries await unlockVault(); setDbInitialized(true); @@ -156,6 +161,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } NativeVaultManager.clearVault(); }, []); + /** + * Manually set the database as available. Used after vault sync to immediately + * mark the database as ready without file system checks. + */ + const setDatabaseAvailable = useCallback(() : void => { + setDbInitialized(true); + setDbAvailable(true); + }, []); + /** * Get the current vault metadata directly from SQLite client */ @@ -199,7 +213,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children } unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams, - }), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams]); + checkStoredVault, + setDatabaseAvailable, + }), [sqliteClient, dbInitialized, dbAvailable, initializeDatabase, hasPendingMigrations, clearDatabase, getVaultMetadata, testDatabaseConnection, unlockVault, storeEncryptionKey, storeEncryptionKeyDerivationParams, checkStoredVault, setDatabaseAvailable]); return ( diff --git a/apps/mobile-app/context/NavigationContext.tsx b/apps/mobile-app/context/NavigationContext.tsx index d274d6b18..7b290690a 100644 --- a/apps/mobile-app/context/NavigationContext.tsx +++ b/apps/mobile-app/context/NavigationContext.tsx @@ -186,12 +186,10 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch } // Database connection failed, navigate to reinitialize flow - console.log('database connection failed, navigating to reinitialize'); router.replace('/reinitialize'); } } catch { // Database query failed, navigate to reinitialize flow - console.log('database query failed, navigating to reinitialize'); router.replace('/reinitialize'); } } diff --git a/apps/mobile-app/hooks/useVaultMutate.ts b/apps/mobile-app/hooks/useVaultMutate.ts index 91272537f..8d72d9354 100644 --- a/apps/mobile-app/hooks/useVaultMutate.ts +++ b/apps/mobile-app/hooks/useVaultMutate.ts @@ -76,6 +76,7 @@ export function useVaultMutate() : { currentRevisionNumber: currentRevision, emailAddressList: privateEmailAddresses, privateEmailDomainList: [], + hiddenPrivateEmailDomainList: [], publicEmailDomainList: [], encryptionPublicKey: '', client: '', diff --git a/apps/mobile-app/hooks/useVaultSync.ts b/apps/mobile-app/hooks/useVaultSync.ts index b30fed6e2..e15e4064d 100644 --- a/apps/mobile-app/hooks/useVaultSync.ts +++ b/apps/mobile-app/hooks/useVaultSync.ts @@ -16,16 +16,7 @@ import { VaultSyncErrorCode, getVaultSyncErrorCode } from '@/utils/types/errors/ /** * Utility function to ensure a minimum time has elapsed for an operation */ -const withMinimumDelay = async ( - operation: () => Promise, - minDelayMs: number, - enableDelay: boolean = true -): Promise => { - if (!enableDelay) { - // If delay is disabled, return the result immediately. - return operation(); - } - +const withMinimumDelay = async (operation: () => Promise, minDelayMs: number): Promise => { const startTime = Date.now(); const result = await operation(); const elapsedTime = Date.now() - startTime; @@ -38,7 +29,6 @@ const withMinimumDelay = async ( }; type VaultSyncOptions = { - initialSync?: boolean; onSuccess?: (hasNewVault: boolean) => void; onError?: (error: string) => void; onStatus?: (message: string) => void; @@ -59,10 +49,7 @@ export const useVaultSync = () : { const dbContext = useDb(); const syncVault = useCallback(async (options: VaultSyncOptions = {}) => { - const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options; - - // For the initial sync, we add an artifical delay to various steps which makes it feel more fluid. - const enableDelay = initialSync; + const { onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options; try { // Check if operation was aborted @@ -87,11 +74,6 @@ export const useVaultSync = () : { // Update status onStatus?.(t('vault.checkingVaultUpdates')); - // Add artificial delay for initial sync UX - if (enableDelay) { - await new Promise(resolve => setTimeout(resolve, 300)); - } - // Check if operation was aborted if (abortSignal?.aborted) { console.debug('VaultSync: Operation aborted after status update'); @@ -128,8 +110,7 @@ export const useVaultSync = () : { // Run downloadVault with a min delay for UX purposes await withMinimumDelay( () => NativeVaultManager.downloadVault(newRevision!), - enableDelay ? 500 : 300, - true + 300 ); } } catch (err) { @@ -183,11 +164,6 @@ export const useVaultSync = () : { return false; } - // Add artificial delay for initial sync UX - if (enableDelay) { - await new Promise(resolve => setTimeout(resolve, hasNewVault ? 1000 : 300)); - } - onSuccess?.(hasNewVault); // Register credential identities after sync diff --git a/apps/mobile-app/ios/VaultModels/VaultMetadata.swift b/apps/mobile-app/ios/VaultModels/VaultMetadata.swift index 51b8930f4..81f5e0521 100644 --- a/apps/mobile-app/ios/VaultModels/VaultMetadata.swift +++ b/apps/mobile-app/ios/VaultModels/VaultMetadata.swift @@ -3,11 +3,13 @@ import Foundation public struct VaultMetadata: Codable { public var publicEmailDomains: [String]? public var privateEmailDomains: [String]? + public var hiddenPrivateEmailDomains: [String]? public var vaultRevisionNumber: Int - public init(publicEmailDomains: [String]? = nil, privateEmailDomains: [String]? = nil, vaultRevisionNumber: Int) { + public init(publicEmailDomains: [String]? = nil, privateEmailDomains: [String]? = nil, hiddenPrivateEmailDomains: [String]? = nil, vaultRevisionNumber: Int) { self.publicEmailDomains = publicEmailDomains self.privateEmailDomains = privateEmailDomains + self.hiddenPrivateEmailDomains = hiddenPrivateEmailDomains self.vaultRevisionNumber = vaultRevisionNumber } } diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift index 552555158..3d02d00f7 100644 --- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift +++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Metadata.swift @@ -42,6 +42,7 @@ extension VaultStore { metadata = VaultMetadata( publicEmailDomains: [], privateEmailDomains: [], + hiddenPrivateEmailDomains: [], vaultRevisionNumber: revisionNumber ) } diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift index b2c3fe6be..8df2b6790 100644 --- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift +++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Mutate.swift @@ -8,13 +8,10 @@ public struct VaultUpload: Codable { public let credentialsCount: Int public let currentRevisionNumber: Int public let emailAddressList: [String] - public let privateEmailDomainList: [String] - public let publicEmailDomainList: [String] public let encryptionPublicKey: String public let updatedAt: String public let username: String public let version: String - public let client: String } /// Vault POST response from API @@ -86,11 +83,6 @@ extension VaultStore { // Get database version let dbVersion = try getDatabaseVersion() - // Get client version - let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0" - let baseVersion = version.split(separator: "-").first.map(String.init) ?? "0.0.0" - let client = "ios-\(baseVersion)" - let dateFormatter = ISO8601DateFormatter() dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] let now = dateFormatter.string(from: Date()) @@ -101,13 +93,11 @@ extension VaultStore { credentialsCount: credentials.count, currentRevisionNumber: currentRevision, emailAddressList: privateEmailAddresses, - privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates - publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates - encryptionPublicKey: "", // Empty on purpose, only required if new public/private key pair is generated + // TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this. + encryptionPublicKey: "", updatedAt: now, username: username, version: dbVersion, - client: client ) } diff --git a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift index aefac0ab0..d18187cd6 100644 --- a/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift +++ b/apps/mobile-app/ios/VaultStoreKit/VaultStore+Sync.swift @@ -19,6 +19,7 @@ public struct VaultData: Codable { public let credentialsCount: Int public let emailAddressList: [String] public let privateEmailDomainList: [String] + public let hiddenPrivateEmailDomainList: [String] public let publicEmailDomainList: [String] public let createdAt: String public let updatedAt: String @@ -177,11 +178,31 @@ extension VaultStore { try storeEncryptedDatabase(vault.vault.blob) setCurrentVaultRevisionNumber(newRevision) + // Store vault metadata (public/private email domains) + let metadata = VaultMetadata( + publicEmailDomains: vault.vault.publicEmailDomainList, + privateEmailDomains: vault.vault.privateEmailDomainList, + hiddenPrivateEmailDomains: vault.vault.hiddenPrivateEmailDomainList, + vaultRevisionNumber: newRevision + ) + try storeVaultMetadata(metadata) + if isVaultUnlocked { try unlockVault() } } + /// Store vault metadata + private func storeVaultMetadata(_ metadata: VaultMetadata) throws { + let encoder = JSONEncoder() + guard let metadataData = try? encoder.encode(metadata), + let metadataJson = String(data: metadataData, encoding: .utf8) else { + throw VaultSyncError.parseError(message: "Failed to encode vault metadata") + } + + try storeMetadata(metadataJson) + } + /// Parse vault response from JSON private func parseVaultResponse(_ body: String) throws -> VaultResponse { guard let vaultData = body.data(using: .utf8) else { diff --git a/apps/mobile-app/utils/SqliteClient.tsx b/apps/mobile-app/utils/SqliteClient.tsx index 74c0c093c..3fae7ead2 100644 --- a/apps/mobile-app/utils/SqliteClient.tsx +++ b/apps/mobile-app/utils/SqliteClient.tsx @@ -30,7 +30,9 @@ class SqliteClient { * Store the vault metadata via the native code implementation. * * Metadata is stored in plain text in UserDefaults. The metadata consists of the following: - * - public and private email domains + * - public email domains + * - private email domains + * - hidden private email domains * - vault revision number */ public async storeMetadata(metadata: string): Promise { @@ -77,7 +79,7 @@ class SqliteClient { return null; } - const { privateEmailDomains, publicEmailDomains } = metadata; + const { privateEmailDomains, publicEmailDomains, hiddenPrivateEmailDomains } = metadata; /** * Check if a domain is valid (not empty, not 'DISABLED.TLD', and exists in either private or public domains) @@ -98,7 +100,8 @@ class SqliteClient { } // If default domain is not valid, fall back to first available private domain - const firstPrivate = privateEmailDomains?.find(isValidDomain); + // Filter out hidden private domains from the list of private domains + const firstPrivate = privateEmailDomains?.filter(domain => !hiddenPrivateEmailDomains?.includes(domain)).find(isValidDomain); if (firstPrivate) { return firstPrivate; } diff --git a/apps/mobile-app/utils/WebApiService.ts b/apps/mobile-app/utils/WebApiService.ts index 51af2837e..739712cd2 100644 --- a/apps/mobile-app/utils/WebApiService.ts +++ b/apps/mobile-app/utils/WebApiService.ts @@ -314,27 +314,6 @@ export class WebApiService { return this.get('Security/authlogs'); } - /** - * Validates the vault response and returns an error message if validation fails - */ - public validateVaultResponse(vaultResponseJson: VaultResponse): string | null { - /** - * Status 0 = OK, vault is ready. - * Status 1 = Merge required, which only the web client supports. - * Status 2 = Outdated, which means the local vault is outdated and the client should fetch the latest vault from the server before saving can continue. - */ - if (vaultResponseJson.status === 1) { - // Note: vault merge is no longer allowed by the API as of 0.20.0, updates with the same revision number are rejected. So this check can be removed later. - return i18n.t('vault.errors.vaultOutdated'); - } - - if (vaultResponseJson.status === 2) { - return i18n.t('vault.errors.vaultOutdated'); - } - - return null; - } - /** * Get the currently configured API URL from native storage. */ diff --git a/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts b/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts index e09bb64c7..f15dd4f7c 100644 --- a/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts +++ b/apps/mobile-app/utils/dist/shared/models/metadata/index.d.ts @@ -1,6 +1,7 @@ type VaultMetadata = { publicEmailDomains: string[]; privateEmailDomains: string[]; + hiddenPrivateEmailDomains: string[]; vaultRevisionNumber: number; }; diff --git a/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts b/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts index af8a6cbfd..46c8a7754 100644 --- a/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts +++ b/apps/mobile-app/utils/dist/shared/models/webapi/index.d.ts @@ -28,18 +28,18 @@ type ApiErrorResponse = { * Vault type. */ type Vault = { - blob: string; - createdAt: string; - credentialsCount: number; - currentRevisionNumber: number; - emailAddressList: string[]; - privateEmailDomainList: string[]; - publicEmailDomainList: string[]; - encryptionPublicKey: string; - updatedAt: string; username: string; + blob: string; version: string; - client: string; + currentRevisionNumber: number; + credentialsCount: number; + createdAt: string; + updatedAt: string; + encryptionPublicKey?: string; + emailAddressList?: string[]; + privateEmailDomainList?: string[]; + hiddenPrivateEmailDomainList?: string[]; + publicEmailDomainList?: string[]; }; /** diff --git a/apps/server/AliasVault.Api/Config.cs b/apps/server/AliasVault.Api/Config.cs index df1759f22..cbad69020 100644 --- a/apps/server/AliasVault.Api/Config.cs +++ b/apps/server/AliasVault.Api/Config.cs @@ -23,4 +23,10 @@ public class Config : SharedConfig /// Gets or sets the list of private email domains that are available. /// public List PrivateEmailDomains { get; set; } = []; + + /// + /// Gets or sets the list of private email domains that should be hidden from UI components. + /// These domains still function as private email domains but are not shown in domain selection dropdowns. + /// + public List HiddenPrivateEmailDomains { get; set; } = []; } diff --git a/apps/server/AliasVault.Api/Controllers/VaultController.cs b/apps/server/AliasVault.Api/Controllers/VaultController.cs index 57efb56d1..f3a95567e 100644 --- a/apps/server/AliasVault.Api/Controllers/VaultController.cs +++ b/apps/server/AliasVault.Api/Controllers/VaultController.cs @@ -89,11 +89,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont Blob = string.Empty, Version = string.Empty, CurrentRevisionNumber = 0, - EncryptionPublicKey = string.Empty, CredentialsCount = 0, - EmailAddressList = [], - PrivateEmailDomainList = [], - PublicEmailDomainList = [], CreatedAt = DateTime.MinValue, UpdatedAt = DateTime.MinValue, }, @@ -121,6 +117,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont // Get dynamic list of private email domains from config. var privateEmailDomainList = config.PrivateEmailDomains; + var hiddenPrivateEmailDomainList = config.HiddenPrivateEmailDomains; // Hardcoded list of public (SpamOK) email domains that are available to the client. var publicEmailDomainList = new List(["spamok.com", "solarflarecorp.com", "spamok.nl", "3060.nl", @@ -137,8 +134,8 @@ public class VaultController(ILogger logger, IAliasServerDbCont CurrentRevisionNumber = vault.RevisionNumber, EncryptionPublicKey = string.Empty, CredentialsCount = 0, - EmailAddressList = [], PrivateEmailDomainList = privateEmailDomainList, + HiddenPrivateEmailDomainList = hiddenPrivateEmailDomainList, PublicEmailDomainList = publicEmailDomainList, CreatedAt = vault.CreatedAt, UpdatedAt = vault.UpdatedAt, @@ -176,11 +173,7 @@ public class VaultController(ILogger logger, IAliasServerDbCont Blob = x.VaultBlob, Version = x.Version, CurrentRevisionNumber = x.RevisionNumber, - EncryptionPublicKey = string.Empty, CredentialsCount = 0, - EmailAddressList = [], - PrivateEmailDomainList = [], - PublicEmailDomainList = [], CreatedAt = x.CreatedAt, UpdatedAt = x.UpdatedAt, }).ToList(), diff --git a/apps/server/AliasVault.Api/Program.cs b/apps/server/AliasVault.Api/Program.cs index 54e3ab151..51cb57af5 100644 --- a/apps/server/AliasVault.Api/Program.cs +++ b/apps/server/AliasVault.Api/Program.cs @@ -48,6 +48,12 @@ var privateEmailDomains = Environment.GetEnvironmentVariable("PRIVATE_EMAIL_DOMA .Where(d => !string.IsNullOrWhiteSpace(d)); config.PrivateEmailDomains = privateEmailDomains?.ToList() ?? new List(); +var hiddenPrivateEmailDomains = Environment.GetEnvironmentVariable("HIDDEN_PRIVATE_EMAIL_DOMAINS")? + .Split(",", StringSplitOptions.RemoveEmptyEntries) + .Select(d => d.Trim()) + .Where(d => !string.IsNullOrWhiteSpace(d)); +config.HiddenPrivateEmailDomains = hiddenPrivateEmailDomains?.ToList() ?? new List(); + var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false"; config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled); diff --git a/apps/server/AliasVault.Api/Properties/launchSettings.json b/apps/server/AliasVault.Api/Properties/launchSettings.json index 689bbec26..0fe4f8535 100644 --- a/apps/server/AliasVault.Api/Properties/launchSettings.json +++ b/apps/server/AliasVault.Api/Properties/launchSettings.json @@ -9,7 +9,8 @@ "JWT_KEY": "12345678901234567890123456789012", "DATA_PROTECTION_CERT_PASS": "Development", "PUBLIC_REGISTRATION_ENABLED": "true", - "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld", + "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld", + "HIDDEN_PRIVATE_EMAIL_DOMAINS": "disabled.tld", "IP_LOGGING_ENABLED": "true" }, "dotnetRunMessages": true, @@ -24,7 +25,8 @@ "JWT_KEY": "12345678901234567890123456789012", "DATA_PROTECTION_CERT_PASS": "Development", "PUBLIC_REGISTRATION_ENABLED": "true", - "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld", + "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld", + "HIDDEN_PRIVATE_EMAIL_DOMAINS": "disabled.tld", "IP_LOGGING_ENABLED": "true" }, "dotnetRunMessages": true, diff --git a/apps/server/AliasVault.Client/Config.cs b/apps/server/AliasVault.Client/Config.cs index 148e38f53..211f2633e 100644 --- a/apps/server/AliasVault.Client/Config.cs +++ b/apps/server/AliasVault.Client/Config.cs @@ -23,6 +23,12 @@ public class Config /// public List PrivateEmailDomains { get; set; } = []; + /// + /// Gets or sets the list of private email domains that should be hidden from UI components. + /// These domains still function as private email domains but are not shown in domain selection dropdowns. + /// + public List HiddenPrivateEmailDomains { get; set; } = []; + /// /// Gets or sets the list of public email domains that are allowed to be used by the client vault users. /// diff --git a/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor b/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor index 92513e40a..25d413b9c 100644 --- a/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor +++ b/apps/server/AliasVault.Client/Main/Components/Forms/EditEmailFormRow.razor @@ -99,7 +99,9 @@ private string SelectedDomain = string.Empty; private bool IsPopupVisible = false; - private List PrivateDomains => Config.PrivateEmailDomains; + private List PrivateDomains => Config.PrivateEmailDomains + .Where(d => !Config.HiddenPrivateEmailDomains.Contains(d)) + .ToList(); private List PublicDomains => Config.PublicEmailDomains; private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD"); diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor index 8a128c555..b2ea49a7d 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/General.razor @@ -125,7 +125,9 @@ @code { private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.General", "AliasVault.Client"); - private List PrivateDomains => Config.PrivateEmailDomains; + private List PrivateDomains => Config.PrivateEmailDomains + .Where(d => !Config.HiddenPrivateEmailDomains.Contains(d)) + .ToList(); private List PublicDomains => Config.PublicEmailDomains; private string DefaultEmailDomain { get; set; } = string.Empty; diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor index 9e0a642cf..fde6e4ac6 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor @@ -196,6 +196,7 @@ else CredentialsCount = vault.CredentialsCount, EmailAddressList = vault.EmailAddressList, PrivateEmailDomainList = [], + HiddenPrivateEmailDomainList = [], PublicEmailDomainList = [], CreatedAt = vault.CreatedAt, UpdatedAt = vault.UpdatedAt, diff --git a/apps/server/AliasVault.Client/Services/Database/DbService.cs b/apps/server/AliasVault.Client/Services/Database/DbService.cs index 8c142ac14..17fa89256 100644 --- a/apps/server/AliasVault.Client/Services/Database/DbService.cs +++ b/apps/server/AliasVault.Client/Services/Database/DbService.cs @@ -490,6 +490,7 @@ public sealed class DbService : IDisposable CredentialsCount = credentialsCount, EmailAddressList = emailAddresses, PrivateEmailDomainList = [], + HiddenPrivateEmailDomainList = [], PublicEmailDomainList = [], CreatedAt = currentDateTime, UpdatedAt = currentDateTime, diff --git a/apps/server/AliasVault.Client/entrypoint.sh b/apps/server/AliasVault.Client/entrypoint.sh index 8f94be757..cd740c802 100755 --- a/apps/server/AliasVault.Client/entrypoint.sh +++ b/apps/server/AliasVault.Client/entrypoint.sh @@ -1,11 +1,13 @@ #!/bin/sh # Set the default values DEFAULT_PRIVATE_EMAIL_DOMAINS="" +DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS="" DEFAULT_SUPPORT_EMAIL="" DEFAULT_PUBLIC_REGISTRATION_ENABLED="true" # Use the provided environment variables if they exist, otherwise use defaults PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS} +HIDDEN_PRIVATE_EMAIL_DOMAINS=${HIDDEN_PRIVATE_EMAIL_DOMAINS:-$DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS} SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL} PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-$DEFAULT_PUBLIC_REGISTRATION_ENABLED} @@ -37,9 +39,25 @@ else json_array=$(echo $PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i public class Vault { + // ------------------------------------------------------------ + // Required properties, always part of the vault get/set model. + // ------------------------------------------------------------ + /// /// Gets or sets the username that owns the vault. /// @@ -34,32 +38,12 @@ public class Vault /// public required long CurrentRevisionNumber { get; set; } - /// - /// Gets or sets the public encryption key that server requires to encrypt user data such as received emails. - /// - public required string EncryptionPublicKey { get; set; } - /// /// Gets or sets the number of credentials stored in the vault. This anonymous data is used in case a vault back-up /// needs to be restored to get a better idea of the vault size. /// public required int CredentialsCount { get; set; } - /// - /// Gets or sets the list of email addresses that are used in the vault and should be registered on the server. - /// - public required List EmailAddressList { get; set; } - - /// - /// Gets or sets the list of private email domains that are available. - /// - public required List PrivateEmailDomainList { get; set; } - - /// - /// Gets or sets the list of public email domains that are available. - /// - public required List PublicEmailDomainList { get; set; } - /// /// Gets or sets the date and time of creation. /// @@ -69,4 +53,34 @@ public class Vault /// Gets or sets the date and time of last update. /// public required DateTime UpdatedAt { get; set; } + + // ------------------------------------------------------------ + // Optional properties, only part of the vault get/set model if available and applicable. + // ------------------------------------------------------------ + + /// + /// Gets or sets the public encryption key that server requires to encrypt user data such as received emails. + /// + public string EncryptionPublicKey { get; set; } = string.Empty; + + /// + /// Gets or sets the list of email addresses that are used in the vault and should be registered on the server. + /// + public List EmailAddressList { get; set; } = []; + + /// + /// Gets or sets the list of private email domains that are available. + /// + public List PrivateEmailDomainList { get; set; } = []; + + /// + /// Gets or sets the list of private email domains that should be hidden from UI components. + /// These domains still function as private email domains but are not shown in domain selection dropdowns. + /// + public List HiddenPrivateEmailDomainList { get; set; } = []; + + /// + /// Gets or sets the list of public email domains that are available. + /// + public List PublicEmailDomainList { get; set; } = []; } diff --git a/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs index 673b40f8c..aa3555411 100644 --- a/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs +++ b/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs @@ -94,8 +94,10 @@ public class ClientPlaywrightTest : PlaywrightTest ApiBaseUrl = "http://localhost:" + apiPort + "/"; // Set environment variables for the API. - string[] privateEmailDomains = ["example.tld", "example2.tld"]; + string[] privateEmailDomains = ["example.tld", "example2.tld","disabled.tld"]; Environment.SetEnvironmentVariable("PRIVATE_EMAIL_DOMAINS", string.Join(",", privateEmailDomains)); + string[] hiddenPrivateEmailDomains = ["disabled.tld"]; + Environment.SetEnvironmentVariable("HIDDEN_PRIVATE_EMAIL_DOMAINS", string.Join(",", hiddenPrivateEmailDomains)); // Start WebAPI in-memory. _apiFactory.Port = apiPort; diff --git a/dockerfiles/all-in-one/Dockerfile b/dockerfiles/all-in-one/Dockerfile index 5785b3700..eeaadd18b 100644 --- a/dockerfiles/all-in-one/Dockerfile +++ b/dockerfiles/all-in-one/Dockerfile @@ -163,6 +163,7 @@ ENV ALIASVAULT_VERBOSITY=0 \ IP_LOGGING_ENABLED=true \ SUPPORT_EMAIL="" \ PRIVATE_EMAIL_DOMAINS="" \ + HIDDEN_PRIVATE_EMAIL_DOMAINS="" \ HOSTNAME=localhost \ POSTGRES_HOST=localhost \ POSTGRES_PORT=5432 \ diff --git a/dockerfiles/all-in-one/s6-scripts/api/run b/dockerfiles/all-in-one/s6-scripts/api/run index 70bdabf0e..7f9334297 100644 --- a/dockerfiles/all-in-one/s6-scripts/api/run +++ b/dockerfiles/all-in-one/s6-scripts/api/run @@ -30,6 +30,7 @@ done export ASPNETCORE_URLS="http://0.0.0.0:3001" export ASPNETCORE_PATHBASE="/api" export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}" +export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}" export PUBLIC_REGISTRATION_ENABLED="${PUBLIC_REGISTRATION_ENABLED:-true}" export IP_LOGGING_ENABLED="${IP_LOGGING_ENABLED:-true}" diff --git a/dockerfiles/all-in-one/s6-scripts/client/run b/dockerfiles/all-in-one/s6-scripts/client/run index 6fc139060..19f7b8dfa 100644 --- a/dockerfiles/all-in-one/s6-scripts/client/run +++ b/dockerfiles/all-in-one/s6-scripts/client/run @@ -27,8 +27,10 @@ done # Client service entrypoint DEFAULT_PRIVATE_EMAIL_DOMAINS="" +DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS="" DEFAULT_SUPPORT_EMAIL="" PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS} +HIDDEN_PRIVATE_EMAIL_DOMAINS=${HIDDEN_PRIVATE_EMAIL_DOMAINS:-$DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS} SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL} PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-true} @@ -58,10 +60,19 @@ else json_array=$(echo "$PRIVATE_EMAIL_DOMAINS" | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i /app/client/wwwroot/appsettings.json << EOF { "PrivateEmailDomains": $json_array, + "HiddenPrivateEmailDomains": $hidden_json_array, "SupportEmail": "$SUPPORT_EMAIL", "PublicRegistrationEnabled": "$PUBLIC_REGISTRATION_ENABLED" } diff --git a/dockerfiles/all-in-one/s6-scripts/smtp/run b/dockerfiles/all-in-one/s6-scripts/smtp/run index 65647fba1..a0234a4a2 100644 --- a/dockerfiles/all-in-one/s6-scripts/smtp/run +++ b/dockerfiles/all-in-one/s6-scripts/smtp/run @@ -28,6 +28,7 @@ for i in {1..30}; do done export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}" +export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}" export SMTP_TLS_ENABLED="${SMTP_TLS_ENABLED:-false}" # Set .NET logging level based on verbosity diff --git a/dockerfiles/docker-compose.all-in-one.nas.yml b/dockerfiles/docker-compose.all-in-one.nas.yml index 35a340c20..3f9508c95 100644 --- a/dockerfiles/docker-compose.all-in-one.nas.yml +++ b/dockerfiles/docker-compose.all-in-one.nas.yml @@ -29,6 +29,7 @@ services: FORCE_HTTPS_REDIRECT: "false" SUPPORT_EMAIL: "" PRIVATE_EMAIL_DOMAINS: "" + HIDDEN_PRIVATE_EMAIL_DOMAINS: "" volumes: avdata: diff --git a/dockerfiles/docker-compose.all-in-one.yml b/dockerfiles/docker-compose.all-in-one.yml index caf414981..81e45ad96 100644 --- a/dockerfiles/docker-compose.all-in-one.yml +++ b/dockerfiles/docker-compose.all-in-one.yml @@ -27,3 +27,4 @@ services: FORCE_HTTPS_REDIRECT: "false" SUPPORT_EMAIL: "" PRIVATE_EMAIL_DOMAINS: "" + HIDDEN_PRIVATE_EMAIL_DOMAINS: "" diff --git a/install.sh b/install.sh index 60d6e1b1c..a60f11fc9 100755 --- a/install.sh +++ b/install.sh @@ -3183,6 +3183,12 @@ check_and_populate_env() { printf " Set PRIVATE_EMAIL_DOMAINS\n" fi + # HIDDEN_PRIVATE_EMAIL_DOMAINS + if ! grep -q "^HIDDEN_PRIVATE_EMAIL_DOMAINS=" "$ENV_FILE"; then + update_env_var "HIDDEN_PRIVATE_EMAIL_DOMAINS" "" + printf " Set HIDDEN_PRIVATE_EMAIL_DOMAINS\n" + fi + # HTTP_PORT if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then update_env_var "HTTP_PORT" "80" diff --git a/shared/models/src/metadata/VaultMetadata.ts b/shared/models/src/metadata/VaultMetadata.ts index 521306858..acb06c9a4 100644 --- a/shared/models/src/metadata/VaultMetadata.ts +++ b/shared/models/src/metadata/VaultMetadata.ts @@ -1,5 +1,6 @@ export type VaultMetadata = { publicEmailDomains: string[], privateEmailDomains: string[], + hiddenPrivateEmailDomains: string[], vaultRevisionNumber: number }; diff --git a/shared/models/src/webapi/Vault.ts b/shared/models/src/webapi/Vault.ts index e318aa196..cb92d0027 100644 --- a/shared/models/src/webapi/Vault.ts +++ b/shared/models/src/webapi/Vault.ts @@ -2,16 +2,18 @@ * Vault type. */ export type Vault = { - blob: string; - createdAt: string; - credentialsCount: number; - currentRevisionNumber: number; - emailAddressList: string[]; - privateEmailDomainList: string[]; - publicEmailDomainList: string[]; - encryptionPublicKey: string; - updatedAt: string; + // Required properties, always part of the vault get/set model. username: string; + blob: string; version: string; - client: string; + currentRevisionNumber: number; + credentialsCount: number; + createdAt: string; + updatedAt: string; + // Optional properties, only part of the vault get/set model if available and applicable. + encryptionPublicKey?: string; + emailAddressList?: string[]; + privateEmailDomainList?: string[]; + hiddenPrivateEmailDomainList?: string[]; + publicEmailDomainList?: string[]; } \ No newline at end of file