diff --git a/Marco.Pms.Services/Controllers/CollectionController.cs b/Marco.Pms.Services/Controllers/CollectionController.cs index 61b18b4..ab6f8c1 100644 --- a/Marco.Pms.Services/Controllers/CollectionController.cs +++ b/Marco.Pms.Services/Controllers/CollectionController.cs @@ -28,9 +28,11 @@ using Invoice = Marco.Pms.Model.Collection.Invoice; namespace Marco.Pms.Services.Controllers { - [Route("api/[controller]")] - [ApiController] [Authorize] + [ApiController] + [EncryptResponse] + [Route("api/[controller]")] + public class CollectionController : ControllerBase { private readonly IDbContextFactory _dbContextFactory; diff --git a/Marco.Pms.Services/Extensions/EncryptResponseAttribute.cs b/Marco.Pms.Services/Extensions/EncryptResponseAttribute.cs new file mode 100644 index 0000000..ad6a580 --- /dev/null +++ b/Marco.Pms.Services/Extensions/EncryptResponseAttribute.cs @@ -0,0 +1,99 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; +using System.Security.Cryptography; + +public class EncryptResponseAttribute : TypeFilterAttribute +{ + public EncryptResponseAttribute() : base(typeof(EncryptResponseFilter)) + { + } + + private class EncryptResponseFilter : IAsyncResultFilter + { + // 32-byte Key + private readonly string _keyBase64 = "h9J4kL2mN5pQ8rS1tV3wX6yZ0aB7cD9eF1gH3jK5mN6="; + + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + // 1. EXECUTE THE CONTROLLER FIRST + // We let the controller run to populate context.Result + // Note: We are intercepting *before* the response goes to the client. + + try + { + if (context.Result is ObjectResult objectResult && objectResult.Value != null) + { + // 2. SERIALIZE (Safe Settings) + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + NullValueHandling = NullValueHandling.Ignore + }; + + var plainJson = JsonConvert.SerializeObject(objectResult.Value, settings); + + // 3. ENCRYPT ASYNC (Prevents Thread Blocking 502) + var encryptedText = await EncryptAsync(plainJson); + + // 4. RETURN CONTENT RESULT + // Use ContentResult to send raw text. + // OkObjectResult would try to JSON-serialize the string again (adding quotes). + context.Result = new ContentResult + { + Content = encryptedText, + ContentType = "text/plain", + StatusCode = 200 + }; + } + } + catch (Exception ex) + { + // FAIL-SAFE LOGGING + Console.WriteLine($"Encryption Crashed: {ex.Message}"); + // We do NOT modify context.Result here. + // The original unencrypted ObjectResult will flow through to the client. + // This ensures the user gets DATA, not a 502. + } + + await next(); + } + + private async Task EncryptAsync(string plainText) + { + if (string.IsNullOrEmpty(plainText)) return plainText; + + using var aes = Aes.Create(); + aes.Key = Convert.FromBase64String(_keyBase64); + aes.GenerateIV(); + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + + // We do NOT use 'using' on the MemoryStream here yet, + // because we need to read from it after the CryptoStream finishes. + using var ms = new MemoryStream(); + + // Write IV first (16 bytes) + ms.Write(aes.IV, 0, aes.IV.Length); + + using (var encryptor = aes.CreateEncryptor(aes.Key, aes.IV)) + using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + using (var sw = new StreamWriter(cs)) + { + // CRITICAL FIX: Use Async Write + await sw.WriteAsync(plainText); + + // Flush the writer, but do not close the underlying streams yet via 'using' exit + await sw.FlushAsync(); + } + + // At this point, CryptoStream is closed (disposed by using block), + // causing the final block to be flushed to MemoryStream. + // MemoryStream is technically closed, but .NET allows ToArray() on closed MemoryStreams. + + return Convert.ToBase64String(ms.ToArray()); + } + } +} \ No newline at end of file