From e0615d6cca2c6a18919f922741feac8776ea49c1 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Tue, 25 Oct 2022 18:06:03 +0200 Subject: [PATCH 01/14] WIP: Version 4 rework --- .editorconfig | 39 --- EntityFrameworkCore.DataEncryption.sln | 18 +- samples/AesSample/AesSample.csproj | 1 + samples/AesSample/DatabaseContext.cs | 29 +- samples/AesSample/Program.cs | 77 ++--- samples/AesSample/UserEntity.cs | 29 +- .../Attributes/EncryptedAttribute.cs | 50 +-- .../Attributes/StorageFormat.cs | 45 +-- .../EntityFrameworkCore.DataEncryption.csproj | 20 +- ...workCore.DataEncryption.csproj.DotSettings | 2 - .../IEncryptionProvider.cs | 76 +---- .../Internal/ConverterBuilder.cs | 206 ------------- .../Internal/ConverterBuilder`1.cs | 38 --- .../Internal/ConverterBuilder`2.cs | 69 ----- .../Internal/EncodingExtensions.cs | 57 ---- .../Internal/EncryptionConverter.cs | 87 ++++-- .../Internal/Extensions/EncodingExtensions.cs | 56 ++++ .../Internal/IEncryptionValueConverter.cs | 23 +- .../Internal/StandardConverters.cs | 57 ---- .../Migration/EncryptionMigrator.cs | 208 ------------- .../Migration/MigrationEncryptionProvider.cs | 105 ------- .../ModelBuilderExtensions.cs | 231 ++++++-------- .../ModelExtensions.cs | 157 +++++----- .../PropertyBuilderExtensions.cs | 282 ++++++++--------- .../Providers/AesKeyInfo.cs | 107 ++++--- .../Providers/AesKeySize.cs | 39 ++- .../Providers/AesProvider.cs | 255 +++++++--------- .../Context/AuthorEntity.cs | 49 ++- .../Context/BookEntity.cs | 43 ++- .../Context/DatabaseContext.cs | 35 ++- .../Context/DatabaseContextFactory.cs | 91 +++--- ...tyFrameworkCore.DataEncryption.Test.csproj | 14 +- .../Helpers/DataHelper.cs | 44 +-- .../EncryptedToOriginalMigratorTest.cs | 20 -- .../Migration/MigratorBaseTest.cs | 77 ----- .../OriginalToEncryptedMigratorTest.cs | 20 -- .../Migration/V1ToV2MigratorTest.cs | 21 -- .../Providers/AesProviderTest.cs | 289 ++++++++---------- 38 files changed, 1026 insertions(+), 2040 deletions(-) delete mode 100644 .editorconfig delete mode 100644 src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj.DotSettings delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`1.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/ConverterBuilder`2.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/EncodingExtensions.cs create mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/StandardConverters.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/Migration/EncryptionMigrator.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/Migration/MigrationEncryptionProvider.cs delete mode 100644 test/EntityFrameworkCore.DataEncryption.Test/Migration/EncryptedToOriginalMigratorTest.cs delete mode 100644 test/EntityFrameworkCore.DataEncryption.Test/Migration/MigratorBaseTest.cs delete mode 100644 test/EntityFrameworkCore.DataEncryption.Test/Migration/OriginalToEncryptedMigratorTest.cs delete mode 100644 test/EntityFrameworkCore.DataEncryption.Test/Migration/V1ToV2MigratorTest.cs 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/EntityFrameworkCore.DataEncryption.sln b/EntityFrameworkCore.DataEncryption.sln index fb35659..18b7c1c 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,15 +22,11 @@ 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 Global @@ -59,8 +55,8 @@ 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} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {4997BAE9-29BF-4D79-AE5E-5605E7A0F049} diff --git a/samples/AesSample/AesSample.csproj b/samples/AesSample/AesSample.csproj index aa33ab0..7499860 100644 --- a/samples/AesSample/AesSample.csproj +++ b/samples/AesSample/AesSample.csproj @@ -3,6 +3,7 @@ Exe net5.0;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..3ecc8d9 100644 --- a/samples/AesSample/Program.cs +++ b/samples/AesSample/Program.cs @@ -4,53 +4,54 @@ 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; + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: "MyInMemoryDatabase") + .Options; - // AES key randomly generated at each run. - byte[] encryptionKey = AesProvider.GenerateKey(AesKeySize.AES256Bits).Key; - var encryptionProvider = new AesProvider(encryptionKey); + // 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); + using var context = new DatabaseContext(options, encryptionProvider); - var user = new UserEntity - { - FirstName = "John", - LastName = "Doe", - Email = "john@doe.com", - Password = BuildPassword(), - }; + var user = new UserEntity + { + FirstName = "John", + LastName = "Doe", + Email = "john@doe.com", + //Password = BuildPassword(), + }; - context.Users.Add(user); - context.SaveChanges(); + context.Users.Add(user); + context.SaveChanges(); - Console.WriteLine($"Users count: {context.Users.Count()}"); + Console.WriteLine($"Users count: {context.Users.Count()}"); - user = context.Users.First(); + user = context.Users.First(); - Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email} ({user.Password.Length})"); - } + Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email}"); + } - static SecureString BuildPassword() - { - 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; - } + static SecureString BuildPassword() + { + 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; } } diff --git a/samples/AesSample/UserEntity.cs b/samples/AesSample/UserEntity.cs index 671890b..564dfc3 100644 --- a/samples/AesSample/UserEntity.cs +++ b/samples/AesSample/UserEntity.cs @@ -3,24 +3,23 @@ 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 FirstName { get; set; } - [Required] - public string LastName { get; set; } + [Required] + public string LastName { get; set; } - [Required] - [Encrypted] - public string Email { get; set; } + [Required] + [Encrypted] + public string Email { get; set; } - public SecureString Password { get; set; } - } + //public SecureString Password { 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..3ce311c 100644 --- a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj +++ b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj @@ -2,12 +2,12 @@ netstandard2.0;net6.0;net5.0 - 9.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,7 +17,7 @@ true entity-framework-core, extensions, dotnet-core, dotnet, encryption, fluent-api icon.png - Filipe GOMES PEIXOTO © 2019 - 2021 + Filipe GOMES PEIXOTO © 2019 - 2022 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 @@ -25,7 +25,7 @@ - Auto + Auto @@ -43,15 +43,15 @@ - + True - True - \ + True + \ @@ -62,4 +62,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..8dbd201 100644 --- a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs +++ b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs @@ -1,28 +1,79 @@ -using System; -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using System; +using System.ComponentModel.DataAnnotations; +using System.Security; +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, IEncryptionValueConverter { + public IEncryptionProvider EncryptionProvider { get; } + /// - /// Defines the internal encryption converter for string values. + /// Creates a new instance. /// - internal sealed class EncryptionConverter : ValueConverter, IEncryptionValueConverter + public EncryptionConverter(IEncryptionProvider encryptionProvider, StorageFormat storageFormat, ConverterMappingHints mappingHints = null) + : base( + x => Encrypt(x, encryptionProvider, storageFormat), + x => Decrypt(x, encryptionProvider, storageFormat), + mappingHints) + { + EncryptionProvider = encryptionProvider; + } + + private static TOutput Encrypt(TInput input, IEncryptionProvider encryptionProvider, StorageFormat storageFormat) { - /// - /// Creates a new instance. - /// - public EncryptionConverter( - IEncryptionProvider encryptionProvider, - Expression> convertToProviderExpression, - Expression> convertFromProviderExpression, - ConverterMappingHints mappingHints = null) - : base(convertToProviderExpression, convertFromProviderExpression, mappingHints) + byte[] inputData = input switch + { + string => Encoding.UTF8.GetBytes(input.ToString()), + byte[] => input as byte[], + SecureString => null, + _ => 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) + { + 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[])) + { + decryptedData = decryptedRawBytes; + } + else if (destinationType == typeof(SecureString)) { - EncryptionProvider = encryptionProvider; + // TODO } - /// - public IEncryptionProvider EncryptionProvider { get; } + return (TModel)Convert.ChangeType(decryptedData, typeof(TModel)); } } diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs b/src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs new file mode 100644 index 0000000..840de71 --- /dev/null +++ b/src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security; +using System.Text; + +namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal.Extensions; + +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); + } + } + } +} diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs index f54fa82..1373715 100644 --- a/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs +++ b/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs @@ -1,18 +1,17 @@ using System; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal +namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal; + +/// +/// Interface for an encryption value converter. +/// +public interface IEncryptionValueConverter { /// - /// Interface for an encryption value converter. + /// Returns the encryption provider, if any. /// - public interface IEncryptionValueConverter - { - /// - /// Returns the encryption provider, if any. - /// - /// - /// The for this converter, if any. - /// - IEncryptionProvider EncryptionProvider { get; } - } + /// + /// The for this converter, if any. + /// + IEncryptionProvider EncryptionProvider { get; } } \ No newline at end of file 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..0b7ff4f 100644 --- a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs @@ -1,175 +1,114 @@ using Microsoft.EntityFrameworkCore.DataEncryption.Internal; 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)>(); + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + { + IEnumerable encryptedProperties = GetEntityEncryptedProperties(entityType); - foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) + 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. - // By default, SecureString properties are created as navigation properties, and need to be reconfigured: - foreach (var navigation in entityType.GetNavigations()) + ValueConverter converter = GetValueConverter(encryptedProperty.Property.ClrType, encryptionProvider, encryptedProperty.StorageFormat); + + 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; + StorageFormat.Default or StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, storageFormat), + StorageFormat.Binary => new EncryptionConverter(encryptionProvider, storageFormat), + _ => throw new NotImplementedException() + }; + } + else if (propertyType == typeof(byte[])) + { + return storageFormat switch + { + StorageFormat.Default or StorageFormat.Binary => new EncryptionConverter(encryptionProvider, storageFormat), + StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, storageFormat), + _ => throw new NotImplementedException() + }; + } + else if (propertyType == typeof(SecureString)) + { + // TODO + } - 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."); - } - } - } - } + return null; + } - return modelBuilder; + private static IEnumerable GetEntityEncryptedProperties(IMutableEntityType entity) + { + return entity.GetProperties() + .Select(p => new { Property = p, EncryptedAttribute = p.PropertyInfo?.GetCustomAttribute(false) }) + .Where(x => x.EncryptedAttribute != null) + .Select(x => new EncryptedProperty(entity, x.Property, x.EncryptedAttribute.Format)); + } + + internal struct EncryptedProperty + { + public IMutableEntityType EntityType { get; } + + public IMutableProperty Property { get; } + + public StorageFormat StorageFormat { get; } + + public EncryptedProperty(IMutableEntityType entityType, IMutableProperty property, StorageFormat storageFormat) + { + EntityType = entityType; + Property = property; + StorageFormat = storageFormat; } } } diff --git a/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs index 8417479..554af2d 100644 --- a/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs @@ -7,101 +7,100 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; -namespace Microsoft.EntityFrameworkCore.DataEncryption +namespace Microsoft.EntityFrameworkCore.DataEncryption; + +/// +/// Extension methods for EF models. +/// +public static class ModelExtensions { /// - /// Extension methods for EF models. + /// Returns a value indicating whether the specified property should be encrypted. /// - public static class ModelExtensions + /// + /// 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) { - /// - /// 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) { - 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); - } + throw new ArgumentNullException(nameof(property)); + } - return attribute is null ? (false, StorageFormat.Default) : (true, attribute.Format); + var attribute = property.PropertyInfo?.GetCustomAttribute(false); + if (property.ClrType == typeof(SecureString)) + { + return (true, attribute?.Format ?? StorageFormat.Binary); } - /// - /// 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) + 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) { - if (property is null) - { - throw new ArgumentNullException(nameof(property)); - } + 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); - } + 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); + var attribute = property.PropertyInfo?.GetCustomAttribute(false); + if (property.ClrType == typeof(SecureString)) + { + return (true, attribute?.Format ?? StorageFormat.Binary); } - /// - /// 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 attribute is null ? (false, StorageFormat.Default) : (true, attribute.Format); + } - return entityType.GetProperties() - .Select(p => (property: p, flag: p.ShouldEncrypt())) - .Where(p => p.flag.shouldEncrypt) - .Select(p => (p.property, p.flag.format)).ToList(); + /// + /// 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..11d8ee0 100644 --- a/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs @@ -1,147 +1,147 @@ -using System; -using System.ComponentModel.DataAnnotations; -using System.Security; -using Microsoft.EntityFrameworkCore.DataEncryption.Internal; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +//using System; +//using System.ComponentModel.DataAnnotations; +//using System.Security; +//using Microsoft.EntityFrameworkCore.DataEncryption.Internal; +//using Microsoft.EntityFrameworkCore.Metadata.Builders; +//using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -namespace Microsoft.EntityFrameworkCore.DataEncryption -{ - /// - /// Provides extensions for the . - /// - public static class PropertyBuilderExtensions - { - /// - /// 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)); - } +//namespace Microsoft.EntityFrameworkCore.DataEncryption +//{ +// /// +// /// Provides extensions for the . +// /// +// public static class PropertyBuilderExtensions +// { +// /// +// /// 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, +// IEncryptionProviderOld 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)), - }; - } +// 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 (property is null) - { - throw new ArgumentNullException(nameof(property)); - } +// /// +// /// 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, +// IEncryptionProviderOld 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.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)), - }; - } +// 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)), +// }; +// } - /// - /// 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)); - } +// /// +// /// 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, +// IEncryptionProviderOld encryptionProvider, +// StorageFormat format = StorageFormat.Default, +// ConverterMappingHints mappingHints = null) +// { +// if (property is null) +// { +// throw new ArgumentNullException(nameof(property)); +// } - 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)), - }; - } - } -} \ No newline at end of file +// 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)), +// }; +// } +// } +//} \ 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..d7ea214 100644 --- a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs +++ b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs @@ -1,166 +1,137 @@ -using System; +using Microsoft.EntityFrameworkCore.DataEncryption.Internal.Extensions; 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 CipherMode _mode; + private readonly PaddingMode _padding; + private readonly byte[] _iv; + + /// + /// 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; + _iv = 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) + using var aes = CreateCryptographyProvider(_key, _mode, _padding); + using var memoryStream = new MemoryStream(); + + byte[] initializationVector = _iv; + if (initializationVector is null) { - 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); + aes.GenerateIV(); + initializationVector = aes.IV; + memoryStream.Write(initializationVector, 0, initializationVector.Length); } - /// - public TModel Decrypt(TStore dataToDecrypt, Func decoder, Func converter) + using var transform = aes.CreateEncryptor(_key, initializationVector); + using var crypto = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write); + crypto.Write(input, 0, input.Length); + crypto.FlushFinalBlock(); + + memoryStream.Seek(0, SeekOrigin.Begin); + + return memoryStream.ToArray(); + } + + /// + 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 var memoryStream = new MemoryStream(input); + + byte[] initializationVector = _iv; + if (initializationVector is null) { - return new AesCryptoServiceProvider - { - BlockSize = AesBlockSize, - Mode = _mode, - Padding = _padding, - Key = _key, - KeySize = _key.Length * 8 - }; + initializationVector = new byte[InitializationVectorSize]; + memoryStream.Read(initializationVector, 0, initializationVector.Length); } - /// - /// 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 aes = CreateCryptographyProvider(_key, _mode, _padding); + using var transform = aes.CreateDecryptor(_key, initializationVector); - crypto.GenerateKey(); - crypto.GenerateIV(); + using var outputStream = new MemoryStream(); + using var crypto = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read); - return new AesKeyInfo(crypto.Key, crypto.IV); - } + crypto.CopyTo(outputStream); + + return outputStream.ToArray(); + } + + /// + /// Generates an AES cryptography provider. + /// + /// + private static Aes CreateCryptographyProvider(byte[] key, CipherMode mode, PaddingMode padding) + { + var aes = Aes.Create(); + + aes.BlockSize = AesBlockSize; + aes.Mode = mode; + aes.Padding = padding; + aes.Key = key; + aes.KeySize = key.Length * 8; + + 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..5064342 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs @@ -4,38 +4,37 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Security; -namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; + +public sealed class AuthorEntity { - public sealed class AuthorEntity - { - [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 FirstName { get; set; } + [Required] + [Encrypted] + public string FirstName { get; set; } - [Required] - [Encrypted] - public string LastName { get; set; } + [Required] + [Encrypted] + public string LastName { get; set; } - [Required] - public int Age { get; set; } + [Required] + public int Age { get; set; } - public SecureString Password { get; set; } + //public SecureString Password { 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..97c1e20 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs @@ -2,34 +2,33 @@ 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; } - public BookEntity(string name, int numberOfPages) - { - Name = name; - NumberOfPages = numberOfPages; - UniqueId = Guid.NewGuid(); - } + public BookEntity(string name, int numberOfPages) + { + Name = name; + NumberOfPages = numberOfPages; + UniqueId = Guid.NewGuid(); } } 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..cb53851 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj +++ b/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj @@ -2,21 +2,22 @@ net6.0 + 10 false Microsoft.EntityFrameworkCore.Encryption.Test Microsoft.EntityFrameworkCore.Encryption.Test - - + + all runtime; build; native; contentfiles; analyzers - - - + + + all runtime; build; native; contentfiles; analyzers @@ -39,5 +40,8 @@ + + + diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs b/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs index 4ac3ba1..f06ac7c 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs @@ -1,42 +1,22 @@ 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(); +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Helpers; - public static byte[] RandomBytes(int length) - { - var result = new byte[length]; - Randomizer.NextBytes(result); - return result; - } +public static class DataHelper +{ + private static readonly string Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + private static readonly Random Randomizer = new(); - public static string RandomString(int length) + public static SecureString RandomSecureString(int length) + { + var result = new SecureString(); + for (int i = 0; i < length; i++) { - 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); + char c = Characters[Randomizer.Next(Characters.Length)]; + result.AppendChar(c); } - 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; - } + 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/Providers/AesProviderTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs index b464d38..b74cf3e 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -1,200 +1,157 @@ -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 -{ - public class AesProviderTest - { - [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); - - byte[] encryptedData = provider.Encrypt(input, b => b, StandardConverters.StreamToBytes); - Assert.NotNull(encryptedData); +namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Providers; - byte[] decryptedData = provider.Decrypt(encryptedData, b => b, StandardConverters.StreamToBytes); - Assert.NotNull(decryptedData); - - Assert.Equal(input, decryptedData); - } - - [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); - - string encryptedData = provider.Encrypt(input, Encoding.UTF8.GetBytes, StandardConverters.StreamToBase64String); - Assert.NotNull(encryptedData); - - string decryptedData = provider.Decrypt(encryptedData, Convert.FromBase64String, StandardConverters.StreamToString); - Assert.NotNull(decryptedData); - - Assert.Equal(input, decryptedData); - } +public class AesProviderTest +{ + private readonly Faker _faker = new(); - [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); + [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); - string encryptedData = provider.Encrypt(input, Encoding.UTF8.GetBytes, StandardConverters.StreamToBase64String); - Assert.NotNull(encryptedData); + byte[] encryptedData = provider.Encrypt(input); + Assert.NotNull(encryptedData); - SecureString decryptedData = provider.Decrypt(encryptedData, Convert.FromBase64String, StandardConverters.StreamToSecureString); - Assert.NotNull(decryptedData); + byte[] decryptedData = provider.Decrypt(encryptedData); + Assert.NotNull(decryptedData); - byte[] inputBytes = Encoding.UTF8.GetBytes(input); - byte[] decryptedBytes = Encoding.UTF8.GetBytes(decryptedData); + Assert.Equal(input, decryptedData); + } - Assert.Equal(inputBytes, decryptedBytes); - } + [Theory] + [InlineData(AesKeySize.AES128Bits)] + [InlineData(AesKeySize.AES192Bits)] + [InlineData(AesKeySize.AES256Bits)] + public void GenerateAesKeyTest(AesKeySize keySize) + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void GenerateAesKeyTest(AesKeySize keySize) - { - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); + Assert.NotNull(encryptionKeyInfo.Key); + Assert.NotNull(encryptionKeyInfo.IV); + Assert.Equal((int)keySize / 8, encryptionKeyInfo.Key.Length); + } - Assert.NotNull(encryptionKeyInfo.Key); - Assert.NotNull(encryptionKeyInfo.IV); - Assert.Equal((int)keySize / 8, encryptionKeyInfo.Key.Length); - } + [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 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 CreateDataContextWithoutProvider() + { + using var contextFactory = new DatabaseContextFactory(); + using var context = contextFactory.CreateContext(); - [Fact] - public void CreateDataContextWithoutProvider() - { - using var contextFactory = new DatabaseContextFactory(); - using var context = contextFactory.CreateContext(); - Assert.NotNull(context); - } + Assert.NotNull(context); + } - [Fact] - public void EncryptUsingAes128Provider() - { - ExecuteAesEncryptionTest(AesKeySize.AES128Bits); - } + [Fact] + public void EncryptUsingAes128Provider() + { + ExecuteAesEncryptionTest(AesKeySize.AES128Bits); + } - [Fact] - public void EncryptUsingAes192Provider() - { - ExecuteAesEncryptionTest(AesKeySize.AES192Bits); - } + [Fact] + public void EncryptUsingAes192Provider() + { + ExecuteAesEncryptionTest(AesKeySize.AES192Bits); + } - [Fact] - public void EncryptUsingAes256Provider() - { - ExecuteAesEncryptionTest(AesKeySize.AES256Bits); - } + [Fact] + public void EncryptUsingAes256Provider() + { + ExecuteAesEncryptionTest(AesKeySize.AES256Bits); + } - private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) where TContext : DatabaseContext + private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) where TContext : DatabaseContext + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(aesKeyType); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV, CipherMode.CBC, PaddingMode.Zeros); + var author = new AuthorEntity("John", "Doe", 42) { - 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 { - 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(); + new("Lorem Ipsum", 300), + new("Dolor sit amet", 390) } + }; - // Read decrypted data and compare with original data - using (var dbContext = contextFactory.CreateContext(provider)) - { - 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); - } - } + using var contextFactory = new DatabaseContextFactory(); - public class Aes128EncryptedDatabaseContext : DatabaseContext + // Save data to an encrypted database context + using (var dbContext = contextFactory.CreateContext(provider)) { - public Aes128EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } + dbContext.Authors.Add(author); + dbContext.SaveChanges(); } - public class Aes192EncryptedDatabaseContext : DatabaseContext + // Read decrypted data and compare with original data + using (var dbContext = contextFactory.CreateContext(provider)) { - public Aes192EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } + 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); } + } - public class Aes256EncryptedDatabaseContext : DatabaseContext - { - public Aes256EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) - : base(options, encryptionProvider) { } - } + public class Aes128EncryptedDatabaseContext : DatabaseContext + { + public Aes128EncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options, encryptionProvider) { } + } - public class SimpleEncryptedDatabaseContext : DatabaseContext - { - public SimpleEncryptedDatabaseContext(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 SimpleEncryptedDatabaseContext : DatabaseContext + { + public SimpleEncryptedDatabaseContext(DbContextOptions options, IEncryptionProvider encryptionProvider = null) + : base(options, encryptionProvider) { } } } From ff0dd50002b6ea0b8ebdce2b0cf21b7d497ff8d4 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Tue, 25 Oct 2022 18:17:39 +0200 Subject: [PATCH 02/14] Update github actions Add codecov support on github build action Remove push rule Add push --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 208444e..3205ba8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,4 +24,10 @@ 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 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 + + - name: Upload code coverage + uses: codecov/codecov-action@v3 + with: + files: ./test/TestResults/TestResults.xml + fail_ci_if_error: true \ No newline at end of file From 96e169b117f7950f60bcec6c263dc3a7b2e33062 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Sat, 29 Oct 2022 12:44:00 +0200 Subject: [PATCH 03/14] Remoive secure string support --- .../Internal/EncryptionConverter.cs | 6 - .../ModelBuilderExtensions.cs | 12 +- .../ModelExtensions.cs | 106 ------------- .../PropertyBuilderExtensions.cs | 147 ------------------ .../Providers/AesProvider.cs | 3 +- .../Providers/AesProviderTest.cs | 40 +++++ 6 files changed, 43 insertions(+), 271 deletions(-) delete mode 100644 src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs delete mode 100644 src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs index 8dbd201..0502584 100644 --- a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs +++ b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs @@ -1,7 +1,6 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using System; using System.ComponentModel.DataAnnotations; -using System.Security; using System.Text; namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal; @@ -33,7 +32,6 @@ private static TOutput Encrypt(TInput input, IEncryptionProvide { string => Encoding.UTF8.GetBytes(input.ToString()), byte[] => input as byte[], - SecureString => null, _ => null, }; @@ -69,10 +67,6 @@ private static TModel Decrypt(TProvider input, IEncryptionProvi { decryptedData = decryptedRawBytes; } - else if (destinationType == typeof(SecureString)) - { - // TODO - } return (TModel)Convert.ChangeType(decryptedData, typeof(TModel)); } diff --git a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs index 0b7ff4f..44e63da 100644 --- a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs @@ -7,7 +7,6 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; -using System.Security; namespace Microsoft.EntityFrameworkCore.DataEncryption; @@ -80,10 +79,6 @@ private static ValueConverter GetValueConverter(Type propertyType, IEncryptionPr _ => throw new NotImplementedException() }; } - else if (propertyType == typeof(SecureString)) - { - // TODO - } return null; } @@ -93,20 +88,17 @@ private static IEnumerable GetEntityEncryptedProperties(IMuta return entity.GetProperties() .Select(p => new { Property = p, EncryptedAttribute = p.PropertyInfo?.GetCustomAttribute(false) }) .Where(x => x.EncryptedAttribute != null) - .Select(x => new EncryptedProperty(entity, x.Property, x.EncryptedAttribute.Format)); + .Select(x => new EncryptedProperty(x.Property, x.EncryptedAttribute.Format)); } internal struct EncryptedProperty { - public IMutableEntityType EntityType { get; } - public IMutableProperty Property { get; } public StorageFormat StorageFormat { get; } - public EncryptedProperty(IMutableEntityType entityType, IMutableProperty property, StorageFormat storageFormat) + public EncryptedProperty(IMutableProperty property, StorageFormat storageFormat) { - EntityType = entityType; Property = property; StorageFormat = storageFormat; } diff --git a/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs deleted file mode 100644 index 554af2d..0000000 --- a/src/EntityFrameworkCore.DataEncryption/ModelExtensions.cs +++ /dev/null @@ -1,106 +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 deleted file mode 100644 index 11d8ee0..0000000 --- a/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs +++ /dev/null @@ -1,147 +0,0 @@ -//using System; -//using System.ComponentModel.DataAnnotations; -//using System.Security; -//using Microsoft.EntityFrameworkCore.DataEncryption.Internal; -//using Microsoft.EntityFrameworkCore.Metadata.Builders; -//using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -//namespace Microsoft.EntityFrameworkCore.DataEncryption -//{ -// /// -// /// Provides extensions for the . -// /// -// public static class PropertyBuilderExtensions -// { -// /// -// /// 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, -// IEncryptionProviderOld 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, -// IEncryptionProviderOld 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.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)), -// }; -// } - -// /// -// /// 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, -// IEncryptionProviderOld encryptionProvider, -// StorageFormat format = StorageFormat.Default, -// ConverterMappingHints mappingHints = null) -// { -// if (property is null) -// { -// throw new ArgumentNullException(nameof(property)); -// } - -// 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)), -// }; -// } -// } -//} \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs index d7ea214..72307b2 100644 --- a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs +++ b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs @@ -1,5 +1,4 @@ -using Microsoft.EntityFrameworkCore.DataEncryption.Internal.Extensions; -using System.IO; +using System.IO; using System.Security.Cryptography; namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers; diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs index b74cf3e..790e107 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -1,6 +1,7 @@ using Bogus; using Microsoft.EntityFrameworkCore.DataEncryption.Providers; using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; +using System; using System.Collections.Generic; using System.Linq; using System.Security.Cryptography; @@ -12,6 +13,26 @@ public class AesProviderTest { private readonly Faker _faker = new(); + [Fact] + public void EncryptNullOrEmptyDataTest() + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); + + Assert.Null(provider.Encrypt(null)); + Assert.Null(provider.Encrypt(Array.Empty())); + } + + [Fact] + public void DecryptNullOrEmptyDataTest() + { + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(AesKeySize.AES256Bits); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); + + Assert.Null(provider.Decrypt(null)); + Assert.Null(provider.Decrypt(Array.Empty())); + } + [Theory] [InlineData(AesKeySize.AES128Bits)] [InlineData(AesKeySize.AES192Bits)] @@ -31,6 +52,25 @@ public void EncryptDecryptByteArrayTest(AesKeySize keySize) Assert.Equal(input, decryptedData); } + [Theory] + [InlineData(AesKeySize.AES128Bits)] + [InlineData(AesKeySize.AES192Bits)] + [InlineData(AesKeySize.AES256Bits)] + public void EncryptDecryptByteArrayWithoutIVTest(AesKeySize keySize) + { + byte[] input = _faker.Random.Bytes(_faker.Random.Int(10, 30)); + AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); + var provider = new AesProvider(encryptionKeyInfo.Key, null); + + byte[] encryptedData = provider.Encrypt(input); + Assert.NotNull(encryptedData); + + byte[] decryptedData = provider.Decrypt(encryptedData); + Assert.NotNull(decryptedData); + + Assert.Equal(input, decryptedData); + } + [Theory] [InlineData(AesKeySize.AES128Bits)] [InlineData(AesKeySize.AES192Bits)] From 3a699ba3966dd98de3b3d2217aae325fdafae6ec Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Sat, 29 Oct 2022 19:33:32 +0200 Subject: [PATCH 04/14] Fix encryption converters storage formats --- README.md | 15 +++-- samples/AesSample/Program.cs | 18 +----- samples/AesSample/UserEntity.cs | 10 +++- .../ModelBuilderExtensions.cs | 10 ++-- .../Context/BookEntity.cs | 4 ++ .../ModelBuilderExtensionsTest.cs | 57 +++++++++++++++++++ .../Providers/AesProviderTest.cs | 1 - 7 files changed, 83 insertions(+), 32 deletions(-) create mode 100644 test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs diff --git a/README.md b/README.md index 0ca13bd..9aa9150 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![.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. @@ -21,13 +22,13 @@ PM> Install-Package EntityFrameworkCore.DataEncryption ## How to use -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`. +To use `EntityFrameworkCore.DataEncryption`, you will need to decorate your `string` or `byte[]` properties of your entities with the `[Encrypted]` attribute and enable the encryption on the `ModelBuilder`. To enable the encryption correctly, you will need to use an **encryption provider**, there is a list of the available 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 | ### Example with `AesProvider` @@ -71,21 +72,19 @@ The code bellow creates a new `AesEncryption` provider and gives it to the curre ## Create an encryption provider -> :warning: This section is outdated and doesn't work for V3.0.0 and will be updated soon. - `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[]. } } ``` diff --git a/samples/AesSample/Program.cs b/samples/AesSample/Program.cs index 3ecc8d9..e7567df 100644 --- a/samples/AesSample/Program.cs +++ b/samples/AesSample/Program.cs @@ -27,7 +27,8 @@ static void Main() FirstName = "John", LastName = "Doe", Email = "john@doe.com", - //Password = BuildPassword(), + EncryptedData = new byte[2] { 1, 2 }, + EncryptedDataAsString = new byte[2] { 3, 4 } }; context.Users.Add(user); @@ -39,19 +40,4 @@ static void Main() Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email}"); } - - static SecureString BuildPassword() - { - 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; - } } diff --git a/samples/AesSample/UserEntity.cs b/samples/AesSample/UserEntity.cs index 564dfc3..5c0f0e5 100644 --- a/samples/AesSample/UserEntity.cs +++ b/samples/AesSample/UserEntity.cs @@ -1,7 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Security; namespace AesSample; @@ -21,5 +20,12 @@ public class UserEntity [Encrypted] public string Email { get; set; } - //public SecureString Password { get; set; } + [Required] + [Encrypted] + public byte[] EncryptedData { get; set; } + + [Required] + [Encrypted(StorageFormat.Base64)] + [Column(TypeName = "VARCHAR(MAX)")] + public byte[] EncryptedDataAsString { get; set; } } diff --git a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs index 44e63da..c1d3efa 100644 --- a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs @@ -65,8 +65,8 @@ private static ValueConverter GetValueConverter(Type propertyType, IEncryptionPr { return storageFormat switch { - StorageFormat.Default or StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, storageFormat), - StorageFormat.Binary => new EncryptionConverter(encryptionProvider, storageFormat), + StorageFormat.Default or StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, StorageFormat.Base64), + StorageFormat.Binary => new EncryptionConverter(encryptionProvider, StorageFormat.Binary), _ => throw new NotImplementedException() }; } @@ -74,13 +74,13 @@ private static ValueConverter GetValueConverter(Type propertyType, IEncryptionPr { return storageFormat switch { - StorageFormat.Default or StorageFormat.Binary => new EncryptionConverter(encryptionProvider, storageFormat), - StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, storageFormat), + StorageFormat.Default or StorageFormat.Binary => new EncryptionConverter(encryptionProvider, StorageFormat.Binary), + StorageFormat.Base64 => new EncryptionConverter(encryptionProvider, StorageFormat.Base64), _ => throw new NotImplementedException() }; } - return null; + throw new NotImplementedException($"Type {propertyType.Name} does not support encryption."); } private static IEnumerable GetEntityEncryptedProperties(IMutableEntityType entity) diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs index 97c1e20..1e489eb 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs @@ -25,6 +25,10 @@ public sealed class BookEntity [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; diff --git a/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs new file mode 100644 index 0000000..3fd6a07 --- /dev/null +++ b/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.DataEncryption; +using Microsoft.EntityFrameworkCore.DataEncryption.Providers; +using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Encryption.Test; + +public class ModelBuilderExtensionsTest +{ + 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); + } + } + + [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)); + } +} diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs index 790e107..947999a 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -138,7 +138,6 @@ private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) wh var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV, CipherMode.CBC, PaddingMode.Zeros); var author = new AuthorEntity("John", "Doe", 42) { - //Password = DataHelper.RandomSecureString(10), Books = new List { new("Lorem Ipsum", 300), From 28d88d543dcbb1fc9b4f93c425f3f820a07e67e2 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Sun, 30 Oct 2022 19:12:42 +0100 Subject: [PATCH 05/14] Add fluent configuration support --- EntityFrameworkCore.DataEncryption.sln | 7 ++ .../AesSample.Fluent/AesSample.Fluent.csproj | 24 ++++++ samples/AesSample.Fluent/DatabaseContext.cs | 35 ++++++++ samples/AesSample.Fluent/Program.cs | 47 +++++++++++ samples/AesSample.Fluent/UserEntity.cs | 18 +++++ samples/AesSample/AesSample.csproj | 16 ++-- samples/AesSample/Program.cs | 10 ++- samples/AesSample/UserEntity.cs | 2 +- .../Internal/EncryptionConverter.cs | 2 - .../Internal/PropertyAnnotations.cs | 7 ++ .../ModelBuilderExtensions.cs | 36 +++++++-- .../PropertyBuilderExtensions.cs | 25 ++++++ .../Helpers/DataHelper.cs | 22 ----- .../ModelBuilderExtensionsTest.cs | 37 ++++++--- .../PropertyBuilderExtensionsTest.cs | 81 +++++++++++++++++++ .../Providers/AesProviderTest.cs | 9 --- 16 files changed, 317 insertions(+), 61 deletions(-) create mode 100644 samples/AesSample.Fluent/AesSample.Fluent.csproj create mode 100644 samples/AesSample.Fluent/DatabaseContext.cs create mode 100644 samples/AesSample.Fluent/Program.cs create mode 100644 samples/AesSample.Fluent/UserEntity.cs create mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/PropertyAnnotations.cs create mode 100644 src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs delete mode 100644 test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs create mode 100644 test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs diff --git a/EntityFrameworkCore.DataEncryption.sln b/EntityFrameworkCore.DataEncryption.sln index 18b7c1c..dd88ef0 100644 --- a/EntityFrameworkCore.DataEncryption.sln +++ b/EntityFrameworkCore.DataEncryption.sln @@ -29,6 +29,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .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 @@ -47,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 @@ -57,6 +63,7 @@ Global {8AA1E576-4016-4623-96C8-90330F05F9A8} = {64C3D7D1-67B8-4070-AE67-C71B761535CC} {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/samples/AesSample.Fluent/AesSample.Fluent.csproj b/samples/AesSample.Fluent/AesSample.Fluent.csproj new file mode 100644 index 0000000..0622c55 --- /dev/null +++ b/samples/AesSample.Fluent/AesSample.Fluent.csproj @@ -0,0 +1,24 @@ + + + + Exe + net6.0;net5.0 + 10 + + + + + + + + + + + + + + + + + + diff --git a/samples/AesSample.Fluent/DatabaseContext.cs b/samples/AesSample.Fluent/DatabaseContext.cs new file mode 100644 index 0000000..e1e00cc --- /dev/null +++ b/samples/AesSample.Fluent/DatabaseContext.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.DataEncryption; +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) + { + var 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.EncryptedData).IsRequired().IsEncrypted(); + userEntityBuilder.Property(x => x.EncryptedDataAsString).IsRequired().HasColumnType("TEXT").IsEncrypted(StorageFormat.Base64); + + modelBuilder.UseEncryption(_encryptionProvider); + + base.OnModelCreating(modelBuilder); + } +} diff --git a/samples/AesSample.Fluent/Program.cs b/samples/AesSample.Fluent/Program.cs new file mode 100644 index 0000000..f80f7df --- /dev/null +++ b/samples/AesSample.Fluent/Program.cs @@ -0,0 +1,47 @@ +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", + 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}"); + } +} diff --git a/samples/AesSample.Fluent/UserEntity.cs b/samples/AesSample.Fluent/UserEntity.cs new file mode 100644 index 0000000..1df0b26 --- /dev/null +++ b/samples/AesSample.Fluent/UserEntity.cs @@ -0,0 +1,18 @@ +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 byte[] EncryptedData { get; set; } + + public byte[] EncryptedDataAsString { get; set; } +} diff --git a/samples/AesSample/AesSample.csproj b/samples/AesSample/AesSample.csproj index 7499860..1808f41 100644 --- a/samples/AesSample/AesSample.csproj +++ b/samples/AesSample/AesSample.csproj @@ -1,10 +1,10 @@ - - Exe - net5.0;net6.0 - 10 - + + Exe + net5.0;net6.0 + 10 + @@ -17,8 +17,8 @@ - - - + + + diff --git a/samples/AesSample/Program.cs b/samples/AesSample/Program.cs index e7567df..6de605f 100644 --- a/samples/AesSample/Program.cs +++ b/samples/AesSample/Program.cs @@ -1,8 +1,8 @@ -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; @@ -10,8 +10,11 @@ static class Program { static void Main() { + using SqliteConnection connection = new("DataSource=:memory:"); + connection.Open(); + var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "MyInMemoryDatabase") + .UseSqlite(connection) .Options; // AES key randomly generated at each run. @@ -21,6 +24,7 @@ static void Main() var encryptionProvider = new AesProvider(encryptionKey, encryptionIV); using var context = new DatabaseContext(options, encryptionProvider); + context.Database.EnsureCreated(); var user = new UserEntity { diff --git a/samples/AesSample/UserEntity.cs b/samples/AesSample/UserEntity.cs index 5c0f0e5..31c7402 100644 --- a/samples/AesSample/UserEntity.cs +++ b/samples/AesSample/UserEntity.cs @@ -26,6 +26,6 @@ public class UserEntity [Required] [Encrypted(StorageFormat.Base64)] - [Column(TypeName = "VARCHAR(MAX)")] + [Column(TypeName = "TEXT")] public byte[] EncryptedDataAsString { get; set; } } diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs index 0502584..1d8f027 100644 --- a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs +++ b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs @@ -54,9 +54,7 @@ private static TModel Decrypt(TProvider input, IEncryptionProvi StorageFormat.Default or StorageFormat.Base64 => Convert.FromBase64String(input.ToString()), _ => input as byte[] }; - byte[] decryptedRawBytes = encryptionProvider.Decrypt(inputData); - object decryptedData = null; if (destinationType == typeof(string)) 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/ModelBuilderExtensions.cs b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs index c1d3efa..9f8568f 100644 --- a/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs +++ b/src/EntityFrameworkCore.DataEncryption/ModelBuilderExtensions.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.DataEncryption.Internal; +using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Metadata.Internal; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -34,6 +35,11 @@ public static ModelBuilder UseEncryption(this ModelBuilder modelBuilder, IEncryp throw new ArgumentNullException(nameof(modelBuilder)); } + if (encryptionProvider is null) + { + throw new ArgumentNullException(nameof(encryptionProvider)); + } + foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) { IEnumerable encryptedProperties = GetEntityEncryptedProperties(entityType); @@ -86,21 +92,41 @@ private static ValueConverter GetValueConverter(Type propertyType, IEncryptionPr private static IEnumerable GetEntityEncryptedProperties(IMutableEntityType entity) { return entity.GetProperties() - .Select(p => new { Property = p, EncryptedAttribute = p.PropertyInfo?.GetCustomAttribute(false) }) - .Where(x => x.EncryptedAttribute != null) - .Select(x => new EncryptedProperty(x.Property, x.EncryptedAttribute.Format)); + .Select(x => EncryptedProperty.Create(x)) + .Where(x => x is not null); } - internal struct EncryptedProperty + internal class EncryptedProperty { public IMutableProperty Property { get; } public StorageFormat StorageFormat { get; } - public EncryptedProperty(IMutableProperty property, StorageFormat storageFormat) + 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 storageFormat.HasValue ? new EncryptedProperty(property, storageFormat.Value) : null; + } } } diff --git a/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs b/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs new file mode 100644 index 0000000..17276ac --- /dev/null +++ b/src/EntityFrameworkCore.DataEncryption/PropertyBuilderExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.EntityFrameworkCore.DataEncryption.Internal; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Microsoft.EntityFrameworkCore.DataEncryption; + +/// +/// Provides extensions for the type. +/// +public static class PropertyBuilderExtensions +{ + public static PropertyBuilder IsEncrypted(this PropertyBuilder builder, StorageFormat storageFormat = StorageFormat.Default) + { + if (builder is null) + { + throw new ArgumentNullException(nameof(builder)); + } + + builder.HasAnnotation(PropertyAnnotations.IsEncrypted, true); + builder.HasAnnotation(PropertyAnnotations.StorageFormat, storageFormat); + + return builder; + } +} diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs b/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs deleted file mode 100644 index f06ac7c..0000000 --- a/test/EntityFrameworkCore.DataEncryption.Test/Helpers/DataHelper.cs +++ /dev/null @@ -1,22 +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 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/ModelBuilderExtensionsTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs index 3fd6a07..621451d 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/ModelBuilderExtensionsTest.cs @@ -2,6 +2,7 @@ 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; @@ -10,6 +11,31 @@ 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] @@ -43,15 +69,4 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.UseEncryption(_encryptionProvider); } } - - [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)); - } } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs new file mode 100644 index 0000000..bb15989 --- /dev/null +++ b/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs @@ -0,0 +1,81 @@ +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 Xunit; + +namespace Microsoft.EntityFrameworkCore.Encryption.Test; + +public class PropertyBuilderExtensionsTest +{ + [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); + + using var contextFactory = new DatabaseContextFactory(); + using var context = contextFactory.CreateContext(provider); + Assert.NotNull(context); + + IEntityType entityType = context.GetUserEntityType(); + Assert.NotNull(entityType); + + IProperty property = entityType.GetProperty("Name"); + Assert.NotNull(property); + + IAnnotation encryptedAnnotation = property.FindAnnotation(PropertyAnnotations.IsEncrypted); + IAnnotation formatAnnotation = property.FindAnnotation(PropertyAnnotations.StorageFormat); + Assert.NotNull(encryptedAnnotation); + Assert.True((bool)encryptedAnnotation.Value); + Assert.NotNull(formatAnnotation); + Assert.Equal(StorageFormat.Default, formatAnnotation.Value); + } + + private class UserEntity + { + public int Id { get; set; } + + public string Name { 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(); + + 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 947999a..ec95868 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -105,15 +105,6 @@ public void CompareTwoAesKeysInstancesTest(AesKeySize keySize) Assert.False(encryptionKeyInfo1.Equals(0)); } - [Fact] - public void CreateDataContextWithoutProvider() - { - using var contextFactory = new DatabaseContextFactory(); - using var context = contextFactory.CreateContext(); - - Assert.NotNull(context); - } - [Fact] public void EncryptUsingAes128Provider() { From 2b913f5e794b8f06c0c381894abe0e5c9d67ae1f Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Sun, 30 Oct 2022 19:16:11 +0100 Subject: [PATCH 06/14] Remove unused interface on encryption converter --- .../Internal/EncryptionConverter.cs | 8 ++++---- .../Internal/IEncryptionValueConverter.cs | 17 ----------------- 2 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs index 1d8f027..57ad58a 100644 --- a/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs +++ b/src/EntityFrameworkCore.DataEncryption/Internal/EncryptionConverter.cs @@ -10,20 +10,20 @@ namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal; /// /// /// -internal sealed class EncryptionConverter : ValueConverter, IEncryptionValueConverter +internal sealed class EncryptionConverter : ValueConverter { - public IEncryptionProvider EncryptionProvider { get; } - /// /// Creates a new instance. /// + /// 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) { - EncryptionProvider = encryptionProvider; } private static TOutput Encrypt(TInput input, IEncryptionProvider encryptionProvider, StorageFormat storageFormat) diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs b/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs deleted file mode 100644 index 1373715..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/IEncryptionValueConverter.cs +++ /dev/null @@ -1,17 +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 From 6eb2c9e6c591915e12f4fbd75ce1a9899db27aa6 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Sun, 30 Oct 2022 19:18:41 +0100 Subject: [PATCH 07/14] Remove encoding extensions --- .../Internal/Extensions/EncodingExtensions.cs | 56 ------------------- 1 file changed, 56 deletions(-) delete mode 100644 src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs diff --git a/src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs b/src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs deleted file mode 100644 index 840de71..0000000 --- a/src/EntityFrameworkCore.DataEncryption/Internal/Extensions/EncodingExtensions.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using System.Diagnostics; -using System.Runtime.InteropServices; -using System.Security; -using System.Text; - -namespace Microsoft.EntityFrameworkCore.DataEncryption.Internal.Extensions; - -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); - } - } - } -} From 2cd78ab820857aaba0046960203363259b60467e Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Tue, 1 Nov 2022 11:29:44 +0100 Subject: [PATCH 08/14] Update readme Update sample versions --- README.md | 69 ++++++++++++++++--- .../AesSample.Fluent/AesSample.Fluent.csproj | 15 ++-- samples/AesSample.Fluent/DatabaseContext.cs | 3 +- samples/AesSample/AesSample.csproj | 15 ++-- 4 files changed, 72 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 9aa9150..c67c817 100644 --- a/README.md +++ b/README.md @@ -20,17 +20,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` or `byte[]` 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://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 | -### Example with `AesProvider` +## How to use + +`EntityFrameworkCore.DataEncryption` supports 2 differents initialization methods: +* Attribute +* Fluent configuration + +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 @@ -59,16 +71,55 @@ 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 [`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`. + +### 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; } +} + +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) + { + var userEntityBuilder = modelBuilder.Entity(); + + userEntityBuilder.Property(x => x.Username).IsRequired().IsEncrypted(); + userEntityBuilder.Property(x => x.Password).IsRequired().IsEncrypted(); + + 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`. ## Create an encryption provider @@ -98,12 +149,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 index 0622c55..31cee5b 100644 --- a/samples/AesSample.Fluent/AesSample.Fluent.csproj +++ b/samples/AesSample.Fluent/AesSample.Fluent.csproj @@ -2,19 +2,14 @@ Exe - net6.0;net5.0 + net6.0 10 - - - - - - - - - + + + + diff --git a/samples/AesSample.Fluent/DatabaseContext.cs b/samples/AesSample.Fluent/DatabaseContext.cs index e1e00cc..978777a 100644 --- a/samples/AesSample.Fluent/DatabaseContext.cs +++ b/samples/AesSample.Fluent/DatabaseContext.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.DataEncryption; +using Microsoft.EntityFrameworkCore.Metadata.Builders; using System.ComponentModel.DataAnnotations; namespace AesSample.Fluent; @@ -18,7 +19,7 @@ public DatabaseContext(DbContextOptions options, IEncryptionPro protected override void OnModelCreating(ModelBuilder modelBuilder) { - var userEntityBuilder = modelBuilder.Entity(); + EntityTypeBuilder userEntityBuilder = modelBuilder.Entity(); userEntityBuilder.HasKey(x => x.Id); userEntityBuilder.Property(x => x.Id).IsRequired().ValueGeneratedOnAdd(); diff --git a/samples/AesSample/AesSample.csproj b/samples/AesSample/AesSample.csproj index 1808f41..4a89054 100644 --- a/samples/AesSample/AesSample.csproj +++ b/samples/AesSample/AesSample.csproj @@ -2,19 +2,14 @@ Exe - net5.0;net6.0 + net6.0 10 - - - - - - - - - + + + + From 3e7f1e6bc3672901b38e7c303bf870a62f6f6058 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Fri, 11 Nov 2022 11:11:39 +0100 Subject: [PATCH 09/14] Add support for .NET 7 and EFCore 7 --- .../EntityFrameworkCore.DataEncryption.csproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj index 3ce311c..f365b2c 100644 --- a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj +++ b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net6.0;net5.0 + netstandard2.0;net7.0;net6.0;net5.0 10.0 true EntityFrameworkCore.DataEncryption @@ -43,6 +43,9 @@ + + + From e82c5beb5e517a3207f1f39b6e7182063627a4ba Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Fri, 11 Nov 2022 11:38:06 +0100 Subject: [PATCH 10/14] Update test project frameworks and github actions --- .github/workflows/build.yml | 24 +++++-- .../EntityFrameworkCore.DataEncryption.csproj | 5 +- ...tyFrameworkCore.DataEncryption.Test.csproj | 64 +++++++------------ .../runsettings.xml | 12 ++++ 4 files changed, 55 insertions(+), 50 deletions(-) create mode 100644 test/EntityFrameworkCore.DataEncryption.Test/runsettings.xml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3205ba8..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,10 +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 + 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/TestResults/TestResults.xml + files: ./test/EntityFrameworkCore.DataEncryption.Test/TestResults/coverage.opencover.xml fail_ci_if_error: true \ No newline at end of file diff --git a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj index f365b2c..cd0c633 100644 --- a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj +++ b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj @@ -1,7 +1,7 @@  - netstandard2.0;net7.0;net6.0;net5.0 + netstandard2.0;net6.0;net7.0 10.0 true EntityFrameworkCore.DataEncryption @@ -37,9 +37,6 @@ - - - diff --git a/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj b/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj index cb53851..d3120db 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj +++ b/test/EntityFrameworkCore.DataEncryption.Test/EntityFrameworkCore.DataEncryption.Test.csproj @@ -1,47 +1,31 @@  - - net6.0 - 10 - 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/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 From f1baf52528c4b0905823374fa709ec1ef6e59129 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Sun, 15 Jan 2023 14:19:21 +0100 Subject: [PATCH 11/14] Fixed decrypt process for byte arrays and improve unit tests --- README.md | 2 +- .../EntityFrameworkCore.DataEncryption.csproj | 9 +- .../Providers/AesProvider.cs | 75 ++++++++--------- .../Context/AuthorEntity.cs | 6 +- .../Context/BookEntity.cs | 8 +- .../PropertyBuilderExtensionsTest.cs | 84 ++++++++++++++++--- .../Providers/AesProviderTest.cs | 62 +++++++------- 7 files changed, 157 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index c67c817..32b7a05 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This library has been developed initialy for a personal project of mine. It provides a simple way to encrypt column data. -I **do not** take responsability if you use this in a production environment and loose your encryption key. +I **do not** take responsability if you use this in a production environment and loose your encryption key or corrupt your data. ## How to install diff --git a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj index cd0c633..759f3b9 100644 --- a/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj +++ b/src/EntityFrameworkCore.DataEncryption/EntityFrameworkCore.DataEncryption.csproj @@ -17,10 +17,10 @@ true entity-framework-core, extensions, dotnet-core, dotnet, encryption, fluent-api icon.png - Filipe GOMES PEIXOTO © 2019 - 2022 + 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 @@ -34,11 +34,8 @@ - - - - + diff --git a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs index 72307b2..03cc6d2 100644 --- a/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs +++ b/src/EntityFrameworkCore.DataEncryption/Providers/AesProvider.cs @@ -1,4 +1,5 @@ -using System.IO; +using System; +using System.IO; using System.Security.Cryptography; namespace Microsoft.EntityFrameworkCore.DataEncryption.Providers; @@ -19,9 +20,9 @@ public class AesProvider : IEncryptionProvider public const int InitializationVectorSize = 16; private readonly byte[] _key; + private readonly byte[] _iv; 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. @@ -32,8 +33,8 @@ public class AesProvider : IEncryptionProvider /// Padding mode used in the symmetric encryption. public AesProvider(byte[] key, byte[] initializationVector, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7) { - _key = key; - _iv = initializationVector; + _key = key ?? throw new ArgumentNullException(nameof(key), ""); + _iv = initializationVector ?? throw new ArgumentNullException(nameof(initializationVector), ""); _mode = mode; _padding = padding; } @@ -46,25 +47,16 @@ public byte[] Encrypt(byte[] input) return null; } - using var aes = CreateCryptographyProvider(_key, _mode, _padding); - using var memoryStream = new MemoryStream(); - - byte[] initializationVector = _iv; - if (initializationVector is null) - { - aes.GenerateIV(); - initializationVector = aes.IV; - memoryStream.Write(initializationVector, 0, initializationVector.Length); - } + 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); - using var transform = aes.CreateEncryptor(_key, initializationVector); - using var crypto = new CryptoStream(memoryStream, transform, CryptoStreamMode.Write); - crypto.Write(input, 0, input.Length); - crypto.FlushFinalBlock(); + cryptoStream.Write(input, 0, input.Length); + cryptoStream.FlushFinalBlock(); + memoryStream.Seek(0L, SeekOrigin.Begin); - memoryStream.Seek(0, SeekOrigin.Begin); - - return memoryStream.ToArray(); + return StreamToBytes(memoryStream); } /// @@ -75,39 +67,46 @@ public byte[] Decrypt(byte[] input) return null; } - using var memoryStream = new MemoryStream(input); + 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); + } - byte[] initializationVector = _iv; - if (initializationVector is null) + /// + /// 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) { - initializationVector = new byte[InitializationVectorSize]; - memoryStream.Read(initializationVector, 0, initializationVector.Length); + return ms.ToArray(); } - using var aes = CreateCryptographyProvider(_key, _mode, _padding); - using var transform = aes.CreateDecryptor(_key, initializationVector); - - using var outputStream = new MemoryStream(); - using var crypto = new CryptoStream(memoryStream, transform, CryptoStreamMode.Read); - - crypto.CopyTo(outputStream); - - return outputStream.ToArray(); + using var output = new MemoryStream(); + stream.CopyTo(output); + return output.ToArray(); } /// /// Generates an AES cryptography provider. /// /// - private static Aes CreateCryptographyProvider(byte[] key, CipherMode mode, PaddingMode padding) + private static Aes CreateCryptographyProvider(byte[] key, byte[] iv, CipherMode mode, PaddingMode padding) { var aes = Aes.Create(); - aes.BlockSize = AesBlockSize; aes.Mode = mode; + aes.KeySize = key.Length * 8; + aes.BlockSize = AesBlockSize; + aes.FeedbackSize = AesBlockSize; aes.Padding = padding; aes.Key = key; - aes.KeySize = key.Length * 8; + aes.IV = iv; return aes; } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs index 5064342..de7b7ea 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/AuthorEntity.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Security; namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; @@ -19,14 +18,13 @@ public sealed class AuthorEntity public string FirstName { get; set; } [Required] - [Encrypted] + [Encrypted(StorageFormat.Binary)] + [Column(TypeName = "BLOB")] public string LastName { get; set; } [Required] public int Age { get; set; } - //public SecureString Password { get; set; } - public IList Books { get; set; } public AuthorEntity(string firstName, string lastName, int age) diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs index 1e489eb..7f3ccbc 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Context/BookEntity.cs @@ -29,10 +29,16 @@ public sealed class BookEntity [Column(TypeName = "TEXT")] public byte[] Content { get; set; } - public BookEntity(string name, int numberOfPages) + [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/PropertyBuilderExtensionsTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs index bb15989..d9a5b8f 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/PropertyBuilderExtensionsTest.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore.DataEncryption; +using Bogus; +using Microsoft.EntityFrameworkCore.DataEncryption; using Microsoft.EntityFrameworkCore.DataEncryption.Internal; using Microsoft.EntityFrameworkCore.DataEncryption.Providers; using Microsoft.EntityFrameworkCore.DataEncryption.Test.Context; @@ -6,12 +7,15 @@ 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() { @@ -24,29 +28,84 @@ 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); + using (var context = contextFactory.CreateContext(provider)) + { + Assert.NotNull(context); + + IEntityType entityType = context.GetUserEntityType(); + Assert.NotNull(entityType); - 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); - IProperty property = entityType.GetProperty("Name"); + 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); - IAnnotation formatAnnotation = property.FindAnnotation(PropertyAnnotations.StorageFormat); - Assert.NotNull(encryptedAnnotation); - Assert.True((bool)encryptedAnnotation.Value); - Assert.NotNull(formatAnnotation); - Assert.Equal(StorageFormat.Default, formatAnnotation.Value); + + 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 @@ -72,6 +131,9 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) 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); } diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs index ec95868..49a23c4 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -4,14 +4,25 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography; using Xunit; namespace Microsoft.EntityFrameworkCore.DataEncryption.Test.Providers; public class AesProviderTest { - private readonly Faker _faker = new(); + private static readonly Faker _faker = new(); + + [Fact] + public void CreateAesProviderWithoutKeyTest() + { + Assert.Throws(() => new AesProvider(null, null)); + } + + [Fact] + public void CreateAesProviderWithoutInitializationVectorTest() + { + Assert.Throws(() => new AesProvider(Array.Empty(), null)); + } [Fact] public void EncryptNullOrEmptyDataTest() @@ -39,7 +50,7 @@ public void DecryptNullOrEmptyDataTest() [InlineData(AesKeySize.AES256Bits)] public void EncryptDecryptByteArrayTest(AesKeySize keySize) { - byte[] input = _faker.Random.Bytes(_faker.Random.Int(10, 30)); + byte[] input = _faker.Random.Bytes(5); AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); @@ -52,25 +63,6 @@ public void EncryptDecryptByteArrayTest(AesKeySize keySize) Assert.Equal(input, decryptedData); } - [Theory] - [InlineData(AesKeySize.AES128Bits)] - [InlineData(AesKeySize.AES192Bits)] - [InlineData(AesKeySize.AES256Bits)] - public void EncryptDecryptByteArrayWithoutIVTest(AesKeySize keySize) - { - byte[] input = _faker.Random.Bytes(_faker.Random.Int(10, 30)); - AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); - var provider = new AesProvider(encryptionKeyInfo.Key, null); - - byte[] encryptedData = provider.Encrypt(input); - Assert.NotNull(encryptedData); - - byte[] decryptedData = provider.Decrypt(encryptedData); - Assert.NotNull(decryptedData); - - Assert.Equal(input, decryptedData); - } - [Theory] [InlineData(AesKeySize.AES128Bits)] [InlineData(AesKeySize.AES192Bits)] @@ -126,13 +118,19 @@ public void EncryptUsingAes256Provider() private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) where TContext : DatabaseContext { AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(aesKeyType); - var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV, CipherMode.CBC, PaddingMode.Zeros); + var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); var author = new AuthorEntity("John", "Doe", 42) { Books = new List { - new("Lorem Ipsum", 300), - new("Dolor sit amet", 390) + 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))), } }; @@ -148,7 +146,7 @@ private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) wh // Read decrypted data and compare with original data using (var dbContext = contextFactory.CreateContext(provider)) { - var authorFromDb = dbContext.Authors.Include(x => x.Books).FirstOrDefault(); + AuthorEntity authorFromDb = dbContext.Authors.Include(x => x.Books).FirstOrDefault(); Assert.NotNull(authorFromDb); Assert.Equal(author.FirstName, authorFromDb.FirstName); @@ -156,8 +154,16 @@ private static void ExecuteAesEncryptionTest(AesKeySize aesKeyType) wh 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); + + foreach (var book in authorFromDb.Books) + { + 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); + } } } From bd98019107982c4cabb7271dfceb8022ff81bbbd Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Tue, 17 Jan 2023 09:29:41 +0100 Subject: [PATCH 12/14] Update README.md --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 32b7a05..65c92ec 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ ## Disclaimer -This library has been developed initialy for a personal project of mine. It provides a simple way to encrypt column data. +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 this in a production environment and loose your encryption key or corrupt your 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 @@ -111,6 +111,7 @@ public class DatabaseContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + // Entities builder *MUST* be called before UseEncryption(). var userEntityBuilder = modelBuilder.Entity(); userEntityBuilder.Property(x => x.Username).IsRequired().IsEncrypted(); From 9a52243e7b10622895407f0a478b3b3a6b4f4074 Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Tue, 17 Jan 2023 09:33:56 +0100 Subject: [PATCH 13/14] Update tests --- README.md | 2 ++ .../Providers/AesProviderTest.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65c92ec..96da64a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ ## Disclaimer +

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


