Skip to content

Commit ed588eb

Browse files
author
Roman Golovanov
committed
add key event logging
1 parent 99f3c0a commit ed588eb

15 files changed

Lines changed: 953 additions & 59 deletions

src/EfCore.EncryptedProperties.Testing/DbContextOptionsBuilderTestExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using EfCore.EncryptedProperties.Extensions;
22
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.EntityFrameworkCore.Diagnostics;
34
using Microsoft.Extensions.DependencyInjection;
45

56
namespace EfCore.EncryptedProperties.Testing;
@@ -23,6 +24,7 @@ public static DbContextOptionsBuilder UseEncryptedPropertiesForTesting(
2324
this DbContextOptionsBuilder builder,
2425
IServiceProvider serviceProvider)
2526
{
27+
builder.ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.ManyServiceProvidersCreatedWarning));
2628
return builder.UseEncryptedProperties(serviceProvider);
2729
}
2830
}

src/EfCore.EncryptedProperties/Cryptography/EncryptedPropertyCryptor.cs

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,25 @@
22
using System.Text;
33
using EfCore.EncryptedProperties.Abstractions;
44
using EfCore.EncryptedProperties.Configuration;
5+
using Microsoft.Extensions.Logging;
6+
using Microsoft.Extensions.Logging.Abstractions;
57

68
namespace EfCore.EncryptedProperties.Cryptography;
79

