Back to: ASP.NET Core Web API Tutorials
Encryption and Decryption in ASP.NET Core Web API
In this article, I will discuss how to Implement AES Encryption and Decryption in ASP.NET Core Web API Application with an Example. Please read our previous article discussing how to Implement HMAC Authentication in an ASP.NET Core Web API Application with an Example.
What is Encryption?
Encryption is the process of converting plain text or readable data into an unreadable format known as ciphertext. The goal is to ensure data confidentiality, making it inaccessible to unauthorized individuals or systems. This transformation is done using a cryptographic algorithm and an encryption key. Only individuals or systems with the corresponding decryption key can convert the data back into its original readable format, ensuring confidentiality.
What is Decryption?
Decryption is the reverse process of encryption, where the encrypted data (ciphertext) is transformed back into its original readable format (plaintext). Decryption requires the appropriate key (which may be the same as the encryption key or different, depending on the encryption method). This ensures that authorized users or systems can access the original data while maintaining security during transmission.
Why Do We Need Encryption and Decryption in ASP.NET Core Web API?
Encryption and decryption are essential for protecting data during transmission, particularly when sensitive or confidential information is exchanged over insecure networks (like the Internet). Without encryption, anyone who intercepts the data can read it, including malicious parties.
By encrypting data before transmission and decrypting it only at the receiving end, organizations and individuals can ensure that only authorized parties can access and read the data, thus preserving confidentiality and integrity. The following are the key reasons for using Encryption and Decryption:
- Data Security: Prevents sensitive information from being accessed by unauthorized parties.
- Data Integrity: Ensures that the data is not altered during transmission.
- Privacy Compliance: Adheres to regulations like GDPR, HIPAA, and PCI DSS, which mandate secure data handling.
- Secure Communication: Protects sensitive communications between systems, such as client-server interactions.
- Risk Mitigation: Reduces the likelihood of data breaches, fraud, and cyberattacks.
Techniques for Encryption and Decryption:
There are two main types of encryptions:
Symmetric Encryption:
- In the case of Symmetric Encryption, it uses the same key for both encryption and decryption. Faster but requires secure key exchange. The key must be kept secret between the sender and receiver.
- Examples include AES (Advanced Encryption Standard, Recommended Approach) and DES (Data Encryption Standard, not Recommended due to vulnerabilities).
Asymmetric Encryption:
- In asymmetric encryption, two keys are used: a public key for encryption and a private key for decryption. This method is more secure because the public key can be freely distributed while the private key remains confidential.
- For example, RSA and Elliptic Curve Cryptography (ECC).
What is the Advanced Encryption Standard (AES) Algorithm?
The Advanced Encryption Standard (AES) is a symmetric key encryption algorithm used widely across the world to secure data. It was established as an encryption standard by the U.S. National Institute of Standards and Technology (NIST) in 2001. AES is used to protect sensitive information in both government and commercial sectors.
What is the Data Encryption Standard (DES) Algorithm?
DES is an older symmetric encryption algorithm that uses a 56-bit key. It was widely used in the 20th century but is now considered insecure due to its susceptibility to brute-force attacks. Modern alternatives like AES are recommended.
AES Algorithm Working Flow:
AES uses the same key for both encrypting and decrypting data, which means both the sender and the receiver must have access to the same secret key. To understand the AES Algorithm Working Flow, please have a look at the following diagram.
Let us Understand How the AES Algorithm Working Works.
Key Sharing (Secure Channel):
To Implement AES encryption and decryption, we need a shared secret key, which must be securely shared between the sender and the receiver. The secret key must be exchanged securely through a protected channel to prevent it from being intercepted by unauthorized parties. This step ensures that both parties (sender and receiver) have the same secret key, which is crucial for both encrypting and decrypting the data.
AES Encryption Process (Plaintext to Ciphertext):
Let us first understand the AES Encryption Process step by step:
- Sender Prepares the Plaintext: The sender prepares the data to be transmitted. This data is in its original, readable form, called plaintext, and needs to be securely transmitted.
- Encryption Server Receives the Plaintext: The plaintext is sent to the Encryption Server, where the encryption process takes place using the AES (Advanced Encryption Standard) algorithm.
- AES Algorithm Encrypts the Data (Ciphertext): The AES algorithm encrypts the plaintext using the shared Secret Key, converting the plaintext into Ciphertext (an unreadable format). The ciphertext ensures that unauthorized parties cannot interpret the data even if they intercept it. The encryption process ensures that only someone with the correct secret key can decrypt the data back into its original form.
- Ciphertext Sent to Receiver: The encrypted Ciphertext is transmitted over the communication channel to the receiver. Since the data is encrypted, it cannot be read without the correct key, even if intercepted.
Decryption Process (Ciphertext to Plaintext):
Let us first understand the AES Decryption Process step by step:
- Receiver Gets the Ciphertext: The Receiver receives the ciphertext and wants to retrieve the original plaintext.
- Decryption Server Receives Ciphertext: The ciphertext is sent to the Decryption Server, which can decrypt the data.
- Secret Key Used for Decryption: The same Secret Key (shared earlier) is provided to the Decryption Server to decrypt the ciphertext back into the plaintext.
- AES Algorithm Decrypts the Data: The AES algorithm uses the secret key to reverse the encryption process, converting the ciphertext back into the Original Plaintext.
- Plaintext Delivered to Receiver: After the ciphertext is successfully decrypted, the receiver gets the original, readable data (plaintext), completing the secure communication process. This is the final step where the data is usable in its original form.
Example to Understand AES Algorithm in ASP.NET Core Web API:
Let us understand how to implement Encryption and Decryption using the AES Algorithm to secure our services. We will create two applications. One ASP.NET Core Web API application that exposes the endpoints. Another Console application will consume the API Endpoints. But here, we need to transfer the data (Both Request Body and Response Body) in ciphertext, i.e., in encrypted format.
AES Server Application:
Implementing AES Encryption and Decryption in an ASP.NET Core Web API application involves several steps. Let us proceed and understand these steps in detail. First, create a new ASP.NET Core Web API Project named AESServerAPP.
Once you create the project, please install the Entity Framework Core packages by executing the following commands in the Package Manager Console:
- Install-Package Microsoft.EntityFrameworkCore.SqlServer
- Install-Package Microsoft.EntityFrameworkCore.Tools
ClientKeyIV:
First, create a folder named Models within the Project root directory. Then, create a class file named ClientKeyIV.cs within the Models folder and copy and paste the following code. This represents the ClientKeyIV entity storing AES keys and Initialization Vectors (IVs) for each client. Each record is associated with a unique ClientId, ensuring each client has distinct encryption credentials.
using System.ComponentModel.DataAnnotations; namespace AESServerAPP.Models { // Represents AES key and IV for a specific client. public class ClientKeyIV { [Key] public int Id { get; set; } // Unique identifier for the client. [Required] [MaxLength(50)] public string ClientId { get; set; } // Base64-encoded AES key. [Required] public string Key { get; set; } // Base64-encoded AES Initialization Vector. [Required] public string IV { get; set; } } }
Key and IV in AES Encryption and Decryption:
The Key and Initialization Vector (IV) play essential roles in the AES encryption and decryption process, ensuring data confidentiality, integrity, and security.
- Key: The encryption key is a fundamental component of the AES algorithm. It’s a fixed-size, secret value (commonly 128, 192, or 256 bits) used by the encryption and decryption process. The key ensures that only parties possessing the correct key can encrypt and decrypt data.
- Initialization Vector (IV): The IV is a random or pseudo-random value used to ensure that the same plaintext, when encrypted with the same key, will result in a different ciphertext. This helps prevent patterns in the ciphertext, making it more secure against certain cryptographic attacks.
Creating Employee Model:
Next, we need to create the Employee model which will hold the data in memory. So, within the Models folder, create a class file named Employee.cs and then copy and paste the following code. This represents the Employee entity with properties such as Id, Name, and Salary. Annotations like [Key], [Required], and [MaxLength] enforce data integrity and schema constraints in the database.
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace AESServerAPP.Models { public class Employee { [Key] public int Id { get; set; } [Required] [MaxLength(100)] public string Name { get; set; } [Required] [Column(TypeName = "decimal(18,2)")] public decimal Salary { get; set; } } }
Application Db Context
Next, create a class file named ApplicationDbContext.cs within the Models folder and copy and paste the following code. This class acts as the bridge between the application and the database. It manages DbSet properties for Employee and ClientKeyIV, enabling CRUD operations. The OnModelCreating method enforces a unique constraint on the ClientId to prevent duplicate entries.
using Microsoft.EntityFrameworkCore; namespace AESServerAPP.Models { public class ApplicationDbContext : DbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } // Configures the schema needed for the context. protected override void OnModelCreating(ModelBuilder modelBuilder) { // Configure unique constraint on ClientId modelBuilder.Entity<ClientKeyIV>() .HasIndex(c => c.ClientId) .IsUnique(); // Seed initial ClientKeyIV data using HasData. modelBuilder.Entity<ClientKeyIV>().HasData( //Key: Base64-encoded AES key (256-bit). //IV: Base64-encoded AES IV (128-bit). new ClientKeyIV { Id = 1, ClientId = "DefaultClient", Key = "Yyj9nVLtBLwPANTqZNFHrofcH/AbvJlaUbytoHT8Qd8=", IV = "/X9EAc4vBALd31ye7N3L1g==" }, new ClientKeyIV { Id = 2, ClientId = "Client1", Key = "gi1D2eDd8Tg565ZbfRWc00j9xKtBka4ZHu0Sen+Drgc=", IV = "Qb4nTgWS7UBo2YU7G/gJCg==" }, new ClientKeyIV { Id = 3, ClientId = "Client2", Key = "mPjeDLj4jq5AnX/0WeDXBewm05AIOqbV83MfNTWap7A=", IV = "3Y/S5SC3qFNaSbfSKEKxxA==" }, new ClientKeyIV { Id = 4, ClientId = "Client3", Key = "J0N55pEAha+B0Oyggc4zWV1GE9iWiW/m7W5DuUo0W3M=", IV = "8PiYfRaj4e5JumnpLh0FzA==" } ); // Seed initial Employee data using HasData. modelBuilder.Entity<Employee>().HasData( new Employee { Id = 1, Name = "Alice Smith", Salary = 75000m }, new Employee { Id = 2, Name = "Bob Johnson", Salary = 60000m }, new Employee { Id = 3, Name = "Carol White", Salary = 55000m } ); } // DbSet representing Employees table. public DbSet<Employee> Employees { get; set; } // DbSet representing ClientKeyIV table. public DbSet<ClientKeyIV> ClientKeyIVs { get; set; } } }
Modify appsettings.json
Stores configuration settings such as database connection strings for the ASP.NET Core application. So, please modify the appsettings.json file as follows.
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*", "ConnectionStrings": { "DefaultConnection": "Server=LAPTOP-6P5NK25R\\SQLSERVER2022DEV;Database=AESServerAPPDb;Trusted_Connection=True;TrustServerCertificate=True;" }, "EncryptionSettings": { "EnableEncryption": true } }
Key Management Service
Create a service to manage keys and IVs of the Clients. This service will be responsible for retrieving the correct key and IV based on the client ID. So, create a class file named KeyManagementService.cs within the Models folder and copy and paste the following code.
using Microsoft.EntityFrameworkCore; namespace AESServerAPP.Models { // Service for managing AES keys and IVs for clients. public class KeyManagementService { private readonly ApplicationDbContext _context; public KeyManagementService(ApplicationDbContext context) { _context = context; } public async Task<ClientKeyIV?> GetKeyAndIVAsync(string clientId) { // Retrieve the ClientKeyIV record matching the provided clientId, case-insensitive. return await _context.ClientKeyIVs .FirstOrDefaultAsync(c => c.ClientId.ToLower() == clientId.ToLower()); } } }
AES Encryption Service
Next, we need to create a service for managing the Encryption and Decryption using the AES Algorithm. So, create a class file named AesEncryptionService.cs within the Models folder and copy and paste the following code. This service ensures that sensitive data is securely handled, maintaining data confidentiality and integrity.
using System.Security.Cryptography; using System.Text; namespace AESServerAPP.Models { // Service class for performing AES encryption and decryption operations. public class AesEncryptionService { // Dependency to retrieve the AES key and IV for a specific client. private readonly KeyManagementService _keyManagementService; // Constructor to inject KeyManagementService as a dependency. public AesEncryptionService(KeyManagementService keyManagementService) { // Assign the injected dependency to the private field. _keyManagementService = keyManagementService; } // Method to convert plain text into encrypted text. public async Task<string> EncryptStringAsync(string clientId, string plainText) { // Retrieve the AES key and IV for the specified client from the key management service. var client = await _keyManagementService.GetKeyAndIVAsync(clientId); // If no matching client configuration is found, throw an exception. if (client == null) throw new ArgumentException("Invalid Client Id"); // Decode the base64-encoded key and IV into byte arrays for AES initialization. byte[] key = Convert.FromBase64String(client.Key); // Convert the Base64 string to a byte array (AES key). byte[] iv = Convert.FromBase64String(client.IV); // Convert the Base64 string to a byte array (Initialization Vector). // Create and configure an AES encryption algorithm instance. using (Aes aesAlg = Aes.Create()) { // Assign the retrieved key and IV to the AES instance. aesAlg.Key = key; // Assign the decoded key to the AES instance. aesAlg.IV = iv; // Assign the decoded IV to the AES instance. // Create an encryptor object to perform the encryption transformation. ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // Convert the plaintext string into a byte array using UTF-8 encoding. byte[] plainBytes = Encoding.UTF8.GetBytes(plainText); // Perform encryption on the plaintext bytes and get the encrypted data. byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); // Convert the encrypted byte array to a Base64-encoded string and return it. return Convert.ToBase64String(encryptedBytes); } } // Method to convert encrypted text back into plain text. public async Task<string> DecryptStringAsync(string clientId, string cipherText) { // Retrieve the AES key and IV for the specified client from the key management service. var client = await _keyManagementService.GetKeyAndIVAsync(clientId); // If no matching client configuration is found, throw an exception. if (client == null) throw new ArgumentException("Invalid Client Id"); // Decode the base64-encoded key and IV into byte arrays for AES initialization. byte[] key = Convert.FromBase64String(client.Key); // Convert the Base64 string to a byte array (AES key). byte[] iv = Convert.FromBase64String(client.IV); // Convert the Base64 string to a byte array (Initialization Vector). // Decode the Base64-encoded ciphertext into a byte array for decryption. byte[] cipherBytes = Convert.FromBase64String(cipherText); // Create and configure an AES decryption algorithm instance. using (Aes aesAlg = Aes.Create()) { aesAlg.Key = key; // Assign the decoded key to the AES instance. aesAlg.IV = iv; // Assign the decoded IV to the AES instance. // Create a decryptor object using the assigned key and IV to perform the decryption transformation. ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); // Perform decryption on the ciphertext bytes and get the original plaintext bytes. byte[] decryptedBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); // Convert the decrypted byte array into a plaintext string using UTF-8 encoding and return it. return Encoding.UTF8.GetString(decryptedBytes); } } } }
Create the Encryption/Decryption Middleware
Create a new class file named AesEncryptionMiddleware.cs within the Models folder, and copy and paste the following code. This Middleware will execute the action method before and after to decrypt and encrypt the data.
using System.Text; namespace AESServerAPP.Models { // Middleware to handle AES encryption and decryption for HTTP request and response bodies. public class AesEncryptionMiddleware { // Delegate representing the next middleware in the pipeline. private readonly RequestDelegate _next; private readonly IConfiguration _configuration; // Constructor to initialize the middleware with the next delegate in the pipeline. public AesEncryptionMiddleware(RequestDelegate next, IConfiguration configuration) { _next = next; // Assign the next middleware delegate. _configuration = configuration; } // Main middleware logic that intercepts requests and responses. public async Task InvokeAsync(HttpContext context) { // Check if HMAC is enabled var isEncryptionEnabled = _configuration.GetValue<bool>("EncryptionSettings:EnableEncryption"); if (!isEncryptionEnabled) { // Skip Encryption and call the next middleware await _next(context); return; } // Proceed with Encryption // Retrieve 'ClientId' from request headers or use 'DefaultClient' if not provided. string clientId = context.Request.Headers["ClientId"].FirstOrDefault() ?? "DefaultClient"; // Resolve the AES encryption service from the dependency injection container. var _encryptionService = context.RequestServices.GetRequiredService<AesEncryptionService>(); // Check if the HTTP method is POST or PUT, indicating a request with a body that may need decryption. if (context.Request.Method == HttpMethods.Post || context.Request.Method == HttpMethods.Put) { // Enable request body buffering to allow multiple reads of the request body. context.Request.EnableBuffering(); // Create a StreamReader to read the encrypted request body. using (var reader = new StreamReader(context.Request.Body, Encoding.UTF8, leaveOpen: true)) { // Read the entire encrypted request body as a string. string encryptedBody = await reader.ReadToEndAsync(); // Reset the request body stream position to the beginning for further use. context.Request.Body.Position = 0; // If the encrypted body is not empty or null, attempt decryption. if (!string.IsNullOrWhiteSpace(encryptedBody)) { try { // Decrypt the encrypted request body using the AES service. string decryptedBody = await _encryptionService.DecryptStringAsync(clientId, encryptedBody); // Convert the decrypted string into a byte array. byte[] decryptedBytes = Encoding.UTF8.GetBytes(decryptedBody); // Replace the original request body with the decrypted data as a MemoryStream. context.Request.Body = new MemoryStream(decryptedBytes); } catch { // If decryption fails, respond with a 400 Bad Request status code and an error message. context.Response.StatusCode = StatusCodes.Status400BadRequest; await context.Response.WriteAsync("Invalid encrypted request data."); return; // Terminate the middleware pipeline execution. } } } } // Store the original response body stream to restore it later. // This variable stores a reference to the original response body stream before the middleware changes it var originalResponseBodyStream = context.Response.Body; // Create a new MemoryStream to capture the response body. // This is a MemoryStream that temporarily replaces the original response body. using (var responseBody = new MemoryStream()) { // Set the response body to the MemoryStream for temporary capture. context.Response.Body = responseBody; try { // Proceed to the next middleware in the pipeline and execute the action method. await _next(context); // Reset the response body stream position to the beginning for reading. context.Response.Body.Seek(0, SeekOrigin.Begin); // Read the response body content as a string. string responseText = await new StreamReader(context.Response.Body).ReadToEndAsync(); // Encrypt the response if the HTTP status code indicates success and the body is not empty. if (!string.IsNullOrWhiteSpace(responseText) && context.Response.StatusCode >= 200 && context.Response.StatusCode < 300) { // Encrypt the response text using the AES service. string encryptedResponse = await _encryptionService.EncryptStringAsync(clientId, responseText); // Convert the encrypted string into a byte array. byte[] encryptedBytes = Encoding.UTF8.GetBytes(encryptedResponse); // Update the Content-Length header with the new encrypted data size. context.Response.ContentLength = encryptedBytes.Length; // Clear the current response body content. context.Response.Body.SetLength(0); // Write the encrypted response data back to the response body stream. await context.Response.Body.WriteAsync(encryptedBytes, 0, encryptedBytes.Length); } } catch { // If an error occurs during processing, set the response status to 500 Internal Server Error. context.Response.StatusCode = StatusCodes.Status500InternalServerError; // Write an error message to the response body. await context.Response.WriteAsync("Error processing response."); } //The finally block ensures that no matter what happens (whether the middleware runs successfully or encounters an error), //the original response stream is restored. finally { // Reset the response body stream position. // This ensures that the subsequent read operation (which will copy the stream’s data) begins from the very first byte. context.Response.Body.Seek(0, SeekOrigin.Begin); // After resetting the position, this line copies the entire content of responseBody(the temporary stream holding the processed response) into originalResponseBodyStream(the original response stream provided by ASP.NET Core). await context.Response.Body.CopyToAsync(originalResponseBodyStream); // This restores the HttpContext’s response body to its original stream, making sure subsequent middleware or the server itself continues working with the correct response stream. context.Response.Body = originalResponseBodyStream; } } } } }
Understanding the Code
originalResponseBodyStream:
This variable stores a reference to the original response body stream before the middleware changes it. It’s important because ASP.NET Core pipelines expect the response to eventually flow through the original response body. By storing it here, the middleware can restore it once it’s done processing. This ensures other middleware (or the server itself) can handle the response as usual after this middleware completes.
responseBody:
This is a MemoryStream that temporarily replaces the original response body. The middleware captures the response here, encrypts it if necessary, and then writes the encrypted result back to the original stream at the end. This approach isolates the encryption logic and doesn’t alter the original response stream until it’s ready to return the final encrypted response.
finally block:
The finally block ensures that no matter what happens (whether the middleware runs successfully or encounters an error), the original response stream is restored. This is important for the middleware chain’s integrity. Even if an exception occurs, the pipeline won’t break because the original stream is put back in place.
Employees Controller:
Next, create a controller that provides API endpoints for CRUD operations on employees. So, create an API Empty Controller named EmployeesController within the Controllers folder and copy and paste the following code.
using AESServerAPP.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace AESServerAPP.Controllers { // API controller for managing employees. [Route("api/[controller]")] [ApiController] public class EmployeesController : ControllerBase { private readonly ApplicationDbContext _context; // Initializes a new instance of EmployeesController with the specified DbContext. public EmployeesController(ApplicationDbContext context) { _context = context; } // Retrieves all employees. [HttpGet] public async Task<IActionResult> GetEmployees() { // Retrieve all employees from the database. var employees = await _context.Employees.ToListAsync(); // Return the employee list as JSON. Middleware will handle encryption. return Ok(employees); } // Retrieves a specific employee by ID. [HttpGet("{id}")] public async Task<IActionResult> GetEmployee(int id) { // Find the employee by ID in the database. var employee = await _context.Employees.FindAsync(id); if (employee == null) { // Return 404 Not Found if the employee does not exist. return NotFound(); } // Return the employee data as JSON. Middleware will handle encryption. return Ok(employee); } // Adds a new employee. [HttpPost] public async Task<IActionResult> PostEmployee([FromBody] Employee employee) { if (employee == null) { // Return 400 Bad Request if the request body is null. return BadRequest("Employee data is required."); } // Add the new employee to the database context. _context.Employees.Add(employee); await _context.SaveChangesAsync(); // Return the newly created employee. Middleware will handle encryption. return CreatedAtAction(nameof(GetEmployee), new { id = employee.Id }, employee); } // Updates an existing employee. [HttpPut("{id}")] public async Task<IActionResult> PutEmployee(int id, [FromBody] Employee employee) { if (employee == null) { // Return 400 Bad Request if the request body is null. return BadRequest("Employee data is required."); } // Find the existing employee in the database. var existingEmployee = await _context.Employees.FindAsync(id); if (existingEmployee == null) { // Return 404 Not Found if the employee does not exist. return NotFound(); } // Update the existing employee's properties. existingEmployee.Name = employee.Name; existingEmployee.Salary = employee.Salary; // Mark the entity as modified. _context.Entry(existingEmployee).State = EntityState.Modified; try { // Save changes to the database. await _context.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { // Return 404 Not Found if the employee no longer exists. return NotFound(); } // Return 204 No Content to indicate successful update. return NoContent(); } // Deletes an employee by ID. [HttpDelete("{id}")] public async Task<IActionResult> DeleteEmployee(int id) { // Find the employee by ID in the database. var employee = await _context.Employees.FindAsync(id); if (employee == null) { // Return 404 Not Found if the employee does not exist. return NotFound(); } // Remove the employee from the database context. _context.Employees.Remove(employee); await _context.SaveChangesAsync(); // Return 204 No Content to indicate successful deletion. return NoContent(); } // Checks if an employee exists in the database. private bool EmployeeExists(int id) { return _context.Employees.Any(e => e.Id == id); } } }
Program Configuration
Configures services, middleware, and the HTTP request pipeline for the ASP.NET Core application. So, please modify the Program class as follows:
using AESServerAPP.Models; using Microsoft.EntityFrameworkCore; namespace AESServerAPP { public class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Configure DbContext with SQL Server provider. builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))); // Register services for dependency injection. builder.Services.AddScoped<KeyManagementService>(); builder.Services.AddScoped<AesEncryptionService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Add the AES Encryption Middleware before routing. app.UseMiddleware<AesEncryptionMiddleware>(); app.UseAuthorization(); app.MapControllers(); app.Run(); } } }
Database Migration
Next, we need to generate the Migration and update the database schema. So, open the Package Manager Console and Execute the Add-Migration and Update-Database commands as follows.
With this, our Database with Employees and ClientKeyIVs tables should be created as shown in the below image:
Key Generator Console Application:
Generates AES keys and IVs and stores them in the database for clients. Create a new Console Application for generating the Valid Key and IV. So, create a Dot Net Console Application named KeyGeneratorService. Then, modify the Program class as follows. The following code is self-explained, so please read the comment line for a better understanding.
using System.Security.Cryptography; namespace KeyGeneratorService { // Main class of the KeyGeneratorService program public class Program { // Entry point of the console application static void Main(string[] args) { // Call the method to generate AES key and IV GenerateAesKeyAndIV(); // Wait for a key press to prevent the console from closing immediately Console.ReadKey(); } // Method to generate an AES key and Initialization Vector (IV) private static void GenerateAesKeyAndIV() { // Create a new AES object to generate keys. using (Aes aesAlg = Aes.Create()) { // Set the size of the encryption key to 256 bits, offering strong security aesAlg.KeySize = 256; // Generate a random key based on the key size set above aesAlg.GenerateKey(); // Generate a random initialization vector (IV) aesAlg.GenerateIV(); // Convert the generated key to a base64 string for easier readability and storage string key = Convert.ToBase64String(aesAlg.Key); // Convert the generated IV to a base64 string for easier readability and storage string iv = Convert.ToBase64String(aesAlg.IV); // Output the base64-encoded AES key to the console Console.WriteLine("AES Key (Base64): " + key); // Output the base64-encoded AES IV to the console Console.WriteLine("AES IV (Base64): " + iv); } } } }
AES Client Application
Next, we need to create the Client APP consuming the service with AES encryption and decryption. For this, we are going to create a Console Application. So, create a Console Application named AESClientApp.
Creating the Employee Model:
Within the client application, at the project root directory, create a class file named Employee.cs and copy and paste the following code. This model will be used when we send the data and receive the data from the server.
namespace AESClientApp { public class Employee { public int Id { get; set; } public string Name { get; set; } public decimal Salary { get; set; } } }
AES Encryption Service
Next, create another class file named AesEncryptionService.cs and copy and paste the following code. This is a helper class used to encrypt and decrypt the data.
using System.Security.Cryptography; using System.Text; namespace AESClientApp { // Service for performing AES encryption and decryption operations. public class AesEncryptionService { private readonly byte[] _key; // Holds the AES encryption key. private readonly byte[] _iv; // Holds the AES initialization vector (IV). // Constructor to initialize the encryption service with a key and IV. public AesEncryptionService(string base64Key, string base64IV) { // Validate that the provided key is not null or empty. if (string.IsNullOrWhiteSpace(base64Key)) throw new ArgumentException("Key cannot be null or empty.", nameof(base64Key)); // Validate that the provided IV is not null or empty. if (string.IsNullOrWhiteSpace(base64IV)) throw new ArgumentException("IV cannot be null or empty.", nameof(base64IV)); // Convert the Base64-encoded key string into a byte array for AES. _key = Convert.FromBase64String(base64Key); // Convert the Base64-encoded IV string into a byte array for AES. _iv = Convert.FromBase64String(base64IV); } // Encrypts a plaintext string using AES encryption. public string EncryptString(string plainText) { // Validate that the plaintext is not null. if (plainText == null) throw new ArgumentNullException(nameof(plainText)); // Create an instance of the AES algorithm. using (Aes aesAlg = Aes.Create()) { // Assign the pre-initialized key to the AES instance. aesAlg.Key = _key; // Assign the pre-initialized IV to the AES instance. aesAlg.IV = _iv; // Create an encryptor object for transforming plaintext into ciphertext. ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); // Convert the plaintext string into a byte array using UTF-8 encoding. byte[] plainBytes = Encoding.UTF8.GetBytes(plainText); // Perform the encryption operation on the plaintext bytes. byte[] encryptedBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); // Convert the encrypted byte array into a Base64-encoded string and return it. return Convert.ToBase64String(encryptedBytes); } } // Decrypts a ciphertext string back to plaintext using AES decryption. public string DecryptString(string cipherText) { // Validate that the ciphertext is not null. if (cipherText == null) throw new ArgumentNullException(nameof(cipherText)); // Create an instance of the AES algorithm. using (Aes aesAlg = Aes.Create()) { // Assign the pre-initialized key to the AES instance. aesAlg.Key = _key; // Assign the pre-initialized IV to the AES instance. aesAlg.IV = _iv; // Create a decryptor object for transforming ciphertext back into plaintext. ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); // Convert the Base64-encoded ciphertext string into a byte array. byte[] cipherBytes = Convert.FromBase64String(cipherText); // Perform the decryption operation on the ciphertext bytes. byte[] decryptedBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); // Convert the decrypted byte array into a plaintext string using UTF-8 encoding and return it. return Encoding.UTF8.GetString(decryptedBytes); } } } }
Consuming the Services:
Please modify the Program class code as follows. In the code below, we are consuming the Employee services with Encryption and Decryption.
using System.Text; using System.Text.Json; namespace AESClientApp { class Program { // Hardcoded Client Credentials private const string ClientId = "Client1"; private const string Base64Key = "gi1D2eDd8Tg565ZbfRWc00j9xKtBka4ZHu0Sen+Drgc="; private const string Base64IV = "Qb4nTgWS7UBo2YU7G/gJCg=="; // API Base URL private const string ApiBaseUrl = "https://localhost:7196/"; static async Task Main(string[] args) { Console.WriteLine("AES Encrypted Employee Client"); Console.WriteLine("------------------------------"); // Initialize HttpClient using (var httpClient = new HttpClient { BaseAddress = new Uri(ApiBaseUrl) }) { // Add ClientId to headers httpClient.DefaultRequestHeaders.Add("ClientId", ClientId); // Initialize AES Encryption Service var encryptionService = new AesEncryptionService(Base64Key, Base64IV); try { // 1. Get All Employees Console.WriteLine("\n1. Fetching All Employees..."); var employees = await GetAllEmployeesAsync(httpClient, encryptionService); DisplayEmployees(employees); // 2. Create a New Employee Console.WriteLine("\n2. Creating a New Employee..."); var newEmployee = new Employee { Name = "Pranaya", Salary = 10000m }; var createdEmployee = await CreateEmployeeAsync(httpClient, encryptionService, newEmployee); Console.WriteLine($"Employee Created: ID = {createdEmployee.Id}, Name = {createdEmployee.Name}, Salary = {createdEmployee.Salary}"); // 3. Get Employee By ID Console.WriteLine("\n3. Fetching Employee By ID..."); var fetchedEmployee = await GetEmployeeByIdAsync(httpClient, encryptionService, createdEmployee.Id); Console.WriteLine($"Fetched Employee: ID = {fetchedEmployee.Id}, Name = {fetchedEmployee.Name}, Salary = {fetchedEmployee.Salary}"); // 4. Update Existing Employee Console.WriteLine("\n4. Updating Existing Employee..."); fetchedEmployee.Name = "Pranaya Updated"; fetchedEmployee.Salary = 12000m; bool updateSuccess = await UpdateEmployeeAsync(httpClient, encryptionService, fetchedEmployee.Id, fetchedEmployee); Console.WriteLine(updateSuccess ? "Employee Updated Successfully." : "Failed to Update Employee."); // 5. Get Updated Employee By ID Console.WriteLine("\n5. Fetching Updated Employee By ID..."); var updatedEmployee = await GetEmployeeByIdAsync(httpClient, encryptionService, fetchedEmployee.Id); Console.WriteLine($"Updated Employee: ID = {updatedEmployee.Id}, Name = {updatedEmployee.Name}, Salary = {updatedEmployee.Salary}"); // 6. Delete Employee Console.WriteLine("\n6. Deleting Employee..."); bool deleteSuccess = await DeleteEmployeeAsync(httpClient, encryptionService, updatedEmployee.Id); Console.WriteLine(deleteSuccess ? "Employee Deleted Successfully." : "Failed to Delete Employee."); // 7. Get All Employees After Deletion Console.WriteLine("\n7. Fetching All Employees After Deletion..."); var employeesAfterDeletion = await GetAllEmployeesAsync(httpClient, encryptionService); DisplayEmployees(employeesAfterDeletion); } catch (Exception ex) { Console.WriteLine($"Unexpected Error: {ex.Message}"); } } Console.WriteLine("\nAll operations completed."); Console.ReadKey(); } // Fetches all employees from the API. private static async Task<List<Employee>> GetAllEmployeesAsync(HttpClient httpClient, AesEncryptionService encryptionService) { var response = await httpClient.GetAsync("api/employees"); response.EnsureSuccessStatusCode(); string encryptedResponse = await response.Content.ReadAsStringAsync(); string decryptedResponse = encryptionService.DecryptString(encryptedResponse); var employees = JsonSerializer.Deserialize<List<Employee>>(decryptedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (employees == null) throw new JsonException("Deserialization returned null for GetAllEmployeesAsync."); return employees; } // Creates a new employee via the API. private static async Task<Employee> CreateEmployeeAsync(HttpClient httpClient, AesEncryptionService encryptionService, Employee employee) { string plainJson = JsonSerializer.Serialize(employee); string encryptedJson = encryptionService.EncryptString(plainJson); var content = new StringContent(encryptedJson, Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync("api/employees", content); response.EnsureSuccessStatusCode(); string encryptedResponse = await response.Content.ReadAsStringAsync(); string decryptedResponse = encryptionService.DecryptString(encryptedResponse); var createdEmployee = JsonSerializer.Deserialize<Employee>(decryptedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (createdEmployee == null) throw new JsonException("Deserialization returned null for CreateEmployeeAsync."); return createdEmployee; } // Fetches an employee by ID from the API. private static async Task<Employee> GetEmployeeByIdAsync(HttpClient httpClient, AesEncryptionService encryptionService, int id) { var response = await httpClient.GetAsync($"api/employees/{id}"); response.EnsureSuccessStatusCode(); string encryptedResponse = await response.Content.ReadAsStringAsync(); string decryptedResponse = encryptionService.DecryptString(encryptedResponse); var employee = JsonSerializer.Deserialize<Employee>(decryptedResponse, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); if (employee == null) throw new JsonException("Deserialization returned null for GetEmployeeByIdAsync."); return employee; } // Updates an existing employee via the API. private static async Task<bool> UpdateEmployeeAsync(HttpClient httpClient, AesEncryptionService encryptionService, int id, Employee employee) { string plainJson = JsonSerializer.Serialize(employee); string encryptedJson = encryptionService.EncryptString(plainJson); var content = new StringContent(encryptedJson, Encoding.UTF8, "application/json"); var response = await httpClient.PutAsync($"api/employees/{id}", content); return response.IsSuccessStatusCode; } // Deletes an employee via the API. private static async Task<bool> DeleteEmployeeAsync(HttpClient httpClient, AesEncryptionService encryptionService, int id) { var response = await httpClient.DeleteAsync($"api/employees/{id}"); return response.IsSuccessStatusCode; } // Displays a list of employees to the console. private static void DisplayEmployees(List<Employee> employees) { if (employees == null || employees.Count == 0) { Console.WriteLine("No employees found."); return; } Console.WriteLine("Employees:"); foreach (var emp in employees) { Console.WriteLine($"ID: {emp.Id}, Name: {emp.Name}, Salary: {emp.Salary}"); } } } }
Now, first run the ASP.NET Core Web API Project and then run the Console Application, and you should see the output as expected, as shown in the below image.
Encryption and decryption are fundamental to maintaining data confidentiality, integrity, and authenticity in digital communications. They prevent unauthorized access and modifications to sensitive information while ensuring that intended recipients can only access the data. In environments where data breaches and cyber threats are prevalent, encryption is a vital barrier against potential data exposure and theft.
In the next article, I will discuss how to Implement RSA Asymmetric Encryption and Decryption in ASP.NET Core Web API Applications. Here, in this article, I explain how to Implement AES Encryption and Decryption in the ASP.NET Core Web API Application with an Example. I hope you enjoy this Encryption and Decryption in ASP.NET Core Web API article.
Hi,
I am following this tutorial but I faced a problem; msEncrypt.ToArray() is returning an empty byte array.