In this article, we are going to learn about the ASP.NET Core built-in data protection mechanism, IDataProtector, which we can use to encrypt and decrypt our sensitive data.

Encryption and decryption are not the only features we are going to cover. We will also show you how to create time-limited encryption and how to configure our protection mechanism in different ways.

You can find a starting project on our GitHub repo, which you can use to follow along with this article and the finished project if you get stuck.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Encrypting and Decrypting Data with IDataProtector

IDataProtector is an interface that provides data protection services. To be able to use its features, we have to add those data protection services to the specified IServiceCollection and then inject it using dependency injection.

If we take a look at our starting project, we can see the Index action which lists all the employees from the database. There is also a Details link next to each employee which directs us to the Details page. Of course, if we take a look at the URI of a Details page, we can see the value of the employee’s Id property:

Details page - IDataProtector

Now, let’s assume that we don’t want to show that Id value in the URI, or at least not in the Guid form. In other words, we want to protect our Id.

To do that, let’s modify the Startup.cs class, by registering protection services in IServiceCollection with the AddDataProtection method:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<EmployeeContext>(opts =>
        opts.UseSqlServer(Configuration.GetConnectionString("sqlConnection")));

    services.AddScoped<IEmployeeRepository, EmployeeRepository>();

    services.AddDataProtection();

     services.AddControllersWithViews();
}

So, that is all it takes. Or at least for now.

Next, let’s create an IDataProtector object.

Creating a Data Protection Object with IDataProtector and DI

After the registration, we are going to create a data protection object to be able to protect and unprotect our data:

private readonly IDataProtector _protector;

public EmployeesController(IEmployeeRepository repo, IDataProtectionProvider provider)
{
    _repo = repo;
    _protector = provider.CreateProtector("EmployeesApp.EmployeesController");
}

We can see that we create an object of type IDataProtector. But additionally, we need an IDataProtectionProvider object with the CreateProtector method to accomplish this action successfully. Both interfaces are part of the Microsoft.AspNetCore.DataProtection namespace. Therefore, we have to include it as a using directive.

To continue, let’s modify the Employee model class and the Index action to protect Id values for each employee:

public class Employee
{
    public Guid Id { get; set; }

    [Required(ErrorMessage = "Name is required")]
    public string Name { get; set; }

    [Required(ErrorMessage = "Age is required")]
    public int Age { get; set; }

    [Required(ErrorMessage = "Account number is required")]
    public string AccountNumber { get; set; }
    [NotMapped]
    public string EncryptedId { get; set; }
}

public IActionResult Index()
{
    var employees = _repo.GetAll();

    foreach (var emp in employees)
    {
        var stringId = emp.Id.ToString();
        emp.EncryptedId = _protector.Protect(stringId);
    }

    return View(employees);
}

So, we just add a new EncryptedId property in the Employee class and in the Index method, we iterate through each employee and assign protected value to the newly created property.

Next, let’s modify the Index view file by changing the value of the asp-route-id attribute in a Details link:

<a asp-action="Details" asp-route-id="@item.EncryptedId">Details</a> |

Finally, let’s modify the Details action in the EmployeesController class:

public IActionResult Details(string id)
{
    var guid_id = Guid.Parse(_protector.Unprotect(id));
    var employee = _repo.GetEmployee(guid_id);

    return View(employee);
}

That’s it. We can test it now:

Details page protected - IDataProtector

We have the same result as before, but this time with encrypted Id in URI.

Purpose String

Let’s take a look at the following code:

_protector = provider.CreateProtector("EmployeesApp.EmployeesController");

As explained, we need an object of type IDataProtectionProvider and CreateProtector method to create a protector object. But we can see an additional parameter in the CreateProtector method.

This is Purpose String.

Any protector must have its own unique purpose string and it provides isolation between cryptographic consumers. In other words, two IDataProtector instances (created with different purpose strings) can’t read each other’s payloads, only their own.

If one protector object tries to read the payload from a different protector, an exception will be thrown. We can test this with an example.

Let’s slightly modify our controller:

private readonly IEmployeeRepository _repo;
private readonly IDataProtector _protector;
private readonly IDataProtector _protectorTest;