+ 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. diff --git a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs index 49a23c4..a496124 100644 --- a/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs +++ b/test/EntityFrameworkCore.DataEncryption.Test/Providers/AesProviderTest.cs @@ -50,7 +50,7 @@ public void DecryptNullOrEmptyDataTest() [InlineData(AesKeySize.AES256Bits)] public void EncryptDecryptByteArrayTest(AesKeySize keySize) { - byte[] input = _faker.Random.Bytes(5); + byte[] input = _faker.Random.Bytes(_faker.Random.Int(10, 30)); AesKeyInfo encryptionKeyInfo = AesProvider.GenerateKey(keySize); var provider = new AesProvider(encryptionKeyInfo.Key, encryptionKeyInfo.IV); From b56a363b82c0607b63cee5beb992f0915efd3c8e Mon Sep 17 00:00:00 2001 From: Filipe GP <4021025+Eastrall@users.noreply.github.com> Date: Wed, 18 Jan 2023 21:31:32 +0100 Subject: [PATCH 14/14] Update samples --- samples/AesSample.Fluent/DatabaseContext.cs | 6 ++- .../EncryptedDatabaseContext.cs | 11 +++++ samples/AesSample.Fluent/Program.cs | 43 ++++++++++++------- samples/AesSample.Fluent/UserEntity.cs | 2 + samples/AesSample/Program.cs | 36 +++++++++------- samples/AesSample/UserEntity.cs | 4 ++ 6 files changed, 71 insertions(+), 31 deletions(-) create mode 100644 samples/AesSample.Fluent/EncryptedDatabaseContext.cs diff --git a/samples/AesSample.Fluent/DatabaseContext.cs b/samples/AesSample.Fluent/DatabaseContext.cs index 978777a..e68fab5 100644 --- a/samples/AesSample.Fluent/DatabaseContext.cs +++ b/samples/AesSample.Fluent/DatabaseContext.cs @@ -26,10 +26,14 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) 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); - modelBuilder.UseEncryption(_encryptionProvider); + 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 index f80f7df..539d827 100644 --- a/samples/AesSample.Fluent/Program.cs +++ b/samples/AesSample.Fluent/Program.cs @@ -23,25 +23,38 @@ static void Main(string[] args) byte[] encryptionIV = keyInfo.IV; var encryptionProvider = new AesProvider(encryptionKey, encryptionIV); - using var context = new DatabaseContext(options, encryptionProvider); - context.Database.EnsureCreated(); - - var user = new UserEntity + using (var context = new DatabaseContext(options, encryptionProvider)) { - FirstName = "John", - LastName = "Doe", - Email = "john@doe.com", - EncryptedData = new byte[2] { 1, 2 }, - EncryptedDataAsString = new byte[2] { 3, 4 } - }; + 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(); - context.Users.Add(user); - context.SaveChanges(); + Console.WriteLine($"Users count: {context.Users.Count()}"); + } - 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})"); + } - user = context.Users.First(); + using (var context = new DatabaseContext(options, encryptionProvider)) + { + UserEntity user = context.Users.First(); - Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email}"); + 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 index 1df0b26..2e22cd6 100644 --- a/samples/AesSample.Fluent/UserEntity.cs +++ b/samples/AesSample.Fluent/UserEntity.cs @@ -12,6 +12,8 @@ public class UserEntity 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/Program.cs b/samples/AesSample/Program.cs index 6de605f..a227d2d 100644 --- a/samples/AesSample/Program.cs +++ b/samples/AesSample/Program.cs @@ -23,25 +23,31 @@ static void Main() byte[] encryptionIV = keyInfo.IV; var encryptionProvider = new AesProvider(encryptionKey, encryptionIV); - using var context = new DatabaseContext(options, encryptionProvider); - context.Database.EnsureCreated(); - - var user = new UserEntity + using (var context = new DatabaseContext(options, encryptionProvider)) { - FirstName = "John", - LastName = "Doe", - Email = "john@doe.com", - EncryptedData = new byte[2] { 1, 2 }, - EncryptedDataAsString = new byte[2] { 3, 4 } - }; + 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(); + context.Users.Add(user); + context.SaveChanges(); - Console.WriteLine($"Users count: {context.Users.Count()}"); + Console.WriteLine($"Users count: {context.Users.Count()}"); + } - user = context.Users.First(); + using (var context = new DatabaseContext(options, encryptionProvider)) + { + UserEntity user = context.Users.First(); - Console.WriteLine($"User: {user.FirstName} {user.LastName} - {user.Email}"); + 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 31c7402..0a9ee26 100644 --- a/samples/AesSample/UserEntity.cs +++ b/samples/AesSample/UserEntity.cs @@ -20,6 +20,10 @@ public class UserEntity [Encrypted] public string Email { get; set; } + [Required] + [Encrypted(StorageFormat.Binary)] + public string Notes { get; set; } + [Required] [Encrypted] public byte[] EncryptedData { get; set; }