Skip to content

Commit 65efbe5

Browse files
author
Roman Golovanov
committed
add [Encrypted] attribute support, reflection optimizations
1 parent cd65a50 commit 65efbe5

14 files changed

Lines changed: 444 additions & 120 deletions

README.md

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ services.AddDbContext<AppDbContext>((sp, options) =>
5353
});
5454
```
5555

56-
If you use the database key chain, add its table to your model:
56+
If you use the database key chain, add its table to your model. Mark encrypted properties with the fluent API:
5757

5858
```csharp
5959
protected override void OnModelCreating(ModelBuilder modelBuilder)
@@ -69,6 +69,23 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
6969
}
7070
```
7171

72+
Or use the `[Encrypted]` data annotation on the entity:
73+
74+
```csharp
75+
using EfCore.EncryptedProperties;
76+
77+
public sealed class Customer
78+
{
79+
public Guid Id { get; set; }
80+
81+
[Encrypted("email")]
82+
public string Email { get; set; } = string.Empty;
83+
84+
[Encrypted(KeyPurpose = "notes")]
85+
public EncryptedValue<string> SecretNotes { get; set; } = default!;
86+
}
87+
```
88+
7289
Then use the entity normally:
7390

7491
```csharp

samples/EfCore.EncryptedProperties.Samples/Customer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public sealed class Customer
99
public string Email { get; set; } = string.Empty;
1010

1111
// DecryptOnRead works for non-string values too
12+
[Encrypted]
1213
public DateTime DateOfBirth { get; set; }
1314

1415
// Lazy: explicit decrypt via GetDecryptedValueAsync

samples/EfCore.EncryptedProperties.Samples/SampleDbContext.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
1818
entity.Property(e => e.Email)
1919
.IsEncrypted();
2020

