简体   繁体   English

使用 ASP.NET Core 实体框架在数据层中进行数据加密

[英]Data Encryption in Data Layer with ASP.NET Core Entity Framework

I am currently designing a web application where the data needs to be stored encrypted.我目前正在设计一个需要加密存储数据的 Web 应用程序。

Planned technologies used:计划使用的技术:

  • ASP.NET Core API ASP.NET 核心 API
  • ASP.NET Core Entity Framework ASP.NET 核心实体框架
  • MS SQL Server 2012微软 SQL Server 2012
  • any Web Frontend任何网络前端
  • Because of the specification, we need to store all data encrypted in the database.由于规范,我们需要将所有数据加密存储在数据库中。

Which would be a good approach to achieve this while still be able to use the Entity Framework & LINQ, so the developer does not have to take care of the encryption.这将是实现这一目标的好方法,同时仍然能够使用实体框架和 LINQ,因此开发人员不必处理加密。

Is it possible to encrypt the whole database?是否可以加密整个数据库?

First of all, don't confuse encrypting with hashing , in Eastrall's answer they imply that you could use encryption for a password field.首先,不要将encrypting 与 hashing混淆,在 Eastrall 的回答中,它们暗示您可以对密码字段使用加密。 Do not do this不要这样做

Also, you should change the initialisation vector every time you encrypt a new value, which means you should avoid implementations like Eastrall's library that set a single IV for the whole database.此外,每次加密新值时都应该更改初始化向量,这意味着您应该避免像 Eastrall 的库那样为整个数据库设置单个 IV 的实现。

Modern encryption algorithms are designed to be slow, so encrypting everything in your database is going to affect your performance at least marginally.现代加密算法的设计速度很慢,因此加密数据库中的所有内容至少会略微影响您的性能。

If done properly, your encrypted payload is not going to just be the cipher text, but should also contain the ID of the encryption key, details about the algorithm used, and a signature.如果处理得当,您的加密负载将不仅仅是密文,还应包含加密密钥的 ID、所用算法的详细信息和签名。 This means your data is going to take up a lot more space compared to the plain text equivalent.这意味着与等效的纯文本相比,您的数据将占用更多空间。 Take a look at https://github.com/blowdart/AspNetCoreIdentityEncryption if you want to see how you could implement that yourself.如果您想了解如何自己实现,请查看https://github.com/blowdart/AspNetCoreIdentityEncryption (The readme in that project is worth reading anyway) (该项目中的自述文件无论如何都值得一读)

With that in mind, the best solution for your project might depend on how important it is for you to minimise those costs.考虑到这一点,您项目的最佳解决方案可能取决于将这些成本最小化对您来说有多重要。

If you're going to use the .NET Core Aes.Create();如果你打算使用 .NET Core Aes.Create(); like in the library in Eastrall's answer, the cipher text is going to be a byte[] type.就像在 Eastrall 的答案中的图书馆一样,密文将是byte[]类型。 You could use the column type in your database provider for byte[] , or you could encode as base64 and store as a string .您可以将数据库提供程序中的列类型用于byte[] ,或者您可以编码为 base64 并存储为string Typically storing as a string is worthwhile: base64 will take up about 33% more space than byte[] , but is easier to work with.通常存储为字符串是值得的:base64 将比byte[]多占用大约 33% 的空间,但更容易使用。

I suggest making use of the ASP.NET Core Data Protection stack instead of using the Aes classes directly, as it helps you do key rotation and handles the encoding in base64 for you.我建议使用ASP.NET Core 数据保护堆栈而不是直接使用Aes类,因为它可以帮助您进行密钥轮换并为您处理 base64 中的编码。 You can install it into your DI container with services.AddDataProtection() and then have your services depend upon IDataProtectionProvider , which can be used like this:您可以使用services.AddDataProtection()将其安装到您的 DI 容器中,然后让您的服务依赖于IDataProtectionProvider ,它可以像这样使用:

// Make sure you read the docs for ASP.NET Core Data Protection!

// protect
var payload = dataProtectionProvider
    .CreateProtector("<your purpose string here>")
    .Protect(plainText);

// unprotect
var plainText = dataProtectionProvider
    .CreateProtector("<your purpose string here>")
    .Unprotect(payload);

Of course, read the documentation and don't just copy the code above.当然, 阅读文档,不要只是复制上面的代码。


In ASP.NET Core Identity, the IdentityUserContext uses a value converter to encrypt personal data marked with the [ProtectedPersonalData] attribute.ASP.NET Core Identity 中, IdentityUserContext使用值转换器来加密标记有[ProtectedPersonalData]属性的个人数据。 Eastrall's library is also using a ValueConverter . Eastrall 的图书馆也在使用ValueConverter

