Asymmetric Encryption in ASP.NET Core Web API

Asymmetric Encryption in ASP.NET Core Web API

In this article, I will discuss how to Implement RSA Asymmetric Encryption and Decryption in ASP.NET Core Web API Application with an Example. Please read our previous article discussing how to Implement AES Symmetric Encryption and Decryption in ASP.NET Core Web API Application.

What is Asymmetric Encryption?

Asymmetric Encryption, also known as public-key cryptography, is a type of encryption in which two different keys are used: a public key and a private key. The public key encrypts data, while the private key decrypts it. This method ensures that even if the public key is known by others, only the intended recipient with the private key can decrypt the message.

The primary advantage of Asymmetric Encryption is that it eliminates the need to share a secret key between the sender and receiver, thus reducing the risk of key compromise.

What are the Different Components of Asymmetric Encryption?

The following are the Different Components of Asymmetric Encryption Algorithm:

  • Public Key: This key is shared openly and used to encrypt data. It is unique to the receiver and can be distributed widely without compromising security.
  • Private Key: This key is kept secret by the owner and used to decrypt data. It is unique to the receiver and must be kept secure.
  • Ciphertext: The encrypted data produced after applying the encryption algorithm to the plaintext. It is unreadable without the private key.
  • Plaintext: The original data or message before encryption. This is the readable form of data that needs to be protected.
  • Encryption Algorithm: The process used to transform plaintext into ciphertext using the public key. Common algorithms include RSA, ECC (Elliptic Curve Cryptography), and DSA. When someone wants to send a secure message, they use the recipient’s public key to encrypt the message. Once encrypted, only the recipient’s private key can decrypt it.
  • Decryption Algorithm: The process used to transform ciphertext back into plaintext using the private key. This algorithm ensures that only the private key holder can read the original message. The recipient uses their private key to decrypt the message, turning it back into readable plaintext.
How Does Asymmetric Encryption Work?

Asymmetric Encryption involves a pair of keys: a public key and a private key. Here is a step-by-step explanation of how asymmetric encryption works:

  • Key Generation: A cryptographic algorithm generates a pair of keys: a public key and a private key. The public key is shared openly, while the owner keeps the private key secure and private.
  • Encryption: When the user wants to send a secure message to the receiver, the user will use the receiver’s public key to encrypt the message. The encryption algorithm takes the plaintext (the original message) and the receiver’s public key as input and produces ciphertext (the encrypted message) as output.
  • Transmission: The encrypted message (ciphertext) is sent to the intended receiver over an insecure channel, such as the Internet.
  • Decryption: The receiver uses their private key to decrypt the ciphertext. The decryption algorithm takes the ciphertext and the private key as input and produces the original plaintext as output.

For a better understanding, please have a look at the following diagram:

RSA Asymmetric Encryption and Decryption in ASP.NET Core Web API

What are the Different Techniques to Implement Asymmetric Encryption in ASP.NET Core Web API?

We can use the following techniques when implementing Asymmetric Encryption in an ASP.NET Core Web API.

  1. RSA (Rivest-Shamir-Adleman) Algorithm: RSA is one of the most widely used algorithms for public-key encryption. It provides strong security by using a pair of keys (public and private) for encryption and decryption.
  2. ECDSA (Elliptic Curve Digital Signature Algorithm): ECDSA is an elliptic curve-based variant of the Digital Signature Algorithm (DSA). It is used for creating digital signatures and provides high security with shorter key lengths compared to RSA.
  3. DSA (Digital Signature Algorithm): DSA is primarily used for digital signatures and not for encrypting data. It is a Federal Information Processing Standard for digital signatures.
Example to Understand RSA Algorithm in ASP.NET Core Web API:

Let us understand how to implement Encryption and Decryption using the RSA Algorithm to secure our services. We will create two applications: one ASP.NET Core Web API application that exposes the endpoints and another Console application that consumes the API Endpoints. But here, we need to transfer the data (both the Request Body and the Response Body) in ciphertext, i.e., in encrypted format using the RSA Algorithm.

Creating Server Application:

Implementing RSA Encryption and Decryption to protect API endpoints for CRUD operations involves several steps, including creating and managing keys, setting up the ASP.NET Core Web API, and developing a .NET console application to consume the API. First, create a new ASP.NET Core Web API Project named RSAServerAPP. Then, create a folder named Models within the Project root directory.

Key Storage Class

This class will store the public and private keys based on the Client ID. So, create a class file named KeyStorage.cs and then copy and paste the following code:

