From 4fc2ce20cf9c637753226368033530a46b21cf2e Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Thu, 11 Dec 2025 13:35:16 +0100 Subject: [PATCH] Add fixed SrpIdentity column to allow username changes (#1429) --- .../src/utils/auth/SrpAuthService.ts | 35 +- .../utils/dist/core/models/webapi/index.d.ts | 2 + apps/mobile-app/utils/SrpUtility.tsx | 30 +- .../utils/dist/core/models/webapi/index.d.ts | 2 + .../AliasVault.Api/AliasVault.Api.csproj | 4 + .../Controllers/AuthController.cs | 30 +- .../AliasVault.Api/Helpers/AuthHelper.cs | 13 +- .../AliasVault.Client/Auth/Pages/Login.razor | 8 +- .../Settings/Security/ChangePassword.razor | 19 +- .../Settings/Security/DeleteAccount.razor | 8 +- .../Services/Auth/UserRegistrationService.cs | 8 +- .../Databases/AliasServerDb/AliasVaultUser.cs | 7 + ...211120421_AddSrpIdentityToUser.Designer.cs | 989 ++++++++++++++++++ .../20251211120421_AddSrpIdentityToUser.cs | 38 + .../AliasServerDbContextModelSnapshot.cs | 4 + .../WebApi/Auth/LoginInitiateResponse.cs | 12 +- .../Models/WebApi/Auth/RegisterRequest.cs | 11 +- .../PasswordChangeInitiateResponse.cs | 13 +- core/models/src/webapi/Login.ts | 1 + .../webapi/PasswordChangeInitiateResponse.ts | 1 + 20 files changed, 1193 insertions(+), 42 deletions(-) create mode 100644 apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.Designer.cs create mode 100644 apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.cs diff --git a/apps/browser-extension/src/utils/auth/SrpAuthService.ts b/apps/browser-extension/src/utils/auth/SrpAuthService.ts index 787a2d9b3..6ed53c404 100644 --- a/apps/browser-extension/src/utils/auth/SrpAuthService.ts +++ b/apps/browser-extension/src/utils/auth/SrpAuthService.ts @@ -13,6 +13,8 @@ export type RegisterRequest = { verifier: string; encryptionType: string; encryptionSettings: string; + /** The SRP identity used for authentication (a random GUID generated at registration). */ + srpIdentity: string; }; /** @@ -193,12 +195,22 @@ export class SrpAuthService { }; } + /** + * Generates a random UUID v4 for use as SRP identity. + * + * @returns A random UUID string + */ + public static generateSrpIdentity(): string { + return crypto.randomUUID(); + } + /** * Prepares SRP registration data for a new user. * * This generates all the cryptographic values needed to register a user: * - Salt for key derivation * - Verifier for SRP authentication + * - SRP identity (random GUID) for immutable authentication identity * * @param username - The username for registration * @param password - The password for registration @@ -211,6 +223,12 @@ export class SrpAuthService { const normalizedUsername = SrpAuthService.normalizeUsername(username); const salt = SrpAuthService.generateSalt(); + /** + * Generate a random GUID for SRP identity. This is used for all SRP operations, + * is set during registration, and never changes. + */ + const srpIdentity = SrpAuthService.generateSrpIdentity(); + // Derive key from password using default Argon2Id settings const credentials = await SrpAuthService.prepareCredentials( password, @@ -219,8 +237,8 @@ export class SrpAuthService { DEFAULT_ENCRYPTION.settings ); - // Generate SRP private key and verifier - const privateKey = SrpAuthService.derivePrivateKey(salt, normalizedUsername, credentials.passwordHashString); + // Generate SRP private key and verifier using srpIdentity (not username) + const privateKey = SrpAuthService.derivePrivateKey(salt, srpIdentity, credentials.passwordHashString); const verifier = SrpAuthService.deriveVerifier(privateKey); return { @@ -229,6 +247,7 @@ export class SrpAuthService { verifier, encryptionType: DEFAULT_ENCRYPTION.type, encryptionSettings: DEFAULT_ENCRYPTION.settings, + srpIdentity, }; } @@ -328,6 +347,12 @@ export class SrpAuthService { const loginResponse = (await initiateResponse.json()) as LoginResponse; + /** + * Use srpIdentity from server response if available, otherwise fall back to normalized username. + * Note: the fallback can be removed in the future after 0.26.0+ is deployed. + */ + const srpIdentity = loginResponse.srpIdentity ?? normalizedUsername; + // Step 2: Prepare credentials const credentials = await SrpAuthService.prepareCredentials( password, @@ -336,18 +361,18 @@ export class SrpAuthService { loginResponse.encryptionSettings ); - // Step 3: Generate SRP session + // Step 3: Generate SRP session using srpIdentity (not the typed username) const clientEphemeral = SrpAuthService.generateEphemeral(); const privateKey = SrpAuthService.derivePrivateKey( loginResponse.salt, - normalizedUsername, + srpIdentity, credentials.passwordHashString ); const session = SrpAuthService.deriveSession( clientEphemeral.secret, loginResponse.serverEphemeral, loginResponse.salt, - normalizedUsername, + srpIdentity, privateKey ); diff --git a/apps/browser-extension/src/utils/dist/core/models/webapi/index.d.ts b/apps/browser-extension/src/utils/dist/core/models/webapi/index.d.ts index 46c8a7754..f25577f53 100644 --- a/apps/browser-extension/src/utils/dist/core/models/webapi/index.d.ts +++ b/apps/browser-extension/src/utils/dist/core/models/webapi/index.d.ts @@ -82,6 +82,7 @@ type LoginResponse = { serverEphemeral: string; encryptionType: string; encryptionSettings: string; + srpIdentity?: string; }; /** @@ -318,6 +319,7 @@ type PasswordChangeInitiateResponse = { serverEphemeral: string; encryptionType: string; encryptionSettings: string; + srpIdentity?: string; }; /** diff --git a/apps/mobile-app/utils/SrpUtility.tsx b/apps/mobile-app/utils/SrpUtility.tsx index 7780e2b49..793cd07d7 100644 --- a/apps/mobile-app/utils/SrpUtility.tsx +++ b/apps/mobile-app/utils/SrpUtility.tsx @@ -49,18 +49,19 @@ export class SrpUtility { rememberMe: boolean, loginResponse: LoginResponse ): Promise { - // Generate client ephemeral + /** + * Use srpIdentity from server response if available, otherwise fall back to username. + * Note: the fallback can be removed in the future after 0.26.0+ is deployed. + */ + const srpIdentity = loginResponse.srpIdentity ?? username; + const clientEphemeral = srp.generateEphemeral(); - - // Derive private key - const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHash); - - // Derive session + const privateKey = srp.derivePrivateKey(loginResponse.salt, srpIdentity, passwordHash); const sessionProof = srp.deriveSession( clientEphemeral.secret, loginResponse.serverEphemeral, loginResponse.salt, - username, + srpIdentity, privateKey ); @@ -98,18 +99,19 @@ export class SrpUtility { loginResponse: LoginResponse, twoFactorCode: number ): Promise { - // Generate client ephemeral + /** + * Use srpIdentity from server response if available, otherwise fall back to username. + * Note: the fallback can be removed in the future after 0.26.0+ is deployed. + */ + const srpIdentity = loginResponse.srpIdentity ?? username; + const clientEphemeral = srp.generateEphemeral(); - - // Derive private key - const privateKey = srp.derivePrivateKey(loginResponse.salt, username, passwordHash); - - // Derive session + const privateKey = srp.derivePrivateKey(loginResponse.salt, srpIdentity, passwordHash); const sessionProof = srp.deriveSession( clientEphemeral.secret, loginResponse.serverEphemeral, loginResponse.salt, - username, + srpIdentity, privateKey ); diff --git a/apps/mobile-app/utils/dist/core/models/webapi/index.d.ts b/apps/mobile-app/utils/dist/core/models/webapi/index.d.ts index 46c8a7754..f25577f53 100644 --- a/apps/mobile-app/utils/dist/core/models/webapi/index.d.ts +++ b/apps/mobile-app/utils/dist/core/models/webapi/index.d.ts @@ -82,6 +82,7 @@ type LoginResponse = { serverEphemeral: string; encryptionType: string; encryptionSettings: string; + srpIdentity?: string; }; /** @@ -318,6 +319,7 @@ type PasswordChangeInitiateResponse = { serverEphemeral: string; encryptionType: string; encryptionSettings: string; + srpIdentity?: string; }; /** diff --git a/apps/server/AliasVault.Api/AliasVault.Api.csproj b/apps/server/AliasVault.Api/AliasVault.Api.csproj index eb548fb5b..cae16f559 100644 --- a/apps/server/AliasVault.Api/AliasVault.Api.csproj +++ b/apps/server/AliasVault.Api/AliasVault.Api.csproj @@ -23,6 +23,10 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/apps/server/AliasVault.Api/Controllers/AuthController.cs b/apps/server/AliasVault.Api/Controllers/AuthController.cs index 75b59b847..4502f91b5 100644 --- a/apps/server/AliasVault.Api/Controllers/AuthController.cs +++ b/apps/server/AliasVault.Api/Controllers/AuthController.cs @@ -166,13 +166,17 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM // Retrieve latest vault of user which contains the current salt and verifier. var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user); + // Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase). + var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant(); + // Server creates ephemeral and sends to client var ephemeral = Srp.GenerateEphemeralServer(latestVaultEncryptionSettings.Verifier); // Store the server ephemeral in memory cache for Validate() endpoint to use. - cache.Set(AuthHelper.CachePrefixEphemeral + model.Username, ephemeral.Secret, TimeSpan.FromMinutes(5)); + // Use SrpIdentity as the cache key to ensure consistency. + cache.Set(AuthHelper.CachePrefixEphemeral + srpIdentity, ephemeral.Secret, TimeSpan.FromMinutes(5)); - return Ok(new LoginInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings)); + return Ok(new LoginInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings, srpIdentity)); } /// @@ -416,9 +420,14 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM return BadRequest(ApiErrorCodeHelper.CreateValidationErrorResponse(apiErrorCode, 400)); } + // Use the SrpIdentity from the request if provided (typically a GUID generated by the client), + // otherwise fall back to lowercase username for backward compatibility. + var srpIdentity = model.SrpIdentity ?? model.Username.ToLowerInvariant(); + var user = new AliasVaultUser { UserName = model.Username, + SrpIdentity = srpIdentity, CreatedAt = timeProvider.UtcNow, UpdatedAt = timeProvider.UtcNow, PasswordChangedAt = timeProvider.UtcNow, @@ -476,13 +485,17 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM // Retrieve latest vault of user which contains the current salt and verifier. var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user); + // Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase). + var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant(); + // Server creates ephemeral and sends to client var ephemeral = Srp.GenerateEphemeralServer(latestVaultEncryptionSettings.Verifier); // Store the server ephemeral in memory cache for the Vault update (and set new password) endpoint to use. - cache.Set(AuthHelper.CachePrefixEphemeral + user.UserName!, ephemeral.Secret, TimeSpan.FromMinutes(5)); + // Use SrpIdentity as the cache key to ensure consistency. + cache.Set(AuthHelper.CachePrefixEphemeral + srpIdentity, ephemeral.Secret, TimeSpan.FromMinutes(5)); - return Ok(new PasswordChangeInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings)); + return Ok(new PasswordChangeInitiateResponse(latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, latestVaultEncryptionSettings.EncryptionSettings, srpIdentity)); } /// @@ -542,17 +555,22 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM // Retrieve latest vault of user which contains the current salt and verifier. var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user); + // Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase). + var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant(); + // Server creates ephemeral and sends to client var ephemeral = Srp.GenerateEphemeralServer(latestVaultEncryptionSettings.Verifier); // Store the server ephemeral in memory cache for confirmation endpoint. - cache.Set(AuthHelper.CachePrefixEphemeral + model.Username, ephemeral.Secret, TimeSpan.FromMinutes(5)); + // Use SrpIdentity as the cache key to ensure consistency. + cache.Set(AuthHelper.CachePrefixEphemeral + srpIdentity, ephemeral.Secret, TimeSpan.FromMinutes(5)); return Ok(new LoginInitiateResponse( latestVaultEncryptionSettings.Salt, ephemeral.Public, latestVaultEncryptionSettings.EncryptionType, - latestVaultEncryptionSettings.EncryptionSettings)); + latestVaultEncryptionSettings.EncryptionSettings, + srpIdentity)); } /// diff --git a/apps/server/AliasVault.Api/Helpers/AuthHelper.cs b/apps/server/AliasVault.Api/Helpers/AuthHelper.cs index 180e6da3b..6123c6a48 100644 --- a/apps/server/AliasVault.Api/Helpers/AuthHelper.cs +++ b/apps/server/AliasVault.Api/Helpers/AuthHelper.cs @@ -28,16 +28,19 @@ public static class AuthHelper public static readonly string CachePrefixFakeData = "FakeData_"; /// - /// Helper method that validates the SRP session based on provided username, ephemeral and proof. + /// Helper method that validates the SRP session based on provided SRP identity, ephemeral and proof. /// /// IMemoryCache instance. /// The user object. /// The client ephemeral value. /// The client session proof. - /// Tuple. + /// SrpSession if validation succeeds, null otherwise. public static SrpSession? ValidateSrpSession(IMemoryCache cache, AliasVaultUser user, string clientEphemeral, string clientSessionProof) { - if (!cache.TryGetValue(CachePrefixEphemeral + user.UserName, out var serverSecretEphemeral) || serverSecretEphemeral is not string) + // Get or create SRP identity. For existing users without SrpIdentity, fall back to username (lowercase). + var srpIdentity = user.SrpIdentity ?? user.UserName!.ToLowerInvariant(); + + if (!cache.TryGetValue(CachePrefixEphemeral + srpIdentity, out var serverSecretEphemeral) || serverSecretEphemeral is not string) { return null; } @@ -45,11 +48,13 @@ public static class AuthHelper // Retrieve latest vault of user which contains the current salt and verifier. var latestVaultEncryptionSettings = GetUserLatestVaultEncryptionSettings(user); + // Use SrpIdentity for the SRP session derivation. This is the fixed identity that was used + // when the verifier was originally created, ensuring username changes don't break authentication. var serverSession = Srp.DeriveSessionServer( serverSecretEphemeral.ToString() ?? string.Empty, clientEphemeral, latestVaultEncryptionSettings.Salt, - user.UserName ?? string.Empty, + srpIdentity, latestVaultEncryptionSettings.Verifier, clientSessionProof); diff --git a/apps/server/AliasVault.Client/Auth/Pages/Login.razor b/apps/server/AliasVault.Client/Auth/Pages/Login.razor index f7ca8fcb8..d977d6d8f 100644 --- a/apps/server/AliasVault.Client/Auth/Pages/Login.razor +++ b/apps/server/AliasVault.Client/Auth/Pages/Login.razor @@ -263,18 +263,22 @@ else ]; } + // Use srpIdentity from server response if available, otherwise fall back to username. + // Note: the fallback can be removed in the future after 0.26.0+ is deployed. + var srpIdentity = loginResponse.SrpIdentity ?? username; + // 3. Client derives shared session key. _passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_loginModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings); var passwordHashString = BitConverter.ToString(_passwordHash).Replace("-", string.Empty); _clientEphemeral = Srp.GenerateEphemeralClient(); - var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString); + var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, srpIdentity, passwordHashString); _clientSession = Srp.DeriveSessionClient( privateKey, _clientEphemeral.Secret, loginResponse.ServerEphemeral, loginResponse.Salt, - username); + srpIdentity); // 4. Client sends proof of session key to server. result = await Http.PostAsJsonAsync("v1/Auth/validate", new ValidateLoginRequest(username, _loginModel.RememberMe, _clientEphemeral.Public, _clientSession.Proof)); 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 fde6e4ac6..72463daf4 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/ChangePassword.razor @@ -93,6 +93,12 @@ else /// private string CurrentEncryptionSettings { get; set; } = string.Empty; + /// + /// Gets or sets the SRP identity for authentication. This is a fixed value that doesn't change + /// even if the display username is updated. + /// + private string? CurrentSrpIdentity { get; set; } + private SrpEphemeral ClientEphemeral = new(); private SrpSession ClientSession = new(); @@ -141,6 +147,7 @@ else CurrentSalt = response.Salt; CurrentEncryptionType = response.EncryptionType; CurrentEncryptionSettings = response.EncryptionSettings; + CurrentSrpIdentity = response.SrpIdentity; } /// @@ -158,13 +165,18 @@ else ClientEphemeral = Srp.GenerateEphemeralClient(); var username = await GetUsernameAsync(); - var privateKey = Srp.DerivePrivateKey(CurrentSalt, username, currentPasswordHashString); + + // Use srpIdentity from server response if available, otherwise fall back to username. + // Note: the fallback can be removed in the future after 0.26.0+ is deployed. + var srpIdentity = CurrentSrpIdentity ?? username.ToLowerInvariant(); + + var privateKey = Srp.DerivePrivateKey(CurrentSalt, srpIdentity, currentPasswordHashString); ClientSession = Srp.DeriveSessionClient( privateKey, ClientEphemeral.Secret, CurrentServerEphemeral, CurrentSalt, - username); + srpIdentity); // Generate salt and verifier for new password. var client = new SrpClient(); @@ -180,7 +192,8 @@ else // it is encrypted with the new password hash. await AuthService.StoreEncryptionKeyAsync(newPasswordHash); - var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, username, newPasswordHashString); + // Use srpIdentity for generating the new verifier to maintain consistency. + var srpPasswordChange = Srp.PasswordChangeAsync(client, newSalt, srpIdentity, newPasswordHashString); // Prepare new vault model to update to. var encryptedBase64String = await DbService.GetEncryptedDatabaseBase64String(); diff --git a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor index dabf0441a..6372a8a40 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Settings/Security/DeleteAccount.razor @@ -171,18 +171,22 @@ return; } + // Use srpIdentity from server response if available, otherwise fall back to username. + // Note: the fallback can be removed in the future after 0.27.0+ is deployed. + var srpIdentity = loginResponse.SrpIdentity ?? username; + // Verify password using SRP var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(_passwordModel.Password, loginResponse.Salt, loginResponse.EncryptionType, loginResponse.EncryptionSettings); var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty); ClientEphemeral = Srp.GenerateEphemeralClient(); - var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, username, passwordHashString); + var privateKey = Srp.DerivePrivateKey(loginResponse.Salt, srpIdentity, passwordHashString); ClientSession = Srp.DeriveSessionClient( privateKey, ClientEphemeral.Secret, loginResponse.ServerEphemeral, loginResponse.Salt, - username); + srpIdentity); // Send final delete request with SRP proof. result = await Http.PostAsJsonAsync("v1/Auth/delete-account/confirm", new DeleteAccountRequest(username, ClientEphemeral.Public, ClientSession.Proof)); diff --git a/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs b/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs index 406ea5f40..2c851f5a5 100644 --- a/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs +++ b/apps/server/AliasVault.Client/Services/Auth/UserRegistrationService.cs @@ -41,6 +41,10 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP var client = new SrpClient(); var salt = client.GenerateSalt(); + // Generate a random GUID for SRP identity. This is used for all SRP operations, + // is set during registration, and never changes. + var srpIdentity = Guid.NewGuid().ToString(); + string encryptionType = Defaults.EncryptionType; string encryptionSettings = Defaults.EncryptionSettings; if (config.CryptographyOverrideType is not null && config.CryptographyOverrideSettings is not null) @@ -51,9 +55,9 @@ public class UserRegistrationService(HttpClient httpClient, AuthenticationStateP var passwordHash = await Encryption.DeriveKeyFromPasswordAsync(password, salt, encryptionType, encryptionSettings); var passwordHashString = BitConverter.ToString(passwordHash).Replace("-", string.Empty); - var srpSignup = Srp.PasswordChangeAsync(client, salt, username, passwordHashString); + var srpSignup = Srp.PasswordChangeAsync(client, salt, srpIdentity, passwordHashString); - var registerRequest = new RegisterRequest(srpSignup.Username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings); + var registerRequest = new RegisterRequest(username, srpSignup.Salt, srpSignup.Verifier, encryptionType, encryptionSettings, srpIdentity); var result = await httpClient.PostAsJsonAsync("v1/Auth/register", registerRequest); var responseContent = await result.Content.ReadAsStringAsync(); diff --git a/apps/server/Databases/AliasServerDb/AliasVaultUser.cs b/apps/server/Databases/AliasServerDb/AliasVaultUser.cs index 4ec09ceb6..0fc52d966 100644 --- a/apps/server/Databases/AliasServerDb/AliasVaultUser.cs +++ b/apps/server/Databases/AliasServerDb/AliasVaultUser.cs @@ -14,6 +14,13 @@ using Microsoft.AspNetCore.Identity; /// public class AliasVaultUser : IdentityUser { + /// + /// Gets or sets the SRP identity used for authentication. This is a fixed value (typically a random GUID) + /// that is used for all SRP operations, is set during registration, and never changes. + /// + [System.ComponentModel.DataAnnotations.StringLength(255)] + public string? SrpIdentity { get; set; } + /// /// Gets or sets created timestamp. /// diff --git a/apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.Designer.cs b/apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.Designer.cs new file mode 100644 index 000000000..7bf7bc8f5 --- /dev/null +++ b/apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.Designer.cs @@ -0,0 +1,989 @@ +// +using System; +using AliasServerDb; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace AliasServerDb.Migrations +{ + [DbContext(typeof(AliasServerDbContext))] + [Migration("20251211120421_AddSrpIdentityToUser")] + partial class AddSrpIdentityToUser + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.4") + .HasAnnotation("Proxies:ChangeTracking", false) + .HasAnnotation("Proxies:CheckEquality", false) + .HasAnnotation("Proxies:LazyLoading", true) + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("AliasServerDb.AdminRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AdminRoles"); + }); + + modelBuilder.Entity("AliasServerDb.AdminUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LastPasswordChanged") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AdminUsers"); + }); + + modelBuilder.Entity("AliasServerDb.AliasVaultRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("NormalizedName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AliasVaultRoles"); + }); + + modelBuilder.Entity("AliasServerDb.AliasVaultUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("Blocked") + .HasColumnType("boolean"); + + b.Property("ConcurrencyStamp") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("EmailsReceived") + .HasColumnType("integer"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxEmailAgeDays") + .HasColumnType("integer"); + + b.Property("MaxEmails") + .HasColumnType("integer"); + + b.Property("NormalizedEmail") + .HasColumnType("text"); + + b.Property("NormalizedUserName") + .HasColumnType("text"); + + b.Property("PasswordChangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("SrpIdentity") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserName") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("AliasVaultUsers"); + }); + + modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceIdentifier") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpireDate") + .HasMaxLength(255) + .HasColumnType("timestamp with time zone"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("PreviousTokenValue") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AliasVaultUserRefreshTokens"); + }); + + modelBuilder.Entity("AliasServerDb.AuthLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalInfo") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("Browser") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Client") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DeviceType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("FailureReason") + .HasColumnType("integer"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsSuccess") + .HasColumnType("boolean"); + + b.Property("IsSuspiciousActivity") + .HasColumnType("boolean"); + + b.Property("OperatingSystem") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequestPath") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("Username") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "EventType" }, "IX_EventType"); + + b.HasIndex(new[] { "IpAddress" }, "IX_IpAddress"); + + b.HasIndex(new[] { "Timestamp" }, "IX_Timestamp"); + + b.HasIndex(new[] { "Username", "IsSuccess", "Timestamp" }, "IX_Username_IsSuccess_Timestamp") + .IsDescending(false, false, true); + + b.HasIndex(new[] { "Username", "Timestamp" }, "IX_Username_Timestamp") + .IsDescending(false, true); + + b.ToTable("AuthLogs"); + }); + + modelBuilder.Entity("AliasServerDb.Email", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("DateSystem") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedSymmetricKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("From") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromDomain") + .IsRequired() + .HasColumnType("text"); + + b.Property("FromLocal") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageHtml") + .HasColumnType("text"); + + b.Property("MessagePlain") + .HasColumnType("text"); + + b.Property("MessagePreview") + .HasColumnType("text"); + + b.Property("MessageSource") + .IsRequired() + .HasColumnType("text"); + + b.Property("PushNotificationSent") + .HasColumnType("boolean"); + + b.Property("Subject") + .IsRequired() + .HasColumnType("text"); + + b.Property("To") + .IsRequired() + .HasColumnType("text"); + + b.Property("ToDomain") + .IsRequired() + .HasColumnType("text"); + + b.Property("ToLocal") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserEncryptionKeyId") + .HasMaxLength(255) + .HasColumnType("uuid"); + + b.Property("Visible") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Date"); + + b.HasIndex("DateSystem"); + + b.HasIndex("PushNotificationSent"); + + b.HasIndex("ToLocal"); + + b.HasIndex("UserEncryptionKeyId"); + + b.HasIndex("Visible"); + + b.ToTable("Emails"); + }); + + modelBuilder.Entity("AliasServerDb.EmailAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Bytes") + .IsRequired() + .HasColumnType("bytea"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("EmailId") + .HasColumnType("integer"); + + b.Property("Filename") + .IsRequired() + .HasColumnType("text"); + + b.Property("Filesize") + .HasColumnType("integer"); + + b.Property("MimeType") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("EmailId"); + + b.ToTable("EmailAttachments"); + }); + + modelBuilder.Entity("AliasServerDb.Log", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Application") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Exception") + .IsRequired() + .HasColumnType("text"); + + b.Property("Level") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("LogEvent") + .IsRequired() + .HasColumnType("text") + .HasColumnName("LogEvent"); + + b.Property("Message") + .IsRequired() + .HasColumnType("text"); + + b.Property("MessageTemplate") + .IsRequired() + .HasColumnType("text"); + + b.Property("Properties") + .IsRequired() + .HasColumnType("text"); + + b.Property("SourceContext") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("TimeStamp") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Application"); + + b.HasIndex("TimeStamp"); + + b.ToTable("Logs", (string)null); + }); + + modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ClearedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ClientIpAddress") + .HasColumnType("text"); + + b.Property("ClientPublicKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedDecryptionKey") + .HasColumnType("text"); + + b.Property("FulfilledAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MobileIpAddress") + .HasColumnType("text"); + + b.Property("RetrievedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "ClientIpAddress" }, "IX_ClientIpAddress"); + + b.HasIndex(new[] { "CreatedAt" }, "IX_CreatedAt"); + + b.HasIndex(new[] { "MobileIpAddress" }, "IX_MobileIpAddress"); + + b.HasIndex(new[] { "RetrievedAt", "ClearedAt", "FulfilledAt" }, "IX_RetrievedAt_ClearedAt_FulfilledAt"); + + b.HasIndex(new[] { "UserId" }, "IX_UserId"); + + b.ToTable("MobileLoginRequests"); + }); + + modelBuilder.Entity("AliasServerDb.ServerSetting", b => + { + b.Property("Key") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Key"); + + b.ToTable("ServerSettings"); + }); + + modelBuilder.Entity("AliasServerDb.TaskRunnerJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("EndTime") + .HasColumnType("time without time zone"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("IsOnDemand") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartTime") + .HasColumnType("time without time zone"); + + b.Property("Status") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("TaskRunnerJobs"); + }); + + modelBuilder.Entity("AliasServerDb.UserEmailClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Address") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AddressDomain") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("AddressLocal") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("Address") + .IsUnique(); + + b.HasIndex("UserId", "Disabled"); + + b.ToTable("UserEmailClaims"); + }); + + modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("PublicKey") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserEncryptionKeys"); + }); + + modelBuilder.Entity("AliasServerDb.Vault", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Client") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialsCount") + .HasColumnType("integer"); + + b.Property("EmailClaimsCount") + .HasColumnType("integer"); + + b.Property("EncryptionSettings") + .IsRequired() + .HasColumnType("text"); + + b.Property("EncryptionType") + .IsRequired() + .HasColumnType("text"); + + b.Property("FileSize") + .HasColumnType("integer"); + + b.Property("RevisionNumber") + .HasColumnType("bigint"); + + b.Property("Salt") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("VaultBlob") + .IsRequired() + .HasColumnType("text"); + + b.Property("Verifier") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("Version") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Vaults"); + }); + + modelBuilder.Entity("AliasVault.WorkerStatus.Database.WorkerServiceStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CurrentStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("DesiredStatus") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Heartbeat") + .HasColumnType("timestamp with time zone"); + + b.Property("ServiceName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.HasKey("Id"); + + b.ToTable("WorkerServiceStatuses"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("FriendlyName") + .HasColumnType("text"); + + b.Property("Xml") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("ProviderKey") + .HasColumnType("text"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("AliasServerDb.AliasVaultUserRefreshToken", b => + { + b.HasOne("AliasServerDb.AliasVaultUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AliasServerDb.Email", b => + { + b.HasOne("AliasServerDb.UserEncryptionKey", "EncryptionKey") + .WithMany("Emails") + .HasForeignKey("UserEncryptionKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EncryptionKey"); + }); + + modelBuilder.Entity("AliasServerDb.EmailAttachment", b => + { + b.HasOne("AliasServerDb.Email", "Email") + .WithMany("Attachments") + .HasForeignKey("EmailId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Email"); + }); + + modelBuilder.Entity("AliasServerDb.MobileLoginRequest", b => + { + b.HasOne("AliasServerDb.AliasVaultUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AliasServerDb.UserEmailClaim", b => + { + b.HasOne("AliasServerDb.AliasVaultUser", "User") + .WithMany("EmailClaims") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b => + { + b.HasOne("AliasServerDb.AliasVaultUser", "User") + .WithMany("EncryptionKeys") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AliasServerDb.Vault", b => + { + b.HasOne("AliasServerDb.AliasVaultUser", "User") + .WithMany("Vaults") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("AliasServerDb.AliasVaultUser", b => + { + b.Navigation("EmailClaims"); + + b.Navigation("EncryptionKeys"); + + b.Navigation("Vaults"); + }); + + modelBuilder.Entity("AliasServerDb.Email", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("AliasServerDb.UserEncryptionKey", b => + { + b.Navigation("Emails"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.cs b/apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.cs new file mode 100644 index 000000000..0171ea96d --- /dev/null +++ b/apps/server/Databases/AliasServerDb/Migrations/20251211120421_AddSrpIdentityToUser.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace AliasServerDb.Migrations +{ + /// + public partial class AddSrpIdentityToUser : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "SrpIdentity", + table: "AliasVaultUsers", + type: "character varying(255)", + maxLength: 255, + nullable: true); + + // Populate SrpIdentity for existing users using their current username which was + // used before this migration was applied. All newly registered users will have + // this field populated with a random GUID generated by the client upon registration. + migrationBuilder.Sql(@" + UPDATE ""AliasVaultUsers"" + SET ""SrpIdentity"" = LOWER(""UserName"") + WHERE ""SrpIdentity"" IS NULL; + "); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "SrpIdentity", + table: "AliasVaultUsers"); + } + } +} diff --git a/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs b/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs index 81f0da260..397ec6651 100644 --- a/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs +++ b/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs @@ -180,6 +180,10 @@ namespace AliasServerDb.Migrations b.Property("SecurityStamp") .HasColumnType("text"); + b.Property("SrpIdentity") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + b.Property("TwoFactorEnabled") .HasColumnType("boolean"); diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/LoginInitiateResponse.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/LoginInitiateResponse.cs index fe3b49f35..e16890bdc 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/LoginInitiateResponse.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/LoginInitiateResponse.cs @@ -21,12 +21,14 @@ public class LoginInitiateResponse /// Server ephemeral. /// Encryption type. /// Encryption settings. - public LoginInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings) + /// The SRP identity to use for authentication (optional for backward compatibility). + public LoginInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings, string? srpIdentity = null) { Salt = salt; ServerEphemeral = serverEphemeral; EncryptionType = encryptionType; EncryptionSettings = encryptionSettings; + SrpIdentity = srpIdentity; } /// @@ -52,4 +54,12 @@ public class LoginInitiateResponse /// [JsonPropertyName("encryptionSettings")] public string EncryptionSettings { get; set; } + + /// + /// Gets or sets the SRP identity to use for authentication. This is a fixed value that doesn't change + /// even if the display username is updated. Clients should use this value (instead of the typed username) + /// for all SRP operations. + /// + [JsonPropertyName("srpIdentity")] + public string? SrpIdentity { get; set; } } diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterRequest.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterRequest.cs index c2dab85ee..5ca5807c9 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterRequest.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/RegisterRequest.cs @@ -20,13 +20,15 @@ public class RegisterRequest /// The verifier value. /// The encryption type. /// The encryption settings. - public RegisterRequest(string username, string salt, string verifier, string encryptionType, string encryptionSettings) + /// The SRP identity. + public RegisterRequest(string username, string salt, string verifier, string encryptionType, string encryptionSettings, string? srpIdentity = null) { Username = username.ToLowerInvariant().Trim(); Salt = salt; Verifier = verifier; EncryptionType = encryptionType; EncryptionSettings = encryptionSettings; + SrpIdentity = srpIdentity; } /// @@ -53,4 +55,11 @@ public class RegisterRequest /// Gets the encryption settings. /// public string EncryptionSettings { get; } + + /// + /// Gets the SRP identity used for authentication. This is a fixed value (typically a GUID) that + /// is used for all SRP operations. If not provided, defaults to the lowercase username for + /// backward compatibility. + /// + public string? SrpIdentity { get; } } diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeInitiateResponse.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeInitiateResponse.cs index 8252c0f7e..e6a68d8ef 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeInitiateResponse.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/PasswordChange/PasswordChangeInitiateResponse.cs @@ -10,7 +10,7 @@ namespace AliasVault.Shared.Models.WebApi.PasswordChange; using System.Text.Json.Serialization; /// -/// Represents a login response. +/// Represents a password change initiate response. /// public class PasswordChangeInitiateResponse { @@ -21,12 +21,14 @@ public class PasswordChangeInitiateResponse /// Server ephemeral. /// Encryption type. /// Encryption settings. - public PasswordChangeInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings) + /// The SRP identity. + public PasswordChangeInitiateResponse(string salt, string serverEphemeral, string encryptionType, string encryptionSettings, string? srpIdentity = null) { Salt = salt; ServerEphemeral = serverEphemeral; EncryptionType = encryptionType; EncryptionSettings = encryptionSettings; + SrpIdentity = srpIdentity; } /// @@ -52,4 +54,11 @@ public class PasswordChangeInitiateResponse /// [JsonPropertyName("encryptionSettings")] public string EncryptionSettings { get; set; } + + /// + /// Gets or sets the SRP identity to use for authentication. This is a fixed value that doesn't change + /// even if the display username is updated. Clients should use this value for all SRP operations. + /// + [JsonPropertyName("srpIdentity")] + public string? SrpIdentity { get; set; } } diff --git a/core/models/src/webapi/Login.ts b/core/models/src/webapi/Login.ts index 40a0822d8..8c00de2a4 100644 --- a/core/models/src/webapi/Login.ts +++ b/core/models/src/webapi/Login.ts @@ -13,4 +13,5 @@ export type LoginResponse = { serverEphemeral: string; encryptionType: string; encryptionSettings: string; + srpIdentity?: string; } diff --git a/core/models/src/webapi/PasswordChangeInitiateResponse.ts b/core/models/src/webapi/PasswordChangeInitiateResponse.ts index a217476d0..50f4be59f 100644 --- a/core/models/src/webapi/PasswordChangeInitiateResponse.ts +++ b/core/models/src/webapi/PasswordChangeInitiateResponse.ts @@ -6,4 +6,5 @@ export type PasswordChangeInitiateResponse = { serverEphemeral: string; encryptionType: string; encryptionSettings: string; + srpIdentity?: string; };