This approach is handy because it doesn't require you to write code in your entities to handle conversion, something that might not be an option if you are following a Domain Driven Design approach (eg like the .NET Architecture Seedwork ).这种方法很方便,因为它不需要您在实体中编写代码来处理转换,如果您遵循域驱动设计方法(例如.NET Architecture Seedwork ),这可能不是一种选择。

But there is a drawback.但是有一个缺点。 If you have a lot of protected fields on your entity.如果您的实体上有很多受保护的字段。 The code below would cause every single encrypted field on the user object to get decrypted, even though not a single one is being read.下面的代码会导致user对象上的每个加密字段都被解密,即使没有一个被读取。

var user = await context.Users.FirstOrDefaultAsync(u => u.Id == id);
user.EmailVerified = true;
await context.SaveChangesAsync();

You could avoid using a value converter by instead using a getter and setter on your property like the code below.您可以通过在您的属性上使用 getter 和 setter 来避免使用值转换器,如下面的代码。 However that means you will need to place encryption specific code in your entity, and you will have to wire up access to whatever your encryption provider is.但是,这意味着您需要在实体中放置特定于加密的代码,并且您必须连接到任何加密提供程序的访问权限。 This could be a static class, or you'll have to pass it in somehow.这可能是一个static类,或者您必须以某种方式传递它。

private string secret;

public string Secret {
  get => SomeAccessibleEncryptionObject.Decrypt(secret);
  set => secret = SomeAccessibleEncryptionObject.Encrypt(value);
}

You would then be decrypting every time you access the property, which could cause you unexpected trouble elsewhere.然后,您每次访问该属性时都会进行解密,这可能会在其他地方导致您意想不到的麻烦。 For example the code below could be very costly if emailsToCompare was very large.例如,如果emailsToCompare非常大,下面的代码可能会非常昂贵。

foreach (var email in emailsToCompare) {
  if(email == user.Email) {
    // do something...
  }
}

You can see that you'd need to memoize your encrypt and decrypt calls, either in the entity itself or in the provider.您可以看到您需要在实体本身或提供程序中记住您的加密和解密调用。

Avoiding the value converter while still hiding the encryption from outside the entity or the database configuration is complex.避免使用值转换器同时仍然对实体外部或数据库配置隐藏加密是复杂的。 And so if performance is so much of an issue that you can't go with the value converters, then your encryption is possibly not something that you can hide away from the rest of your application, and you would want to be running the Protect() and Unprotect() calls in code completely outside of your Entity Framework code.因此,如果性能是一个很大的问题,以至于您无法使用值转换器,那么您的加密可能不是您可以从应用程序的其余部分隐藏的东西,您可能希望运行Protect()Unprotect()调用完全在实体框架代码之外的代码。


Here is an example implementation inspired by the value converter setup in ASP.NET Core Identity but using an IDataProtectionProvider instead of IPersonalDataProtector :这是一个示例实现,其灵感来自 ASP.NET Core Identity 中的值转换器设置,但使用IDataProtectionProvider而不是IPersonalDataProtector

public class ApplicationUser
{
    // other fields...

    [Protected]
    public string Email { get; set; }
}

public class ProtectedAttribute : Attribute
{
}

public class ApplicationDbContext : DbContext
{
    public ApplicationDbContext(DbContextOptions options)
        : base(options)
    {
    }

    public DbSet<ApplicationUser> Users { get; set; }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        // other setup here..
        builder.Entity<ApplicationUser>(b =>
        {
            this.AddProtecedDataConverters(b);
        });
    }

    private void AddProtecedDataConverters<TEntity>(EntityTypeBuilder<TEntity> b)
        where TEntity : class
    {
        var protectedProps = typeof(TEntity).GetProperties()
            .Where(prop => Attribute.IsDefined(prop, typeof(ProtectedAttribute)));

        foreach (var p in protectedProps)
        {
            if (p.PropertyType != typeof(string))
            {
                // You could throw a NotSupportedException here if you only care about strings
                var converterType = typeof(ProtectedDataConverter<>)
                    .MakeGenericType(p.PropertyType);
                var converter = (ValueConverter)Activator
                    .CreateInstance(converterType, this.GetService<IDataProtectionProvider>());

                b.Property(p.PropertyType, p.Name).HasConversion(converter);
            }
            else
            {
                ProtectedDataConverter converter = new ProtectedDataConverter(
                    this.GetService<IDataProtectionProvider>());

                b.Property(typeof(string), p.Name).HasConversion(converter);
            }
        }
    }

    private class ProtectedDataConverter : ValueConverter<string, string>
    {
        public ProtectedDataConverter(IDataProtectionProvider protectionProvider)
            : base(
                    s => protectionProvider
                        .CreateProtector("personal_data")
                        .Protect(s),
                    s => protectionProvider
                        .CreateProtector("personal_data")
                        .Unprotect(s),
                    default)
        {
        }
    }

    // You could get rid of this one if you only care about encrypting strings
    private class ProtectedDataConverter<T> : ValueConverter<T, string>
    {
        public ProtectedDataConverter(IDataProtectionProvider protectionProvider)
            : base(
                    s => protectionProvider
                        .CreateProtector("personal_data")
                        .Protect(JsonSerializer.Serialize(s, default)),
                    s => JsonSerializer.Deserialize<T>(
                        protectionProvider.CreateProtector("personal_data")
                        .Unprotect(s),
                        default),
                    default)
        {
        }
    }
}

