数据保护(Data Protection)框架旨在解决数据在传输与持久化存储过程中的一致性(Integrity)和机密性(confidentiality)问题,前者用于检验接收到的数据是否经过篡改,后者通过对原始的数据进行加密以避免真实的内容被人窥视。数据保护是支撑ASP.NET身份认证的一个重要的基础框架,同时也可以作为独立的框架供我们使用。[本文节选《ASP.NET Core 6框架揭秘》第13章]

[S1301]数据的加解密(源代码)
[S1302]Purpose字符串一致性(源代码)
[S1303]设置加密内容的有效期(源代码)
[S1304]撤销加密密钥(单个密钥)(源代码)
[S1305]撤销加密密钥(所有密钥)(源代码)
[S1306]瞬时加解密(源代码)
[S1307]密钥哈希(源代码)

[S1301]数据的加解密

对提供的原始数据(字符串或者二进制数组)进行加密是数据保护框架体提供的基本功能,接下来我们利用一个简单的控制台程序来演示一下加解密如何实现。数据的加解密均由IDataProtector对象来完成,而该对象由IDataProtectionProvider(不是IDataProtectorProvider)对象来提供,所以在大部分应用场景中针对数据的加密和解密只涉及这两个对象。有了依赖注入的加持,我们也不需要了解这两个接口的具体实现类型,只需要在利用注入的IDataProtectionProvider对象来提供对应的IDataProtector对象,并利用后者完成加解密的工作。

上述的这两个接口定义在 “Microsoft.AspNetCore.DataProtection.Abstractions”这个NuGet包中,它们的默认实现类型以及其他核心类型则承载于NuGet包 “Microsoft.AspNetCore.DataProtection”中,所以我们需要为演示程序添加针对这个NuGet包的引用。由于需要使用到依赖注入框架,我们需要添加针对“Microsoft.Extensions.DependencyInjection”的引用。必要的NuGet包引用添加完成之后,我们编写了如下的演示程序。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;var originalPayload = Guid.NewGuid().ToString();
var protectedPayload = Encrypt("foo", originalPayload);
var unprotectedPayload = Decrypt("foo", protectedPayload);
Debug.Assert(originalPayload == unprotectedPayload);static string Encrypt(string purpose, string originalPayload) => GetDataProtector(purpose).Protect(originalPayload);
static string Decrypt(string purpose, string protectedPayload) => GetDataProtector(purpose).Unprotect(protectedPayload);static IDataProtector GetDataProtector(string purpose)
{var services = new ServiceCollection();services.AddDataProtection();return services.BuildServiceProvider().GetRequiredService<IDataProtectionProvider>().CreateProtector(purpose);
}

如上面的代码片段所示,我们将数据的加密和解密操作分别定义在Encrypt和Decrypt方法中,它们使用IDataProtector对象由GetDataProtector方法来提供。在GetDataProtector方法中,我们创建了一个ServiceCollection对象,并调用AddDataProtection扩展方法注册了数据保护框架的基础服务。我们最终利用构建的IServiceProvider对象来提供所需的IDataProtectionProvider对象。IDataProtectionProvider接口的CreateProtector方法定义了一个字符串类型名为“purpose”的参数。从字面上来讲,该参数表示加密的“目的(Purpose)”,它在整个数据保护模型中起到了“秘钥隔离”的作用,我们在本书后续内容中将其称为“Purpose字符串”。

Encrypt和Decrypt方法来利用指定的Purpose字符串作为参数调用GetDataProtector方法得到对应的IDataProtector对象之后,分别调用了该对象的Protect和Unprotect方法完成了针对给定文本内容的加密和解密。我们使用一个GUID转换的字符串作为待加密的数据,并使用“foo”作为Purpose字符串调用Encrypt方法对它进行了加密,最后采用相同的Purpose字符串调用Decrypt方法对加密内容进行解密。

前面的演示实例通过调用IServiceProvider对象的GetRequiredService<T>扩展方法得到所需的IDataProtectionProvider对象,该对象也可以按照如下的形式调用GetDataProtectionProvider扩展方法来获取。IServiceProvider接口还定义了如下这个GetDataProtector扩展方法直接返回IDataProtector对象。

...
static IDataProtector GetDataProtector(string purpose)
{var services = new ServiceCollection();services.AddDataProtection();return services.BuildServiceProvider().GetDataProtectionProvider().CreateProtector(purpose);
}

或者

...
static IDataProtector GetDataProtector(string purpose)
{var services = new ServiceCollection();services.AddDataProtection();return services.BuildServiceProvider().GetDataProtector(purpose);
}

