패딩을 사용하는 CBC 모드 대칭 암호 해독의 타이밍 취약성

Microsoft는 매우 특정한 상황을 제외하고는 암호문의 무결성을 먼저 확인하지 않고 확인 가능한 패딩이 적용된 경우 CBC(Cipher-Block-Chaining) 모드의 대칭형 암호화로 암호화된 데이터를 해독하는 것이 더 이상 안전하지 않다고 생각합니다. 이 판단은 현재 알려진 암호화 연구를 기반으로 합니다.

소개

패딩 오라클 공격은 암호화된 데이터에 대한 공격 형식으로, 공격자가 키를 알지 못한 채 데이터 콘텐츠를 해독할 수 있습니다.

오라클은 공격자에게 실행 중인 작업이 올바른지 여부에 대한 정보를 제공하는 "tell"을 의미합니다. 아이와 함께 보드 게임이나 카드 게임을 한다고 상상해 보세요. 아이가 좋은 방향으로 이동할 것이라고 생각하여 활짝 웃는 경우 이를 오라클이라고 합니다. 상대방으로서 당신은 이 오라클을 사용하여 다음 행동을 적절하게 계획할 수 있습니다.

패딩은 특정 암호화 용어입니다. 데이터를 암호화하는 데 사용되는 알고리즘인 일부 암호는 각 블록이 고정된 크기인 데이터 블록에서 작동합니다. 암호화하려는 데이터가 블록을 채울 수 있는 크기가 아닌 경우, 채워질 때까지 데이터가 채워집니다. 많은 형태의 패딩에서는 원래 입력의 크기가 올바른 경우에도 항상 패딩이 있어야 합니다. 이를 통해 암호 해독 시 패딩을 항상 안전하게 제거할 수 있습니다.

이 두 가지를 종합하면, 패딩 오라클을 사용한 소프트웨어 구현은 해독된 데이터에 유효한 패딩이 있는지 여부를 드러냅니다. 오라클은 "잘못된 패딩"이라는 값을 반환하는 것처럼 간단한 것일 수도 있고 잘못된 블록이 아닌 유효한 블록을 처리하는 데 측정 가능한 시간이 걸리는 것과 같은 더 복잡한 것일 수도 있습니다.

블록 기반 암호에는 모드라는 또 다른 속성이 있는데, 이 속성은 첫 번째 블록의 데이터와 두 번째 블록의 데이터 간의 관계 등을 결정합니다. 가장 일반적으로 사용되는 모드 중 하나는 CBC입니다. CBC는 IV(초기화 벡터)로 알려진 초기 임의 블록을 도입하고 이전 블록을 정적 암호화 결과와 결합하여 동일한 키로 동일한 메시지를 암호화해도 항상 동일하게 암호화된 출력이 생성되지는 않도록 만듭니다.

공격자는 CBC 데이터의 구조와 함께 패딩 오라클을 사용하여 오라클을 노출하는 코드에 약간 변경된 메시지를 보내고 오라클이 데이터가 정확하다고 말할 때까지 데이터를 계속 보낼 수 있습니다. 이 응답을 통해 공격자는 메시지를 바이트 단위로 해독할 수 있습니다.

최신 시스템 네트워크의 품질은 공격자가 원격 시스템의 실행 시간에서 매우 작은(0.1ms 미만) 차이를 검색할 수 있을 정도로 높습니다. 데이터가 변조되지 않은 경우에만 성공적인 암호 해독이 발생할 수 있다고 가정하는 애플리케이션은 성공적인 암호 해독과 실패한 암호 해독의 차이를 관찰하도록 설계된 도구의 공격에 취약할 수 있습니다. 이러한 타이밍 차이는 일부 언어나 라이브러리에서 다른 언어나 라이브러리보다 더 중요할 수 있지만, 이제 오류에 대한 애플리케이션의 응답을 고려할 때 이것이 모든 언어와 라이브러리에 대한 실질적인 위협이라고 생각됩니다.

이 공격은 암호화된 데이터를 변경하고 오라클을 통해 결과를 테스트하는 기능에 따라 다릅니다. 공격을 완전히 완화하는 유일한 방법은 암호화된 데이터의 변경 내용을 검색하고 이에 대한 어떠한 작업 수행도 거부하는 것입니다. 이를 수행하는 표준 방법은 데이터에 대한 서명을 만들고 작업을 수행하기 전에 해당 서명의 유효성을 검사하는 것입니다. 서명은 검증 가능해야 하며 공격자가 만들 수 없습니다. 그렇지 않으면 암호화된 데이터를 변경한 다음 변경된 데이터를 기반으로 새 서명을 계산합니다. 적절한 서명의 일반적인 형식 중 하나는 HMAC(키 해시 메시지 인증 코드)로 알려져 있습니다. HMAC는 HMAC를 생성하는 사람과 이의 유효성을 검사하는 사람에게만 알려진 비밀 키를 사용한다는 점에서 체크섬과 다릅니다. 키가 없으면 올바른 HMAC를 생성할 수 없습니다. 데이터를 수신하면 암호화된 데이터를 가져오고, 사용자와 보낸 사람이 공유하는 비밀 키를 사용하여 독립적으로 HMAC를 계산한 다음, 그들이 보낸 HMAC와 사용자가 계산한 HMAC를 비교합니다. 이 비교는 일정한 시간이어야 합니다. 그렇지 않으면 검색 가능한 다른 오라클을 추가하여 다른 형식의 공격을 허용합니다.

