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 como true se a chave usada para proteger essa carga não for mais a chave padrão ativa, por exemplo, a chave usada para proteger essa carga for antiga e uma operação de rolagem de chave tiver ocorrido desde então. Talvez o chamador queira considerar proteger novamente o conteúdo dependendo de suas necessidades comerciais.

  • wasRevoked: será definido como true se a chave usada para proteger esse conteúdo 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 no conteúdo desprotegido se você tiver alguma garantia separada de que ele é autêntico, por exemplo, que ele vem de um banco de dados seguro em vez de ser enviado 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
 */