diff --git a/apps/server/AliasVault.Admin/Main/Models/MobileLoginRequestWithUsername.cs b/apps/server/AliasVault.Admin/Main/Models/MobileLoginRequestWithUsername.cs new file mode 100644 index 000000000..460237926 --- /dev/null +++ b/apps/server/AliasVault.Admin/Main/Models/MobileLoginRequestWithUsername.cs @@ -0,0 +1,26 @@ +//----------------------------------------------------------------------- +// +// Copyright (c) aliasvault. All rights reserved. +// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information. +// +//----------------------------------------------------------------------- + +namespace AliasVault.Admin.Main.Models; + +using AliasServerDb; + +/// +/// View model for MobileLoginRequest joined with User to get username. +/// +public class MobileLoginRequestWithUsername +{ + /// + /// Gets or sets the mobile login request. + /// + public required MobileLoginRequest Request { get; set; } + + /// + /// Gets or sets the username from the User table via UserId FK. + /// + public string? Username { get; set; } +} diff --git a/apps/server/AliasVault.Admin/Main/Pages/MobileLoginHistory.razor b/apps/server/AliasVault.Admin/Main/Pages/MobileLoginHistory.razor index 040e0c542..18044ea16 100644 --- a/apps/server/AliasVault.Admin/Main/Pages/MobileLoginHistory.razor +++ b/apps/server/AliasVault.Admin/Main/Pages/MobileLoginHistory.razor @@ -1,4 +1,5 @@ @page "/mobile-login-history" +@using AliasVault.Admin.Main.Models @using AliasVault.RazorComponents.Tables @using Microsoft.AspNetCore.WebUtilities @inject NavigationManager NavigationManager @@ -91,15 +92,15 @@ else @if (request.RetrievedAt.HasValue) { - + } else if (request.FulfilledAt.HasValue) { - + } else { - + } @@ -209,7 +210,16 @@ else StateHasChanged(); await using var dbContext = await DbContextFactory.CreateDbContextAsync(cancellationToken); - IQueryable query = dbContext.MobileLoginRequests; + + // Join with AliasVaultUsers table to get username via UserId FK + var query = from request in dbContext.MobileLoginRequests + join user in dbContext.AliasVaultUsers on request.UserId equals user.Id into userJoin + from user in userJoin.DefaultIfEmpty() + select new MobileLoginRequestWithUsername + { + Request = request, + Username = user != null ? user.UserName : null + }; query = ApplySearchFilter(query); query = ApplyStatusFilter(query); @@ -221,13 +231,13 @@ else .Take(PageSize) .Select(r => new { - r.CreatedAt, - r.ClientIpAddress, - r.MobileIpAddress, - r.FulfilledAt, - r.RetrievedAt, + r.Request.CreatedAt, + r.Request.ClientIpAddress, + r.Request.MobileIpAddress, + r.Request.FulfilledAt, + r.Request.RetrievedAt, r.Username, - r.UserId + r.Request.UserId }) .ToListAsync(cancellationToken); @@ -257,7 +267,7 @@ else } } - private IQueryable ApplySearchFilter(IQueryable query) + private IQueryable ApplySearchFilter(IQueryable query) { if (SearchTerm.Length > 0) { @@ -271,15 +281,15 @@ else var searchTerm = SearchTerm.Trim().ToLower(); query = query.Where(r => (r.Username != null && EF.Functions.Like(r.Username.ToLower(), "%" + searchTerm + "%")) || - (r.ClientIpAddress != null && EF.Functions.Like(r.ClientIpAddress.ToLower(), "%" + searchTerm + "%")) || - (r.MobileIpAddress != null && EF.Functions.Like(r.MobileIpAddress.ToLower(), "%" + searchTerm + "%")) + (r.Request.ClientIpAddress != null && EF.Functions.Like(r.Request.ClientIpAddress.ToLower(), "%" + searchTerm + "%")) || + (r.Request.MobileIpAddress != null && EF.Functions.Like(r.Request.MobileIpAddress.ToLower(), "%" + searchTerm + "%")) ); } return query; } - private IQueryable ApplyStatusFilter(IQueryable query) + private IQueryable ApplyStatusFilter(IQueryable query) { if (!string.IsNullOrEmpty(SelectedStatusFilter)) { @@ -293,13 +303,13 @@ else switch (SelectedStatusFilter) { case "retrieved": - query = query.Where(r => r.RetrievedAt != null); + query = query.Where(r => r.Request.RetrievedAt != null); break; case "fulfilled": - query = query.Where(r => r.FulfilledAt != null && r.RetrievedAt == null); + query = query.Where(r => r.Request.FulfilledAt != null && r.Request.RetrievedAt == null); break; case "pending": - query = query.Where(r => r.FulfilledAt == null); + query = query.Where(r => r.Request.FulfilledAt == null); break; } } @@ -307,34 +317,34 @@ else return query; } - private IQueryable ApplySort(IQueryable query) + private IQueryable ApplySort(IQueryable query) { switch (SortColumn) { case "CreatedAt": query = SortDirection == SortDirection.Ascending - ? query.OrderBy(x => x.CreatedAt) - : query.OrderByDescending(x => x.CreatedAt); + ? query.OrderBy(x => x.Request.CreatedAt) + : query.OrderByDescending(x => x.Request.CreatedAt); break; case "ClientIpAddress": query = SortDirection == SortDirection.Ascending - ? query.OrderBy(x => x.ClientIpAddress) - : query.OrderByDescending(x => x.ClientIpAddress); + ? query.OrderBy(x => x.Request.ClientIpAddress) + : query.OrderByDescending(x => x.Request.ClientIpAddress); break; case "MobileIpAddress": query = SortDirection == SortDirection.Ascending - ? query.OrderBy(x => x.MobileIpAddress) - : query.OrderByDescending(x => x.MobileIpAddress); + ? query.OrderBy(x => x.Request.MobileIpAddress) + : query.OrderByDescending(x => x.Request.MobileIpAddress); break; case "FulfilledAt": query = SortDirection == SortDirection.Ascending - ? query.OrderBy(x => x.FulfilledAt) - : query.OrderByDescending(x => x.FulfilledAt); + ? query.OrderBy(x => x.Request.FulfilledAt) + : query.OrderByDescending(x => x.Request.FulfilledAt); break; case "RetrievedAt": query = SortDirection == SortDirection.Ascending - ? query.OrderBy(x => x.RetrievedAt) - : query.OrderByDescending(x => x.RetrievedAt); + ? query.OrderBy(x => x.Request.RetrievedAt) + : query.OrderByDescending(x => x.Request.RetrievedAt); break; case "Username": query = SortDirection == SortDirection.Ascending @@ -342,7 +352,7 @@ else : query.OrderByDescending(x => x.Username); break; default: - query = query.OrderByDescending(x => x.CreatedAt); + query = query.OrderByDescending(x => x.Request.CreatedAt); break; } diff --git a/apps/server/AliasVault.Api/Controllers/AuthController.cs b/apps/server/AliasVault.Api/Controllers/AuthController.cs index 4facc5ad7..bec017f33 100644 --- a/apps/server/AliasVault.Api/Controllers/AuthController.cs +++ b/apps/server/AliasVault.Api/Controllers/AuthController.cs @@ -15,6 +15,7 @@ using AliasServerDb; using AliasVault.Api.Helpers; using AliasVault.Auth; using AliasVault.Cryptography.Client; +using AliasVault.Cryptography.Server; using AliasVault.Shared.Core; using AliasVault.Shared.Models.Enums; using AliasVault.Shared.Models.WebApi; @@ -590,14 +591,20 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM // If not fulfilled, return pending status if (loginRequest.FulfilledAt == null) { - return Ok(new MobileLoginPollResponse(false, null, null, null, null, null, null)); + return Ok(new MobileLoginPollResponse(false, null, null, null, null, null)); } - // Sanity check: check if user exists. - var user = await userManager.FindByNameAsync(loginRequest.Username!); + // Check if already retrieved (one-time use protection) + if (loginRequest.RetrievedAt != null) + { + return NotFound(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.MOBILE_LOGIN_REQUEST_NOT_FOUND, 404)); + } + + // Sanity check: check if user exists using UserId FK + var user = await userManager.FindByIdAsync(loginRequest.UserId!); if (user == null) { - await authLoggingService.LogAuthEventFailAsync(loginRequest.Username!, AuthEventType.MobileLogin, AuthFailureReason.InvalidUsername); + await authLoggingService.LogAuthEventFailAsync("n/a", AuthEventType.MobileLogin, AuthFailureReason.InvalidUsername); return BadRequest(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USER_NOT_FOUND, 400)); } @@ -618,29 +625,36 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM // Generate token for the user var tokenModel = await GenerateNewTokensForUser(user, extendedLifetime: true); + // Generate a single symmetric key for encrypting all fields + var symmetricKey = Cryptography.Server.Encryption.GenerateRandomSymmetricKey(); + + // Encrypt each field with the symmetric key (returns base64) + var encryptedToken = Cryptography.Server.Encryption.SymmetricEncrypt(tokenModel.Token, symmetricKey); + var encryptedRefreshToken = Cryptography.Server.Encryption.SymmetricEncrypt(tokenModel.RefreshToken, symmetricKey); + var encryptedDecryptionKey = Cryptography.Server.Encryption.SymmetricEncrypt(loginRequest.EncryptedDecryptionKey!, symmetricKey); + var encryptedUsername = Cryptography.Server.Encryption.SymmetricEncrypt(user.UserName!, symmetricKey); + + // Encrypt the symmetric key with the client's RSA public key (returns base64) + var encryptedSymmetricKey = Cryptography.Server.Encryption.EncryptSymmetricKeyWithRsa(symmetricKey, loginRequest.ClientPublicKey); + // Log successful mobile login authentication await authLoggingService.LogAuthEventSuccessAsync(user.UserName!, AuthEventType.MobileLogin); - // Return fulfilled response with encrypted key and token - var response = new MobileLoginPollResponse( - true, - loginRequest.EncryptedDecryptionKey, - loginRequest.Username, - tokenModel, - loginRequest.Salt, - loginRequest.EncryptionType, - loginRequest.EncryptionSettings); - - // Clear sensitive data but keep the record for statistics + // Mark as retrieved and clear sensitive data loginRequest.ClientPublicKey = string.Empty; loginRequest.EncryptedDecryptionKey = null; - loginRequest.Salt = null; - loginRequest.EncryptionType = null; - loginRequest.EncryptionSettings = null; loginRequest.RetrievedAt = timeProvider.UtcNow; await context.SaveChangesAsync(); - return Ok(response); + // Return response with encrypted symmetric key and encrypted fields + // Client will decrypt username to call /login endpoint for salt and encryption settings + return Ok(new MobileLoginPollResponse( + true, + encryptedSymmetricKey, + encryptedToken, + encryptedRefreshToken, + encryptedDecryptionKey, + encryptedUsername)); } /// @@ -684,12 +698,6 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM return Unauthorized(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USER_NOT_FOUND, 401)); } - // Verify the username matches the authenticated user - if (user.UserName != model.Username) - { - return BadRequest(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.USERNAME_MISMATCH, 400)); - } - var loginRequest = await context.MobileLoginRequests.FirstOrDefaultAsync(r => r.Id == model.RequestId); // Check if request exists and hasn't expired @@ -704,15 +712,8 @@ public class AuthController(IAliasServerDbContextFactory dbContextFactory, UserM return BadRequest(ApiErrorCodeHelper.CreateErrorResponse(ApiErrorCode.MOBILE_LOGIN_REQUEST_ALREADY_FULFILLED, 400)); } - // Get latest vault encryption settings for the user - var latestVaultEncryptionSettings = AuthHelper.GetUserLatestVaultEncryptionSettings(user); - - // Update the login request with the encrypted key and user info + // Update the login request with the encrypted key and user ID loginRequest.EncryptedDecryptionKey = model.EncryptedDecryptionKey; - loginRequest.Username = model.Username; - loginRequest.Salt = latestVaultEncryptionSettings.Salt; - loginRequest.EncryptionType = latestVaultEncryptionSettings.EncryptionType; - loginRequest.EncryptionSettings = latestVaultEncryptionSettings.EncryptionSettings; loginRequest.UserId = user.Id; loginRequest.FulfilledAt = timeProvider.UtcNow; loginRequest.MobileIpAddress = IpAddressUtility.GetIpFromContext(HttpContext); diff --git a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css index 54c873584..ee93dfdc7 100644 --- a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css +++ b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css @@ -1825,6 +1825,11 @@ video { background-color: rgb(234 179 8 / var(--tw-bg-opacity)); } +.bg-yellow-100 { + --tw-bg-opacity: 1; + background-color: rgb(254 249 195 / var(--tw-bg-opacity)); +} + .bg-opacity-50 { --tw-bg-opacity: 0.5; } diff --git a/apps/server/Databases/AliasServerDb/Migrations/20251117203100_AddMobileLoginRequest.Designer.cs b/apps/server/Databases/AliasServerDb/Migrations/20251117215634_AddMobileLoginRequest.Designer.cs similarity index 98% rename from apps/server/Databases/AliasServerDb/Migrations/20251117203100_AddMobileLoginRequest.Designer.cs rename to apps/server/Databases/AliasServerDb/Migrations/20251117215634_AddMobileLoginRequest.Designer.cs index 28263febe..bc65a99fe 100644 --- a/apps/server/Databases/AliasServerDb/Migrations/20251117203100_AddMobileLoginRequest.Designer.cs +++ b/apps/server/Databases/AliasServerDb/Migrations/20251117215634_AddMobileLoginRequest.Designer.cs @@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace AliasServerDb.Migrations { [DbContext(typeof(AliasServerDbContext))] - [Migration("20251117203100_AddMobileLoginRequest")] + [Migration("20251117215634_AddMobileLoginRequest")] partial class AddMobileLoginRequest { /// @@ -512,12 +512,6 @@ namespace AliasServerDb.Migrations b.Property("EncryptedDecryptionKey") .HasColumnType("text"); - b.Property("EncryptionSettings") - .HasColumnType("text"); - - b.Property("EncryptionType") - .HasColumnType("text"); - b.Property("FulfilledAt") .HasColumnType("timestamp with time zone"); @@ -527,15 +521,9 @@ namespace AliasServerDb.Migrations b.Property("RetrievedAt") .HasColumnType("timestamp with time zone"); - b.Property("Salt") - .HasColumnType("text"); - b.Property("UserId") .HasColumnType("text"); - b.Property("Username") - .HasColumnType("text"); - b.HasKey("Id"); b.HasIndex(new[] { "ClientIpAddress" }, "IX_ClientIpAddress"); diff --git a/apps/server/Databases/AliasServerDb/Migrations/20251117203100_AddMobileLoginRequest.cs b/apps/server/Databases/AliasServerDb/Migrations/20251117215634_AddMobileLoginRequest.cs similarity index 88% rename from apps/server/Databases/AliasServerDb/Migrations/20251117203100_AddMobileLoginRequest.cs rename to apps/server/Databases/AliasServerDb/Migrations/20251117215634_AddMobileLoginRequest.cs index 6aa4b1194..ca1388dd1 100644 --- a/apps/server/Databases/AliasServerDb/Migrations/20251117203100_AddMobileLoginRequest.cs +++ b/apps/server/Databases/AliasServerDb/Migrations/20251117215634_AddMobileLoginRequest.cs @@ -1,5 +1,4 @@ -// -using System; +using System; using Microsoft.EntityFrameworkCore.Migrations; #nullable disable @@ -19,10 +18,6 @@ namespace AliasServerDb.Migrations Id = table.Column(type: "text", nullable: false), ClientPublicKey = table.Column(type: "text", nullable: false), EncryptedDecryptionKey = table.Column(type: "text", nullable: true), - Username = table.Column(type: "text", nullable: true), - Salt = table.Column(type: "text", nullable: true), - EncryptionType = table.Column(type: "text", nullable: true), - EncryptionSettings = table.Column(type: "text", nullable: true), CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), FulfilledAt = table.Column(type: "timestamp with time zone", nullable: true), RetrievedAt = table.Column(type: "timestamp with time zone", nullable: true), diff --git a/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs b/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs index c598cb0c9..4d5957ab1 100644 --- a/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs +++ b/apps/server/Databases/AliasServerDb/Migrations/AliasServerDbContextModelSnapshot.cs @@ -509,12 +509,6 @@ namespace AliasServerDb.Migrations b.Property("EncryptedDecryptionKey") .HasColumnType("text"); - b.Property("EncryptionSettings") - .HasColumnType("text"); - - b.Property("EncryptionType") - .HasColumnType("text"); - b.Property("FulfilledAt") .HasColumnType("timestamp with time zone"); @@ -524,15 +518,9 @@ namespace AliasServerDb.Migrations b.Property("RetrievedAt") .HasColumnType("timestamp with time zone"); - b.Property("Salt") - .HasColumnType("text"); - b.Property("UserId") .HasColumnType("text"); - b.Property("Username") - .HasColumnType("text"); - b.HasKey("Id"); b.HasIndex(new[] { "ClientIpAddress" }, "IX_ClientIpAddress"); diff --git a/apps/server/Databases/AliasServerDb/MobileLoginRequest.cs b/apps/server/Databases/AliasServerDb/MobileLoginRequest.cs index af759eac0..dfe34dbac 100644 --- a/apps/server/Databases/AliasServerDb/MobileLoginRequest.cs +++ b/apps/server/Databases/AliasServerDb/MobileLoginRequest.cs @@ -35,30 +35,6 @@ public class MobileLoginRequest /// public string? EncryptedDecryptionKey { get; set; } - /// - /// Gets or sets the username provided by the mobile app. - /// Will be null until mobile app responds. - /// - public string? Username { get; set; } - - /// - /// Gets or sets the salt for the user. - /// Will be populated when mobile app provides the username. - /// - public string? Salt { get; set; } - - /// - /// Gets or sets the encryption type for the user. - /// Will be populated when mobile app provides the username. - /// - public string? EncryptionType { get; set; } - - /// - /// Gets or sets the encryption settings for the user. - /// Will be populated when mobile app provides the username. - /// - public string? EncryptionSettings { get; set; } - /// /// Gets or sets the created timestamp. /// diff --git a/apps/server/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs b/apps/server/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs index 5c8484663..8f58140bc 100644 --- a/apps/server/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs +++ b/apps/server/Services/AliasVault.TaskRunner/Tasks/LogCleanupTask.cs @@ -99,9 +99,6 @@ public class LogCleanupTask : IMaintenanceTask // Clear all sensitive data request.ClientPublicKey = string.Empty; request.EncryptedDecryptionKey = null; - request.Salt = null; - request.EncryptionType = null; - request.EncryptionSettings = null; request.ClearedAt = now; } diff --git a/apps/server/Shared/AliasVault.RazorComponents/StatusPill.razor b/apps/server/Shared/AliasVault.RazorComponents/StatusPill.razor index 31a82f827..e7f953f48 100644 --- a/apps/server/Shared/AliasVault.RazorComponents/StatusPill.razor +++ b/apps/server/Shared/AliasVault.RazorComponents/StatusPill.razor @@ -23,9 +23,32 @@ [Parameter] public string TextFalse { get; set; } = "Disabled"; - private string PillClass => Enabled - ? "bg-green-100 text-green-800" - : "bg-red-100 text-red-800"; + /// + /// Optional color override: "green", "yellow", "red". If not specified, uses Enabled parameter. + /// + [Parameter] + public string? Color { get; set; } + + private string PillClass + { + get + { + if (!string.IsNullOrEmpty(Color)) + { + return Color.ToLower() switch + { + "green" => "bg-green-100 text-green-800", + "yellow" => "bg-yellow-100 text-yellow-800", + "red" => "bg-red-100 text-red-800", + _ => "bg-gray-100 text-gray-800" + }; + } + + return Enabled + ? "bg-green-100 text-green-800" + : "bg-red-100 text-red-800"; + } + } private string StatusText => Enabled ? TextTrue : TextFalse; } diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginPollResponse.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginPollResponse.cs index d60d547d9..f8d6ca80b 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginPollResponse.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginPollResponse.cs @@ -9,19 +9,19 @@ namespace AliasVault.Shared.Models.WebApi.Auth; /// /// Response model for polling mobile login status. +/// All sensitive data is encrypted with AES-256, and the AES key is encrypted with client's RSA public key. +/// Client decrypts username to call /login endpoint for salt and encryption settings. /// /// Whether the request has been fulfilled by the mobile app. -/// The encrypted decryption key (base64 encoded) if fulfilled. -/// The username of the user logging in if fulfilled. -/// The authentication token if fulfilled. -/// The salt for key derivation if fulfilled. -/// The encryption type if fulfilled. -/// The encryption settings if fulfilled. +/// The AES symmetric key encrypted with client's RSA public key (base64 encoded). Used to decrypt all encrypted fields. Null if not fulfilled. +/// The JWT token encrypted with AES symmetric key (base64 encoded). Null if not fulfilled. +/// The refresh token encrypted with AES symmetric key (base64 encoded). Null if not fulfilled. +/// The vault decryption key encrypted with AES symmetric key (base64 encoded). Null if not fulfilled. +/// The username encrypted with AES symmetric key (base64 encoded). Retrieved from User via UserId FK. Null if not fulfilled. public record MobileLoginPollResponse( bool Fulfilled, + string? EncryptedSymmetricKey, + string? EncryptedToken, + string? EncryptedRefreshToken, string? EncryptedDecryptionKey, - string? Username, - TokenModel? Token, - string? Salt, - string? EncryptionType, - string? EncryptionSettings); + string? EncryptedUsername); diff --git a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginSubmitRequest.cs b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginSubmitRequest.cs index a874ab462..c0e3faf39 100644 --- a/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginSubmitRequest.cs +++ b/apps/server/Shared/AliasVault.Shared/Models/WebApi/Auth/MobileLoginSubmitRequest.cs @@ -12,5 +12,4 @@ namespace AliasVault.Shared.Models.WebApi.Auth; /// /// The unique identifier for this login request. /// The encrypted decryption key (base64 encoded). -/// The username of the user logging in. -public record MobileLoginSubmitRequest(string RequestId, string EncryptedDecryptionKey, string Username); +public record MobileLoginSubmitRequest(string RequestId, string EncryptedDecryptionKey); diff --git a/apps/server/Tests/AliasVault.IntegrationTests/TaskRunner/TaskRunnerTests.cs b/apps/server/Tests/AliasVault.IntegrationTests/TaskRunner/TaskRunnerTests.cs index f0c08ada1..3e7c82818 100644 --- a/apps/server/Tests/AliasVault.IntegrationTests/TaskRunner/TaskRunnerTests.cs +++ b/apps/server/Tests/AliasVault.IntegrationTests/TaskRunner/TaskRunnerTests.cs @@ -546,10 +546,7 @@ public class TaskRunnerTests Id = Guid.NewGuid().ToString(), ClientPublicKey = "stale-public-key", EncryptedDecryptionKey = "encrypted-key-data", - Username = "testuser", - Salt = "test-salt", - EncryptionType = "Argon2id", - EncryptionSettings = "{\"iterations\":3}", + UserId = "user-id-1", CreatedAt = DateTime.UtcNow.AddMinutes(-15), FulfilledAt = DateTime.UtcNow.AddMinutes(-12), // Fulfilled 12 minutes ago (exceeds 10 min timeout) RetrievedAt = null, // Not yet retrieved @@ -565,10 +562,7 @@ public class TaskRunnerTests Id = Guid.NewGuid().ToString(), ClientPublicKey = "recent-public-key", EncryptedDecryptionKey = "encrypted-key-data", - Username = "testuser2", - Salt = "test-salt", - EncryptionType = "Argon2id", - EncryptionSettings = "{\"iterations\":3}", + UserId = "user-id-2", CreatedAt = DateTime.UtcNow.AddMinutes(-6), FulfilledAt = DateTime.UtcNow.AddMinutes(-5), // Fulfilled 5 minutes ago (under 10 min timeout) RetrievedAt = null, @@ -584,7 +578,7 @@ public class TaskRunnerTests Id = Guid.NewGuid().ToString(), ClientPublicKey = "completed-public-key", EncryptedDecryptionKey = "encrypted-key-data", - Username = "testuser3", + UserId = "user-id-3", CreatedAt = DateTime.UtcNow.AddMinutes(-15), FulfilledAt = DateTime.UtcNow.AddMinutes(-12), RetrievedAt = DateTime.UtcNow.AddMinutes(-11), // Already retrieved @@ -611,15 +605,12 @@ public class TaskRunnerTests // Stale request should have sensitive data cleared Assert.That(staleAfterCleanup.ClientPublicKey, Is.Empty, "Stale request ClientPublicKey should be cleared"); Assert.That(staleAfterCleanup.EncryptedDecryptionKey, Is.Null, "Stale request EncryptedDecryptionKey should be cleared"); - Assert.That(staleAfterCleanup.Salt, Is.Null, "Stale request Salt should be cleared"); - Assert.That(staleAfterCleanup.EncryptionType, Is.Null, "Stale request EncryptionType should be cleared"); - Assert.That(staleAfterCleanup.EncryptionSettings, Is.Null, "Stale request EncryptionSettings should be cleared"); Assert.That(staleAfterCleanup.ClearedAt, Is.Not.Null, "Stale request ClearedAt should be set"); // Metadata should be preserved for abuse tracking Assert.That(staleAfterCleanup.ClientIpAddress, Is.EqualTo("192.168.1.1"), "Client IP should be preserved"); Assert.That(staleAfterCleanup.MobileIpAddress, Is.EqualTo("10.0.0.1"), "Mobile IP should be preserved"); - Assert.That(staleAfterCleanup.Username, Is.EqualTo("testuser"), "Username should be preserved"); + Assert.That(staleAfterCleanup.UserId, Is.EqualTo("user-id-1"), "UserId should be preserved"); }); var recentAfterCleanup = recentRequest;