using System.Security.Cryptography;

namespace RSAServerAPP.Models
{
    public class KeyStorage
    {
        // A private static dictionary to store the RSA keys for each client.
        // The dictionary keys are client IDs (string), and the values are tuples containing both public and private keys.
        private static readonly Dictionary<string, (string PublicKey, string PrivateKey)> _keyStore = new();

        // Method to generate a new pair of RSA keys for a specified clientId.
        public void GenerateKeys(string clientId)
        {
            // Create a new RSA object with a key size of 2048 bits (256 Bytes) for cryptographic operations.
            using (var rsa = RSA.Create(2048))
            {
                // Export the public key information and convert it to a base64 string for easier storage and transmission.
                var publicKey = Convert.ToBase64String(rsa.ExportSubjectPublicKeyInfo());

                // Export the private key in PKCS#8 format and convert it to a base64 string.
                var privateKey = Convert.ToBase64String(rsa.ExportPkcs8PrivateKey());

                // Store the generated keys in the _keyStore dictionary with the provided clientId as the key.
                _keyStore[clientId] = (publicKey, privateKey);
            }
        }

        // Method to retrieve the public key for a given clientId.
        public string GetPublicKey(string clientId)
        {
            // Attempt to retrieve the keys for the specified clientId. If found, return the public key; otherwise, return null.
            return _keyStore.TryGetValue(clientId, out var keys) ? keys.PublicKey : null;
        }

        // Method to retrieve the private key for a given clientId.
        public string GetPrivateKey(string clientId)
        {
            // Attempt to retrieve the keys for the specified clientId. If found, return the private key; otherwise, return null.
            return _keyStore.TryGetValue(clientId, out var keys) ? keys.PrivateKey : null;
        }
    }
}
Objectives of Methods
  • GenerateKeys: The primary purpose of this method is to create a new pair of RSA public and private keys for a specific client. This method ensures that each client has a unique set of keys, which is essential for secure communications (e.g., encrypting and decrypting data).
  • GetPublicKey: This method fetches the public key for a given client. The public key is used to encrypt data that has been decrypted with the corresponding private key.
  • GetPrivateKey: This method fetches the private key for a given client. The private key is used to decrypt data encrypted with the corresponding public key.
Define the Employee Model:

Next, create a simple Employee model. So, within the Models folder, create a class file named Employee.cs and then copy and paste the following code:

namespace RSAServerAPP.Models
{
    public class Employee
    {
        public int? Id { get; set; }
        public string Name { get; set; }
        public string Position { get; set; }
        public decimal Salary { get; set; }
    }
}
Create an RSA Encryption Service in ASP.NET Core Web API

Create a service to handle RSA Encryption and decryption. So, create a class file named RSAEncryptionService.cs and copy and paste the following code. The flowing class is used to handle data encryption and decryption using RSA keys. I’ve added comments to each line of the code to explain its functionality:

using System.Security.Cryptography;
using System.Text;

namespace RSAServerAPP.Models
{
    // The RSAEncryptionService class provides methods to encrypt and decrypt data using RSA encryption.
    public class RSAEncryptionService
    {
        // Encrypts a string using a public key.
        public string Encrypt(string data, string publicKey)
        {
            // Creates a new RSA instance for cryptographic operations.
            using (var rsa = RSA.Create())
            {
                // Imports the public key, provided in base64 format, to set up the RSA object for encryption.
                rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKey), out _);

                // Converts the string data to a byte array in UTF-8 encoding.
                var dataBytes = Encoding.UTF8.GetBytes(data);

                // Encrypts the data bytes using the public key and OAEP padding with SHA-256.
                var encryptedData = rsa.Encrypt(dataBytes, RSAEncryptionPadding.OaepSHA256);

                // Converts the encrypted byte array back to a base64 string for easy storage or transmission.
                return Convert.ToBase64String(encryptedData);
            }
        }

        // Decrypts a string using a private key.
        public string Decrypt(string encryptedData, string privateKey)
        {
            // Creates a new RSA instance for cryptographic operations.
            using (var rsa = RSA.Create())
            {
                // Imports the private key, provided in base64 format, to set up the RSA object for decryption.
                rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);

                // Converts the base64 encoded encrypted data back into a byte array.
                var dataBytes = Convert.FromBase64String(encryptedData);

                // Decrypts the data bytes using the private key and OAEP padding with SHA-256.
                var decryptedData = rsa.Decrypt(dataBytes, RSAEncryptionPadding.OaepSHA256);

                // Converts the decrypted byte array back to a UTF-8 encoded string.
                return Encoding.UTF8.GetString(decryptedData);
            }
        }
    }
}
Objectives of Methods
  • Encrypt: Encrypts plain text data into a ciphertext using RSA public key encryption. This method ensures that data can only be decrypted by the corresponding private key, safeguarding sensitive information during transmission.
  • Decrypt: This method decrypts ciphertext back into plain text using an RSA private key. This method ensures that only the intended recipient, who holds the private key, can read the data.