요약하자면, 패딩된 CBC 블록 암호를 안전하게 사용하려면 데이터 암호 해독을 시도하기 전에 상수 시간 비교를 사용하여 유효성을 검사하는 HMAC(또는 다른 데이터 무결성 검사)와 이를 결합해야 합니다. 변경된 모든 메시지는 응답을 생성하는 데 동일한 시간이 걸리므로 공격이 방지됩니다.

취약한 사용자

이 취약성은 자체 암호화 및 암호 해독을 수행하는 관리 애플리케이션과 네이티브 애플리케이션 모두에 적용됩니다. 여기에는 다음이 포함됩니다. 예:

  • 서버에서 나중에 해독하기 위해 쿠키를 암호화하는 애플리케이션.
  • 사용자가 나중에 열의 암호가 해독되는 테이블에 데이터를 삽입할 수 있는 기능을 제공하는 데이터베이스 애플리케이션.
  • 전송 중인 데이터를 보호하기 위해 공유 키를 사용한 암호화를 사용하는 데이터 전송 애플리케이션.
  • TLS 터널 "내부"에서 메시지를 암호화하고 해독하는 애플리케이션.

이러한 시나리오에서는 TLS만으로는 보호되지 않을 수 있습니다.

취약한 애플리케이션:

  • PKCS#7 또는 ANSI X.923과 같은 확인 가능한 패딩 모드와 함께 CBC 암호화 모드를 사용하여 데이터를 해독합니다.
  • MAC 또는 비대칭 디지털 서명을 통해 데이터 무결성 검사를 수행하지 않고 암호 해독을 수행합니다.

