在 ASP.NET Core 中取消保护密钥已撤销的有效负载
ASP.NET Core 数据保护 API 并非主要用于无限期保留机密有效负载。 Windows CNG DPAPI 和 Azure Rights Management 等其他技术更适用于无限期存储的场景,这些技术具有相应强大的密钥管理功能。 也就是说,开发人员也可以使用 ASP.NET Core 数据保护 API 来长期保护机密数据。 密钥绝不会从密钥链中删除,因此,只要密钥可用并且有效,IDataProtector.Unprotect
便始终能够恢复现有的有效负载。
但是,当开发人员尝试取消保护已撤销的密钥所保护的数据时会出现问题,在这种情况下,IDataProtector.Unprotect
将引发异常。 对于短期或暂时的有效负载(例如身份验证令牌)而言可能问题不大,因为系统可以轻松地重新创建这类有效负载,在最坏的情况下,站点访问者可能需要重新登录。 但是,对于持久性有效负载而言,引发 Unprotect
可能会导致不可接受的数据丢失。
IPersistedDataProtector
为了支持即使在面临密钥已撤销的情况下能够取消对有效负载的保护,数据保护系统包含一个 IPersistedDataProtector
类型。 若要获取 IPersistedDataProtector
的实例,只需以正常方式获取 IDataProtector
的实例并尝试将 IDataProtector
强制转换为 IPersistedDataProtector
即可。
注意
并非所有的 IDataProtector
实例都可强制转换为 IPersistedDataProtector
。 开发人员应将 C# 用作运算符或类似的功能来避免无效的强制转换所导致的运行时异常,并且应准备好适当地处理故障情况。
IPersistedDataProtector
公开以下 API 表面:
DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,
out bool requiresMigration, out bool wasRevoked) : byte[]
此 API 采用受保护的有效负载(用作字节数组)并返回未受保护的有效负载。 没有基于字符串的重载。 两个输出参数如下所示。
requiresMigration
:如果用于保护此有效负载的密钥不再是活动默认密钥,则设置为true
。 例如,用于保护此有效负载的密钥已过时,并且因此进行了密钥滚动操作。 调用方可能需要根据业务需求考虑重新保护有效负载。wasRevoked
:如果用于保护此有效负载的密钥已撤销,则设置为true
。
警告
将 ignoreRevocationErrors: true
传递给 DangerousUnprotect
方法时应极为谨慎。 如果在调用此方法后 wasRevoked
值为 true,这意味着用于保护此有效负载的密钥已撤销,应警惕此有效负载的真实性。 在这种情况下,只有在能够单独保证未受保护的有效负载的真实性时,才可以继续对其进行操作,例如它来自安全的数据库,而不是由不受信任的 Web 客户端发送。
using System;
using System.IO;
using System.Text;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;
public class Program
{
public static void Main(string[] args)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddDataProtection()
// point at a specific folder and use DPAPI to encrypt keys
.PersistKeysToFileSystem(new DirectoryInfo(@"c:\temp-keys"))
.ProtectKeysWithDpapi();
var services = serviceCollection.BuildServiceProvider();
// get a protector and perform a protect operation
var protector = services.GetDataProtector("Sample.DangerousUnprotect");
Console.Write("Input: ");
byte[] input = Encoding.UTF8.GetBytes(Console.ReadLine());
var protectedData = protector.Protect(input);
Console.WriteLine($"Protected payload: {Convert.ToBase64String(protectedData)}");
// demonstrate that the payload round-trips properly
var roundTripped = protector.Unprotect(protectedData);
Console.WriteLine($"Round-tripped payload: {Encoding.UTF8.GetString(roundTripped)}");
// get a reference to the key manager and revoke all keys in the key ring
var keyManager = services.GetService<IKeyManager>();
Console.WriteLine("Revoking all keys in the key ring...");
keyManager.RevokeAllKeys(DateTimeOffset.Now, "Sample revocation.");
// try calling Protect - this should throw
Console.WriteLine("Calling Unprotect...");
try
{
var unprotectedPayload = protector.Unprotect(protectedData);
Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
// try calling DangerousUnprotect
Console.WriteLine("Calling DangerousUnprotect...");
try
{
IPersistedDataProtector persistedProtector = protector as IPersistedDataProtector;
if (persistedProtector == null)
{
throw new Exception("Can't call DangerousUnprotect.");
}
bool requiresMigration, wasRevoked;
var unprotectedPayload = persistedProtector.DangerousUnprotect(
protectedData: protectedData,
ignoreRevocationErrors: true,
requiresMigration: out requiresMigration,
wasRevoked: out wasRevoked);
Console.WriteLine($"Unprotected payload: {Encoding.UTF8.GetString(unprotectedPayload)}");
Console.WriteLine($"Requires migration = {requiresMigration}, was revoked = {wasRevoked}");
}
catch (Exception ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
}
}
}
/*
* SAMPLE OUTPUT
*
* Input: Hello!
* Protected payload: CfDJ8LHIzUCX1ZVBn2BZ...
* Round-tripped payload: Hello!
* Revoking all keys in the key ring...
* Calling Unprotect...
* CryptographicException: The key {...} has been revoked.
* Calling DangerousUnprotect...
* Unprotected payload: Hello!
* Requires migration = True, was revoked = True
*/