2020年2月28日 星期五

[ASP.NET Core] Encrypt and decrypt request content by HttpMessageHandler and ResourceFilter


 ASP.NET Core   Encryption/Decryption   HttpMessageHandler   ResourceFilter  


Introduction


Though the request is often protected with HTTPS, sometimes we would like to encrypt the content (Http body) of a request in some scenarios.

For example, we have 3 microservices:
A (The data owner = Sender) -> B (Forward data) -> C (Receiver)

We don’t want B to see/know the data in this forwarding process, so that we must encrypt it on A (Sender) by C’s public key and decrypt on C (Receiver) with private key.

Shorten the story and this tutorial’s sample code as below picture,




We will create custom HttpMessageHandler to encrypt the content and custom ResourceFilter to decrypt it on the fly in this article.

Furthermore, the Sender may get the old or incorrect Public Key of Receiver to encrypt the content and ends in the Receiver cannot decrypt the encrypted content with its Private Key.
We will also use Polly’s retry policy to get the right Public Key from Receiver and then retry the request again to make sure the whole delivery is secure and stable. The process is as following,




For encrypting the content of a request, we will create these HttpMessageHandlers:
·       CustomHeaderHttpClientHandler: Put some custom headers on the request.
·       EncryptHttpClientHandler: Encrypt the content of the request.

For retrying request, we will use Polly to create a Retry Policy:
·       PollyRetryPolicyHandler: Get the right Public Key from previous response -> encrypt -> send request.

For decrypting the content of a request, we will create one ResourceFilter:
·       DecryptRequestFilter: Decrypt the content of a request with the Private Key, return Http status code 422(Unprocessable Entity) if fails to decrypt.


The sample code is on my Github, notice that I implemented the Sender and Receiver in the sample Web API project.
(In real world, of course they are separated from each other!)




Environment


ASP.NET Core 3.1.102



Implement library



RSA

I created a façade of XC.RSAUtil to issue RSA key pair, encryption and decryption.

RsaService.cs

 public class RsaService : IAsymKeyServiceIDisposable
 {
        private readonly int keySize;
        private readonly int maxDataSize;
        private readonly HashAlgorithmName hashAlgorithm;
        private readonly RSASignaturePadding signaturePadding;
        private readonly RSAEncryptionPadding encryptionPadding;
        private readonly Encoding encoding = Encoding.UTF8;

        /// <summary>
        /// Constructor using default RSA2 options
        /// </summary>
        public RsaService()
        {
            this.keySize = 2048;
            this.maxDataSize = this.keySize / 8// RSA is only able to encrypt data with max size = key size
            this.hashAlgorithm = HashAlgorithmName.SHA256;
            this.signaturePadding = RSASignaturePadding.Pss;
            this.encryptionPadding = RSAEncryptionPadding.OaepSHA256;
            this.encoding = Encoding.UTF8;
        }

        /// <summary>
        /// Create RSA key
        /// </summary>
        /// <param name="meta">Key's metadata</param>
        /// <returns>Cipherkey object</returns>
        public async Task<CipherKeyCreateKeyAsync()
        {
            var keys = RsaKeyGenerator.Pkcs8Key(2048false);
            var privateKey = keys[0];
            var publicKey = keys[1];
            var key = new CipherKey()
            {
                Id = Guid.NewGuid().ToString(),
                KeyType = KeyTypeEnum.RSA,
                PublicKey = publicKey,
                PrivateKey = privateKey
            };
            return await Task.FromResult(key);
        }

        /// <summary>
        /// Encrypt
        /// </summary>
        /// <param name="key">CipherKey object</param>
        /// <param name="data">Input data</param>
        /// <returns>Encrypted data</returns>
        public async Task<stringEncryptAsync(string publicKeystring data)
        {
            using (var rsaUtil = new RsaPkcs8Util(this.encodingpublicKey))
            {
                var cipher = rsaUtil.EncryptByDataSize(this.maxDataSizedatathis.encryptionPadding);
                return await Task.FromResult(cipher);
            }
        }

        /// <summary>
        /// Decrypt
        /// </summary>
        /// <param name="key">CipherKey object</param>
        /// <param name="cipherData">Encrypted data</param>
        /// <returns>Decrypted data</returns>
        public async Task<stringDecryptAsync(string privateKeystring cipherData)
        {
            using (var rsaUtil = new RsaPkcs8Util(this.encodingstring.EmptyprivateKey))
            {
                var text = rsaUtil.DecryptByDataSize(this.maxDataSizecipherDatathis.encryptionPadding);
                return await Task.FromResult(text);
            }
        }

        public void Dispose()
        {
        }
 }




