Encryption and Decryption in ASP.NET Core Web API

Encryption and Decryption in ASP.NET Core Web API

In this article, I will discuss how to implement Encryption and Decryption in an 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. 

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.

What is Encryption?

Encryption is a fundamental security technique used to protect data by converting it from its original readable form (called plaintext) into an unreadable format known as ciphertext. This transformation uses a cryptographic algorithm (symmetric or asymmetric) and an encryption key. The purpose is to ensure that if unauthorized people intercept the data during transmission or storage, they cannot understand or misuse it.

  • Plaintext is the original data you want to protect.
  • Ciphertext is the scrambled, unreadable output of an encryption process.
  • The encryption key is a secret value used by the algorithm to perform the transformation. Without the correct key, decrypting the ciphertext to obtain the plaintext is practically impossible.

In ASP.NET Core Web API, encryption helps protect sensitive data, such as passwords, API keys, or user information, that is transmitted between clients and servers, ensuring confidentiality against attackers.

What is Decryption?

Decryption is the opposite of encryption. It converts ciphertext back into its original readable format (plaintext). This process requires the corresponding decryption key, which could be the same key used for encryption (in symmetric encryption) or a different key (in asymmetric encryption).

  • Only authorized parties with the correct key can decrypt the data.
  • Decryption ensures that even if data is intercepted during transmission, only the intended recipient can access the original information.

In ASP.NET Core Web API, decryption enables the server or client to securely read encrypted messages, thereby providing data confidentiality and privacy.

Why Do We Need Encryption and Decryption?

The following are the reasons why Encryption and Decryption are crucial in web applications:

  • Data Security: If someone intercepts your data (like during a man-in-the-middle attack), encrypted data is meaningless to them. Therefore, it prevents unauthorized access to sensitive information, such as personal data, financial details, or business secrets.
  • Data Integrity: Although encryption doesn’t directly prevent tampering, it helps detect if data has been tampered with during transmission, as decryption will fail or produce invalid output if the data is altered.
  • Privacy Compliance: Many regulations, such as GDPR, HIPAA, and PCI DSS, require that sensitive data (including personal details, medical records, and payment information) be encrypted during storage and transit to protect user privacy and comply with legal standards.
  • Secure Communication: APIs often transfer data over the internet, which is inherently insecure. Encryption ensures that even if data packets are intercepted, the information remains protected.

In ASP.NET Core Web API, encryption mechanisms protect both API requests and responses, safeguarding user data and maintaining the trustworthiness of the system. For example, consider a login system in an ASP.NET Core Web API. User credentials should be encrypted when transmitted from the client to the server. If you store tokens or sensitive configuration, those should also be encrypted.

Techniques for Encryption and Decryption in ASP.NET Core:

There are two main types of encryption and decryption:

1. Symmetric Encryption/Decryption:
  • Uses the same key for both encryption and decryption.
  • It is fast and efficient, suitable for encrypting large volumes of data.
  • The main challenge is securely sharing the secret key between the sender and the receiver.
Examples:
  • AES (Advanced Encryption Standard): The most widely used symmetric encryption algorithm, recognized for its high security and efficiency.
  • DES (Data Encryption Standard): An older standard, now considered insecure due to shorter key length and vulnerabilities.
2. Asymmetric Encryptions/Decryptions:
  • Uses a pair of keys. A public key (for encryption) and a private key (for decryption).
  • Public key can be shared openly, while the private key remains confidential.
  • More secure for key distribution, but slower than symmetric encryption.

Examples:

  • RSA: A popular asymmetric algorithm based on the difficulty of factoring large numbers.
  • Elliptic Curve Cryptography (ECC): It provides similar security to RSA but with smaller key sizes and improved performance.