Create the Employee Controller

Create a controller to handle CRUD operations and protect them with RSA encryption. So, create an API Empty Controller named EmployeesController within the Controllers folder and copy and paste the following code. The following controller handles CRUD operations for an employee database with encryption and decryption using RSA keys. Below, I’ve added comments to each line of the code to explain its functionality and purpose:

using Microsoft.AspNetCore.Mvc;
using RSAServerAPP.Models;
using System.Text.Json;

namespace RSAServerAPP.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class EmployeesController : ControllerBase
    {
        // Static list simulating a database table storing employee records.
        private static List<Employee> Employees = new List<Employee>
        {
            new Employee { Id = 1, Name = "Alice Smith", Position="DBA", Salary = 75000 },
            new Employee { Id = 2, Name = "Bob Johnson", Position="HR", Salary = 60000 },
            new Employee { Id = 3, Name = "Carol White", Position="Developer", Salary = 55000 }
        };

        // Services for RSA encryption and key storage.
        private readonly RSAEncryptionService _encryptionService;
        private readonly KeyStorage _keyStorage;

        // Constructor initializes the RSA encryption and key storage services.
        public EmployeesController()
        {
            _encryptionService = new RSAEncryptionService();
            _keyStorage = new KeyStorage();
        }

        // HTTP GET endpoint to generate RSA keys for a client and return them.
        [HttpGet("generate-keys")]
        public IActionResult GenerateKeys([FromHeader] string clientId)
        {
            _keyStorage.GenerateKeys(clientId);
            var publicKey = _keyStorage.GetPublicKey(clientId);
            var privateKey = _keyStorage.GetPrivateKey(clientId);
            return Ok(new { PublicKey = publicKey, PrivateKey = privateKey });
        }

        // HTTP POST endpoint to create a new employee from encrypted data.
        [HttpPost("create")]
        public IActionResult Create([FromHeader] string clientId, [FromBody] EncryptedRequest request)
        {
            var privateKey = _keyStorage.GetPrivateKey(clientId);
            var decryptedData = _encryptionService.Decrypt(request.Data, privateKey);
            var employee = JsonSerializer.Deserialize<Employee>(decryptedData);
            employee.Id = Employees.Count > 0 ? Employees.Max(e => e.Id) + 1 : 1;
            Employees.Add(employee);
            var encryptedResponse = _encryptionService.Encrypt(JsonSerializer.Serialize(employee), _keyStorage.GetPublicKey(clientId));
            return Ok(new EncryptedDataResponse { Data = encryptedResponse });
        }

        // HTTP GET endpoint to retrieve an employee's details, encrypt the data before sending it back.
        [HttpGet("{id}")]
        public IActionResult Get(int id, [FromHeader] string clientId)
        {
            var employee = Employees.FirstOrDefault(e => e.Id == id);
            if (employee == null) return NotFound();

            var data = JsonSerializer.Serialize(employee);
            var encryptedData = _encryptionService.Encrypt(data, _keyStorage.GetPublicKey(clientId));
            return Ok(new EncryptedDataResponse { Data = encryptedData });
        }

        // HTTP PUT endpoint to update an existing employee's details from encrypted data.
        [HttpPut("{id}")]
        public IActionResult Update(int id, [FromHeader] string clientId, [FromBody] EncryptedRequest request)
        {
            var privateKey = _keyStorage.GetPrivateKey(clientId);
            var decryptedData = _encryptionService.Decrypt(request.Data, privateKey);
            var updatedEmployee = JsonSerializer.Deserialize<Employee>(decryptedData);
            var employee = Employees.FirstOrDefault(e => e.Id == id);
            if (employee == null) return NotFound();

            employee.Name = updatedEmployee.Name;
            employee.Position = updatedEmployee.Position;
            employee.Salary = updatedEmployee.Salary;
            var encryptedResponse = _encryptionService.Encrypt(JsonSerializer.Serialize(employee), _keyStorage.GetPublicKey(clientId));
            return Ok(new EncryptedDataResponse { Data = encryptedResponse });
        }

        // HTTP DELETE endpoint to remove an employee by ID.
        [HttpDelete("{id}")]
        public IActionResult Delete(int id)
        {
            var employee = Employees.FirstOrDefault(e => e.Id == id);
            if (employee == null) return NotFound();

            Employees.Remove(employee);
            return Ok();
        }
    }

    // Class to handle encrypted request data.
    public class EncryptedRequest
    {
        public string Data { get; set; }
    }

    // Class to handle encrypted data responses.
    public class EncryptedDataResponse
    {
        public string Data { get; set; }
    }
}

