C# Entity Framework Core 資料庫欄位自動加解密 示範

今天遇到一個需求,要把資料庫中其中一個資料表的密碼欄位加密,範例資料欄位如下:
    
public class ExternalSystemDto
{
    public string Name { get; set; } = null!;

    public string? Description { get; set; }

    public string? Url { get; set; }

    public string? UserName { get; set; }

    public string? Password { get; set; }
}
    

不能使用雜湊,因為會需要使用到實際的密碼內容,所以存入資料庫前要先加密,要使用時從資料庫中取得後再解密。都是使用 Entity Framework Core 存取。
這個資料表已經被使用很多次了,如果需要每個使用到的地方都修改的話會是個大工程,並且也難保未來使用的人一定會記得需要先解密,很繁瑣麻煩。

不過還好 Entity Framework Core 有提供 Value Conversions ,能夠自動將資料存入資料庫前和讀取時自動轉換資料,常常被用來轉換 enum ,將名稱和魔法數字代號相互轉換,非常方便。

要使用很簡單,繼承 ValueConverter ,這兩個泛型分別代表的意思就是從資料庫讀取時和寫入資料前的類別,可以是 int, string, date 等各種類型,本次範例兩個都是string 。需要注意的是轉換的方法需要在建構子就傳入,所以需要是靜態方法。

下面實作基本的 AES 加解密,在解密的過程中如果遇到問題就會直接回傳原始資訊,之後儲存時會自動加密

註: 在 Entity Framework Core 中預設資料沒有更新時就不會儲存進資料庫,所以只要傳入的還沒加密的資料和目前資料庫中的資料一樣,就不會儲存,造成未加密的假象。
    
using System.Security.Cryptography;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;

namespace Infrastructure.Data.Converter;

public class PasswordConverter : ValueConverter<string, string>
{
    public PasswordConverter(string key, string iv) : base(
        v => Encrypt(v, Key(key), Iv(iv)),
        v => Decrypt(v, Key(key), Iv(iv))
    )
    {
    }

    private static byte[] Key(string input) => Convert.FromBase64String(input);
    private static byte[] Iv(string input) => Convert.FromBase64String(input);

    private static string Encrypt(string value, byte[] key, byte[] iv)
    {
        using var aes = Aes.Create();
        aes.Key = key;
        aes.IV = iv;

        var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);

        using var msEncrypt = new MemoryStream();
        using var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write);
        using var swEncrypt = new StreamWriter(csEncrypt);

        swEncrypt.Write(value);
        swEncrypt.Close();

        return Convert.ToBase64String(msEncrypt.ToArray());
    }

    private static string Decrypt(string value, byte[] key, byte[] iv)
    {
        using var aes = Aes.Create();
        aes.Key = key;
        aes.IV = iv;

        try
        {
            var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);

            using var msDecrypt = new MemoryStream(Convert.FromBase64String(value));
            using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
            using var srDecrypt = new StreamReader(csDecrypt);

            return srDecrypt.ReadToEnd();
        }
        catch (Exception e)
        {
            Console.WriteLine($"解密失敗: {e.Message}");
            Console.WriteLine(e);

            return value;
        }
    }
}
    

建立了資料轉換器,那該怎麼使用呢?只要放在 DbContext 的 OnModelCreating 方法中,並指定欄位即可,下面的示範是 Entity 名稱為 ExternalSystem 的 Password 屬性需要自動加解密:
    
public partial class ApplicationDbContext : DbContext
{

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        string key = "w9DOlBGPKrn0WtAJrO+HcMWpwP9sZOUvy9t9lwxeVBc=";
        string iv = "/zribxOh6/6WI8uVQiZnoA==";

        var passwordConverter = new PasswordConverter(key, iv);
        modelBuilder.Entity<ExternalSystem>()
            .Property(e => e.Password)
            .HasConversion(passwordConverter);


		// 省略 ...
        
    }
}
    

阿要怎麼產生 AES 的 Key 和 IV?這裡也順便附上程式碼好了:
    
using System.Security.Cryptography;

    
// 產生隨機金鑰
using Aes myAes = Aes.Create();

var key = Convert.ToBase64String(myAes.Key);
var iv = Convert.ToBase64String(myAes.IV);

Console.WriteLine($"key: {key}");
Console.WriteLine($"iv: {iv}");
    



參考資料:
Microsoft - Value Conversions

留言