除了利用依赖注入框架,我们也可以按照如下的方法利用静态类型DataProtectorProvider(定义在“Mcrosoft.AspNetCore.DataProtection.Extensions”NuGet包中)来创建IDataProtectionProvider对象。该类型提供了若干用于创建IDataProtector对象的Create方法重载,我们选择的重载传入的参数为当前应用的名称。

...
static IDataProtector GetDataProtector(string purpose) => DataProtectionProvider.Create("App").CreateProtector(purpose);
[S1302]Purpose字符串一致性

前面我们说到参与同一份数据加解密的两个IDataProtector对象必须具有一致的Purpose字符串,我们现在就来验证这一点。如下面的代码片段所示,我们在调用Decrypt方法进行解密的时候将Purpose字符串从“foo”替换成“bar”。

...
var originalPayload = Guid.NewGuid().ToString();
var protectedPayload = Encrypt ("foo", originalPayload);
var unprotectedPayload = Decrypt ("bar", protectedPayload);
Debug.Assert(originalPayload == unprotectedPayload);
...

当我们调用IDataProtector对象的Unprotect方法对指定内容进行解密时,由于当前Purpose字符串与待解密内容采用的Purpose字符串不符,会直接抛出如图1所示的CryptographicException异常。

图1 Purpose字符串不一致导致的异常

[S1303]设置加密内容的有效期

我们知道不论采用的何种加密算法,采用的秘钥位数有多长,如果算力资源或者时间充足,解密都能成功。但是黑客具有的算力资源总归是有限的,如果能够在秘钥能推算出来之前就已经无效了,那么我们采用的加密方式就是安全的。针对有效时间的加解密通过ITimeLimitedDataProtector对象来完成,这个接口都定义在“Mcrosoft.AspNetCore.DataProtection.Extensions” 这个NuGet包中。为了使用这个对象,我们将演示程序改写成如下的形式。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;var originalPayload = Guid.NewGuid().ToString();
var protectedPayload = Encrypt("foo", originalPayload, TimeSpan.FromSeconds(5));var unprotectedPayload = Decrypt("foo", protectedPayload);
Debug.Assert(originalPayload == unprotectedPayload);await Task.Delay(5000);
Decrypt("foo", protectedPayload);static string Encrypt(string purpose, string originalPayload, TimeSpan timeout)=> GetDataProtector(purpose).Protect(originalPayload, DateTimeOffset.UtcNow.Add(timeout));
static string Decrypt(string purpose, string protectedPayload)=> GetDataProtector(purpose).Unprotect(protectedPayload, out _);static ITimeLimitedDataProtector GetDataProtector(string purpose)
{var services = new ServiceCollection();services.AddDataProtection();return services.BuildServiceProvider().GetDataProtector(purpose).ToTimeLimitedDataProtector();
}

我们让GetDataProtector方法返回一个ITimeLimitedDataProtector对象,它通过IDataProtector对象的ToTimeLimitedDataProtector扩展方法“转化”而成。用于加密的Encrypt方法添加了一个表示过期时间的timeout参数(类型为TimeSpan),由于ITimeLimitedDataProtector的Protect方法中表示过期时间的参数类型为DateTimeOffset,所以我们基于当前时间和指定的过期时间(TimeSpan)将这个过期时间点计算出来。ITimeLimitedDataProtector接口用于解密的Unprotect方法具有一个表示过期日期的输出参数。

在演示程序中,我们调用Encrypt方法对数据进行加密时将过期时间设置为5秒。对于加密后的内容,我们采用相同的方式对它进行了两次解密,第一个发生在5秒内,第二次则发生在5秒后。程序运行后,第一次解密成功,第二次抛出如图13-3所示的CryptographicException异常。


图2 加密数据过期导致的解密异常

[S1304]撤销加密密钥(单个密钥)

在如下的演示程序中,我们创建了ServiceCollection对象并调用AddDataProtection扩展方法注册了数据保护框架的核心服务。在利用构建的IServiceProvider对象得到IDataProtector对象之后,我们利用它对指定的文本进行加密。在此之后,我们将加密采用的密钥撤销掉。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.DataProtection.KeyManagement.Internal;
using Microsoft.Extensions.DependencyInjection;var services = new ServiceCollection();
services.AddDataProtection();
var sericeProvider = services.BuildServiceProvider();
var protector = sericeProvider.GetDataProtector("foobar");
var originalPayload = Guid.NewGuid().ToString();
var protectedPayload = protector.Protect(originalPayload);var keyRingProvider = sericeProvider.GetRequiredService<IKeyRingProvider>();
var KeyRing = keyRingProvider.GetCurrentKeyRing();
var keyManager = sericeProvider.GetRequiredService<IKeyManager>();
keyManager.RevokeKey(KeyRing.DefaultKeyId);
protector.Unprotect(protectedPayload);

