Security  

Secure API Payloads Using AES and RSA Encryption in Angular and .NET Core

Introduction

In today’s digital landscape, securing sensitive data during transmission is more critical than ever. While HTTPS provides a secure channel, adding an extra layer of encryption at the application level can significantly enhance security. This article walks you through a robust encryption-decryption strategy using AES and RSA, implemented on the frontend with Angular and backend with .NET Core.

Solution Overview

Part 1. Encrypting the Payload on the Frontend.

  1. Generate a dynamic AES Key.
  2. Encrypt the API Payload using AES Key.
  3. Encrypt the AES KEY using the RSA Public Key.
  4. Prepare the new encrypted payload.
    • { Encrypted AES Key, Encrypted Payload }
  5. Pass this new payload to the backend.

Part 2. Decrypting the Payload on the Backend.

  1. Decrypt the AES Key using the RSA Private key.
  2. Decrypt the payload using the AES Key.

Solution Overview

Part 1. Encrypting the Payload on the Frontend (Angular)

Step 1.1. Define a service for AES Encryption.

In this step, we will define a service in an Angular application that we will use 1.) To generate a dynamic AES key & 2.) To encrypt the payload/data using AES encryption.

In order to generate a dynamic AES key and to perform AES encryption, we will use the crypto-js library. Angular doesn't include built-in cryptographic utilities, so the crypto-js library is often used for cryptographic operations.

Now, install crypto-js in your application using the below cli command.

npm i crypto-js

After successfully installing crypto-js, create a service within your application named aes-encryption.service.ts. Add a function to generate a dynamic AES key and another function to encrypt the data.

import { Injectable } from "@angular/core";
import * as CryptoJS from 'crypto-js';

@Injectable({
    providedIn: "root"
})
export class AESEncryptionService {
  
    constructor() { }
   
    // Generate a random 256 bit AES key
    generateAESKey(){
        // Generate a random 256-bit (12-byte) key
        const secretKey = CryptoJS.lib.WordArray.random(12);
        // Convert the key to a string if needed
        const secretKeyString = secretKey.toString(CryptoJS.enc.Hex);
        return secretKeyString;
    }

    // Encrypt the pain text
    encryptUsingAES256(plain_data,key) {
        const secretKey = CryptoJS.enc.Utf8.parse(key);
        const encrypted_string = CryptoJS.AES.encrypt(JSON.stringify(plain_data), secretKey, {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7
          }).toString();
        return encrypted_string;
    }    
}

Step 1.2. Generate RSA Private & Public Key.

To implement RSA encryption, we will need a Private & Public Key. The private key and public key are two parts of a cryptographic key pair used in asymmetric encryption.To generate RSA keys you can use opneSSL library.

How to generate RSA keys?

I have explained the steps in my other article, which you can refer to by clicking the link below.

Or you can also use any online available tools to generate the keys.

As a result of this site, you will have two files, save both files with you,

  1. private_key.pem: Contains the RSA private key (Note - We will use this file later in our .NET solution for decryption)
  2. public_key.pem: Contains the RSA public key.
    RSA public key
    Key Screenshot

Step 1.3. Define a service for RSA Encryption.

In this step, we will define a service that we will use to encrypt the AES key using RSA encryption with the help of the RSA public key that we have generated in Step 1.2.

In order to implement RSA encryption, we will use jsencrypt. This library allows you to encrypt data on the client side using a public key, which can then be decrypted on the server side using the corresponding private key.

Now, install jsencrypt in your application using the below cli command.

npm i jsencrypt

After successfully installing jsencrypt, create a service within your application named rsa-encryption.service.ts.

Now, let's add a variable named publicKey & copy the content of the file 'public_key.pem' (generated in Step 1.2) in this variable and add a function which will encrypt the data using this public key.

import { Injectable } from '@angular/core';
import { JSEncrypt } from 'jsencrypt';

@Injectable({
    providedIn: 'root',
})
export class RSAEncryptionService {

    $encrypt: any; // JSEncrypt Instance

