Aufheben des Schutzes von Nutzdaten, deren Schlüssel widerrufen wurden, in ASP.NET Core

Die Datenschutz-APIs von ASP.NET Core sind nicht primär für die unbegrenzte Speicherung vertraulicher Nutzdaten gedacht. Andere Technologien wie Windows CNG DPAPI und Azure Rights Management eignen sich besser für das Szenario der unbegrenzten Speicherung und verfügen über entsprechend starke Schlüsselverwaltungsfunktionen. Es spricht jedoch nichts dagegen, dass ein Entwickler die Datenschutz-APIs von ASP.NET Core für den langfristigen Schutz vertraulicher Daten nutzt. Die Schlüssel werden grundsätzlich nicht aus dem Schlüsselring entfernt, sodass IDataProtector.Unprotect stets vorhandene Nutzdaten wiederherstellen kann, solange die Schlüssel verfügbar und gültig sind.

Ein Problem tritt jedoch auf, wenn der Entwickler versucht, den Schutz von Daten aufzuheben, die mit einem widerrufenen Schlüssel geschützt wurden, da IDataProtector.Unprotect in diesem Fall eine Ausnahme auslöst. Dies mag für kurzlebige oder temporäre Nutzdaten (wie Authentifizierungstoken) in Ordnung sein, da diese Art von Nutzdaten leicht vom System neu erstellt werden kann und der Besucher der Website höchstens ggf. aufgefordert wird, sich erneut anzumelden. Bei gespeicherten Nutzdaten kann das Auslösen von Unprotect jedoch zu einem unvertretbaren Datenverlust führen.

IPersistedDataProtector

Um das Szenario zu unterstützen, dass Nutzdaten auch bei widerrufenen Schlüsseln ungeschützt sein dürfen, enthält das Datensicherungssystem den Typ IPersistedDataProtector. Um eine Instanz von IPersistedDataProtector zu erhalten, rufen Sie einfach eine Instanz von IDataProtector auf übliche Weise ab, und versuchen Sie, IDataProtector in IPersistedDataProtector umzuwandeln.

Hinweis

Nicht alle IDataProtector-Instanzen können in IPersistedDataProtector umgewandelt werden. Entwickler sollten C# als Operator oder ähnliches verwenden, um durch ungültige Umwandlungen verursachte Ausnahmen zu vermeiden, und sollten darauf vorbereitet sein, den Fehlerfall angemessen zu behandeln.

IPersistedDataProtector macht die folgende API-Oberfläche verfügbar:

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

Diese API verwendet die geschützten Nutzdaten (als Bytearray) und gibt die ungeschützten Nutzdaten zurück. Es erfolgt keine zeichenfolgenbasierte Überladung. Die beiden Ausgabeparameter sind wie folgt.

  • requiresMigration: wird auf TRUE festgelegt, wenn der zum Schutz dieser Nutzdaten verwendete Schlüssel nicht mehr der aktive Standardschlüssel ist, z. B. wenn der zum Schutz dieser Nutzdaten verwendete Schlüssel veraltet ist und inzwischen ein Vorgang zum Rollieren von Schlüsseln stattgefunden hat. Der Aufrufer kann je nach geschäftlichen Anforderungen einen erneuten Schutz der Nutzdaten in Betracht ziehen.

  • wasRevoked: wird auf TRUE festgelegt, wenn der zum Schutz dieser Nutzdaten verwendete Schlüssel widerrufen wurde.

Warnung

Seien Sie äußerst vorsichtig, wenn Sie ignoreRevocationErrors: true an die DangerousUnprotect-Methode übergeben. Wenn nach dem Aufruf dieser Methode der Wert wasRevoked TRUE ist, wurde der zum Schutz dieser Nutzdaten verwendete Schlüssel widerrufen, und die Authentizität der Nutzdaten ist als fragwürdig zu betrachten. In diesem Fall sollten Sie den Vorgang mit den ungeschützten Nutzdaten nur dann fortsetzen, wenn Sie eine zusätzliche Bestätigung haben, dass sie authentisch sind, z. B. dass sie aus einer sicheren Datenbank stammen und nicht von einem nicht vertrauenswürdigen Webclient gesendet wurden.

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