我正在设计一个Web应用程序,需要将数据存储为加密格式。
计划使用的技术:
- ASP.NET Core API
- ASP.NET Core Entity Framework
- MS SQL Server 2012
- 任何Web前端
- 由于规范要求,我们需要将所有数据以加密形式存储在数据库中。
在仍能使用Entity Framework & LINQ的情况下,有哪些好的方法可以实现这一目标,以便开发人员不必担心加密问题。
是否可能对整个数据库进行加密?
我正在设计一个Web应用程序,需要将数据存储为加密格式。
计划使用的技术:
在仍能使用Entity Framework & LINQ的情况下,有哪些好的方法可以实现这一目标,以便开发人员不必担心加密问题。
是否可能对整个数据库进行加密?
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是一个有用的资源。
[Encrypted]
属性放在Password
属性上,但那是一个错误...;-) - Eastrall一种好的方法是在将数据保存更改到数据库时进行加密,并在从数据库中读取数据时进行解密。
我开发了一个库,在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);
}
}
节约结果:
希望这有所帮助。
new AesProvider(this._encryptionKey, this._encryptionIV)
是一个已弃用的构造函数。你能推荐一个不同的构造函数吗? - Peter B首先,感谢@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));
});
}
}