As many of us know, for security and data protection reasons (GDPR), we must protect sensitive information in our database, such as phone numbers or email addresses.
Let’s not forget that in some cases we’ll keep records in logs. Personally, I do not recommend this, but if we have to store a log with email, phone number, or address, we can also use this method.
To achieve this, we’ll use a Microsoft-provided interface that helps us to do this very easily.
We’ll also see a practical example using C#.
Table of Contents
1 - Why protect sensitive information?
We have to understand that the main reason is to comply with the European data protection law (GDPR), which we must abide by if any of our users reside in Europe. So if your website is in English or Spanish, you need a system to protect sensitive information.
This information is primarily phone number, email, physical address, or any kind of ID. In summary, we must protect/encrypt any information that can identify our users.
If we don’t do this and get caught, the consequences will depend on the severity and the amount of user information leaked, but fines can reach millions of euros.
Not to mention the “personal” impact or what people will think about our company or product if it’s known we don’t follow best practices, or worse, that their info has been leaked—it’s very bad for the brand’s image.
2 - How to encrypt data in C#?
Obviously when we program, we must keep in mind from the start that we’ll need to perform encryption actions on our data. Therefore, even though we may not need it in tests, we do for production.
Microsoft has already thought about this and makes our life easier by providing the IDataProtectionProvider
interface, which lets us create “protectors” that can both encrypt and decrypt data.
3 - Protect sensitive information with IDataProtectionProvider
For this post, we’ll use code that we’ve been working with in the rest of the web C# module, which you can find on Github.
To use IDataProtectionProvider
in our code, we need to add it to services inside IServiceCollection
by calling the AddDataProtection method.
public void ConfigureServices(IServiceCollection services){ ... services.AddDataProtection(); ....}
3.1 - Encrypt data in C# with IDataProtectionProvider
Once we have the service registered, we inject it into the services that we are going to use.
public class PutPersonalProfile{ private readonly IPutPersonalProfileDependencies _dependencies; private readonly IDataProtector _protector; public PutPersonalProfile(IPutPersonalProfileDependencies dependencies, IDataProtectionProvider protectorProvider) { _dependencies = dependencies; _protector = protectorProvider.CreateProtector("PersonalProfile.Protector"); } public async Task<Result<PersonalProfileDto>> Create(PersonalProfileDto personalProfile) { /*Código*/ }}
As we can see, we’re using an IDataProtector
, but we inject IDataProtectionProvider
into our service, so we need to create our IDataProtector
from the injected interface.
Both interfaces are part of Microsoft.AspNetCore.DataProtection
, which means we must include the Assembly with the using
directive (at the start of the file).
And to encrypt the information, we just need to call the Protect
method on our IDataProtector
.
Where to do this depends a bit on your app logic. In my case, I do it in the mapper from our DTO to the entity, but some people prefer to do it right before reading from the database. Personally, I never use email (or phone) as a primary key so the mapper is where it makes the most sense.
public static PostPersonalProfileWrapper MapToWraperEntities(this PersonalProfileDto profileDto, IDataProtector protector){.... string encryptedEmail = protector.Protect(profileDto.Email); string encryptedPhone = protector.Protect(profileDto.Phone);....}
With just this action we’ll have the information encrypted in the database.
3.2 - Decrypt data in C# with IDataProtectionProvider
To decrypt, the process is the same: inject the IDataProtectionProvider
interface into your service to create an IDataProtector
.
You must pass the same string to CreateProtector that you used for your post service. This is because to decrypt you obviously need the same “key” as when encrypting.
In this case, we call the .Unprotect(memberToDecrypt)
method.
public class PersonalProfile{ private readonly IGetPersonalProfileDependencies _dependencies; private readonly IDataProtector _protector; public PersonalProfile(IGetPersonalProfileDependencies dependencies, IDataProtectionProvider protectorProvider) { _dependencies = dependencies; _protector = protectorProvider.CreateProtector("PersonalProfile.Protector"); } public async Task<Result<PersonalProfileDto>> GetPersonalProfileDto(string name) { ... } /*Más código*/ private Task<PersonalProfileDto> Map((PersonalProfileEntity personalProfile, List<SkillEntity> skills, List<InterestEntity> interests) values, UserIdEntity userId) { PersonalProfileDto profile = new PersonalProfileDto() { Description = values.personalProfile.Description, Email = _protector.Unprotect(values.personalProfile.Email),//Here FirstName = values.personalProfile.FirstName, LastName = values.personalProfile.LastName, GitHub = values.personalProfile.GitHub, UserId = userId.UserId, UserName = userId.UserName, Phone = _protector.Unprotect(values.personalProfile.Phone),//Here Website = values.personalProfile.Website, Id = values.personalProfile.Id, Interests = values.interests.Select(a => new InterestDto() { Id = a.Id, Interest = a.Description }).ToList(), Skills = values.skills.Select(a => new SkillDto() { Id = a.Id, Name = a.Name, Punctuation = a.Punctuation }).ToList() }; return Task.FromResult(profile); }}
3.3 - Purpose in CreateProtector
As we can see, when we create the protector with CreateProtector
, we pass a phrase or string as a parameter.
This string
is what the cipher uses to encrypt and decrypt, and obviously, to perform actions on the same item, it needs to be the same.
Moreover, we can have either a public key or a unique string for the whole application, or as in my case, everything related to personal profile uses one, but when I switch to another service, I’ll use another.
The purpose of the key isn’t to be private to the developer (like a production database password).
If we use the single-key-for-the-whole-app option, we can add our IDataProtectionProvider to the dependencies of each service and just return IDataProtector in the dependencies.
public interface IGetPersonalProfileDependencies{ IDataProtector Protector { get; } ...}public class GetPersonalProfileDependencies : IGetPersonalProfileDependencies{ public IDataProtector Protector { get; } public GetPersonalProfileDependencies(IDataProtectionProvider protectorProvider) { Protector = protectorProvider.CreateProtector("test") } ...}##and then just do the call_dependencies.Protector.Protect(values.personalProfile.Phone);_dependencies.Protector.Unprotect(values.personalProfile.Phone);
But this is just a matter of preference—both options are totally valid.
3.4 - Data protector when deploying your application
If you’ve followed this post, you’ll notice that once you publish the app, it fails to protect/unprotect information. This is because we’re using in-memory keys; what we need to do is store them in the file system (or you can do it in the cloud too).
To do this, just call the PersistKeysToFileSystem method in the initial configuration:
public void ConfigureServices(IServiceCollection services){ ... services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(@"PATH to contain the keys")); ....}
Remember that it’s a directory, and your app will need permission to access that directory.
4 - Implementing IDataProtectionProvider in tests
We know we have to implement tests for all our services, which means we need to implement the IDataProtectionProvider
interface inside the tests. But as we know, we can’t implement an interface directly. So how do we “implement” IDataProtectionProvider
?
4.1 - Implementing IDataProtectionProvider in unit tests
To implement IDataProtectionProvider in unit tests we could use a mock, but that's actually not a good idea.
In this case, we need to use the EphemeralDataProtectionProvider
class provided by Microsoft specifically for this scenario, since it implements IDataProtectionProvider
.
IDataProtectionProvider protectionProvider = new EphemeralDataProtectionProvider();//IDataProtector protector = protectionProvider.CreateProtector("Test_PutPersonalProfile");Subject = new PutPersonalProfile(_dependencies.Object, protectionProvider);
4.2 - Implementing IDataProtectionProvider in integration tests
To implement IDataProtectionProvider
in integration tests, we do something similar, but this time just specify it in the dependency container.
private IServiceCollection BuildDependencies(){ IServiceCollection services = new ServiceCollection(); services .AddScoped<IDataProtectionProvider, EphemeralDataProtectionProvider>(); return services;}
Conclusion
In this post, we have seen why you should implement encryption for sensitive or private data you store in databases or config files.
If you want to have a semi-professional application, you definitely should implement these kinds of practices, let alone at a company level.
If there is any problem you can add a comment bellow or contact me in the website's contact form