재우니의 블로그

Action Filters 를 사용하여 Secrets 을 ASP.NET Core MVC Action Arguments 로 Decrypt 하기

 

legacy application single query, string parameter 의 모든 값을 암호화한 다음 ASP.NET Core MVC endpoint에 HTTP 요청을 했습니다.

이 게시물 에서는 ASP.NET Core MVC에서 Action Filters   활용 하여 action methods 의 개발 경험을 지금까지 사용해 온 것과 동일한 방식으로 만드는 방법을 알아봅니다 .

 

 

The Problem

레거시 시스템은 오늘날 우리가 가지고 있는 모든 프로토콜과 보안 패턴에 액세스할 수 없기 때문에 창의적인 문제 해결을 해야 했을 수 있습니다. 우리의 경우에는 TripleDES를 사용하여 암호화 된 query string parameter  있으며 수신자가 해당 암호화 값을 해독 할 것으로 예상합니다.

 

/?secret=1w58y%2BC60jon25f%2F4VvVHUOX%2FIxs%2FEVx

 

이 기술은 URL의 secret log 에 남을 수 있기 때문일 수 있습니다. 이유가 어째든.. 우리는 해결해야 할 문제가 발생했습니다. 

 

IndexPost 라는 endpoint 에는 number 그리고  name 이라는 2개의 파라미터를 받습니다. 이 매개변수는 action 의 context 에 있을 때 값을 가질 것으로 예상됩니다.

 

[Route(""), HttpPost]
public IActionResult IndexPost(int number, string name)
{
    return View("Index",
        new IndexModel
        {
            Number = number,
            Name = name
        });
}

 

 

이 문제에 대한 간단한 솔루션이 있으며, 여기에서 Action Filters  포함됩니다 .

 

Action Filters 및 Action Arguments 를 사용하는 솔루션

 

Action Filters 는 들어오는 요청을 가로채고 실행(execution) 파이프라인을 강화 시키는 좋은 방법입니다. 우리의 경우 기존 query string parameter 인 secret 를 변환하고, 해당 값을 해독하고 action method 에 매개 변수를 설정 하려고 합니다 .

 

우리의 최종 사용법은 구현된 EncryptedParameters 속성 을 사용할 것이며 , 적용 가능한 메소드에 decorate 할 것입니다.

 

[Route(""), HttpPost]
[EncryptedParameters("secret")]
public IActionResult IndexPost(int number, string name)
{
    return View("Index",
        new IndexModel
        {
            Number = number,
            Name = name
        });
}

 

 

EncryptedParameters 의 구현을 살펴보자,

 

using System;
using System.Globalization;
using System.Linq;
using System.Web;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Secrets.Models;

namespace Secrets.Controllers
{
    public class EncryptedParameters : ActionFilterAttribute
    {
        public string ParameterName { get; }

        public EncryptedParameters(string parameterName = "secret")
        {
            ParameterName = parameterName;
        }

        public override void OnActionExecuting(ActionExecutingContext context)
        {
            var config = context.HttpContext.RequestServices.GetRequiredService<IOptions<CryptoEngine.Secrets>>();
            var encrypted = context.HttpContext.Request.Query[ParameterName].FirstOrDefault();

            // decrypt secret
            var decrypted = CryptoEngine.Decrypt(encrypted, config.Value.Key);
            var collection = HttpUtility.ParseQueryString(decrypted);
            var actionParameters = context.ActionDescriptor.Parameters;

            foreach (var parameter in actionParameters)
            {
                try
                {
                    var value = collection[parameter.Name];

                    if (value == null)
                        continue;

                    // set the action arguments to the values 
                    // from the encrypted parameter
                    context.ActionArguments[parameter.Name] =
                        ConvertToType(value, parameter.ParameterType);
                }
                catch (Exception e)
                {
                    context.ModelState.TryAddModelException(parameter.Name, e);
                }
            }
        }

        private static object? ConvertToType(string value, Type type)
        {
            var underlyingType = Nullable.GetUnderlyingType(type);

            if (value.Length > 0)
            {
                if (type == typeof(DateTimeOffset) || underlyingType == typeof(DateTimeOffset))
                {
                    return DateTimeOffset.Parse(value, CultureInfo.InvariantCulture);
                }

                if (type == typeof(DateTime) || underlyingType == typeof(DateTime))
                {
                    return DateTime.Parse(value, CultureInfo.InvariantCulture);
                }

                if (type == typeof(Guid) || underlyingType == typeof(Guid))
                {
                    return new Guid(value);
                }

                if (type == typeof(Uri) || underlyingType == typeof(Uri))
                {
                    if (Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uri))
                    {
                        return uri;
                    }

                    return null;
                }
            }
            else
            {
                if (type == typeof(Guid))
                {
                    return default(Guid);
                }

                if (underlyingType != null)
                {
                    return null;
                }
            }

            if (underlyingType is not null)
            {
                return Convert.ChangeType(value, underlyingType);
            }

            return Convert.ChangeType(value, type);
        }
    }
}

 

 ActionExecutingContext 를 사용하여 대상 endpoint parameters  에 액세스할 수 있습니다. 그런 다음 우리의 secret 을 해독합니다. 이를 ActionArguments 에 설정하여 알고자 하는 파리미터들의 값들을 셋팅해 줍니다.