    publicKey: string = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxJ+L8Tf16me+R3JlE02R
1EX8pJVnRN1BYqPVc0qJSFyxXx7HBT6xYKRhBQp3+qbY1DlBu38zJnSZtIyoWLvt
Yyg9ippQyCYh/B0zuIxfEkcec4DUQ/xz2+Q4vG8LL281+7Jv2xKEdzco05B9lOf1
Gj1xn68NO7zDEzQJqSnc3/SIfKlZBg6s9UJOlft76xoVJMLGFQLKoUjNyiYpB33S
Aiv5WjmfYZGXyBmVnNy6fUg/ZrekT4TIM39RI6T+R0k0vlvlNS7KMBG7ZfOYXTkM
I91i1QTZIFARIDngrZGlanGincqBtLAmn74X6evEn7ugy/Wg89egMr43+HSeOfM6
1QIDAQAB
-----END PUBLIC KEY-----`;

    constructor() {
        this.$encrypt = new JSEncrypt();
     }

    encryptWithPublicKey(plaintext: string): string {
        this.$encrypt.setPublicKey(this.publicKey);
        var cypherText = this.$encrypt.encrypt(plaintext);      
        return cypherText;
    }
}

Step 1.4. Implement Encryption & Pass Encrypted Data To Server

Now, we have created services for AES & RSA encryption. Let's use these services to implement the client-side encryption.

  1. In your Component, import the following services.
    • AESEncryptionService: To encrypt the formData using AES encryption.
    • RSAEncryptionService: To encrypt the AES key using RSA encryption.
    • AppService: To make an HTTP call and pass data to the server (I have not added code for AppService, please create the same if you don't have it already in your solution).
  2. Define a method to capture the data.
  3. Finally, define a method to perform the required encryption and call your service to pass the final payload to the backend server.
import { Component} from '@angular/core';
import { AESEncryptionService } from './Services/aes-encryption.service';
import { RSAEncryptionService } from './Services/rsa-encryption.service';
import { AppService } from './Services/app.service';

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {

  constructor(private appService: AppService,
    private aes_encryptionSvc: AESEncryptionService, 
    private rsa_encryptionSvc: RSAEncryptionService) { }
    
    //** Note - This is a sample code, Define your business logic to capture or prepare data */
    onSubmitButtonClick(){
      const _data = {
        Name: "sandeep nandey",
        Email: "[email protected]",
        Contact: "9876543210",
        Address: "Address Field-1, Landmark, City, State - Pincode"
      }
      this.submitForm(_data);
    }
    
    // Submit Form Data
    submitForm(formData) {

      // 1. Generate Dynamic AES KEY 
      const _KEY = this.aes_encryptionSvc.generateAESKey();    
  
      // 2. Encrypt the formData using AES Key
      const encrypted_formData :any = this.aes_encryptionSvc.encryptUsingAES256(formData,_KEY);
  
      // 3. Encrypt the AES KEY using RSA Public Key 
      const aesKey: any = {AESKey: _KEY};
      const encrypted_aesKey = this.rsa_encryptionSvc.encryptWithPublicKey(JSON.stringify(aesKey));
      
      // 4. Prepare the new payload (Encrypted formData, Encrypted _KEY)
      var final_payload = {
        Data:encrypted_formData,
        Key:encrypted_aesKey
      }
  
      // 5. Pass this new payload to Server
      this.appService.submitData(final_payload).subscribe((d: any) => {     
          if (d && d.Result) {
            console.log("Success - " + d.Result)
        }
      }, _error => {
        console.log(_error)
      });
    }
}

Part 2. Decrypting the Payload on the Backend (.NET)

Step 2.1. Define a service for AES Decryption.

Create a class (AESEncryptionService.cs) and define a method , which we will use to decrypt the form data using AES key.

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

namespace demo.api.Services
{
    public class AESEncryptionService
    {
        public string DecryptPayload(string cipherText, string key)
        {
            try
            {
                var encryptedBytes = Convert.FromBase64String(cipherText);
                var keyBytes = Encoding.UTF8.GetBytes(key);

                using (var aes = Aes.Create())
                {
                    aes.Key = keyBytes;
                    aes.Mode = CipherMode.ECB;
                    aes.Padding = PaddingMode.PKCS7;

                    using (var decryptor = aes.CreateDecryptor())
                    {
                        var resultBytes = decryptor.TransformFinalBlock(encryptedBytes, 0, encryptedBytes.Length);
                        var decryptedData = Encoding.UTF8.GetString(resultBytes);

                        return decryptedData;
                    }
                }

            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
    }
}

Step 2.2. Define a service for RSA Decryption.

In this step, we are going to use the RSA private key for decryption. For the private key, please use the file that you have generated above in Step 1.2: Generate RSA Private & Public Key.

  1. Add private_key.pem file in our solution (as a best practice, store private key in a secure place, i.e, AWS Secret Manager / Parameter Store, Azure Key Vault )
    Azure Key Vault
  2. Create another class, RSAEncryptionService.cs, and define a method to read the private key from the solution and another method to decrypt the data.
using System.Security.Cryptography;
using System.Text;
 
namespace demo.api.Services
{
    public class RSAEncryptionService
    {
        private readonly string privateKeyName = "private_key.pem";
        private readonly string privateKeyPath = "RSAKey";
 
        public string DecryptUsingPrivateKey(string encryptedData)
        {
            using (RSA rsa = RSA.Create())
            {
                // Get the private key file
                string path = GetPath(privateKeyName, privateKeyPath);
 
                // Read the key 
                string strPrivateKeyPem = File.ReadAllText(path);
 
                // Import the key
                rsa.ImportFromPem(strPrivateKeyPem.ToCharArray());
 
                // Convert from base64 string 
                byte[] dataToDecrypt = Convert.FromBase64String(encryptedData);
 
                // Descrypt the data
                byte[] decryptedData_encoded = rsa.Decrypt(dataToDecrypt, RSAEncryptionPadding.Pkcs1);
 
                // Decode data
                string decryptedData_decoded = Encoding.UTF8.GetString(decryptedData_encoded);
 
                // return Descrypted String
                return decryptedData_decoded;
            }
        }
 
        private string GetPath(string fileName, string filePath)
        {
            var path = Path.Combine(".", filePath);
            return Path.Combine(path, fileName);
        }
    }
}

Step 2.3. Define a method to Decrypt the Payload.

Now we have everything in place, let's define a method in your API controller and write logic to decrypt the incoming payload.

using demo.api.Model;
using demo.api.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Text.Json;
 
namespace demo.api.Controllers
{
    [Authorize]
    [Route("api/[controller]/[action]")]
    public class DemoController : Controller
    {
        [HttpPost]
        public async Task<IActionResult> SubmitData([FromBody] dynamic _payloadData)
        {
            try
            {
                string _data = JsonSerializer.Serialize(_payloadData);
 
                var options = new JsonSerializerOptions { DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull  };
 
                EncryptedPayload encryptedPayload = JsonSerializer.Deserialize<EncryptedPayload>(_data, options);
 
                // 1. Decrypt the AES Key using RSA Private Key
                RSAEncryptionService _rsa_encryptionService = new RSAEncryptionService();
                var decrypted_AES_Key = _rsa_encryptionService.DecryptUsingPrivateKey(encryptedPayload.Key);
                
                Key _key = JsonSerializer.Deserialize<Key>(decrypted_AES_Key, options);
 
                // AES Key 
                string aes_Key = _key.AESKey;
 
                // 2. Descrypt the payload using AES Key
                AESEncryptionService _encryptionService = new AESEncryptionService();
                string decryptedPayload = _encryptionService.DecryptPayload(encryptedPayload.Data, aes_Key);
                
                FormData formData = JsonSerializer.Deserialize<FormData>(decryptedPayload, options);
 
                /* Add Logic To Process the data */
                // Read the data
                string name = formData.Name;
                string email = formData.Email;
 
                Response result = new Response();
                result.Result = "Success";
                return Content(JsonSerializer.Serialize(result));
            }
            // exception handling
            catch (Exception ex)
            {              
                return StatusCode(StatusCodes.Status500InternalServerError, ex.Message);
            }
        }
    }
}

Finally, decryption on the server side is completed. After successful implementation of the above steps, you should be able to get End-to-End encryption working.

Benefits of This Approach

  1. End-to-End Encryption: Data is encrypted before it leaves the client and decrypted only on the server.
  2. Dynamic Keys: Each request uses a new AES key, reducing the risk of key reuse.
  3. Hybrid Encryption: Combines the speed of AES with the security of RSA.
    • AES handles the heavy lifting of encrypting large data efficiently.
    • RSA securely transmits the AES key, ensuring only the intended recipient can decrypt it.
    • This hybrid encryption model is ideal for applications requiring high security.

Note. Always ensure your RSA keys are securely stored and rotated periodically.

Conclusion

To ensure both data confidentiality and secure key exchange, a hybrid encryption model was implemented.

  • Data is encrypted using a symmetric AES key, which offers high performance and efficiency for large data volumes.
  • The AES key itself is encrypted using an RSA public key, ensuring that only the intended recipient with the corresponding RSA private key can decrypt it.
  • On the server side, the RSA private key is used to decrypt the AES key, which is then used to decrypt the original data.

This layered encryption strategy combines the speed of symmetric encryption with the security of asymmetric encryption, making it well-suited for secure data transmission in distributed systems.

Bonus Question - Why can't we use RSA directly to encrypt the payload?

While RSA is a powerful encryption algorithm, it's not ideal for encrypting large payloads. RSA is excellent for securely exchanging small pieces of data like encryption keys, but it's not designed for encrypting large payloads. That’s why we use AES for the data and RSA for the key, combining the strengths of both.

  1. Performance Limitations
    • RSA is computationally expensive. It's much slower than symmetric algorithms like AES.
    • Encrypting large data (like JSON payloads or files) with RSA would significantly degrade performance on both the client and server.
  2. Size Limitations
    • RSA can only encrypt data up to a certain size, which is much smaller than the key size.
    • For example, with a 2048-bit RSA key, the maximum data size you can encrypt is around 245 bytes (with padding).
    • Most real-world payloads exceed this limit, making RSA unsuitable for direct payload encryption.
  3. Best Practice: Hybrid Encryption
    • The industry-standard approach is to use hybrid encryption.
    • AES (symmetric) for encrypting the actual data fast and efficiently.
    • RSA (asymmetric) for encrypting the AES key and secure key exchange.
Feature AES RSA
Speed Very fast (suitable for large data) Slower (computationally intensive)
Payload Size Any size Limited
Key Exchange Need a secure channel Secure with public/private keys
Use Case Encrypting actual data Encrypting keys or small secrets
Typical Application File encryption, secure messaging Secure key exchange, digital signatures