mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-30 20:43:05 -04:00
Refactor (#773)
This commit is contained in:
@@ -28,7 +28,7 @@ public class PasswordChangeFormModel : PasswordChangeModel
|
||||
/// Gets or sets the new password.
|
||||
/// </summary>
|
||||
[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!;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@using AliasVault.Shared.Core.MobileApps
|
||||
@using Microsoft.Extensions.Localization
|
||||
|
||||
<div class="bg-gray-100 dark:bg-gray-900 flex flex-col lg:items-center lg:justify-center">
|
||||
<div class="bg-gray-100 dark:bg-gray-900 flex flex-col lg:items-center lg:justify-center pb-16">
|
||||
<div class="w-full mt-4 lg:mt-16 mx-auto lg:max-w-4xl lg:bg-white lg:dark:bg-gray-800 lg:shadow-xl lg:rounded-lg lg:overflow-hidden flex flex-col">
|
||||
<div class="flex flex-col flex-grow">
|
||||
<div class="flex-grow p-6 pt-4 lg:pt-6 lg:pb-4">
|
||||
|
||||
@@ -232,7 +232,7 @@
|
||||
</data>
|
||||
<!-- AliasVault CSV/AVUX/AVEX -->
|
||||
<data name="AliasVaultDescription" xml:space="preserve">
|
||||
<value>Import from AliasVault backup (.csv, .avux, or .avex)</value>
|
||||
<value>Import from AliasVault backup</value>
|
||||
<comment>Description for AliasVault import service</comment>
|
||||
</data>
|
||||
<data name="AliasVaultInstructionsPart1" xml:space="preserve">
|
||||
|
||||
@@ -20,11 +20,6 @@ public static class ValidationMessages
|
||||
/// </summary>
|
||||
private static readonly ResourceManager ResourceManager = new("AliasVault.Client.Resources.ValidationMessages", typeof(ValidationMessages).Assembly);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error message for password minimum length validation.
|
||||
/// </summary>
|
||||
public static string PasswordMinLength => GetResourceValue("PasswordMinLength");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the error message when password confirmation doesn't match.
|
||||
/// </summary>
|
||||
|
||||
@@ -60,10 +60,6 @@
|
||||
</resheader>
|
||||
|
||||
<!-- Password validation messages -->
|
||||
<data name="PasswordMinLength" xml:space="preserve">
|
||||
<value>The new password must be at least {0} characters long.</value>
|
||||
<comment>Error message for password minimum length validation. {0} is the minimum password length.</comment>
|
||||
</data>
|
||||
<data name="PasswordsDoNotMatch" xml:space="preserve">
|
||||
<value>The new passwords do not match.</value>
|
||||
<comment>Error message when password confirmation doesn't match</comment>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="VaultEncryptedExportService.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Service for creating encrypted .avex export files.
|
||||
/// </summary>
|
||||
public static class VaultEncryptedExportService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Exports vault data to .avex (AliasVault Encrypted eXport) format.
|
||||
/// This wraps an existing .avux file in an encrypted container using a user-provided password.
|
||||
/// </summary>
|
||||
/// <param name="avuxBytes">The unencrypted .avux file bytes.</param>
|
||||
/// <param name="exportPassword">The password to encrypt the export with.</param>
|
||||
/// <param name="username">The username creating the export.</param>
|
||||
/// <returns>A byte array containing the encrypted .avex file.</returns>
|
||||
public static async Task<byte[]> 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<string, int>
|
||||
{
|
||||
["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;
|
||||
}
|
||||
}
|
||||
@@ -1,194 +0,0 @@
|
||||
//-----------------------------------------------------------------------
|
||||
// <copyright file="VaultEncryptedImportService.cs" company="aliasvault">
|
||||
// Copyright (c) aliasvault. All rights reserved.
|
||||
// Licensed under the AGPLv3 license. See LICENSE.md file in the project root for full license information.
|
||||
// </copyright>
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Service for importing encrypted .avex export files.
|
||||
/// </summary>
|
||||
public static class VaultEncryptedImportService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the provided file bytes represent an .avex encrypted export.
|
||||
/// </summary>
|
||||
/// <param name="fileBytes">The file bytes to check.</param>
|
||||
/// <returns>True if the file is an .avex format, false otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decrypts an .avex file and returns the unencrypted .avux bytes.
|
||||
/// </summary>
|
||||
/// <param name="avexBytes">The encrypted .avex file bytes.</param>
|
||||
/// <param name="exportPassword">The password to decrypt the export with.</param>
|
||||
/// <returns>The decrypted .avux file bytes.</returns>
|
||||
public static async Task<byte[]> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the .avex header and returns the header object and payload offset.
|
||||
/// </summary>
|
||||
/// <param name="avexBytes">The .avex file bytes.</param>
|
||||
/// <returns>A tuple containing the header and the offset where encrypted data begins.</returns>
|
||||
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<AvexHeader>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the index of a byte pattern within a byte array.
|
||||
/// </summary>
|
||||
/// <param name="source">The source byte array to search in.</param>
|
||||
/// <param name="pattern">The pattern to search for.</param>
|
||||
/// <returns>The index of the pattern, or -1 if not found.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user