This article builds on the ideas presented in my earlier article http://www.heartysoft.com/post/2010/02/25/Encrypted-Hidden-Inputs-in-ASPNET-MVC.aspx
If you haven't read that yet, I'd recommend doing so before proceeding.
Problems with the Previous Approach
The approach outlined in the previous article is pretty secure and easy to use. However, there are a few issues that can be improved upon:
- Security: The approach is using a symmetric encryption process with a fixed private key. Asymmetric encryption would no doubt provide more security, but the performance costs and hassle is probably not worth it. The problem with symmetric encryption is that brute force attacks can potentially break it. As such, using only a fixed encryption key could be a security issue. Given enough time and test pages, the encrypted values could lead to the leaking of the private key. This would result in the whole process being compromised. A couple of people provided feedback on this and were concerned. Still, such brute force attacks could take hundreds of years as the attacker would be required to test at least 2^56 keys. That's not an easy task, but we can use some salting techniques to make it even harder (a lot harder) to break. This would still be within the realms of symmetric encryption but would provide (a LOT of) added security. I'll explain the salting technique later.
- CSRFs: With the previous approach, an attacker could take the encrypted value and submit it as part of another request from his browser. The system would decrypt and use the value as normal. The previous process expected that the developer would use ASP.NET MVC's AntiForgeryToken to prevent CSRFs. In other words, the process did not prevent CSRFs on its own, but expected the developer to handle them by other means. With the new process outlined in this article, this will no longer be necessary.
- Encryption and Hashing method: The previous process used Triple DES for encryption and MD5 for hashing. The new process uses Rijndael encryption and SHA256 hashing. Rijndael is approved by the US government and is natively supported by the .Net framework. If the encryption and decryption are both going to be done on Windows boxes, then Rijndael is definitely the encryption algo to use over TDES. Similarly. SHA256 has a managed implementation and is superior to MD5.
- Code Smell: The previous implementation kind of had some code smells. Stuff was there where it shouldn't have been and it just didn't feel right. I reworked the design and although all smells aren't gone (hey, it's a demo!), a lot of the stench has been removed.
So What's Our Salt?
A salt is basically some random bytes added into our actual data during the encryption process. Having access to the key and the salt enables complete decryption. Having access to one but not the other doesn't. The salt needs to be random enough so that different users have different ones. We still need the salt to facilitate decryption on the server. A common technique is to store part of the salt at the server and part of it in the html sent to the browser. Other techniques generate the salts using some logic and sends some or none of the salt to the client. The bottom line is that the salt needs to be random so that an attacker can't simply take some values from a legitimate page output and then post it himself. The salt still needs to be available on the server when a legitimate user submits the page. ASP.NET MVC actually has such a per user per session value baked right in. It's the AntiForgeryToken. However there are a couple of issues that prevent us from using it directly:
1. The AntiForgeryToken helper uses some internal classes to generate the token data. As it's internal to MVC, we can't directly access the token data. We need to generate the AntiForgeryToken input tag and parse its value.
2. The AntiForgeryToken value is per user per session. If the developer puts an AntiForgeryToken on the page, then that value will be visible to the attacker. That essentially hands the attacker the salt. This is not acceptable. Luckily, the AntiForgeryToken helper can accept any string as its salt (yes, its own salt – not our final salt). We can pass a fixed string as the salt and that would ensure that using AntiForgeryTokens on the page doesn't interfere with or expose our salt.
As I said, the AntiForgeryToken's value is constant for the same user in the same browser session and the same "salt" parameter passed to the helper. But if the user opens another browser window or another user uses the system, then the value would be completely different. With this approach, we don't need to send any of the salt to the browser as all of it can be generated on the server on page submission. Only the encrypted values are ever sent to the client.
The Project
Since quite a bit of things have changed since the previous approach, I'll start afresh. First thing to do is create an MVC 2 project called "HiddenEncryptDemo".
I installed MVC 2 RC 2 before installing VS 2010 RC. If you have some other combination, then slight changes might be necessary, although the core ideas will still work (even with MVC 1.0).
Create a Models folder and add in a class called Computer:
public class Computer { public string Setting { get; set; } }
The reason I'm adding this class is to show that our approach will work not just on simple action parameters, but also on more complex types without a need for a separate model binder.
Next, add this to the Index.aspx view (in the /Views/Home folder):
<% Html.BeginForm("Something", "Home", FormMethod.Post); %> <%= Html.Hidden("computer.Setting", "hello world!") %> <input type="submit" value='submit' /> <% Html.EndForm(); %>
After that, add the following method to the Home controller:
[HttpPost] public ActionResult Something(Computer computer) { ViewData["Message"] = computer.Setting; return View("Index"); }
Nothing fancy, we're just setting the ViewData["Message"] to the computer.Setting value. Notice that in the code we added to Index.aspx, the form submitted to "Something" (the name of our action) and the name of the hidden input was "computer.Setting". The default model binder will find a value for "computer.Something" in the request parameters and upon seeing that it can set the Setting property of the computer parameter, it's going to set computer.Setting to the value it found (in our case, "hello world!"). If you run the project, you should see the index page with a submit button. Clicking the button will result in a post to the server where the Something action will get called. The Something action will then set the ViewData["Message"] and return the Index view. As such, you will see the words "hello world!" displayed on the page:
On the initial Index page (or the page got after clicking the button), if you right click anywhere and select view source, you should see this (among other things):
Notice how the helper added a hidden input control, set its id to "computer_Setting" and name to "computer.Setting". The name is used as the key of the data in the post variables. Also notice how the value is "hello world!". The value is clearly visible to anyone looking at the source. Our goal is to come up with a way to hide that value from clear sight while at the same time ensuring everything works as smoothly and easily as it just did. A bonus would be if no change was required in the controller's code (i.e. no specific attributes needed on the consuming action or controller, no special model binding needed for the parameter etc.) – it would be unobtrusive. These are the exact requirements of the previous approach, but today, we want to add one more – the encryption should use a different salt for each user's session so that brute force approaches of uncovering the encryption key will fail and CSRFs will be prevented by default.
The Settings Provider
In the previous article, we handled settings from within the encryption provider. This seemed kind of messy and I decided to use a separate settings provider. Add a "Helpers" folder to the project and add the interface ISettingsProvider to it:
public interface ISettingsProvider { byte[] EncryptionKey { get; } string EncryptionPrefix { get; } string SaltGeneratorKey { get; } }
Next, add an implementation for the interface. The SettingsProvider class looks like this:
namespace HiddenEncryptDemo.Helpers { using System.Configuration; using System.Security.Cryptography; using System.Text; public class SettingsProvider : ISettingsProvider { private static readonly byte[] _encryptionKey; private static readonly string _encryptionPrefix; private static readonly string _saltGeneratorKey; static SettingsProvider() { //read settings from configuration var useHashingString = ConfigurationManager.AppSettings["UseHashingForEncryption"]; bool useHashing = true; if (string.Compare(useHashingString, "false", true) == 0) { useHashing = false; } _encryptionPrefix = ConfigurationManager.AppSettings["EncryptionPrefix"]; if (string.IsNullOrWhiteSpace(_encryptionPrefix)) { _encryptionPrefix = "encryptedHidden_"; } _saltGeneratorKey = ConfigurationManager.AppSettings["EncryptionSaltGeneratorKey"]; if (string.IsNullOrWhiteSpace(_saltGeneratorKey)) { _saltGeneratorKey = "encryptionSaltKey"; } var key = ConfigurationManager.AppSettings["EncryptionKey"]; if (useHashing) { var hash = new SHA256Managed(); _encryptionKey = hash.ComputeHash(UTF8Encoding.UTF8.GetBytes(key)); hash.Clear(); hash.Dispose(); } else { _encryptionKey = UTF8Encoding.UTF8.GetBytes(key); } } #region ISettingsProvider Members public byte[] EncryptionKey { get { return _encryptionKey; } } public string EncryptionPrefix { get { return _encryptionPrefix; } } public string SaltGeneratorKey { get { return _saltGeneratorKey; } } #endregion } }
Basically, we're just reading in some settings from the web.config file in the static constructor and returning the static values from the instance based getters. Notice that if UseHashingForEncryption setting is set, we first hash the encryption key in web.config using SHA256Managed before using it. The EncryptionPrefix is a prefix used to identify hidden inputs which were previously encrypted. The SaltGeneratorKey is a key used in getting the AntiForgeryToken.
The Encryption Provider
Add an interface called IEncryptString:
public interface IEncryptString : IDisposable { string Encrypt(string value); string Decrypt(string value); }
And an implementation called RijndaelStringEncrypter:
namespace HiddenEncryptDemo.Helpers { using System; using System.Security.Cryptography; using System.Text; public class RijndaelStringEncrypter : IEncryptString { private RijndaelManaged _encryptionProvider; private ICryptoTransform _encrypter; private ICryptoTransform _decrypter; private byte[] _key; private byte[] _iv; public RijndaelStringEncrypter(ISettingsProvider settings, string salt) { _encryptionProvider = new RijndaelManaged(); var saltBytes = UTF8Encoding.UTF8.GetBytes(salt); var derivedbytes = new Rfc2898DeriveBytes(settings.EncryptionKey, saltBytes, 3); _key = derivedbytes.GetBytes(_encryptionProvider.KeySize / 8); _iv = derivedbytes.GetBytes(_encryptionProvider.BlockSize / 8); } #region IEncryptString Members public string Encrypt(string value) { var valueBytes = UTF8Encoding.UTF8.GetBytes(value); if (_encrypter == null) { _encrypter = _encryptionProvider.CreateEncryptor(_key, _iv); } var encryptedBytes = _encrypter.TransformFinalBlock(valueBytes, 0, valueBytes.Length); var encrypted = Convert.ToBase64String(encryptedBytes); return encrypted; } public string Decrypt(string value) { var valueBytes = Convert.FromBase64String(value); if (_decrypter == null) { _decrypter = _encryptionProvider.CreateDecryptor(_key, _iv); } var decryptedBytes = _decrypter.TransformFinalBlock(valueBytes, 0, valueBytes.Length); var decrypted = UTF8Encoding.UTF8.GetString(decryptedBytes); return decrypted; } #endregion #region IDisposable Members public void Dispose() { if (_encrypter != null) { _encrypter.Dispose(); _encrypter = null; } if (_decrypter != null) { _decrypter.Dispose(); _decrypter = null; } if (_encryptionProvider != null) { _encryptionProvider.Clear(); _encryptionProvider.Dispose(); _encryptionProvider = null; } } #endregion } }
This is a sginificant change from our previous encryption method. Previously, we created a single pair of ICryptoTransforms for encryption and decryption based on the encryption key. Since we need to provide different salts for encryption for different users and different sessions, that approach will no longer work. In the new approach, each instance of the RijndaelStringEncrypter has its own salt value. As a consequence, each instance needs its own pair of ICryptoTransforms. Looking at the constructor code:
public RijndaelStringEncrypter(ISettingsProvider settings, string salt) { _encryptionProvider = new RijndaelManaged(); var saltBytes = UTF8Encoding.UTF8.GetBytes(salt); var derivedbytes = new Rfc2898DeriveBytes(settings.EncryptionKey, saltBytes, 3); _key = derivedbytes.GetBytes(_encryptionProvider.KeySize / 8); _iv = derivedbytes.GetBytes(_encryptionProvider.BlockSize / 8); }
we see that we're storing the key and iv bytes as instance variables. Please note that simply getting the bytes for the salt string using UTF8Encoding.UTF8.GetBytes(salt) is not good enough. The values generated by AntiForgeryToken - while different for each user and session – can be very similar. As such, if you're encrypting a small word (for example "hello") and use the encoding derived bytes, the first few bytes may be so similar that the encrypted output of "hello" might appear the same in the html output for different users / sessions. As such, we're using Rfc2898DerivedBytes to ensure that only the exact salt and encryption key produce the same iv bytes. Another thing to notice is that we're using the encryption key itself as the password passed to the Rfc2898DerivedBytes constructor. This essentially means that the encryption key and the passed in salt string is used to derive the actual iv (salt) used for encryption.
The Html Helper
Add a file called InputExtensions.cs and add the following code to it:
namespace HiddenEncryptDemo.Helpers { using System.Web.Mvc; using System.Web.Mvc.Html; using System.Text.RegularExpressions; public static class InputExtensions { public static HeartysoftHtmlHelper Heartysoft(this HtmlHelper helper) { return new HeartysoftHtmlHelper(helper); } } public partial class HeartysoftHtmlHelper { private readonly HtmlHelper _helper; private readonly ISettingsProvider _settings; private static Regex _valueExtractorRegex = new Regex(".*value=\"(.+)\"/*", RegexOptions.Compiled); public HeartysoftHtmlHelper(HtmlHelper helper) : this(helper, new SettingsProvider()) { } public HeartysoftHtmlHelper(HtmlHelper helper, ISettingsProvider settings) { _helper = helper; _settings = settings; } public string GetAntiForgeryToken(string salt) { var input = _helper.AntiForgeryToken(salt).ToString(); var match = _valueExtractorRegex.Match(input); return match.Groups[1].Value; } public MvcHtmlString EncryptedHidden(string name, object value) { if (value == null) { value = string.Empty; } var strValue = value.ToString(); string salt = GetAntiForgeryToken(_settings.SaltGeneratorKey); var encrypter = new RijndaelStringEncrypter(_settings, salt); var encryptedValue = encrypter.Encrypt(strValue); encrypter.Dispose(); var encodedValue = _helper.Encode(encryptedValue); var newName = string.Concat(_settings.EncryptionPrefix, name); return _helper.Hidden(newName, encodedValue); } } }
Let's dissect the helper a bit. There are basically two important methods. The first is GetAntiForgeryToken:
public string GetAntiForgeryToken(string salt) { var input = _helper.AntiForgeryToken(salt).ToString(); var match = _valueExtractorRegex.Match(input); return match.Groups[1].Value; }
This get's the anti forgery token string given a specific salt. As I mentioned before, the AntiForgeryToken helper gets its value from a class internal to ASP.NET MVC and thus we can't get the value directly. To work around this, I'm using the helper to het the html of the anti forgery token hidden input and using a Regex to parse its value. The other interesting method is the EncryptedHidden function:
public MvcHtmlString EncryptedHidden(string name, object value) { if (value == null) { value = string.Empty; } var strValue = value.ToString(); string salt = GetAntiForgeryToken(_settings.SaltGeneratorKey); var encrypter = new RijndaelStringEncrypter(_settings, salt); var encryptedValue = encrypter.Encrypt(strValue); encrypter.Dispose(); var encodedValue = _helper.Encode(encryptedValue); var newName = string.Concat(_settings.EncryptionPrefix, name); return _helper.Hidden(newName, encodedValue); }
This method uses the previous one to get the salt, creates the encrypter and encrypts the input. It then uses the regular Hidden() helper to return the <input> tag with the value set to the encrypted data.
Configuration
Go to web.config and ensure that under <configuration>, the following entries are present:
<appSettings> <add key="EncryptionKey" value="asdjahsdkhaksj dkashdkhak sdhkahsdkha kjsdhkasd"/> </appSettings>
The View
At this point, go to Index.aspx and change the html of the form to:
<% Html.BeginForm("Something", "Home", FormMethod.Post); %> <%= Html.Heartysoft().EncryptedHidden("computer.Setting", "hello world!") %> <%--<%= Html.Hidden("computer.Setting", "hello world!") %>--%> <input type="submit" value='submit' /> <% Html.EndForm(); %>
I've just commented out the old helper and used our new helper. If you run the page and view the source, you should see this:
<input id="encryptedHidden_computer_Setting" name="encryptedHidden_computer.Setting" type="hidden" value="GlafeVH/jrfim7gXfqhSHg==" />
If you refresh the page and view source, you should see this:
<input id="encryptedHidden_computer_Setting" name="encryptedHidden_computer.Setting" type="hidden" value="GlafeVH/jrfim7gXfqhSHg==" />
i.e. the value of the hidden input has not changed. If you open the same page in a different browser or close and open the browser or open the same page in a new window (not tab as sessions are shared across tabs), then you should see this:
<input id="encryptedHidden_computer_Setting" name="encryptedHidden_computer.Setting" type="hidden" value="IM94hlaZP4HmqOhxw7Ewyg==" />
Notice how the value is different from the previous value. If you kept the previous browser open, then refreshing the page and viewing source, you should see this:
<input id="encryptedHidden_computer_Setting" name="encryptedHidden_computer.Setting" type="hidden" value="GlafeVH/jrfim7gXfqhSHg==" />
In other words, each session is getting a unique value that's different from the value got in a different session. As such, we can say that the encryption is using a per session salt which makes brute force attacks that much harder.
Please note that since the salt is per session, the exact values of the hidden inputs will vary greatly from the ones shown here. What's important is that in the same session, the value is the same, but the value differs from session to session.
The Controller Factory
We now need to actually decrypt values when the page is posted back. We do this via a custom ControllerFactory. Our controller factory will look for any encrypted input parameters, decrypt them and add them to RouteData using their original names so that they're available to the model binder. Add a class called DecryptingControllerFactory:
namespace HiddenEncryptDemo.Helpers { using System.Linq; using System.Web.Mvc; using System.IO; public class DecryptingControllerFactory : DefaultControllerFactory { private ISettingsProvider _settings = new SettingsProvider(); public override IController CreateController(System.Web.Routing.RequestContext requestContext, string controllerName) { var parameters = requestContext.HttpContext.Request.Params; var encryptedParamKeys = parameters.AllKeys.Where(x => x.StartsWith(_settings.EncryptionPrefix)).ToList(); IEncryptString decrypter = null; foreach (var key in encryptedParamKeys) { if (decrypter == null) { decrypter = GetDecrypter(requestContext); } var oldKey = key.Replace(_settings.EncryptionPrefix, string.Empty); var oldValue = decrypter.Decrypt(parameters[key]); requestContext.RouteData.Values[oldKey] = oldValue; } if (decrypter != null) { decrypter.Dispose(); } return base.CreateController(requestContext, controllerName); } private IEncryptString GetDecrypter(System.Web.Routing.RequestContext requestContext) { var salt = GetCurrentSalt(requestContext); var decrypter = new RijndaelStringEncrypter(_settings, salt); return decrypter; } private string GetCurrentSalt(System.Web.Routing.RequestContext requestContext) { var controllerContext = new ControllerContext(); controllerContext.RequestContext = requestContext; var viewContext = new ViewContext(controllerContext, new WebFormView("dummy"), new ViewDataDictionary(), new TempDataDictionary(), TextWriter.Null); var helper = new HtmlHelper(viewContext, new ViewPage()); var afToken = helper.Heartysoft().GetAntiForgeryToken(_settings.SaltGeneratorKey); return afToken; } } }
The controller factory is quite simple. It first checks the request parameters to see if there are any that have a key with a prefix equal to the EncryptionPrefix provided by the settings provider. If there are, we cycle through each and add an entry into RouteData with the key set to the parameter's key minus the prefix and the value set to the decrypted value. We create the decrypter only if needed so that if there aren't any encrypted hidden inputs, we don't waste any resources. Furthermore, if we do need a decrypter, we create an instance and reuse it for all the encrypted parameters for the request. Since the salt is per session, reusing the decrypter for all the encrypted parameters in the request is safe and reduces resource usage. To create the decrypter, we need the salt value that has to be equal to the salt used for encrypting. This is where the very interesting GetCurrentSalt method comes in:
private string GetCurrentSalt(System.Web.Routing.RequestContext requestContext) { var controllerContext = new ControllerContext(); controllerContext.RequestContext = requestContext; var viewContext = new ViewContext(controllerContext, new WebFormView("dummy"), new ViewDataDictionary(), new TempDataDictionary(), TextWriter.Null); var helper = new HtmlHelper(viewContext, new ViewPage()); var afToken = helper.Heartysoft().GetAntiForgeryToken(_settings.SaltGeneratorKey); return afToken; }
What this method is doing is it's faking a view context and controller context, setting the controller context's request context to that of the current request and using the fake view context and controller context to create a new instance of the HtmlHelper class. Then, it calls our previously coded GetAntiForgeryToken helper to get the value of the salt string. This is a very useful technique of creating an instance of the HtmlHelper class and can be used to create helpers outside of view code. Over at the ASP.NET forums, Brad Wilson expressed concerns that passing in TextWriter.Null may cause problems, but I believe this is not so. TextWriter.Null provides a TextWriter instance that can be written to but not read from. I don't think any Html helpers should be reading from the TextWriter – they should only be writing to it. And even if there are such helpers, AntiForgeryToken() and our GetAntiForgeryToken() do not read from the TextWriter, so this is perfectly safe for our purposes.
The end result of all of this is that if there are encrypted hidden inputs, then they are decrypted and the entries with original names (i.e. wothout the prefix) are added to RouteData. This ensures the original entries are available for ModelBinding purposes. The last change you need to do is open up the global.asax.cs file and ensure the Application_Start looks like this:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.SetControllerFactory(typeof(DecryptingControllerFactory)); }
Running the Page
We'll need to stop the dev server (if you're using IIS, simply restart it). We need to do this because we made changes to the global.asax.cs page. Right click the dev server's icon in your traybar and click stop:
You can now simply hit ctrl + F5 in VS to run the page. Click the button and you should see this:
Run the page in another browser and click the button. You should see the same results. Viewing the source, you'll see that while pages processed in the same session always has the same encrypted value, a different session in a different browser produces a different encrypted value. Still, when either value is posted to the server, the decrypting controller factory correctly decrypts the values and we get our original value in RouteData. Neither the encryption key nor the encryption iv is ever passed to the client and thus the process is much more secure than our previous approach.
And that's all there is to enable an unobtrusive, easy to use, secure, reusable and salted hidden inputs in ASP.NET MVC. On top of that, it should perform better since we're using managed Rijndael encryption which does not call out to non-managed resources like TDES does.
Possible Questions
I would recommend you first read the "possible questions" section of the previous article. Other than those:
3. Is the data really secure?
The previous approach was pretty secure. As I mentioned, an attacker would need at least 2^56 attempts. This new approach makes it much much more harder for the attacker to succeed. So, it's even more secure. The previous approach didn't tackle CSRFs but relied on the developer using AntiForgeryToken on the pages and the associated ValidateAntiForgeryToken attribute on the controller action being posted to. This approach handles CSRFs itself.
8. How is the performance?
The performance is pretty good. The controller factory only creates a decrypter when needed and reuses it for decrypting all encrypted parameters. We do, however, need to create an encrypter for each call to Html.Heartyfoft().EncryptedHidden(). A way to negate this would be to use some sort of per request IOC container so that one instance is used for processing a whole request. In my opinion, helpers should not have state, but if you want, implementing that part should be easy.
Source Code
---------------------------------------------------------
Questions and comments relating to this article are welcome. Comments completely unrelated to the article and posted with the sole intention of putting your link here are not.
If you spam, your comment will not be approved, will be deleted and your IP blocked. I maintain my site almost daily and such comments – even if they pass the spam filter – will get removed as soon as possible. If this gets too tedious, I may disable comments entirely. Please don't ruin it for everybody else.
---------------------------------------------------------