Update env variables and refactor metadata storage (#1371)

This commit is contained in:
Leendert de Borst
2025-11-21 06:45:05 +01:00
parent a937098315
commit 4d613175ed
54 changed files with 336 additions and 226 deletions
+9 -1
View File
@@ -37,11 +37,19 @@ FORCE_HTTPS_REDIRECT=true
# your DNS. Please refer to the full documentation for more instructions on DNS: # your DNS. Please refer to the full documentation for more instructions on DNS:
# https://docs.aliasvault.net/installation/install.html#3-email-server-setup # 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 # Example: PRIVATE_EMAIL_DOMAINS=example.com,example2.org
# To disable the private email domains feature, keep this empty. # To disable the private email domains feature, keep this empty.
PRIVATE_EMAIL_DOMAINS= 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. # Enable TLS for SMTP.
# ⚠️ Requires valid TLS certificates on your mail server (not provided by the AliasVault installer). # ⚠️ 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. # If set to true without proper certificates, the SMTP service will fail to start.
@@ -105,6 +105,10 @@ export async function handleStoreVault(
await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList); await storage.setItem('session:privateEmailDomains', vaultRequest.privateEmailDomainList);
} }
if (vaultRequest.hiddenPrivateEmailDomainList) {
await storage.setItem('session:hiddenPrivateEmailDomains', vaultRequest.hiddenPrivateEmailDomainList);
}
if (vaultRequest.vaultRevisionNumber) { if (vaultRequest.vaultRevisionNumber) {
await storage.setItem('session:vaultRevisionNumber', 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:encryptedVault', value: vaultResponse.vault.blob },
{ key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList }, { key: 'session:publicEmailDomains', value: vaultResponse.vault.publicEmailDomainList },
{ key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList }, { key: 'session:privateEmailDomains', value: vaultResponse.vault.privateEmailDomainList },
{ key: 'session:hiddenPrivateEmailDomains', value: vaultResponse.vault.hiddenPrivateEmailDomainList },
{ key: 'session:vaultRevisionNumber', value: vaultResponse.vault.currentRevisionNumber } { 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 encryptedVault = await storage.getItem('session:encryptedVault') as string;
const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[]; const publicEmailDomains = await storage.getItem('session:publicEmailDomains') as string[];
const privateEmailDomains = await storage.getItem('session:privateEmailDomains') 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; const vaultRevisionNumber = await storage.getItem('session:vaultRevisionNumber') as number;
if (!encryptedVault) { if (!encryptedVault) {
@@ -208,6 +214,7 @@ export async function handleGetVault(
vault: decryptedVault, vault: decryptedVault,
publicEmailDomains: publicEmailDomains ?? [], publicEmailDomains: publicEmailDomains ?? [],
privateEmailDomains: privateEmailDomains ?? [], privateEmailDomains: privateEmailDomains ?? [],
hiddenPrivateEmailDomains: hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: vaultRevisionNumber ?? 0 vaultRevisionNumber: vaultRevisionNumber ?? 0
}; };
} catch (error) { } catch (error) {
@@ -229,6 +236,7 @@ export function handleClearVault(
'session:encryptionKeyDerivationParams', 'session:encryptionKeyDerivationParams',
'session:publicEmailDomains', 'session:publicEmailDomains',
'session:privateEmailDomains', 'session:privateEmailDomains',
'session:hiddenPrivateEmailDomains',
'session:vaultRevisionNumber' 'session:vaultRevisionNumber'
]); ]);
@@ -497,13 +505,11 @@ async function uploadNewVaultToServer(sqliteClient: SqliteClient) : Promise<Vaul
credentialsCount: sqliteClient.getAllCredentials().length, credentialsCount: sqliteClient.getAllCredentials().length,
currentRevisionNumber: vaultRevisionNumber, currentRevisionNumber: vaultRevisionNumber,
emailAddressList: emailAddresses, emailAddressList: emailAddresses,
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.
client: '', // Empty on purpose, API will not use this for vault updates.
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
username: username, username: username,
version: (await sqliteClient.getDatabaseVersion()).version version: (await sqliteClient.getDatabaseVersion()).version,
// TODO: add public RSA encryption key to payload when implementing vault creation from browser extension. Currently only web app does this.
encryptionPublicKey: '',
}; };
const webApi = new WebApiService(() => {}); const webApi = new WebApiService(() => {});
@@ -45,18 +45,18 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const [selectedDomain, setSelectedDomain] = useState(''); const [selectedDomain, setSelectedDomain] = useState('');
const [isPopupVisible, setIsPopupVisible] = useState(false); const [isPopupVisible, setIsPopupVisible] = useState(false);
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]); const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
const popupRef = useRef<HTMLDivElement>(null); const popupRef = useRef<HTMLDivElement>(null);
// Get private email domains from vault metadata // Get private email domains from vault metadata
useEffect(() => { useEffect(() => {
/** /**
* Load private email domains from vault metadata. * Load private email domains from vault metadata, excluding hidden ones.
*/ */
const loadDomains = async (): Promise<void> => { const loadDomains = async (): Promise<void> => {
const metadata = await dbContext.getVaultMetadata(); const metadata = await dbContext.getVaultMetadata();
if (metadata?.privateEmailDomains) { setPrivateEmailDomains(metadata?.privateEmailDomains ?? []);
setPrivateEmailDomains(metadata.privateEmailDomains); setHiddenPrivateEmailDomains(metadata?.hiddenPrivateEmailDomains ?? []);
}
}; };
loadDomains(); loadDomains();
}, [dbContext]); }, [dbContext]);
@@ -84,9 +84,10 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setLocalPart(local); setLocalPart(local);
setSelectedDomain(domain); 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) || const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
privateEmailDomains.includes(domain); privateEmailDomains.includes(domain) ||
hiddenPrivateEmailDomains.includes(domain);
setIsCustomDomain(!isKnownDomain); setIsCustomDomain(!isKnownDomain);
} else { } else {
setLocalPart(value); setLocalPart(value);
@@ -102,7 +103,7 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [value, privateEmailDomains, showPrivateDomains]); }, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
// Handle local part changes // Handle local part changes
const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => { const handleLocalPartChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@@ -246,20 +247,22 @@ const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
{t('credentials.privateEmailDescription')} {t('credentials.privateEmailDescription')}
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{privateEmailDomains.map((domain) => ( {privateEmailDomains
<button .filter((domain) => !hiddenPrivateEmailDomains.includes(domain))
key={domain} .map((domain) => (
type="button" <button
onClick={() => selectDomain(domain)} key={domain}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${ type="button"
selectedDomain === domain onClick={() => selectDomain(domain)}
? 'bg-primary-600 text-white hover:bg-primary-700' className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600' selectedDomain === domain
}`} ? 'bg-primary-600 text-white hover:bg-primary-700'
> : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 border border-gray-300 dark:border-gray-600'
{domain} }`}
</button> >
))} {domain}
</button>
))}
</div> </div>
</div> </div>
)} )}
@@ -62,8 +62,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setDbInitialized(true); setDbInitialized(true);
setDbAvailable(true); setDbAvailable(true);
setVaultMetadata({ setVaultMetadata({
publicEmailDomains: vaultResponse.vault.publicEmailDomainList, publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [],
privateEmailDomains: vaultResponse.vault.privateEmailDomainList, privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [],
hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [],
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
}); });
@@ -74,6 +75,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
vaultBlob: vaultResponse.vault.blob, vaultBlob: vaultResponse.vault.blob,
publicEmailDomainList: vaultResponse.vault.publicEmailDomainList, publicEmailDomainList: vaultResponse.vault.publicEmailDomainList,
privateEmailDomainList: vaultResponse.vault.privateEmailDomainList, privateEmailDomainList: vaultResponse.vault.privateEmailDomainList,
hiddenPrivateEmailDomainList: vaultResponse.vault.hiddenPrivateEmailDomainList,
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber,
}; };
@@ -96,6 +98,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setVaultMetadata({ setVaultMetadata({
publicEmailDomains: response.publicEmailDomains ?? [], publicEmailDomains: response.publicEmailDomains ?? [],
privateEmailDomains: response.privateEmailDomains ?? [], privateEmailDomains: response.privateEmailDomains ?? [],
hiddenPrivateEmailDomains: response.hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: response.vaultRevisionNumber ?? 0, vaultRevisionNumber: response.vaultRevisionNumber ?? 0,
}); });
} else { } else {
@@ -123,6 +126,7 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
setVaultMetadata({ setVaultMetadata({
publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [], publicEmailDomains: vaultMetadata?.publicEmailDomains ?? [],
privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [], privateEmailDomains: vaultMetadata?.privateEmailDomains ?? [],
hiddenPrivateEmailDomains: vaultMetadata?.hiddenPrivateEmailDomains ?? [],
vaultRevisionNumber: revisionNumber, vaultRevisionNumber: revisionNumber,
}); });
}, [vaultMetadata]); }, [vaultMetadata]);
@@ -1,6 +1,7 @@
type VaultMetadata = { type VaultMetadata = {
publicEmailDomains: string[]; publicEmailDomains: string[];
privateEmailDomains: string[]; privateEmailDomains: string[];
hiddenPrivateEmailDomains: string[];
vaultRevisionNumber: number; vaultRevisionNumber: number;
}; };
@@ -28,18 +28,18 @@ type ApiErrorResponse = {
* Vault type. * Vault type.
*/ */
type Vault = { type Vault = {
blob: string;
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
username: string; username: string;
blob: string;
version: string; version: string;
client: string; currentRevisionNumber: number;
credentialsCount: number;
createdAt: string;
updatedAt: string;
encryptionPublicKey?: string;
emailAddressList?: string[];
privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
publicEmailDomainList?: string[];
}; };
/** /**
@@ -2,5 +2,6 @@ export type StoreVaultRequest = {
vaultBlob: string; vaultBlob: string;
publicEmailDomainList?: string[]; publicEmailDomainList?: string[];
privateEmailDomainList?: string[]; privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
vaultRevisionNumber?: number; vaultRevisionNumber?: number;
} }
@@ -3,5 +3,6 @@ export type VaultResponse = {
vault?: string, vault?: string,
publicEmailDomains?: string[], publicEmailDomains?: string[],
privateEmailDomains?: string[], privateEmailDomains?: string[],
hiddenPrivateEmailDomains?: string[],
vaultRevisionNumber?: number vaultRevisionNumber?: number
}; };
@@ -47,6 +47,7 @@ class VaultMetadataManager(
JSONObject().apply { JSONObject().apply {
put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains)) put("publicEmailDomains", JSONArray(updatedMetadata.publicEmailDomains))
put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains)) put("privateEmailDomains", JSONArray(updatedMetadata.privateEmailDomains))
put("hiddenPrivateEmailDomains", JSONArray(updatedMetadata.hiddenPrivateEmailDomains))
put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber) put("vaultRevisionNumber", updatedMetadata.vaultRevisionNumber)
}.toString(), }.toString(),
) )
@@ -158,6 +159,9 @@ class VaultMetadataManager(
privateEmailDomains = json.optJSONArray("privateEmailDomains")?.let { array -> privateEmailDomains = json.optJSONArray("privateEmailDomains")?.let { array ->
List(array.length()) { i -> array.getString(i) } List(array.length()) { i -> array.getString(i) }
} ?: emptyList(), } ?: emptyList(),
hiddenPrivateEmailDomains = json.optJSONArray("hiddenPrivateEmailDomains")?.let { array ->
List(array.length()) { i -> array.getString(i) }
} ?: emptyList(),
vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0), vaultRevisionNumber = json.optInt("vaultRevisionNumber", 0),
) )
} catch (e: Exception) { } catch (e: Exception) {
@@ -33,13 +33,10 @@ class VaultMutate(
json.put("credentialsCount", vault.credentialsCount) json.put("credentialsCount", vault.credentialsCount)
json.put("currentRevisionNumber", vault.currentRevisionNumber) json.put("currentRevisionNumber", vault.currentRevisionNumber)
json.put("emailAddressList", JSONArray(vault.emailAddressList)) 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("encryptionPublicKey", vault.encryptionPublicKey)
json.put("updatedAt", vault.updatedAt) json.put("updatedAt", vault.updatedAt)
json.put("username", vault.username) json.put("username", vault.username)
json.put("version", vault.version) json.put("version", vault.version)
json.put("client", vault.client)
val response = webApiService.executeRequest( val response = webApiService.executeRequest(
method = "POST", method = "POST",
@@ -124,8 +121,6 @@ class VaultMutate(
} catch (e: Exception) { } catch (e: Exception) {
"0.0.0" "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) 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") dateFormat.timeZone = java.util.TimeZone.getTimeZone("UTC")
@@ -137,13 +132,11 @@ class VaultMutate(
credentialsCount = credentials.size, credentialsCount = credentials.size,
currentRevisionNumber = currentRevision, currentRevisionNumber = currentRevision,
emailAddressList = privateEmailAddresses, emailAddressList = privateEmailAddresses,
privateEmailDomainList = emptyList(), // TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this.
publicEmailDomainList = emptyList(),
encryptionPublicKey = "", encryptionPublicKey = "",
updatedAt = now, updatedAt = now,
username = username, username = username,
version = dbVersion, version = dbVersion,
client = client,
) )
} }
@@ -157,13 +150,10 @@ class VaultMutate(
val credentialsCount: Int, val credentialsCount: Int,
val currentRevisionNumber: Int, val currentRevisionNumber: Int,
val emailAddressList: List<String>, val emailAddressList: List<String>,
val privateEmailDomainList: List<String>,
val publicEmailDomainList: List<String>,
val encryptionPublicKey: String, val encryptionPublicKey: String,
val updatedAt: String, val updatedAt: String,
val username: String, val username: String,
val version: String, val version: String,
val client: String,
) )
private data class VaultPostResponse( private data class VaultPostResponse(
@@ -173,12 +173,34 @@ class VaultSync(
database.storeEncryptedDatabase(vault.vault.blob) database.storeEncryptedDatabase(vault.vault.blob)
metadata.setVaultRevisionNumber(newRevision) 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()) { if (database.isVaultUnlocked()) {
// Re-unlock with new data // Re-unlock with new data
// Note: This requires auth methods to be passed, handled by VaultStore // 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 { private fun parseVaultResponse(body: String): VaultResponse {
return try { return try {
val json = JSONObject(body) val json = JSONObject(body)
@@ -196,6 +218,12 @@ class VaultSync(
privateList.add(privateArray.getString(i)) privateList.add(privateArray.getString(i))
} }
val hiddenPrivateList = mutableListOf<String>()
val hiddenPrivateArray = vaultJson.getJSONArray("hiddenPrivateEmailDomainList")
for (i in 0 until hiddenPrivateArray.length()) {
hiddenPrivateList.add(hiddenPrivateArray.getString(i))
}
val publicList = mutableListOf<String>() val publicList = mutableListOf<String>()
val publicArray = vaultJson.getJSONArray("publicEmailDomainList") val publicArray = vaultJson.getJSONArray("publicEmailDomainList")
for (i in 0 until publicArray.length()) { for (i in 0 until publicArray.length()) {
@@ -213,6 +241,7 @@ class VaultSync(
credentialsCount = vaultJson.getInt("credentialsCount"), credentialsCount = vaultJson.getInt("credentialsCount"),
emailAddressList = emailList, emailAddressList = emailList,
privateEmailDomainList = privateList, privateEmailDomainList = privateList,
hiddenPrivateEmailDomainList = hiddenPrivateList,
publicEmailDomainList = publicList, publicEmailDomainList = publicList,
createdAt = vaultJson.getString("createdAt"), createdAt = vaultJson.getString("createdAt"),
updatedAt = vaultJson.getString("updatedAt"), updatedAt = vaultJson.getString("updatedAt"),
@@ -253,6 +282,7 @@ class VaultSync(
val credentialsCount: Int, val credentialsCount: Int,
val emailAddressList: List<String>, val emailAddressList: List<String>,
val privateEmailDomainList: List<String>, val privateEmailDomainList: List<String>,
val hiddenPrivateEmailDomainList: List<String>,
val publicEmailDomainList: List<String>, val publicEmailDomainList: List<String>,
val createdAt: String, val createdAt: String,
val updatedAt: String, val updatedAt: String,
@@ -14,6 +14,12 @@ data class VaultMetadata(
*/ */
val privateEmailDomains: List<String> = emptyList(), val privateEmailDomains: List<String> = 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<String> = emptyList(),
/** /**
* The revision number of the vault. * The revision number of the vault.
*/ */
@@ -35,7 +35,8 @@ class VaultStoreTest {
val metadata = """ val metadata = """
{ {
"publicEmailDomains": ["spamok.com", "spamok.nl"], "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 "vaultRevisionNumber": 1
} }
""" """
-1
View File
@@ -261,7 +261,6 @@ export default function Initialize() : React.ReactNode {
// Now perform vault sync (network operations - these are skippable) // Now perform vault sync (network operations - these are skippable)
await syncVault({ await syncVault({
initialSync: true,
abortSignal: abortControllerRef.current.signal, abortSignal: abortControllerRef.current.signal,
/** /**
* Handle the status update. * Handle the status update.
+20 -36
View File
@@ -10,7 +10,7 @@ import { StyleSheet, View, Text, SafeAreaView, TextInput, ActivityIndicator, Ani
import { useApiUrl } from '@/utils/ApiUrlUtility'; import { useApiUrl } from '@/utils/ApiUrlUtility';
import ConversionUtility from '@/utils/ConversionUtility'; import ConversionUtility from '@/utils/ConversionUtility';
import type { EncryptionKeyDerivationParams } from '@/utils/dist/shared/models/metadata'; 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 EncryptionUtility from '@/utils/EncryptionUtility';
import { SrpUtility } from '@/utils/SrpUtility'; import { SrpUtility } from '@/utils/SrpUtility';
import { ApiAuthError } from '@/utils/types/errors/ApiAuthError'; 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. * Process the vault response by storing the vault and logging in the user.
* @param token - The token to use for the vault * @param token - The token to use for the vault
* @param refreshToken - The refresh 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 passwordHashBase64 - The password hash base64
* @param initiateLoginResponse - The initiate login response * @param initiateLoginResponse - The initiate login response
*/ */
const processVaultResponse = async ( const processVaultResponse = async (
token: string, token: string,
refreshToken: string, refreshToken: string,
vaultResponseJson: VaultResponse,
passwordHashBase64: string, passwordHashBase64: string,
initiateLoginResponse: LoginResponse initiateLoginResponse: LoginResponse
) : Promise<void> => { ) : Promise<void> => {
@@ -109,7 +107,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse( await continueProcessVaultResponse(
token, token,
refreshToken, refreshToken,
vaultResponseJson,
passwordHashBase64, passwordHashBase64,
initiateLoginResponse initiateLoginResponse
); );
@@ -126,7 +123,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse( await continueProcessVaultResponse(
token, token,
refreshToken, refreshToken,
vaultResponseJson,
passwordHashBase64, passwordHashBase64,
initiateLoginResponse initiateLoginResponse
); );
@@ -140,7 +136,6 @@ export default function LoginScreen() : React.ReactNode {
await continueProcessVaultResponse( await continueProcessVaultResponse(
token, token,
refreshToken, refreshToken,
vaultResponseJson,
passwordHashBase64, passwordHashBase64,
initiateLoginResponse initiateLoginResponse
); );
@@ -151,7 +146,6 @@ export default function LoginScreen() : React.ReactNode {
* Continue processing the vault response after biometric choice * Continue processing the vault response after biometric choice
* @param token - The token to use for the vault * @param token - The token to use for the vault
* @param refreshToken - The refresh 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 passwordHashBase64 - The password hash base64
* @param initiateLoginResponse - The initiate login response * @param initiateLoginResponse - The initiate login response
* @param encryptionKeyDerivationParams - The encryption key derivation parameters * @param encryptionKeyDerivationParams - The encryption key derivation parameters
@@ -159,7 +153,6 @@ export default function LoginScreen() : React.ReactNode {
const continueProcessVaultResponse = async ( const continueProcessVaultResponse = async (
token: string, token: string,
refreshToken: string, refreshToken: string,
vaultResponseJson: VaultResponse,
passwordHashBase64: string, passwordHashBase64: string,
initiateLoginResponse: LoginResponse initiateLoginResponse: LoginResponse
) : Promise<void> => { ) : Promise<void> => {
@@ -169,20 +162,29 @@ export default function LoginScreen() : React.ReactNode {
salt: initiateLoginResponse.salt, 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 authContext.setAuthTokens(ConversionUtility.normalizeUsername(credentials.username), token, refreshToken);
await dbContext.storeEncryptionKey(passwordHashBase64); await dbContext.storeEncryptionKey(passwordHashBase64);
await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams); await dbContext.storeEncryptionKeyDerivationParams(encryptionKeyDerivationParams);
await dbContext.initializeDatabase(vaultResponseJson);
let checkSuccess = true; let checkSuccess = true;
/** /**
* After setting auth tokens, execute a server status check immediately * After setting auth tokens, execute a server status check immediately
* which takes care of certain sanity checks such as ensuring client/server * 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({ await syncVault({
initialSync: true, /**
* Update login status during sync.
*/
onStatus: (status) => {
setLoginStatus(status);
},
/** /**
* Handle the status update. * Handle the status update.
*/ */
@@ -218,6 +220,12 @@ export default function LoginScreen() : React.ReactNode {
return; 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(); await authContext.login();
authContext.setOfflineMode(false); authContext.setOfflineMode(false);
@@ -286,23 +294,10 @@ export default function LoginScreen() : React.ReactNode {
setLoginStatus(t('auth.syncingVault')); setLoginStatus(t('auth.syncingVault'));
await new Promise(resolve => requestAnimationFrame(resolve)); await new Promise(resolve => requestAnimationFrame(resolve));
const vaultResponseJson = await webApi.authFetch<VaultResponse>('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( await processVaultResponse(
validationResponse.token.token, validationResponse.token.token,
validationResponse.token.refreshToken, validationResponse.token.refreshToken,
vaultResponseJson,
passwordHashBase64, passwordHashBase64,
initiateLoginResponse initiateLoginResponse
); );
@@ -357,21 +352,10 @@ export default function LoginScreen() : React.ReactNode {
setLoginStatus(t('auth.syncingVault')); setLoginStatus(t('auth.syncingVault'));
await new Promise(resolve => requestAnimationFrame(resolve)); await new Promise(resolve => requestAnimationFrame(resolve));
const vaultResponseJson = await webApi.authFetch<VaultResponse>('Vault', { method: 'GET', headers: {
'Authorization': `Bearer ${validationResponse.token.token}`
} });
const vaultError = webApi.validateVaultResponse(vaultResponseJson);
if (vaultError) {
setError(vaultError);
setIsLoading(false);
return;
}
await processVaultResponse( await processVaultResponse(
validationResponse.token.token, validationResponse.token.token,
validationResponse.token.refreshToken, validationResponse.token.refreshToken,
vaultResponseJson,
passwordHashBase64, passwordHashBase64,
initiateLoginResponse initiateLoginResponse
); );
-1
View File
@@ -245,7 +245,6 @@ export default function ReinitializeScreen() : React.ReactNode {
// Now perform vault sync (network operations - these are skippable) // Now perform vault sync (network operations - these are skippable)
await syncVault({ await syncVault({
initialSync: true,
/** /**
* Handle the status update. * Handle the status update.
*/ */
@@ -75,7 +75,7 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
}, [dbContext]); }, [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<boolean> => { const isPrivateDomain = useCallback(async (emailAddress: string): Promise<boolean> => {
// Get private domains from stored metadata // Get private domains from stored metadata
@@ -84,7 +84,9 @@ export const EmailPreview: React.FC<EmailPreviewProps> = ({ email }) : React.Rea
return false; return false;
} }
return metadata.privateEmailDomains.includes(emailAddress.split('@')[1]); const domain = emailAddress.split('@')[1];
return metadata.privateEmailDomains.includes(domain) ||
(metadata.hiddenPrivateEmailDomains || []).includes(domain);
}, [dbContext]); }, [dbContext]);
// Handle app state changes // Handle app state changes
@@ -49,18 +49,21 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
const [selectedDomain, setSelectedDomain] = useState(''); const [selectedDomain, setSelectedDomain] = useState('');
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]); const [privateEmailDomains, setPrivateEmailDomains] = useState<string[]>([]);
const [hiddenPrivateEmailDomains, setHiddenPrivateEmailDomains] = useState<string[]>([]);
// Get private email domains from vault metadata // Get private email domains from vault metadata
useEffect(() => { useEffect(() => {
/** /**
* Load private email domains from vault metadata. * Load private email domains from vault metadata, excluding hidden ones from UI.
*/ */
const loadDomains = async (): Promise<void> => { const loadDomains = async (): Promise<void> => {
try { try {
const metadata = await dbContext.getVaultMetadata(); 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) { } catch (err) {
console.error('Error loading email domains:', err); console.error('Error loading email domains:', err);
} }
@@ -91,9 +94,10 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
setLocalPart(local); setLocalPart(local);
setSelectedDomain(domain); 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) || const isKnownDomain = PUBLIC_EMAIL_DOMAINS.includes(domain) ||
privateEmailDomains.includes(domain); privateEmailDomains.includes(domain) ||
hiddenPrivateEmailDomains.includes(domain);
setIsCustomDomain(!isKnownDomain); setIsCustomDomain(!isKnownDomain);
} else { } else {
setLocalPart(value); setLocalPart(value);
@@ -108,7 +112,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
} }
} }
} }
}, [value, privateEmailDomains, showPrivateDomains]); }, [value, privateEmailDomains, hiddenPrivateEmailDomains, showPrivateDomains]);
// Handle local part changes // Handle local part changes
const handleLocalPartChange = useCallback((newText: string) => { const handleLocalPartChange = useCallback((newText: string) => {
@@ -410,7 +414,7 @@ export const EmailDomainField: React.FC<EmailDomainFieldProps> = ({
{t('credentials.privateEmailDescription')} {t('credentials.privateEmailDescription')}
</Text> </Text>
<View style={styles.domainList}> <View style={styles.domainList}>
{privateEmailDomains.map((domain) => ( {privateEmailDomains.filter(domain => !hiddenPrivateEmailDomains.includes(domain)).map((domain) => (
<TouchableOpacity <TouchableOpacity
key={domain} key={domain}
style={[ style={[
+20 -4
View File
@@ -18,6 +18,8 @@ type DbContextType = {
getVaultMetadata: () => Promise<VaultMetadata | null>; getVaultMetadata: () => Promise<VaultMetadata | null>;
testDatabaseConnection: (derivedKey: string) => Promise<boolean>; testDatabaseConnection: (derivedKey: string) => Promise<boolean>;
unlockVault: () => Promise<boolean>; unlockVault: () => Promise<boolean>;
checkStoredVault: () => Promise<void>;
setDatabaseAvailable: () => void;
} }
const DbContext = createContext<DbContextType | undefined>(undefined); const DbContext = createContext<DbContextType | undefined>(undefined);
@@ -80,13 +82,16 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
/** /**
* Initialize the database in the native module. * 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 * @param vaultResponse The vault response from the API
*/ */
const initializeDatabase = useCallback(async (vaultResponse: VaultResponse) => { const initializeDatabase = useCallback(async (vaultResponse: VaultResponse) => {
const metadata: VaultMetadata = { const metadata: VaultMetadata = {
publicEmailDomains: vaultResponse.vault.publicEmailDomainList, publicEmailDomains: vaultResponse.vault.publicEmailDomainList ?? [],
privateEmailDomains: vaultResponse.vault.privateEmailDomainList, privateEmailDomains: vaultResponse.vault.privateEmailDomainList ?? [],
hiddenPrivateEmailDomains: vaultResponse.vault.hiddenPrivateEmailDomainList ?? [],
vaultRevisionNumber: vaultResponse.vault.currentRevisionNumber, 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.storeEncryptedDatabase(vaultResponse.vault.blob);
await sqliteClient.storeMetadata(JSON.stringify(metadata)); await sqliteClient.storeMetadata(JSON.stringify(metadata));
// Initialize the database in the native module // Unlock the vault to make it available for queries
await unlockVault(); await unlockVault();
setDbInitialized(true); setDbInitialized(true);
@@ -156,6 +161,15 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
NativeVaultManager.clearVault(); 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 * Get the current vault metadata directly from SQLite client
*/ */
@@ -199,7 +213,9 @@ export const DbProvider: React.FC<{ children: React.ReactNode }> = ({ children }
unlockVault, unlockVault,
storeEncryptionKey, storeEncryptionKey,
storeEncryptionKeyDerivationParams, 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 ( return (
<DbContext.Provider value={contextValue}> <DbContext.Provider value={contextValue}>
@@ -186,12 +186,10 @@ export const NavigationProvider: React.FC<{ children: React.ReactNode }> = ({ ch
} }
// Database connection failed, navigate to reinitialize flow // Database connection failed, navigate to reinitialize flow
console.log('database connection failed, navigating to reinitialize');
router.replace('/reinitialize'); router.replace('/reinitialize');
} }
} catch { } catch {
// Database query failed, navigate to reinitialize flow // Database query failed, navigate to reinitialize flow
console.log('database query failed, navigating to reinitialize');
router.replace('/reinitialize'); router.replace('/reinitialize');
} }
} }
+1
View File
@@ -76,6 +76,7 @@ export function useVaultMutate() : {
currentRevisionNumber: currentRevision, currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses, emailAddressList: privateEmailAddresses,
privateEmailDomainList: [], privateEmailDomainList: [],
hiddenPrivateEmailDomainList: [],
publicEmailDomainList: [], publicEmailDomainList: [],
encryptionPublicKey: '', encryptionPublicKey: '',
client: '', client: '',
+3 -27
View File
@@ -16,16 +16,7 @@ import { VaultSyncErrorCode, getVaultSyncErrorCode } from '@/utils/types/errors/
/** /**
* Utility function to ensure a minimum time has elapsed for an operation * Utility function to ensure a minimum time has elapsed for an operation
*/ */
const withMinimumDelay = async <T>( const withMinimumDelay = async <T>(operation: () => Promise<T>, minDelayMs: number): Promise<T> => {
operation: () => Promise<T>,
minDelayMs: number,
enableDelay: boolean = true
): Promise<T> => {
if (!enableDelay) {
// If delay is disabled, return the result immediately.
return operation();
}
const startTime = Date.now(); const startTime = Date.now();
const result = await operation(); const result = await operation();
const elapsedTime = Date.now() - startTime; const elapsedTime = Date.now() - startTime;
@@ -38,7 +29,6 @@ const withMinimumDelay = async <T>(
}; };
type VaultSyncOptions = { type VaultSyncOptions = {
initialSync?: boolean;
onSuccess?: (hasNewVault: boolean) => void; onSuccess?: (hasNewVault: boolean) => void;
onError?: (error: string) => void; onError?: (error: string) => void;
onStatus?: (message: string) => void; onStatus?: (message: string) => void;
@@ -59,10 +49,7 @@ export const useVaultSync = () : {
const dbContext = useDb(); const dbContext = useDb();
const syncVault = useCallback(async (options: VaultSyncOptions = {}) => { const syncVault = useCallback(async (options: VaultSyncOptions = {}) => {
const { initialSync = false, onSuccess, onError, onStatus, onOffline, onUpgradeRequired, abortSignal } = options; const { 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;
try { try {
// Check if operation was aborted // Check if operation was aborted
@@ -87,11 +74,6 @@ export const useVaultSync = () : {
// Update status // Update status
onStatus?.(t('vault.checkingVaultUpdates')); 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 // Check if operation was aborted
if (abortSignal?.aborted) { if (abortSignal?.aborted) {
console.debug('VaultSync: Operation aborted after status update'); console.debug('VaultSync: Operation aborted after status update');
@@ -128,8 +110,7 @@ export const useVaultSync = () : {
// Run downloadVault with a min delay for UX purposes // Run downloadVault with a min delay for UX purposes
await withMinimumDelay( await withMinimumDelay(
() => NativeVaultManager.downloadVault(newRevision!), () => NativeVaultManager.downloadVault(newRevision!),
enableDelay ? 500 : 300, 300
true
); );
} }
} catch (err) { } catch (err) {
@@ -183,11 +164,6 @@ export const useVaultSync = () : {
return false; return false;
} }
// Add artificial delay for initial sync UX
if (enableDelay) {
await new Promise(resolve => setTimeout(resolve, hasNewVault ? 1000 : 300));
}
onSuccess?.(hasNewVault); onSuccess?.(hasNewVault);
// Register credential identities after sync // Register credential identities after sync
@@ -3,11 +3,13 @@ import Foundation
public struct VaultMetadata: Codable { public struct VaultMetadata: Codable {
public var publicEmailDomains: [String]? public var publicEmailDomains: [String]?
public var privateEmailDomains: [String]? public var privateEmailDomains: [String]?
public var hiddenPrivateEmailDomains: [String]?
public var vaultRevisionNumber: Int 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.publicEmailDomains = publicEmailDomains
self.privateEmailDomains = privateEmailDomains self.privateEmailDomains = privateEmailDomains
self.hiddenPrivateEmailDomains = hiddenPrivateEmailDomains
self.vaultRevisionNumber = vaultRevisionNumber self.vaultRevisionNumber = vaultRevisionNumber
} }
} }
@@ -42,6 +42,7 @@ extension VaultStore {
metadata = VaultMetadata( metadata = VaultMetadata(
publicEmailDomains: [], publicEmailDomains: [],
privateEmailDomains: [], privateEmailDomains: [],
hiddenPrivateEmailDomains: [],
vaultRevisionNumber: revisionNumber vaultRevisionNumber: revisionNumber
) )
} }
@@ -8,13 +8,10 @@ public struct VaultUpload: Codable {
public let credentialsCount: Int public let credentialsCount: Int
public let currentRevisionNumber: Int public let currentRevisionNumber: Int
public let emailAddressList: [String] public let emailAddressList: [String]
public let privateEmailDomainList: [String]
public let publicEmailDomainList: [String]
public let encryptionPublicKey: String public let encryptionPublicKey: String
public let updatedAt: String public let updatedAt: String
public let username: String public let username: String
public let version: String public let version: String
public let client: String
} }
/// Vault POST response from API /// Vault POST response from API
@@ -86,11 +83,6 @@ extension VaultStore {
// Get database version // Get database version
let dbVersion = try getDatabaseVersion() 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() let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
let now = dateFormatter.string(from: Date()) let now = dateFormatter.string(from: Date())
@@ -101,13 +93,11 @@ extension VaultStore {
credentialsCount: credentials.count, credentialsCount: credentials.count,
currentRevisionNumber: currentRevision, currentRevisionNumber: currentRevision,
emailAddressList: privateEmailAddresses, emailAddressList: privateEmailAddresses,
privateEmailDomainList: [], // Empty on purpose, API will not use this for vault updates // TODO: add public RSA encryption key to payload when implementing vault creation from mobile app. Currently only web app does this.
publicEmailDomainList: [], // Empty on purpose, API will not use this for vault updates encryptionPublicKey: "",
encryptionPublicKey: "", // Empty on purpose, only required if new public/private key pair is generated
updatedAt: now, updatedAt: now,
username: username, username: username,
version: dbVersion, version: dbVersion,
client: client
) )
} }
@@ -19,6 +19,7 @@ public struct VaultData: Codable {
public let credentialsCount: Int public let credentialsCount: Int
public let emailAddressList: [String] public let emailAddressList: [String]
public let privateEmailDomainList: [String] public let privateEmailDomainList: [String]
public let hiddenPrivateEmailDomainList: [String]
public let publicEmailDomainList: [String] public let publicEmailDomainList: [String]
public let createdAt: String public let createdAt: String
public let updatedAt: String public let updatedAt: String
@@ -177,11 +178,31 @@ extension VaultStore {
try storeEncryptedDatabase(vault.vault.blob) try storeEncryptedDatabase(vault.vault.blob)
setCurrentVaultRevisionNumber(newRevision) 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 { if isVaultUnlocked {
try unlockVault() 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 /// Parse vault response from JSON
private func parseVaultResponse(_ body: String) throws -> VaultResponse { private func parseVaultResponse(_ body: String) throws -> VaultResponse {
guard let vaultData = body.data(using: .utf8) else { guard let vaultData = body.data(using: .utf8) else {
+6 -3
View File
@@ -30,7 +30,9 @@ class SqliteClient {
* Store the vault metadata via the native code implementation. * Store the vault metadata via the native code implementation.
* *
* Metadata is stored in plain text in UserDefaults. The metadata consists of the following: * 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 * - vault revision number
*/ */
public async storeMetadata(metadata: string): Promise<void> { public async storeMetadata(metadata: string): Promise<void> {
@@ -77,7 +79,7 @@ class SqliteClient {
return null; 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) * 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 // 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) { if (firstPrivate) {
return firstPrivate; return firstPrivate;
} }
-21
View File
@@ -314,27 +314,6 @@ export class WebApiService {
return this.get<AuthLogModel[]>('Security/authlogs'); return this.get<AuthLogModel[]>('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. * Get the currently configured API URL from native storage.
*/ */
@@ -1,6 +1,7 @@
type VaultMetadata = { type VaultMetadata = {
publicEmailDomains: string[]; publicEmailDomains: string[];
privateEmailDomains: string[]; privateEmailDomains: string[];
hiddenPrivateEmailDomains: string[];
vaultRevisionNumber: number; vaultRevisionNumber: number;
}; };
+10 -10
View File
@@ -28,18 +28,18 @@ type ApiErrorResponse = {
* Vault type. * Vault type.
*/ */
type Vault = { type Vault = {
blob: string;
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
username: string; username: string;
blob: string;
version: string; version: string;
client: string; currentRevisionNumber: number;
credentialsCount: number;
createdAt: string;
updatedAt: string;
encryptionPublicKey?: string;
emailAddressList?: string[];
privateEmailDomainList?: string[];
hiddenPrivateEmailDomainList?: string[];
publicEmailDomainList?: string[];
}; };
/** /**
+6
View File
@@ -23,4 +23,10 @@ public class Config : SharedConfig
/// Gets or sets the list of private email domains that are available. /// Gets or sets the list of private email domains that are available.
/// </summary> /// </summary>
public List<string> PrivateEmailDomains { get; set; } = []; public List<string> PrivateEmailDomains { get; set; } = [];
/// <summary>
/// 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.
/// </summary>
public List<string> HiddenPrivateEmailDomains { get; set; } = [];
} }
@@ -89,11 +89,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
Blob = string.Empty, Blob = string.Empty,
Version = string.Empty, Version = string.Empty,
CurrentRevisionNumber = 0, CurrentRevisionNumber = 0,
EncryptionPublicKey = string.Empty,
CredentialsCount = 0, CredentialsCount = 0,
EmailAddressList = [],
PrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = DateTime.MinValue, CreatedAt = DateTime.MinValue,
UpdatedAt = DateTime.MinValue, UpdatedAt = DateTime.MinValue,
}, },
@@ -121,6 +117,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
// Get dynamic list of private email domains from config. // Get dynamic list of private email domains from config.
var privateEmailDomainList = config.PrivateEmailDomains; var privateEmailDomainList = config.PrivateEmailDomains;
var hiddenPrivateEmailDomainList = config.HiddenPrivateEmailDomains;
// Hardcoded list of public (SpamOK) email domains that are available to the client. // Hardcoded list of public (SpamOK) email domains that are available to the client.
var publicEmailDomainList = new List<string>(["spamok.com", "solarflarecorp.com", "spamok.nl", "3060.nl", var publicEmailDomainList = new List<string>(["spamok.com", "solarflarecorp.com", "spamok.nl", "3060.nl",
@@ -137,8 +134,8 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
CurrentRevisionNumber = vault.RevisionNumber, CurrentRevisionNumber = vault.RevisionNumber,
EncryptionPublicKey = string.Empty, EncryptionPublicKey = string.Empty,
CredentialsCount = 0, CredentialsCount = 0,
EmailAddressList = [],
PrivateEmailDomainList = privateEmailDomainList, PrivateEmailDomainList = privateEmailDomainList,
HiddenPrivateEmailDomainList = hiddenPrivateEmailDomainList,
PublicEmailDomainList = publicEmailDomainList, PublicEmailDomainList = publicEmailDomainList,
CreatedAt = vault.CreatedAt, CreatedAt = vault.CreatedAt,
UpdatedAt = vault.UpdatedAt, UpdatedAt = vault.UpdatedAt,
@@ -176,11 +173,7 @@ public class VaultController(ILogger<VaultController> logger, IAliasServerDbCont
Blob = x.VaultBlob, Blob = x.VaultBlob,
Version = x.Version, Version = x.Version,
CurrentRevisionNumber = x.RevisionNumber, CurrentRevisionNumber = x.RevisionNumber,
EncryptionPublicKey = string.Empty,
CredentialsCount = 0, CredentialsCount = 0,
EmailAddressList = [],
PrivateEmailDomainList = [],
PublicEmailDomainList = [],
CreatedAt = x.CreatedAt, CreatedAt = x.CreatedAt,
UpdatedAt = x.UpdatedAt, UpdatedAt = x.UpdatedAt,
}).ToList(), }).ToList(),
+6
View File
@@ -48,6 +48,12 @@ var privateEmailDomains = Environment.GetEnvironmentVariable("PRIVATE_EMAIL_DOMA
.Where(d => !string.IsNullOrWhiteSpace(d)); .Where(d => !string.IsNullOrWhiteSpace(d));
config.PrivateEmailDomains = privateEmailDomains?.ToList() ?? new List<string>(); config.PrivateEmailDomains = privateEmailDomains?.ToList() ?? new List<string>();
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<string>();
var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false"; var ipLoggingEnabled = Environment.GetEnvironmentVariable("IP_LOGGING_ENABLED") ?? "false";
config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled); config.IpLoggingEnabled = bool.Parse(ipLoggingEnabled);
@@ -9,7 +9,8 @@
"JWT_KEY": "12345678901234567890123456789012", "JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development", "DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true", "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" "IP_LOGGING_ENABLED": "true"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
@@ -24,7 +25,8 @@
"JWT_KEY": "12345678901234567890123456789012", "JWT_KEY": "12345678901234567890123456789012",
"DATA_PROTECTION_CERT_PASS": "Development", "DATA_PROTECTION_CERT_PASS": "Development",
"PUBLIC_REGISTRATION_ENABLED": "true", "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" "IP_LOGGING_ENABLED": "true"
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
+6
View File
@@ -23,6 +23,12 @@ public class Config
/// </summary> /// </summary>
public List<string> PrivateEmailDomains { get; set; } = []; public List<string> PrivateEmailDomains { get; set; } = [];
/// <summary>
/// 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.
/// </summary>
public List<string> HiddenPrivateEmailDomains { get; set; } = [];
/// <summary> /// <summary>
/// Gets or sets the list of public email domains that are allowed to be used by the client vault users. /// Gets or sets the list of public email domains that are allowed to be used by the client vault users.
/// </summary> /// </summary>
@@ -99,7 +99,9 @@
private string SelectedDomain = string.Empty; private string SelectedDomain = string.Empty;
private bool IsPopupVisible = false; private bool IsPopupVisible = false;
private List<string> PrivateDomains => Config.PrivateEmailDomains; private List<string> PrivateDomains => Config.PrivateEmailDomains
.Where(d => !Config.HiddenPrivateEmailDomains.Contains(d))
.ToList();
private List<string> PublicDomains => Config.PublicEmailDomains; private List<string> PublicDomains => Config.PublicEmailDomains;
private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD"); private bool ShowPrivateDomains => PrivateDomains.Count > 0 && !(PrivateDomains.Count == 1 && PrivateDomains[0] == "DISABLED.TLD");
@@ -125,7 +125,9 @@
@code { @code {
private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.General", "AliasVault.Client"); private IStringLocalizer Localizer => LocalizerFactory.Create("Pages.Main.Settings.General", "AliasVault.Client");
private List<string> PrivateDomains => Config.PrivateEmailDomains; private List<string> PrivateDomains => Config.PrivateEmailDomains
.Where(d => !Config.HiddenPrivateEmailDomains.Contains(d))
.ToList();
private List<string> PublicDomains => Config.PublicEmailDomains; private List<string> PublicDomains => Config.PublicEmailDomains;
private string DefaultEmailDomain { get; set; } = string.Empty; private string DefaultEmailDomain { get; set; } = string.Empty;
@@ -196,6 +196,7 @@ else
CredentialsCount = vault.CredentialsCount, CredentialsCount = vault.CredentialsCount,
EmailAddressList = vault.EmailAddressList, EmailAddressList = vault.EmailAddressList,
PrivateEmailDomainList = [], PrivateEmailDomainList = [],
HiddenPrivateEmailDomainList = [],
PublicEmailDomainList = [], PublicEmailDomainList = [],
CreatedAt = vault.CreatedAt, CreatedAt = vault.CreatedAt,
UpdatedAt = vault.UpdatedAt, UpdatedAt = vault.UpdatedAt,
@@ -490,6 +490,7 @@ public sealed class DbService : IDisposable
CredentialsCount = credentialsCount, CredentialsCount = credentialsCount,
EmailAddressList = emailAddresses, EmailAddressList = emailAddresses,
PrivateEmailDomainList = [], PrivateEmailDomainList = [],
HiddenPrivateEmailDomainList = [],
PublicEmailDomainList = [], PublicEmailDomainList = [],
CreatedAt = currentDateTime, CreatedAt = currentDateTime,
UpdatedAt = currentDateTime, UpdatedAt = currentDateTime,
@@ -1,11 +1,13 @@
#!/bin/sh #!/bin/sh
# Set the default values # Set the default values
DEFAULT_PRIVATE_EMAIL_DOMAINS="" DEFAULT_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_SUPPORT_EMAIL="" DEFAULT_SUPPORT_EMAIL=""
DEFAULT_PUBLIC_REGISTRATION_ENABLED="true" DEFAULT_PUBLIC_REGISTRATION_ENABLED="true"
# Use the provided environment variables if they exist, otherwise use defaults # Use the provided environment variables if they exist, otherwise use defaults
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS} 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} SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-$DEFAULT_PUBLIC_REGISTRATION_ENABLED} 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<length(a)) printf ","} printf "]"}') json_array=$(echo $PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi fi
# Handle empty HIDDEN_PRIVATE_EMAIL_DOMAINS by defaulting to empty array
if [ -z "$HIDDEN_PRIVATE_EMAIL_DOMAINS" ]; then
hidden_json_array="[]"
else
# Convert comma-separated list to JSON array
hidden_json_array=$(echo $HIDDEN_PRIVATE_EMAIL_DOMAINS | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi
# Use sed to update the PrivateEmailDomains field in appsettings.json # Use sed to update the PrivateEmailDomains field in appsettings.json
sed -i.bak "s|\"PrivateEmailDomains\": \[.*\]|\"PrivateEmailDomains\": $json_array|" /usr/share/nginx/html/appsettings.json sed -i.bak "s|\"PrivateEmailDomains\": \[.*\]|\"PrivateEmailDomains\": $json_array|" /usr/share/nginx/html/appsettings.json
# Add HiddenPrivateEmailDomains field if it doesn't exist, or update it if it does
if grep -q "HiddenPrivateEmailDomains" /usr/share/nginx/html/appsettings.json; then
sed -i "s|\"HiddenPrivateEmailDomains\": \[.*\]|\"HiddenPrivateEmailDomains\": $hidden_json_array|" /usr/share/nginx/html/appsettings.json
else
# Insert HiddenPrivateEmailDomains after PrivateEmailDomains
sed -i "s|\"PrivateEmailDomains\": $json_array|\"PrivateEmailDomains\": $json_array,\n \"HiddenPrivateEmailDomains\": $hidden_json_array|" /usr/share/nginx/html/appsettings.json
fi
# Update support email in appsettings.json # Update support email in appsettings.json
if [ ! -z "$SUPPORT_EMAIL" ]; then if [ ! -z "$SUPPORT_EMAIL" ]; then
sed -i "s|\"SupportEmail\": \".*\"|\"SupportEmail\": \"$SUPPORT_EMAIL\"|g" /usr/share/nginx/html/appsettings.json sed -i "s|\"SupportEmail\": \".*\"|\"SupportEmail\": \"$SUPPORT_EMAIL\"|g" /usr/share/nginx/html/appsettings.json
@@ -1,6 +1,7 @@
{ {
"ApiUrl": "http://localhost:5092", "ApiUrl": "http://localhost:5092",
"PrivateEmailDomains": ["example.tld"], "PrivateEmailDomains": ["example.tld", "example2.tld", "disabled.tld"],
"HiddenPrivateEmailDomains": ["disabled.tld"],
"SupportEmail": "support@example.tld", "SupportEmail": "support@example.tld",
"PublicRegistrationEnabled": "true" "PublicRegistrationEnabled": "true"
} }
@@ -4,7 +4,7 @@
"commandName": "Project", "commandName": "Project",
"environmentVariables": { "environmentVariables": {
"DOTNET_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development",
"PRIVATE_EMAIL_DOMAINS": "example.tld", "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
"SMTP_TLS_ENABLED": "false" "SMTP_TLS_ENABLED": "false"
}, },
"dotnetRunMessages": true "dotnetRunMessages": true
@@ -4,7 +4,7 @@
"commandName": "Project", "commandName": "Project",
"environmentVariables": { "environmentVariables": {
"DOTNET_ENVIRONMENT": "Development", "DOTNET_ENVIRONMENT": "Development",
"PRIVATE_EMAIL_DOMAINS": "example.tld", "PRIVATE_EMAIL_DOMAINS": "example.tld,example2.tld,disabled.tld",
"SMTP_TLS_ENABLED": "false" "SMTP_TLS_ENABLED": "false"
}, },
"dotnetRunMessages": true "dotnetRunMessages": true
@@ -12,6 +12,10 @@ namespace AliasVault.Shared.Models.WebApi.Vault;
/// </summary> /// </summary>
public class Vault public class Vault
{ {
// ------------------------------------------------------------
// Required properties, always part of the vault get/set model.
// ------------------------------------------------------------
/// <summary> /// <summary>
/// Gets or sets the username that owns the vault. /// Gets or sets the username that owns the vault.
/// </summary> /// </summary>
@@ -34,32 +38,12 @@ public class Vault
/// </summary> /// </summary>
public required long CurrentRevisionNumber { get; set; } public required long CurrentRevisionNumber { get; set; }
/// <summary>
/// Gets or sets the public encryption key that server requires to encrypt user data such as received emails.
/// </summary>
public required string EncryptionPublicKey { get; set; }
/// <summary> /// <summary>
/// Gets or sets the number of credentials stored in the vault. This anonymous data is used in case a vault back-up /// 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. /// needs to be restored to get a better idea of the vault size.
/// </summary> /// </summary>
public required int CredentialsCount { get; set; } public required int CredentialsCount { get; set; }
/// <summary>
/// Gets or sets the list of email addresses that are used in the vault and should be registered on the server.
/// </summary>
public required List<string> EmailAddressList { get; set; }
/// <summary>
/// Gets or sets the list of private email domains that are available.
/// </summary>
public required List<string> PrivateEmailDomainList { get; set; }
/// <summary>
/// Gets or sets the list of public email domains that are available.
/// </summary>
public required List<string> PublicEmailDomainList { get; set; }
/// <summary> /// <summary>
/// Gets or sets the date and time of creation. /// Gets or sets the date and time of creation.
/// </summary> /// </summary>
@@ -69,4 +53,34 @@ public class Vault
/// Gets or sets the date and time of last update. /// Gets or sets the date and time of last update.
/// </summary> /// </summary>
public required DateTime UpdatedAt { get; set; } public required DateTime UpdatedAt { get; set; }
// ------------------------------------------------------------
// Optional properties, only part of the vault get/set model if available and applicable.
// ------------------------------------------------------------
/// <summary>
/// Gets or sets the public encryption key that server requires to encrypt user data such as received emails.
/// </summary>
public string EncryptionPublicKey { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the list of email addresses that are used in the vault and should be registered on the server.
/// </summary>
public List<string> EmailAddressList { get; set; } = [];
/// <summary>
/// Gets or sets the list of private email domains that are available.
/// </summary>
public List<string> PrivateEmailDomainList { get; set; } = [];
/// <summary>
/// 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.
/// </summary>
public List<string> HiddenPrivateEmailDomainList { get; set; } = [];
/// <summary>
/// Gets or sets the list of public email domains that are available.
/// </summary>
public List<string> PublicEmailDomainList { get; set; } = [];
} }
@@ -94,8 +94,10 @@ public class ClientPlaywrightTest : PlaywrightTest
ApiBaseUrl = "http://localhost:" + apiPort + "/"; ApiBaseUrl = "http://localhost:" + apiPort + "/";
// Set environment variables for the API. // 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)); 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. // Start WebAPI in-memory.
_apiFactory.Port = apiPort; _apiFactory.Port = apiPort;
+1
View File
@@ -163,6 +163,7 @@ ENV ALIASVAULT_VERBOSITY=0 \
IP_LOGGING_ENABLED=true \ IP_LOGGING_ENABLED=true \
SUPPORT_EMAIL="" \ SUPPORT_EMAIL="" \
PRIVATE_EMAIL_DOMAINS="" \ PRIVATE_EMAIL_DOMAINS="" \
HIDDEN_PRIVATE_EMAIL_DOMAINS="" \
HOSTNAME=localhost \ HOSTNAME=localhost \
POSTGRES_HOST=localhost \ POSTGRES_HOST=localhost \
POSTGRES_PORT=5432 \ POSTGRES_PORT=5432 \
@@ -30,6 +30,7 @@ done
export ASPNETCORE_URLS="http://0.0.0.0:3001" export ASPNETCORE_URLS="http://0.0.0.0:3001"
export ASPNETCORE_PATHBASE="/api" export ASPNETCORE_PATHBASE="/api"
export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}" 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 PUBLIC_REGISTRATION_ENABLED="${PUBLIC_REGISTRATION_ENABLED:-true}"
export IP_LOGGING_ENABLED="${IP_LOGGING_ENABLED:-true}" export IP_LOGGING_ENABLED="${IP_LOGGING_ENABLED:-true}"
@@ -27,8 +27,10 @@ done
# Client service entrypoint # Client service entrypoint
DEFAULT_PRIVATE_EMAIL_DOMAINS="" DEFAULT_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_HIDDEN_PRIVATE_EMAIL_DOMAINS=""
DEFAULT_SUPPORT_EMAIL="" DEFAULT_SUPPORT_EMAIL=""
PRIVATE_EMAIL_DOMAINS=${PRIVATE_EMAIL_DOMAINS:-$DEFAULT_PRIVATE_EMAIL_DOMAINS} 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} SUPPORT_EMAIL=${SUPPORT_EMAIL:-$DEFAULT_SUPPORT_EMAIL}
PUBLIC_REGISTRATION_ENABLED=${PUBLIC_REGISTRATION_ENABLED:-true} 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<length(a)) printf ","} printf "]"}') json_array=$(echo "$PRIVATE_EMAIL_DOMAINS" | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi fi
# Convert comma-separated HIDDEN_PRIVATE_EMAIL_DOMAINS to JSON array
if [ -z "$HIDDEN_PRIVATE_EMAIL_DOMAINS" ]; then
hidden_json_array="[]"
else
# Convert comma-separated list to JSON array
hidden_json_array=$(echo "$HIDDEN_PRIVATE_EMAIL_DOMAINS" | awk '{split($0,a,","); printf "["; for(i=1;i<=length(a);i++) {printf "\"%s\"", a[i]; if(i<length(a)) printf ","} printf "]"}')
fi
# Create JSON with environment variables # Create JSON with environment variables
cat > /app/client/wwwroot/appsettings.json << EOF cat > /app/client/wwwroot/appsettings.json << EOF
{ {
"PrivateEmailDomains": $json_array, "PrivateEmailDomains": $json_array,
"HiddenPrivateEmailDomains": $hidden_json_array,
"SupportEmail": "$SUPPORT_EMAIL", "SupportEmail": "$SUPPORT_EMAIL",
"PublicRegistrationEnabled": "$PUBLIC_REGISTRATION_ENABLED" "PublicRegistrationEnabled": "$PUBLIC_REGISTRATION_ENABLED"
} }
@@ -28,6 +28,7 @@ for i in {1..30}; do
done done
export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}" export PRIVATE_EMAIL_DOMAINS="${PRIVATE_EMAIL_DOMAINS:-}"
export HIDDEN_PRIVATE_EMAIL_DOMAINS="${HIDDEN_PRIVATE_EMAIL_DOMAINS:-}"
export SMTP_TLS_ENABLED="${SMTP_TLS_ENABLED:-false}" export SMTP_TLS_ENABLED="${SMTP_TLS_ENABLED:-false}"
# Set .NET logging level based on verbosity # Set .NET logging level based on verbosity
@@ -29,6 +29,7 @@ services:
FORCE_HTTPS_REDIRECT: "false" FORCE_HTTPS_REDIRECT: "false"
SUPPORT_EMAIL: "" SUPPORT_EMAIL: ""
PRIVATE_EMAIL_DOMAINS: "" PRIVATE_EMAIL_DOMAINS: ""
HIDDEN_PRIVATE_EMAIL_DOMAINS: ""
volumes: volumes:
avdata: avdata:
@@ -27,3 +27,4 @@ services:
FORCE_HTTPS_REDIRECT: "false" FORCE_HTTPS_REDIRECT: "false"
SUPPORT_EMAIL: "" SUPPORT_EMAIL: ""
PRIVATE_EMAIL_DOMAINS: "" PRIVATE_EMAIL_DOMAINS: ""
HIDDEN_PRIVATE_EMAIL_DOMAINS: ""
+6
View File
@@ -3183,6 +3183,12 @@ check_and_populate_env() {
printf " Set PRIVATE_EMAIL_DOMAINS\n" printf " Set PRIVATE_EMAIL_DOMAINS\n"
fi 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 # HTTP_PORT
if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then if ! grep -q "^HTTP_PORT=" "$ENV_FILE" || [ -z "$(grep "^HTTP_PORT=" "$ENV_FILE" | cut -d '=' -f2)" ]; then
update_env_var "HTTP_PORT" "80" update_env_var "HTTP_PORT" "80"
@@ -1,5 +1,6 @@
export type VaultMetadata = { export type VaultMetadata = {
publicEmailDomains: string[], publicEmailDomains: string[],
privateEmailDomains: string[], privateEmailDomains: string[],
hiddenPrivateEmailDomains: string[],
vaultRevisionNumber: number vaultRevisionNumber: number
}; };
+12 -10
View File
@@ -2,16 +2,18 @@
* Vault type. * Vault type.
*/ */
export type Vault = { export type Vault = {
blob: string; // Required properties, always part of the vault get/set model.
createdAt: string;
credentialsCount: number;
currentRevisionNumber: number;
emailAddressList: string[];
privateEmailDomainList: string[];
publicEmailDomainList: string[];
encryptionPublicKey: string;
updatedAt: string;
username: string; username: string;
blob: string;
version: 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[];
} }