I have been working on a solution to allow us to encrypt and decrypt data in a URL for an asp.net MVC project. My goal is to allow us to encrypt route data and/or querystring data without the developer having to do anything special in the controller or action to decrypt and bind the data to the model(s). I've achieved this by writing my own IValueProvider. My value provider searches the incoming RouteData and query string keys for a prefix that indicates they are an encrypted value. If they begin with the prefix, it decrypts them and will provide a value for them. Here is that much of it in case you're interested TL;DR (right now for testing I'm just using base 64 encoding, not encryption):
public class EncryptedDataValueProvider : IValueProvider
{
string _encryptionPrefix = "_enc!";
Dictionary<string, string> _values;
public EncryptedDataValueProvider(ControllerContext controllerContext)
{
_values = new Dictionary<string, string>();
foreach (KeyValuePair<string, object> entry in controllerContext.RouteData.Values)
{
object val = entry.Value;
if (val != null && val is string && ((string)val).StartsWith(_encryptionPrefix))
{
//TODO: Decrypt substring
string encryptedVal = ((string)val).Substring(_encryptionPrefix.Length);
string decryptedVal = Encoding.UTF8.GetString(Convert.FromBase64String(encryptedVal));
_values.Add(entry.Key, decryptedVal);
}
}
//Note: The .NET source code seems to indicate accessing QS keys might trigger request validation before we want it to.
//May have to use controllerContext.HttpContext.Request.Unvalidated to get at the keys instead. See
//QueryStringValueProvider.cs, NameValueCollectionValueProvider.cs, and UnvalidatedRequestValuesWrapper.cs ("M.W.I" is Microsoft.Web.Infrastructure where request validation is built)
foreach (string key in controllerContext.HttpContext.Request.QueryString)
{
if (key.StartsWith(_encryptionPrefix))
{
string val = controllerContext.HttpContext.Request.QueryString[key];
if (val != null)
{
//TODO: Decrypt val
string decryptedVal = Encoding.UTF8.GetString(Convert.FromBase64String(val));
NameValueCollection decryptedQs = HttpUtility.ParseQueryString(decryptedVal);
foreach (string decryptedKey in decryptedQs)
if (!_values.ContainsKey(decryptedKey))
_values.Add(decryptedKey, decryptedQs[decryptedKey]);
}
}
}
}
public bool ContainsPrefix(string prefix)
{
return _values.ContainsKey(prefix);
}
public ValueProviderResult GetValue(string key)
{
if (_values.ContainsKey(key))
return new ValueProviderResult(_values[key], _values[key], System.Globalization.CultureInfo.InvariantCulture);
else
return null;
}
}
This decrypts incoming encrypted (encoded) values fine. What I need to do now is provide a way of specifying that a given action parameter (a.k.a. model) must be encrypted. That is, I don't want to accept unencrypted URL values. One caveat is I want to leave this as flexible as possible. Ideally, I would have an attribute that I could apply to individual action method parameters, the whole action method, or an entire controller so that we still have the option of accepting unencrypted parameters if needed. I'm picturing something like this:
[RequireEncryption]
public class MyController : Controller
{
public ActionResult Action1(string secret)
{
}
[RequireEncryption(false)]
public ActionResult Action2(string notSecret)
{
}
public ActionResult Action3(string secret, [RequireEncryption(false)]string notSecret)
{
}
}
Well, I've nearly achieved this too (except the class/method-level attribute always overrides the parameter-level attribute), but it just feels like an ugly hack. To do it, I had to create a ModelBinder, a CustomModelBinderAttribute, and an IAuthorizationFilter FilterAttribute. I use these to replace the built-in ValueProviderCollection with just my single EncryptedDataValueProvider so that if it doesn't have a decrypted value for the model, the model remains null. Both attributes use this same approach but I had to use the CustomModelBinderAttribute to get a parameter-level attribute and the authorization filter to get action & controller-level attributes.
What I don't like about this is 1) that I have to have two different attributes and it's not quite achieving my vision above, but 2) that the ModelBinder and authorization filter are modifying the value providers collection. That seems like they are reaching beyond their intended scope. Is there a more proper way to extend the MVC framework to achieve this?
public class RequireEncryptionOnAllAttribute : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
filterContext.Controller.ValueProvider = new EncryptedDataValueProvider(filterContext.Controller.ControllerContext);
}
}
public class RequireEncryption : CustomModelBinderAttribute
{
public override IModelBinder GetBinder()
{
return new EncryptionRequiredBinder();
}
}
/// <summary>
/// Binds the model using only the EncryptedDataValueProvider. Unencrypted values will not be bound.
/// </summary>
public class EncryptionRequiredBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
bindingContext.ValueProvider = new EncryptedDataValueProvider(controllerContext);
return base.BindModel(controllerContext, bindingContext);
}
}