Key Manager

We will store the keys in MemoryCache, so I create a Key Manager that can save or get the key conveniently.





Implement Sender



HttpMessageHandler

Since we will design the retry process if the Receiver cannot decrypt the encrypted content of request from Sender. The Sender must keep the original content before encrypted in MemoryCache for encrypting again with the correct Public Key in the retry process.

How does Sender know which content it shall get back from MemoryCache?
Lets put the content’s MemoryCache key into a custom Http header: “Request-Cache-Id” on the request, and when Receiver response 422 (that means Receiver fails to decrypt the content), Receiver put the “Request-Cache-Id” and “Public-Key” into the response’s header to make sure Sender can get the mapping content from MemoryCache by “Request-Cache-Id” and then encrypt it again with the correct “Public-Key” in the retry process.

Also we would like to know how many times the request is retried, so we put the other custom header: “Retry-Times” to record retry times.

So first lets create a HttpMessageHandler to append the 2 custom headers on the request.


CustomHttpHeaderFactory.cs

 public class CustomHttpHeaderFactory
 {
        /// <summary>
        /// Http header name for keeping value of retried times
        /// </summary>
        /// <remarks>
        /// This header is used to check how many times the request had been retied.
        /// </remarks>
        public static string RetryTimes { get; } = "Retry-Times";

        /// <summary>
        /// Request cache's id
        /// </summary>
        public static string RequestCacheId { get; } = "Request-Cache-Id";

        /// <summary>
        /// Http header name for keeping public key of receiver
        /// </summary>
        /// <remarks>
        /// Put the header on response if receiver cannot decrypt data
        /// </remarks>
        public static string PublicKey { get; } = "Public-Key";
 }



CustomHttpHeaderFactory.cs

 public class CustomHeaderHttpClientHandler : DelegatingHandler
 {
        protected override async Task<HttpResponseMessageSendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            const int Zero = 0;

            var isRetryTimesHeaderSet = request.Headers.TryGetValues(CustomHttpHeaderFactory.RetryTimesout IEnumerable<stringretryTimesValues);
            var isRequestCacheIdHeaderSet = request.Headers.TryGetValues(CustomHttpHeaderFactory.RequestCacheIdout IEnumerable<stringrequestCacheIdValues);

            // Add header: Retry-Times
            if (!isRetryTimesHeaderSet || retryTimesValues.Count() > 1)
            {
                request.Headers.Remove(CustomHttpHeaderFactory.RetryTimes);
                request.Headers.Add(CustomHttpHeaderFactory.RetryTimesZero.ToString());
            }

            // Add header: Request-Cache-Id
            if (!isRequestCacheIdHeaderSet || requestCacheIdValues.Count() > 1)
            {
                request.Headers.Remove(CustomHttpHeaderFactory.RequestCacheId);
                request.Headers.Add(CustomHttpHeaderFactory.RequestCacheIdCacheKeyFactory.GetKeyRequestCache());
            }

            // base.SendAsync calls the inner handler
            var response = await base.SendAsync(requestcancellationToken);
            return response;
        }
 }




Then we will create another HttpMessageHandler to encrypt the content of request.