RSAClientAPP

Next, we need to create the Client APP consuming the service with RSA encryption and decryption. For this, we are going to create a Console Application. So, create a Dot Net Console Application named RSAClientAPP. Once you create the Console Application, modify the Program class code as follows:

using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace RSAClientAPP
{
    public class Program
    {
        // HttpClient instance for sending requests to a server.
        private static readonly HttpClient client = new HttpClient();

        // The entry point of the program.
        public static async Task Main(string[] args)
        {
            // Identifier for the client.
            var clientId = "Client1";

            // Add clientId to the default request headers of HttpClient.
            client.DefaultRequestHeaders.Add("clientId", clientId);

            // Request to generate RSA keys from the server and retrieve them.
            var keysResponse = await client.GetFromJsonAsync<KeysResponse>("https://localhost:7102/api/Employees/generate-keys");
            var publicKey = keysResponse.PublicKey;
            var privateKey = keysResponse.PrivateKey;

            // Create a new employee object.
            var newEmployee = new Employee
            {
                Name = "John Doe",
                Position = "Developer",
                Salary = 60000
            };

            // Encrypt the new employee data using the public key and send to the server.
            var encryptedData = EncryptData(newEmployee, publicKey);
            var createRequest = new EncryptedRequest { Data = encryptedData };

            // Send the encrypted employee data to the server to create a new employee.
            var createResponse = await client.PostAsJsonAsync("https://localhost:7102/api/Employees/create", createRequest);
            var createdEmployeeData = await createResponse.Content.ReadFromJsonAsync<EncryptedDataResponse>();
            var createdEmployeeJson = DecryptData(createdEmployeeData.Data, privateKey);
            var createdEmployee = JsonSerializer.Deserialize<Employee>(createdEmployeeJson);
            Console.WriteLine($"Created Employee: {JsonSerializer.Serialize(createdEmployee)}");

            // Retrieve the created employee's data from the server and decrypt it.
            var getResponse = await client.GetAsync($"https://localhost:7102/api/Employees/{createdEmployee.Id}");
            var encryptedEmployee = await getResponse.Content.ReadFromJsonAsync<EncryptedDataResponse>();
            var decryptedEmployeeJson = DecryptData(encryptedEmployee.Data, privateKey);
            var decryptedEmployee = JsonSerializer.Deserialize<Employee>(decryptedEmployeeJson);
            Console.WriteLine($"Retrieved Employee: {JsonSerializer.Serialize(decryptedEmployee)}");

            // Update the employee's name, encrypt the updated data, and send it to the server.
            createdEmployee.Name = "Jane Doe";
            encryptedData = EncryptData(createdEmployee, publicKey);
            var updateRequest = new EncryptedRequest { Data = encryptedData };

            var updateResponse = await client.PutAsJsonAsync($"https://localhost:7102/api/Employees/{createdEmployee.Id}", updateRequest);
            var updatedEmployeeData = await updateResponse.Content.ReadFromJsonAsync<EncryptedDataResponse>();
            var updatedEmployeeJson = DecryptData(updatedEmployeeData.Data, privateKey);
            var updatedEmployee = JsonSerializer.Deserialize<Employee>(updatedEmployeeJson);
            Console.WriteLine($"Updated Employee: {JsonSerializer.Serialize(updatedEmployee)}");

            // Send a request to delete the employee from the server.
            var deleteResponse = await client.DeleteAsync($"https://localhost:7102/api/Employees/{createdEmployee.Id}");
            if (deleteResponse.IsSuccessStatusCode)
            {
                Console.WriteLine("Employee deleted successfully.");
            }

            // Pause the console until a key is pressed.
            Console.ReadKey();
        }

        // Encrypts data using an RSA public key.
        private static string EncryptData(object data, string publicKey)
        {
            using (var rsa = RSA.Create())
            {
                rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKey), out _);
                var dataBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data));
                var encryptedData = rsa.Encrypt(dataBytes, RSAEncryptionPadding.OaepSHA256);
                return Convert.ToBase64String(encryptedData);
            }
        }

        // Decrypts data using an RSA private key.
        private static string DecryptData(string encryptedData, string privateKey)
        {
            using (var rsa = RSA.Create())
            {
                rsa.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKey), out _);
                var dataBytes = Convert.FromBase64String(encryptedData);
                var decryptedData = rsa.Decrypt(dataBytes, RSAEncryptionPadding.OaepSHA256);
                return Encoding.UTF8.GetString(decryptedData);
            }
        }
    }

    // Definition of the Employee model.
    public class Employee
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string Position { get; set; }
        public decimal Salary { get; set; }
    }

    // Response model for receiving keys.
    public class KeysResponse
    {
        public string PublicKey { get; set; }
        public string PrivateKey { get; set; }
    }

    // Model for encrypted requests.
    public class EncryptedRequest
    {
        public string Data { get; set; }
    }

    // Model for encrypted data responses.
    public class EncryptedDataResponse
    {
        public string Data { get; set; }
    }
}
Testing the Application:

