Add AvexCryptoService.cs (#773)

This commit is contained in:
Leendert de Borst
2026-03-15 15:21:05 +01:00
parent 2f33140831
commit d6a024a7bb
8 changed files with 738 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ using System.Globalization;
using AliasVault.Client;
using AliasVault.Client.Main.Services;
using AliasVault.Client.Providers;
using AliasVault.Client.Services.Crypto;
using AliasVault.Client.Services.JsInterop.RustCore;
using AliasVault.RazorComponents.Services;
using AliasVault.Shared.Core;
@@ -100,6 +101,7 @@ builder.Services.AddScoped<ConfirmModalService>();
builder.Services.AddScoped<QuickCreateStateService>();
builder.Services.AddScoped<LanguageService>();
builder.Services.AddScoped<RustCoreService>();
builder.Services.AddScoped<AvexCryptoService>();
builder.Services.AddAuthorizationCore();
builder.Services.AddBlazoredLocalStorage();

View File

@@ -0,0 +1,224 @@
//-----------------------------------------------------------------------
// <copyright file="AvexCryptoService.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.Client.Services.Crypto;
using System.Text;
using System.Text.Json;
using AliasVault.Client.Services.JsInterop;
using AliasVault.ImportExport.Models.Exports;
/// <summary>
/// Provides cryptographic operations for .avex encrypted vault export format.
/// Handles encryption/decryption of .avex files using Argon2id KDF and AES-256-GCM.
/// </summary>
public class AvexCryptoService
{
private const string HeaderDelimiter = "\n--- ENCRYPTED PAYLOAD FOLLOWS ---\n";
private readonly JsInteropService jsInteropService;
/// <summary>
/// Initializes a new instance of the <see cref="AvexCryptoService"/> class.
/// </summary>
/// <param name="jsInteropService">The JS interop service.</param>
public AvexCryptoService(JsInteropService jsInteropService)
{
this.jsInteropService = jsInteropService;
}
/// <summary>
/// Encrypts vault data to .avex format using Web Crypto API.
/// Uses Argon2id for key derivation and AES-256-GCM for encryption.
/// </summary>
/// <param name="avuxBytes">The unencrypted .avux bytes.</param>
/// <param name="exportPassword">The password to encrypt with.</param>
/// <param name="username">The username creating the export.</param>
/// <returns>The encrypted .avex file bytes.</returns>
public async Task<byte[]> EncryptToAvexAsync(byte[] avuxBytes, string exportPassword, string username)
{
// 1. Generate random salt
var salt = await this.jsInteropService.GenerateSalt(32);
// 2. Derive key using Argon2id (same as existing vault encryption)
var saltBase64 = Convert.ToBase64String(salt);
var key = await AliasVault.Cryptography.Client.Encryption.DeriveKeyFromPasswordAsync(
exportPassword,
saltBase64,
AliasVault.Cryptography.Client.Defaults.EncryptionType,
AliasVault.Cryptography.Client.Defaults.EncryptionSettings);
// 3. Encrypt the .avux bytes using AES-256-GCM via JavaScript
var encryptedPayload = await this.jsInteropService.SymmetricEncryptBytes(avuxBytes, key);
// 4. Create header
var header = new AvexHeader
{
Format = "avex",
Version = "1.0.0",
Kdf = new KdfParams
{
Type = AliasVault.Cryptography.Client.Defaults.EncryptionType, // Argon2Id
Salt = saltBase64,
Params = new Dictionary<string, int>
{
["DegreeOfParallelism"] = AliasVault.Cryptography.Client.Defaults.Argon2IdDegreeOfParallelism,
["MemorySize"] = AliasVault.Cryptography.Client.Defaults.Argon2IdMemorySize,
["Iterations"] = AliasVault.Cryptography.Client.Defaults.Argon2IdIterations,
},
},
Encryption = new EncryptionParams
{
Algorithm = "AES-256-GCM",
EncryptedDataOffset = 0,
},
Metadata = new AvexMetadata
{
ExportedAt = DateTime.UtcNow,
ExportedBy = username,
},
};
// 5. Serialize header
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(HeaderDelimiter);
// 6. Calculate offset
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;
}
/// <summary>
/// Decrypts .avex file to .avux bytes using Web Crypto API.
/// </summary>
/// <param name="avexBytes">The encrypted .avex file bytes.</param>
/// <param name="exportPassword">The password to decrypt with.</param>
/// <returns>The decrypted .avux file bytes.</returns>
public async Task<byte[]> DecryptAvexAsync(byte[] avexBytes, string exportPassword)
{
// 1. Parse header
var (header, payloadOffset) = ParseAvexHeader(avexBytes);
// 2. Validate version
if (header.Version != "1.0.0")
{
throw new InvalidOperationException($"Unsupported .avex version: {header.Version}");
}
// 3. Extract encrypted payload
var encryptedPayloadLength = avexBytes.Length - (int)payloadOffset;
var encryptedPayload = new byte[encryptedPayloadLength];
Buffer.BlockCopy(avexBytes, (int)payloadOffset, encryptedPayload, 0, encryptedPayloadLength);
// 4. Derive key using Argon2id (C# library works in Blazor WASM)
if (header.Kdf.Type != "Argon2Id" && header.Kdf.Type != "Argon2id")
{
throw new InvalidOperationException($"Unsupported KDF type: {header.Kdf.Type}. Only Argon2id is supported.");
}
var kdfSettings = JsonSerializer.Serialize(header.Kdf.Params);
var key = await AliasVault.Cryptography.Client.Encryption.DeriveKeyFromPasswordAsync(
exportPassword,
header.Kdf.Salt,
header.Kdf.Type,
kdfSettings);
// 5. Decrypt payload
byte[] avuxBytes;
try
{
avuxBytes = await this.jsInteropService.SymmetricDecryptBytesRaw(encryptedPayload, key);
}
catch (Exception 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.
/// </summary>
private static (AvexHeader Header, long PayloadOffset) ParseAvexHeader(byte[] avexBytes)
{
var delimiterBytes = Encoding.UTF8.GetBytes(HeaderDelimiter);
var delimiterIndex = IndexOf(avexBytes, delimiterBytes);
if (delimiterIndex == -1)
{
throw new InvalidOperationException("Invalid .avex file: header delimiter not found");
}
var headerBytes = new byte[delimiterIndex];
Buffer.BlockCopy(avexBytes, 0, headerBytes, 0, delimiterIndex);
var headerJson = Encoding.UTF8.GetString(headerBytes);
var jsonOptions = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
var header = JsonSerializer.Deserialize<AvexHeader>(headerJson, jsonOptions);
if (header == null || header.Format != "avex")
{
throw new InvalidOperationException("Invalid .avex file: malformed header");
}
var payloadOffset = delimiterIndex + delimiterBytes.Length;
return (header, payloadOffset);
}
/// <summary>
/// Finds the index of a byte pattern within a byte array.
/// </summary>
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;
}
}

View File

@@ -388,6 +388,48 @@ public sealed class JsInteropService(IJSRuntime jsRuntime)
return await jsRuntime.InvokeAsync<byte[]>("cryptoInterop.decryptBytes", base64Ciphertext, encryptionKey);
}
/// <summary>
/// Generates a random salt of the specified length.
/// </summary>
/// <param name="length">Length of salt to generate in bytes.</param>
/// <returns>Random salt bytes.</returns>
public async Task<byte[]> GenerateSalt(int length)
{
return await jsRuntime.InvokeAsync<byte[]>("cryptoInterop.generateSalt", length);
}
/// <summary>
/// Symmetrically encrypts a byte array using the provided encryption key.
/// </summary>
/// <param name="plainBytes">Plain bytes to encrypt.</param>
/// <param name="encryptionKey">Encryption key bytes.</param>
/// <returns>Encrypted bytes.</returns>
public async Task<byte[]> SymmetricEncryptBytes(byte[] plainBytes, byte[] encryptionKey)
{
if (plainBytes == null || plainBytes.Length == 0)
{
return [];
}
return await jsRuntime.InvokeAsync<byte[]>("cryptoInterop.encryptBytes", plainBytes, encryptionKey);
}
/// <summary>
/// Symmetrically decrypts a byte array using the provided encryption key (raw bytes without base64 encoding).
/// </summary>
/// <param name="encryptedBytes">Encrypted bytes to decrypt.</param>
/// <param name="encryptionKey">Encryption key bytes.</param>
/// <returns>Decrypted bytes.</returns>
public async Task<byte[]> SymmetricDecryptBytesRaw(byte[] encryptedBytes, byte[] encryptionKey)
{
if (encryptedBytes == null || encryptedBytes.Length == 0)
{
return [];
}
return await jsRuntime.InvokeAsync<byte[]>("cryptoInterop.decryptBytesRaw", encryptedBytes, encryptionKey);
}
/// <summary>
/// Gets all available languages for identity generation.
/// </summary>

View File

@@ -112,6 +112,74 @@ window.cryptoInterop = {
);
return new Uint8Array(decrypted);
},
/**
* Encrypts byte array using AES-256-GCM
* @param {Uint8Array} plainBytes - The bytes to encrypt
* @param {Uint8Array} keyBytes - The 32-byte encryption key
* @returns {Promise<Uint8Array>} The encrypted data (nonce + ciphertext + tag)
*/
encryptBytes: async function(plainBytes, keyBytes) {
checkCryptoAvailable();
const key = await window.crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt']
);
const nonce = window.crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await window.crypto.subtle.encrypt(
{ name: 'AES-GCM', iv: nonce },
key,
plainBytes
);
const ciphertextArray = new Uint8Array(ciphertext);
const result = new Uint8Array(12 + ciphertextArray.length);
result.set(nonce, 0);
result.set(ciphertextArray, 12);
return result;
},
/**
* Decrypts byte array using AES-256-GCM
* @param {Uint8Array} encryptedBytes - The encrypted data (nonce + ciphertext + tag)
* @param {Uint8Array} keyBytes - The 32-byte encryption key
* @returns {Promise<Uint8Array>} The decrypted data
*/
decryptBytesRaw: async function(encryptedBytes, keyBytes) {
checkCryptoAvailable();
const key = await window.crypto.subtle.importKey(
'raw',
keyBytes,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt']
);
const nonce = encryptedBytes.slice(0, 12);
const ciphertextWithTag = encryptedBytes.slice(12);
const plaintext = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: nonce },
key,
ciphertextWithTag
);
return new Uint8Array(plaintext);
},
/**
* Generates random salt
* @param {number} length - The length of the salt in bytes
* @returns {Uint8Array} The random salt
*/
generateSalt: function(length) {
checkCryptoAvailable();
return window.crypto.getRandomValues(new Uint8Array(length));
}
};

