using System.Security.Cryptography; using System.Text; using Azaion.Common.Configs; using Azaion.Common.Database; using Azaion.Common.Entities; using Azaion.Common.Requests; using LinqToDB; using Microsoft.Extensions.Options; namespace Azaion.Services; public interface IResourceUpdateService { Task> GetUpdate(GetUpdateRequest request, CancellationToken ct = default); Task Publish(PublishResourceRequest request, CancellationToken ct = default); } public class ResourceUpdateService( IDbFactory dbFactory, ICache cache, IOptions resourcesConfig) : IResourceUpdateService { public static string CacheKey(string architecture, string devStage) => $"Resources.Latest.{architecture}.{devStage}"; public async Task> GetUpdate(GetUpdateRequest request, CancellationToken ct = default) { var latest = await cache.GetFromCacheAsync( CacheKey(request.Architecture, request.DevStage), () => LoadLatest(request.Architecture, request.DevStage, ct)); var updates = new List(); foreach (var (resourceName, resource) in latest) { var currentVersion = request.CurrentVersions.GetValueOrDefault(resourceName, ""); if (string.CompareOrdinal(resource.Version, currentVersion) <= 0) continue; updates.Add(new ResourceUpdateItem { ResourceName = resource.ResourceName, Version = resource.Version, CdnUrl = resource.CdnUrl, Sha256 = resource.Sha256, EncryptionKey = ResourceColumnEncryption.Decrypt(resource.EncryptionKey, MasterKey), SizeBytes = resource.SizeBytes }); } return updates; } public async Task Publish(PublishResourceRequest request, CancellationToken ct = default) { await dbFactory.RunAdmin(async db => { await db.InsertAsync(new Resource { Id = Guid.NewGuid(), ResourceName = request.ResourceName, DevStage = request.DevStage, Architecture = request.Architecture, Version = request.Version, CdnUrl = request.CdnUrl, Sha256 = request.Sha256, EncryptionKey = ResourceColumnEncryption.Encrypt(request.EncryptionKey, MasterKey), SizeBytes = request.SizeBytes, CreatedAt = DateTime.UtcNow }, token: ct); }); cache.Invalidate(CacheKey(request.Architecture, request.DevStage)); } private async Task> LoadLatest(string architecture, string devStage, CancellationToken ct) => await dbFactory.Run(async db => { var rows = await db.Resources .Where(r => r.Architecture == architecture && r.DevStage == devStage) .ToListAsync(token: ct); return rows .GroupBy(r => r.ResourceName) .Select(g => g.OrderByDescending(r => r.Version, StringComparer.Ordinal).First()) .ToDictionary(r => r.ResourceName); }); private string MasterKey { get { var key = resourcesConfig.Value.EncryptionMasterKey; if (string.IsNullOrEmpty(key)) throw new InvalidOperationException( "ResourcesConfig.EncryptionMasterKey is not configured. Set it via " + "appsettings ResourcesConfig:EncryptionMasterKey or env ResourcesConfig__EncryptionMasterKey."); return key; } } } internal static class ResourceColumnEncryption { public static string Encrypt(string plaintext, string masterKey) { using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(masterKey)); aes.GenerateIV(); var input = Encoding.UTF8.GetBytes(plaintext); using var encryptor = aes.CreateEncryptor(); var cipher = encryptor.TransformFinalBlock(input, 0, input.Length); var combined = new byte[aes.IV.Length + cipher.Length]; Buffer.BlockCopy(aes.IV, 0, combined, 0, aes.IV.Length); Buffer.BlockCopy(cipher, 0, combined, aes.IV.Length, cipher.Length); return Convert.ToBase64String(combined); } public static string Decrypt(string ciphertextBase64, string masterKey) { var combined = Convert.FromBase64String(ciphertextBase64); using var aes = Aes.Create(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(masterKey)); var ivLen = aes.BlockSize / 8; var iv = new byte[ivLen]; Buffer.BlockCopy(combined, 0, iv, 0, ivLen); aes.IV = iv; var cipher = new byte[combined.Length - ivLen]; Buffer.BlockCopy(combined, ivLen, cipher, 0, cipher.Length); using var decryptor = aes.CreateDecryptor(); var plain = decryptor.TransformFinalBlock(cipher, 0, cipher.Length); return Encoding.UTF8.GetString(plain); } }