From 45fd26ead54deefca11e98a8bd81110b5cfeb14b Mon Sep 17 00:00:00 2001 From: Leendert de Borst Date: Wed, 18 Mar 2026 18:48:52 +0100 Subject: [PATCH] Refactor (#773) --- .../Validation/PasswordChangeFormModel.cs | 2 +- .../Main/Pages/Welcome.razor | 2 +- .../ImportExport/ImportServices.en.resx | 2 +- .../Resources/ValidationMessages.cs | 5 - .../Resources/ValidationMessages.en.resx | 4 - .../wwwroot/css/tailwind.css | 180 +++++----------- .../Common/ClientPlaywrightTest.cs | 2 +- .../VaultEncryptedExportService.cs | 116 ----------- .../VaultEncryptedImportService.cs | 194 ------------------ 9 files changed, 56 insertions(+), 451 deletions(-) delete mode 100644 apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedExportService.cs delete mode 100644 apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedImportService.cs diff --git a/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs b/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs index 2d3f8d5f3..7be90cff7 100644 --- a/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs +++ b/apps/server/AliasVault.Client/Main/Models/Validation/PasswordChangeFormModel.cs @@ -28,7 +28,7 @@ public class PasswordChangeFormModel : PasswordChangeModel /// Gets or sets the new password. /// [Required(ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordRequired))] - [MinimumPasswordLength(PasswordStrengthConstants.MinimumGoodPasswordLength, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLength))] + [MinimumPasswordLength(PasswordStrengthConstants.MinimumGoodPasswordLength, ErrorMessageResourceType = typeof(ValidationMessages), ErrorMessageResourceName = nameof(ValidationMessages.PasswordMinLengthGeneric))] public new string NewPassword { get; set; } = null!; /// diff --git a/apps/server/AliasVault.Client/Main/Pages/Welcome.razor b/apps/server/AliasVault.Client/Main/Pages/Welcome.razor index 0f385fe2a..4251f585c 100644 --- a/apps/server/AliasVault.Client/Main/Pages/Welcome.razor +++ b/apps/server/AliasVault.Client/Main/Pages/Welcome.razor @@ -4,7 +4,7 @@ @using AliasVault.Shared.Core.MobileApps @using Microsoft.Extensions.Localization -
+
diff --git a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx index b6888e512..fe9fe9d24 100644 --- a/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx +++ b/apps/server/AliasVault.Client/Resources/Components/Main/Settings/ImportExport/ImportServices.en.resx @@ -232,7 +232,7 @@ - Import from AliasVault backup (.csv, .avux, or .avex) + Import from AliasVault backup Description for AliasVault import service diff --git a/apps/server/AliasVault.Client/Resources/ValidationMessages.cs b/apps/server/AliasVault.Client/Resources/ValidationMessages.cs index cd6a401d7..75775b99b 100644 --- a/apps/server/AliasVault.Client/Resources/ValidationMessages.cs +++ b/apps/server/AliasVault.Client/Resources/ValidationMessages.cs @@ -20,11 +20,6 @@ public static class ValidationMessages ///
private static readonly ResourceManager ResourceManager = new("AliasVault.Client.Resources.ValidationMessages", typeof(ValidationMessages).Assembly); - /// - /// Gets the error message for password minimum length validation. - /// - public static string PasswordMinLength => GetResourceValue("PasswordMinLength"); - /// /// Gets the error message when password confirmation doesn't match. /// diff --git a/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx b/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx index 86a55214a..5a1ca92d4 100644 --- a/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx +++ b/apps/server/AliasVault.Client/Resources/ValidationMessages.en.resx @@ -60,10 +60,6 @@ - - The new password must be at least {0} characters long. - Error message for password minimum length validation. {0} is the minimum password length. - The new passwords do not match. Error message when password confirmation doesn't match diff --git a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css index a8140053e..2dedffd63 100644 --- a/apps/server/AliasVault.Client/wwwroot/css/tailwind.css +++ b/apps/server/AliasVault.Client/wwwroot/css/tailwind.css @@ -1539,12 +1539,6 @@ video { margin-bottom: calc(1.5rem * var(--tw-space-y-reverse)); } -.space-y-1\.5 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0.375rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0.375rem * var(--tw-space-y-reverse)); -} - .divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); @@ -1721,11 +1715,6 @@ video { border-color: rgb(251 191 36 / var(--tw-border-opacity)); } -.border-blue-200 { - --tw-border-opacity: 1; - border-color: rgb(191 219 254 / var(--tw-border-opacity)); -} - .border-blue-700 { --tw-border-opacity: 1; border-color: rgb(29 78 216 / var(--tw-border-opacity)); @@ -1930,6 +1919,11 @@ video { background-color: rgb(255 237 213 / var(--tw-bg-opacity)); } +.bg-orange-400 { + --tw-bg-opacity: 1; + background-color: rgb(251 146 60 / var(--tw-bg-opacity)); +} + .bg-orange-50 { --tw-bg-opacity: 1; background-color: rgb(255 247 237 / var(--tw-bg-opacity)); @@ -1940,11 +1934,6 @@ video { background-color: rgb(249 115 22 / var(--tw-bg-opacity)); } -.bg-orange-600 { - --tw-bg-opacity: 1; - background-color: rgb(234 88 12 / var(--tw-bg-opacity)); -} - .bg-primary-100 { --tw-bg-opacity: 1; background-color: rgb(253 222 133 / var(--tw-bg-opacity)); @@ -2024,16 +2013,6 @@ video { background-color: rgb(234 179 8 / var(--tw-bg-opacity)); } -.bg-orange-400 { - --tw-bg-opacity: 1; - background-color: rgb(251 146 60 / var(--tw-bg-opacity)); -} - -.bg-orange-700 { - --tw-bg-opacity: 1; - background-color: rgb(194 65 12 / var(--tw-bg-opacity)); -} - .bg-opacity-50 { --tw-bg-opacity: 0.5; } @@ -2113,6 +2092,16 @@ video { padding: 2rem; } +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; +} + +.px-1\.5 { + padding-left: 0.375rem; + padding-right: 0.375rem; +} + .px-2 { padding-left: 0.5rem; padding-right: 0.5rem; @@ -2193,16 +2182,6 @@ video { padding-bottom: 2rem; } -.px-1 { - padding-left: 0.25rem; - padding-right: 0.25rem; -} - -.px-1\.5 { - padding-left: 0.375rem; - padding-right: 0.375rem; -} - .pb-28 { padding-bottom: 7rem; } @@ -2267,6 +2246,10 @@ video { padding-top: 2rem; } +.pb-16 { + padding-bottom: 4rem; +} + .text-left { text-align: left; } @@ -2602,6 +2585,12 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } +.shadow-inner { + --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .shadow-lg { --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color); @@ -2620,12 +2609,6 @@ video { box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } -.shadow-inner { - --tw-shadow: inset 0 2px 4px 0 rgb(0 0 0 / 0.05); - --tw-shadow-colored: inset 0 2px 4px 0 var(--tw-shadow-color); - box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); -} - .outline-0 { outline-width: 0px; } @@ -2877,11 +2860,6 @@ video { background-color: rgb(254 215 170 / var(--tw-bg-opacity)); } -.hover\:bg-orange-500:hover { - --tw-bg-opacity: 1; - background-color: rgb(249 115 22 / var(--tw-bg-opacity)); -} - .hover\:bg-primary-100:hover { --tw-bg-opacity: 1; background-color: rgb(253 222 133 / var(--tw-bg-opacity)); @@ -2937,16 +2915,6 @@ video { background-color: rgb(153 27 27 / var(--tw-bg-opacity)); } -.hover\:bg-orange-700:hover { - --tw-bg-opacity: 1; - background-color: rgb(194 65 12 / var(--tw-bg-opacity)); -} - -.hover\:bg-orange-800:hover { - --tw-bg-opacity: 1; - background-color: rgb(154 52 18 / var(--tw-bg-opacity)); -} - .hover\:from-primary-600:hover { --tw-gradient-from: #d68338 var(--tw-gradient-from-position); --tw-gradient-to: rgb(214 131 56 / 0) var(--tw-gradient-to-position); @@ -3214,11 +3182,6 @@ video { cursor: not-allowed; } -.disabled\:bg-gray-400:disabled { - --tw-bg-opacity: 1; - background-color: rgb(156 163 175 / var(--tw-bg-opacity)); -} - .disabled\:opacity-50:disabled { opacity: 0.5; } @@ -3246,11 +3209,6 @@ video { border-color: rgb(59 130 246 / var(--tw-border-opacity)); } -.dark\:border-blue-800:is(.dark *) { - --tw-border-opacity: 1; - border-color: rgb(30 64 175 / var(--tw-border-opacity)); -} - .dark\:border-gray-400:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(156 163 175 / var(--tw-border-opacity)); @@ -3355,10 +3313,6 @@ video { background-color: rgb(30 58 138 / var(--tw-bg-opacity)); } -.dark\:bg-blue-900\/20:is(.dark *) { - background-color: rgb(30 58 138 / 0.2); -} - .dark\:bg-gray-500:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(107 114 128 / var(--tw-bg-opacity)); @@ -3397,6 +3351,11 @@ video { background-color: rgb(22 163 74 / var(--tw-bg-opacity)); } +.dark\:bg-green-700:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(21 128 61 / var(--tw-bg-opacity)); +} + .dark\:bg-green-800:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(22 101 52 / var(--tw-bg-opacity)); @@ -3416,9 +3375,14 @@ video { background-color: rgb(251 146 60 / var(--tw-bg-opacity)); } -.dark\:bg-orange-700:is(.dark *) { +.dark\:bg-orange-500:is(.dark *) { --tw-bg-opacity: 1; - background-color: rgb(194 65 12 / var(--tw-bg-opacity)); + background-color: rgb(249 115 22 / var(--tw-bg-opacity)); +} + +.dark\:bg-orange-900:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(124 45 18 / var(--tw-bg-opacity)); } .dark\:bg-orange-900\/20:is(.dark *) { @@ -3493,6 +3457,11 @@ video { background-color: rgb(127 29 29 / 0.4); } +.dark\:bg-yellow-600:is(.dark *) { + --tw-bg-opacity: 1; + background-color: rgb(202 138 4 / var(--tw-bg-opacity)); +} + .dark\:bg-yellow-800:is(.dark *) { --tw-bg-opacity: 1; background-color: rgb(133 77 14 / var(--tw-bg-opacity)); @@ -3502,31 +3471,6 @@ video { background-color: rgb(113 63 18 / 0.2); } -.dark\:bg-orange-600:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(234 88 12 / var(--tw-bg-opacity)); -} - -.dark\:bg-yellow-600:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(202 138 4 / var(--tw-bg-opacity)); -} - -.dark\:bg-orange-500:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(249 115 22 / var(--tw-bg-opacity)); -} - -.dark\:bg-green-700:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(21 128 61 / var(--tw-bg-opacity)); -} - -.dark\:bg-orange-900:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(124 45 18 / var(--tw-bg-opacity)); -} - .dark\:bg-opacity-75:is(.dark *) { --tw-bg-opacity: 0.75; } @@ -3555,11 +3499,6 @@ video { color: rgb(251 191 36 / var(--tw-text-opacity)); } -.dark\:text-blue-200:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(191 219 254 / var(--tw-text-opacity)); -} - .dark\:text-blue-400:is(.dark *) { --tw-text-opacity: 1; color: rgb(96 165 250 / var(--tw-text-opacity)); @@ -3600,6 +3539,16 @@ video { color: rgb(75 85 99 / var(--tw-text-opacity)); } +.dark\:text-green-200:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(187 247 208 / var(--tw-text-opacity)); +} + +.dark\:text-green-300:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(134 239 172 / var(--tw-text-opacity)); +} + .dark\:text-green-400:is(.dark *) { --tw-text-opacity: 1; color: rgb(74 222 128 / var(--tw-text-opacity)); @@ -3703,21 +3652,6 @@ video { color: rgb(250 204 21 / var(--tw-text-opacity)); } -.dark\:text-green-300:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(134 239 172 / var(--tw-text-opacity)); -} - -.dark\:text-green-200:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(187 247 208 / var(--tw-text-opacity)); -} - -.dark\:text-blue-300:is(.dark *) { - --tw-text-opacity: 1; - color: rgb(147 197 253 / var(--tw-text-opacity)); -} - .dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder { --tw-placeholder-opacity: 1; color: rgb(156 163 175 / var(--tw-placeholder-opacity)); @@ -3813,11 +3747,6 @@ video { background-color: rgb(20 83 45 / 0.5); } -.dark\:hover\:bg-orange-600:hover:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(234 88 12 / var(--tw-bg-opacity)); -} - .dark\:hover\:bg-orange-900\/30:hover:is(.dark *) { background-color: rgb(124 45 18 / 0.3); } @@ -3857,11 +3786,6 @@ video { background-color: rgb(127 29 29 / 0.5); } -.dark\:hover\:bg-orange-700:hover:is(.dark *) { - --tw-bg-opacity: 1; - background-color: rgb(194 65 12 / var(--tw-bg-opacity)); -} - .dark\:hover\:from-primary-500:hover:is(.dark *) { --tw-gradient-from: #f49541 var(--tw-gradient-from-position); --tw-gradient-to: rgb(244 149 65 / 0) var(--tw-gradient-to-position); diff --git a/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs b/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs index c7ca74379..be034c1f1 100644 --- a/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs +++ b/apps/server/Tests/AliasVault.E2ETests/Common/ClientPlaywrightTest.cs @@ -285,7 +285,7 @@ public abstract class ClientPlaywrightTest : PlaywrightTest formValues["email"] = localPart; } - // Check if notes field is specified. For Login items, notes is now a separate section that needs to be added. + // Check if notes field is specified, if so, add it as a separate section. if (formValues != null && formValues.ContainsKey("notes") && !string.IsNullOrEmpty(formValues["notes"])) { // Add the Notes section via the + menu diff --git a/apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedExportService.cs b/apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedExportService.cs deleted file mode 100644 index bf1913514..000000000 --- a/apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedExportService.cs +++ /dev/null @@ -1,116 +0,0 @@ -//----------------------------------------------------------------------- -// -// 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.ImportExport; - -using System.Text; -using System.Text.Json; -using AliasVault.Cryptography.Client; -using AliasVault.Cryptography.Server; -using AliasVault.ImportExport.Constants; -using AliasVault.ImportExport.Models.Exports; -using AliasVault.Shared.Core; - -/// -/// Service for creating encrypted .avex export files. -/// -public static class VaultEncryptedExportService -{ - - /// - /// Exports vault data to .avex (AliasVault Encrypted eXport) format. - /// This wraps an existing .avux file in an encrypted container using a user-provided password. - /// - /// The unencrypted .avux file bytes. - /// The password to encrypt the export with. - /// The username creating the export. - /// A byte array containing the encrypted .avex file. - public static async Task ExportToAvexAsync( - byte[] avuxBytes, - string exportPassword, - string username) - { - if (avuxBytes == null || avuxBytes.Length == 0) - { - throw new ArgumentException("AVUX bytes cannot be null or empty", nameof(avuxBytes)); - } - - if (string.IsNullOrWhiteSpace(exportPassword)) - { - throw new ArgumentException("Export password cannot be null or empty", nameof(exportPassword)); - } - - // 1. Generate random salt for key derivation - var salt = System.Security.Cryptography.RandomNumberGenerator.GetBytes(32); - var saltBase64 = Convert.ToBase64String(salt); - - // 2. Derive encryption key from password using Argon2id - var key = await AliasVault.Cryptography.Client.Encryption.DeriveKeyFromPasswordAsync( - exportPassword, - saltBase64, - Defaults.EncryptionType, - Defaults.EncryptionSettings); - - // 3. Encrypt the .avux bytes using AES-256-GCM - var encryptedPayload = AliasVault.Cryptography.Server.Encryption.SymmetricEncrypt(avuxBytes, key); - - // 4. Create the header - var header = new AvexHeader - { - Format = AvexConstants.FormatIdentifier, - Version = AvexConstants.FormatVersion, - Kdf = new KdfParams - { - Type = Defaults.EncryptionType, - Salt = saltBase64, - Params = new Dictionary - { - ["DegreeOfParallelism"] = Defaults.Argon2IdDegreeOfParallelism, - ["MemorySize"] = Defaults.Argon2IdMemorySize, - ["Iterations"] = Defaults.Argon2IdIterations, - }, - }, - Encryption = new EncryptionParams - { - Algorithm = "AES-256-GCM", - EncryptedDataOffset = 0, // Will be calculated below - }, - Metadata = new AvexMetadata - { - ExportedAt = DateTime.UtcNow, - ExportedBy = username, - AppVersion = AppInfo.GetFullVersion(), - }, - }; - - // 5. Serialize header to JSON - var jsonOptions = new JsonSerializerOptions - { - WriteIndented = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }; - - var headerJson = JsonSerializer.Serialize(header, jsonOptions); - var headerBytes = Encoding.UTF8.GetBytes(headerJson); - var delimiterBytes = Encoding.UTF8.GetBytes(AvexConstants.HeaderDelimiter); - - // 6. Calculate the offset where encrypted data begins - header.Encryption.EncryptedDataOffset = headerBytes.Length + delimiterBytes.Length; - - // Re-serialize with correct offset - headerJson = JsonSerializer.Serialize(header, jsonOptions); - headerBytes = Encoding.UTF8.GetBytes(headerJson); - - // 7. Combine header + delimiter + encrypted payload - var avexFile = new byte[headerBytes.Length + delimiterBytes.Length + encryptedPayload.Length]; - Buffer.BlockCopy(headerBytes, 0, avexFile, 0, headerBytes.Length); - Buffer.BlockCopy(delimiterBytes, 0, avexFile, headerBytes.Length, delimiterBytes.Length); - Buffer.BlockCopy(encryptedPayload, 0, avexFile, headerBytes.Length + delimiterBytes.Length, encryptedPayload.Length); - - return avexFile; - } -} diff --git a/apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedImportService.cs b/apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedImportService.cs deleted file mode 100644 index cde8c9c83..000000000 --- a/apps/server/Utilities/AliasVault.ImportExport/VaultEncryptedImportService.cs +++ /dev/null @@ -1,194 +0,0 @@ -//----------------------------------------------------------------------- -// -// 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.ImportExport; - -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; -using AliasVault.Cryptography.Client; -using AliasVault.Cryptography.Server; -using AliasVault.ImportExport.Constants; -using AliasVault.ImportExport.Models.Exports; - -/// -/// Service for importing encrypted .avex export files. -/// -public static class VaultEncryptedImportService -{ - - /// - /// Checks if the provided file bytes represent an .avex encrypted export. - /// - /// The file bytes to check. - /// True if the file is an .avex format, false otherwise. - public static bool IsAvexFormat(byte[] fileBytes) - { - if (fileBytes == null || fileBytes.Length < 50) - { - return false; - } - - try - { - // Read first 500 bytes as string to check for header - var headerLength = Math.Min(500, fileBytes.Length); - var headerText = Encoding.UTF8.GetString(fileBytes, 0, headerLength); - - return headerText.Contains($"\"format\": \"{AvexConstants.FormatIdentifier}\"") || - headerText.Contains($"\"format\":\"{AvexConstants.FormatIdentifier}\""); - } - catch - { - return false; - } - } - - /// - /// Decrypts an .avex file and returns the unencrypted .avux bytes. - /// - /// The encrypted .avex file bytes. - /// The password to decrypt the export with. - /// The decrypted .avux file bytes. - public static async Task DecryptAvexAsync(byte[] avexBytes, string exportPassword) - { - if (avexBytes == null || avexBytes.Length == 0) - { - throw new ArgumentException("AVEX bytes cannot be null or empty", nameof(avexBytes)); - } - - if (string.IsNullOrWhiteSpace(exportPassword)) - { - throw new ArgumentException("Export password cannot be null or empty", nameof(exportPassword)); - } - - // 1. Parse the header - var (header, payloadOffset) = ParseAvexHeader(avexBytes); - - // 2. Validate version - if (header.Version != AvexConstants.FormatVersion) - { - throw new InvalidOperationException($"Unsupported .avex version: {header.Version}. Expected {AvexConstants.FormatVersion}."); - } - - // 3. Validate encryption algorithm - if (header.Encryption.Algorithm != "AES-256-GCM") - { - throw new InvalidOperationException($"Unsupported encryption algorithm: {header.Encryption.Algorithm}"); - } - - // 4. Extract encrypted payload - var encryptedPayloadLength = avexBytes.Length - (int)payloadOffset; - if (encryptedPayloadLength <= 0) - { - throw new InvalidOperationException("Invalid .avex file: no encrypted payload found"); - } - - var encryptedPayload = new byte[encryptedPayloadLength]; - Buffer.BlockCopy(avexBytes, (int)payloadOffset, encryptedPayload, 0, encryptedPayloadLength); - - // 5. Derive decryption key from password using same KDF parameters - var kdfSettings = JsonSerializer.Serialize(header.Kdf.Params); - var key = await AliasVault.Cryptography.Client.Encryption.DeriveKeyFromPasswordAsync( - exportPassword, - header.Kdf.Salt, - header.Kdf.Type, - kdfSettings); - - // 6. Decrypt the payload - byte[] avuxBytes; - try - { - avuxBytes = AliasVault.Cryptography.Server.Encryption.SymmetricDecrypt(encryptedPayload, key); - } - catch (CryptographicException ex) - { - throw new InvalidOperationException("Failed to decrypt .avex file. The password may be incorrect or the file may be corrupted.", ex); - } - - return avuxBytes; - } - - /// - /// Parses the .avex header and returns the header object and payload offset. - /// - /// The .avex file bytes. - /// A tuple containing the header and the offset where encrypted data begins. - private static (AvexHeader Header, long PayloadOffset) ParseAvexHeader(byte[] avexBytes) - { - // Find the delimiter - var delimiterBytes = Encoding.UTF8.GetBytes(AvexConstants.HeaderDelimiter); - var delimiterIndex = IndexOf(avexBytes, delimiterBytes); - - if (delimiterIndex == -1) - { - throw new InvalidOperationException("Invalid .avex file: header delimiter not found"); - } - - // Extract header JSON - var headerBytes = new byte[delimiterIndex]; - Buffer.BlockCopy(avexBytes, 0, headerBytes, 0, delimiterIndex); - var headerJson = Encoding.UTF8.GetString(headerBytes); - - // Deserialize header - var jsonOptions = new JsonSerializerOptions - { - PropertyNameCaseInsensitive = true, - }; - - var header = JsonSerializer.Deserialize(headerJson, jsonOptions); - - if (header == null) - { - throw new InvalidOperationException("Invalid .avex file: failed to parse header JSON"); - } - - if (header.Format != AvexConstants.FormatIdentifier) - { - throw new InvalidOperationException($"Invalid .avex file: expected format '{AvexConstants.FormatIdentifier}', got '{header.Format}'"); - } - - // Calculate payload offset - var payloadOffset = delimiterIndex + delimiterBytes.Length; - - return (header, payloadOffset); - } - - /// - /// Finds the index of a byte pattern within a byte array. - /// - /// The source byte array to search in. - /// The pattern to search for. - /// The index of the pattern, or -1 if not found. - private static int IndexOf(byte[] source, byte[] pattern) - { - if (pattern.Length > source.Length) - { - return -1; - } - - for (int i = 0; i <= source.Length - pattern.Length; i++) - { - bool found = true; - for (int j = 0; j < pattern.Length; j++) - { - if (source[i + j] != pattern[j]) - { - found = false; - break; - } - } - - if (found) - { - return i; - } - } - - return -1; - } -}