具体来说,我们利用IServiceProvider对象提供的IKeyRingProvider对象得到对应的IKeyRing对象,该对象的DefaultKeyId属性代表默认使用的密钥ID,我们撤销的也这是这个ID代表的密钥。,我们借助于依赖注入容器得到IKeyManager对象,并将此密钥ID作为参数调用其RevokeKey方法。在密钥撤销之后,我们利用同一个IDataProtector对加密内容进行解密,此时程序会抛出如图3所示的CryptographicException异常。


图3 秘钥被撤销导致的解密异常

[S1305]撤销加密密钥(所有密钥)

除了调用IKeyManager的RevokeKey方法撤销某个指定的密钥之外,我们还可以按照如下的方式调用它的RevokeAllKeys方法撤销所有密钥。如果我们觉得目前的所有密钥均不安全,可以调用这个方法。我们在调用该方法的时候需要指定一个撤销的时间和原因(可选)。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;var services = new ServiceCollection();
services.AddDataProtection();
var sericeProvider = services.BuildServiceProvider();
var protector = sericeProvider.GetDataProtector("foobar");
var originalPayload = Guid.NewGuid().ToString();
var protectedPayload = protector.Protect(originalPayload);var keyManager = sericeProvider.GetRequiredService<IKeyManager>();
keyManager.RevokeAllKeys(revocationDate: DateTimeOffset.UtcNow, reason: "No reason");
protector.Unprotect(protectedPayload);

[S1306]瞬时加解密

在某些应用场景中,针对数据的加解密只在一个限定的上下文中进行(比如当前应用的生命周期内),这种场景适用一种被称为“瞬时(Transient或者Ephemeral)加解密”的方式。这种加解密方式会使用到EphemeralDataProtectionProvider类型,该类型同样实现了ITimeLimitedDataProtector接口。如果我们利用它提供的IDataProtector对象对一段二进制内容进行加密,密文只能通过它自身提供的IDataProtector对象才能解开。

如下面的代码片段所示,我们定义了一个CreateEphemeralDataProtectionProvider方法用来创建上述的这个对象。我们在调用ServiceCollection对象的AddDataProtection扩展方法并得到返回的IDataProtectionBuilder之后,我们调用了该对象的UseEphemeralDataProtectionProvider扩展方法完成针对EphemeralDataProtectionProvider的服务注册,所以我们最终得到的IDataProtectionProvider对象的类型就是EphemeralDataProtectionProvider。

using Microsoft.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using System.Diagnostics;var originalPayload = Guid.NewGuid().ToString();
var dataProtectionProvider = CreateEphemeralDataProtectionProvider();
var protector = dataProtectionProvider.CreateProtector("foobar");
var protectedPayload = protector.Protect(originalPayload);protector = dataProtectionProvider.CreateProtector("foobar");
Debug.Assert(originalPayload == protector.Unprotect(protectedPayload));protector = CreateEphemeralDataProtectionProvider().CreateProtector("foobar");
protector.Unprotect(protectedPayload);static IDataProtectionProvider CreateEphemeralDataProtectionProvider()
{var services = new ServiceCollection();services.AddDataProtection().UseEphemeralDataProtectionProvider();return services.BuildServiceProvider().GetRequiredService<IDataProtectionProvider>();
}

在利用EphemeralDataProtectionProvider提供的IDataProtector对象对一段文本加密后,我们对密文实施了两次解密。第一次采用的IDataProtector对象通过同一个EphemeralDataProtectionProvider对象提供的,第二个则则不是。该演示程序运行之后,第一次解密顺利完成,第二次则抛出了如图4所示的CryptographicException异常。


图4 利用EphemeralDataProtectionProvider提供“瞬时”加解密

[S1307]密钥哈希

