Desproteger cargas cujas chaves foram revogadas no ASP.NET Core
As APIs de proteção de dados do ASP.NET Core não se destinam principalmente à persistência indefinida de cargas confidenciais. Outras tecnologias, como a DPAPI da CNG do Windows e o Azure Rights Management, são mais adequadas para o cenário de armazenamento indefinido e têm recursos de gerenciamento de chaves correspondentemente fortes. Dito isso, não há nada que proíba um desenvolvedor de usar as APIs de proteção de dados do ASP.NET Core para proteção de longo prazo de dados confidenciais. As chaves nunca são removidas do chaveiro, portanto IDataProtector.Unprotect
sempre pode recuperar cargas existentes, desde que as chaves estejam disponíveis e sejam válidas.
No entanto, um problema surge quando o desenvolvedor tenta desproteger dados que foram protegidos com uma chave revogada, assim como IDataProtector.Unprotect
gerará uma exceção nesse caso. Isso pode ser bom para cargas transitórias ou de curta duração (como tokens de autenticação), pois esses tipos de conteúdo podem ser facilmente recriados pelo sistema e, na pior das hipóteses, o visitante do site pode ser obrigado a fazer logon novamente. Mas para cargas persistentes, fazer com que Unprotect
lance pode levar a uma perda de dados inaceitável.
IPersistedDataProtector
Para dar suporte ao cenário de permitir que as cargas sejam desprotegidas mesmo diante das chaves revogadas, o sistema de proteção de dados contém um tipo IPersistedDataProtector
. Para obter uma instância do IPersistedDataProtector
, basta obter uma instância do IDataProtector
da maneira normal e tentar converter o IDataProtector
em IPersistedDataProtector
.
Observação
Nem todas as instâncias do IDataProtector
podem ser convertidas em IPersistedDataProtector
. Os desenvolvedores devem usar o C# como operador ou semelhante para evitar exceções de runtime causadas por conversões inválidas e devem estar preparados para lidar adequadamente com o caso de falha.
IPersistedDataProtector
expõe a seguinte superfície de API:
DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,
out bool requiresMigration, out bool wasRevoked) : byte[]
Essa API usa o conteúdo protegido (como uma matriz de bytes) e retorna o conteúdo desprotegido. Não há sobrecarga baseada em cadeia de caracteres. Os dois parâmetros de saída são os seguintes.
requiresMigration
: será definido comotrue
se a chave usada para proteger essa carga não for mais a chave padrão ativa. Por exemplo, a chave usada para proteger esse conteúdo é antiga e uma operação de troca de chaves ocorreu desde então. Talvez o chamador queira considerar proteger novamente o conteúdo dependendo de suas necessidades comerciais.wasRevoked
: será definido comotrue
se a chave usada para proteger essa carga tiver sido revogada.
Aviso
Tenha extrema cautela ao passar ignoreRevocationErrors: true
para o método DangerousUnprotect
. Se depois de chamar esse método, o valor de wasRevoked
for true, a chave usada para proteger esse conteúdo foi revogada e a autenticidade da carga deverá ser tratada como suspeita. Nesse caso, continue operando apenas na carga desprotegida se você tiver alguma garantia separada de que ela é autêntica, por exemplo, que ela vem de um banco de dados seguro em vez de ser enviada por um cliente Web não confiável.
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
*/