public EmployeesController(IEmployeeRepository repo, IDataProtectionProvider provider)
{
    _repo = repo;
    _protector = provider.CreateProtector("EmployeesApp.EmployeesController");
    _protectorTest = provider.CreateProtector("TestProtector");
}

public IActionResult Index()
{
    var employees = _repo.GetAll();

    foreach (var emp in employees)
    {
        var stringId = emp.Id.ToString();
        emp.EncryptedId = _protector.Protect(stringId);
    }

    var testData = _protectorTest.Protect("Test");
    var unprotectedTest = _protector.Unprotect(testData);

    return View(employees);
}

In this example, we create another IDataProtector instance (_protectorTest) and use that instance to create protected data. Additionally, we try to unprotect it with the other IDataProtector instance (_protector).

Now, when we run our app:

Invalid payload - IDataProtector

As expected, CryptographicException was thrown.

Data Protection for Limited Time

There are situations where we want our protected payload to expire after a certain period of time. For that purpose, we can use the ITimeLimitedDataProtector interface. To create an instance of this mentioned interface, we have to have an instance of IDataProtector and then call the ToTimeLmitedDataProtector extension method.

So, let’s find out how we can create a time-limited payload with an example. We are going to modify the Index action just for the example purpose:

public IActionResult Index()
{
    //Previous code removed for the example clarity

    var timeLimitedProtector = _protector.ToTimeLimitedDataProtector();
    var timeLimitedData = timeLimitedProtector.Protect("Test timed protector", lifetime: TimeSpan.FromSeconds(2));

    //just to test that this action works as long as life-time hasn't expired
    var timedUnprotectedData = timeLimitedProtector.Unprotect(timeLimitedData);

    Thread.Sleep(3000);

    var anotherTimedUnprotectTry = timeLimitedProtector.Unprotect(timeLimitedData);

    return View(employees);
}

As you can see, there is nothing strange about this code.

We are using the _protector object to create a new instance of a time-limited protector with the help of the ToTimeLimitedDataProtector method. Then, we use the timeLimitedProtector object to protect our data and to define the expiration period for our payload. After that, we unprotect our protected data, just to prove that everything works well if the payload hasn’t expired.

Finally, we create a 3 seconds pause and then try to unprotect our protected data.

Once we start our application, we can see the result:

Protected payload time expired

There we go, it is obvious which action failed and why.

Different Ways to Configure Data Protection in ASP.NET Core

When we register data protection services in the StartUp class, the default configuration is applied, but sometimes we want to change that.

That being said, let’s see how we can apply a different configuration for data protection in ASP.NET Core.

If we want to store the data protection configuration in a local custom file, we can use the PersistKeysToFileSystem method:

services.AddDataProtection()
     .PersistKeysToFileSystem(new DirectoryInfo(@"bin\debug\configuration"));

This is the result:

Local configuration file - IDataProtector

We can see the creation date, expiration date, validation algorithm used, and the value of the master key. Notice that we have a warning that our key is unencrypted, which is not that good. But, we can fix that:

services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(@"bin\debug\configuration"))
    .ProtectKeysWithDpapi();

Let’s delete this configuration file and run our app again.

Now if we take a look at our key, we are going to see that it is encrypted:

Encypted key

We don’t have to use just ProtectKeysWithDpapi method to encrypt our key. There are additional methods as well:

Different methods for key encryption

We can calculate, from the expirationDate and activationDate properties, that the expiration period is 90 days. If we want, we can modify that as well:

services.AddDataProtection()
      .PersistKeysToFileSystem(new DirectoryInfo(@"bin\debug\configuration"))
      .ProtectKeysWithDpapi()
      .SetDefaultKeyLifetime(TimeSpan.FromDays(10));

This is the result:

Expiration date changed for the key - IDataProtector

To check additional configuration options, you can visit the Data Protection configuration in ASP.NET Core.

Conclusion

We have covered many different features related to data protection in ASP.NET Core. This can be quite useful when protecting sensitive data in our application.

To sum up, we have learned:

  • How to register data protection services in our application
  • The way to encrypt and decrypt sensitive data in our app
  • To protect the payloads for a limited time
  • Different ways to configure data protection
Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!