This post is intended as an addendum on Mgr. Jiří Činčura's excellent post Using Data Protection in Entity Framework Core with Value Converters (archive 1, archive 2). In it, he explains how you can use an Entity Framework Core ValueConverter to encrypt data before it being stored in the database and decrypt it when reading from the database.
Though his post is excellent I do have one minor nitpick with it: you can't use (a) purpose string(s) with it; this means all encryption happens with the same 'purpose'. To explain what this 'purpose' is about I'll quote the docs:
Components which consume IDataProtectionProvider must pass a unique purposes parameter to the CreateProtector method. The purposes parameter is inherent to the security of the data protection system, as it provides isolation between cryptographic consumers, even if the root cryptographic keys are the same.
When a consumer specifies a purpose, the purpose string is used along with the root cryptographic keys to derive cryptographic subkeys unique to that consumer. This isolates the consumer from all other cryptographic consumers in the application: no other component can read its payloads, and it cannot read any other component's payloads. This isolation also renders infeasible entire categories of attack against the component.
So it's a good idea to have a purpose string and even better to be able to specify it. Luckily, the solution is rather simple (and elegant) and only a minimal few changes need to be made.
First, the Protected attribute; we'll extend it to have a Purposes property and a constructor overload so you'll be able to specify one (or more) purpose(s):
[AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = false)] | |
sealed class ProtectedAttribute : Attribute | |
{ | |
public IEnumerable<string> Purposes { get; init; } | |
public ProtectedAttribute() | |
: this(Array.Empty<string>()) { } | |
public ProtectedAttribute(params string[] purposes) | |
=> Purposes = purposes; | |
} |
Next, the OnModelCreated method is changed a little:
class MyContext : DbContext | |
{ | |
readonly IDataProtectionProvider _dataProtectionProvider; | |
public MyContext(IDataProtectionProvider dataProtectionProvider) | |
{ | |
_dataProtectionProvider = dataProtectionProvider; | |
} | |
protected override void OnModelCreating(ModelBuilder modelBuilder) | |
{ | |
base.OnModelCreating(modelBuilder); | |
// ... | |
// Iterate all entities with [Protected] attributes and set a ProtectedValueConverter for those properties. | |
foreach (var (prop, att) in modelBuilder.Model.GetEntityTypes() | |
.SelectMany(e => e.GetProperties().Select(p => (prop: p, att: p.PropertyInfo?.GetCustomAttribute<ProtectedAttribute>()))) | |
.Where(a => a.att != null) | |
) | |
{ | |
var purposes = att!.Purposes; | |
// https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/consumer-apis/purpose-strings?view=aspnetcore-6.0 | |
// "Using the namespace and type name of the component consuming the data protection APIs is a good | |
// rule of thumb, as in practice this information will never conflict." | |
prop.SetValueConverter(new ProtectedValueConverter( | |
purposes.Any() // Have purposes from the attribute? | |
? _dataprotectionprovider.CreateProtector(purposes) // ...use them | |
: _dataprotectionprovider.CreateProtector(prop.DeclaringEntityType.Name) // ...use typename | |
)); | |
} | |
} | |
} |
And that's it!
So what this does is: whenever you simply use a [Protected] attribute the typename of the entity is used as 'purpose'. As the comment points out, this is considered a 'good rule of thumb' as stated in the docs:
Using the namespace and type name of the component consuming the data protection APIs is a good rule of thumb, as in practice this information will never conflict.
However, because the ProtectedAttribute class now has an overload that takes any number of strings we can simply specify one or more purposes directly at the entity level.
class FooBar | |
{ | |
public int Id { get; set; } | |
[Protected("MyPurpose","V1")] | |
public string Secret { get; set; } | |
} |
For completeness' sake, here's the ProtectedValueConverter, should the referenced blog (and/or archived versions) ever go offline, which I have modified slightly too:
public class ProtectedValueConverter : ValueConverter<string?, string?> | |
{ | |
private class InternalWrapper | |
{ | |
private readonly IDataProtector _dataProtector; | |
public InternalWrapper(IDataProtectionProvider dataProtectionProvider) | |
=> _dataProtector = dataProtectionProvider.CreateProtector(nameof(ProtectedValueConverter)); | |
public Expression<Func<string?, string?>> To => x => x != null ? _dataProtector.Protect(x) : null; | |
public Expression<Func<string?, string?>> From => x => x != null ? _dataProtector.Unprotect(x) : null; | |
} | |
public ProtectedValueConverter(IDataProtectionProvider provider, ConverterMappingHints? mappingHints = default) | |
: this(new InternalWrapper(provider), mappingHints) { } | |
private ProtectedValueConverter(InternalWrapper wrapper, ConverterMappingHints? mappingHints) | |
: base(wrapper.To, wrapper.From, mappingHints) { } | |
} |
Oh, another little change I made is that this is now nullable aware 😊
And there you have it, a way to specify purpose(s) on "protected" attributes. With credit to Mgr. Jiří Činčura for providing the foundation for this work.
No comments:
Post a Comment