Отмена защиты полезных данных, ключи которых были отозваны в ASP.NET Core

API-интерфейсы защиты данных ASP.NET Core не предназначены в первую очередь для неограниченного сохранения конфиденциальных полезных данных. Другие технологии, такие как 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 принимает защищенные полезные данные (в виде массива байтов) и возвращает незащищенные полезные данные. Перегрузка на основе строк отсутствует. Ниже приведены два параметра out.

  • requiresMigration: будет иметь значение true, если ключ, используемый для защиты этих полезных данных, больше не является активным ключом по умолчанию, например, ключ, используемый для защиты этих полезных данных, старый, и с тех пор была выполнена последовательная операция ключа. Вызывающий объект может рассмотреть возможность повторного включения защиты полезных данных в зависимости от бизнес-потребностей.

  • wasRevoked: имеет значение true, если ключ, используемый для защиты этих полезных данных, был отозван.

Предупреждение

Соблюдайте крайнюю осторожность при передаче ignoreRevocationErrors: true в DangerousUnprotect метод. Если после вызова этого метода wasRevoked значение имеет значение true, то ключ, используемый для защиты этих полезных данных, был отозван, и подлинность полезных данных должна рассматриваться как подозрительная. В этом случае продолжайте работать только с незащищенными полезными данными, если у вас есть некоторая отдельная уверенность в том, что она является подлинной, например, что она поступает из безопасной базы данных, а не отправляется ненадежным веб-клиентом.

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