View File

@@ -18,6 +18,8 @@
<ItemGroup>
<ProjectReference Include="..\..\Databases\AliasClientDb\AliasClientDb.csproj" />
<ProjectReference Include="..\..\Utilities\AliasVault.TotpGenerator\AliasVault.TotpGenerator.csproj" />
<ProjectReference Include="..\..\Utilities\Cryptography\AliasVault.Cryptography.Client\AliasVault.Cryptography.Client.csproj" />
<ProjectReference Include="..\..\Utilities\Cryptography\AliasVault.Cryptography.Server\AliasVault.Cryptography.Server.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,92 @@
//-----------------------------------------------------------------------
// <copyright file="AvexHeader.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.Models.Exports;
/// <summary>
/// Represents the header for an .avex (encrypted export) file.
/// </summary>
public class AvexHeader
{
/// <summary>
/// Gets or sets the file format identifier.
/// </summary>
public string Format { get; set; } = "avex";
/// <summary>
/// Gets or sets the format version.
/// </summary>
public string Version { get; set; } = "1.0.0";
/// <summary>
/// Gets or sets the key derivation function parameters.
/// </summary>
public KdfParams Kdf { get; set; } = new();
/// <summary>
/// Gets or sets the encryption parameters.
/// </summary>
public EncryptionParams Encryption { get; set; } = new();
/// <summary>
/// Gets or sets the export metadata.
/// </summary>
public AvexMetadata Metadata { get; set; } = new();
}
/// <summary>
/// Key derivation function parameters.
/// </summary>
public class KdfParams
{
/// <summary>
/// Gets or sets the KDF algorithm type.
/// </summary>
public string Type { get; set; } = "Argon2id";
/// <summary>
/// Gets or sets the salt value (base64-encoded).
/// </summary>
public string Salt { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the algorithm-specific parameters.
/// </summary>
public Dictionary<string, int> Params { get; set; } = new();
}
/// <summary>
/// Encryption algorithm parameters.
/// </summary>
public class EncryptionParams
{
/// <summary>
/// Gets or sets the encryption algorithm name.
/// </summary>
public string Algorithm { get; set; } = "AES-256-GCM";
/// <summary>
/// Gets or sets the byte offset where encrypted data begins.
/// </summary>
public long EncryptedDataOffset { get; set; }
}
/// <summary>
/// Metadata about the export.
/// </summary>
public class AvexMetadata
{
/// <summary>
/// Gets or sets the timestamp when the export was created.
/// </summary>
public DateTime ExportedAt { get; set; }
/// <summary>
/// Gets or sets the username who created the export.
/// </summary>
public string ExportedBy { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,114 @@
//-----------------------------------------------------------------------
// <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.Models.Exports;
/// <summary>
/// Service for creating encrypted .avex export files.
/// </summary>
public static class VaultEncryptedExportService
{
private const string HeaderDelimiter = "\n--- ENCRYPTED PAYLOAD FOLLOWS ---\n";
/// <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 = "avex",
Version = "1.0.0",
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,
},
};
// 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(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;
}
}

View File

@@ -0,0 +1,194 @@
//-----------------------------------------------------------------------
// <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.Models.Exports;
/// <summary>
/// Service for importing encrypted .avex export files.
/// </summary>
public static class VaultEncryptedImportService
{
private const string HeaderDelimiter = "\n--- ENCRYPTED PAYLOAD FOLLOWS ---\n";
/// <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\": \"avex\"") ||
headerText.Contains("\"format\":\"avex\"");
}
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 != "1.0.0")
{
throw new InvalidOperationException($"Unsupported .avex version: {header.Version}. Expected 1.0.0.");
}
// 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(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 != "avex")
{
throw new InvalidOperationException($"Invalid .avex file: expected format 'avex', 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;
}
}