Eastrall / EntityFrameworkCore.DataEncryption

A plugin for Microsoft.EntityFrameworkCore to add support of encrypted fields using built-in or custom encryption providers.
MIT License
329 stars 55 forks source link

Error using migration functionality #43

Closed sidec15 closed 1 year ago

sidec15 commented 2 years ago

I'm trying to use the MigrationEncryptionProvider in order to encrypt already existing data that are not encrypted. So I used the following:

var destinationProvider = new AesProvider(_encryptionKey, _encryptionIV);
MigrationEncryptionProvider migrationEncryptionProvider = new(null, destinationProvider);
dbContext.EncryptionProvider = migrationEncryptionProvider;
await dbContext.MigrateAsync();

Unfortunately the MigrateAsync method throws the following exception:

System.AggregateException
  HResult=0x80131500
  Message=One or more errors occurred. (The type initializer for 'Microsoft.EntityFrameworkCore.DataEncryption.Migration.EncryptionMigrator' threw an exception.)
  Source=System.Private.CoreLib
  StackTrace:
   at System.Threading.Tasks.Task.ThrowIfExceptional(Boolean includeTaskCanceledExceptions) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 1883
   at System.Threading.Tasks.Task.Wait(Int32 millisecondsTimeout, CancellationToken cancellationToken) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 2733
   at System.Threading.Tasks.Task.Wait() in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 2607
   at MyMainProject.Startup.Configure(IApplicationBuilder app, ILogger`1 logger, IServiceProvider serviceProvider, WalletDbContext dbContext) in C:\MyMainProject\Startup.cs:line 364
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Span`1& arguments, Signature sig, Boolean constructor, Boolean wrapExceptions)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) in /_/src/coreclr/System.Private.CoreLib/src/System/Reflection/RuntimeMethodInfo.cs:line 435
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.Invoke(Object instance, IApplicationBuilder builder) in /_/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs:line 32
   at Microsoft.AspNetCore.Hosting.ConfigureBuilder.<>c__DisplayClass4_0.<Build>b__0(IApplicationBuilder builder) in /_/src/Hosting/Hosting/src/Internal/ConfigureBuilder.cs:line 21
   at Microsoft.AspNetCore.Hosting.GenericWebHostBuilder.<>c__DisplayClass15_0.<UseStartup>b__1(IApplicationBuilder app) in /_/src/Hosting/Hosting/src/GenericHost/GenericWebHostBuilder.cs:line 333
   at Microsoft.AspNetCore.Mvc.Filters.MiddlewareFilterBuilderStartupFilter.<>c__DisplayClass0_0.<Configure>g__MiddlewareFilterBuilder|0(IApplicationBuilder builder) in /_/src/Mvc/Mvc.Core/src/Filters/MiddlewareFilterBuilderStartupFilter.cs:line 23
   at Microsoft.AspNetCore.HostFilteringStartupFilter.<>c__DisplayClass0_0.<Configure>b__0(IApplicationBuilder app) in /_/src/DefaultBuilder/src/HostFilteringStartupFilter.cs:line 18
   at Microsoft.AspNetCore.Hosting.GenericWebHostService.<StartAsync>d__37.MoveNext() in /_/src/Hosting/Hosting/src/GenericHost/GenericWebHostedService.cs:line 105
   at Microsoft.Extensions.Hosting.Internal.Host.<StartAsync>d__12.MoveNext() in /_/src/libraries/Microsoft.Extensions.Hosting/src/Internal/Host.cs:line 68
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.<RunAsync>d__4.MoveNext() in /_/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/HostingAbstractionsHostExtensions.cs:line 64
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.<RunAsync>d__4.MoveNext() in /_/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/HostingAbstractionsHostExtensions.cs:line 76
   at Microsoft.Extensions.Hosting.HostingAbstractionsHostExtensions.Run(IHost host) in /_/src/libraries/Microsoft.Extensions.Hosting.Abstractions/src/HostingAbstractionsHostExtensions.cs:line 51
   at MyMainProject.Program.Main(String[] args) in C:\MyMainProject\Program.cs:line 9

  This exception was originally thrown at this call stack:
    System.RuntimeType.GetMethodImplCommon(string, int, System.Reflection.BindingFlags, System.Reflection.Binder, System.Reflection.CallingConventions, System.Type[], System.Reflection.ParameterModifier[]) in RuntimeType.CoreCLR.cs
    System.Type.GetMethod(string, System.Reflection.BindingFlags) in Type.cs
    System.Type.GetMethod(string) in Type.cs
    Microsoft.EntityFrameworkCore.DataEncryption.Migration.EncryptionMigrator.EncryptionMigrator() in EncryptionMigrator.cs

