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.

Asymmetric Encryption in ASP.NET Core Web API

Asymmetric encryption, also known as public-key cryptography, is a cryptographic method that uses a pair of keys for encryption and decryption:

  • Public Key: This key is used to encrypt data. It can be shared openly with anyone who needs to send secure information.
  • Private Key: This key is used to decrypt the encrypted data. It must remain secret and is known only to the intended recipient.

A distinguishing feature of asymmetric encryption is that data encrypted with the public key can only be decrypted using the private key and vice versa. These keys are generated as a matched pair. Because the encryption process depends on one key and decryption on the other, the two keys must be kept in sync.

How Does Asymmetric Encryption Work?

Asymmetric encryption works by generating a key pair, where the public key encrypts data and the private key decrypts it. The keys are mathematically related but are not identical. Let us understand how asymmetric encryption works. For a better understanding, please have a look at the following diagram:

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

Let us understand the step-by-step process of asymmetric encryption works:

Key Pair Generation:

A pair of keys (public and private) is generated using robust cryptographic algorithms.

  • Public Key: Can be freely shared without compromising security.
  • Private Key: Must be securely stored and protected from unauthorized access.
Sender Prepares the Message:

The process begins with the sender with plaintext data (i.e., the original, readable message) that they want to send to the recipient securely. The message is still in its original form, called plaintext data. At this point, the message is vulnerable if transmitted without encryption.

Encryption Using the Public Key:

The sender encrypts the plaintext data using the recipient’s public key. The encryption algorithm uses the plaintext and the recipient’s public key as input, and it produces ciphertext (the encrypted message) as output.

Note: The public key can be shared openly and is used by anyone to encrypt a message intended for the recipient. However, it cannot decrypt the message, ensuring only the recipient can access the original data.

Ciphered Data:

After encryption, the plaintext is transformed into ciphertext, an unreadable, scrambled format. Ciphertext ensures that even if the data is intercepted during transmission, unauthorized parties cannot understand it without the corresponding private key.

Transmission of Ciphered Data:

The ciphertext is then transmitted to the recipient via a network or communication channel. Since encrypted, the data cannot be decrypted without the corresponding private key, ensuring confidentiality during transmission. Even if the message is intercepted, unauthorized parties cannot access the original data.

Decryption Using the Private Key by Recipient:

Once the recipient receives the ciphertext, they use their private key to decrypt it. The decryption algorithm takes the ciphertext and the private key as input and produces the original plaintext as output.

Note: The private key is securely kept by the recipient and is never shared. It is the only key capable of decrypting the message encrypted with the public key.

Recipient Receives the Message:

After decryption, the recipient can now access the original plaintext data, which is now readable again. This completes the secure communication process, where the message has been transmitted securely, protected by the encryption and decryption process.

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

When implementing Asymmetric Encryption in an ASP.NET Core Web API, we can use the following techniques.

RSA (Rivest–Shamir–Adleman) Algorithm
  • RSA is one of the most widely used algorithms for public-key encryption.
  • It provides strong security and is used for both encryption and digital signatures.
  • In ASP.NET Core, RSA can be implemented using the System.Security.Cryptography namespace.
ECDSA (Elliptic Curve Digital Signature Algorithm)
  • ECDSA is a variant of the Digital Signature Algorithm (DSA) based on elliptic curve cryptography.
  • It offers equivalent security with smaller key sizes than RSA, resulting in better performance.
  • ECDSA is primarily used to generate and verify digital signatures.

Note: RSA is suitable for general encryption, while ECDSA is often used for digital signatures with better performance for shorter keys.

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 *