Desprotección de cargas cuyas claves se han revocado en ASP.NET Core

Las API de protección de datos de ASP.NET Core no están pensadas principalmente para la persistencia indefinida de cargas confidenciales. Otras tecnologías como Windows CNG DPAPI y Azure Rights Management son más adecuadas para el escenario de almacenamiento indefinido y tienen funcionalidades de administración de claves correspondientemente sólidas. Dicho esto, no hay nada que impida que un desarrollador use las API de protección de datos de ASP.NET Core para la protección a largo plazo de datos confidenciales. Las claves nunca se quitan del anillo de claves, por lo que IDataProtector.Unprotect siempre puede recuperar las cargas existentes siempre que las claves estén disponibles y sean válidas.

Sin embargo, se produce un problema cuando el desarrollador intenta desproteger datos que han sido protegidos con una clave revocada, ya que IDataProtector.Unprotect lanzará una excepción en este caso. Esto puede ser adecuado para cargas de corta duración o transitorias (como tokens de autenticación), ya que el sistema puede recrear fácilmente este tipo de cargas y, en el peor de los casos, se puede solicitar al visitante del sitio que inicie sesión de nuevo. Pero en el caso de las cargas persistentes, el lanzamiento de Unprotect podría provocar una pérdida de datos inaceptable.

IPersistedDataProtector

Para admitir el escenario de permitir que las cargas queden desprotegidas incluso ante claves revocadas, el sistema de protección de datos contiene un tipo de IPersistedDataProtector. Para obtener una instancia de IPersistedDataProtector, simplemente obtenga una instancia de IDataProtector de manera normal e intente convertir IDataProtector en IPersistedDataProtector.

Nota:

No todas las instancias de IDataProtector se pueden convertir en IPersistedDataProtector. Los desarrolladores deben usar C# como operador o similar para evitar excepciones de runtime causadas por conversiones no válidas y deben estar preparados para controlar el caso de error correctamente.

IPersistedDataProtector expone la siguiente superficie de API:

DangerousUnprotect(byte[] protectedData, bool ignoreRevocationErrors,
     out bool requiresMigration, out bool wasRevoked) : byte[]

Esta API toma la carga protegida (como una matriz de bytes) y devuelve la carga no protegida. No hay ninguna sobrecarga basada en cadenas. Los dos parámetros de salida son los siguientes.

  • requiresMigration: se establecerá en true si la clave utilizada para proteger esta carga ya no es la clave activa predeterminada, por ejemplo, si la clave utilizada para proteger esta carga es antigua y desde entonces se ha realizado una operación de renovación de claves. Es posible que el autor de la llamada desee considerar la posibilidad de volver a proteger la carga en función de sus necesidades empresariales.

  • wasRevoked: se establecerá en true si la clave utilizada para proteger esta carga útil se revocó.

Advertencia

Tenga extrema precaución al pasar ignoreRevocationErrors: true al método DangerousUnprotect. Si después de llamar a este método, el valor de wasRevoked es true, entonces la clave utilizada para proteger esta carga fue revocada, y la autenticidad de la carga debe ser tratada como sospechosa. En este caso, solo continúe operando con la carga desprotegida si tiene alguna garantía independiente de que es auténtica, por ejemplo, que proviene de una base de datos segura en lugar de ser enviada por un cliente web que no es de confianza.

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
 */