Finally, the responsibility of encryption is complex and I would recommend ensuring you have a firm grasp of whatever setup you go with to prevent things like data loss from losing your encryption keys.最后,加密的责任很复杂,我建议您确保您牢牢掌握要进行的任何设置,以防止数据丢失等情况丢失您的加密密钥。 Also, the DotNet Security CheatSheet from the OWASP Cheatsheet Series is a useful resource to read.此外,来自 OWASP Cheatsheet Series 的DotNet Security CheatSheet是一个有用的阅读资源。

A good approach would be to encrypt your data when saving changes to your database, and decrypt when reading you data from the database.一个好的方法是在保存对数据库的更改时加密数据,并在从数据库读取数据时解密。

I developed a library to provide encrypted fields within an Entity Framework Core context.我开发了一个库来在 Entity Framework Core 上下文中提供加密字段。

You can use my EntityFrameworkCore.DataEncryption plugin to encrypt your string fields when saving changes using a built-in or custom encryption provider.在使用内置或自定义加密提供程序保存更改时,您可以使用我的EntityFrameworkCore.DataEncryption插件来加密您的字符串字段。 Actually, only the AesProvider has been developed.实际上,只开发了AesProvider

To use it, simply add the [Encrypted] attribute to your string properties of your Model and then override the OnModelCreating() method in your DbContext class, and then call the modelBuilder.UseEncryption(...) by passing it an encryption provider ( AesProvider or any class that inherits from IEncryptionProvider .)要使用它,只需将[Encrypted]属性添加到模型的字符串属性中,然后覆盖DbContext类中的OnModelCreating()方法,然后通过将加密提供程序传递给它来调用modelBuilder.UseEncryption(...) ( AesProvider或任何继承自IEncryptionProvider类。)

public class UserEntity
{
    public int Id { get; set; }

    [Encrypted]
    public string Username { get; set; }

    [Encrypted]
    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<UserEntity> Users { get; set; }

    public DatabaseContext(DbContextOptions options)
        : base(options)
    {
        this._provider = new AesProvider(this._encryptionKey, this._encryptionIV);
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.UseEncryption(this._provider);
    }
}

Results on saving:保存结果:

加密

Hope it helps.希望能帮助到你。

First, thanks to @Steven as my answer is based on that.首先,感谢@Steven,因为我的回答基于此。

This solution extends what his solution has by adding some further configuration of the IDataProtectionProvider so as to enable migrations using the Package Manager Console in Visual Studio.该解决方案通过添加IDataProtectionProvider一些进一步配置来扩展他的解决方案,以便使用 Visual Studio 中的包管理器控制台启用迁移。 Also, I have selected to use varbinary as the backing data type in SQL.此外,我选择使用 varbinary 作为 SQL 中的支持数据类型。 Further, I have selected to store the encryption keys in the server runtime directory (so make sure you back them up if you do this).此外,我选择将加密密钥存储在服务器运行时目录中(因此,如果这样做,请确保备份它们)。

Program.cs程序.cs

Note that this solution was built using .NET 6.0, and so there is no longer a Startup.cs and Program.cs, rather only Program.cs.请注意,此解决方案是使用 .NET 6.0 构建的,因此不再有 Startup.cs 和 Program.cs,而只有 Program.cs。 If you are using an older template this should still work, you will just need to move some of the startup things around.如果您使用的是旧模板,这应该仍然有效,您只需要移动一些启动项。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.IO;

var keyDirectory = Path.Combine(AppContext.BaseDirectory, "Keys");
Directory.CreateDirectory(keyDirectory);

builder.Services.AddDataProtection()
    .SetApplicationName("My App Name")
    .PersistKeysToFileSystem(new DirectoryInfo(keyDirectory));

builder.Services.AddDbContext<MyDbContext>(options => options.UseSqlServer("My SQL connection string"));

MyModel.cs我的模型.cs

Just any model class.任何模型类。 You can decorate it with attributes and everything you would normally do for Entity Framework.您可以使用属性以及通常为 Entity Framework 所做的一切来装饰它。