用户密码作为机密性最高的信息是不能以明文形式存储的,我们一般会存储密码的哈希值。虽然哈希的非对称性确保不能直接通过哈希值得到被哈希的原始内容,但是在强大的算力面前已经不足以提供我们期望的安全保障。针对密钥的保护,目前最安全的哈希方式应该是PBKDF2(Password-Based Key Derivation Function 2)。PBKDF2是一种基于密码的Key Derivation(采用某种算法根据指定的密码或者主键生成一个密钥)函数,它采用伪随机函数以任意指定长度导出密钥。它目前是RSA实验室公钥加密标准(PKCS:Public-Key Cryptography Standards)序列的一部分。PBKDF2提高安全系数主要采用“添加随机盐(Salt)”和“多次哈希”这两种手段。如果希望对PBKDF2具有深入的了解,可以参阅官方规范文档(https://tools.ietf.org/html/rfc2898#section-5.2)。

我们在可以利用“Microsoft.AspNetCore.Cryptography.KeyDerivation”这个NuGet包提供的API来对密码进行哈希。这是一个完全独立的类库,与上面介绍的以IDataProtector对象为核心的数据保护框架没有关系。基于PBKDF2的密码哈希可以直接调用KeyDerivation类型的如下这个静态方法Pbkdf2来完成。

public static class KeyDerivation
{public static byte[] Pbkdf2(string password, byte[] salt, KeyDerivationPrf prf,int iterationCount, int numBytesRequested);
}public enum KeyDerivationPrf
{HMACSHA1,HMACSHA256,HMACSHA512
}

PBKDF2并没有限制使用某种固定的加密算法。在调用上面这个Pbkdf2方法的时候,我们可以利用prf参数指定采用的伪随机算法(PRF:Pseudo-random Function)。这是一个KeyDerivationPrf类型的枚举,三个枚举项对应的哈希算法分别为SHA-1、SHA-256和SHA-512。Pbkdf2方法的其他参数分别表示待哈希的密码、随机盐、迭代次数(次数越大、安全系数越大)和最终生成哈希值的字节数。

using Microsoft.AspNetCore.Cryptography.KeyDerivation;
using System.Security.Cryptography;var password = "password";
var salt = new byte[16];
var iteration = 1000;using (var generator = RandomNumberGenerator.Create())
{generator.GetBytes(salt);
}Console.WriteLine(Hash(KeyDerivationPrf.HMACSHA1));
Console.WriteLine(Hash(KeyDerivationPrf.HMACSHA256));
Console.WriteLine(Hash(KeyDerivationPrf.HMACSHA512));string Hash(KeyDerivationPrf prf)
{var hashed = KeyDerivation.Pbkdf2(password: password,salt: salt,prf: prf,iterationCount: iteration,numBytesRequested: 32);return Convert.ToBase64String(hashed);
}

上面的代码片段演示了如何为提供的密码(“password”)生成指定位数(32字节,256位)的哈希值。我们采用一个随机生成的盐值(16字节,128位),执行1000次迭代,针对三种不同的哈希算法生成对应的哈希值。Base64编码后的三个哈希值以如图13-5所示的方式输出到控制台上。


图5 采用PBKDF2生成的密码哈希

《ASP.NET Core 6框架揭秘》实例演示[01]:编程初体验
《ASP.NET Core 6框架揭秘》实例演示[02]:各种形式的API开发
《ASP.NET Core 6框架揭秘》实例演示[03]:Dapr初体验
《ASP.NET Core 6框架揭秘》实例演示[04]:自定义依赖注入框架
《ASP.NET Core 6框架揭秘》实例演示[05]:依赖注入基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[06]:依赖注入框架设计细节
《ASP.NET Core 6框架揭秘》实例演示[07]:文件系统
《ASP.NET Core 6框架揭秘》实例演示[08]:配置的基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[09]:将配置绑定为对象
《ASP.NET Core 6框架揭秘》实例演示[10]:Options基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[11]:诊断跟踪的几种基本编程方式 
《ASP.NET Core 6框架揭秘》实例演示[13]:日志的基本编程模式
《ASP.NET Core 6框架揭秘》实例演示[14]:日志的进阶用法
《ASP.NET Core 6框架揭秘》实例演示[15]:针对控制台的日志输出
《ASP.NET Core 6框架揭秘》实例演示[16]:内存缓存与分布式缓存的使用
《ASP.NET Core 6框架揭秘》实例演示[17]:利用IHttpClientFactory工厂来创建HttpClient
《ASP.NET Core 6框架揭秘》实例演示[18]:HttpClient处理管道

《ASP.NET Core 6框架揭秘》实例演示[19]:数据加解密与哈希相关推荐

  1. 《ASP.NET Core 6框架揭秘》实例演示[10]:Options基本编程模式

    依赖注入使我们可以将依赖的功能定义成服务,最终以一种松耦合的形式注入消费该功能的组件或者服务中.除了可以采用依赖注入的形式消费承载某种功能的服务,还可以采用相同的方式消费承载配置数据的Options对 ...

  2. 《ASP.NET Core 6框架揭秘》实例演示[18]:HttpClient处理管道

    在<<ASP.NET Core 6框架揭秘>实例演示[17]:利用IHttpClientFactory工厂来创建HttpClient>之后,我们将关注点放到HttpClient ...

  3. 《ASP.NET Core 6框架揭秘》实例演示[32]:错误页面的N种呈现方式

    由于ASP.NET是一个同时处理多个请求的Web应用框架,所以在处理某个请求过程中出现异常并不会导致整个应用的中止.出于安全方面的考量,为了避免敏感信息外泄,客户端在默认情况下并不会得到详细的出错信息 ...

  4. 《ASP.NET Core 3 框架揭秘(上下册)》送书结果公告

    [免费送书].Net5实操后的我一夜未眠,来个大胆预测!的送书抽奖结果已经出来了: 这位中奖的同学尽快填写收货地址,4/2 日还没有完成填写将作废,奖品可是热门的<ASP.NET Core 3 ...

  5. [ASP.NET Core 3框架揭秘] 异步线程无法使用IServiceProvider?

    标题反映的是上周五一个同事咨询我的问题,我觉得这是一个很好的问题.这个问题有助于我们深入理解依赖注入框架在ASP.NET Core中的应用,以及服务实例的生命周期. 一.问题重现 我们通过一个简单的实 ...

  6. [ASP.NET Core 3框架揭秘] 依赖注入:依赖注入模式

    IoC主要体现了这样一种设计思想:通过将一组通用流程的控制权从应用转移到框架之中以实现对流程的复用,并按照"好莱坞法则"实现应用程序的代码与框架之间的交互.我们可以采用若干设计模式 ...

  7. [ASP.NET Core 3框架揭秘] 文件系统[1]:抽象的“文件系统”

    ASP.NET Core应用 具有很多读取文件的场景,比如配置文件.静态Web资源文件(比如CSS.JavaScript和图片文件等)以及MVC应用的View文件,甚至是直接编译到程序集中的内嵌资源文 ...

  8. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Docker

    对于一个 .NET Core开发人员,你可能没有使用过Docker,但是你不可能没有听说过Docker.Docker是Github上最受欢迎的开源项目之一,它号称要成为所有云应用的基石,并把互联网升级 ...

  9. [ASP.NET Core 3框架揭秘] 跨平台开发体验: Mac OS

    除了微软自家的Windows平台, .NET Core针对Mac OS以及各种Linux Distribution(RHEL.Ubuntu.Debian.Fedora.CentOS和SUSE等)都提供 ...

最新文章

  1. jQuery事件处理一瞥
  2. 自定义线程池-参数设计分析
  3. c# xmlhttp POST提取远程webservice数据
  4. Visual Studio怎么使用中文帮助文档
  5. java api限流_Java 9:流API的增强
  6. 基于SqlSugar的数据库访问处理的封装,支持多数据库并使之适应于实际业务开发中
  7. 前端学习(2192):Promise的all的使用
  8. 原根(详解+代码实现+例题+快速求解一个数的原根)
  9. 在Ruby中使用&运算符(new_array- arr&old_Array)创建数组实例
  10. mouseover/out和mouseenter/leave的区别及用法
  11. 【编译】StreamInsight应用调优入门(一)——概述
  12. mysql如何实现递归查询
  13. UE4官方文档UI学习:5.UMG 创建3D控件交互
  14. echarts雷达图显示数值
  15. Golang学习日志 ━━ Gin-Vue-Admin按步骤手动创建api及router、service
  16. 关于账本数据库:你想知道的这里都有
  17. 如何改变计算机内存配置文件,电脑内存使用率过高怎么解决?教你如何调整内存大小...
  18. PHP的ES入门(一)—— Index的创建和删除
  19. Mercurial修改历史changeset
  20. 常用路由器默认密码集合

热门文章

  1. 《惢客创业日记》2019.05.12(周日)《惢客》018 目前的市场现状是什么样的?(九)
  2. snowflake做主键 自增_GitHub - MarvinYu/snowflake: Twitter的雪花算法(snowflake)分布式自增ID...
  3. Java小玩意:猜数游戏
  4. 1t硬盘怎么分区最好_1t硬盘怎么分区合理相关阅读-1t硬盘怎么分区合理文章阅读-123文学网...
  5. 《亲密关系》书摘 [美]莎伦 布雷姆
  6. 白杨SEO:2021到2022年做自媒体在哪个平台上更有前途?公众号、百家号、头条号、抖音、微博、B站、知乎、小红书、视频号
  7. Elsevier(爱思维尔)下的information sciences 期刊投稿历程——使用在线的overleaf进行编辑
  8. forEach如何跳出循环
  9. 将其文件夹添加到MATL AB路径
  10. 文贝中的文章如何实现仅自己可见?