diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 0d4a6fd..0000000 --- a/.editorconfig +++ /dev/null @@ -1,39 +0,0 @@ -[*.cs] -indent_style = space -indent_size = 4 - -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = methods, object_collection_array_initializers, control_blocks, types -dotnet_sort_system_directives_first = true - -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_parameter_list_parentheses = false - -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = false -csharp_prefer_braces = true:warning - -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_pattern_matching_over_as_with_null_check = true:suggestion - -dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion -csharp_preferred_modifier_order = public,private,internal,protected,static,readonly,sealed,async,override,abstract:suggestion - -dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion -dotnet_style_predefined_type_for_member_access = true:suggestion -dotnet_style_object_initializer = true:suggestion -dotnet_style_prefer_inferred_tuple_names = true:suggestion -csharp_prefer_simple_default_expression = true:suggestion - -dotnet_style_qualification_for_field = false:suggestion -dotnet_style_qualification_for_method = false:suggestion -dotnet_style_qualification_for_property = false:suggestion diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 208444e..61ceef2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,13 +9,22 @@ on: jobs: build-library: runs-on: ubuntu-latest + steps: - - uses: actions/checkout@v2 + - name: Checkout repository + uses: actions/checkout@v3 - - name: Setup .NET - uses: actions/setup-dotnet@v1 + - name: Setup .NET Core + uses: actions/setup-dotnet@v3 with: - dotnet-version: 6.0.x + dotnet-version: | + 3.1.x + 5.0.x + 6.0.x + 7.0.x + + - name: Display .NET version + run: dotnet --version - name: Restore dependencies run: dotnet restore @@ -24,4 +33,13 @@ jobs: run: dotnet build EntityFrameworkCore.DataEncryption.sln --configuration Release -f net6.0 --no-restore - name: Run unit tests - run: dotnet test EntityFrameworkCore.DataEncryption.sln --configuration Release /p:CollectCoverage=true /p:Exclude="[xunit*]*" /p:CoverletOutputFormat=opencover /p:CoverletOutput="../TestResults/TestResults.xml" /maxcpucount:1 -f net6.0 --no-build --verbosity normal \ No newline at end of file + run: dotnet test --configuration Release --collect:"XPlat Code Coverage" --settings ./test/EntityFrameworkCore.DataEncryption.Test/runsettings.xml + + - name: Copy coverage results + run: cp ./test/EntityFrameworkCore.DataEncryption.Test/TestResults/**/*.xml ./test/EntityFrameworkCore.DataEncryption.Test/TestResults/ + + - name: Upload code coverage + uses: codecov/codecov-action@v3 + with: + files: ./test/EntityFrameworkCore.DataEncryption.Test/TestResults/coverage.opencover.xml + fail_ci_if_error: true \ No newline at end of file diff --git a/EntityFrameworkCore.DataEncryption.sln b/EntityFrameworkCore.DataEncryption.sln index fb35659..dd88ef0 100644 --- a/EntityFrameworkCore.DataEncryption.sln +++ b/EntityFrameworkCore.DataEncryption.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30804.86 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32901.215 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{3EC10767-1816-46B2-A78E-9856071CCFDB}" EndProject @@ -22,17 +22,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{64C3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AesSample", "samples\AesSample\AesSample.csproj", "{8AA1E576-4016-4623-96C8-90330F05F9A8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".azure", ".azure", "{073FEA06-67CF-47F8-8CE4-2B153A7D8443}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{EEF46CDC-C438-48FC-BEF7-83AEE26C63F7}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pipelines", "pipelines", "{68558245-F605-413F-A1D9-A4F60D489D68}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{4F549FEF-C57B-4A34-A2C7-8A632762DF85}" ProjectSection(SolutionItems) = preProject - .azure\pipelines\azure-pipelines.yml = .azure\pipelines\azure-pipelines.yml - EndProjectSection -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5EE4E8BE-6B15-49DB-A4A8-D2CD63D5E90C}" - ProjectSection(SolutionItems) = preProject - .editorconfig = .editorconfig + .github\workflows\build.yml = .github\workflows\build.yml EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AesSample.Fluent", "samples\AesSample.Fluent\AesSample.Fluent.csproj", "{CF04DE64-713F-4ED3-9C14-B7C11D22454C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +49,10 @@ Global {8AA1E576-4016-4623-96C8-90330F05F9A8}.Debug|Any CPU.Build.0 = Debug|Any CPU {8AA1E576-4016-4623-96C8-90330F05F9A8}.Release|Any CPU.ActiveCfg = Release|Any CPU {8AA1E576-4016-4623-96C8-90330F05F9A8}.Release|Any CPU.Build.0 = Release|Any CPU + {CF04DE64-713F-4ED3-9C14-B7C11D22454C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CF04DE64-713F-4ED3-9C14-B7C11D22454C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CF04DE64-713F-4ED3-9C14-B7C11D22454C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CF04DE64-713F-4ED3-9C14-B7C11D22454C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -59,8 +61,9 @@ Global {D037F8D0-E606-4C5A-8669-DB6AAE7B056B} = {3EC10767-1816-46B2-A78E-9856071CCFDB} {5E023B6A-0B47-4EC2-90B9-2DF998E58ADB} = {E4089551-AF4E-41B3-A6F8-2501A3BE0E0C} {8AA1E576-4016-4623-96C8-90330F05F9A8} = {64C3D7D1-67B8-4070-AE67-C71B761535CC} - {073FEA06-67CF-47F8-8CE4-2B153A7D8443} = {3A8D800E-77BD-44EF-82DB-C672281ECAAA} - {68558245-F605-413F-A1D9-A4F60D489D68} = {073FEA06-67CF-47F8-8CE4-2B153A7D8443} + {EEF46CDC-C438-48FC-BEF7-83AEE26C63F7} = {3A8D800E-77BD-44EF-82DB-C672281ECAAA} + {4F549FEF-C57B-4A34-A2C7-8A632762DF85} = {EEF46CDC-C438-48FC-BEF7-83AEE26C63F7} + {CF04DE64-713F-4ED3-9C14-B7C11D22454C} = {64C3D7D1-67B8-4070-AE67-C71B761535CC} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4997BAE9-29BF-4D79-AE5E-5605E7A0F049} diff --git a/README.md b/README.md index 0ca13bd..96da64a 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,17 @@ [![.NET](https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/actions/workflows/build.yml/badge.svg)](https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/Eastrall/EntityFrameworkCore.DataEncryption/branch/master/graph/badge.svg)](https://codecov.io/gh/Eastrall/EntityFrameworkCore.DataEncryption) [![Nuget](https://img.shields.io/nuget/v/EntityFrameworkCore.DataEncryption.svg)](https://www.nuget.org/packages/EntityFrameworkCore.DataEncryption) +[![Nuget Downloads](https://img.shields.io/nuget/dt/EntityFrameworkCore.DataEncryption)](https://www.nuget.org/packages/EntityFrameworkCore.DataEncryption) `EntityFrameworkCore.DataEncryption` is a [Microsoft Entity Framework Core](https://github.com/aspnet/EntityFrameworkCore) extension to add support of encrypted fields using built-in or custom encryption providers. ## Disclaimer -This library has been developed initialy for a personal project of mine. It provides a simple way to encrypt column data. +

:warning: This project is **not** affiliated with Microsoft. :warning:


-I **do not** take responsability if you use this in a production environment and loose your encryption key. +This library has been developed initialy for a personal project of mine which suits my use case. It provides a simple way to encrypt column data. + +I **do not** take responsability if you use/deploy this in a production environment and loose your encryption key or corrupt your data. ## How to install @@ -19,17 +22,29 @@ Install the package from [NuGet](https://www.nuget.org/) or from the `Package Ma PM> Install-Package EntityFrameworkCore.DataEncryption ``` -## How to use +## Supported types -To use `EntityFrameworkCore.DataEncryption`, you will need to decorate your `string` properties of your entities with the `[Encrypted]` attribute and enable the encryption on the `ModelBuilder`. +| Type | Default storage type | +|------|----------------------| +| `string` | Base64 string | +| `byte[]` | BINARY | -To enable the encryption correctly, you will need to use an **encryption provider**, there is a list of the available providers: +## Built-in providers | Name | Class | Extra | |------|-------|-------| -| [AES](https://docs.microsoft.com/en-US/dotnet/api/system.security.cryptography.aes?view=netcore-2.2) | [AesProvider](https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/blob/master/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs) | Can use a 128bits, 192bits or 256bits key | +| [AES](https://learn.microsoft.com/en-US/dotnet/api/system.security.cryptography.aes?view=net-6.0) | [AesProvider](https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/blob/main/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs) | Can use a 128bits, 192bits or 256bits key | + +## How to use + +`EntityFrameworkCore.DataEncryption` supports 2 differents initialization methods: +* Attribute +* Fluent configuration -### Example with `AesProvider` +Depending on the initialization method you will use, you will need to decorate your `string` or `byte[]` properties of your entities with the `[Encrypted]` attribute or use the fluent `IsEncrypted()` method in your model configuration process. +To use an encryption provider on your EF Core model, and enable the encryption on the `ModelBuilder`. + +### Example with `AesProvider` and attribute ```csharp public class UserEntity @@ -58,34 +73,72 @@ public class DatabaseContext : DbContext public DatabaseContext(DbContextOptions options) : base(options) { - this._provider = new AesProvider(this._encryptionKey, this._encryptionIV); + _provider = new AesProvider(this._encryptionKey, this._encryptionIV); } protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.UseEncryption(this._provider); + modelBuilder.UseEncryption(_provider); } } ``` -The code bellow creates a new `AesEncryption` provider and gives it to the current model. It will encrypt every `string` fields of your model that has the `[Encrypted]` attribute when saving changes to database. As for the decrypt process, it will be done when reading the `DbSet` of your `DbContext`. +The code bellow creates a new [`AesProvider`](https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/blob/main/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs) and gives it to the current model. It will encrypt every `string` fields of your model that has the `[Encrypted]` attribute when saving changes to database. As for the decrypt process, it will be done when reading the `DbSet` of your `DbContext`. -## Create an encryption provider +### Example with `AesProvider` and fluent configuration + +```csharp +public class UserEntity +{ + public int Id { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public int Age { get; set; } +} -> :warning: This section is outdated and doesn't work for V3.0.0 and will be updated soon. +public class DatabaseContext : DbContext +{ + // Get key and IV from a Base64String or any other ways. + // You can generate a key and IV using "AesProvider.GenerateKey()" + private readonly byte[] _encryptionKey = ...; + private readonly byte[] _encryptionIV = ...; + private readonly IEncryptionProvider _provider; + + public DbSet Users { get; set; } + + public DatabaseContext(DbContextOptions options) + : base(options) + { + _provider = new AesProvider(this._encryptionKey, this._encryptionIV); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + // Entities builder *MUST* be called before UseEncryption(). + var userEntityBuilder = modelBuilder.Entity(); + + userEntityBuilder.Property(x => x.Username).IsRequired().IsEncrypted(); + userEntityBuilder.Property(x => x.Password).IsRequired().IsEncrypted(); + + modelBuilder.UseEncryption(_provider); + } +} +``` + +## Create an encryption provider `EntityFrameworkCore.DataEncryption` gives the possibility to create your own encryption providers. To do so, create a new class and make it inherit from `IEncryptionProvider`. You will need to implement the `Encrypt(string)` and `Decrypt(string)` methods. ```csharp public class MyCustomEncryptionProvider : IEncryptionProvider { - public string Encrypt(string dataToEncrypt) + public byte[] Encrypt(byte[] input) { - // Encrypt data and return as Base64 string + // Encrypt the given input and return the encrypted data as a byte[]. } - public string Decrypt(string dataToDecrypt) + public byte[] Decrypt(byte[] input) { - // Decrypt a Base64 string to plain string + // Decrypt the given input and return the decrypted data as a byte[]. } } ``` @@ -99,12 +152,12 @@ public class DatabaseContext : DbContext public DatabaseContext(DbContextOptions options) : base(options) { - this._provider = new MyCustomEncryptionProvider(); + _provider = new MyCustomEncryptionProvider(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.UseEncryption(this._provider); + modelBuilder.UseEncryption(_provider); } } ``` diff --git a/samples/AesSample.Fluent/AesSample.Fluent.csproj b/samples/AesSample.Fluent/AesSample.Fluent.csproj new file mode 100644 index 0000000..31cee5b --- /dev/null +++ b/samples/AesSample.Fluent/AesSample.Fluent.csproj @@ -0,0 +1,19 @@ + + + + Exe + net6.0 + 10 + + + + + + + + + + + + + diff --git a/samples/AesSample.Fluent/DatabaseContext.cs b/samples/AesSample.Fluent/DatabaseContext.cs new file mode 100644 index 0000000..e68fab5 --- /dev/null +++ b/samples/AesSample.Fluent/DatabaseContext.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.DataEncryption; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.ComponentModel.DataAnnotations; + +namespace AesSample.Fluent; + +public class DatabaseContext : DbContext +{ + private readonly IEncryptionProvider _encryptionProvider; + + public DbSet Users { get; set; } + + public DatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider) + : base(options) + { + _encryptionProvider = encryptionProvider; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + EntityTypeBuilder userEntityBuilder = modelBuilder.Entity(); + + userEntityBuilder.HasKey(x => x.Id); + userEntityBuilder.Property(x => x.Id).IsRequired().ValueGeneratedOnAdd(); + userEntityBuilder.Property(x => x.FirstName).IsRequired(); + userEntityBuilder.Property(x => x.LastName).IsRequired(); + userEntityBuilder.Property(x => x.Email).IsRequired().IsEncrypted(); + userEntityBuilder.Property(x => x.Notes).IsRequired().HasColumnType("BLOB").IsEncrypted(StorageFormat.Binary); + userEntityBuilder.Property(x => x.EncryptedData).IsRequired().IsEncrypted(); + userEntityBuilder.Property(x => x.EncryptedDataAsString).IsRequired().HasColumnType("TEXT").IsEncrypted(StorageFormat.Base64); + + if (_encryptionProvider is not null) + { + modelBuilder.UseEncryption(_encryptionProvider); + } + + base.OnModelCreating(modelBuilder); + } +} diff --git a/samples/AesSample.Fluent/EncryptedDatabaseContext.cs b/samples/AesSample.Fluent/EncryptedDatabaseContext.cs new file mode 100644 index 0000000..03520a1 --- /dev/null +++ b/samples/AesSample.Fluent/EncryptedDatabaseContext.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; + +namespace AesSample.Fluent; + +public class EncryptedDatabaseContext : DatabaseContext +{ + public EncryptedDatabaseContext(DbContextOptions options) + : base(options, null) + { + } +} \ No newline at end of file diff --git a/samples/AesSample.Fluent/Program.cs b/samples/AesSample.Fluent/Program.cs new file mode 100644 index 0000000..539d827 --- /dev/null +++ b/samples/AesSample.Fluent/Program.cs @@ -0,0 +1,60 @@ +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore.DataEncryption.Providers; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; + +namespace AesSample.Fluent; + +internal class Program +{ + static void Main(string[] args) + { + using SqliteConnection connection = new("DataSource=:memory:"); + connection.Open(); + + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; + + // AES key randomly generated at each run. + AesKeyInfo keyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + byte[] encryptionKey = keyInfo.Key; + byte[] encryptionIV = keyInfo.IV; + var encryptionProvider = new AesProvider(encryptionKey, encryptionIV); + + using (var context = new DatabaseContext(options, encryptionProvider)) + { + context.Database.EnsureCreated(); + + var user = new UserEntity + { + FirstName = "John", + LastName = "Doe", + Email = "john@doe.com", + Notes = "Hello world!", + EncryptedData = new byte[2] { 1, 2 }, + EncryptedDataAsString = new byte[2] { 3, 4 } + }; + + context.Users.Add(user); + context.SaveChanges(); + + Console.WriteLine($"Users count: {context.Users.Count()}"); + } + + using (var context = new EncryptedDatabaseContext(options)) + { + UserEntity user = context.Users.First(); + + Console.WriteLine($"Encrypted User: {user.FirstName} {user.LastName} - {user.Email} (Notes: {user.Notes})"); + } + + using (var context = new DatabaseContext(options, encryptionProvider)) + { + UserEntity user = context.Users.First(); + + Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email} (Notes: {user.Notes})"); + } + } +} diff --git a/samples/AesSample.Fluent/UserEntity.cs b/samples/AesSample.Fluent/UserEntity.cs new file mode 100644 index 0000000..2e22cd6 --- /dev/null +++ b/samples/AesSample.Fluent/UserEntity.cs @@ -0,0 +1,20 @@ +using System; + +namespace AesSample.Fluent; + +public class UserEntity +{ + public Guid Id { get; set; } + + public string FirstName { get; set; } + + public string LastName { get; set; } + + public string Email { get; set; } + + public string Notes { get; set; } + + public byte[] EncryptedData { get; set; } + + public byte[] EncryptedDataAsString { get; set; } +} diff --git a/samples/AesSample/AesSample.csproj b/samples/AesSample/AesSample.csproj index aa33ab0..4a89054 100644 --- a/samples/AesSample/AesSample.csproj +++ b/samples/AesSample/AesSample.csproj @@ -1,23 +1,19 @@ - - Exe - net5.0;net6.0 - + + Exe + net6.0 + 10 + - - - - - - - - - + + + + - - - + + + diff --git a/samples/AesSample/DatabaseContext.cs b/samples/AesSample/DatabaseContext.cs index 029b961..0bd48cb 100644 --- a/samples/AesSample/DatabaseContext.cs +++ b/samples/AesSample/DatabaseContext.cs @@ -1,25 +1,24 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.DataEncryption; -namespace AesSample +namespace AesSample; + +public class DatabaseContext : DbContext { - public class DatabaseContext : DbContext - { - private readonly IEncryptionProvider _encryptionProvider; + private readonly IEncryptionProvider _encryptionProvider; - public DbSet Users { get; set; } + public DbSet Users { get; set; } - public DatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider) - : base(options) - { - _encryptionProvider = encryptionProvider; - } + public DatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider) + : base(options) + { + _encryptionProvider = encryptionProvider; + } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.UseEncryption(_encryptionProvider); + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.UseEncryption(_encryptionProvider); - base.OnModelCreating(modelBuilder); - } + base.OnModelCreating(modelBuilder); } } diff --git a/samples/AesSample/Program.cs b/samples/AesSample/Program.cs index c39313d..a227d2d 100644 --- a/samples/AesSample/Program.cs +++ b/samples/AesSample/Program.cs @@ -1,56 +1,53 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.Data.Sqlite; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.DataEncryption.Providers; using System; using System.Linq; -using System.Security; -namespace AesSample +namespace AesSample; + +static class Program { - static class Program + static void Main() { - static void Main() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "MyInMemoryDatabase") - .Options; + using SqliteConnection connection = new("DataSource=:memory:"); + connection.Open(); - // AES key randomly generated at each run. - byte[] encryptionKey = AesProvider.GenerateKey(AesKeySize.AES256Bits).Key; - var encryptionProvider = new AesProvider(encryptionKey); + var options = new DbContextOptionsBuilder() + .UseSqlite(connection) + .Options; - using var context = new DatabaseContext(options, encryptionProvider); + // AES key randomly generated at each run. + AesKeyInfo keyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + byte[] encryptionKey = keyInfo.Key; + byte[] encryptionIV = keyInfo.IV; + var encryptionProvider = new AesProvider(encryptionKey, encryptionIV); + + using (var context = new DatabaseContext(options, encryptionProvider)) + { + context.Database.EnsureCreated(); var user = new UserEntity { FirstName = "John", LastName = "Doe", Email = "john@doe.com", - Password = BuildPassword(), + Notes = "Hello world!", + EncryptedData = new byte[2] { 1, 2 }, + EncryptedDataAsString = new byte[2] { 3, 4 } }; context.Users.Add(user); context.SaveChanges(); Console.WriteLine($"Users count: {context.Users.Count()}"); - - user = context.Users.First(); - - Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email} ({user.Password.Length})"); } - static SecureString BuildPassword() + using (var context = new DatabaseContext(options, encryptionProvider)) { - SecureString result = new(); - result.AppendChar('L'); - result.AppendChar('e'); - result.AppendChar('t'); - result.AppendChar('M'); - result.AppendChar('e'); - result.AppendChar('I'); - result.AppendChar('n'); - result.AppendChar('!'); - result.MakeReadOnly(); - return result; + UserEntity user = context.Users.First(); + + Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email} (Notes: {user.Notes})"); } } } diff --git a/samples/AesSample/UserEntity.cs b/samples/AesSample/UserEntity.cs index 671890b..0a9ee26 100644 --- a/samples/AesSample/UserEntity.cs +++ b/samples/AesSample/UserEntity.cs @@ -1,26 +1,35 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Security; -namespace AesSample +namespace AesSample; + +public class UserEntity { - public class UserEntity - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public Guid Id { get; set; } + + [Required] + public string FirstName { get; set; } + + [Required] + public string LastName { get; set; } - [Required] - public string FirstName { get; set; } + [Required] + [Encrypted] + public string Email { get; set; } - [Required] - public string LastName { get; set; } + [Required] + [Encrypted(StorageFormat.Binary)] + public string Notes { get; set; } - [Required] - [Encrypted] - public string Email { get; set; } + [Required] + [Encrypted] + public byte[] EncryptedData { get; set; } - public SecureString Password { get; set; } - } + [Required] + [Encrypted(StorageFormat.Base64)] + [Column(TypeName = "TEXT")] + public byte[] EncryptedDataAsString { get; set; } } diff --git a/src/EntityFrameworkCore.DataEncryption/Attributes/EncryptedAttribute.cs b/src/EntityFrameworkCore.DataEncryption/Attributes/EncryptedAttribute.cs index c76c5cf..2e03135 100644 --- a/src/EntityFrameworkCore.DataEncryption/Attributes/EncryptedAttribute.cs +++ b/src/EntityFrameworkCore.DataEncryption/Attributes/EncryptedAttribute.cs @@ -1,32 +1,32 @@ -namespace System.ComponentModel.DataAnnotations +namespace System.ComponentModel.DataAnnotations; + +/// +/// Specifies that the data field value should be encrypted. +/// +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] +public sealed class EncryptedAttribute : Attribute { /// - /// Specifies that the data field value should be encrypted. + /// Returns the storage format for the database value. /// - [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)] - public sealed class EncryptedAttribute : Attribute - { - /// - /// Initializes a new instance of the class. - /// - /// - /// The storage format. - /// - public EncryptedAttribute(StorageFormat format) - { - Format = format; - } + public StorageFormat Format { get; } - /// - /// Initializes a new instance of the class. - /// - public EncryptedAttribute() : this(StorageFormat.Default) - { - } + /// + /// Initializes a new instance of the class. + /// + /// + /// The storage format. + /// + public EncryptedAttribute(StorageFormat format) + { + Format = format; + } - /// - /// Returns the storage format for the database value. - /// - public StorageFormat Format { get; } + /// + /// Initializes a new instance of the class. + /// + public EncryptedAttribute() + : this(StorageFormat.Default) + { } } \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Attributes/StorageFormat.cs b/src/EntityFrameworkCore.DataEncryption/Attributes/StorageFormat.cs index f948d12..e83f2aa 100644 --- a/src/EntityFrameworkCore.DataEncryption/Attributes/StorageFormat.cs +++ b/src/EntityFrameworkCore.DataEncryption/Attributes/StorageFormat.cs @@ -1,26 +1,27 @@ -namespace System.ComponentModel.DataAnnotations +namespace System.ComponentModel.DataAnnotations; + +/// +/// Represents the storage format for an encrypted value. +/// +public enum StorageFormat { /// - /// Represents the storage format for an encrypted value. + /// The format is determined by the model data type. /// - public enum StorageFormat - { - /// - /// The format is determined by the model data type. - /// - Default, - /// - /// The value is stored in binary. - /// - Binary, - /// - /// The value is stored in a Base64-encoded string. - /// - /// - /// NB: If the source property is a , - /// and no encryption provider is configured, - /// the string will not be modified. - /// - Base64, - } + Default, + + /// + /// The value is stored in binary. + /// + Binary, + + /// + /// The value is stored in a Base64-encoded string. + /// + /// + /// NB: If the source property is a , + /// and no encryption provider is configured, + /// the string will not be modified. + /// + Base64, } \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj index 95d89c6..759f3b9 100644 --- a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj +++ b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj @@ -1,13 +1,13 @@  - netstandard2.0;net6.0;net5.0 - 9.0 + netstandard2.0;net6.0;net7.0 + 10.0 true EntityFrameworkCore.DataEncryption Microsoft.EntityFrameworkCore.DataEncryption - true - 3.0.1 + true + 4.0.0 Filipe GOMES PEIXOTO EntityFrameworkCore.DataEncryption https://github.com/Eastrall/EntityFrameworkCore.DataEncryption @@ -17,15 +17,15 @@ true entity-framework-core, extensions, dotnet-core, dotnet, encryption, fluent-api icon.png - Filipe GOMES PEIXOTO © 2019 - 2021 + Filipe GOMES PEIXOTO © 2019 - 2023 A plugin for Microsoft.EntityFrameworkCore to add support of encrypted fields using built-in or custom encryption providers. LICENSE - Add support for .NET 5 and .NET 6 + https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/releases/tag/v4.0.0 README.md - Auto + Auto @@ -34,24 +34,21 @@ - - - - - - - + - + + + + True - True - \ + True + \ @@ -62,4 +59,8 @@ + + + + diff --git a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj.DotSettings b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj.DotSettings deleted file mode 100644 index 6162834..0000000 --- a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj.DotSettings +++ /dev/null @@ -1,2 +0,0 @@ - - CSharp90 \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/IEncryptionProvider.cs b/src/EntityFrameworkCore.DataEncryption/IEncryptionProvider.cs index 49a8c6d..77c9eb9 100644 --- a/src/EntityFrameworkCore.DataEncryption/IEncryptionProvider.cs +++ b/src/EntityFrameworkCore.DataEncryption/IEncryptionProvider.cs @@ -1,67 +1,21 @@ -using System; -using System.IO; +namespace Microsoft.EntityFrameworkCore.DataEncryption; -namespace Microsoft.EntityFrameworkCore.DataEncryption +/// +/// Provides a mechanism to encrypt and decrypt data. +/// +public interface IEncryptionProvider { /// - /// Provides a mechanism for implementing a custom encryption provider. + /// Encrypts the given input byte array. /// - public interface IEncryptionProvider - { - /// - /// Encrypts a value. - /// - /// - /// The type of data stored in the database. - /// - /// - /// The type of value stored in the model. - /// - /// - /// Input data to encrypt. - /// - /// - /// Function which converts the model value to a byte array. - /// - /// - /// Function which encodes the value for storing the the database. - /// - /// - /// Encrypted data. - /// - /// - /// is . - /// -or- - /// is . - /// - TStore Encrypt(TModel dataToEncrypt, Func converter, Func encoder); + /// Input to encrypt. + /// Encrypted input. + byte[] Encrypt(byte[] input); - /// - /// Decrypts a value. - /// - /// - /// The type of data stored in the database. - /// - /// - /// The type of value stored in the model. - /// - /// - /// Encrypted data to decrypt. - /// - /// - /// Function which converts the stored data to a byte array. - /// - /// - /// Function which converts the decrypted to the return value. - /// - /// - /// Decrypted data. - /// - /// - /// is . - /// -or- - /// is . - /// - TModel Decrypt(TStore dataToDecrypt, Func decoder, Func converter); - } + /// + /// Decrypts the given input byte array. + /// + /// Input to decrypt. + /// Decrypted input. + byte[] Decrypt(byte[] input); } diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder.cs b/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder.cs deleted file mode 100644 index 28144e6..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder.cs +++ /dev/null @@ -1,206 +0,0 @@ -using System; -using System.IO; -using System.Security; -using System.Text; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal -{ - /// - /// Utilities for building value converters. - /// - public static class ConverterBuilder - { - /// - /// Builds a converter for a property with a custom model type. - /// - /// - /// The model type. - /// - /// - /// The , if any. - /// - /// - /// The function used to decode the model type to a byte array. - /// - /// - /// The function used to encode a byte array to the model type. - /// - /// - /// An instance. - /// - /// - /// is . - /// -or- - /// is . - /// - public static ConverterBuilder From( - this IEncryptionProvider encryptionProvider, - Func decoder, - Func encoder) - { - if (decoder is null) - { - throw new ArgumentNullException(nameof(decoder)); - } - - if (encoder is null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - return new ConverterBuilder(encryptionProvider, decoder, encoder); - } - - /// - /// Builds a converter for a binary property. - /// - /// - /// The , if any. - /// - /// - /// An instance. - /// - public static ConverterBuilder FromBinary(this IEncryptionProvider encryptionProvider) - { - return new ConverterBuilder(encryptionProvider, b => b, StandardConverters.StreamToBytes); - } - - /// - /// Builds a converter for a string property. - /// - /// - /// The , if any. - /// - /// - /// An instance. - /// - public static ConverterBuilder FromString(this IEncryptionProvider encryptionProvider) - { - return new ConverterBuilder(encryptionProvider, Encoding.UTF8.GetBytes, StandardConverters.StreamToString); - } - - /// - /// Builds a converter for a property. - /// - /// - /// The , if any. - /// - /// - /// An instance. - /// - public static ConverterBuilder FromSecureString(this IEncryptionProvider encryptionProvider) - { - return new ConverterBuilder(encryptionProvider, Encoding.UTF8.GetBytes, StandardConverters.StreamToSecureString); - } - - /// - /// Specifies that the property should be stored in the database using a custom format. - /// - /// - /// The model type. - /// - /// - /// The store type. - /// - /// - /// The representing the model type. - /// - /// - /// The function used to decode the store type into a byte array. - /// - /// - /// The function used to encode a byte array into the store type. - /// - /// - /// An instance. - /// - /// - /// is . - /// -or- - /// is . - /// -or- - /// is . - /// - /// - /// is not a supported type. - /// - public static ConverterBuilder To( - ConverterBuilder modelType, - Func decoder, - Func encoder) - { - if (modelType.IsEmpty) - { - throw new ArgumentNullException(nameof(modelType)); - } - - if (decoder is null) - { - throw new ArgumentNullException(nameof(decoder)); - } - - if (encoder is null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - return new ConverterBuilder(modelType, decoder, encoder); - } - - /// - /// Specifies that the property should be stored in the database in binary. - /// - /// - /// The model type. - /// - /// - /// The representing the model type. - /// - /// - /// An instance. - /// - /// - /// is . - /// - /// - /// is not a supported type. - /// - public static ConverterBuilder ToBinary(this ConverterBuilder modelType) - { - if (modelType.IsEmpty) - { - throw new ArgumentNullException(nameof(modelType)); - } - - return new ConverterBuilder(modelType, b => b, StandardConverters.StreamToBytes); - } - - /// - /// Specifies that the property should be stored in the database in a Base64-encoded string. - /// - /// - /// The model type. - /// - /// - /// The representing the model type. - /// - /// - /// An instance. - /// - /// - /// is . - /// - /// - /// is not a supported type. - /// - public static ConverterBuilder ToBase64(this ConverterBuilder modelType) - { - if (modelType.IsEmpty) - { - throw new ArgumentNullException(nameof(modelType)); - } - - return new ConverterBuilder(modelType, Convert.FromBase64String, StandardConverters.StreamToBase64String); - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`1.cs b/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`1.cs deleted file mode 100644 index c0ae461..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`1.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal -{ - /// - /// A converter builder class which has the model type specified. - /// - /// - /// The model type. - /// - public readonly struct ConverterBuilder - { - internal ConverterBuilder(IEncryptionProvider encryptionProvider, Func decoder, Func encoder) - { - Debug.Assert(decoder is not null); - Debug.Assert(encoder is not null); - - EncryptionProvider = encryptionProvider; - Decoder = decoder; - Encoder = encoder; - } - - private readonly IEncryptionProvider EncryptionProvider; - private readonly Func Decoder; - private readonly Func Encoder; - - internal bool IsEmpty => Decoder is null || Encoder is null; - - internal void Deconstruct(out IEncryptionProvider encryptionProvider, out Func decoder, out Func encoder) - { - encryptionProvider = EncryptionProvider; - decoder = Decoder; - encoder = Encoder; - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`2.cs b/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`2.cs deleted file mode 100644 index dddda9f..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`2.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Diagnostics; -using System.IO; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal -{ - /// - /// A converter builder class which has both the model type and store type specified. - /// - /// - /// The model type. - /// - /// - /// The store type. - /// - public readonly struct ConverterBuilder - { - internal ConverterBuilder(ConverterBuilder modelType, Func decoder, Func encoder) - { - Debug.Assert(!modelType.IsEmpty); - Debug.Assert(decoder is not null); - Debug.Assert(encoder is not null); - - ModelType = modelType; - Decoder = decoder; - Encoder = encoder; - } - - private readonly ConverterBuilder ModelType; - private readonly Func Decoder; - private readonly Func Encoder; - - /// - /// Builds the value converter. - /// - /// - /// The mapping hints to use, if any. - /// - /// - /// The . - /// - public ValueConverter Build(ConverterMappingHints mappingHints = null) - { - var (encryptionProvider, modelDecoder, modelEncoder) = ModelType; - var storeDecoder = Decoder; - var storeEncoder = Encoder; - - if (modelDecoder is null || modelEncoder is null || storeDecoder is null || storeEncoder is null) - { - return null; - } - - if (encryptionProvider is null) - { - return new ValueConverter( - m => storeEncoder(StandardConverters.BytesToStream(modelDecoder(m))), - s => modelEncoder(StandardConverters.BytesToStream(storeDecoder(s))), - mappingHints); - } - - return new EncryptionConverter( - encryptionProvider, - m => encryptionProvider.Encrypt(m, modelDecoder, storeEncoder), - s => encryptionProvider.Decrypt(s, storeDecoder, modelEncoder), - mappingHints); - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/EncodingExtensions.cs b/src/EntityFrameworkCore.DataEncryption/Internal/EncodingExtensions.cs deleted file mode 100644 index 7cf1634..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/EncodingExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Security; -using System.Text; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal -{ - internal static class EncodingExtensions - { - internal static byte[] GetBytes(this Encoding encoding, SecureString value) - { - if (encoding is null) - { - throw new ArgumentNullException(nameof(encoding)); - } - - if (value is null || value.Length == 0) - { - return Array.Empty(); - } - - IntPtr valuePtr = IntPtr.Zero; - try - { - valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value); - if (valuePtr == IntPtr.Zero) - { - return Array.Empty(); - } - - unsafe - { - char* chars = (char*)valuePtr; - Debug.Assert(chars != null); - - int byteCount = encoding.GetByteCount(chars, value.Length); - - var result = new byte[byteCount]; - fixed (byte* bytes = result) - { - encoding.GetBytes(chars, value.Length, bytes, byteCount); - } - - return result; - } - } - finally - { - if (valuePtr != IntPtr.Zero) - { - Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); - } - } - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs index 20794f2..57ad58a 100644 --- a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs +++ b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs @@ -1,28 +1,71 @@ -using System; -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System; +using System.ComponentModel.DataAnnotations; +using System.Text; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal +namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal; + +/// +/// Defines the internal encryption converter for string values. +/// +/// +/// +internal sealed class EncryptionConverter : ValueConverter { /// - /// Defines the internal encryption converter for string values. + /// Creates a new instance. /// - internal sealed class EncryptionConverter : ValueConverter, IEncryptionValueConverter + /// Encryption provider to use. + /// Encryption storage format. + /// Mapping hints. + public EncryptionConverter(IEncryptionProvider encryptionProvider, StorageFormat storageFormat, ConverterMappingHints mappingHints = null) + : base( + x => Encrypt(x, encryptionProvider, storageFormat), + x => Decrypt(x, encryptionProvider, storageFormat), + mappingHints) + { + } + + private static TOutput Encrypt(TInput input, IEncryptionProvider encryptionProvider, StorageFormat storageFormat) + { + byte[] inputData = input switch + { + string => Encoding.UTF8.GetBytes(input.ToString()), + byte[] => input as byte[], + _ => null, + }; + + byte[] encryptedRawBytes = encryptionProvider.Encrypt(inputData); + + object encryptedData = storageFormat switch + { + StorageFormat.Default or StorageFormat.Base64 => Convert.ToBase64String(encryptedRawBytes), + _ => encryptedRawBytes + }; + + return (TOutput)Convert.ChangeType(encryptedData, typeof(TOutput)); + } + + private static TModel Decrypt(TProvider input, IEncryptionProvider encryptionProvider, StorageFormat storageFormat) { - /// - /// Creates a new instance. - /// - public EncryptionConverter( - IEncryptionProvider encryptionProvider, - Expression> convertToProviderExpression, - Expression> convertFromProviderExpression, - ConverterMappingHints mappingHints = null) - : base(convertToProviderExpression, convertFromProviderExpression, mappingHints) + Type destinationType = typeof(TModel); + byte[] inputData = storageFormat switch + { + StorageFormat.Default or StorageFormat.Base64 => Convert.FromBase64String(input.ToString()), + _ => input as byte[] + }; + byte[] decryptedRawBytes = encryptionProvider.Decrypt(inputData); + object decryptedData = null; + + if (destinationType == typeof(string)) + { + decryptedData = Encoding.UTF8.GetString(decryptedRawBytes).Trim('\0'); + } + else if (destinationType == typeof(byte[])) { - EncryptionProvider = encryptionProvider; + decryptedData = decryptedRawBytes; } - /// - public IEncryptionProvider EncryptionProvider { get; } + return (TModel)Convert.ChangeType(decryptedData, typeof(TModel)); } } diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs deleted file mode 100644 index f54fa82..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal -{ - /// - /// Interface for an encryption value converter. - /// - public interface IEncryptionValueConverter - { - /// - /// Returns the encryption provider, if any. - /// - /// - /// The for this converter, if any. - /// - IEncryptionProvider EncryptionProvider { get; } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/PropertyAnnotations.cs b/src/EntityFrameworkCore.DataEncryption/Internal/PropertyAnnotations.cs new file mode 100644 index 0000000..37187a9 --- /dev/null +++ b/src/EntityFrameworkCore.DataEncryption/Internal/PropertyAnnotations.cs @@ -0,0 +1,7 @@ +namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal; + +internal class PropertyAnnotations +{ + public const string IsEncrypted = "Microsoft.EntityFrameworkCore.DataEncryption.IsEncrypted"; + public const string StorageFormat = "Microsoft.EntityFrameworkCore.DataEncryption.StorageFormat"; +} diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/StandardConverters.cs b/src/EntityFrameworkCore.DataEncryption/Internal/StandardConverters.cs deleted file mode 100644 index 0a5df4d..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/StandardConverters.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.IO; -using System.Security; -using System.Text; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal -{ - internal static class StandardConverters - { - internal static Stream BytesToStream(byte[] bytes) => new MemoryStream(bytes); - - internal static byte[] StreamToBytes(Stream stream) - { - if (stream is MemoryStream ms) - { - return ms.ToArray(); - } - - using var output = new MemoryStream(); - stream.CopyTo(output); - return output.ToArray(); - } - - internal static string StreamToBase64String(Stream stream) => Convert.ToBase64String(StreamToBytes(stream)); - - internal static string StreamToString(Stream stream) - { - using var reader = new StreamReader(stream, Encoding.UTF8); - return reader.ReadToEnd().Trim('\0'); - } - - internal static SecureString StreamToSecureString(Stream stream) - { - using var reader = new StreamReader(stream, Encoding.UTF8); - - var result = new SecureString(); - var buffer = new char[100]; - while (!reader.EndOfStream) - { - var charsRead = reader.Read(buffer, 0, buffer.Length); - if (charsRead != 0) - { - for (int index = 0; index < charsRead; index++) - { - char c = buffer[index]; - if (c != '\0') - { - result.AppendChar(c); - } - } - } - } - - return result; - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Migration/EncryptionMigrator.cs b/src/EntityFrameworkCore.DataEncryption/Migration/EncryptionMigrator.cs deleted file mode 100644 index 333de7d..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Migration/EncryptionMigrator.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.DataEncryption.Internal; -using Microsoft.EntityFrameworkCore.DataEncryption.Providers; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.Extensions.Logging; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Migration -{ - /// - /// Utilities for migrating encrypted data from one provider to another. - /// - /// - /// To migrate from v1 to v2 of : - /// - /// var sourceProvider = new AesProvider(key, iv); - /// var destinationProvider = new AesProvider(key); - /// var migrationProvider = new MigrationEncryptionProvider(sourceProvider, destinationProvider); - /// await using var migrationContext = new DatabaseContext(options, migrationProvider); - /// await migrationContext.MigrateAsync(logger, cancellationToken); - /// - /// - public static class EncryptionMigrator - { - private static readonly MethodInfo SetMethod = typeof(DbContext).GetMethod(nameof(DbContext.Set)); - - private static IQueryable Set(this DbContext context, IEntityType entityType) - { - var method = SetMethod.MakeGenericMethod(entityType.ClrType); - var result = method.Invoke(context, null); - return (IQueryable)result; - } - - /// - /// Migrates the data for a single property to a new encryption provider. - /// - /// - /// The . - /// - /// - /// The to migrate. - /// - /// - /// The to use, if any. - /// - /// - /// The to use, if any. - /// - /// - /// is . - /// -or- - /// is . - /// - public static async Task MigrateAsync(this DbContext context, IProperty property, ILogger logger = default, CancellationToken cancellationToken = default) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } - - if (property.GetValueConverter() is not IEncryptionValueConverter converter) - { - logger?.LogWarning("Property {Property} on entity type {EntityType} is not using an encryption value converter. ({Converter})", - property.Name, property.DeclaringEntityType.Name, property.GetValueConverter()); - - return; - } - - if (converter.EncryptionProvider is not MigrationEncryptionProvider { IsEmpty: false }) - { - logger?.LogWarning("Property {Property} on entity type {EntityType} is not using a non-empty migration encryption value converter. ({EncryptionProvider})", - property.Name, property.DeclaringEntityType.Name, converter.EncryptionProvider); - - return; - } - - logger?.LogInformation("Loading data for {EntityType} ({Property})...", - property.DeclaringEntityType.Name, property.Name); - - var set = context.Set(property.DeclaringEntityType); - var list = await set.ToListAsync(cancellationToken); - - logger?.LogInformation("Migrating data for {EntityType} :: {Property} ({RecordCount} records)...", - property.DeclaringEntityType.Name, property.Name, list.Count); - - foreach (var entity in list) - { - context.Entry(entity).Property(property.Name).IsModified = true; - } - - await context.SaveChangesAsync(cancellationToken); - } - - private static async ValueTask MigrateAsyncCore(DbContext context, IEntityType entityType, ILogger logger = default, CancellationToken cancellationToken = default) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (entityType is null) - { - throw new ArgumentNullException(nameof(entityType)); - } - - 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(); - - if (encryptedProperties.Count == 0) - { - logger?.LogDebug("Entity type {EntityType} has no encrypted properties.", entityType.Name); - return; - } - - logger?.LogInformation("Loading data for {EntityType} ({PropertyCount} properties)...", entityType.Name, encryptedProperties.Count); - - var set = context.Set(entityType); - var list = await set.ToListAsync(cancellationToken); - logger?.LogInformation("Migrating data for {EntityType} ({RecordCount} records)...", entityType.Name, list.Count); - - foreach (var entity in list) - { - var entry = context.Entry(entity); - foreach (var property in encryptedProperties) - { - entry.Property(property.Name).IsModified = true; - } - } - } - - /// - /// Migrates the encrypted data for a single entity type to a new encryption provider. - /// - /// - /// The . - /// - /// - /// The to migrate. - /// - /// - /// The to use, if any. - /// - /// - /// The to use, if any. - /// - /// - /// is . - /// -or- - /// is . - /// - public static async Task MigrateAsync(this DbContext context, IEntityType entityType, ILogger logger = default, CancellationToken cancellationToken = default) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (entityType is null) - { - throw new ArgumentNullException(nameof(entityType)); - } - - await MigrateAsyncCore(context, entityType, logger, cancellationToken); - await context.SaveChangesAsync(cancellationToken); - } - - /// - /// Migrates the encrypted data for the entire context to a new encryption provider. - /// - /// - /// The . - /// - /// - /// The to use, if any. - /// - /// - /// The to use, if any. - /// - /// - /// is . - /// - public static async Task MigrateAsync(this DbContext context, ILogger logger = default, CancellationToken cancellationToken = default) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - foreach (var entityType in context.Model.GetEntityTypes()) - { - await MigrateAsyncCore(context, entityType, logger, cancellationToken); - } - - await context.SaveChangesAsync(cancellationToken); - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Migration/MigrationEncryptionProvider.cs b/src/EntityFrameworkCore.DataEncryption/Migration/MigrationEncryptionProvider.cs deleted file mode 100644 index e3e62cf..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Migration/MigrationEncryptionProvider.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System; -using System.IO; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Migration -{ - /// - /// An encryption provided used for migrating from one encryption scheme to another. - /// - public class MigrationEncryptionProvider : IEncryptionProvider - { - /// - /// Initializes a new instance of the class. - /// - /// The source encryption provider. - /// The destination encryption provider. - public MigrationEncryptionProvider( - IEncryptionProvider sourceEncryptionProvider, - IEncryptionProvider destinationEncryptionProvider) - { - SourceEncryptionProvider = sourceEncryptionProvider; - DestinationEncryptionProvider = destinationEncryptionProvider; - } - - /// - /// Returns the original encryption provider, if any. - /// - /// - /// The original , if any. - /// - public IEncryptionProvider SourceEncryptionProvider { get; } - - /// - /// Returns the new encryption provider, if any. - /// - /// - /// The new , if any. - /// - public IEncryptionProvider DestinationEncryptionProvider { get; } - - /// - /// Returns a flag indicating whether this provider is empty. - /// - /// - /// if this provider is empty; - /// otherwise, . - /// - public bool IsEmpty => SourceEncryptionProvider is null && DestinationEncryptionProvider is null; - - /// - public TModel Decrypt(TStore dataToDecrypt, Func decoder, Func converter) - { - if (decoder is null) - { - throw new ArgumentNullException(nameof(decoder)); - } - - if (converter is null) - { - throw new ArgumentNullException(nameof(converter)); - } - - if (SourceEncryptionProvider is not null) - { - return SourceEncryptionProvider.Decrypt(dataToDecrypt, decoder, converter); - } - - byte[] data = decoder(dataToDecrypt); - if (data is null || data.Length == 0) - { - return default; - } - - using var ms = new MemoryStream(data); - return converter(ms); - } - - /// - public TStore Encrypt(TModel dataToEncrypt, Func converter, Func encoder) - { - if (converter is null) - { - throw new ArgumentNullException(nameof(converter)); - } - - if (encoder is null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - if (DestinationEncryptionProvider is not null) - { - return DestinationEncryptionProvider.Encrypt(dataToEncrypt, converter, encoder); - } - - byte[] data = converter(dataToEncrypt); - if (data is null || data.Length == 0) - { - return default; - } - - using var ms = new MemoryStream(data); - return encoder(ms); - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs index f2ffb30..9f8568f 100644 --- a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs @@ -1,175 +1,132 @@ using Microsoft.EntityFrameworkCore.DataEncryption.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; +using System.Linq; using System.Reflection; -using System.Security; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Microsoft.EntityFrameworkCore.DataEncryption +namespace Microsoft.EntityFrameworkCore.DataEncryption; + +/// +/// Provides extensions for the . +/// +public static class ModelBuilderExtensions { /// - /// Provides extensions for the . + /// Enables encryption on this model using an encryption provider. /// - public static class ModelBuilderExtensions + /// + /// The instance. + /// + /// + /// The to use, if any. + /// + /// + /// The updated . + /// + public static ModelBuilder UseEncryption(this ModelBuilder modelBuilder, IEncryptionProvider encryptionProvider) { - /// - /// Enables encryption on this model using an encryption provider. - /// - /// - /// The instance. - /// - /// - /// The to use, if any. - /// - /// - /// The updated . - /// - /// - /// is . - /// - public static ModelBuilder UseEncryption(this ModelBuilder modelBuilder, IEncryptionProvider encryptionProvider) + if (modelBuilder is null) { - if (modelBuilder is null) - { - throw new ArgumentNullException(nameof(modelBuilder)); - } + throw new ArgumentNullException(nameof(modelBuilder)); + } - ValueConverter binaryToBinary = null, binaryToString = null; - ValueConverter stringToBinary = null, stringToString = null; - ValueConverter secureStringToBinary = null, secureStringToString = null; - var secureStringProperties = new List<(Type entityType, string propertyName)>(); + if (encryptionProvider is null) + { + throw new ArgumentNullException(nameof(encryptionProvider)); + } - foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + { + IEnumerable encryptedProperties = GetEntityEncryptedProperties(entityType); + + foreach (EncryptedProperty encryptedProperty in encryptedProperties) { - foreach (IMutableProperty property in entityType.GetProperties()) +#pragma warning disable EF1001 // Internal EF Core API usage. + if (encryptedProperty.Property.FindAnnotation(CoreAnnotationNames.ValueConverter) is not null) { - var (shouldEncrypt, format) = property.ShouldEncrypt(); - if (!shouldEncrypt) - { - continue; - } - - if (property.ClrType == typeof(byte[])) - { - switch (format) - { - case StorageFormat.Base64: - { - binaryToString ??= encryptionProvider.FromBinary().ToBase64().Build(); - property.SetValueConverter(binaryToString); - break; - } - case StorageFormat.Binary: - case StorageFormat.Default: - { - if (encryptionProvider is not null) - { - binaryToBinary ??= encryptionProvider.FromBinary().ToBinary().Build(); - property.SetValueConverter(binaryToBinary); - } - break; - } - default: - { - throw new NotSupportedException($"Storage format {format} is not supported."); - } - } - } - else if (property.ClrType == typeof(string)) - { - switch (format) - { - case StorageFormat.Binary: - { - stringToBinary ??= encryptionProvider.FromString().ToBinary().Build(); - property.SetValueConverter(stringToBinary); - break; - } - case StorageFormat.Base64: - case StorageFormat.Default: - { - if (encryptionProvider is not null) - { - stringToString ??= encryptionProvider.FromString().ToBase64().Build(); - property.SetValueConverter(stringToString); - } - break; - } - default: - { - throw new NotSupportedException($"Storage format {format} is not supported."); - } - } - } - else if (property.ClrType == typeof(SecureString)) - { - switch (format) - { - case StorageFormat.Base64: - { - secureStringToString ??= encryptionProvider.FromSecureString().ToBase64().Build(); - property.SetValueConverter(secureStringToString); - break; - } - case StorageFormat.Binary: - case StorageFormat.Default: - { - secureStringToBinary ??= encryptionProvider.FromSecureString().ToBinary().Build(); - property.SetValueConverter(secureStringToBinary); - break; - } - default: - { - throw new NotSupportedException($"Storage format {format} is not supported."); - } - } - } + continue; } +#pragma warning restore EF1001 // Internal EF Core API usage. + + ValueConverter converter = GetValueConverter(encryptedProperty.Property.ClrType, encryptionProvider, encryptedProperty.StorageFormat); - // By default, SecureString properties are created as navigation properties, and need to be reconfigured: - foreach (var navigation in entityType.GetNavigations()) + if (converter != null) { - if (navigation.ClrType == typeof(SecureString)) - { - secureStringProperties.Add((entityType.ClrType, navigation.Name)); - } + encryptedProperty.Property.SetValueConverter(converter); } } + } + + return modelBuilder; + } - if (secureStringProperties.Count != 0) + private static ValueConverter GetValueConverter(Type propertyType, IEncryptionProvider encryptionProvider, StorageFormat storageFormat) + { + if (propertyType == typeof(string)) + { + return storageFormat switch { - foreach (var (entityType, propertyName) in secureStringProperties) - { - var property = modelBuilder.Entity(entityType).Property(propertyName); - var attribute = property.Metadata.PropertyInfo?.GetCustomAttribute(false); - var format = attribute?.Format ?? StorageFormat.Default; - - switch (format) - { - case StorageFormat.Base64: - { - secureStringToString ??= encryptionProvider.FromSecureString().ToBase64().Build(); - property.HasConversion(secureStringToString); - break; - } - case StorageFormat.Binary: - case StorageFormat.Default: - { - secureStringToBinary ??= encryptionProvider.FromSecureString().ToBinary().Build(); - property.HasConversion(secureStringToBinary); - break; - } - default: - { - throw new NotSupportedException($"Storage format {format} is not supported."); - } - } - } + StorageFormat.Default or StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, StorageFormat.Base64), + StorageFormat.Binary => new EncryptionConverter(encryptionProvider, StorageFormat.Binary), + _ => throw new NotImplementedException() + }; + } + else if (propertyType == typeof(byte[])) + { + return storageFormat switch + { + StorageFormat.Default or StorageFormat.Binary => new EncryptionConverter(encryptionProvider, StorageFormat.Binary), + StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, StorageFormat.Base64), + _ => throw new NotImplementedException() + }; + } + + throw new NotImplementedException($"Type {propertyType.Name} does not support encryption."); + } + + private static IEnumerable GetEntityEncryptedProperties(IMutableEntityType entity) + { + return entity.GetProperties() + .Select(x => EncryptedProperty.Create(x)) + .Where(x => x is not null); + } + + internal class EncryptedProperty + { + public IMutableProperty Property { get; } + + public StorageFormat StorageFormat { get; } + + private EncryptedProperty(IMutableProperty property, StorageFormat storageFormat) + { + Property = property; + StorageFormat = storageFormat; + } + + public static EncryptedProperty Create(IMutableProperty property) + { + StorageFormat? storageFormat = null; + + var encryptedAttribute = property.PropertyInfo?.GetCustomAttribute(false); + + if (encryptedAttribute != null) + { + storageFormat = encryptedAttribute.Format; + } + + IAnnotation encryptedAnnotation = property.FindAnnotation(PropertyAnnotations.IsEncrypted); + + if (encryptedAnnotation != null && (bool)encryptedAnnotation.Value == true) + { + storageFormat = (StorageFormat)property.FindAnnotation(PropertyAnnotations.StorageFormat)?.Value; } - return modelBuilder; + return storageFormat.HasValue ? new EncryptedProperty(property, storageFormat.Value) : null; } } } diff --git a/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs deleted file mode 100644 index 8417479..0000000 --- a/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs +++ /dev/null @@ -1,107 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; -using System.Linq; -using System.Reflection; -using System.Security; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Metadata.Internal; - -namespace Microsoft.EntityFrameworkCore.DataEncryption -{ - /// - /// Extension methods for EF models. - /// - public static class ModelExtensions - { - /// - /// Returns a value indicating whether the specified property should be encrypted. - /// - /// - /// The . - /// - /// - /// A value indicating whether the specified property should be encrypted, - /// and how the encrypted value should be stored. - /// - /// - /// is . - /// - public static (bool shouldEncrypt, StorageFormat format) ShouldEncrypt(this IProperty property) - { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } - - var attribute = property.PropertyInfo?.GetCustomAttribute(false); - if (property.ClrType == typeof(SecureString)) - { - return (true, attribute?.Format ?? StorageFormat.Binary); - } - - return attribute is null ? (false, StorageFormat.Default) : (true, attribute.Format); - } - - /// - /// Returns a value indicating whether the specified property should be encrypted. - /// - /// - /// The . - /// - /// - /// A value indicating whether the specified property should be encrypted, - /// and how the encrypted value should be stored. - /// - /// - /// is . - /// - public static (bool shouldEncrypt, StorageFormat format) ShouldEncrypt(this IMutableProperty property) - { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } - -#pragma warning disable EF1001 // Internal EF Core API usage. - if (property.FindAnnotation(CoreAnnotationNames.ValueConverter) is not null) - { - return (false, StorageFormat.Default); - } -#pragma warning restore EF1001 // Internal EF Core API usage. - - var attribute = property.PropertyInfo?.GetCustomAttribute(false); - if (property.ClrType == typeof(SecureString)) - { - return (true, attribute?.Format ?? StorageFormat.Binary); - } - - return attribute is null ? (false, StorageFormat.Default) : (true, attribute.Format); - } - - /// - /// Returns the list of encrypted properties for the specified entity type. - /// - /// - /// The . - /// - /// - /// A list of the properties for the specified type which should be encrypted. - /// - /// - /// is . - /// - public static IReadOnlyList<(IProperty property, StorageFormat format)> ListEncryptedProperties(this IEntityType entityType) - { - if (entityType is null) - { - throw new ArgumentNullException(nameof(entityType)); - } - - return entityType.GetProperties() - .Select(p => (property: p, flag: p.ShouldEncrypt())) - .Where(p => p.flag.shouldEncrypt) - .Select(p => (p.property, p.flag.format)).ToList(); - } - } -} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs b/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs index 79277fd..17276ac 100644 --- a/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs @@ -1,147 +1,25 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Security; -using Microsoft.EntityFrameworkCore.DataEncryption.Internal; +using Microsoft.EntityFrameworkCore.DataEncryption.Internal; using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.EntityFrameworkCore.DataEncryption; -namespace Microsoft.EntityFrameworkCore.DataEncryption +/// +/// Provides extensions for the type. +/// +public static class PropertyBuilderExtensions { - /// - /// Provides extensions for the . - /// - public static class PropertyBuilderExtensions + public static PropertyBuilder IsEncrypted(this PropertyBuilder builder, StorageFormat storageFormat = StorageFormat.Default) { - /// - /// Configures the property as capable of storing encrypted data. - /// - /// - /// The . - /// - /// - /// The to use, if any. - /// - /// - /// One of the values indicating how the value should be stored in the database. - /// - /// - /// The to use, if any. - /// - /// - /// The updated . - /// - /// - /// is . - /// - /// - /// is not a recognised value. - /// - public static PropertyBuilder IsEncrypted( - this PropertyBuilder property, - IEncryptionProvider encryptionProvider, - StorageFormat format = StorageFormat.Default, - ConverterMappingHints mappingHints = null) - { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } - - return format switch - { - StorageFormat.Default => encryptionProvider is null ? property : property.HasConversion(encryptionProvider.FromBinary().ToBinary().Build(mappingHints)), - StorageFormat.Binary => encryptionProvider is null ? property : property.HasConversion(encryptionProvider.FromBinary().ToBinary().Build(mappingHints)), - StorageFormat.Base64 => property.HasConversion(encryptionProvider.FromBinary().ToBase64().Build(mappingHints)), - _ => throw new ArgumentOutOfRangeException(nameof(format)), - }; - } - - /// - /// Configures the property as capable of storing encrypted data. - /// - /// - /// The . - /// - /// - /// The to use, if any. - /// - /// - /// One of the values indicating how the value should be stored in the database. - /// - /// - /// The to use, if any. - /// - /// - /// The updated . - /// - /// - /// is . - /// - /// - /// is not a recognised value. - /// - public static PropertyBuilder IsEncrypted( - this PropertyBuilder property, - IEncryptionProvider encryptionProvider, - StorageFormat format = StorageFormat.Default, - ConverterMappingHints mappingHints = null) + if (builder is null) { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } - - return format switch - { - StorageFormat.Default => encryptionProvider is null ? property : property.HasConversion(encryptionProvider.FromString().ToBase64().Build(mappingHints)), - StorageFormat.Base64 => encryptionProvider is null ? property : property.HasConversion(encryptionProvider.FromString().ToBase64().Build(mappingHints)), - StorageFormat.Binary => property.HasConversion(encryptionProvider.FromString().ToBinary().Build(mappingHints)), - _ => throw new ArgumentOutOfRangeException(nameof(format)), - }; + throw new ArgumentNullException(nameof(builder)); } - /// - /// Configures the property as capable of storing encrypted data. - /// - /// - /// The . - /// - /// - /// The to use, if any. - /// - /// - /// One of the values indicating how the value should be stored in the database. - /// - /// - /// The to use, if any. - /// - /// - /// The updated . - /// - /// - /// is . - /// - /// - /// is not a recognised value. - /// - public static PropertyBuilder IsEncrypted( - this PropertyBuilder property, - IEncryptionProvider encryptionProvider, - StorageFormat format = StorageFormat.Default, - ConverterMappingHints mappingHints = null) - { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } + builder.HasAnnotation(PropertyAnnotations.IsEncrypted, true); + builder.HasAnnotation(PropertyAnnotations.StorageFormat, storageFormat); - return format switch - { - StorageFormat.Default => property.HasConversion(encryptionProvider.FromSecureString().ToBinary().Build(mappingHints)), - StorageFormat.Binary => property.HasConversion(encryptionProvider.FromSecureString().ToBinary().Build(mappingHints)), - StorageFormat.Base64 => property.HasConversion(encryptionProvider.FromSecureString().ToBase64().Build(mappingHints)), - _ => throw new ArgumentOutOfRangeException(nameof(format)), - }; - } + return builder; } -} \ No newline at end of file +} diff --git a/src/EntityFrameworkCore.DataEncryption/Providers/AesKeyInfo.cs b/src/EntityFrameworkCore.DataEncryption/Providers/AesKeyInfo.cs index 7bb4ca5..45adcfe 100644 --- a/src/EntityFrameworkCore.DataEncryption/Providers/AesKeyInfo.cs +++ b/src/EntityFrameworkCore.DataEncryption/Providers/AesKeyInfo.cs @@ -1,67 +1,66 @@ using System; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers +namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers; + +/// +/// Defines an AES key info structure containing a Key and Initialization Vector used for the AES encryption algorithm. +/// +public readonly struct AesKeyInfo : IEquatable { /// - /// Defines an AES key info structure containing a Key and Initialization Vector used for the AES encryption algorithm. + /// Gets the AES key. /// - public readonly struct AesKeyInfo : IEquatable - { - /// - /// Gets the AES key. - /// - public byte[] Key { get; } + public byte[] Key { get; } - /// - /// Gets the AES initialization vector. - /// - public byte[] IV { get; } + /// + /// Gets the AES initialization vector. + /// + public byte[] IV { get; } - /// - /// Creates a new . - /// - /// AES key. - /// AES initialization vector. - internal AesKeyInfo(byte[] key, byte[] iv) - { - Key = key; - IV = iv; - } + /// + /// Creates a new . + /// + /// AES key. + /// AES initialization vector. + internal AesKeyInfo(byte[] key, byte[] iv) + { + Key = key; + IV = iv; + } - /// - /// Determines whether the current is equal to another . - /// - /// - /// - public bool Equals(AesKeyInfo other) => (Key, IV) == (other.Key, other.IV); + /// + /// Determines whether the current is equal to another . + /// + /// + /// + public bool Equals(AesKeyInfo other) => (Key, IV) == (other.Key, other.IV); - /// - /// Determines whether the current object is equal to another object of the same type. - /// - /// - /// - public override bool Equals(object obj) => (obj is AesKeyInfo keyInfo) && Equals(keyInfo); + /// + /// Determines whether the current object is equal to another object of the same type. + /// + /// + /// + public override bool Equals(object obj) => (obj is AesKeyInfo keyInfo) && Equals(keyInfo); - /// - /// Calculates the hash code for the current instance. - /// - /// - public override int GetHashCode() => (Key, IV).GetHashCode(); + /// + /// Calculates the hash code for the current instance. + /// + /// + public override int GetHashCode() => (Key, IV).GetHashCode(); - /// - /// Determines whether the current is equal to another . - /// - /// - /// - /// - public static bool operator ==(AesKeyInfo left, AesKeyInfo right) => Equals(left, right); + /// + /// Determines whether the current is equal to another . + /// + /// + /// + /// + public static bool operator ==(AesKeyInfo left, AesKeyInfo right) => Equals(left, right); - /// - /// Determines whether the current is not equal to another . - /// - /// - /// - /// - public static bool operator !=(AesKeyInfo left, AesKeyInfo right) => !Equals(left, right); - } + /// + /// Determines whether the current is not equal to another . + /// + /// + /// + /// + public static bool operator !=(AesKeyInfo left, AesKeyInfo right) => !Equals(left, right); } diff --git a/src/EntityFrameworkCore.DataEncryption/Providers/AesKeySize.cs b/src/EntityFrameworkCore.DataEncryption/Providers/AesKeySize.cs index d833a6f..c1632c4 100644 --- a/src/EntityFrameworkCore.DataEncryption/Providers/AesKeySize.cs +++ b/src/EntityFrameworkCore.DataEncryption/Providers/AesKeySize.cs @@ -1,26 +1,25 @@ -namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers +namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers; + +/// +/// Specifies the available AES Key sizes used for generating encryption keys and initialization vectors. +/// +/// +/// The key sizes are defined in bits. +/// +public enum AesKeySize : uint { /// - /// Specifies the available AES Key sizes used for generating encryption keys and initialization vectors. + /// AES 128 bits key size. /// - /// - /// The key sizes are defined in bits. - /// - public enum AesKeySize : uint - { - /// - /// AES 128 bits key size. - /// - AES128Bits = 128, + AES128Bits = 128, - /// - /// AES 192 bits key size. - /// - AES192Bits = 192, + /// + /// AES 192 bits key size. + /// + AES192Bits = 192, - /// - /// AES 256 bits key size. - /// - AES256Bits = 256 - } + /// + /// AES 256 bits key size. + /// + AES256Bits = 256 } diff --git a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs index 4a0565b..03cc6d2 100644 --- a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs +++ b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs @@ -2,165 +2,134 @@ using System.IO; using System.Security.Cryptography; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers +namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers; + +/// +/// Implements the Advanced Encryption Standard (AES) symmetric algorithm. +/// +public class AesProvider : IEncryptionProvider { /// - /// Implements the Advanced Encryption Standard (AES) symmetric algorithm. + /// AES block size constant. + /// + public const int AesBlockSize = 128; + + /// + /// Initialization vector size constant. + /// + public const int InitializationVectorSize = 16; + + private readonly byte[] _key; + private readonly byte[] _iv; + private readonly CipherMode _mode; + private readonly PaddingMode _padding; + + /// + /// Creates a new instance used to perform symmetric encryption and decryption on strings. /// - public class AesProvider : IEncryptionProvider + /// AES key used for the symmetric encryption. + /// AES Initialization Vector used for the symmetric encryption. + /// Mode for operation used in the symmetric encryption. + /// Padding mode used in the symmetric encryption. + public AesProvider(byte[] key, byte[] initializationVector, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) { - /// - /// AES block size constant. - /// - public const int AesBlockSize = 128; - - /// - /// Initialization vector size constant. - /// - public const int InitializationVectorSize = 16; - - private readonly byte[] _key; - private readonly CipherMode _mode; - private readonly PaddingMode _padding; - private readonly byte[] _iv; - - /// - /// Creates a new instance used to perform symmetric encryption and decryption on strings. - /// - /// AES key used for the symmetric encryption. - /// Mode for operation used in the symmetric encryption. - /// Padding mode used in the symmetric encryption. - public AesProvider(byte[] key, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) - { - _key = key; - _mode = mode; - _padding = padding; - } + _key = key ?? throw new ArgumentNullException(nameof(key), ""); + _iv = initializationVector ?? throw new ArgumentNullException(nameof(initializationVector), ""); + _mode = mode; + _padding = padding; + } - /// - /// Creates a new instance used to perform symmetric encryption and decryption on strings. - /// - /// AES key used for the symmetric encryption. - /// AES Initialization Vector used for the symmetric encryption. - /// Mode for operation used in the symmetric encryption. - /// Padding mode used in the symmetric encryption. - public AesProvider(byte[] key, byte[] initializationVector, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) : this(key, mode, padding) + /// + public byte[] Encrypt(byte[] input) + { + if (input is null || input.Length == 0) { - // Re-enabled to allow for a static IV. - // This reduces security, but allows for encrypted values to be searched using LINQ. - _iv = initializationVector; + return null; } - /// - public TStore Encrypt(TModel dataToEncrypt, Func converter, Func encoder) - { - if (converter is null) - { - throw new ArgumentNullException(nameof(converter)); - } - - if (encoder is null) - { - throw new ArgumentNullException(nameof(encoder)); - } - - byte[] data = converter(dataToEncrypt); - if (data is null || data.Length == 0) - { - return default; - } - - using var aes = CreateCryptographyProvider(); - using var memoryStream = new MemoryStream(); - - byte[] initializationVector = _iv; - if (initializationVector is null) - { - aes.GenerateIV(); - initializationVector = aes.IV; - memoryStream.Write(initializationVector, 0, initializationVector.Length); - } - - using var transform = aes.CreateEncryptor(_key, initializationVector); - using var crypto = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write); - crypto.Write(data, 0, data.Length); - crypto.FlushFinalBlock(); - - memoryStream.Seek(0L, SeekOrigin.Begin); - return encoder(memoryStream); - } + using Aes aes = CreateCryptographyProvider(_key, _iv, _mode, _padding); + using ICryptoTransform transform = aes.CreateEncryptor(aes.Key, aes.IV); + using MemoryStream memoryStream = new(); + using CryptoStream cryptoStream = new(memoryStream, transform, CryptoStreamMode.Write); + + cryptoStream.Write(input, 0, input.Length); + cryptoStream.FlushFinalBlock(); + memoryStream.Seek(0L, SeekOrigin.Begin); - /// - public TModel Decrypt(TStore dataToDecrypt, Func decoder, Func converter) + return StreamToBytes(memoryStream); + } + + /// + public byte[] Decrypt(byte[] input) + { + if (input is null || input.Length == 0) { - if (decoder is null) - { - throw new ArgumentNullException(nameof(decoder)); - } - - if (converter is null) - { - throw new ArgumentNullException(nameof(converter)); - } - - byte[] data = decoder(dataToDecrypt); - if (data is null || data.Length == 0) - { - return default; - } - - using var memoryStream = new MemoryStream(data); - - byte[] initializationVector = _iv; - if (initializationVector is null) - { - initializationVector = new byte[InitializationVectorSize]; - memoryStream.Read(initializationVector, 0, initializationVector.Length); - } - - using var aes = CreateCryptographyProvider(); - using var transform = aes.CreateDecryptor(_key, initializationVector); - using var crypto = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read); - return converter(crypto); + return null; } - /// - /// Generates an AES cryptography provider. - /// - /// - private AesCryptoServiceProvider CreateCryptographyProvider() + using Aes aes = CreateCryptographyProvider(_key, _iv, _mode, _padding); + using ICryptoTransform transform = aes.CreateDecryptor(aes.Key, aes.IV); + using MemoryStream memoryStream = new(input); + using CryptoStream cryptoStream = new(memoryStream, transform, CryptoStreamMode.Read); + + return StreamToBytes(cryptoStream); + } + + /// + /// Converts a into a byte array. + /// + /// Stream. + /// The stream's content as a byte array. + internal static byte[] StreamToBytes(Stream stream) + { + if (stream is MemoryStream ms) { - return new AesCryptoServiceProvider - { - BlockSize = AesBlockSize, - Mode = _mode, - Padding = _padding, - Key = _key, - KeySize = _key.Length * 8 - }; + return ms.ToArray(); } - /// - /// Generates an AES key. - /// - /// - /// The key size of the Aes encryption must be 128, 192 or 256 bits. - /// Please check https://blogs.msdn.microsoft.com/shawnfa/2006/10/09/the-differences-between-rijndael-and-aes/ for more informations. - /// - /// AES Key size - /// - public static AesKeyInfo GenerateKey(AesKeySize keySize) - { - var crypto = new AesCryptoServiceProvider - { - KeySize = (int)keySize, - BlockSize = AesBlockSize - }; + using var output = new MemoryStream(); + stream.CopyTo(output); + return output.ToArray(); + } - crypto.GenerateKey(); - crypto.GenerateIV(); + /// + /// Generates an AES cryptography provider. + /// + /// + private static Aes CreateCryptographyProvider(byte[] key, byte[] iv, CipherMode mode, PaddingMode padding) + { + var aes = Aes.Create(); - return new AesKeyInfo(crypto.Key, crypto.IV); - } + aes.Mode = mode; + aes.KeySize = key.Length * 8; + aes.BlockSize = AesBlockSize; + aes.FeedbackSize = AesBlockSize; + aes.Padding = padding; + aes.Key = key; + aes.IV = iv; + + return aes; + } + + /// + /// Generates an AES key. + /// + /// + /// The key size of the Aes encryption must be 128, 192 or 256 bits. + /// Please check https://blogs.msdn.microsoft.com/shawnfa/2006/10/09/the-differences-between-rijndael-and-aes/ for more informations. + /// + /// AES Key size + /// + public static AesKeyInfo GenerateKey(AesKeySize keySize) + { + var aes = Aes.Create(); + + aes.KeySize = (int)keySize; + aes.BlockSize = AesBlockSize; + + aes.GenerateKey(); + aes.GenerateIV(); + + return new AesKeyInfo(aes.Key, aes.IV); } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs index 1361cef..de7b7ea 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs @@ -2,40 +2,37 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Security; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context -{ - public sealed class AuthorEntity - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; - public Guid UniqueId { get; set; } +public sealed class AuthorEntity +{ + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } - [Required] - [Encrypted] - public string FirstName { get; set; } + public Guid UniqueId { get; set; } - [Required] - [Encrypted] - public string LastName { get; set; } + [Required] + [Encrypted] + public string FirstName { get; set; } - [Required] - public int Age { get; set; } + [Required] + [Encrypted(StorageFormat.Binary)] + [Column(TypeName = "BLOB")] + public string LastName { get; set; } - public SecureString Password { get; set; } + [Required] + public int Age { get; set; } - public IList Books { get; set; } + public IList Books { get; set; } - public AuthorEntity(string firstName, string lastName, int age) - { - FirstName = firstName; - LastName = lastName; - Age = age; - Books = new List(); - UniqueId = Guid.NewGuid(); - } + public AuthorEntity(string firstName, string lastName, int age) + { + FirstName = firstName; + LastName = lastName; + Age = age; + Books = new List(); + UniqueId = Guid.NewGuid(); } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs index 0622d27..7f3ccbc 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs @@ -2,34 +2,43 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; + +public sealed class BookEntity { - public sealed class BookEntity - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + public Guid UniqueId { get; set; } - public Guid UniqueId { get; set; } + [Required] + [Encrypted] + public string Name { get; set; } - [Required] - [Encrypted] - public string Name { get; set; } + [Required] + public int NumberOfPages { get; set; } - [Required] - public int NumberOfPages { get; set; } + [Required] + public int AuthorId { get; set; } - [Required] - public int AuthorId { get; set; } + [ForeignKey(nameof(AuthorId))] + public AuthorEntity Author { get; set; } - [ForeignKey(nameof(AuthorId))] - public AuthorEntity Author { get; set; } + [Encrypted(StorageFormat.Base64)] + [Column(TypeName = "TEXT")] + public byte[] Content { get; set; } - public BookEntity(string name, int numberOfPages) - { - Name = name; - NumberOfPages = numberOfPages; - UniqueId = Guid.NewGuid(); - } + [Encrypted] + [Column(TypeName = "BLOB")] + public byte[] Summary { get; set; } + + public BookEntity(string name, int numberOfPages, byte[] content, byte[] summary) + { + Name = name; + NumberOfPages = numberOfPages; + UniqueId = Guid.NewGuid(); + Content = content; + Summary = summary; } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContext.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContext.cs index 5d7ea56..d59bf3d 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContext.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContext.cs @@ -1,25 +1,24 @@ -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; + +public class DatabaseContext : DbContext { - public class DatabaseContext : DbContext - { - private readonly IEncryptionProvider _encryptionProvider; + private readonly IEncryptionProvider _encryptionProvider; - public DbSet Authors { get; set; } + public DbSet Authors { get; set; } - public DbSet Books { get; set; } + public DbSet Books { get; set; } - public DatabaseContext(DbContextOptions options) - : base(options) - { } + public DatabaseContext(DbContextOptions options) + : base(options) + { } - public DatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options) - { - _encryptionProvider = encryptionProvider; - } - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - modelBuilder.UseEncryption(_encryptionProvider); - } + public DatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options) + { + _encryptionProvider = encryptionProvider; + } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.UseEncryption(_encryptionProvider); } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContextFactory.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContextFactory.cs index eefb329..227199d 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContextFactory.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/DatabaseContextFactory.cs @@ -2,55 +2,54 @@ using System; using System.Data.Common; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; + +/// +/// Database context factory used to create entity framework new . +/// +public sealed class DatabaseContextFactory : IDisposable { + private const string InMemoryDatabaseConnectionString = "DataSource=:memory:"; + private const string DatabaseConnectionString = "DataSource={0}"; + private readonly DbConnection _connection; + + /// + /// Creates a new instance. + /// + public DatabaseContextFactory(string databaseName = null) + { + _connection = new SqliteConnection(string.IsNullOrEmpty(databaseName) ? InMemoryDatabaseConnectionString : DatabaseConnectionString.Replace("{0}", databaseName)); + _connection.Open(); + } + + /// + /// Creates a new in memory database context. + /// + /// Context + /// Encryption provider + /// + public TContext CreateContext(IEncryptionProvider provider = null) where TContext : DbContext + { + var context = Activator.CreateInstance(typeof(TContext), CreateOptions(), provider) as TContext; + + context.Database.EnsureCreated(); + + return context; + } + + /// + /// Creates a new instance using SQLite. + /// + /// + /// + public DbContextOptions CreateOptions() where TContext : DbContext + => new DbContextOptionsBuilder().UseSqlite(_connection).Options; + /// - /// Database context factory used to create entity framework new . + /// Dispose the SQLite in memory connection. /// - public sealed class DatabaseContextFactory : IDisposable + public void Dispose() { - private const string InMemoryDatabaseConnectionString = "DataSource=:memory:"; - private const string DatabaseConnectionString = "DataSource={0}"; - private readonly DbConnection _connection; - - /// - /// Creates a new instance. - /// - public DatabaseContextFactory(string databaseName = null) - { - _connection = new SqliteConnection(string.IsNullOrEmpty(databaseName) ? InMemoryDatabaseConnectionString : DatabaseConnectionString.Replace("{0}", databaseName)); - _connection.Open(); - } - - /// - /// Creates a new in memory database context. - /// - /// Context - /// Encryption provider - /// - public TContext CreateContext(IEncryptionProvider provider = null) where TContext : DbContext - { - var context = Activator.CreateInstance(typeof(TContext), CreateOptions(), provider) as TContext; - - context.Database.EnsureCreated(); - - return context; - } - - /// - /// Creates a new instance using SQLite. - /// - /// - /// - public DbContextOptions CreateOptions() where TContext : DbContext - => new DbContextOptionsBuilder().UseSqlite(_connection).Options; - - /// - /// Dispose the SQLite in memory connection. - /// - public void Dispose() - { - _connection?.Dispose(); - } + _connection?.Dispose(); } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj b/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj index 9886f15..d3120db 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj +++ b/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj @@ -1,43 +1,31 @@  - - net6.0 - false - Microsoft.EntityFrameworkCore.Encryption.Test - Microsoft.EntityFrameworkCore.Encryption.Test - + + net6.0 + 10 + false + Microsoft.EntityFrameworkCore.Encryption.Test + Microsoft.EntityFrameworkCore.Encryption.Test + - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - all - runtime; build; native; contentfiles; analyzers - - - - - - - - - - - - - - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs b/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs deleted file mode 100644 index 4ac3ba1..0000000 --- a/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Security; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Helpers -{ - public static class DataHelper - { - private static readonly string Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private static readonly Random Randomizer = new(); - - public static byte[] RandomBytes(int length) - { - var result = new byte[length]; - Randomizer.NextBytes(result); - return result; - } - - public static string RandomString(int length) - { - var result = new char[length]; - for (int i = 0; i < length; i++) - { - char c = Characters[Randomizer.Next(Characters.Length)]; - result[i] = c; - } - - return new(result); - } - - public static SecureString RandomSecureString(int length) - { - var result = new SecureString(); - for (int i = 0; i < length; i++) - { - char c = Characters[Randomizer.Next(Characters.Length)]; - result.AppendChar(c); - } - - return result; - } - } -} diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Migration/EncryptedToOriginalMigratorTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Migration/EncryptedToOriginalMigratorTest.cs deleted file mode 100644 index 12d5aff..0000000 --- a/test/EntityFrameworkCore.DataEncryption.Test/Migration/EncryptedToOriginalMigratorTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.DataEncryption.Migration; -using Microsoft.EntityFrameworkCore.DataEncryption.Providers; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Encryption.Test.Migration -{ - public class EncryptedToOriginalMigratorTest : MigratorBaseTest - { - [Fact] - public async Task MigrateEncryptedToOriginalTest() - { - var aesKeys = AesProvider.GenerateKey(AesKeySize.AES256Bits); - var sourceProvider = new AesProvider(aesKeys.Key); - var provider = new MigrationEncryptionProvider(sourceProvider, null); - await Execute(provider); - } - } -} \ No newline at end of file diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Migration/MigratorBaseTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Migration/MigratorBaseTest.cs deleted file mode 100644 index 7354fd4..0000000 --- a/test/EntityFrameworkCore.DataEncryption.Test/Migration/MigratorBaseTest.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bogus; -using Microsoft.EntityFrameworkCore.DataEncryption.Migration; -using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Encryption.Test.Migration -{ - public abstract class MigratorBaseTest - { - private IEnumerable Authors { get; } - - protected MigratorBaseTest() - { - var faker = new Faker(); - Authors = Enumerable.Range(0, faker.Random.Byte()) - .Select(_ => new AuthorEntity(faker.Name.FirstName(), faker.Name.LastName(), faker.Random.Int(0, 90)) - { - Books = Enumerable.Range(0, 10).Select(_ => new BookEntity(faker.Lorem.Sentence(), faker.Random.Int(100, 500))).ToList() - }).ToList(); - } - - private static void AssertAuthor(AuthorEntity expected, AuthorEntity actual) - { - Assert.NotNull(actual); - Assert.Equal(expected.FirstName, actual.FirstName); - Assert.Equal(expected.LastName, actual.LastName); - Assert.Equal(expected.Age, actual.Age); - Assert.Equal(expected.Books.Count, actual.Books.Count); - - foreach (BookEntity actualBook in expected.Books) - { - BookEntity expectedBook = actual.Books.FirstOrDefault(x => x.UniqueId == actualBook.UniqueId); - - Assert.NotNull(expectedBook); - Assert.Equal(expectedBook.Name, actualBook.Name); - Assert.Equal(expectedBook.NumberOfPages, actualBook.NumberOfPages); - } - } - - protected async Task Execute(MigrationEncryptionProvider provider) - { - string databaseName = Guid.NewGuid().ToString(); - - // Feed database with data. - using (var contextFactory = new DatabaseContextFactory(databaseName)) - { - await using var context = contextFactory.CreateContext(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(provider); - await context.MigrateAsync(); - } - - // Assert if the context has been decrypted - using (var contextFactory = new DatabaseContextFactory(databaseName)) - { - await using var context = contextFactory.CreateContext(provider.DestinationEncryptionProvider); - IEnumerable 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); - } - } - } - } -} \ No newline at end of file diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Migration/OriginalToEncryptedMigratorTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Migration/OriginalToEncryptedMigratorTest.cs deleted file mode 100644 index 6fe6d7e..0000000 --- a/test/EntityFrameworkCore.DataEncryption.Test/Migration/OriginalToEncryptedMigratorTest.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.DataEncryption.Migration; -using Microsoft.EntityFrameworkCore.DataEncryption.Providers; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Encryption.Test.Migration -{ - public class OriginalToEncryptedMigratorTest : MigratorBaseTest - { - [Fact] - public async Task MigrateOriginalToEncryptedTest() - { - var aesKeys = AesProvider.GenerateKey(AesKeySize.AES256Bits); - var destinationProvider = new AesProvider(aesKeys.Key); - var provider = new MigrationEncryptionProvider(null, destinationProvider); - await Execute(provider); - } - } -} \ No newline at end of file diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Migration/V1ToV2MigratorTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Migration/V1ToV2MigratorTest.cs deleted file mode 100644 index 5f8dceb..0000000 --- a/test/EntityFrameworkCore.DataEncryption.Test/Migration/V1ToV2MigratorTest.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Threading.Tasks; -using Microsoft.EntityFrameworkCore.DataEncryption.Migration; -using Microsoft.EntityFrameworkCore.DataEncryption.Providers; -using Xunit; - -namespace Microsoft.EntityFrameworkCore.Encryption.Test.Migration -{ - public class V1ToV2MigratorTest : MigratorBaseTest - { - [Fact] - public async Task MigrateV1ToV2Test() - { - var aesKeys = AesProvider.GenerateKey(AesKeySize.AES256Bits); - var sourceProvider = new AesProvider(aesKeys.Key, aesKeys.IV); - var destinationProvider = new AesProvider(aesKeys.Key); - var provider = new MigrationEncryptionProvider(sourceProvider, destinationProvider); - await Execute(provider); - } - } -} \ No newline at end of file diff --git a/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs new file mode 100644 index 0000000..621451d --- /dev/null +++ b/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.DataEncryption; +using Microsoft.EntityFrameworkCore.DataEncryption.Providers; +using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; +using System; +using System.Collections; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Encryption.Test; + +public class ModelBuilderExtensionsTest +{ + [Fact] + public void ModelBuilderShouldNeverBeNullTest() + { + Assert.Throws(() => ModelBuilderExtensions.UseEncryption(null, null)); + } + + [Fact] + public void EncryptionProviderShouldNeverBeNullTest() + { + using var contextFactory = new DatabaseContextFactory(); + + Assert.Throws(() => contextFactory.CreateContext(null)); + } + + [Fact] + public void UseEncryptionWithUnsupportedTypeTest() + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); + + using var contextFactory = new DatabaseContextFactory(); + + Assert.Throws(() => contextFactory.CreateContext(provider)); + } + + private class InvalidPropertyEntity + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + + [Encrypted] + public string Name { get; set; } + + [Encrypted] + public int Age { get; set; } + } + + private class InvalidPropertyDbContext : DbContext + { + private readonly IEncryptionProvider _encryptionProvider; + + public DbSet InvalidEntities { get; set; } + + public InvalidPropertyDbContext(DbContextOptions options) + : base(options) + { } + + public InvalidPropertyDbContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options) + { + _encryptionProvider = encryptionProvider; + } + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.UseEncryption(_encryptionProvider); + } + } +} diff --git a/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs new file mode 100644 index 0000000..d9a5b8f --- /dev/null +++ b/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs @@ -0,0 +1,143 @@ +using Bogus; +using Microsoft.EntityFrameworkCore.DataEncryption; +using Microsoft.EntityFrameworkCore.DataEncryption.Internal; +using Microsoft.EntityFrameworkCore.DataEncryption.Providers; +using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Encryption.Test; + +public class PropertyBuilderExtensionsTest +{ + private static readonly Faker _faker = new(); + + [Fact] + public void PropertyBuilderShouldNeverBeNullTest() + { + Assert.Throws(() => PropertyBuilderExtensions.IsEncrypted(null)); + } + + [Fact] + public void PropertyShouldHaveEncryptionAnnotationsTest() + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); + + string name = _faker.Name.FullName(); + byte[] bytes = _faker.Random.Bytes(_faker.Random.Int(10, 30)); + + UserEntity user = new() + { + Name = name, + NameAsBytes = name, + ExtraData = bytes, + ExtraDataAsBytes = bytes + }; + + using var contextFactory = new DatabaseContextFactory(); + using (var context = contextFactory.CreateContext(provider)) + { + Assert.NotNull(context); + + IEntityType entityType = context.GetUserEntityType(); + Assert.NotNull(entityType); + + AssertPropertyAnnotations(entityType.GetProperty(nameof(UserEntity.Name)), true, StorageFormat.Default); + AssertPropertyAnnotations(entityType.GetProperty(nameof(UserEntity.NameAsBytes)), true, StorageFormat.Binary); + AssertPropertyAnnotations(entityType.GetProperty(nameof(UserEntity.ExtraData)), true, StorageFormat.Base64); + AssertPropertyAnnotations(entityType.GetProperty(nameof(UserEntity.ExtraDataAsBytes)), true, StorageFormat.Binary); + AssertPropertyAnnotations(entityType.GetProperty(nameof(UserEntity.Id)), false, StorageFormat.Default); + + context.Users.Add(user); + context.SaveChanges(); + } + + using (var context = contextFactory.CreateContext(provider)) + { + UserEntity u = context.Users.First(); + + Assert.NotNull(u); + Assert.Equal(name, u.Name); + Assert.Equal(name, u.NameAsBytes); + Assert.Equal(bytes, u.ExtraData); + Assert.Equal(bytes, u.ExtraDataAsBytes); + } + } + + private static void AssertPropertyAnnotations(IProperty property, bool shouldBeEncrypted, StorageFormat expectedStorageFormat) + { + Assert.NotNull(property); + + IAnnotation encryptedAnnotation = property.FindAnnotation(PropertyAnnotations.IsEncrypted); + + if (shouldBeEncrypted) + { + Assert.NotNull(encryptedAnnotation); + Assert.True((bool)encryptedAnnotation.Value); + + IAnnotation formatAnnotation = property.FindAnnotation(PropertyAnnotations.StorageFormat); + Assert.NotNull(formatAnnotation); + Assert.Equal(expectedStorageFormat, formatAnnotation.Value); + } + else + { + Assert.Null(encryptedAnnotation); + Assert.Null(property.FindAnnotation(PropertyAnnotations.StorageFormat)); + } + } + + private class UserEntity + { + public int Id { get; set; } + + // Encrypted as default (Base64) + public string Name { get; set; } + + // Encrypted as raw byte array. + public string NameAsBytes { get; set; } + + // Encrypted as Base64 string + public byte[] ExtraData { get; set; } + + // Encrypted as raw byte array. + public byte[] ExtraDataAsBytes { get; set; } + } + + private class FluentDbContext : DbContext + { + private readonly IEncryptionProvider _encryptionProvider; + + public DbSet Users { get; set; } + + public FluentDbContext(DbContextOptions options) + : base(options) + { } + + public FluentDbContext(DbContextOptions options, IEncryptionProvider encryptionProvider) + : base(options) + { + _encryptionProvider = encryptionProvider; + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + var userEntityBuilder = modelBuilder.Entity(); + + userEntityBuilder.HasKey(x => x.Id); + userEntityBuilder.Property(x => x.Id).IsRequired().ValueGeneratedOnAdd(); + userEntityBuilder.Property(x => x.Name).IsRequired().IsEncrypted(); + userEntityBuilder.Property(x => x.NameAsBytes).IsRequired().HasColumnType("BLOB").IsEncrypted(StorageFormat.Binary); + userEntityBuilder.Property(x => x.ExtraData).IsRequired().HasColumnType("TEXT").IsEncrypted(StorageFormat.Base64); + userEntityBuilder.Property(x => x.ExtraDataAsBytes).IsRequired().HasColumnType("BLOB").IsEncrypted(StorageFormat.Binary); + + modelBuilder.UseEncryption(_encryptionProvider); + } + + public IEntityType GetUserEntityType() => Model.FindEntityType(typeof(UserEntity)); + } +} diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs index b464d38..a496124 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -1,200 +1,193 @@ -using Microsoft.EntityFrameworkCore.DataEncryption.Providers; +using Bogus; +using Microsoft.EntityFrameworkCore.DataEncryption.Providers; using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; -using Microsoft.EntityFrameworkCore.DataEncryption.Test.Helpers; using System; using System.Collections.Generic; using System.Linq; -using System.Security; -using System.Security.Cryptography; -using System.Text; -using Microsoft.EntityFrameworkCore.DataEncryption.Internal; using Xunit; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Providers +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Providers; + +public class AesProviderTest { - public class AesProviderTest + private static readonly Faker _faker = new(); + + [Fact] + public void CreateAesProviderWithoutKeyTest() { - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void EncryptDecryptByteArrayTest(AesKeySize keySize) - { - byte[] input = DataHelper.RandomBytes(20); - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); - var provider = new AesProvider(encryptionKeyInfo.Key); + Assert.Throws(() => new AesProvider(null, null)); + } - byte[] encryptedData = provider.Encrypt(input, b => b, StandardConverters.StreamToBytes); - Assert.NotNull(encryptedData); + [Fact] + public void CreateAesProviderWithoutInitializationVectorTest() + { + Assert.Throws(() => new AesProvider(Array.Empty(), null)); + } - byte[] decryptedData = provider.Decrypt(encryptedData, b => b, StandardConverters.StreamToBytes); - Assert.NotNull(decryptedData); + [Fact] + public void EncryptNullOrEmptyDataTest() + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); - Assert.Equal(input, decryptedData); - } + Assert.Null(provider.Encrypt(null)); + Assert.Null(provider.Encrypt(Array.Empty())); + } - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void EncryptDecryptStringTest(AesKeySize keySize) - { - string input = DataHelper.RandomString(20); - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); - var provider = new AesProvider(encryptionKeyInfo.Key); + [Fact] + public void DecryptNullOrEmptyDataTest() + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); - string encryptedData = provider.Encrypt(input, Encoding.UTF8.GetBytes, StandardConverters.StreamToBase64String); - Assert.NotNull(encryptedData); + Assert.Null(provider.Decrypt(null)); + Assert.Null(provider.Decrypt(Array.Empty())); + } - string decryptedData = provider.Decrypt(encryptedData, Convert.FromBase64String, StandardConverters.StreamToString); - Assert.NotNull(decryptedData); + [Theory] + [InlineData(AesKeySize.AES128Bits)] + [InlineData(AesKeySize.AES192Bits)] + [InlineData(AesKeySize.AES256Bits)] + public void EncryptDecryptByteArrayTest(AesKeySize keySize) + { + byte[] input = _faker.Random.Bytes(_faker.Random.Int(10, 30)); + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); - Assert.Equal(input, decryptedData); - } + byte[] encryptedData = provider.Encrypt(input); + Assert.NotNull(encryptedData); - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void EncryptDecryptSecureStringTest(AesKeySize keySize) - { - SecureString input = DataHelper.RandomSecureString(20); - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); - var provider = new AesProvider(encryptionKeyInfo.Key); + byte[] decryptedData = provider.Decrypt(encryptedData); + Assert.NotNull(decryptedData); - string encryptedData = provider.Encrypt(input, Encoding.UTF8.GetBytes, StandardConverters.StreamToBase64String); - Assert.NotNull(encryptedData); + Assert.Equal(input, decryptedData); + } - SecureString decryptedData = provider.Decrypt(encryptedData, Convert.FromBase64String, StandardConverters.StreamToSecureString); - Assert.NotNull(decryptedData); + [Theory] + [InlineData(AesKeySize.AES128Bits)] + [InlineData(AesKeySize.AES192Bits)] + [InlineData(AesKeySize.AES256Bits)] + public void GenerateAesKeyTest(AesKeySize keySize) + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); - byte[] inputBytes = Encoding.UTF8.GetBytes(input); - byte[] decryptedBytes = Encoding.UTF8.GetBytes(decryptedData); + Assert.NotNull(encryptionKeyInfo.Key); + Assert.NotNull(encryptionKeyInfo.IV); + Assert.Equal((int)keySize / 8, encryptionKeyInfo.Key.Length); + } - Assert.Equal(inputBytes, decryptedBytes); - } + [Theory] + [InlineData(AesKeySize.AES128Bits)] + [InlineData(AesKeySize.AES192Bits)] + [InlineData(AesKeySize.AES256Bits)] + public void CompareTwoAesKeysInstancesTest(AesKeySize keySize) + { + AesKeyInfo encryptionKeyInfo1 = AesProvider.GenerateKey(keySize); + AesKeyInfo encryptionKeyInfo2 = AesProvider.GenerateKey(keySize); + AesKeyInfo encryptionKeyInfoCopy = encryptionKeyInfo1; + + Assert.NotNull(encryptionKeyInfo1.Key); + Assert.NotNull(encryptionKeyInfo1.IV); + Assert.NotNull(encryptionKeyInfo2.Key); + Assert.NotNull(encryptionKeyInfo2.IV); + Assert.True(encryptionKeyInfo1 == encryptionKeyInfoCopy); + Assert.True(encryptionKeyInfo1.Equals(encryptionKeyInfoCopy)); + Assert.True(encryptionKeyInfo1 != encryptionKeyInfo2); + Assert.True(encryptionKeyInfo1.GetHashCode() != encryptionKeyInfo2.GetHashCode()); + Assert.False(encryptionKeyInfo1.Equals(0)); + } - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void GenerateAesKeyTest(AesKeySize keySize) - { - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); + [Fact] + public void EncryptUsingAes128Provider() + { + ExecuteAesEncryptionTest(AesKeySize.AES128Bits); + } - Assert.NotNull(encryptionKeyInfo.Key); - Assert.NotNull(encryptionKeyInfo.IV); - Assert.Equal((int)keySize / 8, encryptionKeyInfo.Key.Length); - } + [Fact] + public void EncryptUsingAes192Provider() + { + ExecuteAesEncryptionTest(AesKeySize.AES192Bits); + } - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void CompareTwoAesKeysInstancesTest(AesKeySize keySize) - { - AesKeyInfo encryptionKeyInfo1 = AesProvider.GenerateKey(keySize); - AesKeyInfo encryptionKeyInfo2 = AesProvider.GenerateKey(keySize); - AesKeyInfo encryptionKeyInfoCopy = encryptionKeyInfo1; - - Assert.NotNull(encryptionKeyInfo1.Key); - Assert.NotNull(encryptionKeyInfo1.IV); - Assert.NotNull(encryptionKeyInfo2.Key); - Assert.NotNull(encryptionKeyInfo2.IV); - Assert.True(encryptionKeyInfo1 == encryptionKeyInfoCopy); - Assert.True(encryptionKeyInfo1.Equals(encryptionKeyInfoCopy)); - Assert.True(encryptionKeyInfo1 != encryptionKeyInfo2); - Assert.True(encryptionKeyInfo1.GetHashCode() != encryptionKeyInfo2.GetHashCode()); - Assert.False(encryptionKeyInfo1.Equals(0)); - } + [Fact] + public void EncryptUsingAes256Provider() + { + ExecuteAesEncryptionTest(AesKeySize.AES256Bits); + } - [Fact] - public void CreateDataContextWithoutProvider() + private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) where TContext : DatabaseContext + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(aesKeyType); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); + var author = new AuthorEntity("John", "Doe", 42) { - using var contextFactory = new DatabaseContextFactory(); - using var context = contextFactory.CreateContext(); - Assert.NotNull(context); - } + Books = new List + { + new(name: _faker.Lorem.Sentence(2), + numberOfPages: _faker.Random.Int(100, 300), + content: _faker.Random.Bytes(_faker.Random.Int(5, 10)), + summary: _faker.Random.Bytes(_faker.Random.Int(5, 10))), + new(name: _faker.Lorem.Sentence(2), + numberOfPages: _faker.Random.Int(100, 300), + content: _faker.Random.Bytes(_faker.Random.Int(5, 10)), + summary: _faker.Random.Bytes(_faker.Random.Int(5, 10))), + } + }; - [Fact] - public void EncryptUsingAes128Provider() - { - ExecuteAesEncryptionTest(AesKeySize.AES128Bits); - } + using var contextFactory = new DatabaseContextFactory(); - [Fact] - public void EncryptUsingAes192Provider() + // Save data to an encrypted database context + using (var dbContext = contextFactory.CreateContext(provider)) { - ExecuteAesEncryptionTest(AesKeySize.AES192Bits); + dbContext.Authors.Add(author); + dbContext.SaveChanges(); } - [Fact] - public void EncryptUsingAes256Provider() + // Read decrypted data and compare with original data + using (var dbContext = contextFactory.CreateContext(provider)) { - ExecuteAesEncryptionTest(AesKeySize.AES256Bits); - } + AuthorEntity authorFromDb = dbContext.Authors.Include(x => x.Books).FirstOrDefault(); - private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) where TContext : DatabaseContext - { - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(aesKeyType); - var provider = new AesProvider(encryptionKeyInfo.Key, CipherMode.CBC, PaddingMode.Zeros); - var author = new AuthorEntity("John", "Doe", 42) - { - Password = DataHelper.RandomSecureString(10), - Books = new List - { - new("Lorem Ipsum", 300), - new("Dolor sit amet", 390) - } - }; - - using var contextFactory = new DatabaseContextFactory(); - - // Save data to an encrypted database context - using (var dbContext = contextFactory.CreateContext(provider)) - { - dbContext.Authors.Add(author); - dbContext.SaveChanges(); - } + Assert.NotNull(authorFromDb); + Assert.Equal(author.FirstName, authorFromDb.FirstName); + Assert.Equal(author.LastName, authorFromDb.LastName); + Assert.NotNull(authorFromDb.Books); + Assert.NotEmpty(authorFromDb.Books); + Assert.Equal(2, authorFromDb.Books.Count); - // Read decrypted data and compare with original data - using (var dbContext = contextFactory.CreateContext(provider)) + foreach (var book in authorFromDb.Books) { - var authorFromDb = dbContext.Authors.Include(x => x.Books).FirstOrDefault(); - - Assert.NotNull(authorFromDb); - Assert.Equal(author.FirstName, authorFromDb.FirstName); - Assert.Equal(author.LastName, authorFromDb.LastName); - Assert.NotNull(authorFromDb.Books); - Assert.NotEmpty(authorFromDb.Books); - Assert.Equal(2, authorFromDb.Books.Count); - Assert.Equal(author.Books.First().Name, authorFromDb.Books.First().Name); - Assert.Equal(author.Books.Last().Name, authorFromDb.Books.Last().Name); + BookEntity originalBook = author.Books.FirstOrDefault(x => x.Id == book.Id); + + Assert.NotNull(originalBook); + Assert.Equal(originalBook.Name, book.Name); + Assert.Equal(originalBook.Content, book.Content); + Assert.Equal(originalBook.Summary, book.Summary); } } + } - public class Aes128EncryptedDatabaseContext : DatabaseContext - { - public Aes128EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } - } + public class Aes128EncryptedDatabaseContext : DatabaseContext + { + public Aes128EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options, encryptionProvider) { } + } - public class Aes192EncryptedDatabaseContext : DatabaseContext - { - public Aes192EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } - } + public class Aes192EncryptedDatabaseContext : DatabaseContext + { + public Aes192EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options, encryptionProvider) { } + } - public class Aes256EncryptedDatabaseContext : DatabaseContext - { - public Aes256EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } - } + public class Aes256EncryptedDatabaseContext : DatabaseContext + { + public Aes256EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options, encryptionProvider) { } + } - public class SimpleEncryptedDatabaseContext : DatabaseContext - { - public SimpleEncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } - } + public class SimpleEncryptedDatabaseContext : DatabaseContext + { + public SimpleEncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options, encryptionProvider) { } } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/runsettings.xml b/test/EntityFrameworkCore.DataEncryption.Test/runsettings.xml new file mode 100644 index 0000000..9578ea4 --- /dev/null +++ b/test/EntityFrameworkCore.DataEncryption.Test/runsettings.xml @@ -0,0 +1,12 @@ + + + + + + + opencover + + + + + \ No newline at end of file