EncryptHttpClientHandler.cs

 public class EncryptHttpClientHandler : DelegatingHandler
 {
        private const int CacheRequestTimeout = 60// Time(Seconds) to keeping the original request in MemoryCache
        private readonly IKeyManager keyManager = null;
        private readonly IMemoryCache memoryCache = null;
        private readonly ILogger logger;

        /// <summary>
        /// Constructor
        /// </summary>
        public EncryptHttpClientHandler(
            IKeyManager keyManager,
            IMemoryCache memoryCache,
            ILogger<EncryptHttpClientHandlerlogger)
        {
            this.keyManager = keyManager;
            this.memoryCache = memoryCache;
            this.logger = logger;
        }

        /// <summary>
        /// Send the request
        /// </summary>
        /// <param name="request">Request</param>
        /// <param name="cancellationToken">Cancellation token</param>
        /// <returns>HttpResponseMessage</returns>
        protected override async Task<HttpResponseMessageSendAsync(
            HttpRequestMessage request,
            CancellationToken cancellationToken)
        {
            #region Get receiver-name and retry-times from Http Header

            // Retry Times
            request.Headers.TryGetValues(CustomHttpHeaderFactory.RetryTimesout IEnumerable<stringretryTimesValues);
            int retryTimes = 0;
            int.TryParse(retryTimesValues.FirstOrDefault(), out retryTimes);

            // RequestCacheId
            request.Headers.TryGetValues(CustomHttpHeaderFactory.RequestCacheIdout IEnumerable<stringrequestCacheIdValues);
            var requestCacheId = requestCacheIdValues.FirstOrDefault();
            #endregion

            #region Encrypt the Http Request (Only in the first time)

            if (retryTimes.Equals(0))
            {
                this.logger.LogDebug($"Start encrypting request...");
                using (var rsa = new RsaService())
                {
                    #region Get public key
                    var publicKey = await this.keyManager.GetPublicKeyAsync(KeyTypeEnum.RSA);
                    #endregion

                    #region Get original payload

                    // Load payload
                    string content = await request.Content.ReadAsStringAsync();

                    // Remove escapted character, eq. "\"xxxxx\"" => "xxxxx"
                    var jsonPayload = JsonConvert.DeserializeObject<string>(content);
                    #endregion

                    #region Save the original payload before encrypted

                    var cacheKey = requestCacheId;
                    this.memoryCache.Set(cacheKeyjsonPayloadDateTimeOffset.Now.AddSeconds(CacheRequestTimeout));

                    this.logger.LogDebug($"Successfully caching original request to memory cache: {cacheKey}.");

                    #endregion

                    #region Encrypt

                    // Encrypt
                    string encryptedPayload = await rsa.EncryptAsync(publicKeyjsonPayload);

                    // Replace the original content with the encrypted one
                    var newContent = new System.Net.Http.StringContent($"\"{encryptedPayload}\""Encoding.UTF8"application/json");
                    ////newContent.Headers.ContentType.CharSet = string.Empty;
                    request.Content = newContent;

                    this.logger.LogDebug($"Successfully encrypting request.");
                    #endregion
                }
            }
            #endregion

            // base.SendAsync calls the inner handler
            var response = await base.SendAsync(requestcancellationToken);
            return response;
        }
 }



Polly: Retry policy

Now we are going to create a Polly retry policy which can retry request on 422 response for Sender, and also

·       Update the correct Public Key to MemoryCache
·       Encrypt the original data with the correct Public Key


PollyRetryPolicyHandler.cs

public class PollyRetryPolicyHandler
    {
        private const int DefaultMaxRetryTimes = 1;
        private readonly IKeyManager keyManager = null;
        private readonly IMemoryCache memoryCache = null;
        private readonly ILogger logger = null;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="keyManager">Key Manager</param>
        /// <param name="memoryCache">MemoryCache</param>
        /// <param name="logger"></param>
        public PollyRetryPolicyHandler(
            IKeyManager keyManager,
            IMemoryCache memoryCache,
            ILogger<PollyRetryPolicyHandlerlogger)
        {
            this.keyManager = keyManager;
            this.memoryCache = memoryCache;
            this.logger = logger;
        }

        /// <summary>
        /// Create Retry policy hander instance
        /// </summary>
        /// <returns>PolicyBuilder of HttpResponseMessage</returns>
        public async Task<Polly.Retry.AsyncRetryPolicy<HttpResponseMessage>> CreateAsync(int maxRetryTimes = DefaultMaxRetryTimes)
        {
            var retryPolicy = Policy.HandleResult<HttpResponseMessage>(r => r.StatusCode.Equals(HttpStatusCode.UnprocessableEntity))
                .RetryAsync(maxRetryTimesasync (exceptionretryCount) =>
                {
                    this.logger.LogWarning($"The encrypted data was rejected, update public key and will retry {retryCount}/{maxRetryTimes}(times/total)...");

                    var request = exception.Result.RequestMessage;
                    var response = exception.Result;

                    // Public key
                    response.Headers.TryGetValues(CustomHttpHeaderFactory.PublicKeyout IEnumerable<stringpublicKeyValues);
                    var correctPublicKey = publicKeyValues.FirstOrDefault();

                    // RequestICached
                    response.Headers.TryGetValues(CustomHttpHeaderFactory.RequestCacheIdout IEnumerable<stringrequestCacheIdValues);
                    var requestCacheId = requestCacheIdValues.FirstOrDefault();

                    if (string.IsNullOrEmpty(correctPublicKey))
                    {
                        this.logger.LogWarning($"The response does not have required header: \"{CustomHttpHeaderFactory.PublicKey}\". Stop retrying the request!");
                        throw new OperationCanceledException();
                    }
                    else if (string.IsNullOrEmpty(requestCacheId))
                    {
                        this.logger.LogWarning($"The response does not have required header: \"{CustomHttpHeaderFactory.RequestCacheId}\" on response. Stop retrying the request!");
                        throw new OperationCanceledException();
                    }
                    else
                    {
                        #region Get the original request
                        var cacheKey = requestCacheId;
                        this.memoryCache.TryGetValue(cacheKeyout string jsonPayload);
                        if (string.IsNullOrEmpty(jsonPayload))
                        {
                            this.logger.LogWarning($"Lost the original request in MemoryCache (Key: {cacheKey}). Stop retrying the request!");
                            throw new OperationCanceledException();
                        }

                        #endregion

                        #region Encrypt the original request by the new public key

                        using (var rsa = new RsaService())
                        {
                            string encryptedPayload = await rsa.EncryptAsync(correctPublicKeyjsonPayload);

                            // Replace the original content with the encrypted one
                            var newContent = new System.Net.Http.StringContent($"\"{encryptedPayload}\""Encoding.UTF8"application/json");
                            request.Content = newContent;

                            this.logger.LogDebug($"Successfully encrypting request.");
                        }
                        #endregion

                        #region Retry times incremental
                        request.Headers.Remove(CustomHttpHeaderFactory.RetryTimes);
                        request.Headers.Add(CustomHttpHeaderFactory.RetryTimesretryCount.ToString());
                        #endregion

                        #region Update the correct public key with KeyManager

                        var key = (await this.keyManager.GetKeyAsync(KeyTypeEnum.RSA));
                        key.PublicKey = correctPublicKey;

                        await this.keyManager.SaveKeyAsync(key);
                        this.logger.LogWarning($"Updated the correct public key. Now start retrying sending request.");
                        #endregion
                    }
                });

            return await Task.FromResult(retryPolicy);
        }
    }

