【问题标题】:Data Encryption in Data Layer with ASP.NET Core Entity Framework使用 ASP.NET Core Entity Framework 在数据层中进行数据加密
【发布时间】:2019-07-06 21:14:47
【问题描述】:

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

使用的计划技术:

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

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

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

【问题讨论】:

  • 加密不是魔杖。您要防范哪些威胁

标签: c# .net asp.net-core entity-framework-core


【解决方案1】:

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

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

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

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

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

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

我建议使用ASP.NET Core Data Protection stack 而不是直接使用Aes 类,因为它可以帮助您进行密钥轮换并为您处理base64 编码。您可以使用 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);

当然,read the documentation,不要只是复制上面的代码。


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

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

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

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

您可以避免使用值转换器,而是在您的属性上使用 getter 和 setter,如下面的代码。然而,这意味着您将需要在您的实体中放置特定于加密的代码,并且您必须连接对您的加密提供者的访问。这可能是一个 static 类,或者您必须以某种方式将其传递。

private string secret;

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

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

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

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

避免使用值转换器,同时仍然对实体外部隐藏加密或数据库配置很复杂。因此,如果性能问题如此严重以至于您无法使用值转换器,那么您的加密可能无法隐藏在应用程序的其余部分之外,您可能希望运行 @987654347 @ 和 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)
        {
        }
    }
}

最后,加密的责任很复杂,我建议您确保牢牢掌握所采用的任何设置,以防止数据丢失等事情丢失您的加密密钥。此外,OWASP 备忘单系列中的DotNet Security CheatSheet 是一个有用的阅读资源。

【讨论】:

  • 读起来很有趣。谢谢,您将如何继续获取您的实体列表然后解密数据?
  • 感谢您的详细解答!您对 InitializationVector 的看法是正确的,它应该对于每个加密都是相同的。我的路线图已经计划了这些更改,但不幸的是,我没有足够的空闲时间来做这件事。至于其他主题,这确实有助于改进图书馆并避免图书馆。最后,密码不应该像你说的那样加密,而是散列。在我的示例中,我将 [Encrypted] 属性放在了 Password 属性上,但这是一个错误... ;-)
  • Omar,您将能够像在没有 ValueConverter 的情况下通常那样查询您的 DbSet,但基于您的加密字段的过滤将不起作用
  • ASP.NET Core 数据保护 API 不应该用于长期加密。它旨在用于身份验证令牌和 cookie 加密之类的事情。自动密钥轮换可能导致数据丢失和系统中断。这是一篇关于这个主题的好文章andrewlock.net/…
【解决方案2】:

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

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

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

要使用它,只需将[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);
    }
}

保存结果:

希望对你有帮助。

【讨论】:

  • 可能值得在您的帖子中包含一些内容,以明确您推荐的工具是您自己创造的工具之一。 self-promotion:“但是,你必须在你的回答中披露你的从属关系。”
  • 哦,我的错,我不知道宣传工具是被禁止的。如果需要,我会删除答案。
  • 不,正如它所说,这是允许的,但你必须在你的答案中提及它。甚至是不够的只需将其链接到您自己的个人资料页面即可。并谨慎行事。但这不是禁止
  • @Eastrall 在 .NET 5 中使用时,我收到一条警告,指出 new AesProvider(this._encryptionKey, this._encryptionIV) 是一个已弃用的构造函数。你能推荐一个不同的构造函数吗?
  • @PeterB 正在进行拉取请求以重新集成此构造函数。请查收:github.com/Eastrall/EntityFrameworkCore.DataEncryption/pull/25
【解决方案3】:

首先,感谢@Steven,因为我的回答是基于此。

此解决方案通过添加IDataProtectionProvider 的一些进一步配置来扩展他的解决方案,以便使用 Visual Studio 中的包管理器控制台启用迁移。此外,我选择使用 varbinary 作为 SQL 中的支持数据类型。此外,我已选择将加密密钥存储在服务器运行时目录中(因此,如果您这样做,请确保备份它们)。

Program.cs

请注意,此解决方案是使用 .NET 6.0 构建的,因此不再有 Startup.cs 和 Program.cs,而只有 Program.cs。如果您使用的是较旧的模板,这应该仍然有效,您只需要移动一些启动的东西。

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

任何模型类。您可以使用属性和通常为实体框架所做的一切来装饰它。

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

EncryptedConverter.cs

这就是神奇之处,它将在您的应用程序代码和数据库之间运行,以使加密过程完全透明。

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

这里的主要区别是添加了另一个构造函数以在迁移期间启用加密属性的使用。这也允许使用builder.Entity&lt;MyModel&gt;().HasData() 方法播种数据库值。种子值将正确加密。

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));
        });
    }
}

【讨论】:

    猜你喜欢
    • 2019-10-25
    • 2020-06-16
    • 1970-01-01
    • 2019-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-11
    相关资源
    最近更新 更多