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()); } } }