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 数据加解密的一些坑相关推荐
- 《ASP.NET Core 6框架揭秘》实例演示[19]:数据加解密与哈希
数据保护(Data Protection)框架旨在解决数据在传输与持久化存储过程中的一致性(Integrity)和机密性(confidentiality)问题,前者用于检验接收到的数据是否经过篡改,后 ...
- T-SQL问题解决集锦——数据加解密
问题一:如何为数据进行加密与解密,避免使用者窃取机密数据? 对于一些敏感数据,如密码.卡号,一般不能使用正常数值来存储.否则会有安全隐患.以往的加密解密都有前端应用程序来辅助完成.而数据库一般只能加密 ...
- 数据安全之MySQL数据加解密的实现方案
在我们日常的业务需求中,经常会遇到需要对存储的用户敏感数据进行加密处理的场景,如用户的身份信息.住址.身份证号等等,本文我们就讨论下,业务系统(后端)如何实现数据存储(基于MySQL)的加解密功能. ...
- SpringBoot 优雅地对接口进行数据加解密
我是 ABin-阿斌:写一生代码,创一世佳话,筑一览芳华.如果小伙伴们觉得不错就一键三连吧~ 声明: 原作者:掘金:https://juejin.cn/user/3650034336532824 原文 ...
- 护网必备技能:Spring Boot 接口数据加解密 功能实现
护网必备技能:Spring Boot 接口数据加解密 功能实现 文章目录 护网必备技能:Spring Boot 接口数据加解密 功能实现 1. 尽量少改动,不影响之前的业务逻辑: 2. 考虑到时间紧迫 ...
- Spring Boot Post接口数据加解密
概述 今天这篇文章聊一聊接口安全问题,涉及到接口的加密.解密. 接口数据加解密流程图 涉及客户端和服务端的整体改造,可以在接口统一加 /secret/ 前缀来区分. 代码Demo 用户类型: @Dat ...
- Popular MVC框架请求响应数据加解密@Decrypt和@Encrypt的使用示例
简介 此项目用于演示popularmvc如何提供统一全自动化的API隐私数据保护,并且可以做到业务无感和灵活指定数据加解密算法. 请求数据加密使用@Decrypt注解,响应信息加密使用@Encrypt ...
- 数据加解密时Base64异常:Illegal base64 character 3f
现象 用base64工具类对中文进行处理时出现异常,在数据加解密场景中经常使用 java.lang.IllegalArgumentException: Illegal base64 character ...
- Postman-APP登入模拟请求-数据加解密
Postman-APP登入模拟请求-数据加解密 简单说明一下 对于Postman的使用,一般情况下只要发发确定的请求与参数就可以的了.这个大家都会的 ,灰常的简单. 然而,在使用的时候,尤其是接口测试 ...
最新文章
- Window10 64位,通过Python读取.mif, shp文件
- java 开发人员工具_Java开发人员应该知道的7种新工具
- python元胞自动机模拟交通_结构专栏 | 解析DEFORM软件中的元胞自动机法
- 消息队列终极解决方案——Stream(下)
- 写给数据小白:怎么让你的分析结论超出预期,不再是废纸一堆
- python调用TensorFlow时报错:FutureWarning: Passing (type, 1) or ‘1type‘ as a synonym of type is deprecated
- vue+sentry 前端异常日志监控
- 集线器、交换机和路由器之间的区别
- 男生种草潮流社区edge(嘿市),会是男生的小红书吗?
- go---token解析与构造
- Android开发者要搬运多少年代码才能突破年薪40W?
- XMind (2021)新增演说模式
- Java 中代码优化的 30 个小技巧(中)
- sqrt函数,对数进行开根号
- 摄像头8mm可以看多远_视频监控摄像头焦距、角度、距离参考值
- Nova组件源码分析之冷迁移与Resize
- SAP物料移动科目确认由浅入深无敌大解析
- 【单片机】简单I/O口扩展
- 计算机病毒是怎么工作的,计算机病毒是如何工作的(转载)
- vsCode搜索中文, 正则匹配