First, run the ASP.NET Core Web API Application, then run the Console Application, and you should get the output as expected, as shown in the image below:

When Should We Use Asymmetric Encryption in Real-Time Applications?

Asymmetric encryption is useful in scenarios where secure communication between parties is required, especially when there’s no prior exchange of secret keys. It is commonly used for secure communication over insecure channels, like online transactions, email encryption, or secure messaging applications.

  • Secure Key Exchange: One of the most common uses of asymmetric encryption in real-time applications is to securely exchange symmetric keys that are then used for the bulk encryption of data. This is often seen in protocols like TLS/SSL, where a secure and authenticated channel is established using asymmetric encryption before switching to faster symmetric encryption for the rest of the session.
  • Authentication: Asymmetric encryption can be used to authenticate users or devices in real-time applications. Digital signatures, which rely on asymmetric keys, ensure that the message or transaction originates from a verified source and has not been altered in transit.
  • Confidentiality in Initial Communications: In scenarios where two parties have not previously shared secret keys, asymmetric encryption provides a secure method for initial communications. This is essential for services like email encryption, secure file transfers, and initial device setup processes.
Differences Between Asymmetric and Symmetric Encryption

Asymmetric and symmetric encryption are two primary cryptographic methods used to secure data. The following are the key differences between them:

Key Usage:
  • Symmetric Encryption: The sender and receiver use the same secret key for encryption and decryption. This requires that the key be securely exchanged and kept confidential.
  • Asymmetric Encryption: Involves two keys: a public key for encryption and a private key for decryption. The public key can be freely distributed, while the owner keeps the private key secure.
Key Management:
  • Symmetric Encryption: Requires secure key distribution mechanisms, as the same key must be shared between communicating parties. This can be challenging in large-scale environments.
  • Asymmetric Encryption: Simplifies key distribution since the public key can be shared openly without compromising security. Only the private key must be protected.
Speed:
  • Symmetric Encryption: Generally, it is much faster than asymmetric encryption due to the simplicity of the algorithms used. It is well-suited for encrypting large amounts of data.
  • Asymmetric Encryption: It is slower because of the complexity of the algorithms. It is typically used for encrypting small amounts of data, such as keys or digital signatures.
Security:
  • Symmetric Encryption: Security depends on the encryption algorithm’s strength and the key’s secrecy. If the key is compromised, the security is breached.
  • Asymmetric Encryption: Offers higher security due to the use of two keys. Even if the public key is known, it is computationally infeasible to decrypt the data without the private key.
Examples of Algorithms:
  • Symmetric Encryption: Common algorithms include AES (Advanced Encryption Standard), DES (Data Encryption Standard), and 3DES (Triple DES).
  • Asymmetric Encryption: Common algorithms include RSA (Rivest-Shamir-Adleman), ECC (Elliptic Curve Cryptography), and DSA (Digital Signature Algorithm).

In the next article, I will discuss how to Implement Basic Authentication in ASP.NET Core Web API Applications. In this article, I explain How to Implement RSA Asymmetric Encryption and Decryption in the ASP.NET Core Web API Application with an Example. I hope you enjoy this Asymmetric Encryption and Decryption in ASP.NET Core Web API article.

Leave a Reply

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