이는 암호화 메시지 구문(PKCS#7/CMS) EnvelopedData 구조와 같은 기본 요소 위에 추상화를 기반으로 빌드된 애플리케이션에도 적용됩니다.

연구에 따르면 Microsoft는 메시지에 잘 알려져 있거나 예측 가능한 바닥글 구조가 있는 경우 ISO 10126과 동등한 패딩으로 채워진 CBC 메시지에 대해 더욱 우려하게 되었습니다. 예를 들어, W3C XML 암호화 구문 및 처리 권장 사항(xmlenc, EncryptedXml)의 규칙에 따라 준비된 콘텐츠입니다. 메시지에 서명한 후 암호화하라는 W3C 지침은 당시에는 적절하다고 간주되었지만 이제 Microsoft에서는 항상 암호화 후 서명을 수행하는 것이 좋습니다.

비대칭 키와 임의 메시지 사이에는 본질적인 신뢰 관계가 없으므로 애플리케이션 개발자는 항상 비대칭 서명 키의 적용 가능성을 확인하는 데 주의해야 합니다.

세부 정보

역사적으로 HMAC 또는 RSA 서명과 같은 수단을 사용하여 중요한 데이터를 암호화하고 인증하는 것이 중요하다는 합의가 있었습니다. 그러나 암호화 및 인증 작업 시퀀스를 지정하는 방법에 대한 지침이 명확하지 못했습니다. 이 문서에 자세히 설명된 취약성으로 인해 Microsoft의 지침은 이제 항상 "암호화 후 서명" 패러다임을 사용하는 것입니다. 즉, 먼저 대칭 키를 사용하여 데이터를 암호화한 다음 암호화 텍스트(암호화된 데이터)을 통해 MAC 또는 비대칭 서명을 계산합니다. 데이터를 해독할 때는 역방향으로 수행합니다. 먼저 암호화 텍스트의 MAC 또는 서명을 확인한 다음 암호를 해독합니다.

"패딩 오라클 공격"으로 알려진 취약성 종류는 10년 넘게 존재하는 것으로 알려져 있습니다. 이러한 취약성으로 인해 공격자는 데이터 블록당 최대 4096회 시도하여 AES 및 3DES와 같은 대칭 블록 알고리즘으로 암호화된 데이터를 해독할 수 있습니다. 이러한 취약성은 블록 암호가 마지막에 확인 가능한 패딩 데이터와 함께 가장 자주 사용된다는 팩트를 이용합니다. 공격자가 암호화 텍스트을 변조할 수 있고, 변조로 인해 마지막에 패딩 형식에 오류가 발생했는지 여부를 알아내면 공격자는 데이터를 암호 해독할 수 있는 것으로 나타났습니다.

초기에 실질적인 공격은 ASP.NET 취약성 MS10-070과 같이 패딩이 유효한지 여부에 따라 다른 오류 코드를 반환하는 서비스를 기반으로 했습니다. 그러나 이제 Microsoft는 유효한 패딩 처리와 유효하지 않은 패딩 처리 사이의 타이밍 차이만을 사용하여 유사한 공격을 수행하는 것이 실용적이라고 생각합니다.

암호화 방식이 서명을 사용하고 서명 확인이 지정된 길이의 데이터(콘텐츠에 관계없이)에 대해 고정된 런타임으로 수행되는 경우 사이드 채널을 통해 공격자에게 정보를 공개하지 않고도 데이터 무결성을 확인할 수 있습니다. 무결성 검사는 변조된 메시지를 거부하므로 패딩 오라클 위협이 완화됩니다.

지침

무엇보다도 Microsoft에서는 기밀성이 있는 모든 데이터를 SSL(Secure Sockets Layer)의 후속인 TLS(전송 계층 보안)를 통해 전송하도록 권장합니다.

다음으로 애플리케이션을 분석하여 다음을 수행합니다.

  • 수행 중인 암호화와 사용 중인 플랫폼 및 API에서 제공되는 암호화가 무엇인지 정확하게 이해합니다.
  • CBC 모드에서 AES 및 3DES와 같은 대칭 블록 암호화 알고리즘의 각 계층에서 사용할 때 비밀 키 데이터 무결성 검사(비대칭 서명, HMAC 또는 암호화 모드를 GCM 또는 CCM과 같은 AE(인증된 암호화) 모드로 변경합니다.

현재 연구에 따르면 일반적으로 비AE 암호화 모드에 대해 인증 및 암호화 단계가 독립적으로 수행되는 경우 암호화 텍스트를 인증(암호화 후 서명)하는 것이 가장 좋은 일반적인 옵션이라고 생각됩니다. 그러나 암호화에 대한 일률적인 정답은 없으며 이러한 일반화는 전문 암호화 전문가의 직접적인 조언만큼 좋지 않습니다.

메시징 형식을 변경할 수 없지만 인증되지 않은 CBC 암호 해독을 수행하는 애플리케이션은 다음과 같은 완화 조치를 통합하는 것이 좋습니다.

  • 암호 해독기가 패딩을 확인하거나 제거하도록 허용하지 않고 암호를 해독합니다.
    • 적용된 패딩은 여전히 제거하거나 무시해야 하며, 부담을 애플리케이션으로 옮기게 됩니다.
    • 패딩 확인 및 제거를 다른 애플리케이션 데이터 확인 논리에 통합할 수 있다는 이점이 있습니다. 패딩 검증과 데이터 검증이 일정한 시간 내에 이루어질 수 있다면 위협은 줄어든다.
    • 패딩 해석에 따라 인식된 메시지 길이가 변경되므로 이 방식에서 여전히 타이밍 정보가 방출될 수 있습니다.
  • 암호 해독 패딩 모드를 ISO10126으로 변경합니다.
    • ISO10126 암호 해독 패딩은 PKCS7 암호화 패딩 및 ANSIX923 암호화 패딩과 모두 호환됩니다.
    • 모드를 변경하면 패딩 오라클 지식이 전체 블록 대신 1바이트로 줄어듭니다. 그러나 콘텐츠에 닫는 XML 요소와 같이 잘 알려진 바닥글이 있는 경우 관련 공격이 메시지의 나머지 부분을 계속 공격할 수 있습니다.
    • 이는 공격자가 동일한 일반 텍스트를 다른 메시지 오프셋으로 여러 번 암호화하도록 강제할 수 있는 상황에서도 일반 텍스트 복구를 방지하지는 않습니다.
  • 타이밍 신호를 약화시키기 위해 암호 해독 호출 평가를 게이트로 진행합니다.
    • 보류 시간 계산은 패딩이 포함된 데이터 세그먼트에 대해 암호 해독 작업에 소요되는 최대 시간을 초과하는 최소 시간을 가져야 합니다.
    • 시간 계산은 Environment.TickCount(롤오버/오버플로에 따라 다름)을 사용하거나 두 개의 시스템 타임스탬프를 빼는 방식(NTP 조정 오류에 따라 다름)이 아닌 고해상도 타임스탬프 획득의 지침에 따라 수행되어야 합니다.
    • 시간 계산에는 단지 끝 부분을 채우는 것이 아니라 관리되는 애플리케이션이나 C++ 애플리케이션의 모든 잠재적인 예외를 포함하는 암호 해독 작업도 포함되어야 합니다.
    • 성공 또는 실패가 아직 결정된 경우 타이밍 게이트는 만료 시 실패를 반환해야 합니다.
  • 인증되지 않은 암호 해독을 수행하는 서비스에는 "잘못된" 메시지가 매우 많이 발생했음을 감지하기 위한 모니터링 기능이 있어야 합니다.
    • 이 신호는 가양성(합법적으로 손상된 데이터)과 가음성(감지하지 못하도록 충분히 오랜 시간 동안 공격을 확산시킴)을 모두 전달한다는 점을 유념해야 합니다.

취약한 코드 찾기 - 네이티브 애플리케이션

Windows CNG(Cryptography: Next Generation) 라이브러리를 기반으로 빌드된 프로그램의 경우:

  • 암호 해독 호출은 BCRYPT_BLOCK_PADDING 플래그를 지정하는 BCryptDecrypt에 대한 호출입니다.
  • BCRYPT_CHAINING_MODEBCRYPT_CHAIN_MODE_CBC로 설정된 BCryptSetProperty를 호출하여 키 핸들이 초기화되었습니다.
    • BCRYPT_CHAIN_MODE_CBC가 기본값이므로 영향을 받는 코드에서 BCRYPT_CHAINING_MODE에 값을 할당하지 않았을 수 있습니다.

이전 Windows 암호화 API를 기반으로 빌드된 프로그램의 경우:

  • 암호 해독 호출은 Final=TRUE를 사용한 CryptDecrypt입니다.
  • 키 핸들은 KP_MODECRYPT_MODE_CBC로 설정된 CryptSetKeyParam을 호출하여 초기화되었습니다.
    • CRYPT_MODE_CBC가 기본값이므로 영향을 받는 코드에서 KP_MODE에 값을 할당하지 않았을 수 있습니다.

취약한 코드 찾기 - 관리되는 애플리케이션

취약한 코드 찾기 - 암호화 메시지 구문

암호화된 콘텐츠가 AES(2.16.840.1.101.3.4.1.2, 2.16.840.1.101.3.4.1.22, 2.16.840.1.101.3.4.1.42), DES(1.3.14.3.2.7), 3DES(1.2.840.113549.3.7) 또는 RC2(1.2.840.113549.3.2)의 CBC 모드를 사용하는 인증되지 않은 CMS EnvelopedData 메시지는 취약하며, CBC 모드에서 다른 블록 암호화 알고리즘을 사용하는 메시지도 취약합니다.

스트림 암호는 이 특정 취약성에 취약하지 않지만 Microsoft는 항상 ContentEncryptionAlgorithm 값을 검사하는 것보다 데이터를 인증하는 것이 좋습니다.

관리되는 애플리케이션의 경우 CMS EnvelopedData blob은 System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[])에 전달되는 모든 값으로 검색될 수 있습니다.

네이티브 애플리케이션의 경우 CMS EnvelopedData blob은 CryptMsgUpdate를 통해 CMS 핸들에 제공된 값으로 검색될 수 있으며 그 결과 CMSG_TYPE_PARAMCMSG_ENVELOPED이거나 CMS 핸들이 나중에 CryptMsgControl을 통해 CMSG_CTRL_DECRYPT 명령으로 전송됩니다.

취약한 코드 예 - 관리됨

이 방법은 쿠키를 읽고 해독하며 데이터 무결성 검사는 표시되지 않습니다. 따라서 이 방법으로 읽은 쿠키의 콘텐츠는 이를 수신한 사용자나 암호화된 쿠키 값을 획득한 공격자에 의해 공격을 가져올 수 있습니다.

private byte[] DecryptCookie(string cookieName)
{
    HttpCookie cookie = Request.Cookies[cookieName];

    if (cookie == null)
    {
        return null;
    }

    using (ICryptoTransform decryptor = _aes.CreateDecryptor())
    using (MemoryStream memoryStream = new MemoryStream())
    using (CryptoStream cryptoStream =
        new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
    {
        byte[] readCookie = Convert.FromBase64String(cookie.Value);
        cryptoStream.Write(readCookie, 0, readCookie.Length);
        cryptoStream.FlushFinalBlock();
        return memoryStream.ToArray();
    }
}

다음 샘플 코드는 비표준 메시지 형식을 사용합니다.

cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext

여기서 cipher_algorithm_idhmac_algorithm_id 알고리즘 식별자는 해당 알고리즘의 애플리케이션 로컬(비표준) 표현입니다. 이러한 식별자는 단순하게 연결된 바이트스트림 대신 기존 메시징 프로토콜의 다른 부분에서 의미가 있을 수 있습니다.

이 예에서는 또한 단일 마스터 키를 사용하여 암호화 키와 HMAC 키를 모두 파생합니다. 이는 단일 키 애플리케이션을 이중 키 애플리케이션으로 전환하기 위한 편의와 두 키를 서로 다른 값으로 유지하도록 권장하기 위해 제공됩니다. 또한 HMAC 키와 암호화 키가 동기화되지 않도록 보장합니다.

이 샘플은 암호화 또는 암호 해독에 대해 Stream을 허용하지 않습니다. 현재 데이터 서식에서는 hmac_tag 값이 암호화 텍스트 앞에 오기 때문에 원패스 암호화가 어렵습니다. 그러나 이 형식을 선택한 이유는 파서를 더 단순하게 유지하기 위해 처음에 모든 고정 크기 요소를 유지하기 때문입니다. 이 데이터 서식을 사용하면 원패스 암호 해독이 가능하지만 구현자는 TransformFinalBlock을 호출하기 전에 GetHashAndReset을 호출하고 결과를 확인해야 합니다. 스트리밍 암호화가 중요한 경우 다른 AE 모드가 필요할 수 있습니다.

// ==++==
//
//   Copyright (c) Microsoft Corporation.  All rights reserved.
//
//   Shared under the terms of the Microsoft Public License,
//   https://opensource.org/licenses/MS-PL
//
// ==--==

using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;

namespace Microsoft.Examples.Cryptography
{
    public enum AeCipher : byte
    {
        Unknown,
        Aes256CbcPkcs7,
    }

    public enum AeMac : byte
    {
        Unknown,
        HMACSHA256,
        HMACSHA384,
    }

    /// <summary>
    /// Provides extension methods to make HashAlgorithm look like .NET Core's
    /// IncrementalHash
    /// </summary>
    internal static class IncrementalHashExtensions
    {
        public static void AppendData(this HashAlgorithm hash, byte[] data)
        {
            hash.TransformBlock(data, 0, data.Length, null, 0);
        }

        public static void AppendData(
            this HashAlgorithm hash,
            byte[] data,
            int offset,
            int length)
        {
            hash.TransformBlock(data, offset, length, null, 0);
        }

        public static byte[] GetHashAndReset(this HashAlgorithm hash)
        {
            hash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
            return hash.Hash;
        }
    }

    public static partial class AuthenticatedEncryption
    {
        /// <summary>
        /// Use <paramref name="masterKey"/> to derive two keys (one cipher, one HMAC)
        /// to provide authenticated encryption for <paramref name="message"/>.
        /// </summary>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <param name="message">The message to encrypt</param>
        /// <returns>
        /// A concatenation of
        /// [cipher algorithm+chainmode+padding][mac algorithm][authtag][IV][ciphertext],
        /// suitable to be passed to <see cref="Decrypt"/>.
        /// </returns>
        /// <remarks>
        /// <paramref name="masterKey"/> should be a 128-bit (or bigger) value generated
        /// by a secure random number generator, such as the one returned from
        /// <see cref="RandomNumberGenerator.Create()"/>.
        /// This implementation chooses to block deficient inputs by length, but does not
        /// make any attempt at discerning the randomness of the key.
        ///
        /// If the master key is being input by a prompt (like a password/passphrase)
        /// then it should be properly turned into keying material via a Key Derivation
        /// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
        /// never be simply turned to bytes via an Encoding class and used as a key.
        /// </remarks>
        public static byte[] Encrypt(byte[] masterKey, byte[] message)
        {
            if (masterKey == null)
                throw new ArgumentNullException(nameof(masterKey));
            if (masterKey.Length < 16)
                throw new ArgumentOutOfRangeException(
                    nameof(masterKey),
                    "Master Key must be at least 128 bits (16 bytes)");
            if (message == null)
                throw new ArgumentNullException(nameof(message));

            // First, choose an encryption scheme.
            AeCipher aeCipher = AeCipher.Aes256CbcPkcs7;

            // Second, choose an authentication (message integrity) scheme.
            //
            // In this example we use the master key length to change from HMACSHA256 to
            // HMACSHA384, but that is completely arbitrary. This mostly represents a
            // "cryptographic needs change over time" scenario.
            AeMac aeMac = masterKey.Length < 48 ? AeMac.HMACSHA256 : AeMac.HMACSHA384;

            // It's good to be able to identify what choices were made when a message was
            // encrypted, so that the message can later be decrypted. This allows for
            // future versions to add support for new encryption schemes, but still be
            // able to read old data. A practice known as "cryptographic agility".
            //
            // This is similar in practice to PKCS#7 messaging, but this uses a
            // private-scoped byte rather than a public-scoped Object IDentifier (OID).
            // Please note that the scheme in this example adheres to no particular
            // standard, and is unlikely to survive to a more complete implementation in
            // the .NET Framework.
            //
            // You may be well-served by prepending a version number byte to this
            // message, but may want to avoid the value 0x30 (the leading byte value for
            // DER-encoded structures such as X.509 certificates and PKCS#7 messages).
            byte[] algorithmChoices = { (byte)aeCipher, (byte)aeMac };
            byte[] iv;
            byte[] cipherText;
            byte[] tag;

            // Using our algorithm choices, open an HMAC (as an authentication tag
            // generator) and a SymmetricAlgorithm which use different keys each derived
            // from the same master key.
            //
            // A custom implementation may very well have distinctly managed secret keys
            // for the MAC and cipher, this example merely demonstrates the master to
            // derived key methodology to encourage key separation from the MAC and
            // cipher keys.
            using (HMAC tagGenerator = GetMac(aeMac, masterKey))
            {
                using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
                using (ICryptoTransform encryptor = cipher.CreateEncryptor())
                {
                    // Since no IV was provided, a random one has been generated
                    // during the call to CreateEncryptor.
                    //
                    // But note that it only does the auto-generation once. If the cipher
                    // object were used again, a call to GenerateIV would have been
                    // required.
                    iv = cipher.IV;

                    cipherText = Transform(encryptor, message, 0, message.Length);
                }

                // The IV and ciphertext both need to be included in the MAC to prevent
                // tampering.
                //
                // By including the algorithm identifiers, we have technically moved from
                // simple Authenticated Encryption (AE) to Authenticated Encryption with
                // Additional Data (AEAD). By including the algorithm identifiers in the
                // MAC, it becomes harder for an attacker to change them as an attempt to
                // perform a downgrade attack.
                //
                // If you've added a data format version field, it can also be included
                // in the MAC to further inhibit an attacker's options for confusing the
                // data processor into believing the tampered message is valid.
                tagGenerator.AppendData(algorithmChoices);
                tagGenerator.AppendData(iv);
                tagGenerator.AppendData(cipherText);
                tag = tagGenerator.GetHashAndReset();
            }

            // Build the final result as the concatenation of everything except the keys.
            int totalLength =
                algorithmChoices.Length +
                tag.Length +
                iv.Length +
                cipherText.Length;

            byte[] output = new byte[totalLength];
            int outputOffset = 0;

            Append(algorithmChoices, output, ref outputOffset);
            Append(tag, output, ref outputOffset);
            Append(iv, output, ref outputOffset);
            Append(cipherText, output, ref outputOffset);

            Debug.Assert(outputOffset == output.Length);
            return output;
        }

        /// <summary>
        /// Reads a message produced by <see cref="Encrypt"/> after verifying it hasn't
        /// been tampered with.
        /// </summary>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <param name="cipherText">
        /// The output of <see cref="Encrypt"/>: a concatenation of a cipher ID, mac ID,
        /// authTag, IV, and cipherText.
        /// </param>
        /// <returns>The decrypted content.</returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="masterKey"/> is <c>null</c>.
        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="cipherText"/> is <c>null</c>.
        /// </exception>
        /// <exception cref="CryptographicException">
        /// <paramref name="cipherText"/> identifies unknown algorithms, is not long
        /// enough, fails a data integrity check, or fails to decrypt.
        /// </exception>
        /// <remarks>
        /// <paramref name="masterKey"/> should be a 128-bit (or larger) value
        /// generated by a secure random number generator, such as the one returned from
        /// <see cref="RandomNumberGenerator.Create()"/>. This implementation chooses to
        /// block deficient inputs by length, but doesn't make any attempt at
        /// discerning the randomness of the key.
        ///
        /// If the master key is being input by a prompt (like a password/passphrase),
        /// then it should be properly turned into keying material via a Key Derivation
        /// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
        /// never be simply turned to bytes via an Encoding class and used as a key.
        /// </remarks>
        public static byte[] Decrypt(byte[] masterKey, byte[] cipherText)
        {
            // This example continues the .NET practice of throwing exceptions for
            // failures. If you consider message tampering to be normal (and thus
            // "not exceptional") behavior, you may like the signature
            // bool Decrypt(byte[] messageKey, byte[] cipherText, out byte[] message)
            // better.
            if (masterKey == null)
                throw new ArgumentNullException(nameof(masterKey));
            if (masterKey.Length < 16)
                throw new ArgumentOutOfRangeException(
                    nameof(masterKey),
                    "Master Key must be at least 128 bits (16 bytes)");
            if (cipherText == null)
                throw new ArgumentNullException(nameof(cipherText));

            // The format of this message is assumed to be public, so there's no harm in
            // saying ahead of time that the message makes no sense.
            if (cipherText.Length < 2)
            {
                throw new CryptographicException();
            }

            // Use the message algorithm headers to determine what cipher algorithm and
            // MAC algorithm are going to be used. Since the same Key Derivation
            // Functions (KDFs) are being used in Decrypt as Encrypt, the keys are also
            // the same.
            AeCipher aeCipher = (AeCipher)cipherText[0];
            AeMac aeMac = (AeMac)cipherText[1];

            using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
            using (HMAC tagGenerator = GetMac(aeMac, masterKey))
            {
                int blockSizeInBytes = cipher.BlockSize / 8;
                int tagSizeInBytes = tagGenerator.HashSize / 8;
                int headerSizeInBytes = 2;
                int tagOffset = headerSizeInBytes;
                int ivOffset = tagOffset + tagSizeInBytes;
                int cipherTextOffset = ivOffset + blockSizeInBytes;
                int cipherTextLength = cipherText.Length - cipherTextOffset;
                int minLen = cipherTextOffset + blockSizeInBytes;

                // Again, the minimum length is still assumed to be public knowledge,
                // nothing has leaked out yet. The minimum length couldn't just be calculated
                // without reading the header.
                if (cipherText.Length < minLen)
                {
                    throw new CryptographicException();
                }

                // It's very important that the MAC be calculated and verified before
                // proceeding to decrypt the ciphertext, as this prevents any sort of
                // information leaking out to an attacker.
                //
                // Don't include the tag in the calculation, though.

                // First, everything before the tag (the cipher and MAC algorithm ids)
                tagGenerator.AppendData(cipherText, 0, tagOffset);

                // Skip the data before the tag and the tag, then read everything that
                // remains.
                tagGenerator.AppendData(
                    cipherText,
                    tagOffset + tagSizeInBytes,
                    cipherText.Length - tagSizeInBytes - tagOffset);

                byte[] generatedTag = tagGenerator.GetHashAndReset();

                // The time it took to get to this point has so far been a function only
                // of the length of the data, or of non-encrypted values (e.g. it could
                // take longer to prepare the *key* for the HMACSHA384 MAC than the
                // HMACSHA256 MAC, but the algorithm choice wasn't a secret).
                //
                // If the verification of the authentication tag aborts as soon as a
                // difference is found in the byte arrays then your program may be
                // acting as a timing oracle which helps an attacker to brute-force the
                // right answer for the MAC. So, it's very important that every possible
                // "no" (2^256-1 of them for HMACSHA256) be evaluated in as close to the
                // same amount of time as possible. For this, we call CryptographicEquals
                if (!CryptographicEquals(
                    generatedTag,
                    0,
                    cipherText,
                    tagOffset,
                    tagSizeInBytes))
                {
                    // Assuming every tampered message (of the same length) took the same
                    // amount of time to process, we can now safely say
                    // "this data makes no sense" without giving anything away.
                    throw new CryptographicException();
                }

                // Restore the IV into the symmetricAlgorithm instance.
                byte[] iv = new byte[blockSizeInBytes];
                Buffer.BlockCopy(cipherText, ivOffset, iv, 0, iv.Length);
                cipher.IV = iv;

                using (ICryptoTransform decryptor = cipher.CreateDecryptor())
                {
                    return Transform(
                        decryptor,
                        cipherText,
                        cipherTextOffset,
                        cipherTextLength);
                }
            }
        }

        private static byte[] Transform(
            ICryptoTransform transform,
            byte[] input,
            int inputOffset,
            int inputLength)
        {
            // Many of the implementations of ICryptoTransform report true for
            // CanTransformMultipleBlocks, and when the entire message is available in
            // one shot this saves on the allocation of the CryptoStream and the
            // intermediate structures it needs to properly chunk the message into blocks
            // (since the underlying stream won't always return the number of bytes
            // needed).
            if (transform.CanTransformMultipleBlocks)
            {
                return transform.TransformFinalBlock(input, inputOffset, inputLength);
            }

            // If our transform couldn't do multiple blocks at once, let CryptoStream
            // handle the chunking.
            using (MemoryStream messageStream = new MemoryStream())
            using (CryptoStream cryptoStream =
                new CryptoStream(messageStream, transform, CryptoStreamMode.Write))
            {
                cryptoStream.Write(input, inputOffset, inputLength);
                cryptoStream.FlushFinalBlock();
                return messageStream.ToArray();
            }
        }

        /// <summary>
        /// Open a properly configured <see cref="SymmetricAlgorithm"/> conforming to the
        /// scheme identified by <paramref name="aeCipher"/>.
        /// </summary>
        /// <param name="aeCipher">The cipher mode to open.</param>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <returns>
        /// A SymmetricAlgorithm object with the right key, cipher mode, and padding
        /// mode; or <c>null</c> on unknown algorithms.
        /// </returns>
        private static SymmetricAlgorithm GetCipher(AeCipher aeCipher, byte[] masterKey)
        {
            SymmetricAlgorithm symmetricAlgorithm;

            switch (aeCipher)
            {
                case AeCipher.Aes256CbcPkcs7:
                    symmetricAlgorithm = Aes.Create();
                    // While 256-bit, CBC, and PKCS7 are all the default values for these
                    // properties, being explicit helps comprehension more than it hurts
                    // performance.
                    symmetricAlgorithm.KeySize = 256;
                    symmetricAlgorithm.Mode = CipherMode.CBC;
                    symmetricAlgorithm.Padding = PaddingMode.PKCS7;
                    break;
                default:
                    // An algorithm we don't understand
                    throw new CryptographicException();
            }

            // Instead of using the master key directly, derive a key for our chosen
            // HMAC algorithm based upon the master key.
            //
            // Since none of the symmetric encryption algorithms currently in .NET
            // support key sizes greater than 256-bit, we can use HMACSHA256 with
            // NIST SP 800-108 5.1 (Counter Mode KDF) to derive a value that is
            // no smaller than the key size, then Array.Resize to trim it down as
            // needed.

            using (HMAC hmac = new HMACSHA256(masterKey))
            {
                // i=1, Label=ASCII(cipher)
                byte[] cipherKey = hmac.ComputeHash(
                    new byte[] { 1, 99, 105, 112, 104, 101, 114 });

                // Resize the array to the desired keysize. KeySize is in bits,
                // and Array.Resize wants the length in bytes.
                Array.Resize(ref cipherKey, symmetricAlgorithm.KeySize / 8);

                symmetricAlgorithm.Key = cipherKey;
            }

            return symmetricAlgorithm;
        }

        /// <summary>
        /// Open a properly configured <see cref="HMAC"/> conforming to the scheme
        /// identified by <paramref name="aeMac"/>.
        /// </summary>
        /// <param name="aeMac">The message authentication mode to open.</param>
        /// <param name="masterKey">The master key from which other keys derive.</param>
        /// <returns>
        /// An HMAC object with the proper key, or <c>null</c> on unknown algorithms.
        /// </returns>
        private static HMAC GetMac(AeMac aeMac, byte[] masterKey)
        {
            HMAC hmac;

            switch (aeMac)
            {
                case AeMac.HMACSHA256:
                    hmac = new HMACSHA256();
                    break;
                case AeMac.HMACSHA384:
                    hmac = new HMACSHA384();
                    break;
                default:
                    // An algorithm we don't understand
                    throw new CryptographicException();
            }

            // Instead of using the master key directly, derive a key for our chosen
            // HMAC algorithm based upon the master key.
            // Since the output size of the HMAC is the same as the ideal key size for
            // the HMAC, we can use the master key over a fixed input once to perform
            // NIST SP 800-108 5.1 (Counter Mode KDF):
            hmac.Key = masterKey;

            // i=1, Context=ASCII(MAC)
            byte[] newKey = hmac.ComputeHash(new byte[] { 1, 77, 65, 67 });

            hmac.Key = newKey;
            return hmac;
        }

        // A simple helper method to ensure that the offset (writePos) always moves
        // forward with new data.
        private static void Append(byte[] newData, byte[] combinedData, ref int writePos)
        {
            Buffer.BlockCopy(newData, 0, combinedData, writePos, newData.Length);
            writePos += newData.Length;
        }

        /// <summary>
        /// Compare the contents of two arrays in an amount of time which is only
        /// dependent on <paramref name="length"/>.
        /// </summary>
        /// <param name="a">An array to compare to <paramref name="b"/>.</param>
        /// <param name="aOffset">
        /// The starting position within <paramref name="a"/> for comparison.
        /// </param>
        /// <param name="b">An array to compare to <paramref name="a"/>.</param>
        /// <param name="bOffset">
        /// The starting position within <paramref name="b"/> for comparison.
        /// </param>
        /// <param name="length">
        /// The number of bytes to compare between <paramref name="a"/> and
        /// <paramref name="b"/>.</param>
        /// <returns>
        /// <c>true</c> if both <paramref name="a"/> and <paramref name="b"/> have
        /// sufficient length for the comparison and all of the applicable values are the
        /// same in both arrays; <c>false</c> otherwise.
        /// </returns>
        /// <remarks>
        /// An "insufficient data" <c>false</c> response can happen early, but otherwise
        /// a <c>true</c> or <c>false</c> response take the same amount of time.
        /// </remarks>
        [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
        private static bool CryptographicEquals(
            byte[] a,
            int aOffset,
            byte[] b,
            int bOffset,
            int length)
        {
            Debug.Assert(a != null);
            Debug.Assert(b != null);
            Debug.Assert(length >= 0);

            int result = 0;

            if (a.Length - aOffset < length || b.Length - bOffset < length)
            {
                return false;
            }

            unchecked
            {
                for (int i = 0; i < length; i++)
                {
                    // Bitwise-OR of subtraction has been found to have the most
                    // stable execution time.
                    //
                    // This cannot overflow because bytes are 1 byte in length, and
                    // result is 4 bytes.
                    // The OR propagates all set bytes, so the differences are only
                    // present in the lowest byte.
                    result = result | (a[i + aOffset] - b[i + bOffset]);
                }
            }

            return result == 0;
        }
    }
}

참고 항목