Inner Exception 1:
TypeInitializationException: The type initializer for 'Microsoft.EntityFrameworkCore.DataEncryption.Migration.EncryptionMigrator' threw an exception.

Inner Exception 2:
AmbiguousMatchException: Ambiguous match found.

This is caused by the extension method Set of the DbContext class done in the EncryptionMigrator class:

  private static IQueryable<object> Set(this DbContext context, IEntityType entityType)
  {
      var method = SetMethod.MakeGenericMethod(entityType.ClrType);
      var result = method.Invoke(context, null);
      return (IQueryable<object>)result;
  }

I thought it was a problam cause by the fact I didn't use properly the MigrationEncryptionProvider, but then I tried to run the unit test MigrateOriginalToEncryptedTest implemented here and I found somethig strange. The test passes but looking at the SQLite db used for the test, the fields expected to be encrypted (for example FirstName of the AuthorEntity) are in plain text! So I started debugging the code and I realized that the line of code (included in the EncryptionMigrator class) causing the exception to be thrown:

var set = context.Set(entityType);

is never executed! After some hours of debug I found the problem.

In the Execute method of the MigratorBaseTest class we have the following:

  // Feed database with data.
  using (var contextFactory = new DatabaseContextFactory(databaseName))
  {
      await using var context = contextFactory.CreateContext<DatabaseContext>(provider.SourceEncryptionProvider);
      await context.Authors.AddRangeAsync(Authors);
      await context.SaveChangesAsync();
  }

  // Process data migration
  using (var contextFactory = new DatabaseContextFactory(databaseName))
  {
      await using var context = contextFactory.CreateContext<DatabaseContext>(provider);
      await context.MigrateAsync();
  }

  // Assert if the context has been decrypted
  using (var contextFactory = new DatabaseContextFactory(databaseName))
  {
      await using var context = contextFactory.CreateContext<DatabaseContext>(provider.DestinationEncryptionProvider);
      IEnumerable<AuthorEntity> authors = await context.Authors.Include(x => x.Books).ToListAsync();

      foreach (AuthorEntity author in authors)
      {
          AuthorEntity original = Authors.FirstOrDefault(x => x.UniqueId == author.UniqueId);
          AssertAuthor(original, author);
      }
  }

Here we can see that the db context is created three times but debugging the code I realized that the UseEncryption method is called only the first time:

await using var context = contextFactory.CreateContext<DatabaseContext>(provider.SourceEncryptionProvider);

This is caused by the GetModel method of the class Microsoft.EntityFrameworkCore.Infrastructure.ModelSource

  if (!cache.TryGetValue(cacheKey, out IModel model))
  {
      // call CreateModel
  }
  return model;

As we can see a cache is used to check if the CreateModel method has to be called. Since the UseEncryption is called only with the SourceEncryptionProvider, the data are not migrated at all in this test, because in the MigrationAsyncCore method the encrypted properties are obtained by the filter:

  var encryptedProperties = entityType.GetProperties()
      .Select(p => (property: p, encryptionProvider: (p.GetValueConverter() as IEncryptionValueConverter)?.EncryptionProvider))
      .Where(p => p.encryptionProvider is MigrationEncryptionProvider { IsEmpty: false })
      .Select(p => p.property)
      .ToList();

but they are always 0 because the provider is always the SourceEncryptionProvider that is not of type MigrationEncryptionProvider. That's why the exception is not thrown durin the execution of this test. The same happens also for the other tests related to the migration.

Eastrall commented 1 year ago

Migrator has been removed from the library since V3.X. Closing issue.