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

Amo*_*kar 8 .net c# entity-framework-core asp.net-core

我目前正在设计一个需要加密存储数据的 Web 应用程序。

计划使用的技术:

  • ASP.NET 核心 API
  • ASP.NET 核心实体框架
  • 微软 SQL Server 2012
  • 任何网络前端
  • 由于规范,我们需要将所有数据加密存储在数据库中。

这将是实现这一目标的好方法,同时仍然能够使用实体框架和 LINQ,因此开发人员不必处理加密。

是否可以加密整个数据库?

Eas*_*all 9

一个好的方法是在将更改保存到数据库时加密数据,并在从数据库读取数据时解密。

我开发了一个库来在 Entity Framework Core 上下文中提供加密字段。

使用内置或自定义加密提供程序保存更改时,您可以使用我的EntityFrameworkCore.DataEncryption插件来加密字符串字段。实际上,仅AesProvider开发了。

要使用它,只需将该属性添加[Encrypted]到模型的字符串属性中,然后重写类OnModelCreating()中的方法DbContext,然后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);
    }
}
Run Code Online (Sandbox Code Playgroud)

保存结果:

加密

希望能帮助到你。

  • 可能值得在您的帖子中添加一些内容,以明确您推荐的工具是您自己创建的工具之一。[自我推销](https://stackoverflow.com/help/behavior):“但是,您*必须*在回答中透露您的隶属关系。” (3认同)

小智 7

首先,不要将encrypting 与 hashing混淆,在 Eastrall 的回答中,它们暗示您可以对密码字段使用加密。不要这样做

此外,每次加密新值时都应该更改初始化向量,这意味着您应该避免像 Eastrall 的库那样为整个数据库设置单个 IV 的实现。

现代加密算法的设计速度很慢,因此加密数据库中的所有内容至少会略微影响您的性能。

如果处理得当,您的加密负载将不仅仅是密文,还应该包含加密密钥的 ID、所用算法的详细信息和签名。这意味着与等效的纯文本相比,您的数据将占用更多空间。看看https://github.com/blowdart/AspNetCoreIdentityEncryption如果你想看到你如何能实现一个自己。(该项目中的自述文件无论如何都值得一读)

考虑到这一点,您项目的最佳解决方案可能取决于将这些成本最小化对您来说有多重要。

如果您要Aes.Create();像 Eastrall 的答案中的库一样使用 .NET Core ,则密文将是一种byte[]类型。您可以将数据库提供程序中的列类型用于byte[],或者您可以编码为 base64 并存储为string. 通常存储为字符串是值得的:base64 将比 多占用 33% 的空间byte[],但更容易使用。

我建议使用ASP.NET Core 数据保护堆栈而不是Aes直接使用类,因为它可以帮助您进行密钥轮换并为您处理 base64 中的编码。您可以将它安装到您的 DI 容器中services.AddDataProtection(),然后让您的服务依赖于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);
Run Code Online (Sandbox Code Playgroud)

当然,阅读文档,不要只是复制上面的代码。


ASP.NET Core Identity 中,IdentityUserContext使用值转换器来加密标记有该[ProtectedPersonalData]属性的个人数据。Eastrall 的图书馆也在使用ValueConverter.

这种方法很方便,因为它不需要您在实体中编写代码来处理转换,如果您遵循域驱动设计方法(例如.NET Architecture Seedwork),这可能不是一种选择。

但是有一个缺点。如果您的实体上有很多受保护的字段。下面的代码会导致user对象上的每个加密字段都被解密,即使没有一个被读取。

var user = await context.Users.FirstOrDefaultAsync(u => u.Id == id);
user.EmailVerified = true;
await context.SaveChangesAsync();
Run Code Online (Sandbox Code Playgroud)

您可以通过在您的属性上使用 getter 和 setter 来避免使用值转换器,如下面的代码。但是,这意味着您需要在实体中放置特定于加密的代码,并且您必须连接到任何加密提供程序的访问权限。这可能是一个static类,或者你必须以某种方式传递它。

private string secret;

public string Secret {
  get => SomeAccessibleEncryptionObject.Decrypt(secret);
  set => secret = SomeAccessibleEncryptionObject.Encrypt(value);
}
Run Code Online (Sandbox Code Playgroud)

然后,您每次访问该属性时都会进行解密,这可能会在其他地方导致您意想不到的麻烦。例如,如果下面的代码emailsToCompare非常大,则可能会非常昂贵。

foreach (var email in emailsToCompare) {
  if(email == user.Email) {
    // do something...
  }
}
Run Code Online (Sandbox Code Playgroud)

您可以看到您需要在实体本身或提供程序中记住您的加密和解密调用。

避免使用值转换器同时仍然对实体外部或数据库配置隐藏加密是复杂的。因此,如果性能是一个很大的问题,以至于您无法使用值转换器,那么您的加密可能不是您可以从应用程序的其余部分隐藏的东西,您可能希望运行Protect()Unprotect()调用在完全在您的实体框架代码之外的代码中。


这是一个示例实现,其灵感来自 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)
        {
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,加密的责任很复杂,我建议您确保您牢牢掌握要进行的任何设置,以防止数据丢失等情况丢失您的加密密钥。此外,来自 OWASP Cheatsheet Series的DotNet Security CheatSheet是一个有用的阅读资源。

  • ASP.NET Core 数据保护 API 不应该用于长期加密。它旨在用于身份验证令牌和 cookie 加密等事情。自动密钥轮换可能会导致数据丢失和系统中断。这是一篇关于该主题的好文章 https://andrewlock.net/an-introduction-to-the-data-protection-system-in-asp-net-core/ (3认同)