21-
entity.Property(e => e.DateOfBirth)
22-
.IsEncrypted();
23-
2421
entity.Property(e => e.SecretNotes)
2522
.IsEncrypted(options =>
2623
{
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
namespace EfCore.EncryptedProperties;
2+
3+
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
4+
public sealed class EncryptedAttribute : Attribute
5+
{
6+
public EncryptedAttribute()
7+
{
8+
}
9+
10+
public EncryptedAttribute(string keyPurpose)
11+
{
12+
KeyPurpose = keyPurpose;
13+
}
14+
15+
public string KeyPurpose { get; set; } = "default";
16+
}

src/EfCore.EncryptedProperties/Infrastructure/EncryptedPropertiesConventionSetPlugin.cs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1+
using System.Reflection;
12
using EfCore.EncryptedProperties.Configuration;
23
using Microsoft.EntityFrameworkCore;
4+
using Microsoft.EntityFrameworkCore.Metadata;
35
using Microsoft.EntityFrameworkCore.Metadata.Builders;
46
using Microsoft.EntityFrameworkCore.Metadata.Conventions;
57
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
6-
using Microsoft.EntityFrameworkCore.Metadata;
78

89
namespace EfCore.EncryptedProperties.Infrastructure;
910

1011
internal sealed class EncryptedPropertiesConventionSetPlugin : IConventionSetPlugin
1112
{
1213
public ConventionSet ModifyConventions(ConventionSet conventionSet)
1314
{
14-
conventionSet.ModelFinalizingConventions.Add(new EncryptedPropertiesStorageConvention());
15+
conventionSet.ModelFinalizingConventions.Insert(0, new EncryptedPropertiesStorageConvention());
1516
return conventionSet;
1617
}
1718
}
@@ -22,6 +23,9 @@ public void ProcessModelFinalizing(
2223
IConventionModelBuilder modelBuilder,
2324
IConventionContext<IConventionModelBuilder> context)
2425
{
26+
ApplyEncryptedAttributes(modelBuilder);
27+
RemoveUnusedEncryptedValueEntityTypes(modelBuilder);
28+
2529
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes().ToList())
2630
{
2731
foreach (var property in entityType.GetProperties().ToList())
@@ -38,6 +42,89 @@ public void ProcessModelFinalizing(
3842
}
3943
}
4044

45+
private static void ApplyEncryptedAttributes(IConventionModelBuilder modelBuilder)
46+
{
47+
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes().ToList())
48+
{
49+
if (entityType.ClrType is null || IsEncryptedValueType(entityType.ClrType))
50+
continue;
51+
52+
foreach (var propertyInfo in entityType.ClrType.GetProperties(
53+
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
54+
{
55+
var encryptedAttribute = GetEncryptedAttribute(propertyInfo);
56+
if (encryptedAttribute is null)
57+
continue;
58+
59+
var property = entityType.FindProperty(propertyInfo.Name)
60+
?? ConvertAnnotatedNavigationToProperty(entityType, propertyInfo);
61+
62+
ApplyEncryptedAttribute(property, encryptedAttribute);
63+
}
64+
}
65+
}
66+
67+
private static IConventionProperty ConvertAnnotatedNavigationToProperty(
68+
IConventionEntityType entityType,
69+
PropertyInfo propertyInfo)
70+
{
71+
var navigation = entityType.FindNavigation(propertyInfo.Name);
72+
if (navigation is not null)
73+
{
74+
navigation.ForeignKey.DeclaringEntityType.Builder.HasNoRelationship(
75+
navigation.ForeignKey,
76+
fromDataAnnotation: true);
77+
}
78+
79+
var propertyBuilder = entityType.Builder.Property(
80+
propertyInfo.PropertyType,
81+
propertyInfo.Name,
82+
setTypeConfigurationSource: false,
83+
fromDataAnnotation: true);
84+
85+
return propertyBuilder?.Metadata
86+
?? throw new InvalidOperationException(
87+
$"Unable to configure encrypted property '{entityType.ClrType.FullName}.{propertyInfo.Name}' from data annotations.");
88+
}
89+
90+
private static void RemoveUnusedEncryptedValueEntityTypes(IConventionModelBuilder modelBuilder)
91+
{
92+
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes().ToList())
93+
{
94+
if (!IsEncryptedValueType(entityType.ClrType))
95+
continue;
96+
97+
if (entityType.GetForeignKeys().Any() || entityType.GetReferencingForeignKeys().Any())
98+
continue;
99+
100+
modelBuilder.HasNoEntityType(entityType, fromDataAnnotation: true);
101+
}
102+
}
103+
104+
private static void ApplyEncryptedAttribute(IConventionProperty property, EncryptedAttribute encryptedAttribute)
105+
{
106+
if (property.FindAnnotation(EncryptedPropertyAnnotations.IsEncrypted) is not null)
107+
return;
108+
109+
var keyPurpose = string.IsNullOrWhiteSpace(encryptedAttribute.KeyPurpose)
110+
? "default"
111+
: encryptedAttribute.KeyPurpose;
112+
113+
property.Builder.HasAnnotation(EncryptedPropertyAnnotations.IsEncrypted, true, fromDataAnnotation: true);
114+
property.Builder.HasAnnotation(EncryptedPropertyAnnotations.KeyPurpose, keyPurpose, fromDataAnnotation: true);
115+
property.Builder.HasAnnotation(
116+
EncryptedPropertyAnnotations.Materialization,
117+
GetMaterializationMode(property.ClrType),
118+
fromDataAnnotation: true);
119+
}
120+
121+
private static EncryptedAttribute? GetEncryptedAttribute(PropertyInfo propertyInfo)
122+
{
123+
return propertyInfo.GetCustomAttributes(typeof(EncryptedAttribute), inherit: true)
124+
.OfType<EncryptedAttribute>()
125+
.FirstOrDefault();
126+
}
127+
41128
private static void ConfigureCiphertextStorage(IConventionEntityType entityType, IConventionProperty plaintextProperty)
42129
{
43130
if (plaintextProperty.IsKey()
@@ -115,8 +202,13 @@ private static string GetCiphertextPropertyName(IConventionEntityType entityType
115202

116203
private static string GetMaterializationMode(Type clrType)
117204
{
118-
return clrType.IsGenericType && clrType.GetGenericTypeDefinition() == typeof(EncryptedValue<>)
205+
return IsEncryptedValueType(clrType)
119206
? "Lazy"
120207
: "DecryptOnRead";
121208
}
209+
210+
private static bool IsEncryptedValueType(Type type)
211+
{
212+
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(EncryptedValue<>);
213+
}
122214
}

src/EfCore.EncryptedProperties/Interceptors/EncryptedPropertiesMaterializationInterceptor.cs

Lines changed: 10 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
using System.Collections.Concurrent;
2-
using System.Reflection;
32
using EfCore.EncryptedProperties.Abstractions;
4-
using EfCore.EncryptedProperties.Configuration;
53
using EfCore.EncryptedProperties.Infrastructure;
64
using EfCore.EncryptedProperties.Metadata;
75
using Microsoft.EntityFrameworkCore;
@@ -28,7 +26,7 @@ public object InitializedInstance(MaterializationInterceptionData materializatio
2826
var model = GetModel(context);
2927

3028
var entityTypeName = materializationData.EntityType.ClrType.FullName!;
31-
var descriptors = model.GetForEntityType(entityTypeName).ToList();
29+
var descriptors = model.GetForEntityType(entityTypeName);
3230

3331
if (descriptors.Count == 0)
3432
return entity;
@@ -58,13 +56,12 @@ private static void DecryptProperty(
5856
string? payload,
5957
EncryptedPropertyServices services)
6058
{
61-
var propertyContext = CreatePropertyContext(descriptor);
6259
var plaintext = services.Cryptor
63-
.DecryptAsync(payload, descriptor.ClrType, propertyContext)
60+
.DecryptAsync(payload, descriptor.ClrType, descriptor.Context)
6461
.GetAwaiter()
6562
.GetResult();
6663

67-
var assignedValue = GetAssignableValue(plaintext, descriptor.ClrType);
64+
var assignedValue = plaintext ?? descriptor.DefaultValue;
6865
SetClrPropertyValue(entity, descriptor, assignedValue);
6966
services.StateTracker.Track(entity, descriptor, assignedValue, payload);
7067
}
@@ -75,9 +72,8 @@ private static void WireLazyProperty(
7572
string? payload,
7673
EncryptedPropertyServices services)
7774
{
78-
var propertyContext = CreatePropertyContext(descriptor);
79-
var accessor = new EncryptedValueAccessor(services.Cryptor, propertyContext);
80-
var encryptedValue = CreateEncryptedValue(descriptor.ClrType, payload, accessor);
75+
var accessor = new EncryptedValueAccessor(services.Cryptor, descriptor.Context);
76+
var encryptedValue = GetLazyAccessors(descriptor).CreateValue(payload, accessor);
8177

8278
SetClrPropertyValue(entity, descriptor, encryptedValue);
8379
services.StateTracker.Track(entity, descriptor, plaintext: null, payload);
@@ -94,46 +90,16 @@ private static void WireLazyProperty(
9490
return materializationData.GetPropertyValue<string?>(property);
9591
}
9692

97-
private static object? GetAssignableValue(object? value, Type targetType)
98-
{
99-
if (value is not null)
100-
return value;
101-
102-
return targetType.IsValueType && Nullable.GetUnderlyingType(targetType) is null
103-
? Activator.CreateInstance(targetType)
104-
: null;
105-
}
106-
10793
private static void SetClrPropertyValue(object entity, EncryptedPropertyDescriptor descriptor, object? value)
10894
{
109-
var property = entity.GetType().GetProperty(
110-
descriptor.PropertyName,
111-
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
112-
?? throw new InvalidOperationException(
113-
$"Encrypted property '{entity.GetType().FullName}.{descriptor.PropertyName}' was not found.");
114-
115-
property.SetValue(entity, value);
95+
descriptor.Accessors.SetValue(entity, value);
11696
}
11797

118-
private static object CreateEncryptedValue(Type innerType, string? payload, IEncryptedValueAccessor accessor)
98+
private static EncryptedValueAccessors GetLazyAccessors(EncryptedPropertyDescriptor descriptor)
11999
{
120-
var encryptedValueType = typeof(EncryptedValue<>).MakeGenericType(innerType);
121-
return Activator.CreateInstance(
122-
encryptedValueType,
123-
BindingFlags.Instance | BindingFlags.NonPublic,
124-
binder: null,
125-
args: [payload, accessor],
126-
culture: null)!;
127-
}
128-
129-
private static EncryptedPropertyContext CreatePropertyContext(EncryptedPropertyDescriptor descriptor)
130-
{
131-
return new EncryptedPropertyContext
132-
{
133-
Purpose = descriptor.Purpose,
134-
EntityTypeName = descriptor.EntityTypeName,
135-
PropertyName = descriptor.PropertyName
136-
};
100+
return descriptor.Accessors.EncryptedValue
101+
?? throw new InvalidOperationException(
102+
$"Encrypted property '{descriptor.EntityTypeName}.{descriptor.PropertyName}' is not configured for lazy encrypted values.");
137103
}
138104

139105
private EncryptedPropertyServices GetServices(DbContext context)

0 commit comments

Comments
 (0)