diff --git a/Marco.Pms.Services/Middleware/EncryptionMiddleware.cs b/Marco.Pms.Services/Middleware/EncryptionMiddleware.cs new file mode 100644 index 0000000..a62d1a9 --- /dev/null +++ b/Marco.Pms.Services/Middleware/EncryptionMiddleware.cs @@ -0,0 +1,79 @@ +using Marco.Pms.Services.Service.ServiceInterfaces; +using System.Text; + +public class EncryptionMiddleware +{ + private readonly RequestDelegate _next; + private readonly IAesEncryption _encryptionService; + + // Define the paths you want to SKIP encryption for + private readonly List _ignoredPaths = new List + { + "/hubs/marco", + "/swagger" // Always exclude swagger UI + }; + + public EncryptionMiddleware(RequestDelegate next, IAesEncryption encryptionService) + { + _next = next; + _encryptionService = encryptionService; + } + + public async Task InvokeAsync(HttpContext context) + { + // 1. CHECK EXCLUSIONS + // If the path matches an ignored path, skip logic and continue normally + var path = context.Request.Path.Value?.ToLower(); + + // Condition A: Skip if path is in the ignored list + bool isIgnoredPath = _ignoredPaths.Any(p => path != null && path.StartsWith(p.ToLower())); + //bool isIgnoredPath = (path != null && !path.StartsWith("/api/expense/list")); + + // Condition B: User requested to ONLY encrypt 'GET' methods. + // If the method is POST, PUT, DELETE, etc., we skip encryption. + //bool isNotGetMethod = !HttpMethods.IsGet(context.Request.Method); + //if (isIgnoredPath || isNotGetMethod) + if (isIgnoredPath) + { + await _next(context); + return; + } + + // 2. PREPARE TO CAPTURE RESPONSE + // We hold onto the original stream to write back to it later + var originalBodyStream = context.Response.Body; + + using (var memoryStream = new MemoryStream()) + { + // Point the response body to our memory stream + context.Response.Body = memoryStream; + + // 3. EXECUTE THE PIPELINE (The Controller runs here) + await _next(context); + + // 4. ENCRYPT RESPONSE + + // Reset pointer to read the stream + memoryStream.Seek(0, SeekOrigin.Begin); + + // Read the plain JSON + var plainBodyText = await new StreamReader(memoryStream).ReadToEndAsync(); + + // Encrypt it + var encryptedBodyBase64 = _encryptionService.EncryptResponse(plainBodyText); + var encryptedBytes = Encoding.UTF8.GetBytes(encryptedBodyBase64); + + // 5. WRITE TO ORIGINAL STREAM + // Switch back to the original stream + context.Response.Body = originalBodyStream; + + // Important: Update Content-Length because the size changed + context.Response.ContentLength = encryptedBytes.Length; + + // Optional: Change Content-Type to text/plain since it's now a Base64 string, not JSON + // context.Response.ContentType = "text/plain"; + + await context.Response.Body.WriteAsync(encryptedBytes, 0, encryptedBytes.Length); + } + } +} \ No newline at end of file diff --git a/Marco.Pms.Services/Program.cs b/Marco.Pms.Services/Program.cs index f15dba7..efb2bf2 100644 --- a/Marco.Pms.Services/Program.cs +++ b/Marco.Pms.Services/Program.cs @@ -188,7 +188,6 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -215,6 +214,7 @@ builder.Services.AddScoped(); // Singleton services (one instance for the app's lifetime) builder.Services.AddSingleton(); +builder.Services.AddSingleton(); string path = Path.Combine(builder.Environment.ContentRootPath, "FireBase", "service-account.json"); @@ -244,6 +244,7 @@ var app = builder.Build(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); +app.UseMiddleware(); #endregion #region Development Environment Configuration diff --git a/Marco.Pms.Services/Service/AesEncryption.cs b/Marco.Pms.Services/Service/AesEncryption.cs index 29ff568..c167aa6 100644 --- a/Marco.Pms.Services/Service/AesEncryption.cs +++ b/Marco.Pms.Services/Service/AesEncryption.cs @@ -32,5 +32,35 @@ namespace Marco.Pms.Services.Service return Encoding.UTF8.GetString(plaintext); } + + public string EncryptResponse(string plainText) + { + var key = Convert.FromBase64String("h9J4kL2mN5pQ8rS1tV3wX6yZ0aB7cD9eF1gH3jK5mN6="); + if (string.IsNullOrEmpty(plainText)) return plainText; + + var plainBytes = Encoding.UTF8.GetBytes(plainText); + + // 1. Generate Nonce (12 bytes) + var nonce = new byte[12]; + RandomNumberGenerator.Fill(nonce); + + // 2. Prepare Buffers + var tag = new byte[16]; + var ciphertext = new byte[plainBytes.Length]; + + // 3. Encrypt + using (var aesGcm = new AesGcm(key, 16)) + { + aesGcm.Encrypt(nonce, plainBytes, ciphertext, tag); + } + + // 4. Combine: [Nonce] + [Ciphertext] + [Tag] + var combined = new byte[nonce.Length + ciphertext.Length + tag.Length]; + Buffer.BlockCopy(nonce, 0, combined, 0, nonce.Length); + Buffer.BlockCopy(ciphertext, 0, combined, nonce.Length, ciphertext.Length); + Buffer.BlockCopy(tag, 0, combined, nonce.Length + ciphertext.Length, tag.Length); + + return Convert.ToBase64String(combined); + } } } diff --git a/Marco.Pms.Services/Service/ServiceInterfaces/IAesEncryption.cs b/Marco.Pms.Services/Service/ServiceInterfaces/IAesEncryption.cs index c54df66..0dbcdbe 100644 --- a/Marco.Pms.Services/Service/ServiceInterfaces/IAesEncryption.cs +++ b/Marco.Pms.Services/Service/ServiceInterfaces/IAesEncryption.cs @@ -4,5 +4,6 @@ { (byte[] ciphertext, byte[] nonce, byte[] tag) Encrypt(string plaintext, byte[] key); string Decrypt(byte[] ciphertext, byte[] nonce, byte[] tag, byte[] key); + string EncryptResponse(string plainText); } }