From 21e02ab3cace10e0361fbf85f1a698e9ba8faa5c Mon Sep 17 00:00:00 2001 From: Bangara Raju Kottedi Date: Sun, 28 Apr 2024 13:22:57 +0530 Subject: [PATCH] 1. AES Encryption/Decryption Tool 2. Encrypted Connection string 3. Added API Security using ApiKey --- .gitignore | 3 +- AesEncryption/AesEncryption.csproj | 10 ++ AesEncryption/Program.cs | 138 ++++++++++++++++++ PortBlog.API.sln | 8 +- .../Extensions/ConfigurationExtensions.cs | 44 ++++++ PortBlog.API/Middleware/ApiKeyMiddleware.cs | 40 +++++ PortBlog.API/Models/PostDto.cs | 6 + PortBlog.API/Models/ResumeDto.cs | 8 + PortBlog.API/Profiles/BlogProfile.cs | 10 +- PortBlog.API/Profiles/ResumeProfile.cs | 2 +- PortBlog.API/Program.cs | 38 ++++- PortBlog.API/Repositories/ResumeRepository.cs | 2 +- PortBlog.API/appsettings.Development.json | 7 +- PortBlog.API/appsettings.Production.json | 12 -- 14 files changed, 308 insertions(+), 20 deletions(-) create mode 100644 AesEncryption/AesEncryption.csproj create mode 100644 AesEncryption/Program.cs create mode 100644 PortBlog.API/Extensions/ConfigurationExtensions.cs create mode 100644 PortBlog.API/Middleware/ApiKeyMiddleware.cs delete mode 100644 PortBlog.API/appsettings.Production.json diff --git a/.gitignore b/.gitignore index 9491a2f..44a94b0 100644 --- a/.gitignore +++ b/.gitignore @@ -360,4 +360,5 @@ MigrationBackup/ .ionide/ # Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +FodyWeavers.xsd +/PortBlog.API/appsettings.Production.json diff --git a/AesEncryption/AesEncryption.csproj b/AesEncryption/AesEncryption.csproj new file mode 100644 index 0000000..2150e37 --- /dev/null +++ b/AesEncryption/AesEncryption.csproj @@ -0,0 +1,10 @@ + + + + Exe + net8.0 + enable + enable + + + diff --git a/AesEncryption/Program.cs b/AesEncryption/Program.cs new file mode 100644 index 0000000..c849d27 --- /dev/null +++ b/AesEncryption/Program.cs @@ -0,0 +1,138 @@ +// See https://aka.ms/new-console-template for more information +using System.Security.Cryptography; + +string keyBase64; +string cipherText; +string vectorBase64; +string plainText; + +Console.WriteLine("Welcome to the Aes Encryption/Decryption tool"); +Select: +Console.WriteLine("Please select the action you want to perform: /n Press 1 to Generate Key. /n Press 2 to Encrypt using the key. /n Press 3 to Decrpt using the key."); + +string? action = Console.ReadLine(); + +if (action == null || !int.TryParse(action, out int actionId) || !new List{ 1, 2, 3 }.Contains(actionId)) +{ + Console.WriteLine("Invalid option. /n"); + goto Select; +} + +switch (actionId) +{ + case 1: + Console.WriteLine("Creating Aes Encryption 256 bit key"); + using (Aes aesAlgorithm = Aes.Create()) + { + aesAlgorithm.KeySize = 256; + aesAlgorithm.GenerateKey(); + keyBase64 = Convert.ToBase64String(aesAlgorithm.Key); + Console.WriteLine($"Aes Key Size : {aesAlgorithm.KeySize}"); + Console.WriteLine("Here is the Aes key in Base64:"); + Console.WriteLine(keyBase64); + } + break; + case 2: + Console.WriteLine("Encrypt Text"); + Console.WriteLine("Provide the Aes Key in base64 format :"); + keyBase64 = Console.ReadLine(); + Console.WriteLine("--------------------------------------------------------------"); + Console.WriteLine("Please enter the text that you want to encrypt:"); + plainText = Console.ReadLine(); + Console.WriteLine("--------------------------------------------------------------"); + cipherText = EncryptDataWithAes(plainText, keyBase64, out vectorBase64); + + Console.WriteLine("--------------------------------------------------------------"); + Console.WriteLine("Here is the cipher text with vector:"); + Console.WriteLine(cipherText + ":" + vectorBase64); + break; + case 3: + Console.WriteLine("Please enter the text that you want to decrypt:"); + cipherText = Console.ReadLine(); + Console.WriteLine("--------------------------------------------------------------"); + + Console.WriteLine("Provide the Aes Key:"); + keyBase64 = Console.ReadLine(); + Console.WriteLine("--------------------------------------------------------------"); + + vectorBase64 = cipherText.Split(":")[1]; + cipherText = cipherText.Split(":")[0]; + + plainText = DecryptDataWithAes(cipherText, keyBase64, vectorBase64); + + Console.WriteLine("--------------------------------------------------------------"); + Console.WriteLine("Here is the decrypted data:"); + Console.WriteLine(plainText); + break; +} + +goto Select; + + +static string EncryptDataWithAes(string plainText, string keyBase64, out string vectorBase64) +{ + using (Aes aesAlgorithm = Aes.Create()) + { + aesAlgorithm.Key = Convert.FromBase64String(keyBase64); + aesAlgorithm.GenerateIV(); + Console.WriteLine($"Aes Cipher Mode : {aesAlgorithm.Mode}"); + Console.WriteLine($"Aes Padding Mode: {aesAlgorithm.Padding}"); + Console.WriteLine($"Aes Key Size : {aesAlgorithm.KeySize}"); + + //set the parameters with out keyword + vectorBase64 = Convert.ToBase64String(aesAlgorithm.IV); + + // Create encryptor object + ICryptoTransform encryptor = aesAlgorithm.CreateEncryptor(); + + byte[] encryptedData; + + //Encryption will be done in a memory stream through a CryptoStream object + using (MemoryStream ms = new MemoryStream()) + { + using (CryptoStream cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) + { + using (StreamWriter sw = new StreamWriter(cs)) + { + sw.Write(plainText); + } + encryptedData = ms.ToArray(); + } + } + + return Convert.ToBase64String(encryptedData); + } +} + +static string DecryptDataWithAes(string cipherText, string keyBase64, string vectorBase64) +{ + using (Aes aesAlgorithm = Aes.Create()) + { + aesAlgorithm.Key = Convert.FromBase64String(keyBase64); + aesAlgorithm.IV = Convert.FromBase64String(vectorBase64); + + Console.WriteLine($"Aes Cipher Mode : {aesAlgorithm.Mode}"); + Console.WriteLine($"Aes Padding Mode: {aesAlgorithm.Padding}"); + Console.WriteLine($"Aes Key Size : {aesAlgorithm.KeySize}"); + Console.WriteLine($"Aes Block Size : {aesAlgorithm.BlockSize}"); + + + // Create decryptor object + ICryptoTransform decryptor = aesAlgorithm.CreateDecryptor(); + + byte[] cipher = Convert.FromBase64String(cipherText); + + //Decryption will be done in a memory stream through a CryptoStream object + using (MemoryStream ms = new MemoryStream(cipher)) + { + using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) + { + using (StreamReader sr = new StreamReader(cs)) + { + return sr.ReadToEnd(); + } + } + } + } +} + diff --git a/PortBlog.API.sln b/PortBlog.API.sln index 3246ead..22ea6e5 100644 --- a/PortBlog.API.sln +++ b/PortBlog.API.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.9.34701.34 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PortBlog.API", "PortBlog.API\PortBlog.API.csproj", "{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PortBlog.API", "PortBlog.API\PortBlog.API.csproj", "{2E50B5D7-56E2-4E89-8742-BB57FF4245F9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AesEncryption", "AesEncryption\AesEncryption.csproj", "{26654BFD-EE9B-49BA-84BA-9156AC348076}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +17,10 @@ Global {2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|Any CPU.ActiveCfg = Release|Any CPU {2E50B5D7-56E2-4E89-8742-BB57FF4245F9}.Release|Any CPU.Build.0 = Release|Any CPU + {26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26654BFD-EE9B-49BA-84BA-9156AC348076}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26654BFD-EE9B-49BA-84BA-9156AC348076}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PortBlog.API/Extensions/ConfigurationExtensions.cs b/PortBlog.API/Extensions/ConfigurationExtensions.cs new file mode 100644 index 0000000..dd0763a --- /dev/null +++ b/PortBlog.API/Extensions/ConfigurationExtensions.cs @@ -0,0 +1,44 @@ +using System.Security.Cryptography; + +namespace PortBlog.API.Extensions +{ + public static class ConfigurationExtensions + { + public static string DecryptConnectionString(this IConfiguration configuration, string encryptedConnectionString) + { + string keyBase64 = configuration.GetValue("ConnectionStrings:Key").ToString(); + + string vectorBase64 = encryptedConnectionString.Split(":")[1]; + string cipherText = encryptedConnectionString.Split(":")[0]; + + using (Aes aesAlgorithm = Aes.Create()) + { + aesAlgorithm.Key = Convert.FromBase64String(keyBase64); + aesAlgorithm.IV = Convert.FromBase64String(vectorBase64); + + Console.WriteLine($"Aes Cipher Mode : {aesAlgorithm.Mode}"); + Console.WriteLine($"Aes Padding Mode: {aesAlgorithm.Padding}"); + Console.WriteLine($"Aes Key Size : {aesAlgorithm.KeySize}"); + Console.WriteLine($"Aes Block Size : {aesAlgorithm.BlockSize}"); + + + // Create decryptor object + ICryptoTransform decryptor = aesAlgorithm.CreateDecryptor(); + + byte[] cipher = Convert.FromBase64String(cipherText); + + //Decryption will be done in a memory stream through a CryptoStream object + using (MemoryStream ms = new MemoryStream(cipher)) + { + using (CryptoStream cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) + { + using (StreamReader sr = new StreamReader(cs)) + { + return sr.ReadToEnd(); + } + } + } + } + } + } +} diff --git a/PortBlog.API/Middleware/ApiKeyMiddleware.cs b/PortBlog.API/Middleware/ApiKeyMiddleware.cs new file mode 100644 index 0000000..838b4ee --- /dev/null +++ b/PortBlog.API/Middleware/ApiKeyMiddleware.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace PortBlog.API.Middleware +{ + public class ApiKeyMiddleware + { + private readonly RequestDelegate _next; + private const string APIKEY = "XApiKey"; + public ApiKeyMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task InvokeAsync(HttpContext context) + { + if (!context.Request.Headers.TryGetValue(APIKEY, out var extractedApiKey)) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync("Api key was not provided"); + return; + } + + var appSettings = context.RequestServices.GetRequiredService(); + + var apiKey = appSettings.GetValue(APIKEY); + + if (apiKey != null && !apiKey.Equals(extractedApiKey)) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + await context.Response.WriteAsync("Unauthorized client"); + return; + } + + await _next(context); + } + } +} diff --git a/PortBlog.API/Models/PostDto.cs b/PortBlog.API/Models/PostDto.cs index f07e809..0ac0944 100644 --- a/PortBlog.API/Models/PostDto.cs +++ b/PortBlog.API/Models/PostDto.cs @@ -19,5 +19,11 @@ public int Views { get; set; } = 0; public int Comments { get; set; } = 0; + + public string? Image { get; set; } + + public string? CreatedDate { get; set; } + + public string? ModifiedDate { get; set; } } } diff --git a/PortBlog.API/Models/ResumeDto.cs b/PortBlog.API/Models/ResumeDto.cs index cc5e82d..d124ed7 100644 --- a/PortBlog.API/Models/ResumeDto.cs +++ b/PortBlog.API/Models/ResumeDto.cs @@ -31,5 +31,13 @@ public ICollection Hobbies { get; set; } = new List(); public ICollection Projects { get; set; } = new List(); + + public ICollection ProjectsCategories + { + get + { + return Projects.Select(p => p.Category).Distinct().ToList(); + } + } } } diff --git a/PortBlog.API/Profiles/BlogProfile.cs b/PortBlog.API/Profiles/BlogProfile.cs index 23e9afe..b1c8575 100644 --- a/PortBlog.API/Profiles/BlogProfile.cs +++ b/PortBlog.API/Profiles/BlogProfile.cs @@ -9,7 +9,15 @@ namespace PortBlog.API.Profiles public BlogProfile() { CreateMap(); - CreateMap(); + CreateMap() + .ForMember( + dest => dest.CreatedDate, + opts => opts.MapFrom(src => src.CreatedDate != null ? src.CreatedDate.Value.ToString("MMM dd, yyyy") : string.Empty) + ) + .ForMember( + dest => dest.ModifiedDate, + opts => opts.MapFrom(src => src.CreatedDate != null ? src.ModifiedDate.Value.ToString("MMM dd, yyyy") : string.Empty) + ); } } } diff --git a/PortBlog.API/Profiles/ResumeProfile.cs b/PortBlog.API/Profiles/ResumeProfile.cs index 06d7ef4..d418302 100644 --- a/PortBlog.API/Profiles/ResumeProfile.cs +++ b/PortBlog.API/Profiles/ResumeProfile.cs @@ -24,7 +24,7 @@ namespace PortBlog.API.Profiles CreateMap() .ForMember( dest => dest.Period, - src => src.MapFrom(src => src.StartDate.Year + " - " + (src.EndDate != null ? src.EndDate.Value.Year : "Present")) + src => src.MapFrom(src => src.StartDate.ToString("MMM yyyy") + " - " + (src.EndDate != null ? src.EndDate.Value.ToString("MMM yyyy") : "Present")) ) .ForMember ( diff --git a/PortBlog.API/Program.cs b/PortBlog.API/Program.cs index 8471706..1b4b01c 100644 --- a/PortBlog.API/Program.cs +++ b/PortBlog.API/Program.cs @@ -1,7 +1,9 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.FileProviders; +using Microsoft.OpenApi.Models; using PortBlog.API.DbContexts; using PortBlog.API.Extensions; +using PortBlog.API.Middleware; using Serilog; Log.Logger = new LoggerConfiguration() @@ -25,10 +27,42 @@ builder.Services.AddProblemDetails(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); +builder.Services.AddSwaggerGen(c => +{ + c.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme + { + Description = "ApiKey must appear in header", + Type = SecuritySchemeType.ApiKey, + Name = "XApiKey", + In = ParameterLocation.Header, + Scheme = "ApiKeyScheme" + }); + + var key = new OpenApiSecurityScheme() + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ApiKey" + }, + In = ParameterLocation.Header + }; + + var requirement = new OpenApiSecurityRequirement() + { + {key, new List {} } + }; + + c.AddSecurityRequirement(requirement); +}); var connectionString = builder.Configuration.GetConnectionString("PortBlogDBConnectionString"); +if (builder.Configuration.GetValue("ConnectionStrings:Encryption")) +{ + connectionString = builder.Configuration.DecryptConnectionString(connectionString); +} + if (string.IsNullOrEmpty(connectionString)) { throw new Exception("Connection string cannot be empty"); @@ -66,6 +100,8 @@ app.UseStaticFiles(new StaticFileOptions() app.UseAuthorization(); +app.UseMiddleware(); + app.MapControllers(); app.Run(); diff --git a/PortBlog.API/Repositories/ResumeRepository.cs b/PortBlog.API/Repositories/ResumeRepository.cs index 4162797..916646b 100644 --- a/PortBlog.API/Repositories/ResumeRepository.cs +++ b/PortBlog.API/Repositories/ResumeRepository.cs @@ -24,7 +24,7 @@ namespace PortBlog.API.Repositories .ThenInclude(e => e.Details) .Include(r => r.SocialLinks) .ThenInclude(s => s.Blog) - .ThenInclude(b => b.Posts.Take(5)) + .ThenInclude(b => b.Posts.OrderByDescending(p => p.Likes).Take(5)) .Include(r => r.Hobbies) .Include(r => r.Certifications) .Include(r => r.Skills) diff --git a/PortBlog.API/appsettings.Development.json b/PortBlog.API/appsettings.Development.json index 84b1da8..6c5f3a2 100644 --- a/PortBlog.API/appsettings.Development.json +++ b/PortBlog.API/appsettings.Development.json @@ -1,11 +1,14 @@ { "ConnectionStrings": { - "PortBlogDBConnectionString": "SERVER=192.168.0.197; DATABASE=cv_blog; UID=PortBlogDevUser; PWD=p@$$w0rd1234" + "PortBlogDBConnectionString": "SERVER=192.168.0.197; DATABASE=cv_blog; UID=PortBlogDevUser; PWD=p@$$w0rd1234", + "Encryption": "false", + "Key": "rgdBsYjrgQV9YaE+6QFK5oyTOWwbl2bSWkuc2JXcIyw=" }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } - } + }, + "XApiKey": "c6eAXYcNT873TT7BfMgQyS4ii7hxa53TLEUN7pAGaaU=" } diff --git a/PortBlog.API/appsettings.Production.json b/PortBlog.API/appsettings.Production.json deleted file mode 100644 index a683bd2..0000000 --- a/PortBlog.API/appsettings.Production.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "ConnectionStrings": { - "PortBlogDBConnectionString": "SERVER=10.1.0.10;DATABASE=cv_blog;UID=PortBlogProdUser;PWD=pr0dp@$$w0rd6534" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft.AspNetCore": "Warning" - } - }, - "AllowedHosts": "*" -} \ No newline at end of file