前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ASP.NET Core 数据加解密的一些坑

ASP.NET Core 数据加解密的一些坑

作者头像
Edi Wang
发布2019-07-08 19:38:23
1.6K0
发布2019-07-08 19:38:23
举报
文章被收录于专栏:汪宇杰博客汪宇杰博客

ASP.NET Core 给我们提供了自带的Data Protection机制,用于敏感数据加解密,带来方便的同时也有一些限制可能引发问题,这几天我就被狠狠爆了一把

我的场景

我的博客系统有个发送邮件通知的功能,因此需要配置一个邮箱账号,让程序去用该账号像管理员或用户发送邮件。这就牵涉到如何安全存储账户密码的问题了。作为有节操的程序员,我们当然不能像国内众多平台一样存储明文密码到数据库。在这个场景里,我们也没法用HASH存储密码,因为发邮件是系统后台自己完成的,不会要求用户输入密码进行HASH运算之后与数据库存储的HASH对比。因此,我首先想到的就是用AES这样的对称加密算法,在数据库里存储加密后的密文,由程序根据Key去解密,然后使用该账号发送邮件。

不想重复造轮子

在设计一个功能之前,我通常会先查阅资料,看看是否有框架自带的功能可以完成需求。于是,ASP.NET Core自带的Data Protection引起了我的注意。

冗长的官方文档大家可以自己去看,这里我做一下总结:

使用Data Protection API的好处在于:

  1. 淘汰传统的MachineKey。
  2. 无需自己去设计加密算法,直接使用框架提供的,由专业的微软保证安全的算法即可。
  3. 无需自己管理密钥,默认情况下框架会自动生成以及选择对应的存储方式。
  4. 密钥默认情况每90天自动更替一次。
  5. 编程方式简单,通常情况下无需深入了解原理即可完成需求。
  6. 保留灵活性和拓展性,允许自定义算法、密钥存储等步骤。

有关Data Protection的详细介绍,可以看官方文档:

https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/introduction?view=aspnetcore-2.2

Data Protection 默认用的算法就是AES,可以满足我的需要。

加解密过程

框架帮我们隐藏复杂的算法过程之后,我们只要简单3部,就能完成加解密。

通常的实践是:在Startup里添加DataProtection服务

代码语言:javascript
复制
public void ConfigureServices(IServiceCollection services)
{
    services.AddDataProtection();
    // ...
}

然后创建一个类似这样的Service供系统其他地方加解密数据。

代码语言:javascript
复制
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加解密函数:

加密

代码语言:javascript
复制
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;
}

解密

代码语言:javascript
复制
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类型

代码语言:javascript
复制
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)

代码语言:javascript
复制
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了,保证之后的加解密数据都是一致的。

使用方式

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 汪宇杰博客 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档