In ASP.NET Core, asymmetric encryption is often used to securely exchange symmetric keys or for digital signatures, while symmetric encryption is used for bulk data encryption.

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.

  • Encrypts data in fixed-size blocks (each 128 bits). This means that AES processes data in chunks of exactly 128 bits (which is 16 bytes) at a time. Regardless of the length of your data, AES divides it into 128-bit blocks for encryption.
  • Supports 128, 192, and 256-bit keys; longer keys mean higher security. This is the size of the secret key that the algorithm uses for encryption and decryption. The key size determines the strength of the encryption: a larger key size generally means stronger security, but it may require slightly more computational effort.
  • It is very fast and efficient, making it suitable for encrypting large amounts of data (e.g., files, database fields, network traffic).
  • It is used by governments, financial institutions, and commercial applications for securing sensitive data.
  • The algorithm involves multiple rounds of substitution, permutation, mixing, and key addition to produce the ciphertext.

In ASP.NET Core Web API, AES is typically used for encrypting data at rest (in databases and files) or in transit (during API communication) to maintain confidentiality.

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.

  • It uses a 56-bit key, which is now too short to resist modern brute-force attacks.
  • DES was widely used in the past but is now considered deprecated and insecure.
  • Has been replaced by AES and other more secure standards.

In modern ASP.NET Core applications, avoid using DES due to security risks and instead rely on AES.

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.

Encryption and Decryption in ASP.NET Core Web API

Let’s Understand How the AES Algorithm Works.

Step 1: Key Sharing (Secure Channel):
  • Both sender and receiver must securely share the same secret key before any encryption or decryption can occur.
  • The key must be transmitted via a protected channel (e.g., HTTPS, TLS) to avoid interception.
  • This shared secret key is essential because AES uses symmetric encryption.
Sep 2: AES Encryption Process (Plaintext to Ciphertext):

Let us first understand the AES Encryption Process step by step:

  • The sender prepares the plaintext data that needs protection.
  • The plaintext is sent to the Encryption Server, where AES encrypts it using the shared secret key.
  • The plaintext is transformed into ciphertext, an unreadable form.
  • The ciphertext is then transmitted over the network to the receiver. Even if intercepted, it appears meaningless without the key.
Step 3: Decryption Process (Ciphertext to Plaintext):

Let us first understand the AES Decryption Process step by step:

  • The receiver obtains the ciphertext.
  • The ciphertext is sent to the Decryption Server, which uses the same shared secret key.
  • AES decrypts the ciphertext back into the original plaintext.
  • The receiver gets the readable data, completing secure communication.
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.

  • AESServerAPP: An ASP.NET Core Web API that exposes endpoints and encrypts/decrypts data using AES.
  • AESClientApp: A Console application that consumes those APIs by sending encrypted requests and decrypting encrypted responses.

The goal is to transfer both request and response bodies in encrypted format (ciphertext) to secure data in transit.

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 to handle database operations using the EF Core Code First Approach with SQL Server Database:

  • Install-Package Microsoft.EntityFrameworkCore.SqlServer
  • Install-Package Microsoft.EntityFrameworkCore.Tools
ClientKeyIV Model:

First, create a folder named Models within the Project root directory. Then, create a class file named ClientKeyIV.cs within the Models folder and then copy and paste the following code. This represents the ClientKeyIV entity, which stores AES keys and Initialization Vectors (IVs) for each client. Each record is associated with a unique ClientId, ensuring that 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:
  • Key: Secret encryption key used in AES (128/192/256 bits).
  • Initialization Vector (IV): The IV is a fixed-size random value used alongside the AES key. Its purpose is to ensure that each encryption operation produces a unique ciphertext, even if the plaintext and key are the same.
Creating Employee Model:

Create a class file named Employee.cs within the Models folder and then copy and paste the following code. This represents the Employee entity, which includes 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 then 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

In the appsettings.json file, we will add a connection string for the database and a flag (EnableEncryption) to toggle encryption on/off. 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

We need to create a service to fetch the AES key and IV for a given ClientId. It will ensure encryption/decryption uses the correct credentials. 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

Now, we need to create a service to perform actual AES encryption and decryption.

  • Uses the key and IV retrieved by KeyManagementService.
  • Converts plaintext to ciphertext and vice versa.
  • Uses the standard .NET AES class with proper encoding and Base64 conversions.

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. The following code is self-explanatory, so please read the comment lines for a better understanding.

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

