데이터 보호: 보호된 페이로드의 수명 제한하기

등록일시: 2017-04-10 08:00,  수정일시: 2017-04-10 00:40
조회수: 3,737
이 문서는 ASP.NET Core 기술을 널리 알리고자 하는 개인적인 취지로 제공되는 번역문서입니다. 이 문서에 대한 모든 저작권은 마이크로소프트에 있으며 요청이 있을 경우 언제라도 게시가 중단될 수 있습니다. 번역 내용에 오역이 존재할 수 있고 주석은 번역자 개인의 의견일 뿐이며 마이크로소프트는 이에 관한 어떠한 보장도 하지 않습니다. 번역이 완료된 이후에도 대상 제품 및 기술이 개선되거나 변경됨에 따라 원문의 내용도 변경되거나 보완되었을 수 있으므로 주의하시기 바랍니다.
본문에서는 시간 제한/자체 만료 페이로드를 보호하거나 보호 해제하는 작업을 수행하기 위한 방법을 살펴봅니다.

일부 시나리오에서는 응용 프로그램 개발자가 일정 시간 후에 만료되는 보호된 페이로드를 생성해야 할 경우도 있습니다. 가령, 비밀번호 재설정 토큰으로 사용되는, 한 시간 동안만 유효한 보호된 페이로드 같은 경우를 들 수 있습니다. 물론 개발자가 내부적으로 만료 일자를 담고 있는 페이로드 형식을 직접 만드는 것도 불가능한 일은 아닙니다. 오히려 고급 개발자라면 이런 방식을 더 선호할 수도 있겠지만, 만료를 관리하는 이런 류의 작업은 대다수의 개발자들에게는 지루한 작업입니다.

개발자가 이런 작업을 손쉽게 처리할 수 있도록 Microsoft.AspNetCore.DataProtection.Extensions 패키지에는 일정 시간 뒤에 자동으로 만료되는 페이로드를 생성하는 유틸리티 API들이 포함되어 있습니다. 이 API들은 ITimeLimitedDataProtector 형식에서 제공됩니다.

API 사용법

ITimeLimitedDataProtector 인터페이스는 시간 제한/자체 만료 페이로드를 보호하거나 보호 해제하는 작업을 수행하기 위한 핵심 인터페이스입니다. ITimeLimitedDataProtector의 인스턴스를 생성하려면, 먼저 특정 용도를 지정해서 생성한 일반적인 IDataProtector의 인스턴스가 필요합니다. IDataProtector의 인스턴스를 얻은 뒤에 IDataProtector.ToTimeLimitedDataProtector 확장 메서드를 호출해서 만료 기능이 내장된 보호자를 다시 얻습니다.

ITimeLimitedDataProtector 인터페이스는 다음과 같은 API 표면 및 확장 메서드들을 노출합니다:

  • CreateProtector(string purpose) : ITimeLimitedDataProtector - 이 API는 루트 시간 제한 보호자로부터 용도 체인을 생성할 수 있다는 점에서 기존의 IDataProtectionProvider.CreateProtector와 비슷합니다.

  • Protect(byte[] plaintext, DateTimeOffset expiration) : byte[]

  • Protect(byte[] plaintext, TimeSpan lifetime) : byte[]

  • Protect(byte[] plaintext) : byte[]

  • Protect(string plaintext, DateTimeOffset expiration) : string

  • Protect(string plaintext, TimeSpan lifetime) : string

  • Protect(string plaintext) : string

이렇게 평문만 전달하는 핵심 Protect 메서드들 외에도 페이로드의 만료 일자를 지정할 수 있는 새로운 오버로드 메서드들이 함께 제공됩니다. 만료 일자는 절대 일시 (DateTimeOffset 형식으로) 또는 상대적 시간으로 (현재 시스템 시간에 대한 TimeSpan 형식으로) 지정할 수 있습니다. 만료가 지정되지 않은 오버로드 메서드가 호출되면 페이로드가 만료되지 않는 것으로 간주됩니다.

  • Unprotect(byte[] protectedData, out DateTimeOffset expiration) : byte[]

  • Unprotect(byte[] protectedData) : byte[]

  • Unprotect(string protectedData, out DateTimeOffset expiration) : string

  • Unprotect(string protectedData) : string

Unprotect 메서드는 보호가 해제된 원본 데이터를 반환합니다. 페이로드가 아직 만료되지 않았다면 보호되지 않은 원본 데이터와 함께 절대 만료일시가 선택적 out 매개변수를 통해서 함께 반환됩니다. 반면 이미 페이로드가 만료됐다면 모든 Unprotect 오버로드 메서드가 CryptographicException을 던집니다.

주의

이 API들을 이용해서 장기간 혹은 무기한 유지되어야 하는 페이로드를 보호하는 것은 바람직하지 않습니다. 경험적으로 "보호된 페이로드가 한 달 뒤에 영구적으로 복구할 수 없게 되더라도 감당할 수 있는가?"라는 질문이 좋은 판단의 기준이 될 수 있습니다. 만약 이 질문에 대한 답이 "아니오"라면 다른 API를 고려해봐야 합니다.

다음 예제는 데이터 보호 시스템의 인스턴스를 생성하기 위해서 비-DI 코드 경로를 사용하고 있습니다. 이 예제를 실행하려면, 먼저 Microsoft.AspNetCore.DataProtection.Extensions 패키지 참조를 추가했는지 확인해야 합니다.

using System;
using System.IO;
using System.Threading;
using Microsoft.AspNetCore.DataProtection;

public class Program
{
    public static void Main(string[] args)
    {
        // create a protector for my application
        var provider = DataProtectionProvider.Create(new DirectoryInfo(@"c:\myapp-keys\"));
        var baseProtector = provider.CreateProtector("Contoso.TimeLimitedSample");

        // convert the normal protector into a time-limited protector
        var timeLimitedProtector = baseProtector.ToTimeLimitedDataProtector();

        // get some input and protect it for five seconds
        Console.Write("Enter input: ");
        string input = Console.ReadLine();
        string protectedData = timeLimitedProtector.Protect(input, lifetime: TimeSpan.FromSeconds(5));
        Console.WriteLine($"Protected data: {protectedData}");

        // unprotect it to demonstrate that round-tripping works properly
        string roundtripped = timeLimitedProtector.Unprotect(protectedData);
        Console.WriteLine($"Round-tripped data: {roundtripped}");

        // wait 6 seconds and perform another unprotect, demonstrating that the payload self-expires
        Console.WriteLine("Waiting 6 seconds...");
        Thread.Sleep(6000);
        timeLimitedProtector.Unprotect(protectedData);
    }
}

/*
 * SAMPLE OUTPUT
 *
 * Enter input: Hello!
 * Protected data: CfDJ8Hu5z0zwxn...nLk7Ok
 * Round-tripped data: Hello!
 * Waiting 6 seconds...
 * <<throws CryptographicException with message 'The payload expired at ...'>>
 */