public override void OnActionExecuting(ActionExecutingContext context)
{
    var config = context.HttpContext.RequestServices.GetRequiredService<IOptions<CryptoEngine.Secrets>>();
    var encrypted = context.HttpContext.Request.Query[ParameterName].FirstOrDefault();

    // decrypt secret
    var decrypted = CryptoEngine.Decrypt(encrypted, config.Value.Key);
    var collection = HttpUtility.ParseQueryString(decrypted);
    var actionParameters = context.ActionDescriptor.Parameters;

    foreach (var parameter in actionParameters)
    {
        try
        {
            var value = collection[parameter.Name];

            if (value == null)
                continue;

            // set the action arguments to the values 
            // from the encrypted parameter
            context.ActionArguments[parameter.Name] =
                ConvertToType(value, parameter.ParameterType);
        }
        catch (Exception e)
        {
            context.ModelState.TryAddModelException(parameter.Name, e);
        }
    }
}

 

 

type 에 대한 변환은 primitives 및 nullable 유형을 처리하지만 복잡한 models 은 처리하지 않습니다. 이 부분은 특정 요구 사항에 맞게 이 코드를 자유롭게 수정해서 사용하시면 됩니다.

 

CryptoEngine 코드에 관심이 있는 사람들을 위해 여기 있습니다. 하지만 저는 제 자신을 암호화(cryptography) 전문가라고 생각하지 않습니다.

 

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

namespace Secrets.Models
{
    /// <summary>
    /// modified from the following post
    /// https://dotnetcodr.com/2015/10/23/encrypt-and-decrypt-plain-string-with-triple-des-in-c/
    /// </summary>
    public static class CryptoEngine
    {
        public class Secrets
        {
            public string Key { get; set; }
        }

        public static string Encrypt(string source, string key)
        {
            var byteHash = MD5.HashData(Encoding.UTF8.GetBytes(key));
            var tripleDes = new TripleDESCryptoServiceProvider
            {
                Key = byteHash, 
                Mode = CipherMode.ECB
            };
            
            var byteBuff = Encoding.UTF8.GetBytes(source);
            return Convert.ToBase64String(tripleDes.CreateEncryptor()
                .TransformFinalBlock(byteBuff, 0, byteBuff.Length));
        }

        public static string Decrypt(string encodedText, string key)
        {
            var byteHash = MD5.HashData(Encoding.UTF8.GetBytes(key));
            var tripleDes = new TripleDESCryptoServiceProvider
            {
                Key = byteHash, 
                Mode = CipherMode.ECB
            };
            var byteBuff = Convert.FromBase64String(encodedText);
            return Encoding.UTF8.GetString(
                tripleDes
                    .CreateDecryptor()
                    .TransformFinalBlock(byteBuff, 0, byteBuff.Length));
        }
    }
}

 

 

In Practice

사용 중인 이 action filter 를 보여주기 위해 간단한 ASP.NET Core MVC 응용 프로그램을 만들었습니다. view 부터 시작하여 일부 값을 암호화하고 EncryptedParameters 속성을 사용한 다음, endpoint 에 이를 게시합니다 .

 

@model IndexModel
@inject Microsoft.Extensions.Options.IOptions<CryptoEngine.Secrets> config
@{
    ViewData["Title"] = "Home Page";
    var values = "name=Khalid&number=57";
    var secret = CryptoEngine.Encrypt(values, config.Value.Key);
    var url = Url.Action("IndexPost", new {secret});
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>

    <p>Click this link to transmit secret values via Querystring</p>
    <form action="@url" method="POST">
        <p>@url</p>
        <button type="submit">Submit</button>
    </form>
</div>

@if (Model != null)
{
    <section style="margin-top: 1em">
        <div class="text-center">
            <h2>Secrets</h2>
            <div>
                <label asp-for="Name"></label>
                @Model.Name
            </div>
            <div>
                <label asp-for="Number"></label>
                @Model.Number
            </div>
        </div>
    </section>
}

form 을 submit 하면 page 에 secrets 이 표시됩니다.

 

 

 

 

 

Conclusion

당신은 그것을 가지고 있습니다! 이 기술을 사용하여 들어오는 쿼리 문자열을 다른 매개변수 집합으로 변환할 수 있습니다. 이 기술은 강력하며 모든 MVC 작업에 적용하거나 전역적으로 적용할 수 있습니다.

 

항상 그렇듯이이 코드를 시작점으로 사용하고 특정 요구 사항에 맞게 변경하십시오.

작업 샘플을 가지고 놀고 싶다면 내 GitHub 리포지토리에서 코드를 찾을 수 있습니다 .

 

 

원본 사이트 출처

 

https://khalidabuhakmeh.com/decrypt-secrets-into-aspnet-core-mvc-action-arguments-using-action-filters

 

Decrypt Secrets Into ASP.NET Core MVC Action Arguments Using Action Filters

Mapping decrypted secrets from a query string into action method parameters using action filters.

khalidabuhakmeh.com