Notice that the retry request will not run the logic of HttpMessageHandlers again!


Apply Handlers and Retry policy to HttpClientFactory

The handlers and retry policy for the Http request(s) can be applied to HttpClientFactory as following,


Startup.cs: ConfigureServices

 services.AddTransient<CustomHeaderHttpClientHandler>();
 services.AddTransient<EncryptHttpClientHandler>();
 services.AddSingleton<PollyRetryPolicyHandler>();

 services.AddHttpClient("MyHttpClient"x =>
 {
                x.BaseAddress = new Uri("https://localhost:5001");
                x.Timeout = TimeSpan.FromMinutes(1);
 })
 .AddHttpMessageHandler<CustomHeaderHttpClientHandler>()
 .AddHttpMessageHandler<EncryptHttpClientHandler>()
 .AddPolicyHandler((serviceProviderrequest) =>
 {
                var pollyHandler = serviceProvider.GetService<PollyRetryPolicyHandler>();
                return pollyHandler.CreateAsync().Result;
 });




Send a request with encrypted content!

Now we can easily use the HttpClient from injected HttpClientFactory to encrypt the content of a request!

var httpClient = this.httpClientFactory.CreateClient("MyHttpClient");
var response = await httpClient.PostAsJsonAsync("api/Demo/Receive"this.testData);





Implement Receiver



Resource Filter for decrypting request’s content

The Receiver can use Resource Filters (IResourceFilter or IAsyncResourceFilter), to decrypt the content of request like following,

[HttpPost]
[TypeFilter(typeof(DecryptRequestFilter))]
[Route("Receive")]
public async Task<IActionResult> ReceiveAsync([FromBody]string jsonStr)
{
var model = await Task.Run(() => JsonConvert.DeserializeObject<MyModel>(jsonStr));

// Now you can do something with the data...
}

Why not using Action Filters but Resource Filters?
That is because the Action Model’s value (i.e. jsonStr)  has been already bind before Action Filters being triggered.
Resource Filters will be triggered before binding value to the Model, see below diagram from Microsoft Doc for more details.




So here is the Resource Filter to decrypt the content of request, including of response 422(Unprocessable Entity) when cannot decrypt data.

DecryptRequestFilter.cs

