ASP.NET Core 数据加解密的一些坑
點擊藍字
關注我
ASP.NET Core 給我們提供了自帶的Data Protection機制,用于敏感數據加解密,帶來方便的同時也有一些限制可能引發問題,這幾天我就被狠狠爆了一把
我的場景
我的博客系統有個發送郵件通知的功能,因此需要配置一個郵箱賬號,讓程序去用該賬號像管理員或用戶發送郵件。這就牽涉到如何安全存儲賬戶密碼的問題了。作為有節操的程序員,我們當然不能像國內眾多平臺一樣存儲明文密碼到數據庫。在這個場景里,我們也沒法用HASH存儲密碼,因為發郵件是系統后臺自己完成的,不會要求用戶輸入密碼進行HASH運算之后與數據庫存儲的HASH對比。因此,我首先想到的就是用AES這樣的對稱加密算法,在數據庫里存儲加密后的密文,由程序根據Key去解密,然后使用該賬號發送郵件。
不想重復造輪子
在設計一個功能之前,我通常會先查閱資料,看看是否有框架自帶的功能可以完成需求。于是,ASP.NET Core自帶的Data Protection引起了我的注意。
冗長的官方文檔大家可以自己去看,這里我做一下總結:
使用Data Protection API的好處在于:
淘汰傳統的MachineKey。
無需自己去設計加密算法,直接使用框架提供的,由專業的微軟保證安全的算法即可。
無需自己管理密鑰,默認情況下框架會自動生成以及選擇對應的存儲方式。
密鑰默認情況每90天自動更替一次。
編程方式簡單,通常情況下無需深入了解原理即可完成需求。
保留靈活性和拓展性,允許自定義算法、密鑰存儲等步驟。
有關Data Protection的詳細介紹,可以看官方文檔:
https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-2.2
Data Protection 默認用的算法就是AES,可以滿足我的需要。
加解密過程
框架幫我們隱藏復雜的算法過程之后,我們只要簡單3部,就能完成加解密。
通常的實踐是:在Startup里添加DataProtection服務
public void ConfigureServices(IServiceCollection services)
{
? ? services.AddDataProtection();
? ? // ...
}
然后創建一個類似這樣的Service供系統其他地方加解密數據。
public class EncryptionService
{
? ? private readonly IDataProtectionProvider _dataProtectionProvider;
? ? private const string Key = "cxz92k13md8f981hu6y7alkc";
? ? public EncryptionService(IDataProtectionProvider dataProtectionProvider)
? ? {
? ? ? ? _dataProtectionProvider = dataProtectionProvider;
? ? }
? ? public string Encrypt(string input)
? ? {
? ? ? ? var protector = _dataProtectionProvider.CreateProtector(Key);
? ? ? ? return protector.Protect(input);
? ? }
? ? public string Decrypt(string cipherText)
? ? {
? ? ? ? var protector = _dataProtectionProvider.CreateProtector(Key);
? ? ? ? return protector.Unprotect(cipherText);
? ? }
}
我用該方法,加密了郵箱密碼,并存儲到數據庫。然后更改了對應的代碼從數據中成功解密,并在自己機器上調試完成發送郵件的功能,沒有問題。于是我部署到了生產環境……
坑來了
生產環境解密數據庫中的密文時發生了異常
System.Security.Cryptography.CryptographicException: The key {bd424a84-5faa-4b97-8cd9-6bea01f052cd} was not found in the key ring.
經過研究,這是因為,ASP.NET Core在不同機器上運行的時候,會生成不同的Key用來加密數據,而我數據庫里的密文是用開發機的Key加密的,和服務器的Key不一樣。因此嘗試解密的時候,找不到加密用的Key,就產生了這個異常。
ASP.NET Core 可以將Key保存在注冊表、用戶profile、Azure KeyVault、Azure 存儲賬戶、文件系統等多種位置。
在Azure App Service下,Key被保存在了%HOME%\ASP.NET\DataProtection-Keys文件夾里。這個文件夾會非常神奇的自動同步到App Service的其他Instance下。
有興趣的猿可以在Kudu工具里看到這個文件夾:
因此要解決不同環境Key不一致的問題,只需要找一個一致的存儲位置即可。但這并不能解決問題!因為默認情況下,每90天會重新生成一個新的Key,這樣數據庫里的密文如果不更新的話,又會失效。
另外,ASP.NET Core表單使用的AntiForgeryToken也使用這套機制加密。因此如果你自己部署了多個instance的服務器(而不是用App Service去彈性擴充),就會導致每臺服務器的key不同,用戶提交表單會驗證失敗。
解決方法
雖然我們可以做到用統一的位置保存Key,也能指定自動刷新周期,但我并不建議這樣做。因為這套機制只適用于加密短時效的數據,并不是針對被持久化到數據庫里的數據而設計的。所以在這種場景下,我們還是得自己寫一個加解密的服務。
先(很不要臉的)從微軟官方文檔里拷一對AES加解密函數:
加密
private static byte[] EncryptStringToBytes_Aes(string plainText, byte[] key, byte[] iv)
{
? ? if (plainText == null || plainText.Length <= 0)
? ? ? ? throw new ArgumentNullException(nameof(plainText));
? ? if (key == null || key.Length <= 0)
? ? ? ? throw new ArgumentNullException(nameof(key));
? ? if (iv == null || iv.Length <= 0)
? ? ? ? throw new ArgumentNullException(nameof(iv));
? ? byte[] encrypted;
? ? using (var aesAlg = Aes.Create())
? ? {
? ? ? ? aesAlg.Key = key;
? ? ? ? aesAlg.IV = iv;
? ? ? ? var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
? ? ? ? using (var msEncrypt = new MemoryStream())
? ? ? ? {
? ? ? ? ? ? using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? using (var swEncrypt = new StreamWriter(csEncrypt))
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? swEncrypt.Write(plainText);
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? encrypted = msEncrypt.ToArray();
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? return encrypted;
}
解密
private static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] key, byte[] iv)
{
? ? if (cipherText == null || cipherText.Length <= 0)
? ? ? ? throw new ArgumentNullException(nameof(cipherText));
? ? if (key == null || key.Length <= 0)
? ? ? ? throw new ArgumentNullException(nameof(key));
? ? if (iv == null || iv.Length <= 0)
? ? ? ? throw new ArgumentNullException(nameof(iv));
? ? string plaintext;
? ? using (var aesAlg = Aes.Create())
? ? {
? ? ? ? aesAlg.Key = key;
? ? ? ? aesAlg.IV = iv;
? ? ? ? var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
? ? ? ? using (var msDecrypt = new MemoryStream(cipherText))
? ? ? ? {
? ? ? ? ? ? using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
? ? ? ? ? ? {
? ? ? ? ? ? ? ? using (var srDecrypt = new StreamReader(csDecrypt))
? ? ? ? ? ? ? ? {
? ? ? ? ? ? ? ? ? ? plaintext = srDecrypt.ReadToEnd();
? ? ? ? ? ? ? ? }
? ? ? ? ? ? }
? ? ? ? }
? ? }
? ? return plaintext;
}
定義一個EncryptionService
為了方便使用,加密結果我喜歡輸出為string類型
public class EncryptionService
{
? ? private readonly KeyInfo _keyInfo;
? ? public EncryptionService(KeyInfo keyInfo = null)
? ? {
? ? ? ? _keyInfo = keyInfo;
? ? }
? ? public string Encrypt(string input)
? ? {
? ? ? ? var enc = EncryptStringToBytes_Aes(input, _keyInfo.Key, _keyInfo.Iv);
? ? ? ? return Convert.ToBase64String(enc);
? ? }
? ? public string Decrypt(string cipherText)
? ? {
? ? ? ? var cipherBytes = Convert.FromBase64String(cipherText);
? ? ? ? return DecryptStringFromBytes_Aes(cipherBytes, _keyInfo.Key, _keyInfo.Iv);
? ? }
? ? // 微軟那兩個加解密函數...
}
其中KeyInfo設計成一個單獨的類,用來靈活的讓用戶選擇賦值byte[]數組還是string類型的Key以及初始向量(IV)
public class KeyInfo
{
? ? public byte[] Key { get; }
? ? public byte[] Iv { get; }
? ? public string KeyString => Convert.ToBase64String(Key);
? ? public string IVString => Convert.ToBase64String(Iv);
? ? public KeyInfo()
? ? {
? ? ? ? using (var myAes = Aes.Create())
? ? ? ? {
? ? ? ? ? ? Key = myAes.Key;
? ? ? ? ? ? Iv = myAes.IV;
? ? ? ? }
? ? }
? ? public KeyInfo(string key, string iv)
? ? {
? ? ? ? Key = Convert.FromBase64String(key);
? ? ? ? Iv = Convert.FromBase64String(iv);
? ? }
? ? public KeyInfo(byte[] key, byte[] iv)
? ? {
? ? ? ? Key = key;
? ? ? ? Iv = iv;
? ? }
}
注冊到DI容器
services.AddTransient(ec => new EncryptionService(new KeyInfo("45BLO2yoJkvBwz99kBEMlNkxvL40vUSGaqr/WBu3+Vg=", "Ou3fn+I9SVicGWMLkFEgZQ==")));
其中的Key和IV可以通過KeyInfo的無參構造函數獲得。自己保存下來以后,就可以一直用這一對Key了,保證之后的加解密數據都是一致的。
使用方式
private readonly EncryptionService _encryptionService;
public HomeController(EncryptionService encryptionService)
{
? ? _encryptionService = encryptionService;
}
public IActionResult Index()
{
? ? var str = "Hello";
? ? var enc = _encryptionService.Encrypt(str);
? ? var dec = _encryptionService.Decrypt(enc);
? ? return Content($"str: {str}, enc: {enc}, dec: {dec}");
}
總結
ASP.NET Core 自帶的Data Protection API非常安全,使用方便,也比較靈活。但要注意Key存儲以及定時刷新,只適用短時效的加密。對于長時間保存的固定密文,可以自己實現一個加解密服務。
完整的案例代碼參見我的GitHub:
https://github.com/EdiWang/DotNet-Samples/tree/master/AspNet-AES-Non-DPAPI
總結
以上是生活随笔為你收集整理的ASP.NET Core 数据加解密的一些坑的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【.NET Core项目实战-统一认证平
- 下一篇: 人工智能第六课:如何做研究