using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; using System.Security.Cryptography; using System.Text; public class EncryptResponseAttribute : TypeFilterAttribute { public EncryptResponseAttribute() : base(typeof(EncryptResponseFilter)) { } private class EncryptResponseFilter : IAsyncResultFilter { // 32-byte Key //private readonly string _keyBase64 = "h9J4kL2mN5pQ8rS1tV3wX6yZ0aB7cD9eF1gH3jK5mN6="; private readonly string _keyBase64 = "u4J7p9Qx2hF5vYtLz8Kq3mN1sG0bRwXyZcD6eH8jFQw="; 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; // 1. Convert string to bytes directly (Avoids StreamWriter encoding issues) var plainBytes = Encoding.UTF8.GetBytes(plainText); using var ms = new MemoryStream(); // 2. Write IV (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)) { // 3. Write Data await cs.WriteAsync(plainBytes, 0, plainBytes.Length); // 4. CRITICAL: Flush the final block (Padding) to the MemoryStream // Without this, Dart receives incomplete data and throws "Invalid Padding" cs.FlushFinalBlock(); } // 5. Convert full stream to Base64 return Convert.ToBase64String(ms.ToArray()); } //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()); //} } }