public class DecryptRequestFilter : AttributeIAsyncResourceFilter
    {
        private readonly IKeyManager keyManager = null;
        private readonly ILogger logger = null;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="legalReceiver">The legal receiver</param>
        /// <param name="keyManager">Key manager</param>
        /// <param name="logger">Logger</param>
        public DecryptRequestFilter(
            IKeyManager keyManager,
            ILogger<DecryptRequestFilter> logger)
        {
            this.keyManager = keyManager;
            this.logger = logger;
        }

        /// <summary>
        /// OnResourceExecutionAsync
        /// </summary>
        /// <param name="context">ResourceExecutingContext</param>
        /// <param name="next">ResourceExecutionDelegate</param>
        public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
        {
            this.logger.LogDebug($"Start decrpt process from {nameof(DecryptRequestFilter)}");

            var isForwardRequestToApiAction = await this.OnBeforeActionAsync(context);

            if (isForwardRequestToApiAction)
            {
                await next();
            }

            this.logger.LogDebug($"End decrpt process from {nameof(DecryptRequestFilter)}");

            // HACK: remove this methods cus we cannot write Response after it's set by the action
            ////await this.OnAfterActionAsync(context);
        }

        /// <summary>
        /// OnActionExecutionAsync
        /// </summary>
        /// <param name="context">ActionExecutingContext</param>
        /// <param name="next">ActionExecutionDelegate</param>
        private async Task<bool> OnBeforeActionAsync(ResourceExecutingContext context)
        {
            var request = context.HttpContext.Request;
            var err = string.Empty;
            bool isForwardRequestToApiAction = true;

            // Get request's custom headers
            var retryTimes = int.Parse(this.GetHeaderSingleValue(request.Headers, CustomHttpHeaderFactory.RetryTimes) ?? "0");
            var requestCacheId = this.GetHeaderSingleValue(request.Headers, CustomHttpHeaderFactory.RequestCacheId);

            // Validate the Receiver value from header
            if (this.ValidateRequiredHeaders(requestCacheId, ref err))
            {
                var encryptedPayload = string.Empty;

                #region Reading body from Http request

                var initialBody = request.Body;
                request.EnableBuffering();  // Use request.EnableRewind() instead before ASP.NET Core 3
                using (var reader = new StreamReader(request.Body))
                {
                        var content = await reader.ReadToEndAsync();

                        // Remove escapted character, eq. "\"xxxxx\"" => "xxxxx"
                        encryptedPayload = JsonConvert.DeserializeObject<string>(content);
                }
                
                #endregion

                #region Decrypt the encrypted content

                if (string.IsNullOrEmpty(encryptedPayload))
                {
                    isForwardRequestToApiAction = false;
                }
                else
                {
                    using (var rsa = new RsaService())
                    {
                        try
                        {
                            var privateKey = await this.keyManager.GetPrivateKeyAsync(KeyTypeEnum.RSA);

                            #region Decrypt and refactor to add escape charater for "\"

                            // Get decrypted string
                            var decryptedPayload = await rsa.DecryptAsync(privateKey, encryptedPayload);

                            // Add escape charater for \"
                            var escaptedDecryptedPayload = decryptedPayload.Replace("\"""\\\"");
                            escaptedDecryptedPayload = $"\"{escaptedDecryptedPayload}\"";
                            #endregion

                            #region Convert the decrypted payload to byte array and bind to HttpRequest's body

                            byte[] byteContent = Encoding.UTF8.GetBytes(escaptedDecryptedPayload);
                            request.EnableBuffering(); // Use request.EnableRewind() instead before ASP.NET Core 3
                            request.Body.Position = 0;
                            using (var reader = new StreamReader(request.Body))
                            {
                                request.Body = new MemoryStream(byteContent);
                                request.Headers.Remove("content-type");
                                request.Headers.Add("content-type""application/json");
                            }
                            #endregion
                        }
                        catch (Exception ex) when (ex is FormatException || ex is CryptographicException)
                        {
                            /*
                             * FormatException occurs when the private key is incorrect RSA key
                             * CryptographicException occuers when the data is incorrect encrypted
                             */

                            var publicKey = await this.keyManager.GetPublicKeyAsync(KeyTypeEnum.RSA);

                            this.logger.LogWarning(ex, $"Cannot decrypt with private key!");
                            context.Result = new EmptyResult();
                            context.HttpContext.Response.StatusCode = StatusCodes.Status422UnprocessableEntity;
                            context.HttpContext.Response.Headers.Add(CustomHttpHeaderFactory.RequestCacheId, requestCacheId);
                            context.HttpContext.Response.Headers.Add(CustomHttpHeaderFactory.PublicKey, publicKey);

                            isForwardRequestToApiAction = false;
                        }
                        catch (Exception ex)
                        {
                            this.logger.LogError(ex, ex.Message);
                            context.Result = new EmptyResult();
                            context.HttpContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
                            isForwardRequestToApiAction = false;
                        }
                    }
                }

                this.logger.LogDebug($"{nameof(DecryptRequestFilter)}: end decrypting content from Http request.");
                #endregion
            }

            if (!string.IsNullOrEmpty(err))
            {
                isForwardRequestToApiAction = false;
                await context.HttpContext.Response.WriteAsync(err);
            }

            return isForwardRequestToApiAction;
        }
 }