public class MyModel 
{
    public string EncryptedProperty { get; set; }
}

EncryptedConverter.cs加密转换器.cs

This is where the magic happens, it will run between your application code and the database to make the encryption process entirely transparent.这就是魔法发生的地方,它将在您的应用程序代码和数据库之间运行,使加密过程完全透明。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System.Text.Json;

/// <summary>
/// Converts string values to and from their underlying encrypted representation
/// </summary>
public class EncryptedConverter : EncryptedConverter<string>
{
    public EncryptedConverter(IDataProtectionProvider dataProtectionProvider) : base(dataProtectionProvider) { }
}

/// <summary>
/// Converts property values to and from their underlying encrypted representation
/// </summary>
/// <typeparam name="TProperty">The property to encrypt or decrypt</typeparam>
public class EncryptedConverter<TProperty> : ValueConverter<TProperty, byte[]>
{
    private static readonly JsonSerializerOptions? options;

    public EncryptedConverter(IDataProtectionProvider dataProtectionProvider) :
        base(
            x => dataProtectionProvider.CreateProtector("encryptedProperty").Protect(JsonSerializer.SerializeToUtf8Bytes(x, options)),
            x => JsonSerializer.Deserialize<TProperty>(dataProtectionProvider.CreateProtector("encryptedProperty").Unprotect(x), options),
            default
        )
    { }
}

MyDbContext.cs MyDbContext.cs

The main difference here is adding another constructor to enable usage of encrypted properties during migrations.这里的主要区别是添加另一个构造函数以在迁移期间启用加密属性的使用。 This also allows seeding database values using the builder.Entity<MyModel>().HasData() method.这也允许使用builder.Entity<MyModel>().HasData()方法播种数据库值。 The seeded values will encrypt correctly.种子值将正确加密。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using System;
using System.IO;

public class MyDbContext : DbContext 
{
    private IDataProtectionProvider dataProtectionProvider;

    /// <summary>
    /// For migrations
    /// </summary>
    public MyDbContext() 
    {
        // Note that this should match your options in Program.cs
        var info = new DirectoryInfo(Path.Combine(AppContext.BaseDirectory, "Keys"));

        var provider = DataProtectionProvider.Create(info, x =>
        {
            x.SetApplicationName("My App Name");
            x.PersistKeysToFileSystem(info);
        });

        dataProtectionProvider = provider;
    }

    public MyDbContext(DbContextOptions<MyDbContext> options, IDataProtectionProvider dataProtectionProvider) : base(options)
    {
        this.dataProtectionProvider = dataProtectionProvider;
    }

    public DbSet<MyModel> MyModel { get; set; }

    protected override void OnModelCreating(ModelBuilder builder) 
    {
        builder.Entity<MyModel>(model => 
        {
            model.Property(x => x.EncryptedProperty)
                .HasColumnType("varbinary(max)")
                .HasConversion(new EncryptedConverter(dataProtectionProvider));
        });
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 在 Entity Framework Core 和 ASP.NET Core 中加载相关数据 - Loading related data in Entity Framework Core and ASP.NET Core ASP.NET 核心 Web API - 使用实体框架在 .NET 核心 6 中播种数据 - ASP.NET Core Web API - seeding data in .NET Core 6 using Entity Framework 如何使表示层(ASP.NET MVC)的数据访问技术(Entity Framework)无知? - How to make the data access technology (Entity Framework) ignorance from the presentation layer (ASP.NET MVC)? 带有身份的ASP.NET:实体框架在SaveChanges上引发System.Data.Entity.Core.UpdateException - ASP.NET w/ Identity: Entity Framework throws System.Data.Entity.Core.UpdateException on SaveChanges 使用 ASP.NET Core 和 Entity Framework Core 进行集成测试 - 如何在每个测试中恢复数据库中的测试数据? - Integration Testing with ASP.NET Core and Entity Framework Core - How to Restore Test Data in Database Per Test? 是否可以禁用 ASP.NET Core 中的数据保护加密? - Is it possible to disable the data protection encryption in ASP.NET Core? 在 ASP.Net Core MVC 数据层中获取连接字符串 - Getting Connection String in ASP.Net Core MVC data layer ASP.NET Core MVC &amp; C#:使用实体框架将数据插入数据库 - ASP.NET Core MVC & C# : insert data into database using Entity Framework ASP.NET Core如何在ViewModel Entity Framework中加载相关数据 - Asp.net core how to loading related data in viewmodel Entity Framework ASP.Net Core - 实体框架 - 调用没有返回数据的存储过程(在 void 方法中) - ASP.Net Core - Entity Framework - Call Stored Procedure With No Return Data (in a void method)
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM