Implementing HTTP Request and Response Encryption in ASP.NET Core with Custom Attributes

Published on Nov 7, 2024 | Reading time: 4 min


Securing Data Transmission in ASP.NET Core Web APIs: A Custom Encryption Implementation

In modern web application development, safeguarding sensitive data during network transmission represents a critical security requirement. This technical guide presents a robust solution for implementing end-to-end encryption in ASP.NET Core Web APIs through custom attributes and cryptographic filters, ensuring both request and response payloads remain protected using industry-standard AES encryption.

Prerequisites

To effectively implement this solution, developers should possess:

Architectural Overview

Our implementation employs a dual-component strategy within the ASP.NET Core request processing pipeline:

  1. SecureHttpAttribute: A declarative marker for controllers/actions requiring encryption
  2. SecureHttpFilter: An executable filter handling cryptographic operations

This design leverages ASP.NET Core’s filter pipeline to intercept and transform HTTP message bodies before they traverse network boundaries, while maintaining separation of concerns through attribute-based configuration.

Implementation Breakdown

Component 1: SecureHttpAttribute Factory

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class SecureHttpAttribute : Attribute, IFilterFactory
{
    public bool IsReusable => false;

    public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
    {
        var config = serviceProvider.GetRequiredService<IOptions<SystemSettings>>();
        return new SecureHttpFilter(config.Value);
    }
}

Key Functionality:

Component 2: SecureHttpFilter Execution

public class SecureHttpFilter : IAsyncResourceFilter
{
    private readonly Aes _cryptoProvider;

    public SecureHttpFilter(SystemSettings settings)
    {
        _cryptoProvider = InitializeAes(settings.DataEncryptionKey);
    }

    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        context.HttpContext.Request.Body = DecryptInputStream(context.HttpContext.Request.Body);
        context.HttpContext.Response.Body = EncryptOutputStream(context.HttpContext.Response.Body);

        if (context.HttpContext.Request.QueryString.HasValue)
        {
            var decodedQuery = DecodeString(context.HttpContext.Request.QueryString.Value[1..]);
            context.HttpContext.Request.QueryString = new QueryString($"?{decodedQuery}");
        }

        await next();
        await context.HttpContext.Request.Body.DisposeAsync();
        await context.HttpContext.Response.Body.DisposeAsync();
    }


    private CryptoStream EncryptOutputStream(Stream outputStream)
    {
        var encryptor = _cryptoProvider.CreateEncryptor();
        var base64Transform = new ToBase64Transform();
        var encodedStream = new CryptoStream(outputStream, base64Transform, CryptoStreamMode.Write);
        return new CryptoStream(encodedStream, encryptor, CryptoStreamMode.Write);
    }

    private CryptoStream DecryptInputStream(Stream inputStream)
    {
        var decryptor = _cryptoProvider.CreateDecryptor();
        var base64Transform = new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
        var decodedStream = new CryptoStream(inputStream, base64Transform, CryptoStreamMode.Read);
        return new CryptoStream(decodedStream, decryptor, CryptoStreamMode.Read);
    }

    private string DecodeString(string encryptedText)
    {
        using var memoryStream = new MemoryStream(Convert.FromBase64String(encryptedText));
        using var cryptoStream = new CryptoStream(memoryStream, _cryptoProvider.CreateDecryptor(), CryptoStreamMode.Read);
        using var reader = new StreamReader(cryptoStream);
        return reader.ReadToEnd();
    }
}

Execution Flow:

  1. Request Decryption: Inbound payloads are decrypted before controller processing
  2. Response Encryption: Outbound data undergoes encryption prior to transmission
  3. Query Parameter Handling: URL parameters are automatically decoded
  4. Resource Cleanup: Cryptographic streams are properly disposed post-processing

Cryptographic Configuration

private static Aes InitializeAes(string secretKey)
{
    var paddedKey = secretKey.PadRight(32, '0');
    var aes = Aes.Create();
    aes.Key = Encoding.UTF8.GetBytes(paddedKey[..32]);
    aes.IV = Encoding.UTF8.GetBytes(paddedKey[..16]);
    aes.Mode = CipherMode.CBC;
    aes.Padding = PaddingMode.PKCS7;
    return aes;
}

Security Parameters:

Implementation Integration

Controller Configuration

Class-Level Application:

[SecureHttp]
[Route("api/[controller]")]
public class ConfidentialController : ControllerBase
{
    [HttpPost]
    public IActionResult SubmitConfidentialData([FromBody] ConfidentialPayload payload)
    {
        return Ok(payload.Process());
    }
}

Action-Level Application:

[Route("api/[controller]")]
public class ConfidentialController : ControllerBase
{
    [SecureHttp]
    [HttpPost]
    public IActionResult SubmitConfidentialData([FromBody] ConfidentialPayload payload)
    {
        return Ok(payload.Process());
    }
}

Client-Side Implementation

Axios Interceptor Configuration

import axios from 'axios';
import { API_ROOT } from '../../constants/NetworkConfig';
import { encodePayload, decodePayload } from '../../utilities/securityUtils';

const protectedAPI = axios.create({ baseURL: API_ROOT });

protectedAPI.interceptors.request.use((config) => {
  const [path, params] = config.url ? config.url.split('?') : [];

  if (params) {
    config.url = `${path}?${encodePayload(params)}`;
  }

  if (config.data) {
    config.headers['Content-Type'] = 'application/json';
    config.transformRequest = encodePayload;
  }

  config.transformResponse = decodePayload;

  return config;
});

Interceptor Functionality:

Cryptographic Utilities

import CryptoJS from 'crypto-js';
import { ENCRYPTION_SECRET } from '../constants/appSettings';

const secretPadded = ENCRYPTION_SECRET.padEnd(32, '0');

const secureKey = CryptoJS.enc.Utf8.parse(secretPadded.substring(0, 32));
const ivParameter = CryptoJS.enc.Utf8.parse(secretPadded.substring(0, 16));

const securityConfig = {
  iv: ivParameter,
  mode: CryptoJS.mode.CBC,
  padding: CryptoJS.pad.Pkcs7
};


export const encodePayload = (input) => {
  if (input === null || input === undefined) return input;

  const inputData = CryptoJS.enc.Utf8.parse(
    typeof input === 'string' ? input : JSON.stringify(input)
  );

  const encrypted = CryptoJS.AES.encrypt(inputData, secureKey, securityConfig);

  return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
};

export const decodePayload = (encodedInput) => {
  if (!encodedInput) return encodedInput;

  try {
    const encryptedData = CryptoJS.enc.Base64.parse(encodedInput);

    const decrypted = CryptoJS.AES.decrypt(
      { ciphertext: encryptedData },
      secureKey,
      securityConfig
    ).toString(CryptoJS.enc.Utf8);

    try {
      return JSON.parse(decrypted);
    } catch (_) {
      return decrypted;
    }
  } catch (_) {
    return encodedInput;
  }
};

Client-Security Considerations:

Security Analysis

This implementation provides multiple layers of protection:

  1. Payload Confidentiality: AES-256 encryption secures message contents
  2. Data Integrity: CBC mode prevents bit-flipping attacks
  3. Authentication Binding: Shared encryption key validates endpoint legitimacy
  4. Query Parameter Protection: URL parameters receive equivalent security to body content

Operational Considerations

Conclusion

This architectural pattern demonstrates an effective method for implementing transparent encryption in ASP.NET Core Web APIs. By leveraging framework-native filter pipelines and attribute-based programming, developers can enforce encryption policies without compromising code maintainability. The client-side implementation completes the security circle, ensuring end-to-end protection from browser to backend.

When properly implemented with secure key management practices, this solution meets or exceeds industry standards for data-in-transit protection, particularly suitable for applications handling financial data, healthcare records, or other sensitive information subject to regulatory compliance requirements.