Key points:

1.  When the Sender used an incorrect Public Key to encrypt, the decryption will throw CryptographicException. The Resource Filter will overwrite the Http response to 422 response and then force to response without going to API’s Action

2.  While the request is specified as “content-type: application/json” and the Model is string type (JSON string), we have to deal with the escape character of JSON string.
l   Remove escape character of the encrypted cipher, eq. \"HJDS1234OFHREQELPKGGUIEHR\" to HJDS1234OFHREQELPKGGUIEHR
l   After decryption, we get the JSON string with escape character, like {\"Name\":\"JB\"}. We have to escape the “escape character” and add " with escape character on the first and last position before overwriting to memory stream of Http request’s body. Eq. {\"Name\":\"JB\"} to \"{\\""Name\\"":\\""JB\\""}\"




Put all together



Lets create 2 APIs to simulate sending and receiving…


DemoController.cs

[Route("api/[controller]")]
[ApiController]
[AllowAnonymous]
public class DemoController : ControllerBase
{
        private readonly IHttpClientFactory httpClientFactory = null;
        private readonly ILogger logger = null;
        private readonly string testData = null;

        public DemoController(
            IHttpClientFactory httpClientFactory,
            ILogger<DemoController> logger)
        {
            this.httpClientFactory = httpClientFactory;
            this.logger = logger;

            var requestModel = new Bank()
            {
                Name = "NoMoney bank",
                Merchants = new List<Merchant>()
                   {
                    new Merchant{ Name="Merchant1", Address = "@#$#$%^&" },
                    new Merchant{ Name="Merchant2", Address = ")^%$(*&#" }
                   }
            };

            this.testData = JsonConvert.SerializeObject(requestModel);
        }

        [HttpPost]
        [Route("Send")]
        public async Task<IActionResult> SendAsync()
        {
            var httpClient = this.httpClientFactory.CreateClient(HttpClientNameEnum.CipherHttpClient.ToString());
            var response = await httpClient.PostAsJsonAsync("api/Demo/Receive"this.testData);

            if (response.IsSuccessStatusCode)
            {
                return this.Ok();
            }
            else
            {
                return this.BadRequest();
            }
        }

        [HttpPost]
        [TypeFilter(typeof(DecryptRequestFilter))]
        [Route("Receive")]
        public async Task<IActionResult> ReceiveAsync([FromBody]string jsonStr)
        {
            // (Optional)Deserialize to an object, array or something...
            var model = await Task.Run(() => JsonConvert.DeserializeObject<Bank>(jsonStr));

            if (string.IsNullOrEmpty(jsonStr))
            {
                return this.BadRequest();
            }

            if (jsonStr.Equals(this.testData))
            {
                return this.Ok();
            }
            else
            {
                return this.StatusCode(StatusCodes.Status409Conflict);
            }
        }
}


By using HttpMessageHandler and Resource Filter, the API’s code is clean :)



Demo

Here is the traffic for sending the request with encrypted content.




See if retry policy works

To see if our retry policy works, update the following code in DecryptRequestFilter,

#region Get public key 

// var privateKey = await this.keyManager.GetPrivateKeyAsync(KeyTypeEnum.RSA);

// To test retry policy...
var privateKey = string.Empty;
if (retryTimes > 0)
     privateKey = await this.keyManager.GetPrivateKeyAsync(KeyTypeEnum.RSA);
else
     privateKey = (await this.keyManager.CreateDefaultAsymmetricKey(KeyTypeEnum.RSA, isIncludePrivateKey: true)).PrivateKey; // Set a incorrect private key to decrypt

#endregion


The first decryption will not use the mapping/correct Private Key, that results in Sender will get 422 response and trigger retry policy.
Lets see what will happens…

The first request will get 422 response with a correct Public Key in response’s header.




The retry request succeeds after using the correct Public Key.









Reference