Now, we need to create an AES Encryption Middleware that will do the following things:

  • Intercepts HTTP requests and responses.
  • For incoming POST/PUT requests, decrypts the request body if encrypted.
  • For outgoing responses, it encrypts the response body before sending it to the client.
  • Uses ClientId from request headers to determine which key/IV to use.
  • Allows toggling encryption on and off via configuration.

Create a new class file named AesEncryptionMiddleware.cs within the Models folder and copy and paste the following code. This Middleware will execute before and after the action method is executed to decrypt and encrypt the data. The following code is self-explanatory, so please read the comment lines for a better understanding.

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;
                }
            }
        }
    }
}
Creating API Controller:

Next, create a controller that provides API endpoints for creating, reading, updating, and deleting (CRUD) operations on employees. So, create an API Empty Controller named EmployeesController within the Controllers folder and then copy and paste the following code. The Middleware automatically handles encryption/decryption; the controller deals with normal objects.

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

Now, we need to register DbContext, services, and middleware in the ASP.NET Core pipeline. Configures JSON options and Swagger for API documentation. We need to ensure encryption middleware runs before authorization and routing. 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();
            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

Run EF Core migrations to create the database schema with the Employees and ClientKeyIVs tables. So, open Package Manager Console and execute the Add-Migration and Update-Database commands as follows.

How to Implement AES Encryption and Decryption in ASP.NET Core Web API Application with an Example

With this, our Database with Employees and ClientKeyIVs tables should be created as shown in the image below:

How to Implement AES Encryption and Decryption in ASP.NET Core Web API Application

Key Generator Console Application:

Now, we will create a Console Application for generating the Valid Key and IV.

  • Generates new AES keys (256-bit) and IVs.
  • Outputs them in Base64 for easy storage in the database.
  • It is useful for adding new clients with unique credentials.

So, create a .NET Console Application named KeyGeneratorService. Then modify the Program class as follows. The following code is self-explanatory, 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 that consumes the service with AES encryption and decryption. For this, we will create a Console Application. So, create a Console Application named AESClientApp.

Creating the Employee Model:

Within the AESClientApp application, at the project root directory, create a class file named Employee.cs and copy and paste the following code. This mirrors the server-side Employee class for serialization and deserialization.

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 into it. This is a helper class used to encrypt outgoing requests and decrypt incoming responses. It is initialized with the same Base64 Key and IV matching the server credentials.

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:

Now, we need to consume the API endpoints with Encryption and Decryption:

  • Sends encrypted JSON in the request body.
  • Reads the encrypted response body and decrypts it.
  • Performs CRUD operations on employees, demonstrating end-to-end encrypted communication.
  • Uses HttpClient with ClientId header for key selection.
  • Handles serialization and encryption seamlessly.

So, please, modify the Program class code as follows.

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

First, run the ASP.NET Core Web API Project, and then run the Console Application. You should see the expected output, as shown in the image below.

AES Encryption and Decryption in the ASP.NET Core Web API

How This Example Secures Communication
  • Data (request/response bodies) is never sent in plain text over the network.
  • Only clients with the correct AES key and initialization vector (IV) can encrypt requests and decrypt responses.
  • Even if attackers intercept the data, without the key and IV, the data is a meaningless ciphertext.
  • Using unique keys per client improves security isolation.
  • Middleware makes encryption and decryption transparent to API controllers.
  • The console client demonstrates the practical application of encrypted communication with ASP.NET Core Web API.

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 the data can only be accessed by intended recipients. In environments where data breaches and cyber threats are prevalent, encryption acts as 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 an ASP.NET Core Web API Application with an Example. I hope you enjoy this article on Encryption and Decryption in ASP.NET Core Web API.

2 thoughts on “Encryption and Decryption in ASP.NET Core Web API”

Leave a Reply

Your email address will not be published. Required fields are marked *