ASP.NET Core Entity Framework中的数据层数据加密

13

我正在设计一个Web应用程序,需要将数据存储为加密格式。

计划使用的技术:

  • ASP.NET Core API
  • ASP.NET Core Entity Framework
  • MS SQL Server 2012
  • 任何Web前端
  • 由于规范要求,我们需要将所有数据以加密形式存储在数据库中。

在仍能使用Entity Framework & LINQ的情况下,有哪些好的方法可以实现这一目标,以便开发人员不必担心加密问题。

是否可能对整个数据库进行加密?


1
加密并非万能的。你想要保护自己免受哪些威胁? - Damien_The_Unbeliever
3个回答

19
首先,不要将加密和哈希混淆,在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);

当然,阅读文档,不要仅仅复制上面的代码。
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();

你可以通过在属性上使用getter和setter来避免使用值转换器,就像下面的代码一样。但是这意味着你需要将加密特定的代码放在实体中,并且你将不得不连接到任何你的加密提供程序的访问。这可以是一个静态类,或者你必须以某种方式传递它。
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...
  }
}

你可以看到,你需要在实体本身或提供程序中记忆你的加密和解密调用。
避免值转换器,同时仍然隐藏实体外部或数据库配置中的加密是复杂的。因此,如果性能是如此重要,以至于您无法使用值转换器,则您的加密可能不是可以隐藏在应用程序其余部分之外的东西,您将希望在完全独立于Entity Framework代码的代码中运行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)
        {
        }
    }
}

最后,加密的责任是复杂的,我建议确保您对所选择的设置有牢固的掌握,以防止像丢失加密密钥这样的数据丢失。此外,来自OWASP Cheatsheet系列的DotNet安全CheatSheet是一个有用的资源。


1
那是一篇有趣的阅读。谢谢,您会如何获取实体列表,然后解密数据呢? - Omar Ruder
感谢您详细的回答!您关于InitializationVector的正确,每个加密应该不同。这个更改已经计划在我的路线图上,但不幸的是,我没有足够的空闲时间来完成它。至于其他话题,这确实可以帮助改进库并避免库。最后,密码不应该像您所说的那样被加密,而应该被哈希。在我的示例中,我将[Encrypted]属性放在Password属性上,但那是一个错误...;-) - Eastrall
Omar,您可以像平常一样查询DbSet<ApplicationUser>,而无需使用ValueConverter,但是基于加密字段的过滤将不起作用。 - Steven
1
ASP.NET Core数据保护API不应用于长期加密。它的用途是用于身份验证令牌和cookie加密等事项。自动密钥轮换可能会导致数据丢失和系统故障。这里有一篇关于该主题的好文章:https://andrewlock.net/an-introduction-to-the-data-protection-system-in-asp-net-core/ - twinmind

9

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

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

您可以使用我的EntityFrameworkCore.DataEncryption插件,使用内置或自定义加密提供程序来加密您的字符串字段。实际上,目前只有AesProvider已经开发完成。

要使用它,只需将[Encrypted]属性添加到您的模型的字符串属性中,然后在您的DbContext类中重写OnModelCreating()方法,然后通过传递加密提供程序(AesProvider或任何继承自IEncryptionProvider的类)调用modelBuilder.UseEncryption(...)方法即可。

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

节约结果:

加密

希望这有所帮助。


3
可能值得在您的帖子中包含一些内容,明确指出您推荐的工具是您自己创建的之一。自我推广:“但是,您必须在答案中披露您的关联。” - Damien_The_Unbeliever
哦,我的错,我不知道宣传工具是被禁止的。如果必要的话,我会删除这个回答。 - Eastrall
1
不,正如所说的,是允许的,但您必须在答案中提及它。即使只是在自己的个人资料页面上链接也不足够。而且要节制使用。但它并不是被禁止的。 - Damien_The_Unbeliever
@Eastrall 当在.NET 5中使用时,我收到一个警告,即new AesProvider(this._encryptionKey, this._encryptionIV)是一个已弃用的构造函数。你能推荐一个不同的构造函数吗? - Peter B
@PeterB,一个拉取请求正在进行中,以重新集成此构造函数。请查看:https://github.com/Eastrall/EntityFrameworkCore.DataEncryption/pull/25 - Eastrall
这个能在 Fluent 设置中使用吗(而不是不使用 [Attribute])? - granadaCoder

4

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

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

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

这只是一个普通的模型类。您可以使用属性进行修饰,以及进行 Entity Framework 中通常所做的所有操作。

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<MyModel>().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));
        });
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接