810
internal sealed class EncryptedPropertyCryptor : IEncryptedPropertyCryptor
911
{
1012
private readonly IKeyChainManager _keyChainManager;
1113
private readonly IValueSerializer _serializer;
14+
private readonly ILogger<EncryptedPropertyCryptor> _logger;
1215

13-
public EncryptedPropertyCryptor(IKeyChainManager keyChainManager, IValueSerializer serializer)
16+
public EncryptedPropertyCryptor(
17+
IKeyChainManager keyChainManager,
18+
IValueSerializer serializer,
19+
ILogger<EncryptedPropertyCryptor>? logger = null)
1420
{
1521
_keyChainManager = keyChainManager;
1622
_serializer = serializer;
23+
_logger = logger ?? NullLogger<EncryptedPropertyCryptor>.Instance;
1724
}
1825

1926
public async ValueTask<string?> EncryptAsync(
@@ -64,32 +71,60 @@ public EncryptedPropertyCryptor(IKeyChainManager keyChainManager, IValueSerializ
6471
if (string.IsNullOrEmpty(payload))
6572
return null;
6673

67-
var components = JweCompactSerializer.Deserialize(payload);
74+
string? keyId = null;
6875

69-
if (components.Header.Alg != "A256GCMKW")
70-
throw new CryptographicException($"Unsupported key wrap algorithm: {components.Header.Alg}");
76+
try
77+
{
78+
var components = JweCompactSerializer.Deserialize(payload);
79+
keyId = components.Header.Kid;
7180

72-
if (components.Header.Enc != "A256GCM")
73-
throw new CryptographicException($"Unsupported content encryption: {components.Header.Enc}");
81+
if (components.Header.Alg != "A256GCMKW")
82+
throw new CryptographicException($"Unsupported key wrap algorithm: {components.Header.Alg}");
7483

75-
if (components.Header.Iv is null || components.Header.Tag is null)
76-
throw new CryptographicException("JWE header missing key-wrap iv or tag for A256GCMKW.");
84+
if (components.Header.Enc != "A256GCM")
85+
throw new CryptographicException($"Unsupported content encryption: {components.Header.Enc}");
7786

78-
var kek = await _keyChainManager.GetKeyForDecryptAsync(components.Header.Kid, cancellationToken);
87+
if (components.Header.Iv is null || components.Header.Tag is null)
88+
throw new CryptographicException("JWE header missing key-wrap iv or tag for A256GCMKW.");
7989

80-
var kwIv = Base64Url.Decode(components.Header.Iv);
81-
var kwTag = Base64Url.Decode(components.Header.Tag);
82-
var cek = AesGcmKeyWrapper.UnwrapKey(kek.Key, components.WrappedCek, kwIv, kwTag);
90+
var kek = await _keyChainManager.GetKeyForDecryptAsync(components.Header.Kid, cancellationToken);
8391

84-
try
85-
{
86-
var aad = Encoding.ASCII.GetBytes(components.RawHeaderB64);
87-
var plaintext = AesGcmEncryptor.Decrypt(cek, components.Ciphertext, components.Tag, components.Iv, aad);
88-
return _serializer.Deserialize(plaintext, targetType);
92+
var kwIv = Base64Url.Decode(components.Header.Iv);
93+
var kwTag = Base64Url.Decode(components.Header.Tag);
94+
var cek = AesGcmKeyWrapper.UnwrapKey(kek.Key, components.WrappedCek, kwIv, kwTag);
95+
96+
try
97+
{
98+
var aad = Encoding.ASCII.GetBytes(components.RawHeaderB64);
99+
var plaintext = AesGcmEncryptor.Decrypt(cek, components.Ciphertext, components.Tag, components.Iv, aad);
100+
return _serializer.Deserialize(plaintext, targetType);
101+
}
102+
finally
103+
{
104+
CryptographicOperations.ZeroMemory(cek);
105+
}
89106
}
90-
finally
107+
catch (Exception ex) when (ex is not OperationCanceledException)
91108
{
92-
CryptographicOperations.ZeroMemory(cek);
109+
LogDecryptionFailed(ex, context, targetType, keyId);
110+
throw;
93111
}
94112
}
113+
114+
private void LogDecryptionFailed(
115+
Exception exception,
116+
EncryptedPropertyContext context,
117+
Type targetType,
118+
string? keyId)
119+
{
120+
_logger.LogError(
121+
EncryptedPropertiesEventIds.DecryptionFailed,
122+
exception,
123+
"Failed to decrypt encrypted property {EntityTypeName}.{PropertyName} for purpose {Purpose} as {TargetType} with KEK {KeyId}.",
124+
context.EntityTypeName ?? "(unknown entity)",
125+
context.PropertyName ?? "(unknown property)",
126+
context.Purpose,
127+
targetType.FullName ?? targetType.Name,
128+
keyId ?? "(unknown)");
129+
}
95130
}

src/EfCore.EncryptedProperties/EfCore.EncryptedProperties.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
<ItemGroup>
3434
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.16" />
35+
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.16" />
3536
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.16" />
3637
<PackageReference Include="Azure.Security.KeyVault.Keys" Version="4.8.0" />
3738
<PackageReference Include="Azure.Identity" Version="1.17.1" />
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Microsoft.Extensions.Logging;
2+
3+
namespace EfCore.EncryptedProperties;
4+
5+
public static class EncryptedPropertiesEventIds
6+
{
7+
public static readonly EventId KeyCreated = new(1000, nameof(KeyCreated));
8+
public static readonly EventId KeyRotated = new(1001, nameof(KeyRotated));
9+
public static readonly EventId KeyPreloadFailed = new(1002, nameof(KeyPreloadFailed));
10+
public static readonly EventId DecryptionFailed = new(1003, nameof(DecryptionFailed));
11+
public static readonly EventId EncryptedPropertyModelDiscovered = new(1004, nameof(EncryptedPropertyModelDiscovered));
12+
public static readonly EventId EncryptedPropertyDiscovered = new(1005, nameof(EncryptedPropertyDiscovered));
13+
}

src/EfCore.EncryptedProperties/Extensions/DbContextOptionsBuilderExtensions.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ public static DbContextOptionsBuilder UseEncryptedProperties(
1616
ArgumentNullException.ThrowIfNull(builder);
1717
ArgumentNullException.ThrowIfNull(serviceProvider);
1818

19-
var cryptor = serviceProvider.GetRequiredService<IEncryptedPropertyCryptor>();
20-
var extension = new EncryptedPropertiesDbContextOptionsExtension(cryptor);
19+
_ = serviceProvider.GetRequiredService<IEncryptedPropertyCryptor>();
20+
var extension = new EncryptedPropertiesDbContextOptionsExtension();
2121
((IDbContextOptionsBuilderInfrastructure)builder).AddOrUpdateExtension(extension);
2222

2323
builder.AddInterceptors(

src/EfCore.EncryptedProperties/Extensions/EncryptedPropertiesServiceBuilder.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using EfCore.EncryptedProperties.Infrastructure;
88
using EfCore.EncryptedProperties.Interceptors;
99
using EfCore.EncryptedProperties.KeyManagement;
10+
using EfCore.EncryptedProperties.Metadata;
1011
using EfCore.EncryptedProperties.Providers;
1112
using EfCore.EncryptedProperties.Serialization;
1213
using Microsoft.Extensions.DependencyInjection;
@@ -35,14 +36,17 @@ public static IServiceCollection AddEncryptedProperties(
3536
services.RemoveAll<IKeyChainManager>();
3637
services.RemoveAll<IEncryptedPropertyCryptor>();
3738
services.RemoveAll<EncryptedPropertyStateTracker>();
39+
services.RemoveAll<EncryptedPropertyModelCache>();
3840
services.RemoveAll<EncryptedPropertiesSaveChangesInterceptor>();
3941
services.RemoveAll<EncryptedPropertiesMaterializationInterceptor>();
4042

43+
services.AddLogging();
4144
services.AddSingleton(options);
4245
services.AddSingleton<IValueSerializer, ValueSerializer>();
4346
services.AddSingleton<IKeyChainManager, KeyChainManager>();
4447
services.AddSingleton<IEncryptedPropertyCryptor, EncryptedPropertyCryptor>();
4548
services.AddScoped<EncryptedPropertyStateTracker>();
49+
services.AddSingleton<EncryptedPropertyModelCache>();
4650
services.AddSingleton<EncryptedPropertiesSaveChangesInterceptor>();
4751
services.AddSingleton<EncryptedPropertiesMaterializationInterceptor>();
4852

src/EfCore.EncryptedProperties/Infrastructure/EncryptedPropertiesConventionSetPlugin.cs

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Reflection;
22
using EfCore.EncryptedProperties.Configuration;
3+
using EfCore.EncryptedProperties.Metadata;
34
using Microsoft.EntityFrameworkCore;
45
using Microsoft.EntityFrameworkCore.Metadata;
56
using Microsoft.EntityFrameworkCore.Metadata.Builders;
@@ -46,7 +47,7 @@ private static void ApplyEncryptedAttributes(IConventionModelBuilder modelBuilde
4647
{
4748
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes().ToList())
4849
{
49-
if (entityType.ClrType is null || IsEncryptedValueType(entityType.ClrType))
50+
if (entityType.ClrType is null || EncryptedPropertyTypeSupport.IsEncryptedValueType(entityType.ClrType))
5051
continue;
5152

5253
foreach (var propertyInfo in entityType.ClrType.GetProperties(
@@ -91,7 +92,7 @@ private static void RemoveUnusedEncryptedValueEntityTypes(IConventionModelBuilde
9192
{
9293
foreach (var entityType in modelBuilder.Metadata.GetEntityTypes().ToList())
9394
{
94-
if (!IsEncryptedValueType(entityType.ClrType))
95+
if (!EncryptedPropertyTypeSupport.IsEncryptedValueType(entityType.ClrType))
9596
continue;
9697

9798
if (entityType.GetForeignKeys().Any() || entityType.GetReferencingForeignKeys().Any())
@@ -136,9 +137,11 @@ private static void ConfigureCiphertextStorage(IConventionEntityType entityType,
136137
}
137138

138139
var plaintextPropertyName = plaintextProperty.Name;
140+
var materialization = GetConfiguredMaterializationMode(entityType, plaintextProperty);
141+
ValidateEncryptedProperty(entityType, plaintextProperty, materialization);
142+
139143
var ciphertextPropertyName = GetCiphertextPropertyName(entityType, plaintextPropertyName);
140144
var purpose = plaintextProperty.FindAnnotation(EncryptedPropertyAnnotations.KeyPurpose)?.Value as string ?? "default";
141-
var materialization = plaintextProperty.FindAnnotation(EncryptedPropertyAnnotations.Materialization)?.Value as string;
142145
var columnName = plaintextProperty.GetColumnName() ?? plaintextPropertyName;
143146
var columnType = plaintextProperty.GetColumnType();
144147
var maxLength = plaintextProperty.GetMaxLength();
@@ -165,7 +168,7 @@ private static void ConfigureCiphertextStorage(IConventionEntityType entityType,
165168
ciphertextPropertyBuilder.HasAnnotation(EncryptedPropertyAnnotations.KeyPurpose, purpose, fromDataAnnotation: false);
166169
ciphertextPropertyBuilder.HasAnnotation(
167170
EncryptedPropertyAnnotations.Materialization,
168-
materialization ?? GetMaterializationMode(plaintextProperty.ClrType),
171+
materialization,
169172
fromDataAnnotation: false);
170173

171174
ciphertextPropertyBuilder.HasColumnName(columnName, fromDataAnnotation: false);
@@ -202,13 +205,79 @@ private static string GetCiphertextPropertyName(IConventionEntityType entityType
202205

203206
private static string GetMaterializationMode(Type clrType)
204207
{
205-
return IsEncryptedValueType(clrType)
206-
? "Lazy"
207-
: "DecryptOnRead";
208+
return EncryptedPropertyTypeSupport.IsEncryptedValueType(clrType)
209+
? EncryptedPropertyTypeSupport.LazyMaterialization
210+
: EncryptedPropertyTypeSupport.DecryptOnReadMaterialization;
211+
}
212+
213+
private static string GetConfiguredMaterializationMode(IConventionEntityType entityType, IConventionProperty property)
214+
{
215+
var materialization = property.FindAnnotation(EncryptedPropertyAnnotations.Materialization)?.Value as string
216+
?? GetMaterializationMode(property.ClrType);
217+
218+
if (string.Equals(
219+
materialization,
220+
EncryptedPropertyTypeSupport.LazyMaterialization,
221+
StringComparison.OrdinalIgnoreCase))
222+
{
223+
return EncryptedPropertyTypeSupport.LazyMaterialization;
224+
}
225+
226+
if (string.Equals(
227+
materialization,
228+
EncryptedPropertyTypeSupport.DecryptOnReadMaterialization,
229+
StringComparison.OrdinalIgnoreCase))
230+
{
231+
return EncryptedPropertyTypeSupport.DecryptOnReadMaterialization;
232+
}
233+
234+
throw new InvalidOperationException(
235+
$"Encrypted property '{GetPropertyDisplayName(entityType, property)}' has invalid materialization mode '{materialization}'. Supported modes are '{EncryptedPropertyTypeSupport.DecryptOnReadMaterialization}' and '{EncryptedPropertyTypeSupport.LazyMaterialization}'.");
236+
}
237+
238+
private static void ValidateEncryptedProperty(
239+
IConventionEntityType entityType,
240+
IConventionProperty property,
241+
string materialization)
242+
{
243+
var isEncryptedValue = EncryptedPropertyTypeSupport.IsEncryptedValueType(property.ClrType);
244+
Type plaintextType;
245+
246+
if (materialization == EncryptedPropertyTypeSupport.LazyMaterialization)
247+
{
248+
if (!isEncryptedValue)
249+
{
250+
throw new InvalidOperationException(
251+
$"Encrypted property '{GetPropertyDisplayName(entityType, property)}' is configured for lazy materialization but CLR type '{GetTypeDisplayName(property.ClrType)}' must be EncryptedValue<T>.");
252+
}
253+
254+
plaintextType = property.ClrType.GetGenericArguments()[0];
255+
}
256+
else
257+
{
258+
if (isEncryptedValue)
259+
{
260+
throw new InvalidOperationException(
261+
$"Encrypted property '{GetPropertyDisplayName(entityType, property)}' uses EncryptedValue<T> and must be configured for lazy materialization.");
262+
}
263+
264+
plaintextType = property.ClrType;
265+
}
266+
267+
if (!EncryptedPropertyTypeSupport.IsSupportedPlaintextType(plaintextType))
268+
{
269+
throw new InvalidOperationException(
270+
$"Encrypted property '{GetPropertyDisplayName(entityType, property)}' has unsupported CLR type '{GetTypeDisplayName(plaintextType)}'. Supported encrypted property types are {EncryptedPropertyTypeSupport.SupportedTypesDescription}.");
271+
}
272+
}
273+
274+
private static string GetPropertyDisplayName(IConventionEntityType entityType, IConventionProperty property)
275+
{
276+
return $"{entityType.ClrType.FullName}.{property.Name}";
208277
}
209278

210-
private static bool IsEncryptedValueType(Type type)
279+
private static string GetTypeDisplayName(Type type)
211280
{
212-
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(EncryptedValue<>);
281+
return type.FullName ?? type.Name;
213282
}
214283
}

src/EfCore.EncryptedProperties/Infrastructure/EncryptedPropertiesDbContextOptionsExtension.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Runtime.CompilerServices;
2-
using EfCore.EncryptedProperties.Abstractions;
31
using Microsoft.EntityFrameworkCore.Infrastructure;
42
using Microsoft.EntityFrameworkCore.Metadata.Conventions.Infrastructure;
53
using Microsoft.Extensions.DependencyInjection;
@@ -8,20 +6,13 @@ namespace EfCore.EncryptedProperties.Infrastructure;
86

97
internal sealed class EncryptedPropertiesDbContextOptionsExtension : IDbContextOptionsExtension
108
{
11-
private readonly IEncryptedPropertyCryptor _cryptor;
129
private DbContextOptionsExtensionInfo? _info;
1310

14-
public EncryptedPropertiesDbContextOptionsExtension(IEncryptedPropertyCryptor cryptor)
15-
{
16-
_cryptor = cryptor;
17-
}
18-
1911
public DbContextOptionsExtensionInfo Info => _info ??= new ExtensionInfo(this);
2012

2113
public void ApplyServices(IServiceCollection services)
2214
{
2315
services.AddSingleton<IConventionSetPlugin, EncryptedPropertiesConventionSetPlugin>();
24-
services.AddSingleton(_cryptor);
2516
services.AddScoped<EncryptedPropertyStateTracker>();
2617
}
2718

@@ -31,22 +22,18 @@ public void Validate(IDbContextOptions options)
3122

3223
private sealed class ExtensionInfo : DbContextOptionsExtensionInfo
3324
{
34-
private readonly EncryptedPropertiesDbContextOptionsExtension _extension;
35-
3625
public ExtensionInfo(IDbContextOptionsExtension extension) : base(extension)
3726
{
38-
_extension = (EncryptedPropertiesDbContextOptionsExtension)extension;
3927
}
4028

4129
public override bool IsDatabaseProvider => false;
4230
public override string LogFragment => "using EncryptedProperties ";
4331

4432
public override int GetServiceProviderHashCode()
45-
=> RuntimeHelpers.GetHashCode(_extension._cryptor);
33+
=> 0;
4634

4735
public override bool ShouldUseSameServiceProvider(DbContextOptionsExtensionInfo other)
48-
=> other is ExtensionInfo otherInfo
49-
&& ReferenceEquals(_extension._cryptor, otherInfo._extension._cryptor);
36+
=> other is ExtensionInfo;
5037

5138
public override void PopulateDebugInfo(IDictionary<string, string> debugInfo)
5239
{

src/EfCore.EncryptedProperties/Interceptors/EncryptedPropertiesMaterializationInterceptor.cs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
using System.Collections.Concurrent;
21
using EfCore.EncryptedProperties.Abstractions;
32
using EfCore.EncryptedProperties.Infrastructure;
43
using EfCore.EncryptedProperties.Metadata;
@@ -13,11 +12,14 @@ namespace EfCore.EncryptedProperties.Interceptors;
1312
internal sealed class EncryptedPropertiesMaterializationInterceptor : IMaterializationInterceptor
1413
{
1514
private readonly IEncryptedPropertyCryptor _cryptor;
16-
private readonly ConcurrentDictionary<IModel, EncryptedPropertyModel> _models = new();
15+
private readonly EncryptedPropertyModelCache _modelCache;
1716

18-
public EncryptedPropertiesMaterializationInterceptor(IEncryptedPropertyCryptor cryptor)
17+
public EncryptedPropertiesMaterializationInterceptor(
18+
IEncryptedPropertyCryptor cryptor,
19+
EncryptedPropertyModelCache? modelCache = null)
1920
{
2021
_cryptor = cryptor;
22+
_modelCache = modelCache ?? new EncryptedPropertyModelCache();
2123
}
2224

2325
public object InitializedInstance(MaterializationInterceptionData materializationData, object entity)
@@ -112,7 +114,7 @@ private EncryptedPropertyServices GetServices(DbContext context)
112114

113115
private EncryptedPropertyModel GetModel(DbContext context)
114116
{
115-
return _models.GetOrAdd(context.Model, EncryptedPropertyModelBuilder.Build);
117+
return _modelCache.GetOrAdd(context.Model);
116118
}
117119

118120
private readonly record struct EncryptedPropertyServices(

0 commit comments

Comments
 (0)